pbc3: promote simplified codec
Browse files- PBC3.py +124 -184
- learned_filler.py +21 -43
- pbc3_features.py +1 -1
- pbc3_heads.py +1 -25
- pbc3_ops.py +30 -50
- pbc3_stream.py +109 -0
- pbc3_types.py +1 -1
PBC3.py
CHANGED
|
@@ -1,10 +1,3 @@
|
|
| 1 |
-
# ====================================================================================================
|
| 2 |
-
#
|
| 3 |
-
# PBC v3.0 - Probabilistic Brush Compression
|
| 4 |
-
# Lossy Image Compression Algorithm by EgeEken (github.com/EgeEken)
|
| 5 |
-
# 3.0 Update - 2026-06 - Whole algorithm overhaul
|
| 6 |
-
#
|
| 7 |
-
# ====================================================================================================
|
| 8 |
import lzma
|
| 9 |
import math
|
| 10 |
import time
|
|
@@ -12,11 +5,43 @@ import time
|
|
| 12 |
import numpy as np
|
| 13 |
from PIL import Image, ImageOps
|
| 14 |
|
|
|
|
| 15 |
import pbc3_ops as ops
|
| 16 |
from pbc3_heads import DownsampleInitHead, FillerHead, SearchHead
|
| 17 |
from pbc3_types import BitReader, BitWriter, PBC3Config, PBC3Result
|
| 18 |
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
class PBC3:
|
| 21 |
MAGIC = b"PBC3"
|
| 22 |
VERSION = 0
|
|
@@ -30,8 +55,6 @@ class PBC3:
|
|
| 30 |
RESAMPLE_FILTER = ops.RESAMPLE_FILTER
|
| 31 |
RESAMPLE_REDUCING_GAP = ops.RESAMPLE_REDUCING_GAP
|
| 32 |
|
| 33 |
-
# ---- image conversion -------------------------------------------------
|
| 34 |
-
|
| 35 |
@staticmethod
|
| 36 |
def _to_image(image) -> Image.Image:
|
| 37 |
"""## Returns a PIL image from a path, PIL image, or image-like array"""
|
|
@@ -60,8 +83,6 @@ class PBC3:
|
|
| 60 |
return color
|
| 61 |
return Image.fromarray(arr, color_space).convert("RGB")
|
| 62 |
|
| 63 |
-
# ---- entropy + bitstream framing -------------------------------------
|
| 64 |
-
|
| 65 |
@classmethod
|
| 66 |
def _entropy_pack(cls, body: bytes, use_lzma: bool = True) -> tuple[int, bytes]:
|
| 67 |
"""## Returns the smaller of raw body or LZMA-compressed body"""
|
|
@@ -91,131 +112,18 @@ class PBC3:
|
|
| 91 |
raise ValueError(f"unsupported PBC3 version {version}")
|
| 92 |
return version, cls._entropy_unpack(data[5], data[6:])
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
bw.write(int(value), bitcount)
|
| 99 |
-
|
| 100 |
-
@classmethod
|
| 101 |
-
def _read_grid(cls, br: BitReader, n: int, bitcount: int) -> np.ndarray:
|
| 102 |
-
"""## Reads a flat grid of palette indices"""
|
| 103 |
-
flat = np.zeros(n, dtype=np.uint16)
|
| 104 |
-
for k in range(n):
|
| 105 |
-
flat[k] = br.read(bitcount)
|
| 106 |
-
return flat
|
| 107 |
-
|
| 108 |
-
@classmethod
|
| 109 |
-
def _write_header(
|
| 110 |
-
cls,
|
| 111 |
-
bw: BitWriter,
|
| 112 |
-
w: int,
|
| 113 |
-
h: int,
|
| 114 |
-
original_w: int,
|
| 115 |
-
original_h: int,
|
| 116 |
-
downsampled: bool,
|
| 117 |
-
color_id: int,
|
| 118 |
-
channels: int,
|
| 119 |
-
channel_bits: int,
|
| 120 |
-
positive_bias: bool,
|
| 121 |
-
has_alpha: bool,
|
| 122 |
-
patch_count: int,
|
| 123 |
-
base_values,
|
| 124 |
-
warmup=None,
|
| 125 |
-
) -> None:
|
| 126 |
-
"""## Writes the image-level stream header"""
|
| 127 |
-
bw.write(int(downsampled), 1)
|
| 128 |
-
if downsampled:
|
| 129 |
-
bw.write(original_w, 16)
|
| 130 |
-
bw.write(original_h, 16)
|
| 131 |
-
bw.write(w, 16)
|
| 132 |
-
bw.write(h, 16)
|
| 133 |
-
bw.write(color_id, 2)
|
| 134 |
-
bw.write(channels, 8)
|
| 135 |
-
bw.write(channel_bits, 4)
|
| 136 |
-
bw.write(int(positive_bias), 1)
|
| 137 |
-
bw.write(int(has_alpha), 1)
|
| 138 |
-
bw.write(patch_count, 32)
|
| 139 |
-
for base in base_values:
|
| 140 |
-
bw.write(base, 8)
|
| 141 |
-
bw.write(int(warmup is not None), 1)
|
| 142 |
-
if warmup is not None:
|
| 143 |
-
wm_w, wm_h, wm_split = warmup
|
| 144 |
-
bw.write(wm_w, 16)
|
| 145 |
-
bw.write(wm_h, 16)
|
| 146 |
-
bw.write(wm_split, 32)
|
| 147 |
-
|
| 148 |
-
@classmethod
|
| 149 |
-
def _read_header(cls, br: BitReader):
|
| 150 |
-
"""## Reads the image-level stream header"""
|
| 151 |
-
downsampled = bool(br.read(1))
|
| 152 |
-
original_w = br.read(16) if downsampled else None
|
| 153 |
-
original_h = br.read(16) if downsampled else None
|
| 154 |
-
w = br.read(16)
|
| 155 |
-
h = br.read(16)
|
| 156 |
-
color_id = br.read(2)
|
| 157 |
-
channels = br.read(8)
|
| 158 |
-
channel_bits = br.read(4)
|
| 159 |
-
positive_bias = bool(br.read(1))
|
| 160 |
-
has_alpha = bool(br.read(1))
|
| 161 |
-
patch_count = br.read(32)
|
| 162 |
-
base_values = [br.read(8) for _ in range(channels)]
|
| 163 |
-
warmup_on = bool(br.read(1))
|
| 164 |
-
warm_w = warm_h = warmup_split = None
|
| 165 |
-
if warmup_on:
|
| 166 |
-
warm_w = br.read(16)
|
| 167 |
-
warm_h = br.read(16)
|
| 168 |
-
warmup_split = br.read(32)
|
| 169 |
-
return (
|
| 170 |
-
downsampled, original_w, original_h, w, h, cls.COLOR_SPACE_NAMES[color_id], channels,
|
| 171 |
-
channel_bits, positive_bias, has_alpha, patch_count, base_values, warmup_on, warm_w,
|
| 172 |
-
warm_h, warmup_split,
|
| 173 |
-
)
|
| 174 |
|
| 175 |
@classmethod
|
| 176 |
-
def
|
| 177 |
-
|
| 178 |
-
bw.write(patch["channel"], channel_bits)
|
| 179 |
-
bw.write(patch["x"], 16)
|
| 180 |
-
bw.write(patch["y"], 16)
|
| 181 |
-
bw.write(patch["w"], 16)
|
| 182 |
-
bw.write(patch["h"], 16)
|
| 183 |
-
bw.write(cls.PALETTE_GENERATED, 1)
|
| 184 |
-
mask = patch["mask"]
|
| 185 |
-
bw.write(len(mask), 10)
|
| 186 |
-
for bit in mask:
|
| 187 |
-
bw.write(bit, 1)
|
| 188 |
-
bw.write(patch["neg"], 8)
|
| 189 |
-
bw.write(patch["pos"], 8)
|
| 190 |
-
bw.write(patch["max_bitcount"], 4)
|
| 191 |
-
bw.write(patch["cell_size"], 16)
|
| 192 |
-
cls._write_grid(bw, patch["indices"].ravel().astype(np.int64), patch["bitcount"])
|
| 193 |
|
| 194 |
-
@
|
| 195 |
-
def _read_patch(
|
| 196 |
-
|
| 197 |
-
channel = br.read(channel_bits)
|
| 198 |
-
x = br.read(16)
|
| 199 |
-
y = br.read(16)
|
| 200 |
-
w = br.read(16)
|
| 201 |
-
h = br.read(16)
|
| 202 |
-
pm = br.read(1)
|
| 203 |
-
if pm != cls.PALETTE_GENERATED:
|
| 204 |
-
raise ValueError("explicit palette patches were removed in PBC3 3.0 release cleanup")
|
| 205 |
-
mask_size = br.read(10)
|
| 206 |
-
mask = [br.read(1) for _ in range(mask_size)]
|
| 207 |
-
negative_max = br.read(8)
|
| 208 |
-
positive_max = br.read(8)
|
| 209 |
-
max_bitcount = br.read(4)
|
| 210 |
-
bitcount = ops.resolve_palette_bitcount(mask, max_bitcount, negative_max, positive_max, positive_bias)
|
| 211 |
-
pal = ops.palette_generator(mask, max_bitcount, negative_max, positive_max, positive_bias)
|
| 212 |
-
cell_size = br.read(16)
|
| 213 |
-
gw = ops.ceil_div(w, cell_size)
|
| 214 |
-
gh = ops.ceil_div(h, cell_size)
|
| 215 |
-
indices = cls._read_grid(br, gh * gw, bitcount).reshape(gh, gw)
|
| 216 |
-
return channel, x, y, w, h, cell_size, pal[indices], bitcount
|
| 217 |
-
|
| 218 |
-
# ---- image preparation -----------------------------------------------
|
| 219 |
|
| 220 |
@classmethod
|
| 221 |
def _auto_downsample_rate(cls, image_size, downsample_rate: float, max_pixels: int) -> float:
|
|
@@ -268,10 +176,7 @@ class PBC3:
|
|
| 268 |
@classmethod
|
| 269 |
def prepare(cls, image, config: PBC3Config = None, **kwargs) -> dict:
|
| 270 |
"""## Prepares the source image and reusable encoder arrays"""
|
| 271 |
-
|
| 272 |
-
config = PBC3Config(**kwargs)
|
| 273 |
-
elif kwargs:
|
| 274 |
-
config = PBC3Config(**{**config.__dict__, **kwargs})
|
| 275 |
|
| 276 |
src = cls._to_image(image)
|
| 277 |
has_alpha = cls._has_alpha(src)
|
|
@@ -286,12 +191,17 @@ class PBC3:
|
|
| 286 |
orig_compare = src.convert("RGB")
|
| 287 |
|
| 288 |
original_w, original_h = color_img.size
|
| 289 |
-
rate = cls._auto_downsample_rate(
|
|
|
|
|
|
|
| 290 |
color_ds = cls._downsample_image(color_img, rate)
|
| 291 |
downsampled = color_ds.size != color_img.size
|
| 292 |
arr = np.asarray(color_ds, dtype=np.uint8)
|
| 293 |
if has_alpha:
|
| 294 |
-
alpha_ds =
|
|
|
|
|
|
|
|
|
|
| 295 |
arr = np.dstack([arr, np.asarray(alpha_ds, dtype=np.uint8)])
|
| 296 |
|
| 297 |
warm_plan = cls._warmup_plan(config, color_img.size, rate)
|
|
@@ -302,7 +212,9 @@ class PBC3:
|
|
| 302 |
warm_w, warm_h = warm_color_ds.size
|
| 303 |
warm_arr = np.asarray(warm_color_ds, dtype=np.uint8)
|
| 304 |
if has_alpha:
|
| 305 |
-
warm_alpha = alpha_img.resize(
|
|
|
|
|
|
|
| 306 |
warm_arr = np.dstack([warm_arr, np.asarray(warm_alpha, dtype=np.uint8)])
|
| 307 |
warm_target = warm_arr.astype(np.int32)
|
| 308 |
|
|
@@ -327,12 +239,14 @@ class PBC3:
|
|
| 327 |
"warm_target": warm_target,
|
| 328 |
}
|
| 329 |
|
| 330 |
-
# ---- encode -----------------------------------------------------------
|
| 331 |
-
|
| 332 |
@staticmethod
|
| 333 |
def _choose_channel(scores, step: int, channels: int, mode: str) -> int:
|
| 334 |
"""## Chooses the next channel by round-robin or current total error"""
|
| 335 |
-
return (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
|
| 337 |
@staticmethod
|
| 338 |
def _channel_sum_error(target, canvas, c: int) -> float:
|
|
@@ -351,10 +265,7 @@ class PBC3:
|
|
| 351 |
@classmethod
|
| 352 |
def compress_stream(cls, image, config: PBC3Config = None, *, reuse=None, frame_every: int = 25, **kwargs):
|
| 353 |
"""## Compresses an image and yields optional preview frames plus the final result"""
|
| 354 |
-
|
| 355 |
-
config = PBC3Config(**kwargs)
|
| 356 |
-
elif kwargs:
|
| 357 |
-
config = PBC3Config(**{**config.__dict__, **kwargs})
|
| 358 |
|
| 359 |
t0 = time.perf_counter()
|
| 360 |
debug_lines = []
|
|
@@ -370,37 +281,37 @@ class PBC3:
|
|
| 370 |
did_warmup = False
|
| 371 |
warmup_split = None
|
| 372 |
|
| 373 |
-
|
| 374 |
-
raise ValueError("this prototype stores dimensions as uint16")
|
| 375 |
-
if config.mask_size < 1 or config.mask_size > 1023:
|
| 376 |
-
raise ValueError("mask_size must be in 1..1023")
|
| 377 |
-
if config.auto_downsample_max_pixels < 1:
|
| 378 |
-
raise ValueError("auto_downsample_max_pixels must be >= 1")
|
| 379 |
-
if not (1 <= config.downsample_palette_bitcount <= 9 and 1 <= config.patch_palette_bitcount <= 9):
|
| 380 |
-
raise ValueError("palette bitcounts must be in 1..9")
|
| 381 |
-
if str(config.channel_cycle).lower() not in {"sum", "mod"}:
|
| 382 |
-
raise ValueError('channel_cycle must be "Sum" or "Mod"')
|
| 383 |
|
| 384 |
channel_bits = max(1, math.ceil(math.log2(channels)))
|
| 385 |
base_values = [int(round(float(np.mean(arr[:, :, c])))) for c in range(channels)]
|
| 386 |
-
canvas =
|
| 387 |
-
for c, base in enumerate(base_values):
|
| 388 |
-
canvas[:, :, c] = base
|
| 389 |
if frame_every:
|
| 390 |
-
yield {
|
|
|
|
|
|
|
|
|
|
| 391 |
|
| 392 |
patches = []
|
| 393 |
init_head = DownsampleInitHead()
|
| 394 |
for c in range(channels):
|
| 395 |
-
patch, values, init_cell, init_bits = init_head.select(
|
|
|
|
|
|
|
| 396 |
if config.debug_print:
|
| 397 |
print(f"[auto-init] channel {c}: cell={init_cell}, bitcount={init_bits}")
|
| 398 |
ops.apply_grid(canvas[:, :, c], 0, 0, w, h, init_cell, values)
|
| 399 |
patches.append(patch)
|
| 400 |
if config.debug_mode:
|
| 401 |
-
debug_lines.append(ops.debug_line(
|
|
|
|
|
|
|
|
|
|
| 402 |
if frame_every:
|
| 403 |
-
yield {
|
|
|
|
|
|
|
|
|
|
| 404 |
|
| 405 |
channel_scores = [cls._channel_sum_error(target, canvas, c) for c in range(channels)]
|
| 406 |
quality_target = float(config.quality_target_mae)
|
|
@@ -410,13 +321,22 @@ class PBC3:
|
|
| 410 |
applied = 0
|
| 411 |
for step in range(1, max(0, int(config.patch_count)) + 1):
|
| 412 |
current_channel = cls._choose_channel(channel_scores, step, channels, config.channel_cycle)
|
| 413 |
-
boxes =
|
| 414 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
if patch is None:
|
| 416 |
break
|
| 417 |
|
| 418 |
c = patch["channel"]
|
| 419 |
-
ops.apply_grid(
|
|
|
|
|
|
|
|
|
|
| 420 |
patches.append(patch)
|
| 421 |
channel_scores[c] = cls._channel_sum_error(target, canvas, c)
|
| 422 |
applied += 1
|
|
@@ -428,12 +348,21 @@ class PBC3:
|
|
| 428 |
warmup_split = len(patches)
|
| 429 |
did_warmup = True
|
| 430 |
if config.debug_mode:
|
| 431 |
-
debug_lines.append(ops.debug_line(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
if config.debug_print:
|
| 433 |
print("|", end="", flush=True)
|
| 434 |
if frame_every and applied % frame_every == 0:
|
| 435 |
-
yield {
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
break
|
| 438 |
if config.debug_print:
|
| 439 |
print()
|
|
@@ -473,8 +402,6 @@ class PBC3:
|
|
| 473 |
),
|
| 474 |
}
|
| 475 |
|
| 476 |
-
# ---- decode (deterministic, ML-free) ---------------------------------
|
| 477 |
-
|
| 478 |
@classmethod
|
| 479 |
def _decode_to_canvas(cls, data, max_patches: int = None):
|
| 480 |
"""## Decodes a PBC3 stream to the internal canvas without making a PIL image"""
|
|
@@ -484,17 +411,24 @@ class PBC3:
|
|
| 484 |
version, body = cls._open_body(data)
|
| 485 |
br = BitReader(body)
|
| 486 |
header = cls._read_header(br)
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
|
|
|
|
|
|
| 491 |
patches_to_read = patch_count if max_patches is None else min(int(max_patches), patch_count)
|
| 492 |
for idx in range(patches_to_read):
|
| 493 |
if warmup_on and idx == warmup_split:
|
| 494 |
canvas = cls._resize_canvas(canvas, warm_w, warm_h)
|
| 495 |
-
channel, x, y, pw, ph, cell_size, values, _ = cls._read_patch(
|
|
|
|
|
|
|
| 496 |
ops.apply_grid(canvas[:, :, channel], x, y, pw, ph, cell_size, values)
|
| 497 |
-
return
|
|
|
|
|
|
|
|
|
|
| 498 |
|
| 499 |
@classmethod
|
| 500 |
def decompress(cls, data, max_patches: int = None) -> PBC3Result:
|
|
@@ -503,17 +437,23 @@ class PBC3:
|
|
| 503 |
if isinstance(data, str):
|
| 504 |
with open(data, "rb") as f:
|
| 505 |
data = f.read()
|
| 506 |
-
|
|
|
|
|
|
|
|
|
|
| 507 |
img = cls._canvas_to_image(canvas, color_space, has_alpha)
|
| 508 |
if downsampled and img.size != (original_w, original_h):
|
| 509 |
img = img.resize((original_w, original_h), cls.RESAMPLE_FILTER, reducing_gap=cls.RESAMPLE_REDUCING_GAP)
|
| 510 |
cfg = PBC3Config(color_space=color_space)
|
| 511 |
-
return PBC3Result(
|
| 512 |
-
|
| 513 |
-
|
|
|
|
| 514 |
|
| 515 |
@classmethod
|
| 516 |
-
def encode_file(
|
|
|
|
|
|
|
| 517 |
"""## Compresses a file and writes the .pbc3 output"""
|
| 518 |
result = cls.compress(Image.open(input_path), config=config, **kwargs)
|
| 519 |
with open(output_path, "wb") as f:
|
|
@@ -545,4 +485,4 @@ if __name__ == "__main__":
|
|
| 545 |
else:
|
| 546 |
preload_numba()
|
| 547 |
res = PBC3.encode_file(sys.argv[1], sys.argv[2])
|
| 548 |
-
print(f"MSE: {res.mse:.2f} | Size: {len(res.data) / 1024:.2f} KB | Rate: {res.compression_rate:.2f}x | Time: {res.encode_seconds:.3f}s")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import lzma
|
| 2 |
import math
|
| 3 |
import time
|
|
|
|
| 5 |
import numpy as np
|
| 6 |
from PIL import Image, ImageOps
|
| 7 |
|
| 8 |
+
import pbc3_stream as stream
|
| 9 |
import pbc3_ops as ops
|
| 10 |
from pbc3_heads import DownsampleInitHead, FillerHead, SearchHead
|
| 11 |
from pbc3_types import BitReader, BitWriter, PBC3Config, PBC3Result
|
| 12 |
|
| 13 |
|
| 14 |
+
def _config(config, kwargs):
|
| 15 |
+
if config is None:
|
| 16 |
+
return PBC3Config(**kwargs)
|
| 17 |
+
return PBC3Config(**{**config.__dict__, **kwargs}) if kwargs else config
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _canvas_from_bases(shape, bases):
|
| 21 |
+
canvas = np.zeros(shape, dtype=np.int32)
|
| 22 |
+
for channel, base in enumerate(bases):
|
| 23 |
+
canvas[:, :, channel] = base
|
| 24 |
+
return canvas
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _validate(prep, config):
|
| 28 |
+
h, w = prep["h"], prep["w"]
|
| 29 |
+
ow, oh = prep["original_w"], prep["original_h"]
|
| 30 |
+
if max(w, h, ow, oh) > 65535:
|
| 31 |
+
raise ValueError("this prototype stores dimensions as uint16")
|
| 32 |
+
if not 1 <= config.mask_size <= 1023:
|
| 33 |
+
raise ValueError("mask_size must be in 1..1023")
|
| 34 |
+
if config.auto_downsample_max_pixels < 1:
|
| 35 |
+
raise ValueError("auto_downsample_max_pixels must be >= 1")
|
| 36 |
+
if not (
|
| 37 |
+
1 <= config.downsample_palette_bitcount <= 9
|
| 38 |
+
and 1 <= config.patch_palette_bitcount <= 9
|
| 39 |
+
):
|
| 40 |
+
raise ValueError("palette bitcounts must be in 1..9")
|
| 41 |
+
if str(config.channel_cycle).lower() not in {"sum", "mod"}:
|
| 42 |
+
raise ValueError('channel_cycle must be "Sum" or "Mod"')
|
| 43 |
+
|
| 44 |
+
|
| 45 |
class PBC3:
|
| 46 |
MAGIC = b"PBC3"
|
| 47 |
VERSION = 0
|
|
|
|
| 55 |
RESAMPLE_FILTER = ops.RESAMPLE_FILTER
|
| 56 |
RESAMPLE_REDUCING_GAP = ops.RESAMPLE_REDUCING_GAP
|
| 57 |
|
|
|
|
|
|
|
| 58 |
@staticmethod
|
| 59 |
def _to_image(image) -> Image.Image:
|
| 60 |
"""## Returns a PIL image from a path, PIL image, or image-like array"""
|
|
|
|
| 83 |
return color
|
| 84 |
return Image.fromarray(arr, color_space).convert("RGB")
|
| 85 |
|
|
|
|
|
|
|
| 86 |
@classmethod
|
| 87 |
def _entropy_pack(cls, body: bytes, use_lzma: bool = True) -> tuple[int, bytes]:
|
| 88 |
"""## Returns the smaller of raw body or LZMA-compressed body"""
|
|
|
|
| 112 |
raise ValueError(f"unsupported PBC3 version {version}")
|
| 113 |
return version, cls._entropy_unpack(data[5], data[6:])
|
| 114 |
|
| 115 |
+
_write_grid = staticmethod(stream.write_grid)
|
| 116 |
+
_read_grid = staticmethod(stream.read_grid)
|
| 117 |
+
_write_header = staticmethod(stream.write_header)
|
| 118 |
+
_write_patch = staticmethod(stream.write_patch)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
@classmethod
|
| 121 |
+
def _read_header(cls, br):
|
| 122 |
+
return stream.read_header(br, cls.COLOR_SPACE_NAMES)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
+
@staticmethod
|
| 125 |
+
def _read_patch(br, channel_bits: int, positive_bias: bool = True):
|
| 126 |
+
return stream.read_patch(br, channel_bits, positive_bias)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
@classmethod
|
| 129 |
def _auto_downsample_rate(cls, image_size, downsample_rate: float, max_pixels: int) -> float:
|
|
|
|
| 176 |
@classmethod
|
| 177 |
def prepare(cls, image, config: PBC3Config = None, **kwargs) -> dict:
|
| 178 |
"""## Prepares the source image and reusable encoder arrays"""
|
| 179 |
+
config = _config(config, kwargs)
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
src = cls._to_image(image)
|
| 182 |
has_alpha = cls._has_alpha(src)
|
|
|
|
| 191 |
orig_compare = src.convert("RGB")
|
| 192 |
|
| 193 |
original_w, original_h = color_img.size
|
| 194 |
+
rate = cls._auto_downsample_rate(
|
| 195 |
+
color_img.size, config.downsample_rate, config.auto_downsample_max_pixels
|
| 196 |
+
)
|
| 197 |
color_ds = cls._downsample_image(color_img, rate)
|
| 198 |
downsampled = color_ds.size != color_img.size
|
| 199 |
arr = np.asarray(color_ds, dtype=np.uint8)
|
| 200 |
if has_alpha:
|
| 201 |
+
alpha_ds = (
|
| 202 |
+
alpha_img.resize(color_ds.size, cls.RESAMPLE_FILTER, reducing_gap=cls.RESAMPLE_REDUCING_GAP)
|
| 203 |
+
if downsampled else alpha_img
|
| 204 |
+
)
|
| 205 |
arr = np.dstack([arr, np.asarray(alpha_ds, dtype=np.uint8)])
|
| 206 |
|
| 207 |
warm_plan = cls._warmup_plan(config, color_img.size, rate)
|
|
|
|
| 212 |
warm_w, warm_h = warm_color_ds.size
|
| 213 |
warm_arr = np.asarray(warm_color_ds, dtype=np.uint8)
|
| 214 |
if has_alpha:
|
| 215 |
+
warm_alpha = alpha_img.resize(
|
| 216 |
+
warm_color_ds.size, cls.RESAMPLE_FILTER, reducing_gap=cls.RESAMPLE_REDUCING_GAP
|
| 217 |
+
)
|
| 218 |
warm_arr = np.dstack([warm_arr, np.asarray(warm_alpha, dtype=np.uint8)])
|
| 219 |
warm_target = warm_arr.astype(np.int32)
|
| 220 |
|
|
|
|
| 239 |
"warm_target": warm_target,
|
| 240 |
}
|
| 241 |
|
|
|
|
|
|
|
| 242 |
@staticmethod
|
| 243 |
def _choose_channel(scores, step: int, channels: int, mode: str) -> int:
|
| 244 |
"""## Chooses the next channel by round-robin or current total error"""
|
| 245 |
+
return (
|
| 246 |
+
(step - 1) % channels
|
| 247 |
+
if str(mode).lower() == "mod"
|
| 248 |
+
else int(max(range(channels), key=lambda c: scores[c]))
|
| 249 |
+
)
|
| 250 |
|
| 251 |
@staticmethod
|
| 252 |
def _channel_sum_error(target, canvas, c: int) -> float:
|
|
|
|
| 265 |
@classmethod
|
| 266 |
def compress_stream(cls, image, config: PBC3Config = None, *, reuse=None, frame_every: int = 25, **kwargs):
|
| 267 |
"""## Compresses an image and yields optional preview frames plus the final result"""
|
| 268 |
+
config = _config(config, kwargs)
|
|
|
|
|
|
|
|
|
|
| 269 |
|
| 270 |
t0 = time.perf_counter()
|
| 271 |
debug_lines = []
|
|
|
|
| 281 |
did_warmup = False
|
| 282 |
warmup_split = None
|
| 283 |
|
| 284 |
+
_validate(prep, config)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
channel_bits = max(1, math.ceil(math.log2(channels)))
|
| 287 |
base_values = [int(round(float(np.mean(arr[:, :, c])))) for c in range(channels)]
|
| 288 |
+
canvas = _canvas_from_bases((h, w, channels), base_values)
|
|
|
|
|
|
|
| 289 |
if frame_every:
|
| 290 |
+
yield {
|
| 291 |
+
"event": "frame", "step": 0, "total": int(config.patch_count),
|
| 292 |
+
"image": cls._canvas_to_image(canvas, config.color_space, has_alpha),
|
| 293 |
+
}
|
| 294 |
|
| 295 |
patches = []
|
| 296 |
init_head = DownsampleInitHead()
|
| 297 |
for c in range(channels):
|
| 298 |
+
patch, values, init_cell, init_bits = init_head.select(
|
| 299 |
+
c, target, canvas, w, h, config, channel_bits
|
| 300 |
+
)
|
| 301 |
if config.debug_print:
|
| 302 |
print(f"[auto-init] channel {c}: cell={init_cell}, bitcount={init_bits}")
|
| 303 |
ops.apply_grid(canvas[:, :, c], 0, 0, w, h, init_cell, values)
|
| 304 |
patches.append(patch)
|
| 305 |
if config.debug_mode:
|
| 306 |
+
debug_lines.append(ops.debug_line(
|
| 307 |
+
"INIT", stream_patch=len(patches), channel=c, x=0, y=0, w=w, h=h,
|
| 308 |
+
cell_size=init_cell, bitcount=init_bits,
|
| 309 |
+
))
|
| 310 |
if frame_every:
|
| 311 |
+
yield {
|
| 312 |
+
"event": "frame", "step": 0, "total": int(config.patch_count),
|
| 313 |
+
"image": cls._canvas_to_image(canvas, config.color_space, has_alpha),
|
| 314 |
+
}
|
| 315 |
|
| 316 |
channel_scores = [cls._channel_sum_error(target, canvas, c) for c in range(channels)]
|
| 317 |
quality_target = float(config.quality_target_mae)
|
|
|
|
| 321 |
applied = 0
|
| 322 |
for step in range(1, max(0, int(config.patch_count)) + 1):
|
| 323 |
current_channel = cls._choose_channel(channel_scores, step, channels, config.channel_cycle)
|
| 324 |
+
boxes = (
|
| 325 |
+
None if filler.learned is not None
|
| 326 |
+
else search.propose(target, canvas, config, rng, step, current_channel)
|
| 327 |
+
)
|
| 328 |
+
patch, values = filler.select(
|
| 329 |
+
target, canvas, config, rng, channel_bits, step, current_channel,
|
| 330 |
+
boxes, len(patches), debug_lines,
|
| 331 |
+
)
|
| 332 |
if patch is None:
|
| 333 |
break
|
| 334 |
|
| 335 |
c = patch["channel"]
|
| 336 |
+
ops.apply_grid(
|
| 337 |
+
canvas[:, :, c], patch["x"], patch["y"], patch["w"], patch["h"],
|
| 338 |
+
patch["cell_size"], values,
|
| 339 |
+
)
|
| 340 |
patches.append(patch)
|
| 341 |
channel_scores[c] = cls._channel_sum_error(target, canvas, c)
|
| 342 |
applied += 1
|
|
|
|
| 348 |
warmup_split = len(patches)
|
| 349 |
did_warmup = True
|
| 350 |
if config.debug_mode:
|
| 351 |
+
debug_lines.append(ops.debug_line(
|
| 352 |
+
"APPLIED", patch_step=step, stream_patch=len(patches), channel=c,
|
| 353 |
+
channel_score=f"{channel_scores[c]:.4f}", x=patch["x"], y=patch["y"],
|
| 354 |
+
w=patch["w"], h=patch["h"], cell_size=patch["cell_size"],
|
| 355 |
+
))
|
| 356 |
if config.debug_print:
|
| 357 |
print("|", end="", flush=True)
|
| 358 |
if frame_every and applied % frame_every == 0:
|
| 359 |
+
yield {
|
| 360 |
+
"event": "frame", "step": step, "total": int(config.patch_count),
|
| 361 |
+
"image": cls._canvas_to_image(canvas, config.color_space, has_alpha),
|
| 362 |
+
}
|
| 363 |
+
if quality_target > 0 and float(
|
| 364 |
+
np.mean(np.abs(target - np.clip(canvas, 0, 255)))
|
| 365 |
+
) <= quality_target:
|
| 366 |
break
|
| 367 |
if config.debug_print:
|
| 368 |
print()
|
|
|
|
| 402 |
),
|
| 403 |
}
|
| 404 |
|
|
|
|
|
|
|
| 405 |
@classmethod
|
| 406 |
def _decode_to_canvas(cls, data, max_patches: int = None):
|
| 407 |
"""## Decodes a PBC3 stream to the internal canvas without making a PIL image"""
|
|
|
|
| 411 |
version, body = cls._open_body(data)
|
| 412 |
br = BitReader(body)
|
| 413 |
header = cls._read_header(br)
|
| 414 |
+
(
|
| 415 |
+
downsampled, original_w, original_h, w, h, color_space, channels,
|
| 416 |
+
channel_bits, positive_bias, has_alpha, patch_count, base_values,
|
| 417 |
+
warmup_on, warm_w, warm_h, warmup_split,
|
| 418 |
+
) = header
|
| 419 |
+
canvas = _canvas_from_bases((h, w, channels), base_values)
|
| 420 |
patches_to_read = patch_count if max_patches is None else min(int(max_patches), patch_count)
|
| 421 |
for idx in range(patches_to_read):
|
| 422 |
if warmup_on and idx == warmup_split:
|
| 423 |
canvas = cls._resize_canvas(canvas, warm_w, warm_h)
|
| 424 |
+
channel, x, y, pw, ph, cell_size, values, _ = cls._read_patch(
|
| 425 |
+
br, channel_bits, positive_bias
|
| 426 |
+
)
|
| 427 |
ops.apply_grid(canvas[:, :, channel], x, y, pw, ph, cell_size, values)
|
| 428 |
+
return (
|
| 429 |
+
canvas, color_space, downsampled, original_w, original_h,
|
| 430 |
+
canvas.shape[1], canvas.shape[0], has_alpha, channels, patch_count,
|
| 431 |
+
)
|
| 432 |
|
| 433 |
@classmethod
|
| 434 |
def decompress(cls, data, max_patches: int = None) -> PBC3Result:
|
|
|
|
| 437 |
if isinstance(data, str):
|
| 438 |
with open(data, "rb") as f:
|
| 439 |
data = f.read()
|
| 440 |
+
(
|
| 441 |
+
canvas, color_space, downsampled, original_w, original_h, w, h,
|
| 442 |
+
has_alpha, channels, patch_count,
|
| 443 |
+
) = cls._decode_to_canvas(data, max_patches=max_patches)
|
| 444 |
img = cls._canvas_to_image(canvas, color_space, has_alpha)
|
| 445 |
if downsampled and img.size != (original_w, original_h):
|
| 446 |
img = img.resize((original_w, original_h), cls.RESAMPLE_FILTER, reducing_gap=cls.RESAMPLE_REDUCING_GAP)
|
| 447 |
cfg = PBC3Config(color_space=color_space)
|
| 448 |
+
return PBC3Result(
|
| 449 |
+
img, data, cfg, None, time.perf_counter() - t0, len(data) * 8,
|
| 450 |
+
original_w or w, original_h or h, w, h, channels=channels,
|
| 451 |
+
)
|
| 452 |
|
| 453 |
@classmethod
|
| 454 |
+
def encode_file(
|
| 455 |
+
cls, input_path: str, output_path: str, config: PBC3Config = None, **kwargs
|
| 456 |
+
) -> PBC3Result:
|
| 457 |
"""## Compresses a file and writes the .pbc3 output"""
|
| 458 |
result = cls.compress(Image.open(input_path), config=config, **kwargs)
|
| 459 |
with open(output_path, "wb") as f:
|
|
|
|
| 485 |
else:
|
| 486 |
preload_numba()
|
| 487 |
res = PBC3.encode_file(sys.argv[1], sys.argv[2])
|
| 488 |
+
print(f"MSE: {res.mse:.2f} | Size: {len(res.data) / 1024:.2f} KB | Rate: {res.compression_rate:.2f}x | Time: {res.encode_seconds:.3f}s")
|
learned_filler.py
CHANGED
|
@@ -47,27 +47,25 @@ def _score_patch(target, canvas, box, channel_bits: int, patch, values) -> tuple
|
|
| 47 |
return reduction, bits
|
| 48 |
|
| 49 |
|
| 50 |
-
def
|
| 51 |
-
"""## Builds and scores one legacy single-index action"""
|
| 52 |
c, x, y, bw, bh = box
|
| 53 |
-
|
| 54 |
-
cell = max(1, min(cs, bw, bh))
|
| 55 |
residual = target[y:y + bh, x:x + bw, c] - canvas[y:y + bh, x:x + bw, c]
|
| 56 |
-
patch, values = ops.make_patch(c, x, y, bw, bh, cell, residual, config,
|
| 57 |
reduction, bits = _score_patch(target, canvas, box, channel_bits, patch, values)
|
| 58 |
return patch, values, reduction, bits
|
| 59 |
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
def build_action_factored(target, canvas, box, config, channel_bits: int, cs_idx: int, bc_idx: int, ms_idx: int):
|
| 62 |
"""## Builds and scores one factored action: cell size, bitcount, and mask size"""
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
cell = max(1, min(cs, bw, bh))
|
| 66 |
-
cfg = dataclasses.replace(config, mask_size=ms)
|
| 67 |
-
residual = target[y:y + bh, x:x + bw, c] - canvas[y:y + bh, x:x + bw, c]
|
| 68 |
-
patch, values = ops.make_patch(c, x, y, bw, bh, cell, residual, cfg, bc)
|
| 69 |
-
reduction, bits = _score_patch(target, canvas, box, channel_bits, patch, values)
|
| 70 |
-
return patch, values, reduction, bits
|
| 71 |
|
| 72 |
|
| 73 |
def extract_state(target, canvas, box, step: int, image_w: int, image_h: int, patch_count: int, channels: int, lam: float, bits_spent: float, pixels: int) -> np.ndarray:
|
|
@@ -84,30 +82,8 @@ def extract_state(target, canvas, box, step: int, image_w: int, image_h: int, pa
|
|
| 84 |
|
| 85 |
def propose_boxes(target, canvas, config, rng, channel: int, step: int) -> list[tuple[int, int, int, int, int]]:
|
| 86 |
"""## Returns candidate boxes sorted by the learned filler's prescore front-end"""
|
| 87 |
-
h_img, w_img, _ = target.shape
|
| 88 |
search_q = ops.interp(config.search_q_start, config.search_q_end, step, config.patch_count)
|
| 89 |
-
|
| 90 |
-
abs_error = np.abs(target[:, :, channel] - visible).astype(np.int64)
|
| 91 |
-
integral_abs = ops.integral(abs_error)
|
| 92 |
-
anchors = ops.top_anchors(abs_error.astype(np.float32), config.top_k, config.anchor_block_size, channel)
|
| 93 |
-
if not anchors:
|
| 94 |
-
return []
|
| 95 |
-
|
| 96 |
-
specs, sums, areas = [], [], []
|
| 97 |
-
for i in range(max(1, int(config.search_depth))):
|
| 98 |
-
c, x, y, bw, bh, _, _ = ops.sample_box(rng, anchors[i % len(anchors)], w_img, h_img, config)
|
| 99 |
-
s = integral_abs[y + bh, x + bw] - integral_abs[y, x + bw] - integral_abs[y + bh, x] + integral_abs[y, x]
|
| 100 |
-
if s <= 0:
|
| 101 |
-
continue
|
| 102 |
-
sums.append(float(s))
|
| 103 |
-
areas.append(float(bw * bh))
|
| 104 |
-
specs.append((c, x, y, bw, bh))
|
| 105 |
-
if not specs:
|
| 106 |
-
return []
|
| 107 |
-
|
| 108 |
-
pre = search_q * ops.norm(sums) - (1.0 - search_q) * ops.norm(areas)
|
| 109 |
-
keep = sorted(ops.select_top_indices(pre, config.proposal_depth), key=lambda i: pre[i], reverse=True)
|
| 110 |
-
return [specs[i] for i in keep]
|
| 111 |
|
| 112 |
|
| 113 |
def _load_npz_cached(path: str) -> dict:
|
|
@@ -126,6 +102,12 @@ def _silu(x):
|
|
| 126 |
return x / (1.0 + np.exp(-x))
|
| 127 |
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
class LearnedFiller:
|
| 130 |
"""## Torch-free numpy inference for the legacy single-action learned filler"""
|
| 131 |
|
|
@@ -154,10 +136,7 @@ class LearnedFiller:
|
|
| 154 |
|
| 155 |
def _forward(self, x) -> tuple[np.ndarray, np.ndarray]:
|
| 156 |
"""## Runs the legacy action and value heads"""
|
| 157 |
-
h = x
|
| 158 |
-
if self.hidden > 0:
|
| 159 |
-
h = _silu(h @ self.W0.T + self.b0)
|
| 160 |
-
h = _silu(h @ self.W2.T + self.b2)
|
| 161 |
return h @ self.Wa.T + self.ba, h @ self.Wv.T + self.bv[None, :]
|
| 162 |
|
| 163 |
def _featurize(self, target, canvas, boxes, step: int, q: float, w_img: int, h_img: int, patch_count: int, channels: int) -> np.ndarray:
|
|
@@ -234,8 +213,7 @@ class FactoredFiller:
|
|
| 234 |
|
| 235 |
def _forward(self, x) -> list[np.ndarray]:
|
| 236 |
"""## Runs the factored policy heads"""
|
| 237 |
-
h =
|
| 238 |
-
h = _silu(h @ self.W2.T + self.b2)
|
| 239 |
return [h @ W.T + b for W, b in self.heads]
|
| 240 |
|
| 241 |
def select_patch(self, target, canvas, config, rng, channel_bits: int, step: int, current_channel: int):
|
|
@@ -263,4 +241,4 @@ class FactoredFiller:
|
|
| 263 |
if reduction <= 0:
|
| 264 |
return None, None
|
| 265 |
self._bits += bits
|
| 266 |
-
return patch, values
|
|
|
|
| 47 |
return reduction, bits
|
| 48 |
|
| 49 |
|
| 50 |
+
def _build_action(target, canvas, box, config, channel_bits, cell_size, bitcount):
|
|
|
|
| 51 |
c, x, y, bw, bh = box
|
| 52 |
+
cell = max(1, min(cell_size, bw, bh))
|
|
|
|
| 53 |
residual = target[y:y + bh, x:x + bw, c] - canvas[y:y + bh, x:x + bw, c]
|
| 54 |
+
patch, values = ops.make_patch(c, x, y, bw, bh, cell, residual, config, bitcount)
|
| 55 |
reduction, bits = _score_patch(target, canvas, box, channel_bits, patch, values)
|
| 56 |
return patch, values, reduction, bits
|
| 57 |
|
| 58 |
|
| 59 |
+
def build_action(target, canvas, box, config, channel_bits: int, idx: int):
|
| 60 |
+
"""## Builds and scores one legacy single-index action"""
|
| 61 |
+
cell_size, bitcount = ACTIONS[idx]
|
| 62 |
+
return _build_action(target, canvas, box, config, channel_bits, cell_size, bitcount)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
def build_action_factored(target, canvas, box, config, channel_bits: int, cs_idx: int, bc_idx: int, ms_idx: int):
|
| 66 |
"""## Builds and scores one factored action: cell size, bitcount, and mask size"""
|
| 67 |
+
cfg = dataclasses.replace(config, mask_size=MASK_SIZES[ms_idx])
|
| 68 |
+
return _build_action(target, canvas, box, cfg, channel_bits, CELL_SIZES[cs_idx], BITCOUNTS[bc_idx])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
|
| 71 |
def extract_state(target, canvas, box, step: int, image_w: int, image_h: int, patch_count: int, channels: int, lam: float, bits_spent: float, pixels: int) -> np.ndarray:
|
|
|
|
| 82 |
|
| 83 |
def propose_boxes(target, canvas, config, rng, channel: int, step: int) -> list[tuple[int, int, int, int, int]]:
|
| 84 |
"""## Returns candidate boxes sorted by the learned filler's prescore front-end"""
|
|
|
|
| 85 |
search_q = ops.interp(config.search_q_start, config.search_q_end, step, config.patch_count)
|
| 86 |
+
return ops.search_boxes(target, canvas, config, rng, channel, config.search_depth, search_q, ordered=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
|
| 89 |
def _load_npz_cached(path: str) -> dict:
|
|
|
|
| 102 |
return x / (1.0 + np.exp(-x))
|
| 103 |
|
| 104 |
|
| 105 |
+
def _mlp(x, weights):
|
| 106 |
+
"""## Runs the shared two-layer SiLU trunk"""
|
| 107 |
+
W0, b0, W2, b2 = weights
|
| 108 |
+
return _silu(_silu(x @ W0.T + b0) @ W2.T + b2)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
class LearnedFiller:
|
| 112 |
"""## Torch-free numpy inference for the legacy single-action learned filler"""
|
| 113 |
|
|
|
|
| 136 |
|
| 137 |
def _forward(self, x) -> tuple[np.ndarray, np.ndarray]:
|
| 138 |
"""## Runs the legacy action and value heads"""
|
| 139 |
+
h = _mlp(x, (self.W0, self.b0, self.W2, self.b2)) if self.hidden > 0 else x
|
|
|
|
|
|
|
|
|
|
| 140 |
return h @ self.Wa.T + self.ba, h @ self.Wv.T + self.bv[None, :]
|
| 141 |
|
| 142 |
def _featurize(self, target, canvas, boxes, step: int, q: float, w_img: int, h_img: int, patch_count: int, channels: int) -> np.ndarray:
|
|
|
|
| 213 |
|
| 214 |
def _forward(self, x) -> list[np.ndarray]:
|
| 215 |
"""## Runs the factored policy heads"""
|
| 216 |
+
h = _mlp(x, (self.W0, self.b0, self.W2, self.b2))
|
|
|
|
| 217 |
return [h @ W.T + b for W, b in self.heads]
|
| 218 |
|
| 219 |
def select_patch(self, target, canvas, config, rng, channel_bits: int, step: int, current_channel: int):
|
|
|
|
| 241 |
if reduction <= 0:
|
| 242 |
return None, None
|
| 243 |
self._bits += bits
|
| 244 |
+
return patch, values
|
pbc3_features.py
CHANGED
|
@@ -132,4 +132,4 @@ def extract_cheap(names, target, canvas, box, step: int, q: float, image_w: int,
|
|
| 132 |
d["before_mse"] = float((before ** 2).mean())
|
| 133 |
if "before_mae" in names:
|
| 134 |
d["before_mae"] = float(np.abs(before).mean())
|
| 135 |
-
return np.array([d[nm] for nm in names], dtype=np.float32)
|
|
|
|
| 132 |
d["before_mse"] = float((before ** 2).mean())
|
| 133 |
if "before_mae" in names:
|
| 134 |
d["before_mae"] = float(np.abs(before).mean())
|
| 135 |
+
return np.array([d[nm] for nm in names], dtype=np.float32)
|
pbc3_heads.py
CHANGED
|
@@ -76,31 +76,7 @@ class SearchHead:
|
|
| 76 |
|
| 77 |
def search(self, target, canvas, config, rng, channel: int, depth: int, search_q: float) -> list[tuple[int, int, int, int, int]]:
|
| 78 |
"""## Samples boxes around strong error anchors and keeps the best prescores"""
|
| 79 |
-
|
| 80 |
-
visible_error = (target[:, :, channel] - visible_canvas_channel).astype(np.int64)
|
| 81 |
-
abs_error = np.abs(visible_error)
|
| 82 |
-
integral_abs = ops.integral(abs_error)
|
| 83 |
-
|
| 84 |
-
anchors = ops.top_anchors(abs_error.astype(np.float32), config.top_k, config.anchor_block_size, channel)
|
| 85 |
-
if not anchors:
|
| 86 |
-
return []
|
| 87 |
-
|
| 88 |
-
h, w, _ = target.shape
|
| 89 |
-
box_sums, box_areas, box_specs = [], [], []
|
| 90 |
-
for i in range(max(1, int(depth))):
|
| 91 |
-
c, x, y, bw, bh, ax, ay = ops.sample_box(rng, anchors[i % len(anchors)], w, h, config)
|
| 92 |
-
box_sum = integral_abs[y + bh, x + bw] - integral_abs[y, x + bw] - integral_abs[y + bh, x] + integral_abs[y, x]
|
| 93 |
-
if box_sum <= 0:
|
| 94 |
-
continue
|
| 95 |
-
box_sums.append(float(box_sum))
|
| 96 |
-
box_areas.append(float(bw * bh))
|
| 97 |
-
box_specs.append((c, x, y, bw, bh))
|
| 98 |
-
if not box_specs:
|
| 99 |
-
return []
|
| 100 |
-
|
| 101 |
-
pre_scores = search_q * ops.norm(box_sums) - (1.0 - search_q) * ops.norm(box_areas)
|
| 102 |
-
keep = ops.select_top_indices(pre_scores, config.proposal_depth)
|
| 103 |
-
return [box_specs[i] for i in keep]
|
| 104 |
|
| 105 |
|
| 106 |
class FillerHead:
|
|
|
|
| 76 |
|
| 77 |
def search(self, target, canvas, config, rng, channel: int, depth: int, search_q: float) -> list[tuple[int, int, int, int, int]]:
|
| 78 |
"""## Samples boxes around strong error anchors and keeps the best prescores"""
|
| 79 |
+
return ops.search_boxes(target, canvas, config, rng, channel, depth, search_q)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
|
| 82 |
class FillerHead:
|
pbc3_ops.py
CHANGED
|
@@ -10,8 +10,6 @@ RESAMPLE_REDUCING_GAP = None
|
|
| 10 |
PALETTE_GENERATED = 0
|
| 11 |
|
| 12 |
|
| 13 |
-
# ===== DETERMINISTIC RNG + LOCAL BICUBIC RESAMPLER =====
|
| 14 |
-
|
| 15 |
_UINT64_MASK = (1 << 64) - 1
|
| 16 |
_UINT32_SCALE = 1.0 / 4294967296.0
|
| 17 |
_RESAMPLE_COEFF_CACHE = {}
|
|
@@ -105,31 +103,6 @@ def bicubic_resample_2d(values, out_h: int, out_w: int) -> np.ndarray:
|
|
| 105 |
return out
|
| 106 |
|
| 107 |
|
| 108 |
-
def resize_array_bicubic(values, out_h: int, out_w: int):
|
| 109 |
-
arr = np.asarray(values)
|
| 110 |
-
if arr.ndim == 2:
|
| 111 |
-
out = bicubic_resample_2d(arr, out_h, out_w)
|
| 112 |
-
elif arr.ndim == 3:
|
| 113 |
-
chans = [bicubic_resample_2d(arr[:, :, c], out_h, out_w) for c in range(arr.shape[2])]
|
| 114 |
-
out = np.stack(chans, axis=2)
|
| 115 |
-
else:
|
| 116 |
-
raise ValueError(f"expected 2D or 3D array, got shape {arr.shape}")
|
| 117 |
-
|
| 118 |
-
if np.issubdtype(arr.dtype, np.integer):
|
| 119 |
-
info = np.iinfo(arr.dtype)
|
| 120 |
-
return np.clip(np.rint(out), info.min, info.max).astype(arr.dtype)
|
| 121 |
-
return out.astype(arr.dtype, copy=False)
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
def resize_image_bicubic(img: Image.Image, out_h: int, out_w: int) -> Image.Image:
|
| 125 |
-
if img.size == (int(out_w), int(out_h)):
|
| 126 |
-
return img.copy()
|
| 127 |
-
arr = resize_array_bicubic(np.asarray(img), out_h, out_w)
|
| 128 |
-
return Image.fromarray(arr, img.mode)
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
# ===== CORE MATH =====
|
| 132 |
-
|
| 133 |
def ceil_div(a: int, b: int) -> int:
|
| 134 |
"""## Returns ceil(a / b) for integer cell/grid math"""
|
| 135 |
return (int(a) + int(b) - 1) // int(b)
|
|
@@ -157,16 +130,6 @@ def integral(a) -> np.ndarray:
|
|
| 157 |
return np.pad(a.astype(np.int64).cumsum(0).cumsum(1), ((1, 0), (1, 0)))
|
| 158 |
|
| 159 |
|
| 160 |
-
def cell_edges(start: int, length: int, cell_size: int) -> np.ndarray:
|
| 161 |
-
"""## Returns the edge coordinates of patch cells, clamped to the patch end"""
|
| 162 |
-
n = ceil_div(length, cell_size)
|
| 163 |
-
edges = start + np.arange(n + 1) * cell_size
|
| 164 |
-
edges[n] = start + length
|
| 165 |
-
return edges
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
# ===== NUMBA KERNELS =====
|
| 169 |
-
|
| 170 |
@njit(cache=True)
|
| 171 |
def _box_cell_bound_kernel(integral_arr, x, y, bw, bh, cell_size):
|
| 172 |
nx = (bw + cell_size - 1) // cell_size
|
|
@@ -345,8 +308,6 @@ def _quantize_signed_kernel(vals, pal):
|
|
| 345 |
out[i] = best_i
|
| 346 |
return out
|
| 347 |
|
| 348 |
-
# ===== PALETTE OPS =====
|
| 349 |
-
|
| 350 |
|
| 351 |
def palette_bounds(values) -> tuple[int, int]:
|
| 352 |
"""## Returns negative max (min or 0) and positive max (true max)
|
|
@@ -364,14 +325,10 @@ def range_counts(mask_size, negative_max=255, positive_max=255, positive_bias=Tr
|
|
| 364 |
negative_max = max(0, int(negative_max))
|
| 365 |
positive_max = max(0, int(positive_max))
|
| 366 |
if side_bits == 0 or (negative_max == 0 and positive_max == 0):
|
| 367 |
-
# The reason it's 0,0 if mask size is 1 is that the only range that can be represented by it is the 0 bin
|
| 368 |
-
# This actually just makes mask size 1 useless for palette generation.
|
| 369 |
return 0, 0
|
| 370 |
if negative_max == 0:
|
| 371 |
-
# Obviously if there are no negative values, all bins should be positive
|
| 372 |
return min(side_bits, positive_max), 0
|
| 373 |
if positive_max == 0:
|
| 374 |
-
# Obviously same
|
| 375 |
return 0, min(side_bits, negative_max)
|
| 376 |
raw_pos = side_bits * positive_max / (positive_max + negative_max)
|
| 377 |
pos_count = math.ceil(raw_pos) if positive_bias else math.floor(raw_pos)
|
|
@@ -488,8 +445,6 @@ def quantize_signed(values, palette):
|
|
| 488 |
|
| 489 |
|
| 490 |
|
| 491 |
-
# ===== IMAGE / GRID OPS =====
|
| 492 |
-
|
| 493 |
def signed_resample(values, out_h: int, out_w: int) -> np.ndarray:
|
| 494 |
"""## Resizes a signed grid to a patch-sized int16 delta image"""
|
| 495 |
return np.rint(bicubic_resample_2d(values, int(out_h), int(out_w))).astype(np.int16)
|
|
@@ -520,8 +475,6 @@ def final_mse(reference_image, reconstructed_image) -> float:
|
|
| 520 |
return image_mse(np.asarray(reference_image, dtype=np.float32), np.asarray(reconstructed_image, dtype=np.float32))
|
| 521 |
|
| 522 |
|
| 523 |
-
# ===== SEARCH OPS =====
|
| 524 |
-
|
| 525 |
def box_cell_bound(integral_arr, x: int, y: int, bw: int, bh: int, cell_size: int) -> float:
|
| 526 |
"""## Returns the best-case signed-error energy for a box/cell-size pair"""
|
| 527 |
return float(_box_cell_bound_kernel(
|
|
@@ -589,6 +542,35 @@ def sample_box(rng, anchor, image_w: int, image_h: int, config) -> tuple[int, in
|
|
| 589 |
return c, x, y, w, h, ax, ay
|
| 590 |
|
| 591 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 592 |
def base_cell_size(residual_patch, config) -> int:
|
| 593 |
"""## Returns the rough cell size suggested by local residual frequency"""
|
| 594 |
if residual_patch.size == 0:
|
|
@@ -596,8 +578,6 @@ def base_cell_size(residual_patch, config) -> int:
|
|
| 596 |
return int(_base_cell_size_kernel(np.ascontiguousarray(residual_patch, dtype=np.float64), int(config.max_cell_size)))
|
| 597 |
|
| 598 |
|
| 599 |
-
# ===== PATCH OPS =====
|
| 600 |
-
|
| 601 |
def patch_bits_for(patch, channel_bits: int) -> int:
|
| 602 |
"""## Returns the serialized bit cost of a complete patch"""
|
| 603 |
grid_bits = ceil_div(patch["w"], patch["cell_size"]) * ceil_div(patch["h"], patch["cell_size"]) * patch["bitcount"]
|
|
@@ -639,4 +619,4 @@ def make_patch(channel: int, x: int, y: int, w: int, h: int, cell_size: int, res
|
|
| 639 |
|
| 640 |
def debug_line(kind: str, **items) -> str:
|
| 641 |
"""## Returns one plain-text debug line with key=value fields"""
|
| 642 |
-
return kind + " " + " ".join(f"{k}={v}" for k, v in items.items())
|
|
|
|
| 10 |
PALETTE_GENERATED = 0
|
| 11 |
|
| 12 |
|
|
|
|
|
|
|
| 13 |
_UINT64_MASK = (1 << 64) - 1
|
| 14 |
_UINT32_SCALE = 1.0 / 4294967296.0
|
| 15 |
_RESAMPLE_COEFF_CACHE = {}
|
|
|
|
| 103 |
return out
|
| 104 |
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
def ceil_div(a: int, b: int) -> int:
|
| 107 |
"""## Returns ceil(a / b) for integer cell/grid math"""
|
| 108 |
return (int(a) + int(b) - 1) // int(b)
|
|
|
|
| 130 |
return np.pad(a.astype(np.int64).cumsum(0).cumsum(1), ((1, 0), (1, 0)))
|
| 131 |
|
| 132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
@njit(cache=True)
|
| 134 |
def _box_cell_bound_kernel(integral_arr, x, y, bw, bh, cell_size):
|
| 135 |
nx = (bw + cell_size - 1) // cell_size
|
|
|
|
| 308 |
out[i] = best_i
|
| 309 |
return out
|
| 310 |
|
|
|
|
|
|
|
| 311 |
|
| 312 |
def palette_bounds(values) -> tuple[int, int]:
|
| 313 |
"""## Returns negative max (min or 0) and positive max (true max)
|
|
|
|
| 325 |
negative_max = max(0, int(negative_max))
|
| 326 |
positive_max = max(0, int(positive_max))
|
| 327 |
if side_bits == 0 or (negative_max == 0 and positive_max == 0):
|
|
|
|
|
|
|
| 328 |
return 0, 0
|
| 329 |
if negative_max == 0:
|
|
|
|
| 330 |
return min(side_bits, positive_max), 0
|
| 331 |
if positive_max == 0:
|
|
|
|
| 332 |
return 0, min(side_bits, negative_max)
|
| 333 |
raw_pos = side_bits * positive_max / (positive_max + negative_max)
|
| 334 |
pos_count = math.ceil(raw_pos) if positive_bias else math.floor(raw_pos)
|
|
|
|
| 445 |
|
| 446 |
|
| 447 |
|
|
|
|
|
|
|
| 448 |
def signed_resample(values, out_h: int, out_w: int) -> np.ndarray:
|
| 449 |
"""## Resizes a signed grid to a patch-sized int16 delta image"""
|
| 450 |
return np.rint(bicubic_resample_2d(values, int(out_h), int(out_w))).astype(np.int16)
|
|
|
|
| 475 |
return image_mse(np.asarray(reference_image, dtype=np.float32), np.asarray(reconstructed_image, dtype=np.float32))
|
| 476 |
|
| 477 |
|
|
|
|
|
|
|
| 478 |
def box_cell_bound(integral_arr, x: int, y: int, bw: int, bh: int, cell_size: int) -> float:
|
| 479 |
"""## Returns the best-case signed-error energy for a box/cell-size pair"""
|
| 480 |
return float(_box_cell_bound_kernel(
|
|
|
|
| 542 |
return c, x, y, w, h, ax, ay
|
| 543 |
|
| 544 |
|
| 545 |
+
def search_boxes(target, canvas, config, rng, channel: int, depth: int, search_q: float, ordered=False):
|
| 546 |
+
"""Samples error-centered boxes and keeps the best prescores."""
|
| 547 |
+
h_img, w_img, _ = target.shape
|
| 548 |
+
visible = np.clip(canvas[:, :, channel], 0, 255).astype(np.int32)
|
| 549 |
+
abs_error = np.abs(target[:, :, channel] - visible).astype(np.int64)
|
| 550 |
+
integral_abs = integral(abs_error)
|
| 551 |
+
anchors = top_anchors(abs_error.astype(np.float32), config.top_k, config.anchor_block_size, channel)
|
| 552 |
+
if not anchors:
|
| 553 |
+
return []
|
| 554 |
+
|
| 555 |
+
specs, sums, areas = [], [], []
|
| 556 |
+
for i in range(max(1, int(depth))):
|
| 557 |
+
c, x, y, bw, bh, _, _ = sample_box(rng, anchors[i % len(anchors)], w_img, h_img, config)
|
| 558 |
+
total = integral_abs[y + bh, x + bw] - integral_abs[y, x + bw] - integral_abs[y + bh, x] + integral_abs[y, x]
|
| 559 |
+
if total <= 0:
|
| 560 |
+
continue
|
| 561 |
+
sums.append(float(total))
|
| 562 |
+
areas.append(float(bw * bh))
|
| 563 |
+
specs.append((c, x, y, bw, bh))
|
| 564 |
+
if not specs:
|
| 565 |
+
return []
|
| 566 |
+
|
| 567 |
+
scores = search_q * norm(sums) - (1.0 - search_q) * norm(areas)
|
| 568 |
+
keep = select_top_indices(scores, config.proposal_depth)
|
| 569 |
+
if ordered:
|
| 570 |
+
keep = sorted(keep, key=lambda i: scores[i], reverse=True)
|
| 571 |
+
return [specs[i] for i in keep]
|
| 572 |
+
|
| 573 |
+
|
| 574 |
def base_cell_size(residual_patch, config) -> int:
|
| 575 |
"""## Returns the rough cell size suggested by local residual frequency"""
|
| 576 |
if residual_patch.size == 0:
|
|
|
|
| 578 |
return int(_base_cell_size_kernel(np.ascontiguousarray(residual_patch, dtype=np.float64), int(config.max_cell_size)))
|
| 579 |
|
| 580 |
|
|
|
|
|
|
|
| 581 |
def patch_bits_for(patch, channel_bits: int) -> int:
|
| 582 |
"""## Returns the serialized bit cost of a complete patch"""
|
| 583 |
grid_bits = ceil_div(patch["w"], patch["cell_size"]) * ceil_div(patch["h"], patch["cell_size"]) * patch["bitcount"]
|
|
|
|
| 619 |
|
| 620 |
def debug_line(kind: str, **items) -> str:
|
| 621 |
"""## Returns one plain-text debug line with key=value fields"""
|
| 622 |
+
return kind + " " + " ".join(f"{k}={v}" for k, v in items.items())
|
pbc3_stream.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PBC3 bitstream framing, separate from the image algorithm."""
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
import pbc3_ops as ops
|
| 6 |
+
from pbc3_types import BitReader, BitWriter
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def write_grid(bw: BitWriter, values, bitcount: int) -> None:
|
| 10 |
+
"""Writes a flat grid of palette indices."""
|
| 11 |
+
for value in values:
|
| 12 |
+
bw.write(int(value), bitcount)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def read_grid(br: BitReader, count: int, bitcount: int) -> np.ndarray:
|
| 16 |
+
"""Reads a flat grid of palette indices."""
|
| 17 |
+
values = np.zeros(count, dtype=np.uint16)
|
| 18 |
+
for i in range(count):
|
| 19 |
+
values[i] = br.read(bitcount)
|
| 20 |
+
return values
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def write_header(bw, w, h, original_w, original_h, downsampled, color_id, channels,
|
| 24 |
+
channel_bits, positive_bias, has_alpha, patch_count, base_values, warmup=None):
|
| 25 |
+
"""Writes the image-level stream header."""
|
| 26 |
+
bw.write(int(downsampled), 1)
|
| 27 |
+
if downsampled:
|
| 28 |
+
bw.write(original_w, 16)
|
| 29 |
+
bw.write(original_h, 16)
|
| 30 |
+
bw.write(w, 16)
|
| 31 |
+
bw.write(h, 16)
|
| 32 |
+
bw.write(color_id, 2)
|
| 33 |
+
bw.write(channels, 8)
|
| 34 |
+
bw.write(channel_bits, 4)
|
| 35 |
+
bw.write(int(positive_bias), 1)
|
| 36 |
+
bw.write(int(has_alpha), 1)
|
| 37 |
+
bw.write(patch_count, 32)
|
| 38 |
+
for base in base_values:
|
| 39 |
+
bw.write(base, 8)
|
| 40 |
+
bw.write(int(warmup is not None), 1)
|
| 41 |
+
if warmup is not None:
|
| 42 |
+
warm_w, warm_h, warm_split = warmup
|
| 43 |
+
bw.write(warm_w, 16)
|
| 44 |
+
bw.write(warm_h, 16)
|
| 45 |
+
bw.write(warm_split, 32)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def read_header(br, color_space_names):
|
| 49 |
+
"""Reads the image-level stream header."""
|
| 50 |
+
downsampled = bool(br.read(1))
|
| 51 |
+
original_w = br.read(16) if downsampled else None
|
| 52 |
+
original_h = br.read(16) if downsampled else None
|
| 53 |
+
w = br.read(16)
|
| 54 |
+
h = br.read(16)
|
| 55 |
+
color_id = br.read(2)
|
| 56 |
+
channels = br.read(8)
|
| 57 |
+
channel_bits = br.read(4)
|
| 58 |
+
positive_bias = bool(br.read(1))
|
| 59 |
+
has_alpha = bool(br.read(1))
|
| 60 |
+
patch_count = br.read(32)
|
| 61 |
+
base_values = [br.read(8) for _ in range(channels)]
|
| 62 |
+
warmup_on = bool(br.read(1))
|
| 63 |
+
warm_w = warm_h = warmup_split = None
|
| 64 |
+
if warmup_on:
|
| 65 |
+
warm_w = br.read(16)
|
| 66 |
+
warm_h = br.read(16)
|
| 67 |
+
warmup_split = br.read(32)
|
| 68 |
+
return (
|
| 69 |
+
downsampled, original_w, original_h, w, h, color_space_names[color_id], channels,
|
| 70 |
+
channel_bits, positive_bias, has_alpha, patch_count, base_values, warmup_on, warm_w,
|
| 71 |
+
warm_h, warmup_split,
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def write_patch(bw, patch, channel_bits: int) -> None:
|
| 76 |
+
"""Writes one generated-palette patch."""
|
| 77 |
+
bw.write(patch["channel"], channel_bits)
|
| 78 |
+
bw.write(patch["x"], 16)
|
| 79 |
+
bw.write(patch["y"], 16)
|
| 80 |
+
bw.write(patch["w"], 16)
|
| 81 |
+
bw.write(patch["h"], 16)
|
| 82 |
+
bw.write(0, 1)
|
| 83 |
+
mask = patch["mask"]
|
| 84 |
+
bw.write(len(mask), 10)
|
| 85 |
+
for bit in mask:
|
| 86 |
+
bw.write(bit, 1)
|
| 87 |
+
bw.write(patch["neg"], 8)
|
| 88 |
+
bw.write(patch["pos"], 8)
|
| 89 |
+
bw.write(patch["max_bitcount"], 4)
|
| 90 |
+
bw.write(patch["cell_size"], 16)
|
| 91 |
+
write_grid(bw, patch["indices"].ravel().astype(np.int64), patch["bitcount"])
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def read_patch(br, channel_bits: int, positive_bias: bool = True):
|
| 95 |
+
"""Reads one generated-palette patch and returns its decoded values."""
|
| 96 |
+
channel = br.read(channel_bits)
|
| 97 |
+
x, y = br.read(16), br.read(16)
|
| 98 |
+
w, h = br.read(16), br.read(16)
|
| 99 |
+
if br.read(1) != 0:
|
| 100 |
+
raise ValueError("explicit palette patches were removed in PBC3 3.0 release cleanup")
|
| 101 |
+
mask = [br.read(1) for _ in range(br.read(10))]
|
| 102 |
+
negative_max, positive_max = br.read(8), br.read(8)
|
| 103 |
+
max_bitcount = br.read(4)
|
| 104 |
+
bitcount = ops.resolve_palette_bitcount(mask, max_bitcount, negative_max, positive_max, positive_bias)
|
| 105 |
+
palette = ops.palette_generator(mask, max_bitcount, negative_max, positive_max, positive_bias)
|
| 106 |
+
cell_size = br.read(16)
|
| 107 |
+
gw, gh = ops.ceil_div(w, cell_size), ops.ceil_div(h, cell_size)
|
| 108 |
+
indices = read_grid(br, gh * gw, bitcount).reshape(gh, gw)
|
| 109 |
+
return channel, x, y, w, h, cell_size, palette[indices], bitcount
|
pbc3_types.py
CHANGED
|
@@ -283,4 +283,4 @@ class PBC3Result:
|
|
| 283 |
bbox=dict(boxstyle="round,pad=0.5", facecolor="black", alpha=0.72, edgecolor="none"),
|
| 284 |
)
|
| 285 |
image_ax.imshow(self.image)
|
| 286 |
-
plt.show()
|
|
|
|
| 283 |
bbox=dict(boxstyle="round,pad=0.5", facecolor="black", alpha=0.72, edgecolor="none"),
|
| 284 |
)
|
| 285 |
image_ax.imshow(self.image)
|
| 286 |
+
plt.show()
|