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.
- imagecalculator-0.1.0/.gitignore +11 -0
- imagecalculator-0.1.0/CHANGELOG.md +12 -0
- imagecalculator-0.1.0/LICENSE +21 -0
- imagecalculator-0.1.0/PKG-INFO +160 -0
- imagecalculator-0.1.0/README.md +127 -0
- imagecalculator-0.1.0/examples/compress_for_claude.py +81 -0
- imagecalculator-0.1.0/pyproject.toml +66 -0
- imagecalculator-0.1.0/src/imagecalculator/__init__.py +27 -0
- imagecalculator-0.1.0/src/imagecalculator/_version.py +1 -0
- imagecalculator-0.1.0/src/imagecalculator/bands.py +94 -0
- imagecalculator-0.1.0/src/imagecalculator/cli.py +128 -0
- imagecalculator-0.1.0/src/imagecalculator/codec.py +331 -0
- imagecalculator-0.1.0/src/imagecalculator/config.py +163 -0
- imagecalculator-0.1.0/src/imagecalculator/image.py +69 -0
- imagecalculator-0.1.0/src/imagecalculator/policy.py +91 -0
- imagecalculator-0.1.0/src/imagecalculator/py.typed +0 -0
- imagecalculator-0.1.0/src/imagecalculator/tokens.py +175 -0
- imagecalculator-0.1.0/src/imagecalculator/transform.py +212 -0
- imagecalculator-0.1.0/tests/conftest.py +38 -0
- imagecalculator-0.1.0/tests/test_bands.py +48 -0
- imagecalculator-0.1.0/tests/test_cli.py +20 -0
- imagecalculator-0.1.0/tests/test_codec.py +135 -0
- imagecalculator-0.1.0/tests/test_config.py +44 -0
- imagecalculator-0.1.0/tests/test_policy.py +28 -0
- imagecalculator-0.1.0/tests/test_tokens.py +60 -0
- imagecalculator-0.1.0/tests/test_transform.py +100 -0
|
@@ -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])
|