imagecalculator 0.1.0__tar.gz

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,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ *.icf
8
+ /stores/
9
+ /scratch/
10
+ .venv/
11
+ *.whl
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-11-08)
4
+
5
+ Initial release.
6
+
7
+ - Progressive block-DCT codec: `compress`, `decompress`, `Encoder`, `EncodedImage`, `.icf` container.
8
+ - `keep` truncation to the first `k` anti-diagonals with reduced-native-size decode (`S*k/N` px).
9
+ - Per-diagonal quantization (scalar, vector or bands), YCrCb + chroma subsampling, support-only storage, blosc/zstd payloads.
10
+ - Token estimators for Anthropic, OpenAI and Google image pricing (`token_estimate`, `EncodedImage.savings`).
11
+ - `imagecalculator compress | decompress | info` CLI.
12
+ - Byte-exact compatibility with the research Frequency-Store dir stores when no `transform` is set.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Can Gokmen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.5
2
+ Name: imagecalculator
3
+ Version: 0.1.0
4
+ Summary: Progressive block-DCT image codec: truncate to the first k anti-diagonals, quantize per diagonal, decode at the reduced native size (Image Calculator / Frequency-Store)
5
+ Project-URL: Homepage, https://github.com/cangokmen/imagecalculator
6
+ Project-URL: Repository, https://github.com/cangokmen/imagecalculator
7
+ Project-URL: Issues, https://github.com/cangokmen/imagecalculator/issues
8
+ Author: Can Gokmen
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: dct,image-compression,progressive-codec,tokens,vision-language-models
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Multimedia :: Graphics
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: blosc<2,>=1.10
27
+ Requires-Dist: numpy>=1.24
28
+ Requires-Dist: pillow>=9.0
29
+ Requires-Dist: torch>=2.1
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # imagecalculator
35
+
36
+ A progressive block-DCT image codec for sending images to vision models.
37
+
38
+ **Truncation reduces your token count.** Keep only the first `k` anti-diagonals
39
+ of each DCT block and decode them at the *reduced native size*: the model sees
40
+ fewer pixels, and pixel-priced APIs (Claude, GPT, Gemini) charge fewer image
41
+ tokens. Tokens scale with `(k/N)^2`.
42
+
43
+ **Truncation + the other optimizations reduce your wifi cost.** Per-diagonal
44
+ quantization, YCrCb with chroma subsampling, support-only (zero-free) storage
45
+ and blosc/zstd entropy coding shrink the bytes that leave the device for the
46
+ same pixels.
47
+
48
+ The two levers are independent: `keep` sets the pixel/token budget, the rest
49
+ sets bytes per pixel.
50
+
51
+ ```python
52
+ import imagecalculator as ic
53
+
54
+ enc = ic.compress("photo.jpg", keep=8, quantization=20) # 8 of 63 diagonals (block 32, 448 px)
55
+ enc.pixels # (112, 112) -> what the model will see
56
+ enc.token_estimate() # ~17 tokens on Anthropic (vs ~268 for the full 448 px image)
57
+ enc.nbytes # bytes over the air (.icf container)
58
+ enc.save("photo.icf")
59
+
60
+ img = ic.decompress(enc) # PIL image, 112 x 112, k-point inverse DCT per block
61
+ img = ic.decompress(enc, size=224) # any S*m/N with m >= k
62
+ img = ic.decompress(enc, size="full") # blurry full-size reconstruction (legacy behaviour)
63
+ ```
64
+
65
+ ### Sending it to Claude
66
+
67
+ ```python
68
+ import os, anthropic, imagecalculator as ic
69
+
70
+ enc = ic.compress("photo.jpg", keep=8, quantization=20)
71
+ img = ic.decompress(enc) # receiver side
72
+ client = anthropic.Anthropic()
73
+ r = client.messages.create(
74
+ model="claude-opus-5-5", max_tokens=1024,
75
+ messages=[{"role": "user", "content": [
76
+ {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": ic.png_b64(img)}},
77
+ {"type": "text", "text": "What color is the bus?"},
78
+ ]}],
79
+ )
80
+ print("tokens:", r.usage.input_tokens, "(full image: ~268)") # tier 1: truncation
81
+ print("bytes over wifi:", enc.nbytes, "(original JPEG:", os.path.getsize("photo.jpg"), ")") # tier 2
82
+ ```
83
+
84
+ `examples/compress_for_claude.py` runs the same question at `keep = 4, 8, 16, 32`
85
+ and prints measured `usage.input_tokens` and bytes next to a Lanczos resize of
86
+ the original to the same pixel size.
87
+
88
+ ### Honest framing
89
+
90
+ A `k`-point inverse DCT of the low corner is a near-ideal low-pass filter
91
+ followed by decimation. It carries the **same information as a good resize**
92
+ (Lanczos / area) to that size, not more. What the format adds: one encoding
93
+ serves every size, and bytes are competitive with a JPEG of the resized image.
94
+
95
+ ## Per-diagonal control (the adaptivity knobs)
96
+
97
+ ```python
98
+ # scalar step, a per-diagonal vector, or [[from, to, q], ...] bands tiling [0, max_diag)
99
+ enc = ic.compress("photo.jpg", keep=16, quantization=[[0, 8, 1], [8, 20, 10], [20, 63, 50]])
100
+
101
+ # anything else per diagonal: a hook on the un-rounded (3, n_d) coefficients
102
+ import torch
103
+ def dead_zone(d, coeffs): # drop tiny high-frequency coefficients
104
+ return coeffs if d < 8 else torch.where(coeffs.abs() < 30, torch.zeros_like(coeffs), coeffs)
105
+ pol = ic.DiagonalPolicy(keep=16, quantization=20, transform=dead_zone, clevel=9)
106
+ enc = ic.compress("photo.jpg", policy=pol)
107
+ ```
108
+
109
+ ## CLI
110
+
111
+ ```
112
+ imagecalculator compress photo.jpg --keep 8 --quantization 20 [--block-size 32 --image-size 448] -o photo.icf
113
+ imagecalculator decompress photo.icf [--size native|full|224] -o photo.png
114
+ imagecalculator info photo.icf [--original photo.jpg]
115
+ ```
116
+
117
+ `info` prints the config, then two lines: `tokens: 17 (full image 268, ...)` and
118
+ `bytes over the air: 6.1 KiB (original file 182 KiB, ...)`.
119
+
120
+ `compress` and `info` take `--provider anthropic | openai[:model] | google[:model]`
121
+ and `--detail` (OpenAI `detail`, Gemini `media_resolution`); the same arguments
122
+ go to `token_estimate()` / `EncodedImage.savings()`. Truncation only saves tokens
123
+ where cost scales with pixels: Anthropic, OpenAI patch models (gpt-5.2 and
124
+ later, gpt-4.1-mini; default `openai` = gpt-5.5) and Gemini 2.5 and earlier. Tile-priced OpenAI
125
+ models (gpt-4o, gpt-4.1, gpt-5.1) bill a 448 px image as one 512 px tile, and
126
+ Gemini 3/4 bill a fixed count per image (1120 by default), so `keep` doesn't
127
+ change their token count, only bytes.
128
+
129
+ ## Install
130
+
131
+ ```
132
+ pip install imagecalculator
133
+ ```
134
+
135
+ `pip` pulls in a default torch build; to pick the CPU wheel (much smaller),
136
+ install torch from the PyTorch index first:
137
+
138
+ ```
139
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
140
+ pip install imagecalculator
141
+ ```
142
+
143
+ From a checkout: `pip install -e ".[dev]"` and `OMP_NUM_THREADS=4 pytest -q`.
144
+
145
+ Dependencies: numpy, torch, blosc (1.x), Pillow.
146
+
147
+ On a shared machine with many cores, set `OMP_NUM_THREADS` (or
148
+ `torch.set_num_threads`) to a small number: the codec's tensors are small, and
149
+ 32 contending OpenMP threads turn a 2 ms encode into seconds.
150
+
151
+ `quantization=1` stores the DCT coefficients as rounded integers (the
152
+ research "q1" stores); a full decode is within 2 levels of the input (one from
153
+ coefficient rounding, one from uint8 truncation), not bit-exact in pixel space.
154
+
155
+ ## Compatibility with the research Frequency-Store
156
+
157
+ With no `transform`, `enc.payloads[d]` is byte-identical to the file
158
+ `img_<i>/diag_<d>.col` of a research dir store built with the same config, and
159
+ `decompress(enc, size="full")` reproduces the research reader's reconstruction
160
+ exactly.
@@ -0,0 +1,127 @@
1
+ # imagecalculator
2
+
3
+ A progressive block-DCT image codec for sending images to vision models.
4
+
5
+ **Truncation reduces your token count.** Keep only the first `k` anti-diagonals
6
+ of each DCT block and decode them at the *reduced native size*: the model sees
7
+ fewer pixels, and pixel-priced APIs (Claude, GPT, Gemini) charge fewer image
8
+ tokens. Tokens scale with `(k/N)^2`.
9
+
10
+ **Truncation + the other optimizations reduce your wifi cost.** Per-diagonal
11
+ quantization, YCrCb with chroma subsampling, support-only (zero-free) storage
12
+ and blosc/zstd entropy coding shrink the bytes that leave the device for the
13
+ same pixels.
14
+
15
+ The two levers are independent: `keep` sets the pixel/token budget, the rest
16
+ sets bytes per pixel.
17
+
18
+ ```python
19
+ import imagecalculator as ic
20
+
21
+ enc = ic.compress("photo.jpg", keep=8, quantization=20) # 8 of 63 diagonals (block 32, 448 px)
22
+ enc.pixels # (112, 112) -> what the model will see
23
+ enc.token_estimate() # ~17 tokens on Anthropic (vs ~268 for the full 448 px image)
24
+ enc.nbytes # bytes over the air (.icf container)
25
+ enc.save("photo.icf")
26
+
27
+ img = ic.decompress(enc) # PIL image, 112 x 112, k-point inverse DCT per block
28
+ img = ic.decompress(enc, size=224) # any S*m/N with m >= k
29
+ img = ic.decompress(enc, size="full") # blurry full-size reconstruction (legacy behaviour)
30
+ ```
31
+
32
+ ### Sending it to Claude
33
+
34
+ ```python
35
+ import os, anthropic, imagecalculator as ic
36
+
37
+ enc = ic.compress("photo.jpg", keep=8, quantization=20)
38
+ img = ic.decompress(enc) # receiver side
39
+ client = anthropic.Anthropic()
40
+ r = client.messages.create(
41
+ model="claude-opus-5-5", max_tokens=1024,
42
+ messages=[{"role": "user", "content": [
43
+ {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": ic.png_b64(img)}},
44
+ {"type": "text", "text": "What color is the bus?"},
45
+ ]}],
46
+ )
47
+ print("tokens:", r.usage.input_tokens, "(full image: ~268)") # tier 1: truncation
48
+ print("bytes over wifi:", enc.nbytes, "(original JPEG:", os.path.getsize("photo.jpg"), ")") # tier 2
49
+ ```
50
+
51
+ `examples/compress_for_claude.py` runs the same question at `keep = 4, 8, 16, 32`
52
+ and prints measured `usage.input_tokens` and bytes next to a Lanczos resize of
53
+ the original to the same pixel size.
54
+
55
+ ### Honest framing
56
+
57
+ A `k`-point inverse DCT of the low corner is a near-ideal low-pass filter
58
+ followed by decimation. It carries the **same information as a good resize**
59
+ (Lanczos / area) to that size, not more. What the format adds: one encoding
60
+ serves every size, and bytes are competitive with a JPEG of the resized image.
61
+
62
+ ## Per-diagonal control (the adaptivity knobs)
63
+
64
+ ```python
65
+ # scalar step, a per-diagonal vector, or [[from, to, q], ...] bands tiling [0, max_diag)
66
+ enc = ic.compress("photo.jpg", keep=16, quantization=[[0, 8, 1], [8, 20, 10], [20, 63, 50]])
67
+
68
+ # anything else per diagonal: a hook on the un-rounded (3, n_d) coefficients
69
+ import torch
70
+ def dead_zone(d, coeffs): # drop tiny high-frequency coefficients
71
+ return coeffs if d < 8 else torch.where(coeffs.abs() < 30, torch.zeros_like(coeffs), coeffs)
72
+ pol = ic.DiagonalPolicy(keep=16, quantization=20, transform=dead_zone, clevel=9)
73
+ enc = ic.compress("photo.jpg", policy=pol)
74
+ ```
75
+
76
+ ## CLI
77
+
78
+ ```
79
+ imagecalculator compress photo.jpg --keep 8 --quantization 20 [--block-size 32 --image-size 448] -o photo.icf
80
+ imagecalculator decompress photo.icf [--size native|full|224] -o photo.png
81
+ imagecalculator info photo.icf [--original photo.jpg]
82
+ ```
83
+
84
+ `info` prints the config, then two lines: `tokens: 17 (full image 268, ...)` and
85
+ `bytes over the air: 6.1 KiB (original file 182 KiB, ...)`.
86
+
87
+ `compress` and `info` take `--provider anthropic | openai[:model] | google[:model]`
88
+ and `--detail` (OpenAI `detail`, Gemini `media_resolution`); the same arguments
89
+ go to `token_estimate()` / `EncodedImage.savings()`. Truncation only saves tokens
90
+ where cost scales with pixels: Anthropic, OpenAI patch models (gpt-5.2 and
91
+ later, gpt-4.1-mini; default `openai` = gpt-5.5) and Gemini 2.5 and earlier. Tile-priced OpenAI
92
+ models (gpt-4o, gpt-4.1, gpt-5.1) bill a 448 px image as one 512 px tile, and
93
+ Gemini 3/4 bill a fixed count per image (1120 by default), so `keep` doesn't
94
+ change their token count, only bytes.
95
+
96
+ ## Install
97
+
98
+ ```
99
+ pip install imagecalculator
100
+ ```
101
+
102
+ `pip` pulls in a default torch build; to pick the CPU wheel (much smaller),
103
+ install torch from the PyTorch index first:
104
+
105
+ ```
106
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
107
+ pip install imagecalculator
108
+ ```
109
+
110
+ From a checkout: `pip install -e ".[dev]"` and `OMP_NUM_THREADS=4 pytest -q`.
111
+
112
+ Dependencies: numpy, torch, blosc (1.x), Pillow.
113
+
114
+ On a shared machine with many cores, set `OMP_NUM_THREADS` (or
115
+ `torch.set_num_threads`) to a small number: the codec's tensors are small, and
116
+ 32 contending OpenMP threads turn a 2 ms encode into seconds.
117
+
118
+ `quantization=1` stores the DCT coefficients as rounded integers (the
119
+ research "q1" stores); a full decode is within 2 levels of the input (one from
120
+ coefficient rounding, one from uint8 truncation), not bit-exact in pixel space.
121
+
122
+ ## Compatibility with the research Frequency-Store
123
+
124
+ With no `transform`, `enc.payloads[d]` is byte-identical to the file
125
+ `img_<i>/diag_<d>.col` of a research dir store built with the same config, and
126
+ `decompress(enc, size="full")` reproduces the research reader's reconstruction
127
+ exactly.
@@ -0,0 +1,81 @@
1
+ """Tier 1 / tier 2 demo: ask Claude the same question at several `keep` values.
2
+
3
+ export ANTHROPIC_API_KEY=... (or `ant auth login`)
4
+ python examples/compress_for_claude.py photo.jpg "What color is the bus?" --keep 4 8 16 32
5
+
6
+ Prints, per `keep`: pixels, measured input tokens, bytes over the air, and the
7
+ same for a Lanczos resize of the original to the same pixel size encoded as
8
+ JPEG at the quality closest in bytes -- so the token claim (tier 1) and the
9
+ byte claim (tier 2) are measured, not asserted.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import io
15
+ import os
16
+
17
+ import anthropic
18
+ from PIL import Image
19
+
20
+ import imagecalculator as ic
21
+
22
+ MODEL = "claude-opus-5-5"
23
+
24
+
25
+ def ask(client: anthropic.Anthropic, img: Image.Image, question: str) -> tuple[str, int]:
26
+ r = client.messages.create(
27
+ model=MODEL, max_tokens=512, output_config={"effort": "low"},
28
+ messages=[{"role": "user", "content": [
29
+ {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": ic.png_b64(img)}},
30
+ {"type": "text", "text": question},
31
+ ]}],
32
+ )
33
+ if r.stop_reason == "refusal":
34
+ raise RuntimeError(f"refused: {r.stop_details}")
35
+ text = next((b.text for b in r.content if b.type == "text"), "")
36
+ return text.strip(), r.usage.input_tokens
37
+
38
+
39
+ def jpeg_at_bytes(img: Image.Image, target: int) -> tuple[bytes, int]:
40
+ """Smallest-quality JPEG whose size is >= target (the fair byte competitor)."""
41
+ best = None
42
+ for q in range(5, 96, 5):
43
+ buf = io.BytesIO()
44
+ img.save(buf, format="JPEG", quality=q)
45
+ b = buf.getvalue()
46
+ best = (b, q)
47
+ if len(b) >= target:
48
+ break
49
+ return best # type: ignore[return-value]
50
+
51
+
52
+ def main() -> None:
53
+ ap = argparse.ArgumentParser()
54
+ ap.add_argument("image")
55
+ ap.add_argument("question")
56
+ ap.add_argument("--keep", type=int, nargs="+", default=[4, 8, 16, 32])
57
+ ap.add_argument("--quantization", type=int, default=20)
58
+ ap.add_argument("--block-size", type=int, default=32)
59
+ ap.add_argument("--image-size", type=int, default=448)
60
+ a = ap.parse_args()
61
+
62
+ client = anthropic.Anthropic()
63
+ original = Image.open(a.image).convert("RGB")
64
+ orig_bytes = os.path.getsize(a.image)
65
+ full = original.resize((a.image_size, a.image_size), Image.BILINEAR)
66
+ ans, toks = ask(client, full, a.question)
67
+ print(f"full {a.image_size}px: tokens={toks:4d} bytes={orig_bytes:7d} -> {ans!r}")
68
+ print()
69
+ print(f"{'keep':>4} {'px':>4} | {'IC tokens':>9} {'IC bytes':>8} | {'resize tokens':>13} {'JPEG bytes':>10} q | answers")
70
+ for k in a.keep:
71
+ enc = ic.compress(a.image, keep=k, quantization=a.quantization, block_size=a.block_size, image_size=a.image_size)
72
+ img = ic.decompress(enc)
73
+ ans_ic, tok_ic = ask(client, img, a.question)
74
+ small = original.resize(enc.pixels, Image.LANCZOS)
75
+ jpg, q = jpeg_at_bytes(small, enc.nbytes)
76
+ ans_rs, tok_rs = ask(client, Image.open(io.BytesIO(jpg)), a.question)
77
+ print(f"{k:>4} {enc.pixels[0]:>4} | {tok_ic:>9} {enc.nbytes:>8} | {tok_rs:>13} {len(jpg):>10} {q:>2} | {ans_ic!r} / {ans_rs!r}")
78
+
79
+
80
+ if __name__ == "__main__":
81
+ main()
@@ -0,0 +1,66 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "imagecalculator"
7
+ dynamic = ["version"]
8
+ description = "Progressive block-DCT image codec: truncate to the first k anti-diagonals, quantize per diagonal, decode at the reduced native size (Image Calculator / Frequency-Store)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Can Gokmen" }]
13
+ keywords = ["image-compression", "dct", "vision-language-models", "tokens", "progressive-codec"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Science/Research",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Multimedia :: Graphics",
25
+ "Topic :: Scientific/Engineering :: Image Processing",
26
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
27
+ "Typing :: Typed",
28
+ ]
29
+ dependencies = [
30
+ "numpy>=1.24",
31
+ "torch>=2.1",
32
+ "blosc>=1.10,<2",
33
+ "pillow>=9.0",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ dev = ["pytest>=7"]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/cangokmen/imagecalculator"
41
+ Repository = "https://github.com/cangokmen/imagecalculator"
42
+ Issues = "https://github.com/cangokmen/imagecalculator/issues"
43
+
44
+ [project.scripts]
45
+ imagecalculator = "imagecalculator.cli:main"
46
+
47
+ [tool.hatch.version]
48
+ path = "src/imagecalculator/_version.py"
49
+
50
+ [tool.hatch.build.targets.wheel]
51
+ packages = ["src/imagecalculator"]
52
+
53
+ [tool.hatch.build.targets.sdist]
54
+ include = [
55
+ "src/imagecalculator",
56
+ "tests",
57
+ "examples",
58
+ "README.md",
59
+ "LICENSE",
60
+ "CHANGELOG.md",
61
+ "pyproject.toml",
62
+ ]
63
+ exclude = ["**/__pycache__"]
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
@@ -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])