EgeEken commited on
Commit
7deb682
·
1 Parent(s): 4d7d9bd

pbc3: promote simplified codec

Browse files
Files changed (7) hide show
  1. PBC3.py +124 -184
  2. learned_filler.py +21 -43
  3. pbc3_features.py +1 -1
  4. pbc3_heads.py +1 -25
  5. pbc3_ops.py +30 -50
  6. pbc3_stream.py +109 -0
  7. 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
- @classmethod
95
- def _write_grid(cls, bw: BitWriter, flat, bitcount: int) -> None:
96
- """## Writes a flat grid of palette indices"""
97
- for value in flat:
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 _write_patch(cls, bw: BitWriter, patch, channel_bits: int) -> None:
177
- """## Writes one generated-palette patch"""
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
- @classmethod
195
- def _read_patch(cls, br: BitReader, channel_bits: int, positive_bias: bool = True):
196
- """## Reads one generated-palette patch and returns its decoded values"""
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
- if config is None:
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(color_img.size, config.downsample_rate, config.auto_downsample_max_pixels)
 
 
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 = alpha_img.resize(color_ds.size, cls.RESAMPLE_FILTER, reducing_gap=cls.RESAMPLE_REDUCING_GAP) if downsampled else alpha_img
 
 
 
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(warm_color_ds.size, cls.RESAMPLE_FILTER, reducing_gap=cls.RESAMPLE_REDUCING_GAP)
 
 
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 (step - 1) % channels if str(mode).lower() == "mod" else int(max(range(channels), key=lambda c: scores[c]))
 
 
 
 
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
- if config is None:
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
- if w > 65535 or h > 65535 or original_w > 65535 or original_h > 65535:
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 = np.zeros((h, w, channels), dtype=np.int32)
387
- for c, base in enumerate(base_values):
388
- canvas[:, :, c] = base
389
  if frame_every:
390
- yield {"event": "frame", "step": 0, "total": int(config.patch_count), "image": cls._canvas_to_image(canvas, config.color_space, has_alpha)}
 
 
 
391
 
392
  patches = []
393
  init_head = DownsampleInitHead()
394
  for c in range(channels):
395
- patch, values, init_cell, init_bits = init_head.select(c, target, canvas, w, h, config, channel_bits)
 
 
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("INIT", stream_patch=len(patches), channel=c, x=0, y=0, w=w, h=h, cell_size=init_cell, bitcount=init_bits))
 
 
 
402
  if frame_every:
403
- yield {"event": "frame", "step": 0, "total": int(config.patch_count), "image": cls._canvas_to_image(canvas, config.color_space, has_alpha)}
 
 
 
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 = None if filler.learned is not None else search.propose(target, canvas, config, rng, step, current_channel)
414
- patch, values = filler.select(target, canvas, config, rng, channel_bits, step, current_channel, boxes, len(patches), debug_lines)
 
 
 
 
 
 
415
  if patch is None:
416
  break
417
 
418
  c = patch["channel"]
419
- ops.apply_grid(canvas[:, :, c], patch["x"], patch["y"], patch["w"], patch["h"], patch["cell_size"], values)
 
 
 
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("APPLIED", patch_step=step, stream_patch=len(patches), channel=c, channel_score=f"{channel_scores[c]:.4f}", x=patch["x"], y=patch["y"], w=patch["w"], h=patch["h"], cell_size=patch["cell_size"]))
 
 
 
 
432
  if config.debug_print:
433
  print("|", end="", flush=True)
434
  if frame_every and applied % frame_every == 0:
435
- yield {"event": "frame", "step": step, "total": int(config.patch_count), "image": cls._canvas_to_image(canvas, config.color_space, has_alpha)}
436
- if quality_target > 0 and float(np.mean(np.abs(target - np.clip(canvas, 0, 255)))) <= quality_target:
 
 
 
 
 
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
- downsampled, original_w, original_h, w, h, color_space, channels, channel_bits, positive_bias, has_alpha, patch_count, base_values, warmup_on, warm_w, warm_h, warmup_split = header
488
- canvas = np.zeros((h, w, channels), dtype=np.int32)
489
- for c, base in enumerate(base_values):
490
- canvas[:, :, c] = base
 
 
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(br, channel_bits, positive_bias)
 
 
496
  ops.apply_grid(canvas[:, :, channel], x, y, pw, ph, cell_size, values)
497
- return canvas, color_space, downsampled, original_w, original_h, canvas.shape[1], canvas.shape[0], has_alpha, channels, patch_count
 
 
 
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
- canvas, color_space, downsampled, original_w, original_h, w, h, has_alpha, channels, patch_count = cls._decode_to_canvas(data, max_patches=max_patches)
 
 
 
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(img, data, cfg, None, time.perf_counter() - t0, len(data) * 8, original_w or w, original_h or h, w, h, channels=channels)
512
-
513
- # ---- file helpers -----------------------------------------------------
 
514
 
515
  @classmethod
516
- def encode_file(cls, input_path: str, output_path: str, config: PBC3Config = None, **kwargs) -> PBC3Result:
 
 
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 build_action(target, canvas, box, config, channel_bits: int, idx: int):
51
- """## Builds and scores one legacy single-index action"""
52
  c, x, y, bw, bh = box
53
- cs, bc = ACTIONS[idx]
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, bc)
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
- c, x, y, bw, bh = box
64
- cs, bc, ms = CELL_SIZES[cs_idx], BITCOUNTS[bc_idx], MASK_SIZES[ms_idx]
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
- visible = np.clip(canvas[:, :, channel], 0, 255).astype(np.int32)
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 = _silu(x @ self.W0.T + self.b0)
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
- visible_canvas_channel = np.clip(canvas[:, :, channel], 0, 255).astype(np.int32)
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()