imagecalculator 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ """imagecalculator: progressive block-DCT image codec.
2
+
3
+ Tier 1 -- truncation reduces your token count::
4
+
5
+ enc = ic.compress("photo.jpg", keep=8) # 8 of 63 diagonals -> 112 px for the model
6
+ img = ic.decompress(enc) # PIL image at the reduced native size
7
+
8
+ Tier 2 -- truncation + per-diagonal quantization, YCrCb, chroma subsampling and
9
+ entropy coding reduce the bytes over the air (``enc.nbytes``).
10
+ """
11
+ from ._version import __version__
12
+ from .bands import decode_payload, diag_support_index, encode_payload
13
+ from .codec import EncodedImage, Encoder, compress, decode, decompress, load
14
+ from .config import IC_BLOCK_SIZES, IC_QUANTIZATION_FACTORS, SUBSAMPLING_STRATEGIES, CodecConfig, quantization_bands
15
+ from .image import load_image, load_pil, png_b64, png_bytes, to_pil
16
+ from .policy import DiagonalPolicy
17
+ from .tokens import TokenEstimate, estimate as token_estimate
18
+
19
+ __all__ = [
20
+ "__version__",
21
+ "CodecConfig", "quantization_bands", "IC_BLOCK_SIZES", "IC_QUANTIZATION_FACTORS", "SUBSAMPLING_STRATEGIES",
22
+ "DiagonalPolicy",
23
+ "Encoder", "EncodedImage", "compress", "decompress", "decode", "load",
24
+ "load_image", "load_pil", "to_pil", "png_bytes", "png_b64",
25
+ "token_estimate", "TokenEstimate",
26
+ "encode_payload", "decode_payload", "diag_support_index",
27
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,94 @@
1
+ """Per-diagonal band payloads: support indices and the blosc record format.
2
+
3
+ A payload is ``[1-byte dtype code][blosc(zstd, BITSHUFFLE) of the compact
4
+ (3, n_d) integer array]`` -- byte-identical to one ``img_<i>/diag_<d>.col``
5
+ file of the research dir store.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from functools import lru_cache
10
+
11
+ import blosc
12
+ import numpy as np
13
+
14
+ DTYPE_CODES = {
15
+ "int8": 0, "int16": 1, "int32": 2, "int64": 3,
16
+ "uint8": 4, "uint16": 5, "uint32": 6, "uint64": 7,
17
+ }
18
+ CODE_TO_DTYPE = {v: k for k, v in DTYPE_CODES.items()}
19
+
20
+
21
+ @lru_cache(maxsize=64)
22
+ def diag_support_index(image_size: int, block_size: int) -> tuple[np.ndarray, ...]:
23
+ """Flat (C-raster) positions of each anti-diagonal's support in an ``S x S`` plane.
24
+
25
+ Position ``(i, j)`` belongs to anti-diagonal ``(i % N) + (j % N)``. Entry
26
+ ``d`` of the result gathers and scatters diagonal ``d`` with the same index,
27
+ so a compact band round-trips to its dense positions exactly.
28
+ """
29
+ S, N = image_size, block_size
30
+ within = np.arange(S) % N
31
+ antidiag = (within[:, None] + within[None, :]).ravel()
32
+ return tuple(np.flatnonzero(antidiag == d) for d in range(2 * N - 1))
33
+
34
+
35
+ def diag_support_sizes(image_size: int, block_size: int) -> tuple[int, ...]:
36
+ return tuple(int(ix.size) for ix in diag_support_index(image_size, block_size))
37
+
38
+
39
+ def choose_int_dtype(min_val: int, max_val: int) -> np.dtype:
40
+ if min_val >= 0:
41
+ for t in (np.uint8, np.uint16, np.uint32):
42
+ if max_val <= np.iinfo(t).max:
43
+ return np.dtype(t)
44
+ return np.dtype(np.uint64)
45
+ for t in (np.int8, np.int16, np.int32):
46
+ if min_val >= np.iinfo(t).min and max_val <= np.iinfo(t).max:
47
+ return np.dtype(t)
48
+ return np.dtype(np.int64)
49
+
50
+
51
+ def encode_blob(arr: np.ndarray, clevel: int) -> tuple[bytes, np.dtype]:
52
+ """Narrow an int64 array to the smallest dtype holding its range and blosc-compress it."""
53
+ arr = arr if arr.dtype == np.int64 else arr.astype(np.int64)
54
+ dtype = choose_int_dtype(int(arr.min()), int(arr.max()))
55
+ arr = np.ascontiguousarray(arr.astype(dtype, copy=False))
56
+ blob = blosc.compress(
57
+ arr.tobytes(),
58
+ typesize=arr.itemsize,
59
+ cname="zstd",
60
+ clevel=int(clevel),
61
+ shuffle=blosc.BITSHUFFLE,
62
+ )
63
+ return blob, dtype
64
+
65
+
66
+ def decode_blob(blob: bytes, dtype: str | np.dtype, shape: tuple[int, ...]) -> np.ndarray:
67
+ raw = blosc.decompress(blob)
68
+ return np.frombuffer(raw, dtype=np.dtype(dtype)).reshape(shape).astype(np.int64)
69
+
70
+
71
+ def encode_payload(compact: np.ndarray, clevel: int) -> bytes:
72
+ """``(3, n_d)`` int64 -> ``[dtype code][blob]`` (the dir-store file content)."""
73
+ blob, dtype = encode_blob(compact, clevel)
74
+ return bytes((DTYPE_CODES[dtype.name],)) + blob
75
+
76
+
77
+ def decode_payload(payload: bytes, n_d: int) -> np.ndarray:
78
+ """``[dtype code][blob]`` -> ``(3, n_d)`` int64."""
79
+ if len(payload) < 2:
80
+ raise ValueError("payload too short")
81
+ code = payload[0]
82
+ if code not in CODE_TO_DTYPE:
83
+ raise ValueError(f"unknown dtype code {code}")
84
+ return decode_blob(payload[1:], CODE_TO_DTYPE[code], (3, n_d))
85
+
86
+
87
+ def compact_to_dense(compact: np.ndarray, index: np.ndarray, image_size: int) -> np.ndarray:
88
+ dense = np.zeros((3, image_size * image_size), dtype=compact.dtype)
89
+ dense[:, index] = compact
90
+ return dense.reshape(3, image_size, image_size)
91
+
92
+
93
+ def dense_to_compact(dense: np.ndarray, index: np.ndarray) -> np.ndarray:
94
+ return np.ascontiguousarray(dense.reshape(3, -1)[:, index])
imagecalculator/cli.py ADDED
@@ -0,0 +1,128 @@
1
+ """``imagecalculator compress | decompress | info``."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from . import __version__
10
+ from .codec import EncodedImage, compress, decompress
11
+
12
+
13
+ def _parse_quantization(text: str | None):
14
+ if text is None:
15
+ return None
16
+ text = text.strip()
17
+ if text.startswith("["):
18
+ return json.loads(text)
19
+ return int(text)
20
+
21
+
22
+ def _parse_keep(text: str):
23
+ if text == "all":
24
+ return "all"
25
+ if "." in text or text.endswith("%"):
26
+ v = float(text.rstrip("%"))
27
+ return v / 100.0 if text.endswith("%") else v
28
+ return int(text)
29
+
30
+
31
+ def _fmt_bytes(n: int) -> str:
32
+ return f"{n / 1024:.1f} KiB" if n >= 1024 else f"{n} B"
33
+
34
+
35
+ def cmd_compress(a: argparse.Namespace) -> int:
36
+ enc = compress(
37
+ a.image, keep=_parse_keep(a.keep), quantization=_parse_quantization(a.quantization),
38
+ block_size=a.block_size, image_size=a.image_size, use_ycrcb=not a.no_ycrcb,
39
+ subsampling=a.subsampling, clevel=a.clevel, device=a.device,
40
+ )
41
+ out = Path(a.output) if a.output else Path(a.image).with_suffix(".icf")
42
+ enc.save(out)
43
+ _print_info(enc, original=a.image, provider=a.provider, detail=a.detail)
44
+ print(f"wrote {out}")
45
+ return 0
46
+
47
+
48
+ def cmd_decompress(a: argparse.Namespace) -> int:
49
+ size: str | int = a.size if a.size in ("native", "full") else int(a.size)
50
+ img = decompress(a.icf, size=size, device=a.device)
51
+ out = Path(a.output) if a.output else Path(a.icf).with_suffix(".png")
52
+ img.save(out)
53
+ print(f"wrote {out} ({img.size[0]}x{img.size[1]})")
54
+ return 0
55
+
56
+
57
+ def _print_info(enc: EncodedImage, original: str | None = None, provider: str = "anthropic", detail: str | None = None) -> None:
58
+ c = enc.config
59
+ sv = enc.savings(original, provider, detail)
60
+ q = c.quantization if c.is_uniform else f"schedule(min={c.quantization_factor})"
61
+ print(f"config: block {c.block_size}, image {c.image_size}, ycrcb={c.use_ycrcb}, subsampling={c.subsampling}, q={q}, clevel={c.clevel}")
62
+ print(f"keep: {enc.keep_k}/{c.max_diag} diagonals -> native decode {enc.pixels[0]}x{enc.pixels[1]} px")
63
+ t = sv["tokens"]
64
+ print(f"tokens: {t['reduced']} (full image {t['full_image']}, {t['provider']} estimate, {t['ratio']:.1f}x fewer)")
65
+ b = sv["bytes"]
66
+ if b["original_file"]:
67
+ print(f"bytes over the air: {_fmt_bytes(b['over_the_air'])} (original file {_fmt_bytes(b['original_file'])}, {b['ratio']:.1f}x smaller)")
68
+ else:
69
+ print(f"bytes over the air: {_fmt_bytes(b['over_the_air'])}")
70
+ if enc.transform_label:
71
+ print(f"transform: {enc.transform_label} (not research-store compatible)")
72
+
73
+
74
+ def cmd_info(a: argparse.Namespace) -> int:
75
+ enc = EncodedImage.load(a.icf)
76
+ _print_info(enc, original=a.original, provider=a.provider, detail=a.detail)
77
+ if a.json:
78
+ print(json.dumps(enc.savings(a.original, a.provider, a.detail), indent=2))
79
+ return 0
80
+
81
+
82
+ def _add_token_args(p: argparse.ArgumentParser) -> None:
83
+ p.add_argument("--provider", default="anthropic", help="token estimate: anthropic | openai[:model] | google[:model]")
84
+ p.add_argument("--detail", default=None, help="OpenAI detail or Gemini media_resolution (default: the API default)")
85
+
86
+
87
+ def build_parser() -> argparse.ArgumentParser:
88
+ p = argparse.ArgumentParser(prog="imagecalculator", description="Progressive block-DCT image codec")
89
+ p.add_argument("--version", action="version", version=f"imagecalculator {__version__}")
90
+ sub = p.add_subparsers(dest="cmd", required=True)
91
+
92
+ c = sub.add_parser("compress", help="truncate + quantize an image into an .icf blob")
93
+ c.add_argument("image")
94
+ c.add_argument("--keep", default="all", help="diagonals to keep: int, fraction (0.25) or percent (25%%), or 'all'")
95
+ c.add_argument("--quantization", "-q", default=None, help="scalar step, or JSON bands [[from,to,q],...]")
96
+ c.add_argument("--block-size", type=int, default=32)
97
+ c.add_argument("--image-size", type=int, default=448)
98
+ c.add_argument("--no-ycrcb", action="store_true")
99
+ c.add_argument("--subsampling", default="1x1", choices=["1x1", "2x2-r", "2x2-c", "4x4"])
100
+ c.add_argument("--clevel", type=int, default=9)
101
+ c.add_argument("--device", default="cpu")
102
+ c.add_argument("-o", "--output")
103
+ _add_token_args(c)
104
+ c.set_defaults(fn=cmd_compress)
105
+
106
+ d = sub.add_parser("decompress", help="decode an .icf blob to PNG")
107
+ d.add_argument("icf")
108
+ d.add_argument("--size", default="native", help="native | full | <side px>")
109
+ d.add_argument("--device", default="cpu")
110
+ d.add_argument("-o", "--output")
111
+ d.set_defaults(fn=cmd_decompress)
112
+
113
+ i = sub.add_parser("info", help="print config, keep, tokens and bytes")
114
+ i.add_argument("icf")
115
+ i.add_argument("--original", help="original image file for the byte baseline")
116
+ i.add_argument("--json", action="store_true")
117
+ _add_token_args(i)
118
+ i.set_defaults(fn=cmd_info)
119
+ return p
120
+
121
+
122
+ def main(argv: list[str] | None = None) -> int:
123
+ a = build_parser().parse_args(argv)
124
+ return a.fn(a)
125
+
126
+
127
+ if __name__ == "__main__":
128
+ sys.exit(main())
@@ -0,0 +1,331 @@
1
+ """Encoder / EncodedImage / decode: the compress-for-the-model path.
2
+
3
+ Pipeline (bit-exact with the research dir-store builder when no ``transform``
4
+ is used): load -> resize -> [YCrCb -> chroma subsample] -> block DCT ->
5
+ ``round(coeff / q[d])`` -> keep the first ``k`` anti-diagonals -> one compact
6
+ blosc payload per diagonal. Decoding runs an ``m``-point inverse DCT on each
7
+ block's low corner, so ``k`` diagonals come back as an ``S*k/N`` px image.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import struct
14
+ from dataclasses import dataclass, field
15
+ from pathlib import Path
16
+ from typing import Sequence
17
+
18
+ import numpy as np
19
+ import torch
20
+ from PIL import Image
21
+ from torch import Tensor
22
+
23
+ from . import tokens as _tokens
24
+ from .bands import decode_payload, diag_support_index, encode_payload
25
+ from .config import CodecConfig, quantization_bands
26
+ from .image import ImageLike, load_image, to_pil
27
+ from .policy import DiagonalPolicy, ResolvedPolicy
28
+ from .transform import deblockify, forward_plane, inverse_plane
29
+
30
+ MAGIC = b"ICF1"
31
+ FORMAT_VERSION = 1
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # EncodedImage
36
+ # ---------------------------------------------------------------------------
37
+
38
+ @dataclass
39
+ class EncodedImage:
40
+ """One image's kept diagonals as compressed payloads.
41
+
42
+ ``payloads[d]`` is byte-identical to ``img_<i>/diag_<d>.col`` of a research
43
+ dir store built with the same config (``transform=None``).
44
+ """
45
+
46
+ config: CodecConfig
47
+ payloads: list[bytes]
48
+ transform_label: str | None = None
49
+ _support: tuple[int, ...] = field(default_factory=tuple, repr=False, compare=False)
50
+
51
+ def __post_init__(self) -> None:
52
+ if not self.payloads:
53
+ raise ValueError("an EncodedImage needs at least one diagonal")
54
+ if len(self.payloads) > self.config.max_diag:
55
+ raise ValueError(f"{len(self.payloads)} payloads but max_diag is {self.config.max_diag}")
56
+ if not self._support:
57
+ self._support = tuple(int(ix.size) for ix in diag_support_index(self.config.image_size, self.config.block_size))
58
+
59
+ # -- geometry ------------------------------------------------------------
60
+ @property
61
+ def keep_k(self) -> int:
62
+ return len(self.payloads)
63
+
64
+ @property
65
+ def is_truncated(self) -> bool:
66
+ return self.keep_k < self.config.max_diag
67
+
68
+ @property
69
+ def native_m(self) -> int:
70
+ """Output block size of the native decode: the smallest corner holding the kept triangle."""
71
+ return min(self.keep_k, self.config.block_size)
72
+
73
+ @property
74
+ def native_side(self) -> int:
75
+ return self.config.image_size * self.native_m // self.config.block_size
76
+
77
+ @property
78
+ def pixels(self) -> tuple[int, int]:
79
+ """``(width, height)`` of the native-size decode (tier 1: what the model sees)."""
80
+ return (self.native_side, self.native_side)
81
+
82
+ def valid_sizes(self) -> list[int]:
83
+ S, N = self.config.image_size, self.config.block_size
84
+ return [S * m // N for m in range(self.native_m, N + 1)]
85
+
86
+ # -- bytes ---------------------------------------------------------------
87
+ @property
88
+ def payload_bytes(self) -> int:
89
+ return sum(len(p) for p in self.payloads)
90
+
91
+ @property
92
+ def nbytes(self) -> int:
93
+ """Bytes over the air: the ``.icf`` container (header + kept payloads) (tier 2)."""
94
+ return len(self.to_bytes())
95
+
96
+ def cumulative_bytes(self) -> np.ndarray:
97
+ """int64 ``(keep_k,)``: payload bytes through each kept diagonal."""
98
+ return np.cumsum([len(p) for p in self.payloads], dtype=np.int64)
99
+
100
+ # -- tokens --------------------------------------------------------------
101
+ def token_estimate(self, provider: str = "anthropic", detail: str | None = None) -> _tokens.TokenEstimate:
102
+ """``provider`` is ``"anthropic"``, ``"openai[:model]"`` or ``"google[:model]"``."""
103
+ w, h = self.pixels
104
+ return _tokens.estimate(w, h, provider, detail)
105
+
106
+ def savings(self, original: str | Path | int | None = None, provider: str = "anthropic",
107
+ detail: str | None = None) -> dict:
108
+ """Tier 1 and tier 2 numbers next to their full-image baselines."""
109
+ S = self.config.image_size
110
+ full = _tokens.estimate(S, S, provider, detail)
111
+ red = self.token_estimate(provider, detail)
112
+ if isinstance(original, (str, Path)):
113
+ orig_bytes: int | None = os.path.getsize(original)
114
+ elif isinstance(original, int):
115
+ orig_bytes = original
116
+ else:
117
+ orig_bytes = None
118
+ out = {
119
+ "tokens": {"reduced": red.tokens, "full_image": full.tokens, "provider": provider},
120
+ "pixels": {"reduced": self.pixels, "full_image": (S, S)},
121
+ "bytes": {"over_the_air": self.nbytes, "original_file": orig_bytes},
122
+ "keep": self.keep_k,
123
+ "max_diag": self.config.max_diag,
124
+ }
125
+ if orig_bytes:
126
+ out["bytes"]["ratio"] = orig_bytes / self.nbytes
127
+ out["tokens"]["ratio"] = full.tokens / red.tokens
128
+ return out
129
+
130
+ # -- coefficients --------------------------------------------------------
131
+ def coefficients(self, d: int) -> np.ndarray:
132
+ """Quantized integer coefficients of diagonal ``d`` as ``(3, n_d)`` int64."""
133
+ if not (0 <= d < self.keep_k):
134
+ raise IndexError(f"diagonal {d} not kept (keep_k={self.keep_k})")
135
+ return decode_payload(self.payloads[d], self._support[d])
136
+
137
+ def dense_plane(self) -> np.ndarray:
138
+ """Dequantized coefficients scattered into a dense ``(3, S, S)`` float32 plane (research reader arithmetic)."""
139
+ S = self.config.image_size
140
+ idx = diag_support_index(S, self.config.block_size)
141
+ q = self.config.q_vector
142
+ acc = np.zeros((3, S * S), dtype=np.float32)
143
+ for d in range(self.keep_k):
144
+ vals = self.coefficients(d)
145
+ acc[:, idx[d]] += vals.astype(np.float32) * float(q[d])
146
+ return acc.reshape(3, S, S)
147
+
148
+ def truncate(self, k: int) -> "EncodedImage":
149
+ if not (1 <= k <= self.keep_k):
150
+ raise ValueError(f"k must be in 1..{self.keep_k}; got {k}")
151
+ return EncodedImage(self.config, list(self.payloads[:k]), self.transform_label, self._support)
152
+
153
+ # -- container -----------------------------------------------------------
154
+ def header(self) -> dict:
155
+ h = self.config.to_dict()
156
+ h.update({
157
+ "format_version": FORMAT_VERSION,
158
+ "keep": self.keep_k,
159
+ "lengths": [len(p) for p in self.payloads],
160
+ })
161
+ if self.transform_label is not None:
162
+ h["transform"] = self.transform_label
163
+ return h
164
+
165
+ def to_bytes(self) -> bytes:
166
+ hdr = json.dumps(self.header(), separators=(",", ":")).encode("utf-8")
167
+ return b"".join([MAGIC, struct.pack("<I", len(hdr)), hdr, *self.payloads])
168
+
169
+ @classmethod
170
+ def from_bytes(cls, data: bytes) -> "EncodedImage":
171
+ if data[:4] != MAGIC:
172
+ raise ValueError("not an .icf blob (bad magic)")
173
+ (hlen,) = struct.unpack("<I", data[4:8])
174
+ hdr = json.loads(data[8:8 + hlen].decode("utf-8"))
175
+ config = CodecConfig.from_dict(hdr)
176
+ lengths = [int(v) for v in hdr["lengths"]]
177
+ if len(lengths) != int(hdr["keep"]):
178
+ raise ValueError("corrupt header: keep != len(lengths)")
179
+ pos = 8 + hlen
180
+ payloads = []
181
+ for n in lengths:
182
+ payloads.append(data[pos:pos + n])
183
+ pos += n
184
+ if pos != len(data):
185
+ raise ValueError(f"container length mismatch: expected {pos} bytes, got {len(data)}")
186
+ return cls(config, payloads, hdr.get("transform"))
187
+
188
+ def save(self, path: str | Path) -> None:
189
+ Path(path).write_bytes(self.to_bytes())
190
+
191
+ @classmethod
192
+ def load(cls, path: str | Path) -> "EncodedImage":
193
+ return cls.from_bytes(Path(path).read_bytes())
194
+
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # Encoder
198
+ # ---------------------------------------------------------------------------
199
+
200
+ class Encoder:
201
+ """Encode images under one :class:`CodecConfig` on one device."""
202
+
203
+ def __init__(self, config: CodecConfig, device: torch.device | str = "cpu"):
204
+ self.config = config
205
+ self.device = torch.device(device)
206
+ S, N = config.image_size, config.block_size
207
+ self._support = diag_support_index(S, N)
208
+ # per-position diagonal index of the dense S x S plane
209
+ within = np.arange(S) % N
210
+ self._diag_of_pos = torch.from_numpy((within[:, None] + within[None, :]).ravel()).to(self.device)
211
+
212
+ def _plane(self, image: ImageLike) -> Tensor:
213
+ """Un-quantized dense coefficient plane ``(3, S*S)`` float32 on the device."""
214
+ cfg = self.config
215
+ img = load_image(image, cfg.image_size, self.device)
216
+ blocks = forward_plane(img, cfg.block_size, cfg.use_ycrcb, cfg.subsampling)
217
+ return deblockify(blocks, (cfg.image_size, cfg.image_size)).reshape(3, -1)
218
+
219
+ def _quantize(self, plane: Tensor, policy: ResolvedPolicy) -> np.ndarray:
220
+ q_pos = torch.from_numpy(policy.q_vector).to(self.device)[self._diag_of_pos]
221
+ if policy.transform is not None:
222
+ plane = plane.clone()
223
+ for d in range(policy.keep_k):
224
+ ix = torch.from_numpy(self._support[d]).to(self.device)
225
+ plane[:, ix] = policy.transform(d, plane[:, ix])
226
+ return torch.round(plane / q_pos).to(torch.int64).cpu().numpy()
227
+
228
+ def encode(self, image: ImageLike, policy: DiagonalPolicy | None = None, transform_label: str | None = None) -> EncodedImage:
229
+ cfg = self.config
230
+ pol = (policy or DiagonalPolicy()).resolve(cfg.max_diag, cfg.q_vector, cfg.clevel)
231
+ plane_q = self._quantize(self._plane(image), pol)
232
+ payloads = [
233
+ encode_payload(np.ascontiguousarray(plane_q[:, self._support[d]]), pol.clevels[d])
234
+ for d in range(pol.keep_k)
235
+ ]
236
+ eff_cfg = cfg
237
+ if not np.array_equal(pol.q_vector, cfg.q_vector):
238
+ qv = [int(v) for v in pol.q_vector]
239
+ quant = qv[0] if len(set(qv)) == 1 else tuple(qv)
240
+ eff_cfg = CodecConfig(cfg.block_size, cfg.image_size, cfg.use_ycrcb, cfg.subsampling, quant, cfg.clevel)
241
+ label = transform_label if transform_label is not None else ("custom" if pol.transform is not None else None)
242
+ return EncodedImage(eff_cfg, payloads, label, tuple(int(ix.size) for ix in self._support))
243
+
244
+ def coefficient_plane(self, image: ImageLike) -> Tensor:
245
+ """Quantized integer coefficient plane ``(1, 3, S, S)`` (config quantization, nothing truncated)."""
246
+ cfg = self.config
247
+ pol = DiagonalPolicy().resolve(cfg.max_diag, cfg.q_vector, cfg.clevel)
248
+ plane_q = self._quantize(self._plane(image), pol)
249
+ return torch.from_numpy(plane_q.reshape(3, cfg.image_size, cfg.image_size)).unsqueeze(0)
250
+
251
+
252
+ # ---------------------------------------------------------------------------
253
+ # Decoding
254
+ # ---------------------------------------------------------------------------
255
+
256
+ def _resolve_m(enc: EncodedImage, size: str | int) -> int:
257
+ S, N = enc.config.image_size, enc.config.block_size
258
+ if size == "native":
259
+ return enc.native_m
260
+ if size == "full":
261
+ return N
262
+ side = int(size)
263
+ if side * N % S != 0:
264
+ raise ValueError(f"size {side} is not S*m/N for an integer m; valid sizes: {enc.valid_sizes()}")
265
+ m = side * N // S
266
+ if not (enc.native_m <= m <= N):
267
+ raise ValueError(f"size {side} is outside the valid range; valid sizes: {enc.valid_sizes()}")
268
+ return m
269
+
270
+
271
+ def decode(encoded: EncodedImage, size: str | int = "native", device: torch.device | str = "cpu") -> Tensor:
272
+ """RGB float32 ``(1, 3, h, w)`` in [0, 255] at ``size`` = ``"native"`` (``S*k/N``), ``"full"`` or an int side."""
273
+ m = _resolve_m(encoded, size)
274
+ plane = torch.from_numpy(encoded.dense_plane()).unsqueeze(0).to(device)
275
+ return inverse_plane(plane, encoded.config.block_size, encoded.config.use_ycrcb, m)
276
+
277
+
278
+ def decompress(encoded: EncodedImage | bytes | str | Path, size: str | int = "native", device: torch.device | str = "cpu") -> Image.Image:
279
+ """Decode to a PIL image (uint8 truncation, as the research reader)."""
280
+ if isinstance(encoded, (bytes, bytearray)):
281
+ encoded = EncodedImage.from_bytes(bytes(encoded))
282
+ elif isinstance(encoded, (str, Path)):
283
+ encoded = EncodedImage.load(encoded)
284
+ return to_pil(decode(encoded, size, device))
285
+
286
+
287
+ # ---------------------------------------------------------------------------
288
+ # One-call API
289
+ # ---------------------------------------------------------------------------
290
+
291
+ def _as_quantization(quantization, block_size: int):
292
+ if quantization is None:
293
+ return 1
294
+ if isinstance(quantization, (int, np.integer)):
295
+ return int(quantization)
296
+ seq = list(quantization)
297
+ if seq and isinstance(seq[0], (list, tuple)):
298
+ return quantization_bands(seq, block_size)
299
+ return tuple(int(v) for v in seq)
300
+
301
+
302
+ def compress(
303
+ image: ImageLike,
304
+ *,
305
+ keep: int | float | str = "all",
306
+ quantization: int | Sequence[int] | Sequence[Sequence[int]] | None = None,
307
+ block_size: int = 32,
308
+ image_size: int = 448,
309
+ use_ycrcb: bool = True,
310
+ subsampling: str = "1x1",
311
+ clevel: int = 9,
312
+ policy: DiagonalPolicy | None = None,
313
+ device: torch.device | str = "cpu",
314
+ ) -> EncodedImage:
315
+ """Truncate to ``keep`` diagonals, quantize per diagonal, and pack the compact payloads.
316
+
317
+ ``keep`` sets the pixel/token budget (tier 1). ``quantization``, ``use_ycrcb``,
318
+ ``subsampling`` and ``clevel`` set bytes per pixel (tier 2). ``policy`` is
319
+ the general form; when given, its ``keep``/``quantization``/``clevel`` win
320
+ over the keyword arguments they override (``None`` fields fall back).
321
+ """
322
+ cfg = CodecConfig(block_size, image_size, use_ycrcb, subsampling, _as_quantization(quantization, block_size), clevel)
323
+ if policy is None:
324
+ policy = DiagonalPolicy(keep=keep)
325
+ elif policy.keep == "all" and keep != "all":
326
+ policy = DiagonalPolicy(keep, policy.quantization, policy.transform, policy.clevel)
327
+ return Encoder(cfg, device).encode(image, policy)
328
+
329
+
330
+ def load(path: str | Path) -> EncodedImage:
331
+ return EncodedImage.load(path)