User-2468 commited on
Commit
1a9eb8a
·
verified ·
1 Parent(s): 45011cd

Release v3.0: selected sub-4M semantic colorizer, inference and verified ONNX

Browse files
DEPLOYMENT.md CHANGED
@@ -1,80 +1,34 @@
1
- # Colorizer deployment
2
 
3
- Use the release's inference wrapper or its ONNX graph to obtain the complete
4
- improvement. The `.safetensors` file contains the learned colorizer; guided
5
- decoding is implemented by `spatial.py` and is included in the ONNX graph.
6
 
7
- ## Python / PyTorch
8
-
9
- Run from the extracted release directory:
10
-
11
- ```bash
12
- python -m pip install -r requirements.txt
13
- python inference.py --model . --output-dir colorized photo.jpg
14
- ```
15
-
16
- The default guided radius is 8 at the model's input resolution. Use
17
- `--guided-radius 0` for raw predictions or `--guided-radius 16` for stronger
18
- smoothing. Stronger smoothing can remove legitimate small color details.
19
- `--flip-tta --guided-radius 4` is an optional two-pass mode. The released
20
- ONNX graph is the single-pass radius-8 mode.
21
 
22
  ```python
23
- from PIL import Image
24
- from model import load_model
25
- from inference import colorize
26
-
27
- model = load_model('.')
28
- result = colorize(model, Image.open('photo.jpg'))
29
- result.save('colorized.png')
30
  ```
31
 
32
- The wrapper handles EXIF orientation, preserves aspect ratio, bounds the
33
- longest network-input side to 256 pixels, upsamples chroma to the original
34
- oriented image dimensions and combines it with the original Lab luminance.
35
- Final RGB conversion can clip colors outside the display gamut. Very large
36
- inputs still require memory for full-resolution color conversion. There is
37
- no video temporal-consistency guarantee.
38
 
39
- ## ONNX without PyTorch
40
 
41
- ```bash
42
- python -m pip install -r requirements-onnx.txt
43
- python colorize_onnx.py --model colorizer.onnx --output-dir colorized photo.jpg
44
- ```
45
 
46
- Input name: `luminance`, float32, shape `N x 1 x H x W`, values `L*/50 - 1`.
47
- Output name: `chroma`, float32, shape `N x 2 x H x W`, Lab a and b values.
48
- Height and width must each be at least 8. Dynamic shapes and batches are
49
- supported. Prefer a longest input side of 256 to match the evaluated
50
- operating point. Ordinary RGB values are not valid graph inputs.
51
 
52
- The graph includes temperature-0.38 decoding and radius-8 guided filtering
53
- with epsilon 0.001. It does not include file loading, EXIF handling, Lab
54
- conversion, aspect-ratio resizing or final chroma upsampling; these are
55
- implemented in `colorize_onnx.py`. The ONNX wrapper uses Pillow resizing,
56
- whereas the PyTorch wrapper uses PyTorch interpolation. Their image-level
57
- comparison is recorded in the release checks.
58
 
59
- ## Publish to main
60
 
61
- Authenticate normally on your own computer with repository write access:
62
 
63
- ```bash
64
- hf auth login
65
- python upload_main.py --folder . --repo User-2468/mini-unet-colorizer
66
- ```
67
 
68
- This performs one upload to `main` with an optimistic-concurrency guard.
69
- It also verifies that `stable` retains its pre-upload revision. The app can
70
- keep using `stable` until you choose to switch it to the tested release.
71
- The current chat connection has Jobs/read access but no repository write
72
- scope; the downloadable release is the publication fallback.
73
 
74
- ## Deployment limits
75
 
76
- This is a measured app-testing candidate. Semantic color mistakes remain,
77
- and smoothing cannot infer an object's unknown original color. Review the
78
- included failure examples on your app's real input photos before describing
79
- it as generally production-ready. Browser/mobile performance and real-time
80
- video have not been validated.
 
1
+ # v3 deployment
2
 
3
+ ## Local Python
 
 
4
 
5
+ Use Python 3.10 or 3.11 with the supplied requirements. CPU inference works without a GPU. Install a CUDA-compatible PyTorch build for GPU execution. Load once, put the model in evaluation mode, and reuse it across requests. The supplied loader does this automatically.
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  ```python
8
+ from inference import load_colorizer, colorize
9
+ model = load_colorizer('.', 'cpu')
 
 
 
 
 
10
  ```
11
 
12
+ The runtime loads a single 3,994,676-parameter network. The training-only teacher and discriminator are not required or downloaded.
 
 
 
 
 
13
 
14
+ ## Hugging Face Space
15
 
16
+ Copy `app.py`, `inference.py`, `semantic_model.py`, and `requirements-space.txt` into a Gradio Space. Rename the latter to `requirements.txt`. Use Gradio 6.28.0 in the Space README and choose ZeroGPU hardware.
 
 
 
17
 
18
+ The released app pins `MODEL_REVISION` to the verified model commit. Setting this environment variable deliberately overrides that pin. `MODEL_ID` can be a local directory for offline use or a Hub repository ID.
 
 
 
 
19
 
20
+ Import `spaces` before PyTorch. The model is placed on CUDA once at startup, and only the network stage is inside `@spaces.GPU`. Image preparation and full-resolution rendering happen on CPU. `ZEROGPU_CPU_TEST=1` enables the local CPU path for verification.
 
 
 
 
 
21
 
22
+ ## Input handling
23
 
24
+ The image pipeline applies EXIF orientation, preserves alpha, rejects images over 12 megapixels, and restores colour at the original output dimensions. Network processing preserves aspect ratio with a maximum 256-pixel side. Errors are returned explicitly rather than replaced by a blank image.
25
 
26
+ The default API `/colorize` takes an image, colour strength (0–1.5), and smoothing (`Gentle`, `Balanced`, or `Strong`). It returns a PNG. Requests are serialized to bound memory usage.
27
+
28
+ ## Versioning and rollback
 
29
 
30
+ Model and source are committed atomically. The app pins the model commit rather than following moving main. `RELEASE_MANIFEST.json` records selection and previous commits. To roll back, use the recorded prior Space commit and model revision. The user-managed stable branch is not modified.
 
 
 
 
31
 
32
+ ## Verification scope
33
 
34
+ `QA.json` records tests for strict reload, parameter count, finite outputs, dimensions, transparency, black/white inputs, grayscale strength zero, input validation, a Gradio HTTP request, and ONNX parity. `RELEASE_REPORT.md` records visual selection and the separate live Space check. These checks do not imply factual colour reconstruction or eliminate every visual failure case.
 
 
 
 
PIPELINE_EVAL.json ADDED
@@ -0,0 +1,1196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "n_color": 78,
3
+ "n_total": 80,
4
+ "pipeline": "actual aspect-preserving release vs prior Space 256/radius8/temp0.38; metrics measured after output resize to256",
5
+ "summary": {
6
+ "old_space": {
7
+ "ab_error": 16.144625015747852,
8
+ "patch_excess": 1.8169478370020022,
9
+ "color_coverage": 0.47298392271384215,
10
+ "missed_color": 0.24084079671968467
11
+ },
12
+ "release": {
13
+ "ab_error": 14.243927399317423,
14
+ "patch_excess": 0.6932277647444071,
15
+ "color_coverage": 0.479247068747496,
16
+ "missed_color": 0.1663338435453805
17
+ }
18
+ },
19
+ "rows": [
20
+ {
21
+ "index": 1616,
22
+ "old_space": {
23
+ "ab_error": 26.323200225830078,
24
+ "patch_excess": 4.660755157470703,
25
+ "color_coverage": 0.823211669921875,
26
+ "missed_color": 0.00838758732119945
27
+ },
28
+ "release": {
29
+ "ab_error": 16.94342803955078,
30
+ "patch_excess": 0.8918601274490356,
31
+ "color_coverage": 0.4547271728515625,
32
+ "missed_color": 0.1244119184526921
33
+ }
34
+ },
35
+ {
36
+ "index": 1890,
37
+ "old_space": {
38
+ "ab_error": 8.562353134155273,
39
+ "patch_excess": 0.4099482297897339,
40
+ "color_coverage": 0.081634521484375,
41
+ "missed_color": 0.5827405612444305
42
+ },
43
+ "release": {
44
+ "ab_error": 8.610569953918457,
45
+ "patch_excess": 0.49974769353866577,
46
+ "color_coverage": 0.0383148193359375,
47
+ "missed_color": 0.44008442116782615
48
+ }
49
+ },
50
+ {
51
+ "index": 801,
52
+ "old_space": {
53
+ "ab_error": 22.603069305419922,
54
+ "patch_excess": 2.7419018745422363,
55
+ "color_coverage": 0.364776611328125,
56
+ "missed_color": 0.3159938757982001
57
+ },
58
+ "release": {
59
+ "ab_error": 16.933408737182617,
60
+ "patch_excess": 0.5424809455871582,
61
+ "color_coverage": 0.7138671875,
62
+ "missed_color": 0.01211770417117891
63
+ }
64
+ },
65
+ {
66
+ "index": 1350,
67
+ "old_space": {
68
+ "ab_error": 19.007339477539062,
69
+ "patch_excess": 1.4587503671646118,
70
+ "color_coverage": 0.2160186767578125,
71
+ "missed_color": 0.8389771099393459
72
+ },
73
+ "release": {
74
+ "ab_error": 16.581514358520508,
75
+ "patch_excess": 0.950056791305542,
76
+ "color_coverage": 0.64739990234375,
77
+ "missed_color": 0.000883903806882258
78
+ }
79
+ },
80
+ {
81
+ "index": 1452,
82
+ "old_space": {
83
+ "ab_error": 11.529170036315918,
84
+ "patch_excess": 1.2069720029830933,
85
+ "color_coverage": 0.402740478515625,
86
+ "missed_color": 0.35575881190643127
87
+ },
88
+ "release": {
89
+ "ab_error": 9.721474647521973,
90
+ "patch_excess": 0.5086259245872498,
91
+ "color_coverage": 0.2576904296875,
92
+ "missed_color": 0.18481101408630252
93
+ }
94
+ },
95
+ {
96
+ "index": 2287,
97
+ "old_space": {
98
+ "ab_error": 15.350773811340332,
99
+ "patch_excess": 1.9994475841522217,
100
+ "color_coverage": 0.5487213134765625,
101
+ "missed_color": 0.08166368855263659
102
+ },
103
+ "release": {
104
+ "ab_error": 14.244087219238281,
105
+ "patch_excess": 1.2213833332061768,
106
+ "color_coverage": 0.6115264892578125,
107
+ "missed_color": 0.009846785537771357
108
+ }
109
+ },
110
+ {
111
+ "index": 1064,
112
+ "old_space": {
113
+ "ab_error": 21.012693405151367,
114
+ "patch_excess": 1.974622130393982,
115
+ "color_coverage": 0.4133453369140625,
116
+ "missed_color": 0.06151211801896733
117
+ },
118
+ "release": {
119
+ "ab_error": 15.226168632507324,
120
+ "patch_excess": 0.29835397005081177,
121
+ "color_coverage": 0.716217041015625,
122
+ "missed_color": 0.0
123
+ }
124
+ },
125
+ {
126
+ "index": 1342,
127
+ "old_space": {
128
+ "ab_error": 25.887615203857422,
129
+ "patch_excess": 2.6944580078125,
130
+ "color_coverage": 0.3334808349609375,
131
+ "missed_color": 0.17779714872198635
132
+ },
133
+ "release": {
134
+ "ab_error": 29.920133590698242,
135
+ "patch_excess": 0.9985980987548828,
136
+ "color_coverage": 0.80023193359375,
137
+ "missed_color": 0.01242864775187695
138
+ }
139
+ },
140
+ {
141
+ "index": 1974,
142
+ "old_space": {
143
+ "ab_error": 9.522749900817871,
144
+ "patch_excess": 1.2533193826675415,
145
+ "color_coverage": 0.207183837890625,
146
+ "missed_color": 0.5120499211017071
147
+ },
148
+ "release": {
149
+ "ab_error": 10.576430320739746,
150
+ "patch_excess": 2.0199408531188965,
151
+ "color_coverage": 0.503326416015625,
152
+ "missed_color": 0.03285038014632047
153
+ }
154
+ },
155
+ {
156
+ "index": 1324,
157
+ "old_space": {
158
+ "ab_error": 26.204675674438477,
159
+ "patch_excess": 2.7856273651123047,
160
+ "color_coverage": 0.9070587158203125,
161
+ "missed_color": 0.0
162
+ },
163
+ "release": {
164
+ "ab_error": 33.15073776245117,
165
+ "patch_excess": 1.4181828498840332,
166
+ "color_coverage": 0.987152099609375,
167
+ "missed_color": 0.0
168
+ }
169
+ },
170
+ {
171
+ "index": 1241,
172
+ "old_space": {
173
+ "ab_error": 12.267753601074219,
174
+ "patch_excess": 2.981765031814575,
175
+ "color_coverage": 0.386505126953125,
176
+ "missed_color": 0.2614813208263298
177
+ },
178
+ "release": {
179
+ "ab_error": 10.905852317810059,
180
+ "patch_excess": 1.2132973670959473,
181
+ "color_coverage": 0.6724700927734375,
182
+ "missed_color": 0.052052269153608416
183
+ }
184
+ },
185
+ {
186
+ "index": 1076,
187
+ "old_space": {
188
+ "ab_error": 32.03386306762695,
189
+ "patch_excess": 1.8285555839538574,
190
+ "color_coverage": 0.5383453369140625,
191
+ "missed_color": 0.18550559192337193
192
+ },
193
+ "release": {
194
+ "ab_error": 25.441709518432617,
195
+ "patch_excess": 0.3919871747493744,
196
+ "color_coverage": 0.240875244140625,
197
+ "missed_color": 0.4045627240782691
198
+ }
199
+ },
200
+ {
201
+ "index": 1592,
202
+ "old_space": {
203
+ "ab_error": 14.867029190063477,
204
+ "patch_excess": 1.3357399702072144,
205
+ "color_coverage": 0.639739990234375,
206
+ "missed_color": 0.15845956598129066
207
+ },
208
+ "release": {
209
+ "ab_error": 14.02211856842041,
210
+ "patch_excess": 0.548101544380188,
211
+ "color_coverage": 0.6025848388671875,
212
+ "missed_color": 0.09786789496177406
213
+ }
214
+ },
215
+ {
216
+ "index": 894,
217
+ "old_space": {
218
+ "ab_error": 19.769493103027344,
219
+ "patch_excess": 1.7964528799057007,
220
+ "color_coverage": 0.419647216796875,
221
+ "missed_color": 0.21247737003936895
222
+ },
223
+ "release": {
224
+ "ab_error": 18.06083106994629,
225
+ "patch_excess": 0.9287981390953064,
226
+ "color_coverage": 0.205474853515625,
227
+ "missed_color": 0.29653150952613583
228
+ }
229
+ },
230
+ {
231
+ "index": 2148,
232
+ "old_space": {
233
+ "ab_error": 7.6851372718811035,
234
+ "patch_excess": 0.2038302719593048,
235
+ "color_coverage": 0.357147216796875,
236
+ "missed_color": 0.06188071827189552
237
+ },
238
+ "release": {
239
+ "ab_error": 8.425639152526855,
240
+ "patch_excess": 0.2777884006500244,
241
+ "color_coverage": 0.5030364990234375,
242
+ "missed_color": 0.029231995748073347
243
+ }
244
+ },
245
+ {
246
+ "index": 499,
247
+ "old_space": {
248
+ "ab_error": 12.139986038208008,
249
+ "patch_excess": 2.4101762771606445,
250
+ "color_coverage": 0.259033203125,
251
+ "missed_color": 0.5720882503848127
252
+ },
253
+ "release": {
254
+ "ab_error": 10.870327949523926,
255
+ "patch_excess": 1.7260417938232422,
256
+ "color_coverage": 0.6872406005859375,
257
+ "missed_color": 0.028860954335556695
258
+ }
259
+ },
260
+ {
261
+ "index": 1091,
262
+ "old_space": {
263
+ "ab_error": 7.706239700317383,
264
+ "patch_excess": 0.9490010738372803,
265
+ "color_coverage": 0.70660400390625,
266
+ "missed_color": 0.04750045695485286
267
+ },
268
+ "release": {
269
+ "ab_error": 8.351296424865723,
270
+ "patch_excess": 0.41664353013038635,
271
+ "color_coverage": 0.585662841796875,
272
+ "missed_color": 0.03813288247121185
273
+ }
274
+ },
275
+ {
276
+ "index": 643,
277
+ "old_space": {
278
+ "ab_error": 12.94747543334961,
279
+ "patch_excess": 1.5321152210235596,
280
+ "color_coverage": 0.28607177734375,
281
+ "missed_color": 0.27395094734394976
282
+ },
283
+ "release": {
284
+ "ab_error": 10.66858959197998,
285
+ "patch_excess": 0.8456946015357971,
286
+ "color_coverage": 0.5540924072265625,
287
+ "missed_color": 0.05258630710217366
288
+ }
289
+ },
290
+ {
291
+ "index": 2199,
292
+ "old_space": {
293
+ "ab_error": 30.45133399963379,
294
+ "patch_excess": 0.11608487367630005,
295
+ "color_coverage": 0.0208892822265625,
296
+ "missed_color": 0.9190106905012267
297
+ },
298
+ "release": {
299
+ "ab_error": 20.48551368713379,
300
+ "patch_excess": 0.5652980804443359,
301
+ "color_coverage": 0.413116455078125,
302
+ "missed_color": 0.29834822993340343
303
+ }
304
+ },
305
+ {
306
+ "index": 28,
307
+ "old_space": {
308
+ "ab_error": 9.457603454589844,
309
+ "patch_excess": 0.390183687210083,
310
+ "color_coverage": 0.058074951171875,
311
+ "missed_color": 0.667576025034101
312
+ },
313
+ "release": {
314
+ "ab_error": 15.951315879821777,
315
+ "patch_excess": 0.9229965209960938,
316
+ "color_coverage": 0.7261199951171875,
317
+ "missed_color": 0.0
318
+ }
319
+ },
320
+ {
321
+ "index": 193,
322
+ "old_space": {
323
+ "ab_error": 8.022804260253906,
324
+ "patch_excess": 0.6247572898864746,
325
+ "color_coverage": 0.52301025390625,
326
+ "missed_color": 0.018376565601259325
327
+ },
328
+ "release": {
329
+ "ab_error": 10.262919425964355,
330
+ "patch_excess": 0.3918754458427429,
331
+ "color_coverage": 0.5903778076171875,
332
+ "missed_color": 0.0
333
+ }
334
+ },
335
+ {
336
+ "index": 1572,
337
+ "old_space": {
338
+ "ab_error": 13.333690643310547,
339
+ "patch_excess": 0.11408320814371109,
340
+ "color_coverage": 0.109130859375,
341
+ "missed_color": 0.43947592309001193
342
+ },
343
+ "release": {
344
+ "ab_error": 12.270377159118652,
345
+ "patch_excess": 0.104813352227211,
346
+ "color_coverage": 0.170135498046875,
347
+ "missed_color": 0.3524247064828994
348
+ }
349
+ },
350
+ {
351
+ "index": 2130,
352
+ "old_space": {
353
+ "ab_error": 16.954084396362305,
354
+ "patch_excess": 1.4006093740463257,
355
+ "color_coverage": 0.595916748046875,
356
+ "missed_color": 0.25908369733414294
357
+ },
358
+ "release": {
359
+ "ab_error": 9.94679069519043,
360
+ "patch_excess": 0.05882778391242027,
361
+ "color_coverage": 0.43670654296875,
362
+ "missed_color": 0.13325367755078057
363
+ }
364
+ },
365
+ {
366
+ "index": 1865,
367
+ "old_space": {
368
+ "ab_error": 7.544837951660156,
369
+ "patch_excess": 0.5778899788856506,
370
+ "color_coverage": 0.1146240234375,
371
+ "missed_color": 0.33362932893568437
372
+ },
373
+ "release": {
374
+ "ab_error": 8.306941986083984,
375
+ "patch_excess": 0.6006697416305542,
376
+ "color_coverage": 0.4653778076171875,
377
+ "missed_color": 0.30293035646327543
378
+ }
379
+ },
380
+ {
381
+ "index": 1802,
382
+ "old_space": {
383
+ "ab_error": 18.950891494750977,
384
+ "patch_excess": 2.2092673778533936,
385
+ "color_coverage": 0.5519561767578125,
386
+ "missed_color": 0.18835859782093795
387
+ },
388
+ "release": {
389
+ "ab_error": 15.6486177444458,
390
+ "patch_excess": 0.3804606795310974,
391
+ "color_coverage": 0.32672119140625,
392
+ "missed_color": 0.17589412600663193
393
+ }
394
+ },
395
+ {
396
+ "index": 1843,
397
+ "old_space": {
398
+ "ab_error": 11.737858772277832,
399
+ "patch_excess": 1.105494499206543,
400
+ "color_coverage": 0.6038665771484375,
401
+ "missed_color": 0.02552606103836552
402
+ },
403
+ "release": {
404
+ "ab_error": 14.591524124145508,
405
+ "patch_excess": 0.31478655338287354,
406
+ "color_coverage": 0.5148162841796875,
407
+ "missed_color": 0.10567075966780455
408
+ }
409
+ },
410
+ {
411
+ "index": 1310,
412
+ "old_space": {
413
+ "ab_error": 23.062294006347656,
414
+ "patch_excess": 0.9265129566192627,
415
+ "color_coverage": 0.88275146484375,
416
+ "missed_color": 0.007003060940543549
417
+ },
418
+ "release": {
419
+ "ab_error": 8.736886024475098,
420
+ "patch_excess": 0.38463208079338074,
421
+ "color_coverage": 0.2847442626953125,
422
+ "missed_color": 0.11650125220294963
423
+ }
424
+ },
425
+ {
426
+ "index": 760,
427
+ "old_space": {
428
+ "ab_error": 13.43350601196289,
429
+ "patch_excess": 0.9921309947967529,
430
+ "color_coverage": 0.2053070068359375,
431
+ "missed_color": 0.9260222323343951
432
+ },
433
+ "release": {
434
+ "ab_error": 16.876251220703125,
435
+ "patch_excess": 1.1696078777313232,
436
+ "color_coverage": 0.6178131103515625,
437
+ "missed_color": 0.44172138074497824
438
+ }
439
+ },
440
+ {
441
+ "index": 1547,
442
+ "old_space": {
443
+ "ab_error": 10.504715919494629,
444
+ "patch_excess": 0.8381835222244263,
445
+ "color_coverage": 0.14837646484375,
446
+ "missed_color": 0.39370380186706716
447
+ },
448
+ "release": {
449
+ "ab_error": 8.540243148803711,
450
+ "patch_excess": 0.3024168610572815,
451
+ "color_coverage": 0.0621490478515625,
452
+ "missed_color": 0.17516270577495066
453
+ }
454
+ },
455
+ {
456
+ "index": 1984,
457
+ "old_space": {
458
+ "ab_error": 27.105270385742188,
459
+ "patch_excess": 3.5282702445983887,
460
+ "color_coverage": 0.7821044921875,
461
+ "missed_color": 0.013363312404547769
462
+ },
463
+ "release": {
464
+ "ab_error": 28.28919792175293,
465
+ "patch_excess": 0.48088812828063965,
466
+ "color_coverage": 0.1480865478515625,
467
+ "missed_color": 0.258590700831495
468
+ }
469
+ },
470
+ {
471
+ "index": 20,
472
+ "old_space": {
473
+ "ab_error": 15.894635200500488,
474
+ "patch_excess": 1.3054187297821045,
475
+ "color_coverage": 0.531951904296875,
476
+ "missed_color": 0.07939068100358423
477
+ },
478
+ "release": {
479
+ "ab_error": 17.26842498779297,
480
+ "patch_excess": 0.5532290935516357,
481
+ "color_coverage": 0.55316162109375,
482
+ "missed_color": 0.08535586277521762
483
+ }
484
+ },
485
+ {
486
+ "index": 995,
487
+ "old_space": {
488
+ "ab_error": 15.624862670898438,
489
+ "patch_excess": 2.2529406547546387,
490
+ "color_coverage": 0.259613037109375,
491
+ "missed_color": 0.4021378891845213
492
+ },
493
+ "release": {
494
+ "ab_error": 13.449739456176758,
495
+ "patch_excess": 0.6617642641067505,
496
+ "color_coverage": 0.57403564453125,
497
+ "missed_color": 0.05704149745600523
498
+ }
499
+ },
500
+ {
501
+ "index": 719,
502
+ "old_space": {
503
+ "ab_error": 17.673622131347656,
504
+ "patch_excess": 6.900073051452637,
505
+ "color_coverage": 0.7959136962890625,
506
+ "missed_color": 0.03709780024776509
507
+ },
508
+ "release": {
509
+ "ab_error": 10.906693458557129,
510
+ "patch_excess": 0.8480014801025391,
511
+ "color_coverage": 0.5038604736328125,
512
+ "missed_color": 0.0922087923125858
513
+ }
514
+ },
515
+ {
516
+ "index": 1494,
517
+ "old_space": {
518
+ "ab_error": 8.360665321350098,
519
+ "patch_excess": 0.411716490983963,
520
+ "color_coverage": 0.1339569091796875,
521
+ "missed_color": 0.5299967917869747
522
+ },
523
+ "release": {
524
+ "ab_error": 7.159595966339111,
525
+ "patch_excess": 0.266187459230423,
526
+ "color_coverage": 0.0975189208984375,
527
+ "missed_color": 0.6700352903432788
528
+ }
529
+ },
530
+ {
531
+ "index": 842,
532
+ "old_space": {
533
+ "ab_error": 11.821839332580566,
534
+ "patch_excess": 2.703233003616333,
535
+ "color_coverage": 0.928680419921875,
536
+ "missed_color": 0.0
537
+ },
538
+ "release": {
539
+ "ab_error": 11.372722625732422,
540
+ "patch_excess": 0.5155807137489319,
541
+ "color_coverage": 0.907684326171875,
542
+ "missed_color": 7.853608733212911e-05
543
+ }
544
+ },
545
+ {
546
+ "index": 514,
547
+ "old_space": {
548
+ "ab_error": 9.687101364135742,
549
+ "patch_excess": 0.6117255687713623,
550
+ "color_coverage": 0.211181640625,
551
+ "missed_color": 0.5444506483032446
552
+ },
553
+ "release": {
554
+ "ab_error": 9.208121299743652,
555
+ "patch_excess": 0.18699583411216736,
556
+ "color_coverage": 0.1870574951171875,
557
+ "missed_color": 0.7229356659842422
558
+ }
559
+ },
560
+ {
561
+ "index": 1700,
562
+ "old_space": {
563
+ "ab_error": 15.625609397888184,
564
+ "patch_excess": 2.0040969848632812,
565
+ "color_coverage": 0.3100738525390625,
566
+ "missed_color": 0.4106577942620268
567
+ },
568
+ "release": {
569
+ "ab_error": 12.55972957611084,
570
+ "patch_excess": 0.6425396203994751,
571
+ "color_coverage": 0.3192138671875,
572
+ "missed_color": 0.323370059270572
573
+ }
574
+ },
575
+ {
576
+ "index": 81,
577
+ "old_space": {
578
+ "ab_error": 16.702062606811523,
579
+ "patch_excess": 3.883439064025879,
580
+ "color_coverage": 0.579315185546875,
581
+ "missed_color": 0.027358006001459816
582
+ },
583
+ "release": {
584
+ "ab_error": 16.94745445251465,
585
+ "patch_excess": 0.9333924651145935,
586
+ "color_coverage": 0.3153076171875,
587
+ "missed_color": 0.003919872401394934
588
+ }
589
+ },
590
+ {
591
+ "index": 669,
592
+ "old_space": {
593
+ "ab_error": 16.509859085083008,
594
+ "patch_excess": 0.4290950298309326,
595
+ "color_coverage": 0.1493682861328125,
596
+ "missed_color": 0.43240032314784016
597
+ },
598
+ "release": {
599
+ "ab_error": 15.242349624633789,
600
+ "patch_excess": 0.8886958360671997,
601
+ "color_coverage": 0.548095703125,
602
+ "missed_color": 0.007888609038635176
603
+ }
604
+ },
605
+ {
606
+ "index": 1863,
607
+ "old_space": {
608
+ "ab_error": 14.619690895080566,
609
+ "patch_excess": 1.6612411737442017,
610
+ "color_coverage": 0.47161865234375,
611
+ "missed_color": 0.12886517030293518
612
+ },
613
+ "release": {
614
+ "ab_error": 15.573603630065918,
615
+ "patch_excess": 2.386152505874634,
616
+ "color_coverage": 0.613861083984375,
617
+ "missed_color": 0.11965677810914038
618
+ }
619
+ },
620
+ {
621
+ "index": 118,
622
+ "old_space": {
623
+ "ab_error": 12.41354751586914,
624
+ "patch_excess": 2.3019068241119385,
625
+ "color_coverage": 0.42901611328125,
626
+ "missed_color": 0.13236981670440096
627
+ },
628
+ "release": {
629
+ "ab_error": 9.036662101745605,
630
+ "patch_excess": 1.469712257385254,
631
+ "color_coverage": 0.6489715576171875,
632
+ "missed_color": 0.0034115932140309523
633
+ }
634
+ },
635
+ {
636
+ "index": 247,
637
+ "old_space": {
638
+ "ab_error": 22.328853607177734,
639
+ "patch_excess": 1.0756666660308838,
640
+ "color_coverage": 0.4027252197265625,
641
+ "missed_color": 0.29081412325978323
642
+ },
643
+ "release": {
644
+ "ab_error": 23.825349807739258,
645
+ "patch_excess": 0.08932070434093475,
646
+ "color_coverage": 0.217254638671875,
647
+ "missed_color": 0.20771556067367208
648
+ }
649
+ },
650
+ {
651
+ "index": 718,
652
+ "old_space": {
653
+ "ab_error": 14.24968433380127,
654
+ "patch_excess": 1.4334428310394287,
655
+ "color_coverage": 0.6019744873046875,
656
+ "missed_color": 0.1952375935096407
657
+ },
658
+ "release": {
659
+ "ab_error": 9.221932411193848,
660
+ "patch_excess": 0.7787426114082336,
661
+ "color_coverage": 0.536376953125,
662
+ "missed_color": 0.23814666526182698
663
+ }
664
+ },
665
+ {
666
+ "index": 875,
667
+ "old_space": {
668
+ "ab_error": 12.56160831451416,
669
+ "patch_excess": 0.5671428442001343,
670
+ "color_coverage": 0.9713592529296875,
671
+ "missed_color": 0.003802414009628131
672
+ },
673
+ "release": {
674
+ "ab_error": 14.186704635620117,
675
+ "patch_excess": 0.3387293219566345,
676
+ "color_coverage": 0.8649749755859375,
677
+ "missed_color": 0.03068094606851322
678
+ }
679
+ },
680
+ {
681
+ "index": 2311,
682
+ "old_space": {
683
+ "ab_error": 22.329435348510742,
684
+ "patch_excess": 0.5803585052490234,
685
+ "color_coverage": 0.1852569580078125,
686
+ "missed_color": 0.4449274087109547
687
+ },
688
+ "release": {
689
+ "ab_error": 23.35008430480957,
690
+ "patch_excess": 0.3492544889450073,
691
+ "color_coverage": 0.12896728515625,
692
+ "missed_color": 0.3160756709194897
693
+ }
694
+ },
695
+ {
696
+ "index": 272,
697
+ "old_space": {
698
+ "ab_error": 50.092628479003906,
699
+ "patch_excess": 2.4126620292663574,
700
+ "color_coverage": 0.942230224609375,
701
+ "missed_color": 0.01947199203006838
702
+ },
703
+ "release": {
704
+ "ab_error": 13.429896354675293,
705
+ "patch_excess": 0.572797417640686,
706
+ "color_coverage": 0.0374298095703125,
707
+ "missed_color": 0.5612462074899244
708
+ }
709
+ },
710
+ {
711
+ "index": 1947,
712
+ "old_space": {
713
+ "ab_error": 13.949646949768066,
714
+ "patch_excess": 1.9255857467651367,
715
+ "color_coverage": 0.27593994140625,
716
+ "missed_color": 0.4522735813603908
717
+ },
718
+ "release": {
719
+ "ab_error": 14.641658782958984,
720
+ "patch_excess": 0.21683070063591003,
721
+ "color_coverage": 0.1020965576171875,
722
+ "missed_color": 0.6970526654855854
723
+ }
724
+ },
725
+ {
726
+ "index": 2307,
727
+ "old_space": {
728
+ "ab_error": 14.67344856262207,
729
+ "patch_excess": 1.292840838432312,
730
+ "color_coverage": 0.1552886962890625,
731
+ "missed_color": 0.40270344180958706
732
+ },
733
+ "release": {
734
+ "ab_error": 13.693403244018555,
735
+ "patch_excess": 0.16015203297138214,
736
+ "color_coverage": 0.4368133544921875,
737
+ "missed_color": 0.07055514318084151
738
+ }
739
+ },
740
+ {
741
+ "index": 1936,
742
+ "old_space": {
743
+ "ab_error": 6.898637771606445,
744
+ "patch_excess": 0.40601056814193726,
745
+ "color_coverage": 0.3411102294921875,
746
+ "missed_color": 0.24669745462356352
747
+ },
748
+ "release": {
749
+ "ab_error": 7.870009422302246,
750
+ "patch_excess": 0.27915287017822266,
751
+ "color_coverage": 0.320526123046875,
752
+ "missed_color": 0.16385637060108116
753
+ }
754
+ },
755
+ {
756
+ "index": 1935,
757
+ "old_space": {
758
+ "ab_error": 19.05333137512207,
759
+ "patch_excess": 0.9947030544281006,
760
+ "color_coverage": 0.8654022216796875,
761
+ "missed_color": 0.010393743440430646
762
+ },
763
+ "release": {
764
+ "ab_error": 21.285797119140625,
765
+ "patch_excess": 1.0532053709030151,
766
+ "color_coverage": 0.874114990234375,
767
+ "missed_color": 0.01535362426786742
768
+ }
769
+ },
770
+ {
771
+ "index": 406,
772
+ "old_space": {
773
+ "ab_error": 11.752641677856445,
774
+ "patch_excess": 2.6542296409606934,
775
+ "color_coverage": 0.50799560546875,
776
+ "missed_color": 0.2511842190643047
777
+ },
778
+ "release": {
779
+ "ab_error": 14.617018699645996,
780
+ "patch_excess": 1.5975663661956787,
781
+ "color_coverage": 0.750701904296875,
782
+ "missed_color": 0.01739017584841996
783
+ }
784
+ },
785
+ {
786
+ "index": 630,
787
+ "old_space": {
788
+ "ab_error": 9.711251258850098,
789
+ "patch_excess": 0.9617525339126587,
790
+ "color_coverage": 0.1025543212890625,
791
+ "missed_color": 0.411119306656832
792
+ },
793
+ "release": {
794
+ "ab_error": 7.769914627075195,
795
+ "patch_excess": 0.10242865979671478,
796
+ "color_coverage": 0.0,
797
+ "missed_color": 0.8865019361976766
798
+ }
799
+ },
800
+ {
801
+ "index": 1023,
802
+ "old_space": {
803
+ "ab_error": 12.51215934753418,
804
+ "patch_excess": 3.8293511867523193,
805
+ "color_coverage": 0.8065338134765625,
806
+ "missed_color": 0.0007825250966977441
807
+ },
808
+ "release": {
809
+ "ab_error": 12.0440673828125,
810
+ "patch_excess": 0.339896023273468,
811
+ "color_coverage": 0.067352294921875,
812
+ "missed_color": 0.3461667449191763
813
+ }
814
+ },
815
+ {
816
+ "index": 941,
817
+ "old_space": {
818
+ "ab_error": 11.803794860839844,
819
+ "patch_excess": 1.020538091659546,
820
+ "color_coverage": 0.15338134765625,
821
+ "missed_color": 0.613625304136253
822
+ },
823
+ "release": {
824
+ "ab_error": 11.58432674407959,
825
+ "patch_excess": 0.5445202589035034,
826
+ "color_coverage": 0.40020751953125,
827
+ "missed_color": 0.032907542579075424
828
+ }
829
+ },
830
+ {
831
+ "index": 1746,
832
+ "old_space": {
833
+ "ab_error": 21.40471076965332,
834
+ "patch_excess": 2.445641279220581,
835
+ "color_coverage": 0.553466796875,
836
+ "missed_color": 0.26700241575564015
837
+ },
838
+ "release": {
839
+ "ab_error": 16.30540657043457,
840
+ "patch_excess": 0.5714777708053589,
841
+ "color_coverage": 0.1327056884765625,
842
+ "missed_color": 0.39769889039020595
843
+ }
844
+ },
845
+ {
846
+ "index": 584,
847
+ "old_space": {
848
+ "ab_error": 20.1906681060791,
849
+ "patch_excess": 1.2620389461517334,
850
+ "color_coverage": 0.69354248046875,
851
+ "missed_color": 0.09630933556105857
852
+ },
853
+ "release": {
854
+ "ab_error": 22.57025909423828,
855
+ "patch_excess": 1.746338963508606,
856
+ "color_coverage": 0.646820068359375,
857
+ "missed_color": 0.1843243727063878
858
+ }
859
+ },
860
+ {
861
+ "index": 1435,
862
+ "old_space": {
863
+ "ab_error": 21.27211570739746,
864
+ "patch_excess": 5.1058244705200195,
865
+ "color_coverage": 0.6849822998046875,
866
+ "missed_color": 0.35429673620628394
867
+ },
868
+ "release": {
869
+ "ab_error": 11.184368133544922,
870
+ "patch_excess": 0.9250662922859192,
871
+ "color_coverage": 0.50732421875,
872
+ "missed_color": 0.014263235368763007
873
+ }
874
+ },
875
+ {
876
+ "index": 1955,
877
+ "old_space": {
878
+ "ab_error": 7.787717819213867,
879
+ "patch_excess": 1.0435097217559814,
880
+ "color_coverage": 0.270172119140625,
881
+ "missed_color": 0.2432694031188385
882
+ },
883
+ "release": {
884
+ "ab_error": 12.496732711791992,
885
+ "patch_excess": 0.31601670384407043,
886
+ "color_coverage": 0.7804718017578125,
887
+ "missed_color": 0.0
888
+ }
889
+ },
890
+ {
891
+ "index": 1047,
892
+ "old_space": {
893
+ "ab_error": 12.725871086120605,
894
+ "patch_excess": 1.604668140411377,
895
+ "color_coverage": 0.195526123046875,
896
+ "missed_color": 0.4522170570355802
897
+ },
898
+ "release": {
899
+ "ab_error": 12.001117706298828,
900
+ "patch_excess": 0.2767108678817749,
901
+ "color_coverage": 0.17236328125,
902
+ "missed_color": 0.46093330949401035
903
+ }
904
+ },
905
+ {
906
+ "index": 1689,
907
+ "old_space": {
908
+ "ab_error": 26.23225212097168,
909
+ "patch_excess": 1.7044408321380615,
910
+ "color_coverage": 0.58978271484375,
911
+ "missed_color": 0.15641351282702565
912
+ },
913
+ "release": {
914
+ "ab_error": 17.07444953918457,
915
+ "patch_excess": 0.4091215431690216,
916
+ "color_coverage": 0.90069580078125,
917
+ "missed_color": 0.0014901363136059606
918
+ }
919
+ },
920
+ {
921
+ "index": 772,
922
+ "old_space": {
923
+ "ab_error": 6.426523208618164,
924
+ "patch_excess": 0.38687315583229065,
925
+ "color_coverage": 0.258514404296875,
926
+ "missed_color": 0.26649261974093785
927
+ },
928
+ "release": {
929
+ "ab_error": 6.300887107849121,
930
+ "patch_excess": 0.348586767911911,
931
+ "color_coverage": 0.2451171875,
932
+ "missed_color": 0.32392810523144894
933
+ }
934
+ },
935
+ {
936
+ "index": 618,
937
+ "old_space": {
938
+ "ab_error": 13.832884788513184,
939
+ "patch_excess": 1.9853154420852661,
940
+ "color_coverage": 0.485137939453125,
941
+ "missed_color": 0.20293154973672975
942
+ },
943
+ "release": {
944
+ "ab_error": 14.936275482177734,
945
+ "patch_excess": 0.5301600694656372,
946
+ "color_coverage": 0.8293914794921875,
947
+ "missed_color": 0.0007115411982353779
948
+ }
949
+ },
950
+ {
951
+ "index": 336,
952
+ "old_space": {
953
+ "ab_error": 16.60566520690918,
954
+ "patch_excess": 1.2380704879760742,
955
+ "color_coverage": 0.4294891357421875,
956
+ "missed_color": 0.08609339996553507
957
+ },
958
+ "release": {
959
+ "ab_error": 14.536388397216797,
960
+ "patch_excess": 1.1959733963012695,
961
+ "color_coverage": 0.43182373046875,
962
+ "missed_color": 0.1851456143374117
963
+ }
964
+ },
965
+ {
966
+ "index": 1086,
967
+ "old_space": {
968
+ "ab_error": 17.58598518371582,
969
+ "patch_excess": 1.7815990447998047,
970
+ "color_coverage": 0.70526123046875,
971
+ "missed_color": 0.0451198129748685
972
+ },
973
+ "release": {
974
+ "ab_error": 8.847654342651367,
975
+ "patch_excess": 0.4222590923309326,
976
+ "color_coverage": 0.053924560546875,
977
+ "missed_color": 0.43115137346580945
978
+ }
979
+ },
980
+ {
981
+ "index": 2140,
982
+ "old_space": {
983
+ "ab_error": 7.6119256019592285,
984
+ "patch_excess": 0.15633070468902588,
985
+ "color_coverage": 0.7803955078125,
986
+ "missed_color": 0.0044853635505193576
987
+ },
988
+ "release": {
989
+ "ab_error": 9.672757148742676,
990
+ "patch_excess": 0.08361931890249252,
991
+ "color_coverage": 0.7842254638671875,
992
+ "missed_color": 0.001731192949323261
993
+ }
994
+ },
995
+ {
996
+ "index": 1994,
997
+ "old_space": {
998
+ "ab_error": 23.052637100219727,
999
+ "patch_excess": 4.873438835144043,
1000
+ "color_coverage": 0.569427490234375,
1001
+ "missed_color": 0.049524982406755805
1002
+ },
1003
+ "release": {
1004
+ "ab_error": 17.308002471923828,
1005
+ "patch_excess": 1.5104169845581055,
1006
+ "color_coverage": 0.5511016845703125,
1007
+ "missed_color": 0.014250527797325828
1008
+ }
1009
+ },
1010
+ {
1011
+ "index": 451,
1012
+ "old_space": {
1013
+ "ab_error": 10.520956993103027,
1014
+ "patch_excess": 1.2838455438613892,
1015
+ "color_coverage": 0.65618896484375,
1016
+ "missed_color": 0.016560718085831993
1017
+ },
1018
+ "release": {
1019
+ "ab_error": 9.030373573303223,
1020
+ "patch_excess": 0.754244327545166,
1021
+ "color_coverage": 0.6208953857421875,
1022
+ "missed_color": 0.035385916568902956
1023
+ }
1024
+ },
1025
+ {
1026
+ "index": 1807,
1027
+ "old_space": {
1028
+ "ab_error": 17.55420684814453,
1029
+ "patch_excess": 3.6267919540405273,
1030
+ "color_coverage": 0.8811798095703125,
1031
+ "missed_color": 0.03126752664049355
1032
+ },
1033
+ "release": {
1034
+ "ab_error": 16.857818603515625,
1035
+ "patch_excess": 0.4508571922779083,
1036
+ "color_coverage": 0.965179443359375,
1037
+ "missed_color": 0.0
1038
+ }
1039
+ },
1040
+ {
1041
+ "index": 194,
1042
+ "old_space": {
1043
+ "ab_error": 21.62356185913086,
1044
+ "patch_excess": 3.4757049083709717,
1045
+ "color_coverage": 0.4154815673828125,
1046
+ "missed_color": 0.3036251799502683
1047
+ },
1048
+ "release": {
1049
+ "ab_error": 17.168527603149414,
1050
+ "patch_excess": 1.7961444854736328,
1051
+ "color_coverage": 0.7869110107421875,
1052
+ "missed_color": 0.006255725690354666
1053
+ }
1054
+ },
1055
+ {
1056
+ "index": 1636,
1057
+ "old_space": {
1058
+ "ab_error": 18.11170768737793,
1059
+ "patch_excess": 2.316526174545288,
1060
+ "color_coverage": 0.4608001708984375,
1061
+ "missed_color": 0.2738500960922486
1062
+ },
1063
+ "release": {
1064
+ "ab_error": 17.05701446533203,
1065
+ "patch_excess": 0.8802337646484375,
1066
+ "color_coverage": 0.3453216552734375,
1067
+ "missed_color": 0.262780269058296
1068
+ }
1069
+ },
1070
+ {
1071
+ "index": 409,
1072
+ "old_space": {
1073
+ "ab_error": 21.841514587402344,
1074
+ "patch_excess": 2.8149540424346924,
1075
+ "color_coverage": 0.5799102783203125,
1076
+ "missed_color": 0.07750204292288504
1077
+ },
1078
+ "release": {
1079
+ "ab_error": 21.5656795501709,
1080
+ "patch_excess": 0.5761880278587341,
1081
+ "color_coverage": 0.636871337890625,
1082
+ "missed_color": 0.043051911745731364
1083
+ }
1084
+ },
1085
+ {
1086
+ "index": 2471,
1087
+ "old_space": {
1088
+ "ab_error": 16.08145523071289,
1089
+ "patch_excess": 2.2105460166931152,
1090
+ "color_coverage": 0.7911224365234375,
1091
+ "missed_color": 0.02534622878306507
1092
+ },
1093
+ "release": {
1094
+ "ab_error": 17.141895294189453,
1095
+ "patch_excess": 0.7329339981079102,
1096
+ "color_coverage": 0.7151947021484375,
1097
+ "missed_color": 0.037562970634182305
1098
+ }
1099
+ },
1100
+ {
1101
+ "index": 665,
1102
+ "old_space": {
1103
+ "ab_error": 19.746185302734375,
1104
+ "patch_excess": 2.317701578140259,
1105
+ "color_coverage": 0.7936553955078125,
1106
+ "missed_color": 0.09188258199790325
1107
+ },
1108
+ "release": {
1109
+ "ab_error": 14.18701457977295,
1110
+ "patch_excess": 0.9515336751937866,
1111
+ "color_coverage": 0.8133697509765625,
1112
+ "missed_color": 0.05256851879586641
1113
+ }
1114
+ },
1115
+ {
1116
+ "index": 2151,
1117
+ "old_space": {
1118
+ "ab_error": 17.301462173461914,
1119
+ "patch_excess": 0.5416412353515625,
1120
+ "color_coverage": 0.3140869140625,
1121
+ "missed_color": 0.5995582777385642
1122
+ },
1123
+ "release": {
1124
+ "ab_error": 22.86566162109375,
1125
+ "patch_excess": 0.31695353984832764,
1126
+ "color_coverage": 0.8281402587890625,
1127
+ "missed_color": 0.020811038502698147
1128
+ }
1129
+ },
1130
+ {
1131
+ "index": 1377,
1132
+ "old_space": {
1133
+ "ab_error": 12.53142261505127,
1134
+ "patch_excess": 2.1505064964294434,
1135
+ "color_coverage": 0.8076019287109375,
1136
+ "missed_color": 0.01162006310521162
1137
+ },
1138
+ "release": {
1139
+ "ab_error": 14.857101440429688,
1140
+ "patch_excess": 1.1797388792037964,
1141
+ "color_coverage": 0.5686492919921875,
1142
+ "missed_color": 0.08688934827548689
1143
+ }
1144
+ },
1145
+ {
1146
+ "index": 246,
1147
+ "old_space": {
1148
+ "ab_error": 16.83350372314453,
1149
+ "patch_excess": 1.9284921884536743,
1150
+ "color_coverage": 0.5128631591796875,
1151
+ "missed_color": 0.0004520182615377661
1152
+ },
1153
+ "release": {
1154
+ "ab_error": 9.959344863891602,
1155
+ "patch_excess": 0.281173974275589,
1156
+ "color_coverage": 0.2016754150390625,
1157
+ "missed_color": 0.1439678162997785
1158
+ }
1159
+ },
1160
+ {
1161
+ "index": 1679,
1162
+ "old_space": {
1163
+ "ab_error": 5.608643531799316,
1164
+ "patch_excess": 1.0197176933288574,
1165
+ "color_coverage": 0.28912353515625,
1166
+ "missed_color": 0.13227684346701166
1167
+ },
1168
+ "release": {
1169
+ "ab_error": 4.4221906661987305,
1170
+ "patch_excess": 0.4705710709095001,
1171
+ "color_coverage": 0.210845947265625,
1172
+ "missed_color": 0.023673997412677877
1173
+ }
1174
+ },
1175
+ {
1176
+ "index": 2332,
1177
+ "old_space": {
1178
+ "ab_error": 13.967047691345215,
1179
+ "patch_excess": 3.781674861907959,
1180
+ "color_coverage": 0.5753173828125,
1181
+ "missed_color": 0.03037200832466181
1182
+ },
1183
+ "release": {
1184
+ "ab_error": 7.873258590698242,
1185
+ "patch_excess": 0.19574034214019775,
1186
+ "color_coverage": 0.108612060546875,
1187
+ "missed_color": 0.39815296566077
1188
+ }
1189
+ }
1190
+ ],
1191
+ "median_pipeline_seconds": 0.06851194500001156,
1192
+ "torch": "2.8.0+cu128",
1193
+ "device": "NVIDIA L4",
1194
+ "checkpoint": "round6 palette9000",
1195
+ "limitations": "Previously inspected development/reporting set, no independent human ratings. Exact hue error is auxiliary."
1196
+ }
QA.json ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "parameters": 3994676,
3
+ "torch": "2.8.0+cu128",
4
+ "tests": [
5
+ {
6
+ "size": [
7
+ 1,
8
+ 1
9
+ ],
10
+ "alpha": "exact",
11
+ "max_L_error": 0.0753021240234375,
12
+ "seconds": 0.03764470492023975
13
+ },
14
+ {
15
+ "size": [
16
+ 17,
17
+ 23
18
+ ],
19
+ "alpha": "exact",
20
+ "max_L_error": 0.17739105224609375,
21
+ "seconds": 0.019733823952265084
22
+ },
23
+ {
24
+ "size": [
25
+ 320,
26
+ 191
27
+ ],
28
+ "alpha": "exact",
29
+ "max_L_error": 0.2049694061279297,
30
+ "seconds": 0.15181890805251896
31
+ },
32
+ {
33
+ "size": [
34
+ 193,
35
+ 320
36
+ ],
37
+ "alpha": "exact",
38
+ "max_L_error": 0.21184539794921875,
39
+ "seconds": 0.20581824099645019
40
+ },
41
+ {
42
+ "size": [
43
+ 1024,
44
+ 768
45
+ ],
46
+ "alpha": "exact",
47
+ "max_L_error": 0.2049694061279297,
48
+ "seconds": 1.8695425690384582
49
+ }
50
+ ],
51
+ "gradio_config_components": 11,
52
+ "ui_function_all_modes": "passed",
53
+ "gradio_http_request": "passed",
54
+ "onnx_max_abs_error": 0.000911712646484375
55
+ }
README.md CHANGED
@@ -3,78 +3,75 @@ license: apache-2.0
3
  pipeline_tag: image-to-image
4
  tags:
5
  - colorization
6
- - unet
7
  - pytorch
8
- - safetensors
9
  - onnx
10
- datasets:
11
- - johnowhitaker/imagenette2-320
12
- - detection-datasets/coco
13
  ---
14
- # Mini U-Net Colorizer — broader-data trained candidate
15
 
16
- **Status: evaluated app-testing candidate.** Some broad color patches and
17
- incorrect object hues remain. Predicted colors are not evidence of original
18
- historical colors.
19
 
20
- This checkpoint has 3,968,892 learned parameters and 236 fixed color bins.
21
- It starts from the audited bin-mapping repair of main commit
22
- `6c47ea40724d8fcd67d4f36ce837dc1cb5b1b2a8` and changes all 65 learned parameter
23
- tensors. The color vocabulary is unchanged. The selected weights are update
24
- 748 of a completed 1,122-update BF16 L4 run using a 17,325-photo mixed training
25
- pool (11,943 Imagenette plus 5,382 COCO), batch 32, initial learning rate 1e-5,
26
- weighted classification loss and frozen BatchNorm running statistics.
27
 
28
- Selection compared Imagenette50 and reserved COCO200 validation images.
29
- The selected model retained color strength better than spatial-loss candidates.
30
- It was then scored on separate Imagenette200 and COCO-val100 checks.
31
 
32
- | Test sample | Previous repaired error | This release error | Fine excess-edge reduction |
33
- |---|---:|---:|---:|
34
- | Imagenette 200 | 13.318 | 12.847 | 85.8% |
35
- | COCO-val 100 | 15.266 | 14.532 | 84.3% |
 
 
36
 
37
- Error is mean Lab chroma distance. The release includes guided8 decoding;
38
- raw learned weights alone improve error by 1.97% and 3.25%, respectively.
39
- Excess-edge reductions are proxies, not counts of visible blotches removed.
40
- COCO is a convenience sample; older upstream training exposure is unknown.
41
 
42
- ## Use the complete pipeline
43
 
44
  ```bash
45
- python -m pip install -r requirements.txt
46
- python inference.py --model . --output-dir colorized photo.jpg
47
  ```
48
 
 
 
49
  ```python
50
  from PIL import Image
51
- from model import load_model
52
- from inference import colorize
53
- model = load_model('.')
54
- colorize(model, Image.open('photo.jpg')).save('colorized.png')
 
55
  ```
56
 
57
- Defaults: temperature 0.38, guided radius 8, epsilon 0.001, one network pass.
58
- The wrapper preserves aspect ratio and original luminance. Old app code that
59
- only loads safetensors will not automatically gain guided filtering.
60
 
61
- For ONNX without PyTorch:
62
 
63
- ```bash
64
- python -m pip install -r requirements-onnx.txt
65
- python colorize_onnx.py --model colorizer.onnx --output-dir colorized photo.jpg
66
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
- The 15.9MB ONNX graph includes the model and guided decoder, with dynamic
69
- batch/spatial sizes and verified PyTorch parity. See `DEPLOYMENT.md` for the
70
- Lab input contract, CPU timings, publication commands and integration limits.
71
- See `RESEARCH_ROUND2.md` and `reports/round2/` for complete measured evidence.
72
 
73
- ## Limitations
74
 
75
- Smoothing removes fine color fluctuations but can suppress true small color
76
- details, especially without luminance boundaries. Semantically wrong hues
77
- remain. The coffee and rocket failure examples are retained. Browser/mobile
78
- performance, video consistency and general production quality are unvalidated.
79
- The separate experiment bundle contains all runs and reproduction code; GPU
80
- optimizer state is not included. This is a weights-only continuation point.
 
3
  pipeline_tag: image-to-image
4
  tags:
5
  - colorization
6
+ - image-colorization
7
  - pytorch
 
8
  - onnx
9
+ - mobilenet-v3
10
+ - small-model
11
+ library_name: pytorch
12
  ---
 
13
 
14
+ # Mini Photo Colorizer — v3.0
 
 
15
 
16
+ A compact automatic photo colouriser with **3,994,676 learned parameters in total**, including the complete deployed MobileNetV3 encoder. It predicts plausible colours from grayscale photographs. No teacher, critic, external semantic model, retrieval service or ensemble is needed at inference.
 
 
 
 
 
 
17
 
18
+ ## Release contents
 
 
19
 
20
+ - `model.safetensors` and `config.json`: the selected checkpoint.
21
+ - `semantic_model.py`: the complete architecture and strict checkpoint loader.
22
+ - `inference.py`: Python API and command-line image colourisation.
23
+ - `colorizer.onnx`: equivalent fixed 256×256 network graph, with float Lab `ab` output.
24
+ - `app.py` and `requirements-space.txt`: the tested Gradio / ZeroGPU application.
25
+ - `RELEASE_REPORT.md`, `QA.json` and `SHA256SUMS.json`: selection evidence, runtime checks and file hashes.
26
 
27
+ ## Use
 
 
 
28
 
29
+ Download this repository, then:
30
 
31
  ```bash
32
+ pip install -r requirements.txt
33
+ python inference.py input.jpg output.png --model . --device cpu
34
  ```
35
 
36
+ For CUDA, use `--device cuda`. Python API:
37
+
38
  ```python
39
  from PIL import Image
40
+ from inference import load_colorizer, colorize
41
+
42
+ model = load_colorizer('.', 'cpu')
43
+ output = colorize(model, Image.open('input.jpg'))
44
+ output.save('output.png')
45
  ```
46
 
47
+ Default processing uses a 256-pixel maximum network side and retains aspect ratio. The output keeps the input resolution, orientation and alpha, up to 12 megapixels. Original Lab lightness is retained; colour is upsampled with a lightness-guided local linear model and compressed into the sRGB gamut. Quantisation can cause small lightness differences.
 
 
48
 
49
+ `saturation=1.0` is the default. Values between 0 and 1.5 are supported. Smoothing radius defaults to 8 at network resolution; 4 and 16 are available for gentler or stronger smoothing.
50
 
51
+ ## Architecture and training
52
+
53
+ MobileNetV3 Large feature encoder, 128-channel feature pyramid, 16 colour queries and two attention blocks. The decoder produces a shared palette and spatial assignment masks with a bounded local residual. The ImageNet classifier is discarded, and every remaining learned parameter is included in the count above. There are 5,324 parameters of headroom below the strict four-million limit.
54
+
55
+ Training uses 16,230 filtered photographs from pinned Imagenette and COCO parquet files, with ImageNet pretrained encoder features and DDColor artistic targets. The final selection and any fine-tuning are documented in `RELEASE_REPORT.md`. Data revisions, training scripts and historical experiments are retained under `experiments/`.
56
+
57
+ Teacher source and checkpoint references:
58
+
59
+ - [DDColor](https://arxiv.org/abs/2212.11613), artistic checkpoint revision `aa10f72fffc89a6658e37b48556050b4d9a26f63`.
60
+ - [TorchVision MobileNetV3 Large](https://docs.pytorch.org/vision/0.21/models/generated/torchvision.models.mobilenet_v3_large.html), ImageNet V2 encoder initialization.
61
+ - [PalGAN](https://arxiv.org/abs/2210.11204) informed palette and realism experiments; this implementation is not a reproduction.
62
+
63
+ ## Intended use and limits
64
+
65
+ Intended for adding plausible colour to ordinary photographs and consumer photo applications. A grayscale image can admit several equally plausible colours. Outputs must not be presented as recovered historical fact.
66
+
67
+ Known limitations include muted or warm/sepia colour choices, incorrect clothing and object hues, residual colour bleeding around small objects, and poor results on unusual surfaces or scenes. Severe scan damage, astronomical images and illustrations are outside the principal evaluation domain. The model does not repair scratches or reconstruct lost detail. Surface consistency is not the same as assigning a uniform colour to an entire patterned object.
68
+
69
+ Metrics against an original colour image are auxiliary because that original may not be inferable. The report includes visual review and colour-retention diagnostics. Upstream pretraining overlap is possible; the evaluation is not a guarantee of unseen content or universal quality. No claim is made that further improvements below four million parameters are impossible.
70
+
71
+ ## Migration from the old U-Net
72
 
73
+ This release changes the architecture. The old U-Net class and colour-bin decoder cannot load these weights. Use `semantic_model.py` and `inference.py` together with the new checkpoint. The network directly returns Lab `ab`; do not apply the old bin softmax or temperature decoder. Prior main commits and the user-managed `stable` branch remain available. Old root-level training and ONNX helpers are archived under `legacy/v2/` to avoid silently mixing architectures.
 
 
 
74
 
75
+ ## ONNX
76
 
77
+ The ONNX graph accepts float32 `L` shaped `[1,1,256,256]`, normalized as `L_lab / 50 - 1`, and returns float32 Lab `ab` shaped `[1,2,256,256]`. It contains only the compact network. Image loading, resizing, guided upsampling and gamut compression are handled by the Python pipeline, not embedded in the graph. See `colorizer.json` for its contract. The fixed-size export is numerically compared with PyTorch before release.
 
 
 
 
 
RELEASE_MANIFEST.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "release": "v3.0.0",
3
+ "selected": "round6 palette step9000",
4
+ "selected_source_revision": "7b10485ee9d205ce2f7b660dfc6a90012d78b1cb",
5
+ "parameters": 3994676,
6
+ "previous_main_commit": "45011cd907b845d44f627db4e387678800261257",
7
+ "previous_space_commit": "ab8faf4771154605d5dbecf084f2a01f32704efb",
8
+ "model_sha256": "ec1f27d74533adc83f7ab3639a091fc4d8738a434dafc7d172c7873c28a9e715",
9
+ "engineering_qa": "QA.json",
10
+ "pipeline_eval": "PIPELINE_EVAL.json",
11
+ "critic_runs_rejected": [
12
+ "realism_low_v2",
13
+ "realism_high_v2"
14
+ ],
15
+ "scope": "Final tested release from this project; not a proof of optimality or historical colour truth."
16
+ }
RELEASE_REPORT.md ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Final selection — Mini Photo Colorizer v3.0
2
+
3
+ ## Decision
4
+ Release the round6 palette checkpoint at step9000 with the new, verified image pipeline. All 3,994,676 learned parameters, including the encoder, are in model.safetensors. No discriminator, teacher, second model or weight ensemble is loaded in production.
5
+
6
+ The final two training runs tested conditional colour critics with strengths0.04 and0.12. Both completed 2,500 fine-tuning updates, with checkpoints saved and verified every500 updates. They produced stronger colours in some images but reintroduced local colour patches. They were rejected after visual review and matched reporting. Two earlier startup attempts failed on a typed optimiser argument before training; that argument was corrected. Every successful run is retained under experiments/final-20260928.
7
+
8
+ ## Matched 80-image reporting set
9
+ 78 images had appreciable original colour. These are repeated reporting/development images, not an independent unseen benchmark.
10
+
11
+ | Checkpoint | Blotch proxy (lower) | Missed colour (lower) | Colour coverage |
12
+ |---|---:|---:|---:|
13
+ | Selected round6 palette9000 | 0.779 | 0.169 | 0.474 |
14
+ | Gentle colour critic | 1.070 | 0.170 | 0.487 |
15
+ | Strong colour critic | 1.413 | 0.171 | 0.497 |
16
+
17
+ The critics increased the patchiness measure by about37% and81%. On reviewed images, this corresponded to red patches on faces, small coloured areas within otherwise consistent objects, and colour bleeding. More saturation did not make them better releases.
18
+
19
+ Round7's four objective/degradation variants were also rejected: they were visually almost unchanged and did not improve blotches.
20
+
21
+ ## What improves over the previously deployed app
22
+ The previous Space used the old v2 U-Net. The release uses pretrained semantic features and a shared palette. In the prior pinned300-image fresh COCO comparison (272 chromatic images), selected round6 lowered the patch-excess proxy from1.714 to0.747 and missed-colour fraction from0.223 to0.159. Those are approximately56% and29% reductions under that evaluation pipeline, not universal guarantees.
23
+
24
+ The production image pipeline additionally retains aspect ratio, uses original-resolution lightness to guide chroma upsampling, preserves EXIF orientation and alpha, checks the image-size limit, compresses out-of-gamut chroma, and reports input errors. See the actual pipeline comparison appended below for its distinct measurements.
25
+
26
+ ## Verification
27
+ QA.json records CPU inference on tiny, portrait, landscape and1024×768 images; exact alpha retention; worst observed lightness difference below0.22 Lab L after8-bit quantisation; black/white and saturation-zero behaviour; Gradio function and HTTP calls; and ONNX parity with maximum observed difference0.000912 Lab units. These are engineering tests, not evidence of historical colour accuracy.
28
+
29
+ The Space pins the model release commit and uses the same tested source. ZeroGPU GPU operations are isolated in a decorated function; full-resolution rendering runs on CPU. Separate live Space verification is recorded in LIVE_SPACE_CHECK.json when completed.
30
+
31
+ ## Evidence and provenance
32
+ - [Final low-critic training](experiments/final-20260928/realism_low_v2/)
33
+ - [Final high-critic training](experiments/final-20260928/realism_high_v2/)
34
+ - [Round7 review](experiments/round7-20260928/REVIEW.md)
35
+ - [Round6 selected checkpoint and evaluation](experiments/round6-20260928/RESULTS.md)
36
+ - [Actual release pipeline comparisons](experiments/final-20260928/pipeline_eval/)
37
+
38
+ ## Limits
39
+ The release may choose muted, warm or incorrect colours and can still show residual bleeding. Real archival evaluation is limited, and two familiar historical examples are not representative of the entire domain. The moon remains an out-of-domain failure. There is no basis to claim that all further improvement below4M parameters is impossible. This is the selected, tested final release from these experiments; it is not a guarantee of perfect colourisation.
40
+
41
+ ## Actual production image pipeline comparison
42
+
43
+ {
44
+ "n_total": 80,
45
+ "n_color": 78,
46
+ "summary": {
47
+ "old_space": {
48
+ "ab_error": 16.144625015747852,
49
+ "patch_excess": 1.8169478370020022,
50
+ "color_coverage": 0.47298392271384215,
51
+ "missed_color": 0.24084079671968467
52
+ },
53
+ "release": {
54
+ "ab_error": 14.243927399317423,
55
+ "patch_excess": 0.6932277647444071,
56
+ "color_coverage": 0.479247068747496,
57
+ "missed_color": 0.1663338435453805
58
+ }
59
+ },
60
+ "median_pipeline_seconds_L4": 0.06851194500001156
61
+ }
62
+
63
+ This comparison preserves aspect ratio and original output resolution before measuring the resized results. Median pipeline time excludes ZeroGPU queue/allocation and network transfer.
SHA256SUMS.json CHANGED
@@ -1,44 +1,23 @@
1
  {
2
- "DEPLOYMENT.md": "089cadec89caa645a5f2617f869ec50186dd8db574b0680a51d4bc12f84b96cb",
3
- "README.md": "f05ab35140319d6d8d5a65669d62645e3909b1ec1bd233bd66e0af3c1b135eb3",
4
- "RESEARCH_REPORT.md": "6730cf20f7a584707fb0c084b882ac87db1ef858d72e7d5e716e5e0d382ddca1",
5
- "RESEARCH_ROUND2.md": "19f42c72003cb685f46b315105ad2d4e62a10a7e0cd1455ab11db51bd5083728",
6
- "colorization_project_log.md": "66dbd61e5e45f208fdd36525f19c232678892295d6b31882a5fb71627cb731b9",
7
- "colorize_onnx.py": "4ec6bc9dba4a998243e97b3bd824d25639455e6246a47886be1ee796121cf1ba",
8
- "colorizer.json": "9f1db0072e91747bd6e9d6de1e2387440a4a2e0669554e499bbf29288a9758cd",
9
- "colorizer.onnx": "0ef86749901e66ad53b1e8e1d940330572e2f4c9347ac01e7bc02c7683f8c79a",
10
- "config.json": "90299ee25ee3c3e645b362ddbd5a859b9e739c2a0a1c8a5e9ea79df171288bff",
11
- "export_onnx.py": "4b0b8494f0eba3e813457fe393c3b96ce1ab35fefcf5f63a9251f4e852552737",
12
- "inference.py": "db958511d7a9eecec4c5730d96ac3f0005e002720b1a7f4b035e29cac7e7c5e3",
13
- "model.py": "4cc57f82ffd6378bdf23a088ce7a1ed56c09e5673de3ee5232b8ee1cacc9be0a",
14
- "model.safetensors": "0e4c417375684a044860f8af3ac3a2fb44e1a5729254ca33abda758ea71aea6e",
15
- "reports/round2/coco_manifest.json": "80061d796a1e4f08b508203cd45513916f2f0e74efa0c516673413c44f9d6dcb",
16
- "reports/round2/coco_revision.json": "c83949c5adf6e0e29dfb1d37c6beff0044bfa6a6a7161a6e0264bdbf41afd450",
17
- "reports/round2/coco_validation200.json": "bf6adcb05a2a0f92a4f428073ff95ff9977064a2a99b3b911ee5c3a5797955ea",
18
- "reports/round2/environment-lock.txt": "f95748796cbc5d0667fe17163b6054fb70567a9e87206473e743d150adc26e45",
19
- "reports/round2/final_coco100.json": "dabe07889d4ce4fc512f60768aa5c3c6268060464e21aed9c4c1fe28294675a1",
20
- "reports/round2/final_coco100.png": "2ba6c18c60e7858574f45f1fe6b1d25ef14031d8b92a3986ec057f5c945c337e",
21
- "reports/round2/final_integrity.json": "de32a8c9173efdc112c776a4937a5024b61814e93b16818eeccf2c43bc60b62a",
22
- "reports/round2/final_results.json": "40e2e317c3d083be9dedefdcc0ce942d0b972fd64164201680a6aeab589a870b",
23
- "reports/round2/final_test200.json": "d525d24e8706ff3f7ee3478c1ab95b8d5875d0d8242f6389dc29b9416449ce63",
24
- "reports/round2/final_test_comparison.png": "9681e45700aa5def0f69f67a49a45cadfddb06c1dc4b321995e09641eebf7f73",
25
- "reports/round2/final_visual.json": "0ca89a7d8017046c085c7983374706df0c1970284d95a8519cf16bf4d65065a8",
26
- "reports/round2/final_visual.png": "df2b3c271150059ab2f0d0a10f7fe889f1d3878c6096e14e0fd5df13edd7022a",
27
- "reports/round2/hub_refs.json": "e714d59b825a333abbea48270353a8dffd5c1b1b8b629028a729fac063f7cbd8",
28
- "reports/round2/jobs.json": "aa8dc41c57ae6ee41b5e51e8aff09055dafb536424413240cbeff3fcbabcbacc",
29
- "reports/round2/manifest.json": "adaf3a771a4c1cd3e1c58e6d74eae2a164e1d00ddbca8232067711111caa62ea",
30
- "reports/round2/onnx_benchmark.json": "2afbcbd29f9619dcc0cad1954f8fe2af3fdd11b94235b53883e8e0db53ef67f0",
31
- "reports/round2/onnx_export.json": "9f1db0072e91747bd6e9d6de1e2387440a4a2e0669554e499bbf29288a9758cd",
32
- "reports/round2/selection_protocol.md": "c297f6141a6c5419c2d6bba4ea1ecb5ce7295a78f9d16abec9e38224d5000a7b",
33
- "reports/round2/tests-final.txt": "60e907edbabdfcc838d135fbd18b7733ab87c401bbf608c734f589abcc4687f8",
34
- "reports/round2/weight_selection.json": "5ca680f16fab8fedb1ca1a14f59a127ee5d5a71e21fb3560f78417b530201df0",
35
- "requirements-onnx.txt": "c243944cabecd501038b8027dc5fd08f6d2ce74bcc47cc121f5c8df1b8c4445c",
36
- "requirements.txt": "a5b6a1804d983178099da00bad2f92a4196009384e431119cc73c5433d249622",
37
- "spatial.py": "d13abe399ef049e21a6459a7003461afaae0c00e0c560262c6cf375de4c9884a",
38
- "training/color_statistics.npz": "1d14920885324137169e63f908a413ebf083e8db1e604d2333847fd2094f36a6",
39
- "training/environment.txt": "ee1e54f3b86a977f74fbc0283ce0cfd2fcf54cc4c8ec4b9096634746eadcc18b",
40
- "training/history.json": "714ff0c8364e33e75cff21b6abe94f78db6c4536faf016ad1f591eb0ebb7dc72",
41
- "training/run_config.json": "6cdd4da9e0ae293e8f1895dd03d28bfb1f45c3abf19fc147cd8c18e6dfd36559",
42
- "training/split_manifest.json": "dc5901733751e8585f959918e46ce75b520c6e241a79d26ab9b3df943a12ac38",
43
- "upload_main.py": "d59fe5ebfc27f6051b7e7c6d8ad66b23092fa61b3db9fc87b33bff0fad2b483a"
44
  }
 
1
  {
2
+ "DEPLOYMENT.md": "201879644b48093363f025589641bc2a264ec8097512b4b4c30b621f3b9d9ee6",
3
+ "PIPELINE_EVAL.json": "595be88704d580c01a3c4dd642409b3dc5dd25e86ca1c7eb40a696605f181a19",
4
+ "QA.json": "b249b1d49045e31b0bf85f9e01515b91a72c4dd973963751c30d14272d262b90",
5
+ "README.md": "5ade5fd9022a03937b91fbd3b0fa653ae93c068034b54b35677c3db47a9ccc59",
6
+ "RELEASE_MANIFEST.json": "872eebfb322c8990b5e6ac493e4e4068b87f8daac0ddf219b8b55299514ca03c",
7
+ "RELEASE_REPORT.md": "69677ec5e13f5c1249f59b01c31ed1b0f1588bddc216bce9167b5d31423a5574",
8
+ "app.py": "58be79b62642c2f3393860791a1a7b39d8f2e83ac79caf6545839124ec787404",
9
+ "colorization_project_log.md": "699238b4078fca93b15f4f2d951e439651502338dac4bf3a5b5a6d0b0449de21",
10
+ "colorize_image.py": "9a85b7633bdf8a0d63f740ae69ed76644bb2087da4b21f8c1e01ca804cd4e6b2",
11
+ "colorize_onnx.py": "cc106f066878a7372f4260279620238d46a3841baf09fbbe954f61a684199d05",
12
+ "colorizer.json": "7d444d5f560b2ea6a070f73750f75f66aa2c61d3dc21a8e4300ed5f5196da179",
13
+ "colorizer.onnx": "911fb777328fcfbeeb89cc25dca70092cf2de4686ede1edd44a8ccebc89ea341",
14
+ "config.json": "eaeb49b39f9116bfed0d954e81852dc93476298752787dba88a63c0eec97ccec",
15
+ "export_onnx.py": "f46a413c2b1b2a6e130cb7d2ad8d6d69c16a16955bf830efdc4c04c30d47ca60",
16
+ "inference.py": "0ac1f382205b42cef0afaa9173017fa660e6859194a65a61b9b9052c437cd476",
17
+ "model.py": "31628744fb5d362d91342063c90eb38b2b97fda8c91e303e4a58b6f388f53c55",
18
+ "model.safetensors": "ec1f27d74533adc83f7ab3639a091fc4d8738a434dafc7d172c7873c28a9e715",
19
+ "requirements-onnx.txt": "9bcca098a9da63b31fb9d803dfd7f99b753ad125ee9bce3a5bbe52e368739e11",
20
+ "requirements-space.txt": "4c7f918aadc4afb31ec6808131570f9978cf0bcf9e2af562e93adc749e3245cf",
21
+ "requirements.txt": "c855eb70020dc89f71fd38e44a0be2cfb80f2fe3d442d5ba45c91398c336f83f",
22
+ "semantic_model.py": "b019358cbcaa214743cbd244d20eb6e59eeff0ab5264093ec0927be8529b25a2"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  }
app.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio and ZeroGPU entry point. The model revision is pinned at release."""
2
+ import os
3
+ from pathlib import Path
4
+ import spaces
5
+ import torch
6
+ import gradio as gr
7
+ from huggingface_hub import snapshot_download
8
+ from inference import load_colorizer, prepare, chroma_coefficients, render
9
+
10
+ REPO=os.getenv('MODEL_ID','User-2468/mini-unet-colorizer')
11
+ REV=os.getenv('MODEL_REVISION','v3.0.0')
12
+ CPU=os.getenv('ZEROGPU_CPU_TEST')=='1'
13
+ DEVICE='cpu' if CPU else 'cuda'
14
+ if CPU:torch.set_num_threads(min(4,os.cpu_count() or 1))
15
+ path=Path(REPO)
16
+ if not path.is_dir():path=Path(snapshot_download(REPO,revision=REV,allow_patterns=['model.safetensors','config.json']))
17
+ MODEL=load_colorizer(path,DEVICE)
18
+
19
+ @spaces.GPU(duration=20)
20
+ def infer(small,radius):
21
+ return chroma_coefficients(MODEL,small,int(radius))
22
+
23
+ def run(image,strength,smoothing):
24
+ if image is None:raise gr.Error('Upload a photograph first.')
25
+ radius={'Balanced':8,'Gentle':4,'Strong':16}[smoothing]
26
+ try:
27
+ light,alpha,small=prepare(image,256)
28
+ coefficients=infer(small,radius)
29
+ result=render(light,alpha,coefficients,strength)
30
+ except (ValueError,RuntimeError,TypeError) as exc:
31
+ raise gr.Error(str(exc)) from exc
32
+ return result
33
+
34
+ with gr.Blocks(title='Mini Photo Colorizer') as demo:
35
+ gr.Markdown('# Mini Photo Colorizer\nAdd plausible colour to a black-and-white photograph. Original colours may be unknowable.')
36
+ with gr.Row():
37
+ source=gr.Image(type='pil',image_mode='RGBA',label='Original photograph',sources=['upload','clipboard'],height=440)
38
+ result=gr.Image(type='pil',label='Colourised photograph',format='png',height=440)
39
+ with gr.Row():
40
+ strength=gr.Slider(0,1.5,value=1.,step=.05,label='Colour strength')
41
+ smoothing=gr.Radio(['Gentle','Balanced','Strong'],value='Balanced',label='Colour smoothing')
42
+ button=gr.Button('Colourise',variant='primary')
43
+ button.click(run,[source,strength,smoothing],result,api_name='colorize',concurrency_limit=1)
44
+ gr.ClearButton([source,result])
45
+ gr.Markdown('3.995 million parameters · Full-resolution output, up to 12 megapixels · PNG download\n\nBest suited to ordinary photographs. Colours can be muted or inaccurate on unusual scenes, tiny objects and heavily damaged scans.')
46
+ demo.queue(default_concurrency_limit=1,max_size=12)
47
+ if __name__=='__main__':demo.launch()
colorization_project_log.md CHANGED
@@ -249,3 +249,8 @@ All GPU weights were recovered through checksum-verified artifact transport;
249
  best/last weights and experiment records are retained. Main and stable remain
250
  unchanged. The saved release includes a guarded uploader targeting main.
251
  The current candidate is release-v2; release/ preserves the earlier repair.
 
 
 
 
 
 
249
  best/last weights and experiment records are retained. Main and stable remain
250
  unchanged. The saved release includes a guarded uploader targeting main.
251
  The current candidate is release-v2; release/ preserves the earlier repair.
252
+
253
+
254
+ ## Final release — 28 September 2026
255
+
256
+ Selected round6 palette9000 after rejecting round7 and final critic variants. All3,994,676 parameters include the encoder. Root files now use SemanticColorizer, with aspect-preserving guided colour upsampling, gamut compression, CPU/CUDA support, fixed256 ONNX and Gradio/ZeroGPU app. See RELEASE_REPORT.md, RELEASE_MANIFEST.json and QA.json. Stable branch unchanged.
colorize_image.py CHANGED
@@ -1,176 +1,2 @@
1
- # /// script
2
- # requires-python = ">=3.10"
3
- # dependencies = [
4
- # "torch>=2.3",
5
- # "torchvision>=0.18",
6
- # "huggingface_hub>=0.24",
7
- # "safetensors>=0.4",
8
- # "scikit-image>=0.22",
9
- # "pillow>=10.0",
10
- # "numpy",
11
- # ]
12
- # ///
13
- """
14
- Colorize photos with a SmallUNetColorizer checkpoint trained by colorize_train.py
15
- (classification-head version: predicts a distribution over quantized Lab ab
16
- bins per pixel, decoded with an annealed mean).
17
-
18
- Usage:
19
- uv run colorize_image.py --model User-2468/mini-unet-colorizer photo.jpg
20
- uv run colorize_image.py --model ./local_checkpoint --temperature 0.2 a.jpg b.jpg
21
-
22
- --temperature is the main "vividness" knob: lower values weight the decode
23
- toward the most likely color bin (more saturated, can be a bit blotchy);
24
- higher values move toward the full expectation over the distribution
25
- (smoother, but can drift back toward desaturated -- the same hedging effect
26
- plain regression had). 0.38 (the default) is a reasonable middle ground.
27
- --saturation-boost is an optional *additional* post-decode multiplier on top
28
- of that, for further hand-tuning after picking a temperature.
29
- """
30
- import argparse
31
- from pathlib import Path
32
-
33
- import numpy as np
34
- import torch
35
- import torch.nn as nn
36
- import torch.nn.functional as F
37
- from huggingface_hub import PyTorchModelHubMixin
38
- from PIL import Image
39
- from skimage.color import lab2rgb, rgb2lab
40
-
41
-
42
- def double_conv(in_ch, out_ch):
43
- return nn.Sequential(
44
- nn.Conv2d(in_ch, out_ch, 3, padding=1, bias=False),
45
- nn.BatchNorm2d(out_ch),
46
- nn.ReLU(inplace=True),
47
- nn.Conv2d(out_ch, out_ch, 3, padding=1, bias=False),
48
- nn.BatchNorm2d(out_ch),
49
- nn.ReLU(inplace=True),
50
- )
51
-
52
-
53
- class DilatedContextBlock(nn.Module):
54
- def __init__(self, channels, mid_ch=96, dilations=(2, 4, 8)):
55
- super().__init__()
56
- self.proj_in = nn.Sequential(
57
- nn.Conv2d(channels, mid_ch, 1, bias=False),
58
- nn.BatchNorm2d(mid_ch),
59
- nn.ReLU(inplace=True),
60
- )
61
- layers = []
62
- for d in dilations:
63
- layers += [
64
- nn.Conv2d(mid_ch, mid_ch, 3, padding=d, dilation=d, bias=False),
65
- nn.BatchNorm2d(mid_ch),
66
- nn.ReLU(inplace=True),
67
- ]
68
- self.dilated = nn.Sequential(*layers)
69
- self.proj_out = nn.Sequential(
70
- nn.Conv2d(mid_ch, channels, 1, bias=False),
71
- nn.BatchNorm2d(channels),
72
- )
73
- self.relu = nn.ReLU(inplace=True)
74
-
75
- def forward(self, x):
76
- y = self.proj_in(x)
77
- y = self.dilated(y)
78
- y = self.proj_out(y)
79
- return self.relu(x + y)
80
-
81
-
82
- class SmallUNetColorizer(
83
- nn.Module,
84
- PyTorchModelHubMixin,
85
- pipeline_tag="image-to-image",
86
- license="apache-2.0",
87
- tags=["colorization", "unet", "image-to-image", "classification"],
88
- ):
89
- def __init__(self, bin_centers, in_ch: int = 1, base: int = 44,
90
- context_mid_ch: int = 96, context_dilations=(2, 4, 8)):
91
- super().__init__()
92
- self.in_ch, self.base = in_ch, base
93
- num_bins = len(bin_centers)
94
- self.num_bins = num_bins
95
- self.register_buffer("bin_centers", torch.tensor(bin_centers, dtype=torch.float32))
96
-
97
- self.enc1 = double_conv(in_ch, base)
98
- self.enc2 = double_conv(base, base * 2)
99
- self.enc3 = double_conv(base * 2, base * 4)
100
- self.enc4 = double_conv(base * 4, base * 8)
101
- self.pool = nn.MaxPool2d(2)
102
- self.context = DilatedContextBlock(base * 8, mid_ch=context_mid_ch,
103
- dilations=tuple(context_dilations))
104
- self.up3 = nn.ConvTranspose2d(base * 8, base * 4, 2, stride=2)
105
- self.dec3 = double_conv(base * 8, base * 4)
106
- self.up2 = nn.ConvTranspose2d(base * 4, base * 2, 2, stride=2)
107
- self.dec2 = double_conv(base * 4, base * 2)
108
- self.up1 = nn.ConvTranspose2d(base * 2, base, 2, stride=2)
109
- self.dec1 = double_conv(base * 2, base)
110
- self.out_conv = nn.Conv2d(base, num_bins, 1)
111
-
112
- def forward(self, x):
113
- e1 = self.enc1(x)
114
- e2 = self.enc2(self.pool(e1))
115
- e3 = self.enc3(self.pool(e2))
116
- e4 = self.context(self.enc4(self.pool(e3)))
117
- d3 = self.dec3(torch.cat([self.up3(e4), e3], dim=1))
118
- d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=1))
119
- d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=1))
120
- return self.out_conv(d1)
121
-
122
- def decode(self, logits, temperature: float = 0.38):
123
- logp = F.log_softmax(logits, dim=1)
124
- probs_t = F.softmax(logp / temperature, dim=1)
125
- return torch.einsum("bqhw,qc->bchw", probs_t, self.bin_centers)
126
-
127
-
128
- def colorize(model, img, size, temperature, saturation_boost, device):
129
- img = img.convert("RGB").resize((size, size))
130
- arr = np.asarray(img).astype(np.float32) / 255.0
131
- lab = rgb2lab(arr).astype(np.float32)
132
- L = torch.from_numpy(lab[:, :, 0:1] / 50.0 - 1.0).permute(2, 0, 1)[None].to(device)
133
-
134
- with torch.no_grad():
135
- logits = model(L)
136
- ab = model.decode(logits, temperature=temperature)[0].permute(1, 2, 0).cpu().numpy()
137
-
138
- ab = np.clip(ab * saturation_boost, -128, 127)
139
- L_out = (L[0, 0].cpu().numpy() + 1.0) * 50.0
140
- lab_out = np.concatenate([L_out[:, :, None], ab], axis=-1)
141
- rgb_out = np.clip(lab2rgb(lab_out), 0, 1)
142
- return Image.fromarray((rgb_out * 255).astype(np.uint8))
143
-
144
-
145
- def main():
146
- p = argparse.ArgumentParser(description=__doc__)
147
- p.add_argument("images", nargs="+", help="Path(s) to input photo(s)")
148
- p.add_argument("--model", required=True, help="Hub model id or local checkpoint path")
149
- p.add_argument("--size", type=int, default=256, help="Resize input to this square size")
150
- p.add_argument("--temperature", type=float, default=0.38,
151
- help="Annealed-mean decode temperature. Lower = more vivid/mode-like, "
152
- "higher = smoother but can desaturate again. Try 0.15-0.6.")
153
- p.add_argument("--saturation-boost", type=float, default=1.0,
154
- help="Extra multiplier on the decoded ab, applied after temperature. "
155
- "1.0 = no extra boost.")
156
- p.add_argument("--output-dir", default="./colorized")
157
- args = p.parse_args()
158
-
159
- device = "cuda" if torch.cuda.is_available() else "cpu"
160
- print(f"Loading {args.model} on {device} ...")
161
- model = SmallUNetColorizer.from_pretrained(args.model).to(device).eval()
162
- print(f"({model.num_bins} color bins)")
163
-
164
- out_dir = Path(args.output_dir)
165
- out_dir.mkdir(parents=True, exist_ok=True)
166
-
167
- for path in args.images:
168
- img = Image.open(path)
169
- result = colorize(model, img, args.size, args.temperature, args.saturation_boost, device)
170
- out_path = out_dir / f"{Path(path).stem}_colorized.png"
171
- result.save(out_path)
172
- print(f"{path} -> {out_path}")
173
-
174
-
175
- if __name__ == "__main__":
176
- main()
 
1
+ from inference import main
2
+ if __name__ == "__main__": main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
colorize_onnx.py CHANGED
@@ -1,32 +1,24 @@
1
- """Run the exported chroma pipeline without importing PyTorch."""
 
 
 
2
  import argparse
3
- from pathlib import Path
4
  import numpy as np
5
  import onnxruntime as ort
6
  from PIL import Image,ImageOps
7
  from skimage.color import rgb2lab,lab2rgb
8
 
9
- def load_session(path,threads=2):
10
- opts=ort.SessionOptions();opts.intra_op_num_threads=threads;opts.inter_op_num_threads=1
11
- return ort.InferenceSession(str(path),sess_options=opts,providers=['CPUExecutionProvider'])
12
-
13
- def colorize(session,image,size=256):
14
- if size<8:raise ValueError('size must be at least8')
15
  image=ImageOps.exif_transpose(image).convert('RGB')
16
- L=rgb2lab(np.asarray(image,dtype=np.float32)/255)[...,0].astype(np.float32)
17
- h,w=L.shape;scale=min(size/max(h,w),1)
18
- target=(max(8,round(w*scale)),max(8,round(h*scale)))
19
- small=np.asarray(Image.fromarray(L).resize(target,Image.Resampling.BILINEAR),dtype=np.float32)
20
- x=(small[None,None]/50-1).copy()
21
- ab=session.run(['chroma'],{'luminance':x})[0][0]
22
- full=np.stack([np.asarray(Image.fromarray(c).resize((w,h),Image.Resampling.BILINEAR)) for c in ab],axis=-1)
23
- rgb=np.clip(lab2rgb(np.concatenate([L[...,None],full],axis=-1)),0,1)
24
- return Image.fromarray(np.rint(rgb*255).astype(np.uint8))
25
 
26
  if __name__=='__main__':
27
- p=argparse.ArgumentParser(__doc__);p.add_argument('images',nargs='+');p.add_argument('--model',required=True)
28
- p.add_argument('--output-dir',default='colorized');p.add_argument('--size',type=int,default=256);p.add_argument('--threads',type=int,default=2)
29
- a=p.parse_args();session=load_session(a.model,a.threads);out=Path(a.output_dir);out.mkdir(parents=True,exist_ok=True)
30
- for source in a.images:
31
- with Image.open(source) as im:result=colorize(session,im,a.size)
32
- dest=out/(Path(source).stem+'_colorized.png');result.save(dest);print(dest)
 
1
+ """Minimal ONNX-only image example; fixed square network input.
2
+
3
+ For aspect-preserving guided upsampling and gamut compression use inference.py.
4
+ """
5
  import argparse
 
6
  import numpy as np
7
  import onnxruntime as ort
8
  from PIL import Image,ImageOps
9
  from skimage.color import rgb2lab,lab2rgb
10
 
11
+ def colorize(image,model='colorizer.onnx'):
 
 
 
 
 
12
  image=ImageOps.exif_transpose(image).convert('RGB')
13
+ if image.width*image.height>12_000_000:raise ValueError('Maximum 12 megapixels')
14
+ light=rgb2lab(np.asarray(image,np.float32)/255)[...,0].astype(np.float32)
15
+ resized=np.asarray(Image.fromarray(light).resize((256,256),Image.Resampling.BILINEAR))
16
+ session=ort.InferenceSession(model,providers=['CPUExecutionProvider'])
17
+ ab=session.run(['ab'],{'L':(resized[None,None]/50-1).astype(np.float32)})[0][0]
18
+ ab=np.stack([np.asarray(Image.fromarray(channel).resize(image.size,Image.Resampling.BILINEAR)) for channel in ab],-1)
19
+ rgb=np.clip(lab2rgb(np.concatenate([light[...,None],ab],-1)),0,1)
20
+ return Image.fromarray(np.uint8(np.rint(rgb*255)))
 
21
 
22
  if __name__=='__main__':
23
+ p=argparse.ArgumentParser();p.add_argument('input');p.add_argument('output');p.add_argument('--model',default='colorizer.onnx');a=p.parse_args()
24
+ colorize(Image.open(a.input),a.model).save(a.output)
 
 
 
 
colorizer.json CHANGED
@@ -1,54 +1,30 @@
1
  {
2
- "input": "N,1,H,W Lab L*/50-1; H,W>=8",
3
- "output": "N,2,H,W Lab chroma a,b",
4
- "guided_radius": 8,
5
- "guided_epsilon": 0.001,
6
- "temperature": 0.38,
7
  "opset": 17,
8
- "checks": [
9
- {
10
- "shape": [
11
- 1,
12
- 1,
13
- 256,
14
- 256
15
- ],
16
- "max_ab_difference": 8.58306884765625e-06,
17
- "mean_ab_difference": 1.0279118214384653e-06
18
- },
19
- {
20
- "shape": [
21
- 1,
22
- 1,
23
- 173,
24
- 241
25
- ],
26
- "max_ab_difference": 7.3909759521484375e-06,
27
- "mean_ab_difference": 1.0783561492644367e-06
28
- },
29
- {
30
- "shape": [
31
- 2,
32
- 1,
33
- 64,
34
- 80
35
- ],
36
- "max_ab_difference": 7.152557373046875e-06,
37
- "mean_ab_difference": 9.707116532808868e-07
38
- },
39
- {
40
- "shape": [
41
- 1,
42
- 1,
43
- 8,
44
- 9
45
- ],
46
- "max_ab_difference": 1.5497207641601562e-06,
47
- "mean_ab_difference": 3.6218099808138504e-07
48
- }
49
- ],
50
- "parameters": 3968892,
51
- "weights_source": "release-v2",
52
- "runtime": "1.30.0",
53
- "bytes": 15885757
54
  }
 
1
  {
2
+ "release": "v3.0.0",
3
+ "architecture": "SemanticColorizer",
4
+ "parameters": 3994676,
5
+ "format": "onnx",
 
6
  "opset": 17,
7
+ "input": {
8
+ "name": "L",
9
+ "shape": [
10
+ 1,
11
+ 1,
12
+ 256,
13
+ 256
14
+ ],
15
+ "dtype": "float32",
16
+ "normalization": "Lab L /50 -1"
17
+ },
18
+ "output": {
19
+ "name": "ab",
20
+ "shape": [
21
+ 1,
22
+ 2,
23
+ 256,
24
+ 256
25
+ ],
26
+ "dtype": "float32",
27
+ "units": "unscaled Lab ab"
28
+ },
29
+ "postprocessing": "See inference.py; no colour bins or temperature decode."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
colorizer.onnx CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:0ef86749901e66ad53b1e8e1d940330572e2f4c9347ac01e7bc02c7683f8c79a
3
- size 15885757
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:911fb777328fcfbeeb89cc25dca70092cf2de4686ede1edd44a8ccebc89ea341
3
+ size 16039257
config.json CHANGED
@@ -1,956 +1,7 @@
1
  {
2
- "base": 44,
3
- "bin_centers": [
4
- [
5
- -75.0,
6
- 25.0
7
- ],
8
- [
9
- -75.0,
10
- 35.0
11
- ],
12
- [
13
- -75.0,
14
- 45.0
15
- ],
16
- [
17
- -75.0,
18
- 55.0
19
- ],
20
- [
21
- -75.0,
22
- 65.0
23
- ],
24
- [
25
- -75.0,
26
- 75.0
27
- ],
28
- [
29
- -65.0,
30
- 25.0
31
- ],
32
- [
33
- -65.0,
34
- 35.0
35
- ],
36
- [
37
- -65.0,
38
- 45.0
39
- ],
40
- [
41
- -65.0,
42
- 55.0
43
- ],
44
- [
45
- -65.0,
46
- 65.0
47
- ],
48
- [
49
- -65.0,
50
- 75.0
51
- ],
52
- [
53
- -55.0,
54
- 5.0
55
- ],
56
- [
57
- -55.0,
58
- 15.0
59
- ],
60
- [
61
- -55.0,
62
- 25.0
63
- ],
64
- [
65
- -55.0,
66
- 35.0
67
- ],
68
- [
69
- -55.0,
70
- 45.0
71
- ],
72
- [
73
- -55.0,
74
- 55.0
75
- ],
76
- [
77
- -55.0,
78
- 65.0
79
- ],
80
- [
81
- -55.0,
82
- 75.0
83
- ],
84
- [
85
- -45.0,
86
- -15.0
87
- ],
88
- [
89
- -45.0,
90
- -5.0
91
- ],
92
- [
93
- -45.0,
94
- 5.0
95
- ],
96
- [
97
- -45.0,
98
- 15.0
99
- ],
100
- [
101
- -45.0,
102
- 25.0
103
- ],
104
- [
105
- -45.0,
106
- 35.0
107
- ],
108
- [
109
- -45.0,
110
- 45.0
111
- ],
112
- [
113
- -45.0,
114
- 55.0
115
- ],
116
- [
117
- -45.0,
118
- 65.0
119
- ],
120
- [
121
- -45.0,
122
- 75.0
123
- ],
124
- [
125
- -45.0,
126
- 85.0
127
- ],
128
- [
129
- -35.0,
130
- -35.0
131
- ],
132
- [
133
- -35.0,
134
- -25.0
135
- ],
136
- [
137
- -35.0,
138
- -15.0
139
- ],
140
- [
141
- -35.0,
142
- -5.0
143
- ],
144
- [
145
- -35.0,
146
- 5.0
147
- ],
148
- [
149
- -35.0,
150
- 15.0
151
- ],
152
- [
153
- -35.0,
154
- 25.0
155
- ],
156
- [
157
- -35.0,
158
- 35.0
159
- ],
160
- [
161
- -35.0,
162
- 45.0
163
- ],
164
- [
165
- -35.0,
166
- 55.0
167
- ],
168
- [
169
- -35.0,
170
- 65.0
171
- ],
172
- [
173
- -35.0,
174
- 75.0
175
- ],
176
- [
177
- -35.0,
178
- 85.0
179
- ],
180
- [
181
- -35.0,
182
- 95.0
183
- ],
184
- [
185
- -25.0,
186
- -35.0
187
- ],
188
- [
189
- -25.0,
190
- -25.0
191
- ],
192
- [
193
- -25.0,
194
- -15.0
195
- ],
196
- [
197
- -25.0,
198
- -5.0
199
- ],
200
- [
201
- -25.0,
202
- 5.0
203
- ],
204
- [
205
- -25.0,
206
- 15.0
207
- ],
208
- [
209
- -25.0,
210
- 25.0
211
- ],
212
- [
213
- -25.0,
214
- 35.0
215
- ],
216
- [
217
- -25.0,
218
- 45.0
219
- ],
220
- [
221
- -25.0,
222
- 55.0
223
- ],
224
- [
225
- -25.0,
226
- 65.0
227
- ],
228
- [
229
- -25.0,
230
- 75.0
231
- ],
232
- [
233
- -25.0,
234
- 85.0
235
- ],
236
- [
237
- -25.0,
238
- 95.0
239
- ],
240
- [
241
- -15.0,
242
- -45.0
243
- ],
244
- [
245
- -15.0,
246
- -35.0
247
- ],
248
- [
249
- -15.0,
250
- -25.0
251
- ],
252
- [
253
- -15.0,
254
- -15.0
255
- ],
256
- [
257
- -15.0,
258
- -5.0
259
- ],
260
- [
261
- -15.0,
262
- 5.0
263
- ],
264
- [
265
- -15.0,
266
- 15.0
267
- ],
268
- [
269
- -15.0,
270
- 25.0
271
- ],
272
- [
273
- -15.0,
274
- 35.0
275
- ],
276
- [
277
- -15.0,
278
- 45.0
279
- ],
280
- [
281
- -15.0,
282
- 55.0
283
- ],
284
- [
285
- -15.0,
286
- 65.0
287
- ],
288
- [
289
- -15.0,
290
- 75.0
291
- ],
292
- [
293
- -15.0,
294
- 85.0
295
- ],
296
- [
297
- -15.0,
298
- 95.0
299
- ],
300
- [
301
- -5.0,
302
- -55.0
303
- ],
304
- [
305
- -5.0,
306
- -45.0
307
- ],
308
- [
309
- -5.0,
310
- -35.0
311
- ],
312
- [
313
- -5.0,
314
- -25.0
315
- ],
316
- [
317
- -5.0,
318
- -15.0
319
- ],
320
- [
321
- -5.0,
322
- -5.0
323
- ],
324
- [
325
- -5.0,
326
- 5.0
327
- ],
328
- [
329
- -5.0,
330
- 15.0
331
- ],
332
- [
333
- -5.0,
334
- 25.0
335
- ],
336
- [
337
- -5.0,
338
- 35.0
339
- ],
340
- [
341
- -5.0,
342
- 45.0
343
- ],
344
- [
345
- -5.0,
346
- 55.0
347
- ],
348
- [
349
- -5.0,
350
- 65.0
351
- ],
352
- [
353
- -5.0,
354
- 75.0
355
- ],
356
- [
357
- -5.0,
358
- 85.0
359
- ],
360
- [
361
- 5.0,
362
- -65.0
363
- ],
364
- [
365
- 5.0,
366
- -55.0
367
- ],
368
- [
369
- 5.0,
370
- -45.0
371
- ],
372
- [
373
- 5.0,
374
- -35.0
375
- ],
376
- [
377
- 5.0,
378
- -25.0
379
- ],
380
- [
381
- 5.0,
382
- -15.0
383
- ],
384
- [
385
- 5.0,
386
- -5.0
387
- ],
388
- [
389
- 5.0,
390
- 5.0
391
- ],
392
- [
393
- 5.0,
394
- 15.0
395
- ],
396
- [
397
- 5.0,
398
- 25.0
399
- ],
400
- [
401
- 5.0,
402
- 35.0
403
- ],
404
- [
405
- 5.0,
406
- 45.0
407
- ],
408
- [
409
- 5.0,
410
- 55.0
411
- ],
412
- [
413
- 5.0,
414
- 65.0
415
- ],
416
- [
417
- 5.0,
418
- 75.0
419
- ],
420
- [
421
- 5.0,
422
- 85.0
423
- ],
424
- [
425
- 15.0,
426
- -75.0
427
- ],
428
- [
429
- 15.0,
430
- -65.0
431
- ],
432
- [
433
- 15.0,
434
- -55.0
435
- ],
436
- [
437
- 15.0,
438
- -45.0
439
- ],
440
- [
441
- 15.0,
442
- -35.0
443
- ],
444
- [
445
- 15.0,
446
- -25.0
447
- ],
448
- [
449
- 15.0,
450
- -15.0
451
- ],
452
- [
453
- 15.0,
454
- -5.0
455
- ],
456
- [
457
- 15.0,
458
- 5.0
459
- ],
460
- [
461
- 15.0,
462
- 15.0
463
- ],
464
- [
465
- 15.0,
466
- 25.0
467
- ],
468
- [
469
- 15.0,
470
- 35.0
471
- ],
472
- [
473
- 15.0,
474
- 45.0
475
- ],
476
- [
477
- 15.0,
478
- 55.0
479
- ],
480
- [
481
- 15.0,
482
- 65.0
483
- ],
484
- [
485
- 15.0,
486
- 75.0
487
- ],
488
- [
489
- 15.0,
490
- 85.0
491
- ],
492
- [
493
- 25.0,
494
- -75.0
495
- ],
496
- [
497
- 25.0,
498
- -65.0
499
- ],
500
- [
501
- 25.0,
502
- -55.0
503
- ],
504
- [
505
- 25.0,
506
- -45.0
507
- ],
508
- [
509
- 25.0,
510
- -35.0
511
- ],
512
- [
513
- 25.0,
514
- -25.0
515
- ],
516
- [
517
- 25.0,
518
- -15.0
519
- ],
520
- [
521
- 25.0,
522
- -5.0
523
- ],
524
- [
525
- 25.0,
526
- 5.0
527
- ],
528
- [
529
- 25.0,
530
- 15.0
531
- ],
532
- [
533
- 25.0,
534
- 25.0
535
- ],
536
- [
537
- 25.0,
538
- 35.0
539
- ],
540
- [
541
- 25.0,
542
- 45.0
543
- ],
544
- [
545
- 25.0,
546
- 55.0
547
- ],
548
- [
549
- 25.0,
550
- 65.0
551
- ],
552
- [
553
- 25.0,
554
- 75.0
555
- ],
556
- [
557
- 25.0,
558
- 85.0
559
- ],
560
- [
561
- 35.0,
562
- -85.0
563
- ],
564
- [
565
- 35.0,
566
- -75.0
567
- ],
568
- [
569
- 35.0,
570
- -65.0
571
- ],
572
- [
573
- 35.0,
574
- -55.0
575
- ],
576
- [
577
- 35.0,
578
- -45.0
579
- ],
580
- [
581
- 35.0,
582
- -35.0
583
- ],
584
- [
585
- 35.0,
586
- -25.0
587
- ],
588
- [
589
- 35.0,
590
- -15.0
591
- ],
592
- [
593
- 35.0,
594
- -5.0
595
- ],
596
- [
597
- 35.0,
598
- 5.0
599
- ],
600
- [
601
- 35.0,
602
- 15.0
603
- ],
604
- [
605
- 35.0,
606
- 25.0
607
- ],
608
- [
609
- 35.0,
610
- 35.0
611
- ],
612
- [
613
- 35.0,
614
- 45.0
615
- ],
616
- [
617
- 35.0,
618
- 55.0
619
- ],
620
- [
621
- 35.0,
622
- 65.0
623
- ],
624
- [
625
- 35.0,
626
- 75.0
627
- ],
628
- [
629
- 45.0,
630
- -95.0
631
- ],
632
- [
633
- 45.0,
634
- -85.0
635
- ],
636
- [
637
- 45.0,
638
- -75.0
639
- ],
640
- [
641
- 45.0,
642
- -65.0
643
- ],
644
- [
645
- 45.0,
646
- -55.0
647
- ],
648
- [
649
- 45.0,
650
- -45.0
651
- ],
652
- [
653
- 45.0,
654
- -35.0
655
- ],
656
- [
657
- 45.0,
658
- -25.0
659
- ],
660
- [
661
- 45.0,
662
- -15.0
663
- ],
664
- [
665
- 45.0,
666
- -5.0
667
- ],
668
- [
669
- 45.0,
670
- 5.0
671
- ],
672
- [
673
- 45.0,
674
- 15.0
675
- ],
676
- [
677
- 45.0,
678
- 25.0
679
- ],
680
- [
681
- 45.0,
682
- 35.0
683
- ],
684
- [
685
- 45.0,
686
- 45.0
687
- ],
688
- [
689
- 45.0,
690
- 55.0
691
- ],
692
- [
693
- 45.0,
694
- 65.0
695
- ],
696
- [
697
- 45.0,
698
- 75.0
699
- ],
700
- [
701
- 55.0,
702
- -95.0
703
- ],
704
- [
705
- 55.0,
706
- -85.0
707
- ],
708
- [
709
- 55.0,
710
- -75.0
711
- ],
712
- [
713
- 55.0,
714
- -65.0
715
- ],
716
- [
717
- 55.0,
718
- -55.0
719
- ],
720
- [
721
- 55.0,
722
- -45.0
723
- ],
724
- [
725
- 55.0,
726
- -35.0
727
- ],
728
- [
729
- 55.0,
730
- -25.0
731
- ],
732
- [
733
- 55.0,
734
- -15.0
735
- ],
736
- [
737
- 55.0,
738
- -5.0
739
- ],
740
- [
741
- 55.0,
742
- 5.0
743
- ],
744
- [
745
- 55.0,
746
- 15.0
747
- ],
748
- [
749
- 55.0,
750
- 25.0
751
- ],
752
- [
753
- 55.0,
754
- 35.0
755
- ],
756
- [
757
- 55.0,
758
- 45.0
759
- ],
760
- [
761
- 55.0,
762
- 55.0
763
- ],
764
- [
765
- 55.0,
766
- 65.0
767
- ],
768
- [
769
- 55.0,
770
- 75.0
771
- ],
772
- [
773
- 65.0,
774
- -105.0
775
- ],
776
- [
777
- 65.0,
778
- -95.0
779
- ],
780
- [
781
- 65.0,
782
- -85.0
783
- ],
784
- [
785
- 65.0,
786
- -75.0
787
- ],
788
- [
789
- 65.0,
790
- -65.0
791
- ],
792
- [
793
- 65.0,
794
- -55.0
795
- ],
796
- [
797
- 65.0,
798
- -45.0
799
- ],
800
- [
801
- 65.0,
802
- -35.0
803
- ],
804
- [
805
- 65.0,
806
- -25.0
807
- ],
808
- [
809
- 65.0,
810
- -15.0
811
- ],
812
- [
813
- 65.0,
814
- -5.0
815
- ],
816
- [
817
- 65.0,
818
- 5.0
819
- ],
820
- [
821
- 65.0,
822
- 15.0
823
- ],
824
- [
825
- 65.0,
826
- 25.0
827
- ],
828
- [
829
- 65.0,
830
- 35.0
831
- ],
832
- [
833
- 65.0,
834
- 45.0
835
- ],
836
- [
837
- 65.0,
838
- 55.0
839
- ],
840
- [
841
- 65.0,
842
- 65.0
843
- ],
844
- [
845
- 75.0,
846
- -105.0
847
- ],
848
- [
849
- 75.0,
850
- -95.0
851
- ],
852
- [
853
- 75.0,
854
- -45.0
855
- ],
856
- [
857
- 75.0,
858
- -35.0
859
- ],
860
- [
861
- 75.0,
862
- -25.0
863
- ],
864
- [
865
- 75.0,
866
- -15.0
867
- ],
868
- [
869
- 75.0,
870
- -5.0
871
- ],
872
- [
873
- 75.0,
874
- 5.0
875
- ],
876
- [
877
- 75.0,
878
- 15.0
879
- ],
880
- [
881
- 75.0,
882
- 25.0
883
- ],
884
- [
885
- 75.0,
886
- 35.0
887
- ],
888
- [
889
- 75.0,
890
- 45.0
891
- ],
892
- [
893
- 75.0,
894
- 55.0
895
- ],
896
- [
897
- 75.0,
898
- 65.0
899
- ],
900
- [
901
- 85.0,
902
- -45.0
903
- ],
904
- [
905
- 85.0,
906
- -35.0
907
- ],
908
- [
909
- 85.0,
910
- -25.0
911
- ],
912
- [
913
- 85.0,
914
- -15.0
915
- ],
916
- [
917
- 85.0,
918
- -5.0
919
- ],
920
- [
921
- 85.0,
922
- 5.0
923
- ],
924
- [
925
- 85.0,
926
- 35.0
927
- ],
928
- [
929
- 85.0,
930
- 45.0
931
- ],
932
- [
933
- 85.0,
934
- 65.0
935
- ],
936
- [
937
- 95.0,
938
- -55.0
939
- ],
940
- [
941
- 95.0,
942
- -45.0
943
- ],
944
- [
945
- 95.0,
946
- -35.0
947
- ]
948
- ],
949
- "context_dilations": [
950
- 2,
951
- 4,
952
- 8
953
- ],
954
- "context_mid_ch": 96,
955
- "in_ch": 1
956
  }
 
1
  {
2
+ "architecture": "SemanticColorizer",
3
+ "head": "palette",
4
+ "width": 128,
5
+ "queries": 16,
6
+ "format_version": 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  }
export_onnx.py CHANGED
@@ -1,48 +1,29 @@
1
- """Export the colorizer AND luminance-guided chroma decoder as one ONNX graph.
2
-
3
- Input: N,1,H,W normalized Lab luminance (L*/50-1), minimum8px per side.
4
- Output: N,2,H,W Lab a,b values. Resize/preserve original L* in the app.
5
- """
6
- import argparse,json
7
  from pathlib import Path
8
  import numpy as np
9
  import torch
10
- from model import load_model
11
- from spatial import guided_chroma
12
-
13
- class ChromaPipeline(torch.nn.Module):
14
- def __init__(self,model,radius=8,temperature=.38):
15
- super().__init__();self.model=model;self.radius=radius;self.temperature=temperature
16
- def forward(self,L):
17
- return guided_chroma(L,self.model.decode(self.model(L),self.temperature),self.radius)
18
-
19
- def export(a):
20
- import onnx,onnxruntime as ort
21
- torch.set_num_threads(2);torch.manual_seed(2026)
22
- model=ChromaPipeline(load_model(a.model),a.radius).eval()
23
- out=Path(a.output);out.parent.mkdir(parents=True,exist_ok=True)
24
- with torch.inference_mode():
25
- torch.onnx.export(model,torch.zeros(1,1,256,256),str(out),opset_version=17,
26
- input_names=['luminance'],output_names=['chroma'],dynamo=False,
27
- dynamic_axes={'luminance':{0:'batch',2:'height',3:'width'},'chroma':{0:'batch',2:'height',3:'width'}})
28
- onnx.checker.check_model(str(out))
29
- opts=ort.SessionOptions();opts.intra_op_num_threads=2;opts.inter_op_num_threads=1
30
- session=ort.InferenceSession(str(out),sess_options=opts,providers=['CPUExecutionProvider'])
31
- checks=[]
32
- for shape in [(1,1,256,256),(1,1,173,241),(2,1,64,80),(1,1,8,9)]:
33
- x=torch.rand(shape)*2-1
34
- with torch.inference_mode():expected=model(x).numpy()
35
- actual=session.run(None,{'luminance':x.numpy()})[0]
36
- error=np.abs(expected-actual)
37
- assert actual.shape==expected.shape and np.isfinite(actual).all()
38
- np.testing.assert_allclose(actual,expected,atol=.01,rtol=.001)
39
- checks.append({'shape':list(shape),'max_ab_difference':float(error.max()),'mean_ab_difference':float(error.mean())})
40
- result={'input':'N,1,H,W Lab L*/50-1; H,W>=8','output':'N,2,H,W Lab chroma a,b',
41
- 'guided_radius':a.radius,'guided_epsilon':.001,'temperature':.38,
42
- 'opset':17,'checks':checks,'parameters':sum(p.numel() for p in model.parameters()),
43
- 'weights_source':a.model,'runtime':ort.__version__,'bytes':out.stat().st_size}
44
- out.with_suffix('.json').write_text(json.dumps(result,indent=2));print(json.dumps(result,indent=2))
45
 
46
- if __name__=='__main__':
47
- p=argparse.ArgumentParser(__doc__);p.add_argument('--model',required=True);p.add_argument('--output',required=True);p.add_argument('--radius',type=int,default=8)
48
- export(p.parse_args())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Export the release network at fixed 256x256, with numerical verification."""
2
+ import argparse
3
+ import importlib.util
4
+ import tempfile
 
 
5
  from pathlib import Path
6
  import numpy as np
7
  import torch
8
+ import onnx
9
+ import onnxruntime as ort
10
+ from semantic_model import load_semantic
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ def main():
13
+ p=argparse.ArgumentParser();p.add_argument('--model',default='.');p.add_argument('--output',default='colorizer.onnx');args=p.parse_args()
14
+ source=Path(__file__).with_name('semantic_model.py').read_text()
15
+ source=source.replace("x=F.pad(self.neutral_rgb(L),(0,(-w)%32,0,(-h)%32),mode='replicate')","x=self.neutral_rgb(L)")
16
+ source=source.replace("F.adaptive_avg_pool2d(f,(8,8)).flatten(2).transpose(1,2) for f in projected[1:]","F.avg_pool2d(f,k).flatten(2).transpose(1,2) for f,k in zip(projected[1:],[4,2,1])")
17
+ with tempfile.TemporaryDirectory() as tmp:
18
+ path=Path(tmp)/'export_model.py';path.write_text(source)
19
+ spec=importlib.util.spec_from_file_location('export_model',path);module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
20
+ model=module.load_semantic(args.model,'cpu');reference=load_semantic(args.model,'cpu')
21
+ x=torch.linspace(-1,1,256*256).reshape(1,1,256,256)
22
+ with torch.no_grad():expected=reference(x).numpy();assert np.max(np.abs(expected-model(x).numpy()))<1e-5
23
+ torch.onnx.export(model,x,args.output,input_names=['L'],output_names=['ab'],opset_version=17,do_constant_folding=True,dynamo=False)
24
+ onnx.checker.check_model(args.output)
25
+ session=ort.InferenceSession(args.output,providers=['CPUExecutionProvider'])
26
+ error=float(np.max(np.abs(session.run(None,{'L':x.numpy()})[0]-expected)))
27
+ if error>=.005:raise RuntimeError(f'ONNX parity failed: {error}')
28
+ print(f'Exported {args.output}; maximum Lab error {error:.6f}')
29
+ if __name__=='__main__':main()
inference.py CHANGED
@@ -1,63 +1,111 @@
1
- """Aspect-preserving inference; infer chroma globally and retain original luminance."""
2
- import argparse
 
 
 
 
3
  import math
4
  from pathlib import Path
5
  import numpy as np
6
- from PIL import Image, ImageOps
7
  import torch
8
  import torch.nn.functional as F
9
- from skimage.color import rgb2lab, lab2rgb
10
- from model import load_model
11
- from spatial import guided_chroma
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  @torch.inference_mode()
14
- def colorize(model, image, size=256, temperature=0.38, saturation=1.0, flip_tta=False,
15
- guided_radius=8, guided_epsilon=.001):
16
- if size < 8 or not math.isfinite(saturation) or saturation < 0:
17
- raise ValueError("size must be >=8 and saturation finite and nonnegative")
18
- image = ImageOps.exif_transpose(image).convert("RGB")
19
- rgb = np.asarray(image, dtype=np.float32) / 255.0
20
- luminance = rgb2lab(rgb)[..., 0].astype(np.float32)
21
- h, w = luminance.shape
22
- scale = min(size / max(h, w), 1.0)
23
- target = (max(8, round(h*scale)), max(8, round(w*scale)))
24
  device = next(model.parameters()).device
25
- L = torch.from_numpy(luminance)[None, None].to(device) / 50 - 1
26
- small = F.interpolate(L, size=target, mode="bilinear", align_corners=False, antialias=True)
27
- logits = model(small)
28
- if flip_tta:
29
- logits = (logits + model(small.flip(-1)).flip(-1)) * 0.5
30
- ab = model.decode(logits, temperature)
31
- ab = guided_chroma(small, ab, guided_radius, guided_epsilon)
32
- ab = F.interpolate(ab, size=(h,w), mode="bilinear", align_corners=False)
33
- ab = ab[0].permute(1,2,0).cpu().numpy() * saturation
34
- lab = np.concatenate([luminance[...,None], ab], axis=-1)
35
- result = np.clip(lab2rgb(lab), 0, 1)
36
- return Image.fromarray(np.rint(result * 255).astype(np.uint8))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  def main():
39
- p = argparse.ArgumentParser(__doc__)
40
- p.add_argument("images", nargs="+")
41
- p.add_argument("--model", required=True)
42
- p.add_argument("--revision", default=None)
43
- p.add_argument("--size", type=int, default=256)
44
- p.add_argument("--temperature", type=float, default=.38)
45
- p.add_argument("--saturation", type=float, default=1)
46
- p.add_argument("--flip-tta", action="store_true")
47
- p.add_argument("--guided-radius",type=int,default=8,help="Chroma smoothing radius at model resolution;0 disables")
48
- p.add_argument("--guided-epsilon",type=float,default=.001)
49
- p.add_argument("--output-dir", default="colorized")
50
- p.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
51
- a = p.parse_args()
52
- torch.set_num_threads(min(torch.get_num_threads(),4))
53
- model = load_model(a.model, a.revision, a.device)
54
- out = Path(a.output_dir); out.mkdir(parents=True, exist_ok=True)
55
- for file in a.images:
56
- with Image.open(file) as im:
57
- result = colorize(model, im, a.size, a.temperature, a.saturation, a.flip_tta,
58
- a.guided_radius,a.guided_epsilon)
59
- dest = out / (Path(file).stem + "_colorized.png")
60
- result.save(dest); print(dest)
61
-
62
- if __name__ == "__main__":
63
- main()
 
1
+ """Aspect-preserving colourisation with a compact semantic model.
2
+
3
+ Input and output are PIL images. No teacher, critic or second learned model is
4
+ loaded. Original lightness and alpha are retained; out-of-gamut chroma is reduced.
5
+ """
6
+ import json
7
  import math
8
  from pathlib import Path
9
  import numpy as np
 
10
  import torch
11
  import torch.nn.functional as F
12
+ from PIL import Image, ImageOps
13
+ from skimage.color import rgb2lab
14
+ from semantic_model import load_semantic
15
+
16
+ MAX_PIXELS = 12_000_000
17
+
18
+ def load_colorizer(path, device='cpu'):
19
+ model = load_semantic(path, device)
20
+ count = sum(p.numel() for p in model.parameters())
21
+ if count >= 4_000_000:
22
+ raise ValueError('Model exceeds the four-million-parameter limit')
23
+ return model
24
+
25
+ def box_mean(x, r):
26
+ return F.avg_pool2d(x, 2*r+1, 1, r, count_include_pad=False)
27
+
28
+ def prepare(image, size=256):
29
+ if not isinstance(image, Image.Image):
30
+ raise TypeError('Expected a PIL image')
31
+ if not 128 <= int(size) <= 512:
32
+ raise ValueError('Input size must be between 128 and 512')
33
+ image = ImageOps.exif_transpose(image)
34
+ if image.width * image.height > MAX_PIXELS:
35
+ raise ValueError('Please resize the image to at most 12 megapixels')
36
+ alpha = np.asarray(image.getchannel('A')).copy() if 'A' in image.getbands() else None
37
+ rgb = np.asarray(image.convert('RGB'), dtype=np.float32) / 255
38
+ light = rgb2lab(rgb)[..., 0].astype(np.float32)
39
+ scale = min(int(size)/max(image.size), 1.)
40
+ shape = (max(8, round(image.height*scale)), max(8, round(image.width*scale)))
41
+ x = torch.from_numpy(light)[None,None]/50-1
42
+ small = F.interpolate(x, size=shape, mode='bilinear', align_corners=False, antialias=True)
43
+ return light, alpha, small
44
 
45
  @torch.inference_mode()
46
+ def chroma_coefficients(model, small, radius=8):
47
+ if radius not in (0,4,8,12,16):
48
+ raise ValueError('Unsupported smoothing radius')
 
 
 
 
 
 
 
49
  device = next(model.parameters()).device
50
+ L = small.to(device)
51
+ ab = model(L).float()
52
+ if not torch.isfinite(ab).all():
53
+ raise RuntimeError('Model returned non-finite colours')
54
+ if radius == 0:
55
+ return torch.zeros_like(ab).cpu(), ab.cpu()
56
+ guide = (L.float()+1)/2
57
+ mi, mp = box_mean(guide,radius), box_mean(ab,radius)
58
+ var = (box_mean(guide*guide,radius)-mi*mi).clamp_min(0)
59
+ cov = box_mean(guide*ab,radius)-mi*mp
60
+ a = cov/(var+.001)
61
+ b = mp-a*mi
62
+ # Coefficients are upsampled, then evaluated against original-resolution L.
63
+ return box_mean(a,radius).cpu(), box_mean(b,radius).cpu()
64
+
65
+ def _linear_rgb(light, ab):
66
+ fy = (light+16)/116
67
+ f = np.stack([fy+ab[...,0]/500,fy,fy-ab[...,1]/200],axis=-1)
68
+ xyz = np.where(f>6/29,f**3,(f-4/29)*(3*(6/29)**2))
69
+ xyz *= np.array([.95047,1.,1.08883],np.float32)
70
+ matrix = np.array([[3.24048134,-1.53715152,-.49853633],[-.96925495,1.87599,.04155593],[.05564664,-.20404134,1.05731107]],np.float32)
71
+ return xyz @ matrix.T
72
+
73
+ def render(light, alpha, coefficients, saturation=1.):
74
+ saturation = float(saturation)
75
+ if not math.isfinite(saturation) or not 0 <= saturation <= 1.5:
76
+ raise ValueError('Colour strength must be between 0 and 1.5')
77
+ a,b = [F.interpolate(v.float(),size=light.shape,mode='bilinear',align_corners=False)[0].permute(1,2,0).numpy() for v in coefficients]
78
+ ab = (a*(light[...,None]/100)+b)*saturation
79
+ # Binary-search chroma compression retains Lab hue and lightness.
80
+ linear = _linear_rgb(light,ab)
81
+ invalid = ((linear < -1e-5)|(linear > 1+1e-5)).any(-1)
82
+ if invalid.any():
83
+ L = light[invalid]; colors=ab[invalid];lo=np.zeros(len(L),np.float32);hi=np.ones(len(L),np.float32)
84
+ for _ in range(9):
85
+ mid=(lo+hi)/2; candidate=_linear_rgb(L,colors*mid[:,None])
86
+ valid=((candidate>=-1e-5)&(candidate<=1+1e-5)).all(-1)
87
+ lo=np.where(valid,mid,lo);hi=np.where(valid,hi,mid)
88
+ ab[invalid]=colors*lo[:,None]
89
+ linear[invalid]=_linear_rgb(L,ab[invalid])
90
+ linear=np.clip(linear,0,1)
91
+ rgb=np.where(linear<=.0031308,12.92*linear,1.055*np.power(linear,1/2.4)-.055)
92
+ pixels=np.uint8(np.clip(np.rint(rgb*255),0,255))
93
+ if alpha is not None: pixels=np.concatenate([pixels,alpha[...,None]],axis=-1)
94
+ return Image.fromarray(pixels)
95
+
96
+ def colorize(model, image, size=256, radius=8, saturation=1.):
97
+ light,alpha,small=prepare(image,size)
98
+ return render(light,alpha,chroma_coefficients(model,small,radius),saturation)
99
 
100
  def main():
101
+ import argparse
102
+ parser=argparse.ArgumentParser(description='Compact photo colouriser')
103
+ parser.add_argument('input');parser.add_argument('output')
104
+ parser.add_argument('--model',default='.');parser.add_argument('--device',default='cpu')
105
+ parser.add_argument('--size',type=int,default=256);parser.add_argument('--saturation',type=float,default=1.)
106
+ args=parser.parse_args()
107
+ model=load_colorizer(args.model,args.device)
108
+ with Image.open(args.input) as image:
109
+ colorize(model,image,args.size,saturation=args.saturation).save(args.output)
110
+
111
+ if __name__=='__main__':main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
legacy/v2/ARCHIVE.md ADDED
@@ -0,0 +1 @@
 
 
1
+ Historical v2 helpers. Load weights/config from immutable model revision 704fa80d792c3d759db91daa00b2dcfe6f0f6412. Do not combine these helpers with the new semantic checkpoint.
legacy/v2/DEPLOYMENT.md ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Colorizer deployment
2
+
3
+ Use the release's inference wrapper or its ONNX graph to obtain the complete
4
+ improvement. The `.safetensors` file contains the learned colorizer; guided
5
+ decoding is implemented by `spatial.py` and is included in the ONNX graph.
6
+
7
+ ## Python / PyTorch
8
+
9
+ Run from the extracted release directory:
10
+
11
+ ```bash
12
+ python -m pip install -r requirements.txt
13
+ python inference.py --model . --output-dir colorized photo.jpg
14
+ ```
15
+
16
+ The default guided radius is 8 at the model's input resolution. Use
17
+ `--guided-radius 0` for raw predictions or `--guided-radius 16` for stronger
18
+ smoothing. Stronger smoothing can remove legitimate small color details.
19
+ `--flip-tta --guided-radius 4` is an optional two-pass mode. The released
20
+ ONNX graph is the single-pass radius-8 mode.
21
+
22
+ ```python
23
+ from PIL import Image
24
+ from model import load_model
25
+ from inference import colorize
26
+
27
+ model = load_model('.')
28
+ result = colorize(model, Image.open('photo.jpg'))
29
+ result.save('colorized.png')
30
+ ```
31
+
32
+ The wrapper handles EXIF orientation, preserves aspect ratio, bounds the
33
+ longest network-input side to 256 pixels, upsamples chroma to the original
34
+ oriented image dimensions and combines it with the original Lab luminance.
35
+ Final RGB conversion can clip colors outside the display gamut. Very large
36
+ inputs still require memory for full-resolution color conversion. There is
37
+ no video temporal-consistency guarantee.
38
+
39
+ ## ONNX without PyTorch
40
+
41
+ ```bash
42
+ python -m pip install -r requirements-onnx.txt
43
+ python colorize_onnx.py --model colorizer.onnx --output-dir colorized photo.jpg
44
+ ```
45
+
46
+ Input name: `luminance`, float32, shape `N x 1 x H x W`, values `L*/50 - 1`.
47
+ Output name: `chroma`, float32, shape `N x 2 x H x W`, Lab a and b values.
48
+ Height and width must each be at least 8. Dynamic shapes and batches are
49
+ supported. Prefer a longest input side of 256 to match the evaluated
50
+ operating point. Ordinary RGB values are not valid graph inputs.
51
+
52
+ The graph includes temperature-0.38 decoding and radius-8 guided filtering
53
+ with epsilon 0.001. It does not include file loading, EXIF handling, Lab
54
+ conversion, aspect-ratio resizing or final chroma upsampling; these are
55
+ implemented in `colorize_onnx.py`. The ONNX wrapper uses Pillow resizing,
56
+ whereas the PyTorch wrapper uses PyTorch interpolation. Their image-level
57
+ comparison is recorded in the release checks.
58
+
59
+ ## Publish to main
60
+
61
+ Authenticate normally on your own computer with repository write access:
62
+
63
+ ```bash
64
+ hf auth login
65
+ python upload_main.py --folder . --repo User-2468/mini-unet-colorizer
66
+ ```
67
+
68
+ This performs one upload to `main` with an optimistic-concurrency guard.
69
+ It also verifies that `stable` retains its pre-upload revision. The app can
70
+ keep using `stable` until you choose to switch it to the tested release.
71
+ The current chat connection has Jobs/read access but no repository write
72
+ scope; the downloadable release is the publication fallback.
73
+
74
+ ## Deployment limits
75
+
76
+ This is a measured app-testing candidate. Semantic color mistakes remain,
77
+ and smoothing cannot infer an object's unknown original color. Review the
78
+ included failure examples on your app's real input photos before describing
79
+ it as generally production-ready. Browser/mobile performance and real-time
80
+ video have not been validated.
legacy/v2/README.md ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ pipeline_tag: image-to-image
4
+ tags:
5
+ - colorization
6
+ - unet
7
+ - pytorch
8
+ - safetensors
9
+ - onnx
10
+ datasets:
11
+ - johnowhitaker/imagenette2-320
12
+ - detection-datasets/coco
13
+ ---
14
+ # Mini U-Net Colorizer — broader-data trained candidate
15
+
16
+ **Status: evaluated app-testing candidate.** Some broad color patches and
17
+ incorrect object hues remain. Predicted colors are not evidence of original
18
+ historical colors.
19
+
20
+ This checkpoint has 3,968,892 learned parameters and 236 fixed color bins.
21
+ It starts from the audited bin-mapping repair of main commit
22
+ `6c47ea40724d8fcd67d4f36ce837dc1cb5b1b2a8` and changes all 65 learned parameter
23
+ tensors. The color vocabulary is unchanged. The selected weights are update
24
+ 748 of a completed 1,122-update BF16 L4 run using a 17,325-photo mixed training
25
+ pool (11,943 Imagenette plus 5,382 COCO), batch 32, initial learning rate 1e-5,
26
+ weighted classification loss and frozen BatchNorm running statistics.
27
+
28
+ Selection compared Imagenette50 and reserved COCO200 validation images.
29
+ The selected model retained color strength better than spatial-loss candidates.
30
+ It was then scored on separate Imagenette200 and COCO-val100 checks.
31
+
32
+ | Test sample | Previous repaired error | This release error | Fine excess-edge reduction |
33
+ |---|---:|---:|---:|
34
+ | Imagenette 200 | 13.318 | 12.847 | 85.8% |
35
+ | COCO-val 100 | 15.266 | 14.532 | 84.3% |
36
+
37
+ Error is mean Lab chroma distance. The release includes guided8 decoding;
38
+ raw learned weights alone improve error by 1.97% and 3.25%, respectively.
39
+ Excess-edge reductions are proxies, not counts of visible blotches removed.
40
+ COCO is a convenience sample; older upstream training exposure is unknown.
41
+
42
+ ## Use the complete pipeline
43
+
44
+ ```bash
45
+ python -m pip install -r requirements.txt
46
+ python inference.py --model . --output-dir colorized photo.jpg
47
+ ```
48
+
49
+ ```python
50
+ from PIL import Image
51
+ from model import load_model
52
+ from inference import colorize
53
+ model = load_model('.')
54
+ colorize(model, Image.open('photo.jpg')).save('colorized.png')
55
+ ```
56
+
57
+ Defaults: temperature 0.38, guided radius 8, epsilon 0.001, one network pass.
58
+ The wrapper preserves aspect ratio and original luminance. Old app code that
59
+ only loads safetensors will not automatically gain guided filtering.
60
+
61
+ For ONNX without PyTorch:
62
+
63
+ ```bash
64
+ python -m pip install -r requirements-onnx.txt
65
+ python colorize_onnx.py --model colorizer.onnx --output-dir colorized photo.jpg
66
+ ```
67
+
68
+ The 15.9MB ONNX graph includes the model and guided decoder, with dynamic
69
+ batch/spatial sizes and verified PyTorch parity. See `DEPLOYMENT.md` for the
70
+ Lab input contract, CPU timings, publication commands and integration limits.
71
+ See `RESEARCH_ROUND2.md` and `reports/round2/` for complete measured evidence.
72
+
73
+ ## Limitations
74
+
75
+ Smoothing removes fine color fluctuations but can suppress true small color
76
+ details, especially without luminance boundaries. Semantically wrong hues
77
+ remain. The coffee and rocket failure examples are retained. Browser/mobile
78
+ performance, video consistency and general production quality are unvalidated.
79
+ The separate experiment bundle contains all runs and reproduction code; GPU
80
+ optimizer state is not included. This is a weights-only continuation point.
colorize_eval.py → legacy/v2/colorize_eval.py RENAMED
File without changes
legacy/v2/colorize_image.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = [
4
+ # "torch>=2.3",
5
+ # "torchvision>=0.18",
6
+ # "huggingface_hub>=0.24",
7
+ # "safetensors>=0.4",
8
+ # "scikit-image>=0.22",
9
+ # "pillow>=10.0",
10
+ # "numpy",
11
+ # ]
12
+ # ///
13
+ """
14
+ Colorize photos with a SmallUNetColorizer checkpoint trained by colorize_train.py
15
+ (classification-head version: predicts a distribution over quantized Lab ab
16
+ bins per pixel, decoded with an annealed mean).
17
+
18
+ Usage:
19
+ uv run colorize_image.py --model User-2468/mini-unet-colorizer photo.jpg
20
+ uv run colorize_image.py --model ./local_checkpoint --temperature 0.2 a.jpg b.jpg
21
+
22
+ --temperature is the main "vividness" knob: lower values weight the decode
23
+ toward the most likely color bin (more saturated, can be a bit blotchy);
24
+ higher values move toward the full expectation over the distribution
25
+ (smoother, but can drift back toward desaturated -- the same hedging effect
26
+ plain regression had). 0.38 (the default) is a reasonable middle ground.
27
+ --saturation-boost is an optional *additional* post-decode multiplier on top
28
+ of that, for further hand-tuning after picking a temperature.
29
+ """
30
+ import argparse
31
+ from pathlib import Path
32
+
33
+ import numpy as np
34
+ import torch
35
+ import torch.nn as nn
36
+ import torch.nn.functional as F
37
+ from huggingface_hub import PyTorchModelHubMixin
38
+ from PIL import Image
39
+ from skimage.color import lab2rgb, rgb2lab
40
+
41
+
42
+ def double_conv(in_ch, out_ch):
43
+ return nn.Sequential(
44
+ nn.Conv2d(in_ch, out_ch, 3, padding=1, bias=False),
45
+ nn.BatchNorm2d(out_ch),
46
+ nn.ReLU(inplace=True),
47
+ nn.Conv2d(out_ch, out_ch, 3, padding=1, bias=False),
48
+ nn.BatchNorm2d(out_ch),
49
+ nn.ReLU(inplace=True),
50
+ )
51
+
52
+
53
+ class DilatedContextBlock(nn.Module):
54
+ def __init__(self, channels, mid_ch=96, dilations=(2, 4, 8)):
55
+ super().__init__()
56
+ self.proj_in = nn.Sequential(
57
+ nn.Conv2d(channels, mid_ch, 1, bias=False),
58
+ nn.BatchNorm2d(mid_ch),
59
+ nn.ReLU(inplace=True),
60
+ )
61
+ layers = []
62
+ for d in dilations:
63
+ layers += [
64
+ nn.Conv2d(mid_ch, mid_ch, 3, padding=d, dilation=d, bias=False),
65
+ nn.BatchNorm2d(mid_ch),
66
+ nn.ReLU(inplace=True),
67
+ ]
68
+ self.dilated = nn.Sequential(*layers)
69
+ self.proj_out = nn.Sequential(
70
+ nn.Conv2d(mid_ch, channels, 1, bias=False),
71
+ nn.BatchNorm2d(channels),
72
+ )
73
+ self.relu = nn.ReLU(inplace=True)
74
+
75
+ def forward(self, x):
76
+ y = self.proj_in(x)
77
+ y = self.dilated(y)
78
+ y = self.proj_out(y)
79
+ return self.relu(x + y)
80
+
81
+
82
+ class SmallUNetColorizer(
83
+ nn.Module,
84
+ PyTorchModelHubMixin,
85
+ pipeline_tag="image-to-image",
86
+ license="apache-2.0",
87
+ tags=["colorization", "unet", "image-to-image", "classification"],
88
+ ):
89
+ def __init__(self, bin_centers, in_ch: int = 1, base: int = 44,
90
+ context_mid_ch: int = 96, context_dilations=(2, 4, 8)):
91
+ super().__init__()
92
+ self.in_ch, self.base = in_ch, base
93
+ num_bins = len(bin_centers)
94
+ self.num_bins = num_bins
95
+ self.register_buffer("bin_centers", torch.tensor(bin_centers, dtype=torch.float32))
96
+
97
+ self.enc1 = double_conv(in_ch, base)
98
+ self.enc2 = double_conv(base, base * 2)
99
+ self.enc3 = double_conv(base * 2, base * 4)
100
+ self.enc4 = double_conv(base * 4, base * 8)
101
+ self.pool = nn.MaxPool2d(2)
102
+ self.context = DilatedContextBlock(base * 8, mid_ch=context_mid_ch,
103
+ dilations=tuple(context_dilations))
104
+ self.up3 = nn.ConvTranspose2d(base * 8, base * 4, 2, stride=2)
105
+ self.dec3 = double_conv(base * 8, base * 4)
106
+ self.up2 = nn.ConvTranspose2d(base * 4, base * 2, 2, stride=2)
107
+ self.dec2 = double_conv(base * 4, base * 2)
108
+ self.up1 = nn.ConvTranspose2d(base * 2, base, 2, stride=2)
109
+ self.dec1 = double_conv(base * 2, base)
110
+ self.out_conv = nn.Conv2d(base, num_bins, 1)
111
+
112
+ def forward(self, x):
113
+ e1 = self.enc1(x)
114
+ e2 = self.enc2(self.pool(e1))
115
+ e3 = self.enc3(self.pool(e2))
116
+ e4 = self.context(self.enc4(self.pool(e3)))
117
+ d3 = self.dec3(torch.cat([self.up3(e4), e3], dim=1))
118
+ d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=1))
119
+ d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=1))
120
+ return self.out_conv(d1)
121
+
122
+ def decode(self, logits, temperature: float = 0.38):
123
+ logp = F.log_softmax(logits, dim=1)
124
+ probs_t = F.softmax(logp / temperature, dim=1)
125
+ return torch.einsum("bqhw,qc->bchw", probs_t, self.bin_centers)
126
+
127
+
128
+ def colorize(model, img, size, temperature, saturation_boost, device):
129
+ img = img.convert("RGB").resize((size, size))
130
+ arr = np.asarray(img).astype(np.float32) / 255.0
131
+ lab = rgb2lab(arr).astype(np.float32)
132
+ L = torch.from_numpy(lab[:, :, 0:1] / 50.0 - 1.0).permute(2, 0, 1)[None].to(device)
133
+
134
+ with torch.no_grad():
135
+ logits = model(L)
136
+ ab = model.decode(logits, temperature=temperature)[0].permute(1, 2, 0).cpu().numpy()
137
+
138
+ ab = np.clip(ab * saturation_boost, -128, 127)
139
+ L_out = (L[0, 0].cpu().numpy() + 1.0) * 50.0
140
+ lab_out = np.concatenate([L_out[:, :, None], ab], axis=-1)
141
+ rgb_out = np.clip(lab2rgb(lab_out), 0, 1)
142
+ return Image.fromarray((rgb_out * 255).astype(np.uint8))
143
+
144
+
145
+ def main():
146
+ p = argparse.ArgumentParser(description=__doc__)
147
+ p.add_argument("images", nargs="+", help="Path(s) to input photo(s)")
148
+ p.add_argument("--model", required=True, help="Hub model id or local checkpoint path")
149
+ p.add_argument("--size", type=int, default=256, help="Resize input to this square size")
150
+ p.add_argument("--temperature", type=float, default=0.38,
151
+ help="Annealed-mean decode temperature. Lower = more vivid/mode-like, "
152
+ "higher = smoother but can desaturate again. Try 0.15-0.6.")
153
+ p.add_argument("--saturation-boost", type=float, default=1.0,
154
+ help="Extra multiplier on the decoded ab, applied after temperature. "
155
+ "1.0 = no extra boost.")
156
+ p.add_argument("--output-dir", default="./colorized")
157
+ args = p.parse_args()
158
+
159
+ device = "cuda" if torch.cuda.is_available() else "cpu"
160
+ print(f"Loading {args.model} on {device} ...")
161
+ model = SmallUNetColorizer.from_pretrained(args.model).to(device).eval()
162
+ print(f"({model.num_bins} color bins)")
163
+
164
+ out_dir = Path(args.output_dir)
165
+ out_dir.mkdir(parents=True, exist_ok=True)
166
+
167
+ for path in args.images:
168
+ img = Image.open(path)
169
+ result = colorize(model, img, args.size, args.temperature, args.saturation_boost, device)
170
+ out_path = out_dir / f"{Path(path).stem}_colorized.png"
171
+ result.save(out_path)
172
+ print(f"{path} -> {out_path}")
173
+
174
+
175
+ if __name__ == "__main__":
176
+ main()
legacy/v2/colorize_onnx.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run the exported chroma pipeline without importing PyTorch."""
2
+ import argparse
3
+ from pathlib import Path
4
+ import numpy as np
5
+ import onnxruntime as ort
6
+ from PIL import Image,ImageOps
7
+ from skimage.color import rgb2lab,lab2rgb
8
+
9
+ def load_session(path,threads=2):
10
+ opts=ort.SessionOptions();opts.intra_op_num_threads=threads;opts.inter_op_num_threads=1
11
+ return ort.InferenceSession(str(path),sess_options=opts,providers=['CPUExecutionProvider'])
12
+
13
+ def colorize(session,image,size=256):
14
+ if size<8:raise ValueError('size must be at least8')
15
+ image=ImageOps.exif_transpose(image).convert('RGB')
16
+ L=rgb2lab(np.asarray(image,dtype=np.float32)/255)[...,0].astype(np.float32)
17
+ h,w=L.shape;scale=min(size/max(h,w),1)
18
+ target=(max(8,round(w*scale)),max(8,round(h*scale)))
19
+ small=np.asarray(Image.fromarray(L).resize(target,Image.Resampling.BILINEAR),dtype=np.float32)
20
+ x=(small[None,None]/50-1).copy()
21
+ ab=session.run(['chroma'],{'luminance':x})[0][0]
22
+ full=np.stack([np.asarray(Image.fromarray(c).resize((w,h),Image.Resampling.BILINEAR)) for c in ab],axis=-1)
23
+ rgb=np.clip(lab2rgb(np.concatenate([L[...,None],full],axis=-1)),0,1)
24
+ return Image.fromarray(np.rint(rgb*255).astype(np.uint8))
25
+
26
+ if __name__=='__main__':
27
+ p=argparse.ArgumentParser(__doc__);p.add_argument('images',nargs='+');p.add_argument('--model',required=True)
28
+ p.add_argument('--output-dir',default='colorized');p.add_argument('--size',type=int,default=256);p.add_argument('--threads',type=int,default=2)
29
+ a=p.parse_args();session=load_session(a.model,a.threads);out=Path(a.output_dir);out.mkdir(parents=True,exist_ok=True)
30
+ for source in a.images:
31
+ with Image.open(source) as im:result=colorize(session,im,a.size)
32
+ dest=out/(Path(source).stem+'_colorized.png');result.save(dest);print(dest)
colorize_train.py → legacy/v2/colorize_train.py RENAMED
File without changes
legacy/v2/colorizer.json ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "input": "N,1,H,W Lab L*/50-1; H,W>=8",
3
+ "output": "N,2,H,W Lab chroma a,b",
4
+ "guided_radius": 8,
5
+ "guided_epsilon": 0.001,
6
+ "temperature": 0.38,
7
+ "opset": 17,
8
+ "checks": [
9
+ {
10
+ "shape": [
11
+ 1,
12
+ 1,
13
+ 256,
14
+ 256
15
+ ],
16
+ "max_ab_difference": 8.58306884765625e-06,
17
+ "mean_ab_difference": 1.0279118214384653e-06
18
+ },
19
+ {
20
+ "shape": [
21
+ 1,
22
+ 1,
23
+ 173,
24
+ 241
25
+ ],
26
+ "max_ab_difference": 7.3909759521484375e-06,
27
+ "mean_ab_difference": 1.0783561492644367e-06
28
+ },
29
+ {
30
+ "shape": [
31
+ 2,
32
+ 1,
33
+ 64,
34
+ 80
35
+ ],
36
+ "max_ab_difference": 7.152557373046875e-06,
37
+ "mean_ab_difference": 9.707116532808868e-07
38
+ },
39
+ {
40
+ "shape": [
41
+ 1,
42
+ 1,
43
+ 8,
44
+ 9
45
+ ],
46
+ "max_ab_difference": 1.5497207641601562e-06,
47
+ "mean_ab_difference": 3.6218099808138504e-07
48
+ }
49
+ ],
50
+ "parameters": 3968892,
51
+ "weights_source": "release-v2",
52
+ "runtime": "1.30.0",
53
+ "bytes": 15885757
54
+ }
eval_grid.png → legacy/v2/eval_grid.png RENAMED
File without changes
eval_grid_temp038.png → legacy/v2/eval_grid_temp038.png RENAMED
File without changes
eval_grid_temp075.png → legacy/v2/eval_grid_temp075.png RENAMED
File without changes
legacy/v2/export_onnx.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Export the colorizer AND luminance-guided chroma decoder as one ONNX graph.
2
+
3
+ Input: N,1,H,W normalized Lab luminance (L*/50-1), minimum8px per side.
4
+ Output: N,2,H,W Lab a,b values. Resize/preserve original L* in the app.
5
+ """
6
+ import argparse,json
7
+ from pathlib import Path
8
+ import numpy as np
9
+ import torch
10
+ from model import load_model
11
+ from spatial import guided_chroma
12
+
13
+ class ChromaPipeline(torch.nn.Module):
14
+ def __init__(self,model,radius=8,temperature=.38):
15
+ super().__init__();self.model=model;self.radius=radius;self.temperature=temperature
16
+ def forward(self,L):
17
+ return guided_chroma(L,self.model.decode(self.model(L),self.temperature),self.radius)
18
+
19
+ def export(a):
20
+ import onnx,onnxruntime as ort
21
+ torch.set_num_threads(2);torch.manual_seed(2026)
22
+ model=ChromaPipeline(load_model(a.model),a.radius).eval()
23
+ out=Path(a.output);out.parent.mkdir(parents=True,exist_ok=True)
24
+ with torch.inference_mode():
25
+ torch.onnx.export(model,torch.zeros(1,1,256,256),str(out),opset_version=17,
26
+ input_names=['luminance'],output_names=['chroma'],dynamo=False,
27
+ dynamic_axes={'luminance':{0:'batch',2:'height',3:'width'},'chroma':{0:'batch',2:'height',3:'width'}})
28
+ onnx.checker.check_model(str(out))
29
+ opts=ort.SessionOptions();opts.intra_op_num_threads=2;opts.inter_op_num_threads=1
30
+ session=ort.InferenceSession(str(out),sess_options=opts,providers=['CPUExecutionProvider'])
31
+ checks=[]
32
+ for shape in [(1,1,256,256),(1,1,173,241),(2,1,64,80),(1,1,8,9)]:
33
+ x=torch.rand(shape)*2-1
34
+ with torch.inference_mode():expected=model(x).numpy()
35
+ actual=session.run(None,{'luminance':x.numpy()})[0]
36
+ error=np.abs(expected-actual)
37
+ assert actual.shape==expected.shape and np.isfinite(actual).all()
38
+ np.testing.assert_allclose(actual,expected,atol=.01,rtol=.001)
39
+ checks.append({'shape':list(shape),'max_ab_difference':float(error.max()),'mean_ab_difference':float(error.mean())})
40
+ result={'input':'N,1,H,W Lab L*/50-1; H,W>=8','output':'N,2,H,W Lab chroma a,b',
41
+ 'guided_radius':a.radius,'guided_epsilon':.001,'temperature':.38,
42
+ 'opset':17,'checks':checks,'parameters':sum(p.numel() for p in model.parameters()),
43
+ 'weights_source':a.model,'runtime':ort.__version__,'bytes':out.stat().st_size}
44
+ out.with_suffix('.json').write_text(json.dumps(result,indent=2));print(json.dumps(result,indent=2))
45
+
46
+ if __name__=='__main__':
47
+ p=argparse.ArgumentParser(__doc__);p.add_argument('--model',required=True);p.add_argument('--output',required=True);p.add_argument('--radius',type=int,default=8)
48
+ export(p.parse_args())
legacy/v2/inference.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Aspect-preserving inference; infer chroma globally and retain original luminance."""
2
+ import argparse
3
+ import math
4
+ from pathlib import Path
5
+ import numpy as np
6
+ from PIL import Image, ImageOps
7
+ import torch
8
+ import torch.nn.functional as F
9
+ from skimage.color import rgb2lab, lab2rgb
10
+ from model import load_model
11
+ from spatial import guided_chroma
12
+
13
+ @torch.inference_mode()
14
+ def colorize(model, image, size=256, temperature=0.38, saturation=1.0, flip_tta=False,
15
+ guided_radius=8, guided_epsilon=.001):
16
+ if size < 8 or not math.isfinite(saturation) or saturation < 0:
17
+ raise ValueError("size must be >=8 and saturation finite and nonnegative")
18
+ image = ImageOps.exif_transpose(image).convert("RGB")
19
+ rgb = np.asarray(image, dtype=np.float32) / 255.0
20
+ luminance = rgb2lab(rgb)[..., 0].astype(np.float32)
21
+ h, w = luminance.shape
22
+ scale = min(size / max(h, w), 1.0)
23
+ target = (max(8, round(h*scale)), max(8, round(w*scale)))
24
+ device = next(model.parameters()).device
25
+ L = torch.from_numpy(luminance)[None, None].to(device) / 50 - 1
26
+ small = F.interpolate(L, size=target, mode="bilinear", align_corners=False, antialias=True)
27
+ logits = model(small)
28
+ if flip_tta:
29
+ logits = (logits + model(small.flip(-1)).flip(-1)) * 0.5
30
+ ab = model.decode(logits, temperature)
31
+ ab = guided_chroma(small, ab, guided_radius, guided_epsilon)
32
+ ab = F.interpolate(ab, size=(h,w), mode="bilinear", align_corners=False)
33
+ ab = ab[0].permute(1,2,0).cpu().numpy() * saturation
34
+ lab = np.concatenate([luminance[...,None], ab], axis=-1)
35
+ result = np.clip(lab2rgb(lab), 0, 1)
36
+ return Image.fromarray(np.rint(result * 255).astype(np.uint8))
37
+
38
+ def main():
39
+ p = argparse.ArgumentParser(__doc__)
40
+ p.add_argument("images", nargs="+")
41
+ p.add_argument("--model", required=True)
42
+ p.add_argument("--revision", default=None)
43
+ p.add_argument("--size", type=int, default=256)
44
+ p.add_argument("--temperature", type=float, default=.38)
45
+ p.add_argument("--saturation", type=float, default=1)
46
+ p.add_argument("--flip-tta", action="store_true")
47
+ p.add_argument("--guided-radius",type=int,default=8,help="Chroma smoothing radius at model resolution;0 disables")
48
+ p.add_argument("--guided-epsilon",type=float,default=.001)
49
+ p.add_argument("--output-dir", default="colorized")
50
+ p.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
51
+ a = p.parse_args()
52
+ torch.set_num_threads(min(torch.get_num_threads(),4))
53
+ model = load_model(a.model, a.revision, a.device)
54
+ out = Path(a.output_dir); out.mkdir(parents=True, exist_ok=True)
55
+ for file in a.images:
56
+ with Image.open(file) as im:
57
+ result = colorize(model, im, a.size, a.temperature, a.saturation, a.flip_tta,
58
+ a.guided_radius,a.guided_epsilon)
59
+ dest = out / (Path(file).stem + "_colorized.png")
60
+ result.save(dest); print(dest)
61
+
62
+ if __name__ == "__main__":
63
+ main()
legacy/v2/model.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared, checkpoint-compatible Mini U-Net. Parameters remain under 4M."""
2
+ import json
3
+ import math
4
+ from pathlib import Path
5
+ import numpy as np
6
+ import torch
7
+ from torch import nn
8
+ import torch.nn.functional as F
9
+ from huggingface_hub import PyTorchModelHubMixin, snapshot_download
10
+ from safetensors.torch import load_file, save_file
11
+ def double_conv(in_ch, out_ch):
12
+ return nn.Sequential(
13
+ nn.Conv2d(in_ch, out_ch, 3, padding=1, bias=False),
14
+ nn.BatchNorm2d(out_ch),
15
+ nn.ReLU(inplace=True),
16
+ nn.Conv2d(out_ch, out_ch, 3, padding=1, bias=False),
17
+ nn.BatchNorm2d(out_ch),
18
+ nn.ReLU(inplace=True),
19
+ )
20
+
21
+
22
+ class DilatedContextBlock(nn.Module):
23
+ def __init__(self, channels, mid_ch=96, dilations=(2, 4, 8)):
24
+ super().__init__()
25
+ self.proj_in = nn.Sequential(
26
+ nn.Conv2d(channels, mid_ch, 1, bias=False),
27
+ nn.BatchNorm2d(mid_ch),
28
+ nn.ReLU(inplace=True),
29
+ )
30
+ layers = []
31
+ for d in dilations:
32
+ layers += [
33
+ nn.Conv2d(mid_ch, mid_ch, 3, padding=d, dilation=d, bias=False),
34
+ nn.BatchNorm2d(mid_ch),
35
+ nn.ReLU(inplace=True),
36
+ ]
37
+ self.dilated = nn.Sequential(*layers)
38
+ self.proj_out = nn.Sequential(
39
+ nn.Conv2d(mid_ch, channels, 1, bias=False),
40
+ nn.BatchNorm2d(channels),
41
+ )
42
+ nn.init.zeros_(self.proj_out[-1].weight)
43
+ self.relu = nn.ReLU(inplace=True)
44
+
45
+ def forward(self, x):
46
+ y = self.proj_in(x)
47
+ y = self.dilated(y)
48
+ y = self.proj_out(y)
49
+ return self.relu(x + y)
50
+
51
+
52
+ class SmallUNetColorizer(
53
+ nn.Module,
54
+ PyTorchModelHubMixin,
55
+ pipeline_tag="image-to-image",
56
+ license="apache-2.0",
57
+ tags=["colorization", "unet", "image-to-image", "classification"],
58
+ ):
59
+ def __init__(self, bin_centers, in_ch: int = 1, base: int = 44,
60
+ context_mid_ch: int = 96, context_dilations=(2, 4, 8)):
61
+ super().__init__()
62
+ self.in_ch, self.base = in_ch, base
63
+ num_bins = len(bin_centers)
64
+ self.num_bins = num_bins
65
+ self.register_buffer("bin_centers", torch.tensor(bin_centers, dtype=torch.float32))
66
+
67
+ self.enc1 = double_conv(in_ch, base)
68
+ self.enc2 = double_conv(base, base * 2)
69
+ self.enc3 = double_conv(base * 2, base * 4)
70
+ self.enc4 = double_conv(base * 4, base * 8)
71
+ self.pool = nn.MaxPool2d(2)
72
+ self.context = DilatedContextBlock(base * 8, mid_ch=context_mid_ch,
73
+ dilations=tuple(context_dilations))
74
+ self.up3 = nn.ConvTranspose2d(base * 8, base * 4, 2, stride=2)
75
+ self.dec3 = double_conv(base * 8, base * 4)
76
+ self.up2 = nn.ConvTranspose2d(base * 4, base * 2, 2, stride=2)
77
+ self.dec2 = double_conv(base * 4, base * 2)
78
+ self.up1 = nn.ConvTranspose2d(base * 2, base, 2, stride=2)
79
+ self.dec1 = double_conv(base * 2, base)
80
+ self.out_conv = nn.Conv2d(base, num_bins, 1)
81
+
82
+ def forward(self, x):
83
+ h, w = x.shape[-2:]
84
+ x = F.pad(x, (0, (-w) % 8, 0, (-h) % 8), mode="replicate")
85
+ e1 = self.enc1(x)
86
+ e2 = self.enc2(self.pool(e1))
87
+ e3 = self.enc3(self.pool(e2))
88
+ e4 = self.context(self.enc4(self.pool(e3)))
89
+ d3 = self.dec3(torch.cat([self.up3(e4), e3], dim=1))
90
+ d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=1))
91
+ d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=1))
92
+ return self.out_conv(d1)[..., :h, :w]
93
+
94
+ def decode(self, logits, temperature: float = 0.38):
95
+ if not math.isfinite(temperature) or temperature <= 0:
96
+ raise ValueError("temperature must be finite and positive")
97
+ probs_t = F.softmax(logits.float() / temperature, dim=1)
98
+ return torch.einsum("bqhw,qc->bchw", probs_t, self.bin_centers)
99
+
100
+
101
+ def load_model(source, revision=None, device="cpu"):
102
+ path = Path(source)
103
+ if not path.is_dir():
104
+ path = Path(snapshot_download(source, revision=revision,
105
+ allow_patterns=["config.json", "model.safetensors"]))
106
+ config = json.loads((path / "config.json").read_text())
107
+ state = load_file(str(path / "model.safetensors"))
108
+ centers = torch.tensor(config["bin_centers"], dtype=torch.float32)
109
+ if not torch.equal(centers, state["bin_centers"]):
110
+ raise ValueError("Checkpoint config and state color bins differ; refusing ambiguous decode")
111
+ model = SmallUNetColorizer(**config)
112
+ model.load_state_dict(state, strict=True)
113
+ model.to(device).eval()
114
+ return model
115
+
116
+ def save_model(model, path):
117
+ path = Path(path); path.mkdir(parents=True, exist_ok=True)
118
+ model.save_pretrained(path)
119
+ # Mixin config can retain constructor bins; use the actual authoritative buffer.
120
+ cfg = json.loads((path / "config.json").read_text())
121
+ cfg["bin_centers"] = model.bin_centers.detach().cpu().tolist()
122
+ (path / "config.json").write_text(json.dumps(cfg, indent=2))
legacy/v2/requirements-onnx.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ onnxruntime>=1.17
2
+ numpy
3
+ pillow
4
+ scikit-image
legacy/v2/requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch>=2.3
2
+ huggingface_hub>=0.24
3
+ safetensors>=0.4
4
+ scikit-image>=0.22
5
+ scipy>=1.11
6
+ pillow>=10
7
+ numpy
8
+ pyarrow
9
+ pytest
10
+ datasets
11
+ torchvision>=0.18
12
+ tqdm
sample.png → legacy/v2/sample.png RENAMED
File without changes
upload_main.py → legacy/v2/upload_main.py RENAMED
File without changes
mini-colorizer-v3.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ec6c3e507c15a74faf869d92d6b80e7374d24e990e95176187483905fba2afc6
3
+ size 29784576
model.py CHANGED
@@ -1,122 +1,8 @@
1
- """Shared, checkpoint-compatible Mini U-Net. Parameters remain under 4M."""
2
- import json
3
- import math
4
  from pathlib import Path
5
- import numpy as np
6
- import torch
7
- from torch import nn
8
- import torch.nn.functional as F
9
- from huggingface_hub import PyTorchModelHubMixin, snapshot_download
10
- from safetensors.torch import load_file, save_file
11
- def double_conv(in_ch, out_ch):
12
- return nn.Sequential(
13
- nn.Conv2d(in_ch, out_ch, 3, padding=1, bias=False),
14
- nn.BatchNorm2d(out_ch),
15
- nn.ReLU(inplace=True),
16
- nn.Conv2d(out_ch, out_ch, 3, padding=1, bias=False),
17
- nn.BatchNorm2d(out_ch),
18
- nn.ReLU(inplace=True),
19
- )
20
-
21
-
22
- class DilatedContextBlock(nn.Module):
23
- def __init__(self, channels, mid_ch=96, dilations=(2, 4, 8)):
24
- super().__init__()
25
- self.proj_in = nn.Sequential(
26
- nn.Conv2d(channels, mid_ch, 1, bias=False),
27
- nn.BatchNorm2d(mid_ch),
28
- nn.ReLU(inplace=True),
29
- )
30
- layers = []
31
- for d in dilations:
32
- layers += [
33
- nn.Conv2d(mid_ch, mid_ch, 3, padding=d, dilation=d, bias=False),
34
- nn.BatchNorm2d(mid_ch),
35
- nn.ReLU(inplace=True),
36
- ]
37
- self.dilated = nn.Sequential(*layers)
38
- self.proj_out = nn.Sequential(
39
- nn.Conv2d(mid_ch, channels, 1, bias=False),
40
- nn.BatchNorm2d(channels),
41
- )
42
- nn.init.zeros_(self.proj_out[-1].weight)
43
- self.relu = nn.ReLU(inplace=True)
44
-
45
- def forward(self, x):
46
- y = self.proj_in(x)
47
- y = self.dilated(y)
48
- y = self.proj_out(y)
49
- return self.relu(x + y)
50
-
51
-
52
- class SmallUNetColorizer(
53
- nn.Module,
54
- PyTorchModelHubMixin,
55
- pipeline_tag="image-to-image",
56
- license="apache-2.0",
57
- tags=["colorization", "unet", "image-to-image", "classification"],
58
- ):
59
- def __init__(self, bin_centers, in_ch: int = 1, base: int = 44,
60
- context_mid_ch: int = 96, context_dilations=(2, 4, 8)):
61
- super().__init__()
62
- self.in_ch, self.base = in_ch, base
63
- num_bins = len(bin_centers)
64
- self.num_bins = num_bins
65
- self.register_buffer("bin_centers", torch.tensor(bin_centers, dtype=torch.float32))
66
-
67
- self.enc1 = double_conv(in_ch, base)
68
- self.enc2 = double_conv(base, base * 2)
69
- self.enc3 = double_conv(base * 2, base * 4)
70
- self.enc4 = double_conv(base * 4, base * 8)
71
- self.pool = nn.MaxPool2d(2)
72
- self.context = DilatedContextBlock(base * 8, mid_ch=context_mid_ch,
73
- dilations=tuple(context_dilations))
74
- self.up3 = nn.ConvTranspose2d(base * 8, base * 4, 2, stride=2)
75
- self.dec3 = double_conv(base * 8, base * 4)
76
- self.up2 = nn.ConvTranspose2d(base * 4, base * 2, 2, stride=2)
77
- self.dec2 = double_conv(base * 4, base * 2)
78
- self.up1 = nn.ConvTranspose2d(base * 2, base, 2, stride=2)
79
- self.dec1 = double_conv(base * 2, base)
80
- self.out_conv = nn.Conv2d(base, num_bins, 1)
81
-
82
- def forward(self, x):
83
- h, w = x.shape[-2:]
84
- x = F.pad(x, (0, (-w) % 8, 0, (-h) % 8), mode="replicate")
85
- e1 = self.enc1(x)
86
- e2 = self.enc2(self.pool(e1))
87
- e3 = self.enc3(self.pool(e2))
88
- e4 = self.context(self.enc4(self.pool(e3)))
89
- d3 = self.dec3(torch.cat([self.up3(e4), e3], dim=1))
90
- d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=1))
91
- d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=1))
92
- return self.out_conv(d1)[..., :h, :w]
93
-
94
- def decode(self, logits, temperature: float = 0.38):
95
- if not math.isfinite(temperature) or temperature <= 0:
96
- raise ValueError("temperature must be finite and positive")
97
- probs_t = F.softmax(logits.float() / temperature, dim=1)
98
- return torch.einsum("bqhw,qc->bchw", probs_t, self.bin_centers)
99
-
100
-
101
  def load_model(source, revision=None, device="cpu"):
102
- path = Path(source)
103
- if not path.is_dir():
104
- path = Path(snapshot_download(source, revision=revision,
105
- allow_patterns=["config.json", "model.safetensors"]))
106
- config = json.loads((path / "config.json").read_text())
107
- state = load_file(str(path / "model.safetensors"))
108
- centers = torch.tensor(config["bin_centers"], dtype=torch.float32)
109
- if not torch.equal(centers, state["bin_centers"]):
110
- raise ValueError("Checkpoint config and state color bins differ; refusing ambiguous decode")
111
- model = SmallUNetColorizer(**config)
112
- model.load_state_dict(state, strict=True)
113
- model.to(device).eval()
114
- return model
115
-
116
- def save_model(model, path):
117
- path = Path(path); path.mkdir(parents=True, exist_ok=True)
118
- model.save_pretrained(path)
119
- # Mixin config can retain constructor bins; use the actual authoritative buffer.
120
- cfg = json.loads((path / "config.json").read_text())
121
- cfg["bin_centers"] = model.bin_centers.detach().cpu().tolist()
122
- (path / "config.json").write_text(json.dumps(cfg, indent=2))
 
 
 
 
1
  from pathlib import Path
2
+ from semantic_model import SemanticColorizer, load_semantic, save_semantic
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  def load_model(source, revision=None, device="cpu"):
4
+ from huggingface_hub import snapshot_download
5
+ path=Path(source)
6
+ if not path.is_dir(): path=Path(snapshot_download(source,revision=revision,allow_patterns=["config.json","model.safetensors"]))
7
+ return load_semantic(path,device)
8
+ save_model=save_semantic
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:0e4c417375684a044860f8af3ac3a2fb44e1a5729254ca33abda758ea71aea6e
3
- size 15909320
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ec1f27d74533adc83f7ab3639a091fc4d8738a434dafc7d172c7873c28a9e715
3
+ size 16112432
requirements-onnx.txt CHANGED
@@ -1,4 +1,5 @@
1
- onnxruntime>=1.17
2
- numpy
3
- pillow
4
- scikit-image
 
 
1
+ onnxruntime==1.20.1
2
+ onnx==1.17.0
3
+ numpy==1.26.4
4
+ Pillow==11.1.0
5
+ scikit-image==0.25.2
requirements-space.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ torch==2.8.0
2
+ torchvision==0.23.0
3
+ numpy==1.26.4
4
+ Pillow==11.1.0
5
+ scikit-image==0.25.2
6
+ safetensors==0.5.3
7
+ huggingface-hub>=1.0,<2
8
+ gradio==6.28.0
9
+ spaces==0.51.3
requirements.txt CHANGED
@@ -1,12 +1,7 @@
1
- torch>=2.3
2
- huggingface_hub>=0.24
3
- safetensors>=0.4
4
- scikit-image>=0.22
5
- scipy>=1.11
6
- pillow>=10
7
- numpy
8
- pyarrow
9
- pytest
10
- datasets
11
- torchvision>=0.18
12
- tqdm
 
1
+ torch==2.8.0
2
+ torchvision==0.23.0
3
+ numpy==1.26.4
4
+ Pillow==11.1.0
5
+ scikit-image==0.25.2
6
+ safetensors==0.5.3
7
+ huggingface-hub>=1.0,<2
 
 
 
 
 
semantic_model.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compact pretrained semantic colorizer; dense and shared-palette variants."""
2
+ import json
3
+ from pathlib import Path
4
+ import torch
5
+ from torch import nn
6
+ import torch.nn.functional as F
7
+ from torchvision.models import mobilenet_v3_large, MobileNet_V3_Large_Weights
8
+ from safetensors.torch import save_file, load_file
9
+
10
+ class QueryBlock(nn.Module):
11
+ def __init__(self,d=96):
12
+ super().__init__()
13
+ self.self_attn=nn.MultiheadAttention(d,4,batch_first=True,dropout=0)
14
+ self.cross_attn=nn.MultiheadAttention(d,4,batch_first=True,dropout=0)
15
+ self.norms=nn.ModuleList([nn.LayerNorm(d) for _ in range(3)])
16
+ self.ff=nn.Sequential(nn.Linear(d,2*d),nn.GELU(),nn.Linear(2*d,d))
17
+ def forward(self,q,memory):
18
+ x=self.norms[0](q);q=q+self.self_attn(x,x,x,need_weights=False)[0]
19
+ x=self.norms[1](q);q=q+self.cross_attn(x,memory,memory,need_weights=False)[0]
20
+ return q+self.ff(self.norms[2](q))
21
+
22
+ def refine(d):
23
+ return nn.Sequential(nn.Conv2d(d,d,3,padding=1,bias=False),nn.GroupNorm(8,d),nn.SiLU())
24
+
25
+ class SemanticColorizer(nn.Module):
26
+ def __init__(self,head='palette',pretrained=False,width=128,queries=16):
27
+ super().__init__()
28
+ if head not in ['palette','dense']:raise ValueError(head)
29
+ self.config={'architecture':'SemanticColorizer','head':head,'width':width,'queries':queries,'format_version':1}
30
+ self.encoder=mobilenet_v3_large(weights=MobileNet_V3_Large_Weights.IMAGENET1K_V2 if pretrained else None,progress=False).features
31
+ self.lateral=nn.ModuleList([nn.Conv2d(c,width,1) for c in [24,40,112,960]])
32
+ self.refine=nn.ModuleList([refine(width) for _ in range(3)])
33
+ self.register_buffer('rgb_mean',torch.tensor([.485,.456,.406]).view(1,3,1,1))
34
+ self.register_buffer('rgb_std',torch.tensor([.229,.224,.225]).view(1,3,1,1))
35
+ if head=='palette':
36
+ self.queries=nn.Parameter(torch.randn(queries,width)*.2)
37
+ self.query_blocks=nn.ModuleList([QueryBlock(width) for _ in range(2)])
38
+ self.memory_norm=nn.LayerNorm(width)
39
+ self.query_norm=nn.LayerNorm(width)
40
+ self.pixel=nn.Conv2d(width,width,1)
41
+ self.palette=nn.Sequential(nn.Linear(width,width),nn.GELU(),nn.Linear(width,2))
42
+ self.residual=nn.Conv2d(width,2,1)
43
+ nn.init.normal_(self.palette[-1].weight,std=.01);nn.init.zeros_(self.palette[-1].bias)
44
+ nn.init.zeros_(self.residual.weight);nn.init.zeros_(self.residual.bias)
45
+ else:
46
+ self.dense=nn.Sequential(refine(width),nn.Conv2d(width,2,1))
47
+ nn.init.normal_(self.dense[-1].weight,std=.01);nn.init.zeros_(self.dense[-1].bias)
48
+ count=sum(p.numel() for p in self.parameters())
49
+ if count>=4_000_000:raise ValueError(f'Parameter budget exceeded: {count}')
50
+
51
+ @staticmethod
52
+ def neutral_rgb(L):
53
+ light=(L.float()*50+50).clamp(0,100)
54
+ y=torch.where(light>8,((light+16)/116)**3,light/903.296296)
55
+ g=torch.where(y<=.0031308,12.92*y,1.055*y.clamp_min(1e-8).pow(1/2.4)-.055)
56
+ return g.expand(-1,3,-1,-1)
57
+
58
+ def forward(self,L):
59
+ h,w=L.shape[-2:]
60
+ x=F.pad(self.neutral_rgb(L),(0,(-w)%32,0,(-h)%32),mode='replicate')
61
+ x=(x-self.rgb_mean)/self.rgb_std
62
+ features=[]
63
+ for i,layer in enumerate(self.encoder):
64
+ x=layer(x)
65
+ if i in [3,6,12,16]:features.append(x)
66
+ projected=[layer(f) for layer,f in zip(self.lateral,features)]
67
+ x=projected[-1]
68
+ for i in range(2,-1,-1):
69
+ x=self.refine[2-i](F.interpolate(x,size=projected[i].shape[-2:],mode='bilinear',align_corners=False)+projected[i])
70
+ if self.config['head']=='palette':
71
+ memory=torch.cat([F.adaptive_avg_pool2d(f,(8,8)).flatten(2).transpose(1,2) for f in projected[1:]],1)
72
+ memory=self.memory_norm(memory)
73
+ q=self.queries[None].expand(L.shape[0],-1,-1)
74
+ for block in self.query_blocks:q=block(q,memory)
75
+ q=self.query_norm(q)
76
+ palette=80*torch.tanh(self.palette(q))
77
+ masks=torch.einsum('bqd,bdhw->bqhw',q,self.pixel(x))/(self.config['width']**.5)
78
+ weights=F.softmax(masks.float(),dim=1)
79
+ ab=torch.einsum('bqhw,bqc->bchw',weights,palette.float())+2*torch.tanh(self.residual(x).float())
80
+ else:ab=80*torch.tanh(self.dense(x).float())
81
+ return F.interpolate(ab,size=(x.shape[-2]*4,x.shape[-1]*4),mode='bilinear',align_corners=False)[...,:h,:w]
82
+ def decode(self,z,temperature=.38):return z.float()
83
+
84
+ def save_semantic(model,path):
85
+ path=Path(path);path.mkdir(parents=True,exist_ok=True)
86
+ save_file({k:v.detach().cpu().contiguous() for k,v in model.state_dict().items()},str(path/'model.safetensors'))
87
+ (path/'config.json').write_text(json.dumps(model.config,indent=2))
88
+ (path/'README.md').write_text('# Experimental semantic colorizer\n\nHead: '+model.config['head']+'. Parameters: '+str(sum(p.numel() for p in model.parameters()))+'.\n\nNot approved for production. Requires semantic_model.py; incompatible with the old U-Net loader. Input is Lab lightness normalized to [-1,1]; output is Lab ab. See the run protocol, provenance, selection and visual comparisons. Predictions are plausible colors, not recovered historical truth.\n')
89
+
90
+ def load_semantic(path,device='cpu'):
91
+ path=Path(path);cfg=json.loads((path/'config.json').read_text())
92
+ model=SemanticColorizer(**{k:cfg[k] for k in ['head','width','queries']})
93
+ model.load_state_dict(load_file(str(path/'model.safetensors')),strict=True)
94
+ return model.to(device).eval()