from dataclasses import dataclass import numpy as np from PIL import Image class BitWriter: """## Small helper for writing arbitrary-width integer fields""" def __init__(self) -> None: self.data = bytearray() self.acc = 0 self.nbits = 0 def write(self, value: int, bitcount: int) -> None: """## Writes one unsigned integer using bitcount bits""" value = int(value) if bitcount <= 0: return if value < 0 or value >= (1 << bitcount): raise ValueError(f"value {value} does not fit in {bitcount} bits") self.acc = (self.acc << bitcount) | value self.nbits += bitcount while self.nbits >= 8: shift = self.nbits - 8 self.data.append((self.acc >> shift) & 255) self.acc &= (1 << shift) - 1 self.nbits -= 8 def finish(self) -> bytes: """## Flushes the last partial byte and returns the written bytes""" if self.nbits: self.data.append((self.acc << (8 - self.nbits)) & 255) self.acc = 0 self.nbits = 0 return bytes(self.data) class BitReader: """## Small helper for reading arbitrary-width integer fields""" def __init__(self, data: bytes) -> None: self.data = data self.i = 0 self.acc = 0 self.nbits = 0 def read(self, bitcount: int) -> int: """## Reads one unsigned integer using bitcount bits""" while self.nbits < bitcount: if self.i >= len(self.data): raise EOFError("bitstream ended early") self.acc = (self.acc << 8) | self.data[self.i] self.i += 1 self.nbits += 8 shift = self.nbits - bitcount value = (self.acc >> shift) & ((1 << bitcount) - 1) self.acc &= (1 << shift) - 1 self.nbits -= bitcount return value @dataclass class PBC3Config: patch_count: int = 50 search_depth: int = 200 proposal_depth: int = 50 exact_depth: int = 10 min_patch_size: int = 16 max_patch_size: int = 400 min_cell_size: int = 1 max_cell_size: int = 64 cell_sizes_per_candidate: int = 3 top_k: int = 20 search_q_start: float = 0.5 search_q_end: float = 0.2 q_init: float = 0.7 q_start: float = 0.8 q_end: float = 0.8 color_space: str = "YCbCr" channel_cycle: str = "Sum" auto_downsample_init: bool = True init_search_depth: int = 3 downsample_init_cell_size: int = 12 downsample_palette_bitcount: int = 6 downsample_rate: float = -1 auto_downsample_max_pixels: int = 250_000 warmup_ratio: float = -1 warm_downsample_max_pixels: int = 750_000 patch_palette_bitcount: int = 2 quality_target_mae: float = 0.0 mask_size: int = 4 anchor_block_size: int = 8 positive_bias: bool = True learned_filler_enabled: bool = True learned_filler_model_path: str = "patch_policy.npz" learned_filler_top_k: int = 1 learned_filler_q: float = 0.6 learned_filler_candidates: int = 1 use_lzma: bool = True random_seed: int = 2003 compute_final_mse: bool = True debug_mode: bool = False debug_print: bool = False debug_path: str = None reuse_selected_delta: bool = True residual_projection_mode: str = "bicubic" cell_selection_mode: str = "gradient" representability_threshold: float = 0.0 def __post_init__(self) -> None: cycle = str(self.channel_cycle).strip().lower().replace("_", " ") if cycle in {"off", "cycle", "round robin", "roundrobin"}: self.channel_cycle = "Mod" elif cycle in {"sum", "sum target", "target", "max", "max sum"}: self.channel_cycle = "Sum" elif cycle == "mod": self.channel_cycle = "Mod" else: self.channel_cycle = str(self.channel_cycle) mode = str(self.residual_projection_mode).strip().lower() if mode not in {"bicubic", "cell_mean"}: raise ValueError("residual_projection_mode must be 'bicubic' or 'cell_mean'") self.residual_projection_mode = mode selection_mode = str(self.cell_selection_mode).strip().lower() if selection_mode not in {"gradient", "hybrid"}: raise ValueError("cell_selection_mode must be 'gradient' or 'hybrid'") self.cell_selection_mode = selection_mode self.representability_threshold = float(self.representability_threshold) if not 0.0 <= self.representability_threshold <= 1.0: raise ValueError("representability_threshold must be in 0..1") @classmethod def _preset(cls, **values): values.update(values.pop("overrides", {})) return cls(**values) @classmethod def compression(cls, **kwargs): return cls._preset( patch_count=50, search_q_start=0.5, search_q_end=0.2, init_search_depth=3, q_init=0.7, q_start=0.8, q_end=0.8, quality_target_mae=0.0, learned_filler_enabled=True, learned_filler_q=0.4, overrides=kwargs, ) @classmethod def balanced(cls, **kwargs): return cls._preset( patch_count=50, search_q_start=0.5, search_q_end=0.2, init_search_depth=3, q_init=0.7, q_start=0.8, q_end=0.8, quality_target_mae=0.0, learned_filler_enabled=True, learned_filler_q=0.6, overrides=kwargs, ) @classmethod def quality(cls, **kwargs): return cls._preset( patch_count=50, search_q_start=0.5, search_q_end=0.2, init_search_depth=3, q_init=0.7, q_start=0.8, q_end=0.8, quality_target_mae=0.0, learned_filler_enabled=True, learned_filler_q=0.8, overrides=kwargs, ) @classmethod def high_quality(cls, **kwargs): return cls._preset( patch_count=20, search_q_start=0.7, search_q_end=0.2, init_search_depth=3, q_init=0.7, q_start=0.8, q_end=0.8, quality_target_mae=0.0, learned_filler_enabled=True, learned_filler_q=0.95, overrides=kwargs, ) @dataclass class PBC3Result: image: Image.Image data: bytes config: PBC3Config mse: float | None encode_seconds: float total_bits: int original_width: int = None original_height: int = None working_width: int = None working_height: int = None debug_path: str = None channels: int = 3 timings: dict | None = None @property def time(self) -> float: return self.encode_seconds @property def encode_time(self) -> float: return self.encode_seconds @property def decode_time(self) -> float: return self.encode_seconds @property def decode_seconds(self) -> float: return self.encode_seconds @property def original_bits(self) -> int: w = self.original_width or self.image.width h = self.original_height or self.image.height return w * h * self.channels * 8 @property def compressed_kb(self) -> float: return self.total_bits / 8 / 1024 @property def original_kb(self) -> float: return self.original_bits / 8 / 1024 @property def compression_rate(self) -> float: return self.original_bits / self.total_bits if self.total_bits else float("inf") @property def compressed_percent(self) -> float: return self.total_bits / self.original_bits * 100 if self.original_bits else 0 def save(self, path: str) -> None: if self.data is None: raise ValueError("result has no compressed data to save") with open(path, "wb") as f: f.write(self.data) def verify(self) -> bool: from PBC3 import PBC3 if self.data is None: return False decoded = PBC3.decompress(self.data).image return np.array_equal(np.asarray(self.image), np.asarray(decoded)) def show(self, subtitle: str = None) -> None: import os from matplotlib import pyplot as plt fig = plt.figure(figsize=(8, 7.4), dpi=130) gs = fig.add_gridspec(3, 1, height_ratios=[0.09, 0.16, 1.0], hspace=0.04) title_ax = fig.add_subplot(gs[0]) info_ax = fig.add_subplot(gs[1]) image_ax = fig.add_subplot(gs[2]) for ax in (title_ax, info_ax, image_ax): ax.axis("off") title_ax.text(0.5, 0.5, "PBC3 Result" if subtitle is None else f"PBC3 Result\n{subtitle}", ha="center", va="center", fontsize=16, fontweight="bold") mse = "N/A" if self.mse is None else f"{self.mse:.2f}" seconds = "N/A" if self.encode_seconds is None else f"{self.encode_seconds:.3f}s" debug = f" | Debug: {os.path.basename(self.debug_path)}" if self.debug_path else "" info = ( f"MSE: {mse} | Compressed: {self.compressed_kb:.2f} KB | Original: {self.original_kb:.2f} KB\n" f"Compression: {self.compression_rate:.2f}x ({self.compressed_percent:.2f}%) | Time: {seconds}{debug}" ) info_ax.text( 0.5, 0.5, info, ha="center", va="center", color="white", fontsize=10, linespacing=1.35, bbox=dict(boxstyle="round,pad=0.5", facecolor="black", alpha=0.72, edgecolor="none"), ) image_ax.imshow(self.image) plt.show()