@xprite/pixelate 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +12 -0
  3. package/README.md +238 -2
  4. package/README.zh-CN.md +209 -0
  5. package/dist/analyze.d.ts +8 -0
  6. package/dist/analyze.js +217 -0
  7. package/dist/base.d.ts +18 -0
  8. package/dist/base.js +42 -0
  9. package/dist/cell-color-picker.d.ts +14 -0
  10. package/dist/cell-color-picker.js +278 -0
  11. package/dist/color-lab.d.ts +3 -0
  12. package/dist/color-lab.js +31 -0
  13. package/dist/color-step.d.ts +33 -0
  14. package/dist/color-step.js +62 -0
  15. package/dist/contours.d.ts +4 -0
  16. package/dist/contours.js +66 -0
  17. package/dist/denoise.d.ts +3 -0
  18. package/dist/denoise.js +37 -0
  19. package/dist/deskew.d.ts +24 -0
  20. package/dist/deskew.js +202 -0
  21. package/dist/grid-ac-rl-consensus.d.ts +127 -0
  22. package/dist/grid-ac-rl-consensus.js +497 -0
  23. package/dist/grid-arbitrate.d.ts +52 -0
  24. package/dist/grid-arbitrate.js +366 -0
  25. package/dist/grid-cut-snap.d.ts +15 -0
  26. package/dist/grid-cut-snap.js +77 -0
  27. package/dist/grid-description-length.d.ts +47 -0
  28. package/dist/grid-description-length.js +697 -0
  29. package/dist/grid-detect.d.ts +8 -0
  30. package/dist/grid-detect.js +665 -0
  31. package/dist/grid-edge-coherence.d.ts +4 -0
  32. package/dist/grid-edge-coherence.js +158 -0
  33. package/dist/grid-edge-path.d.ts +9 -0
  34. package/dist/grid-edge-path.js +201 -0
  35. package/dist/grid-frame-lattice.d.ts +25 -0
  36. package/dist/grid-frame-lattice.js +420 -0
  37. package/dist/grid-frequency-spectrum.d.ts +11 -0
  38. package/dist/grid-frequency-spectrum.js +89 -0
  39. package/dist/grid-period-boundary.d.ts +13 -0
  40. package/dist/grid-period-boundary.js +255 -0
  41. package/dist/grid-period-comb.d.ts +4 -0
  42. package/dist/grid-period-comb.js +230 -0
  43. package/dist/grid-period-distance.d.ts +7 -0
  44. package/dist/grid-period-distance.js +233 -0
  45. package/dist/grid-phase-field.d.ts +11 -0
  46. package/dist/grid-phase-field.js +446 -0
  47. package/dist/grid-pitch-lattice.d.ts +4 -0
  48. package/dist/grid-pitch-lattice.js +255 -0
  49. package/dist/grid-pitch-search.d.ts +11 -0
  50. package/dist/grid-pitch-search.js +233 -0
  51. package/dist/grid-sample.d.ts +4 -0
  52. package/dist/grid-sample.js +164 -0
  53. package/dist/grid-sampling.d.ts +13 -0
  54. package/dist/grid-sampling.js +132 -0
  55. package/dist/grid-strip-shift.d.ts +44 -0
  56. package/dist/grid-strip-shift.js +223 -0
  57. package/dist/grid-test-images.d.ts +69 -0
  58. package/dist/grid-test-images.js +547 -0
  59. package/dist/grid-warp.d.ts +38 -0
  60. package/dist/grid-warp.js +252 -0
  61. package/dist/grid.d.ts +8 -0
  62. package/dist/grid.js +43 -0
  63. package/dist/index.d.ts +16 -0
  64. package/dist/index.js +14 -0
  65. package/dist/line-guided/cv-lab.d.ts +4 -0
  66. package/dist/line-guided/cv-lab.js +134 -0
  67. package/dist/line-guided/cv-ops.d.ts +29 -0
  68. package/dist/line-guided/cv-ops.js +759 -0
  69. package/dist/line-guided/pixeloe-entry.d.ts +15 -0
  70. package/dist/line-guided/pixeloe-entry.js +122 -0
  71. package/dist/line-guided/pixeloe-legacy.d.ts +31 -0
  72. package/dist/line-guided/pixeloe-legacy.js +301 -0
  73. package/dist/line-guided/xdog.d.ts +7 -0
  74. package/dist/line-guided/xdog.js +84 -0
  75. package/dist/most-frequent.d.ts +3 -0
  76. package/dist/most-frequent.js +56 -0
  77. package/dist/outline-prepass.d.ts +37 -0
  78. package/dist/outline-prepass.js +56 -0
  79. package/dist/palette-snap.d.ts +4 -0
  80. package/dist/palette-snap.js +371 -0
  81. package/dist/palette.d.ts +21 -0
  82. package/dist/palette.js +280 -0
  83. package/dist/pixelate.d.ts +15 -0
  84. package/dist/pixelate.js +214 -0
  85. package/dist/pixelation-algorithm-registry.d.ts +34 -0
  86. package/dist/pixelation-algorithm-registry.js +716 -0
  87. package/dist/pixeloe-prepass.d.ts +12 -0
  88. package/dist/pixeloe-prepass.js +527 -0
  89. package/dist/quantization.d.ts +6 -0
  90. package/dist/quantization.js +145 -0
  91. package/dist/remove-background.d.ts +40 -0
  92. package/dist/remove-background.js +229 -0
  93. package/dist/resize.d.ts +3 -0
  94. package/dist/resize.js +86 -0
  95. package/dist/sampling.d.ts +7 -0
  96. package/dist/sampling.js +301 -0
  97. package/dist/settings.d.ts +36 -0
  98. package/dist/settings.js +181 -0
  99. package/dist/types.d.ts +255 -0
  100. package/dist/types.js +87 -0
  101. package/licenses/PixelOE-Apache-2.0.txt +201 -0
  102. package/licenses/pixel-art-fixer-MIT.txt +21 -0
  103. package/licenses/proper-pixel-art-MIT.txt +21 -0
  104. package/licenses/tensorhead-dithering-MIT.txt +21 -0
  105. package/package.json +62 -4
  106. package/wasm/ac-rl-detect-browser-worker.js +45 -0
  107. package/wasm/ac-rl-detect-worker.mjs +51 -0
  108. package/wasm/pixelfixer_detect.d.ts +65 -0
  109. package/wasm/pixelfixer_detect.js +334 -0
  110. package/wasm/pixelfixer_detect_bg.wasm +0 -0
@@ -0,0 +1,697 @@
1
+ /**
2
+ * Description length of an image under a grid hypothesis: the cost of the cuts and the cell
3
+ * colours plus the residual left by rebuilding the image from them. Grids from every proposer
4
+ * compete on this one scale, so a finer grid has to buy its extra cells with a smaller residual,
5
+ * and a free mesh its cut positions. Every grid is measured on the content inside the image's
6
+ * frame (outer lines of one colour, such as padding), where each grid's cuts are clipped.
7
+ *
8
+ * - The residual is Gaussian at the rebuild's own mean squared error. The variance floor stands
9
+ * for the few pixels a correct grid still misplaces (rounded cuts, compression), so that a
10
+ * residual near zero is not worth any number of extra cells.
11
+ * - A cell colour is coded against its left or upper neighbour at the residual's precision: a cell
12
+ * that repeats its neighbour costs almost nothing, while a cell of new colour costs about the
13
+ * precision it is given to.
14
+ * - Each cell costs CELL_PARAMETERS parameters of half the log of the samples (as in BIC). A
15
+ * lattice costs four cut parameters; a lattice snapped to edges adds each cut's offset within
16
+ * its radius; a free mesh pays MESH_CUT_PARAMETERS per cut.
17
+ *
18
+ * The cells structure the image; how a cell's colour spreads over its pixels is the rendering,
19
+ * and the cheapest of several renderings stands:
20
+ *
21
+ * - Nearest: every pixel takes its cell's mean.
22
+ * - Bilinear: pixels blend the colours of the neighbouring cell centres, which are fitted to the
23
+ * pixels.
24
+ * - Planar: a cell may carry a colour slope, which pays for itself only where it removes enough
25
+ * residual (shaded or gradient-filled cells).
26
+ * - Blurred: cell colours seen through a Gaussian of one of BLUR_WIDTHS, as a soft or resampled
27
+ * upscale renders them. The colours are the cell means sharpened by one deconvolution step.
28
+ */
29
+ const STRIDE = 4;
30
+ const CHANNELS = 3;
31
+ const ALPHA_CHANNEL = 3;
32
+ const ALPHA_SCALE = 255;
33
+ export const RESIDUAL_FLOOR = 16;
34
+ /** Pixels a description covers; larger images are described on evenly spread whole rows. */
35
+ const PIXEL_BUDGET = 1 << 19;
36
+ /** Pixels a cell's centre sits from its first pixel, as a share of the cell. */
37
+ const HALF = 0.5;
38
+ /** Cut parameters of a uniform lattice: a pitch and a phase per axis. */
39
+ const LATTICE_CUT_PARAMETERS = 4;
40
+ /**
41
+ * Parameters a free mesh cut costs. Native pixel grids are regular, so a mesh must explain
42
+ * markedly more than a lattice of the same edges; a drifting or warped grid does.
43
+ */
44
+ const MESH_CUT_PARAMETERS = 80;
45
+ /**
46
+ * Weight on the snapped cut offsets. Snapping chooses each offset from the image, so the offsets
47
+ * cost more than a free choice within the radius would: mild snaps that only chase content edges
48
+ * must not pay for themselves.
49
+ */
50
+ const SNAP_OFFSET_WEIGHT = 400;
51
+ /**
52
+ * Parameters per described cell. Below one, so that a better residual at the true count can win
53
+ * over a coarser covering lattice on painterly scale noise, while invented fine grids still lose.
54
+ */
55
+ const CELL_PARAMETERS = 0.55;
56
+ /** Parameters of a planar cell's slope: one per channel and axis. */
57
+ const SLOPE_PARAMETERS = CHANNELS * 2;
58
+ /** Gaussian widths, in pixels, of the blurred renderings. */
59
+ const BLUR_WIDTHS = [1.2, 2];
60
+ /**
61
+ * The widest blur a cell may carry, as a share of its pitch: the rendering mixes only the
62
+ * neighbouring cells, so a wider blur would reach cells it does not model.
63
+ */
64
+ const MAX_BLUR_SHARE = 0.5;
65
+ /** Deconvolution steps from the cell means to the blurred colours. */
66
+ const DECONVOLUTION_STEPS = 1;
67
+ /** Neighbours on each side a blurred pixel mixes. */
68
+ const BLUR_REACH = 1;
69
+ const BLUR_TAPS = 2 * BLUR_REACH + 1;
70
+ /** Coefficients of the Abramowitz-Stegun approximation of erf (7.1.26). */
71
+ const ERF_P = 0.3275911;
72
+ const ERF_A = [0.254829592, -0.284496736, 1.421413741, -1.453152027, 1.061405429];
73
+ const MIN_PLANAR_PIXELS = 3;
74
+ const MIN_PLANAR_AREA = 4;
75
+ const SINGULAR = 1e-9;
76
+ export var CutModel;
77
+ (function (CutModel) {
78
+ /** One lattice per axis. */
79
+ CutModel["Lattice"] = "lattice";
80
+ /** A lattice whose cuts each moved within a radius. */
81
+ CutModel["Snapped"] = "snapped";
82
+ /** Every cut placed on its own. */
83
+ CutModel["Free"] = "free";
84
+ })(CutModel || (CutModel = {}));
85
+ /** Premultiplied RGBA of a pixel as one number, so every clear pixel reads the same. */
86
+ function packedColour(data, o) {
87
+ const alpha = data[o + ALPHA_CHANNEL];
88
+ if (!alpha)
89
+ return 0;
90
+ let packed = alpha;
91
+ for (let c = 0; c < CHANNELS; c++)
92
+ packed = packed * (ALPHA_SCALE + 1) + data[o + c];
93
+ return packed;
94
+ }
95
+ /**
96
+ * The image less its frame: the outer rows and columns of the corner's colour. A frame describes
97
+ * the same for every grid, so it says nothing about which one fits; only what it encloses does.
98
+ */
99
+ function contentOf(image) {
100
+ const { width, height, data, offset } = image;
101
+ const frame = packedColour(data, offset ? offset(0, 0) : 0);
102
+ const framed = offset
103
+ ? (x, y) => packedColour(data, offset(x, y)) === frame
104
+ : (x, y) => packedColour(data, (y * width + x) * STRIDE) === frame;
105
+ const column = (x) => {
106
+ for (let y = 0; y < height; y++)
107
+ if (!framed(x, y))
108
+ return false;
109
+ return true;
110
+ };
111
+ const row = (y) => {
112
+ for (let x = 0; x < width; x++)
113
+ if (!framed(x, y))
114
+ return false;
115
+ return true;
116
+ };
117
+ let left = 0;
118
+ while (left < width && column(left))
119
+ left++;
120
+ if (left === width)
121
+ return { content: image, left: 0, top: 0 };
122
+ let right = width, top = 0, bottom = height;
123
+ while (column(right - 1))
124
+ right--;
125
+ while (row(top))
126
+ top++;
127
+ while (row(bottom - 1))
128
+ bottom--;
129
+ if (right - left === width && bottom - top === height)
130
+ return { content: image, left, top };
131
+ const content = {
132
+ width: right - left,
133
+ height: bottom - top,
134
+ data,
135
+ offset: offset
136
+ ? (x, y) => offset(x + left, y + top)
137
+ : (x, y) => ((y + top) * width + x + left) * STRIDE,
138
+ };
139
+ return { content, left, top };
140
+ }
141
+ /** Cuts of the input seen inside the content: those within it, plus its two ends. */
142
+ function contentCuts(cuts, start, length) {
143
+ const inside = [0];
144
+ for (const cut of cuts)
145
+ if (cut - start > 0 && cut - start < length)
146
+ inside.push(cut - start);
147
+ inside.push(length);
148
+ return inside;
149
+ }
150
+ /** The lines a description covers: every one, or as many evenly spread as PIXEL_BUDGET allows. */
151
+ export function describedLines(lines, length) {
152
+ const count = Math.min(lines, Math.max(1, Math.floor(PIXEL_BUDGET / length)));
153
+ return Int32Array.from({ length: count }, (_, i) => Math.floor(((i + HALF) * lines) / count));
154
+ }
155
+ export function describeImage(input) {
156
+ const { content: image, left, top } = contentOf(input);
157
+ const { width, height, data, offset } = image;
158
+ const lines = describedLines(height, width);
159
+ const count = lines.length;
160
+ const values = new Float64Array(count * width * CHANNELS);
161
+ for (let r = 0; r < count; r++)
162
+ for (let x = 0; x < width; x++) {
163
+ const o = offset ? offset(x, lines[r]) : (lines[r] * width + x) * STRIDE, v = (r * width + x) * CHANNELS;
164
+ const alpha = data[o + ALPHA_CHANNEL] / ALPHA_SCALE;
165
+ for (let c = 0; c < CHANNELS; c++)
166
+ values[v + c] = data[o + c] * alpha;
167
+ }
168
+ const samples = count * width * CHANNELS;
169
+ return { content: image, left, top, lines, values, samples, parameter: Math.log(samples) / 2 };
170
+ }
171
+ function cellOf(cuts, length) {
172
+ const index = new Int32Array(length);
173
+ for (let cell = 0; cell + 1 < cuts.length; cell++)
174
+ index.fill(cell, cuts[cell], cuts[cell + 1]);
175
+ return index;
176
+ }
177
+ function centres(cuts, length) {
178
+ const points = Array.from({ length: cuts.length - 1 }, (_, i) => (cuts[i] + cuts[i + 1]) * HALF - HALF);
179
+ const lower = new Int32Array(length), weight = new Float64Array(length);
180
+ let a = 0;
181
+ for (let p = 0; p < length; p++) {
182
+ while (a + 1 < points.length && points[a + 1] <= p)
183
+ a++;
184
+ lower[p] = a;
185
+ weight[p] =
186
+ a + 1 < points.length
187
+ ? Math.min(1, Math.max(0, (p - points[a]) / (points[a + 1] - points[a])))
188
+ : 0;
189
+ }
190
+ return { lower, weight };
191
+ }
192
+ /**
193
+ * Cost of the described cells' colours, each coded against the nearer of its left and upper
194
+ * neighbours (or against black when it has neither).
195
+ */
196
+ function colourCost(colours, described, across, down, variance) {
197
+ let cost = 0;
198
+ const distance = (a, b) => {
199
+ let sum = 0;
200
+ for (let c = 0; c < CHANNELS; c++)
201
+ sum += (colours[a * CHANNELS + c] - colours[b * CHANNELS + c]) ** 2;
202
+ return sum;
203
+ };
204
+ for (let j = 0; j < down; j++)
205
+ for (let i = 0; i < across; i++) {
206
+ const cell = j * across + i;
207
+ if (!described[cell])
208
+ continue;
209
+ let step = Infinity;
210
+ if (i > 0 && described[cell - 1])
211
+ step = distance(cell, cell - 1);
212
+ if (j > 0 && described[cell - across])
213
+ step = Math.min(step, distance(cell, cell - across));
214
+ if (!Number.isFinite(step)) {
215
+ step = 0;
216
+ for (let c = 0; c < CHANNELS; c++)
217
+ step += colours[cell * CHANNELS + c] ** 2;
218
+ }
219
+ cost += (CHANNELS / 2) * Math.log1p(step / (CHANNELS * variance));
220
+ }
221
+ return cost;
222
+ }
223
+ function erf(x) {
224
+ const t = 1 / (1 + ERF_P * Math.abs(x));
225
+ let polynomial = 0;
226
+ for (let k = ERF_A.length - 1; k >= 0; k--)
227
+ polynomial = (polynomial + ERF_A[k]) * t;
228
+ return Math.sign(x) * (1 - polynomial * Math.exp(-x * x));
229
+ }
230
+ /** Share of a Gaussian at `centre` that falls left of `edge`. */
231
+ const below = (edge, centre, sigma) => HALF * (1 + erf((edge - centre) / (sigma * Math.SQRT2)));
232
+ /** Per pixel, the weights of the cell before, at and after its own under a Gaussian blur. */
233
+ function blurWeights(cuts, cells, sigma) {
234
+ const length = cells.length, count = cuts.length - 1;
235
+ const weights = new Float64Array(length * BLUR_TAPS);
236
+ for (let p = 0; p < length; p++) {
237
+ const centre = p + HALF;
238
+ let sum = 0;
239
+ for (let d = -BLUR_REACH; d <= BLUR_REACH; d++) {
240
+ const k = cells[p] + d;
241
+ if (k < 0 || k >= count)
242
+ continue;
243
+ const w = below(cuts[k + 1], centre, sigma) - below(cuts[k], centre, sigma);
244
+ weights[p * BLUR_TAPS + d + BLUR_REACH] = w;
245
+ sum += w;
246
+ }
247
+ if (sum)
248
+ for (let d = 0; d < BLUR_TAPS; d++)
249
+ weights[p * BLUR_TAPS + d] /= sum;
250
+ }
251
+ return weights;
252
+ }
253
+ function cellMoments(described, cutsX, cutsY) {
254
+ const { content: image, lines, values } = described;
255
+ const { width } = image;
256
+ const across = cutsX.length - 1, down = cutsY.length - 1, cells = across * down;
257
+ const counts = new Float64Array(cells), means = new Float64Array(cells * CHANNELS), slopes = new Float64Array(cells);
258
+ let flat = 0, first = 0;
259
+ for (let j = 0; j < down; j++) {
260
+ const bottom = Math.trunc(cutsY[j + 1]);
261
+ let last = first;
262
+ while (last < lines.length && lines[last] < bottom)
263
+ last++;
264
+ for (let i = 0; i < across; i++) {
265
+ const cell = j * across + i, left = Math.trunc(cutsX[i]), right = Math.trunc(cutsX[i + 1]);
266
+ let n = 0, sx = 0, sy = 0, sxx = 0, syy = 0, sxy = 0, squares = 0, sr = 0, sg = 0, sb = 0, srx = 0, sgx = 0, sbx = 0, sry = 0, sgy = 0, sby = 0;
267
+ for (let r = first; r < last; r++) {
268
+ const ly = lines[r] - cutsY[j], row = r * width * CHANNELS;
269
+ for (let x = left; x < right; x++) {
270
+ const lx = x - cutsX[i], at = row + x * CHANNELS, red = values[at], green = values[at + 1], blue = values[at + 2];
271
+ n++;
272
+ sx += lx;
273
+ sy += ly;
274
+ sxx += lx * lx;
275
+ syy += ly * ly;
276
+ sxy += lx * ly;
277
+ sr += red;
278
+ srx += red * lx;
279
+ sry += red * ly;
280
+ squares += red * red;
281
+ sg += green;
282
+ sgx += green * lx;
283
+ sgy += green * ly;
284
+ squares += green * green;
285
+ sb += blue;
286
+ sbx += blue * lx;
287
+ sby += blue * ly;
288
+ squares += blue * blue;
289
+ }
290
+ }
291
+ if (!n)
292
+ continue;
293
+ counts[cell] = n;
294
+ const mxx = sxx - (sx * sx) / n, myy = syy - (sy * sy) / n, mxy = sxy - (sx * sy) / n, det = mxx * myy - mxy * mxy;
295
+ let residual = squares, removed = 0;
296
+ means[cell * CHANNELS] = sr / n;
297
+ residual -= (sr * sr) / n;
298
+ const grx = srx - (sx * sr) / n, gry = sry - (sy * sr) / n;
299
+ if (det > SINGULAR && n >= MIN_PLANAR_AREA)
300
+ removed += (myy * grx * grx - 2 * mxy * grx * gry + mxx * gry * gry) / det;
301
+ else if (mxx > SINGULAR && n >= MIN_PLANAR_PIXELS)
302
+ removed += (grx * grx) / mxx;
303
+ else if (myy > SINGULAR && n >= MIN_PLANAR_PIXELS)
304
+ removed += (gry * gry) / myy;
305
+ means[cell * CHANNELS + 1] = sg / n;
306
+ residual -= (sg * sg) / n;
307
+ const ggx = sgx - (sx * sg) / n, ggy = sgy - (sy * sg) / n;
308
+ if (det > SINGULAR && n >= MIN_PLANAR_AREA)
309
+ removed += (myy * ggx * ggx - 2 * mxy * ggx * ggy + mxx * ggy * ggy) / det;
310
+ else if (mxx > SINGULAR && n >= MIN_PLANAR_PIXELS)
311
+ removed += (ggx * ggx) / mxx;
312
+ else if (myy > SINGULAR && n >= MIN_PLANAR_PIXELS)
313
+ removed += (ggy * ggy) / myy;
314
+ means[cell * CHANNELS + 2] = sb / n;
315
+ residual -= (sb * sb) / n;
316
+ const gbx = sbx - (sx * sb) / n, gby = sby - (sy * sb) / n;
317
+ if (det > SINGULAR && n >= MIN_PLANAR_AREA)
318
+ removed += (myy * gbx * gbx - 2 * mxy * gbx * gby + mxx * gby * gby) / det;
319
+ else if (mxx > SINGULAR && n >= MIN_PLANAR_PIXELS)
320
+ removed += (gbx * gbx) / mxx;
321
+ else if (myy > SINGULAR && n >= MIN_PLANAR_PIXELS)
322
+ removed += (gby * gby) / myy;
323
+ residual = Math.max(0, residual);
324
+ flat += residual;
325
+ slopes[cell] = Math.max(0, Math.min(removed, residual));
326
+ }
327
+ first = last;
328
+ }
329
+ return { counts, means, flat, slopes };
330
+ }
331
+ /** Gram matrix of one axis's blend weights over the given positions: diagonal and next entry. */
332
+ function blendGram(axis, count, positions) {
333
+ const diagonal = new Float64Array(count).fill(SINGULAR), next = new Float64Array(count);
334
+ for (let k = 0; k < positions.length; k++) {
335
+ const i = axis.lower[positions[k]], t = axis.weight[positions[k]];
336
+ diagonal[i] += (1 - t) * (1 - t);
337
+ if (i + 1 < count) {
338
+ diagonal[i + 1] += t * t;
339
+ next[i] += (1 - t) * t;
340
+ }
341
+ }
342
+ return { diagonal, next };
343
+ }
344
+ /** Solves a symmetric tridiagonal system in place, on every `stride`th value from `base`. */
345
+ function solveTridiagonal({ diagonal, next }, values, base, stride, scratch) {
346
+ const count = diagonal.length;
347
+ let pivot = diagonal[0];
348
+ scratch[0] = next[0] / pivot;
349
+ values[base] /= pivot;
350
+ for (let k = 1; k < count; k++) {
351
+ pivot = diagonal[k] - next[k - 1] * scratch[k - 1];
352
+ scratch[k] = next[k] / pivot;
353
+ values[base + k * stride] =
354
+ (values[base + k * stride] - next[k - 1] * values[base + (k - 1) * stride]) / pivot;
355
+ }
356
+ for (let k = count - 2; k >= 0; k--)
357
+ values[base + k * stride] -= scratch[k] * values[base + (k + 1) * stride];
358
+ }
359
+ /**
360
+ * Residual of the bilinear blend of the cell centres, with the centre colours that rebuild the
361
+ * described pixels best. The blend is separable, so the least-squares colours solve one
362
+ * tridiagonal system along each axis.
363
+ */
364
+ function bilinearError(described, cutsX, cutsY, bound) {
365
+ const { content: image, lines, values, samples, parameter } = described;
366
+ const useBound = bound !== undefined &&
367
+ Number.isFinite(bound.best) &&
368
+ Number.isFinite(bound.structure) &&
369
+ Number.isFinite(samples) &&
370
+ samples > 0 &&
371
+ Number.isFinite(parameter);
372
+ const { width, height } = image;
373
+ const across = cutsX.length - 1, down = cutsY.length - 1;
374
+ const cx = centres(cutsX, width), cy = centres(cutsY, height);
375
+ const finiteWeights = cx.weight.every(Number.isFinite) && cy.weight.every(Number.isFinite);
376
+ const colours = new Float64Array(across * down * CHANNELS);
377
+ const lefts = new Int32Array(width), rights = new Int32Array(width);
378
+ for (let x = 0; x < width; x++) {
379
+ lefts[x] = cx.lower[x] * CHANNELS;
380
+ rights[x] = Math.min(across - 1, cx.lower[x] + 1) * CHANNELS;
381
+ }
382
+ for (let r = 0; r < lines.length; r++) {
383
+ const y = lines[r], top = cy.lower[y] * across * CHANNELS, bottom = Math.min(down - 1, cy.lower[y] + 1) * across * CHANNELS, ty = cy.weight[y];
384
+ for (let x = 0; x < width; x++) {
385
+ const v = (r * width + x) * CHANNELS;
386
+ // Finite interpolation weights add only zeros here; the other additions keep their order.
387
+ if (finiteWeights && values[v] === 0 && values[v + 1] === 0 && values[v + 2] === 0)
388
+ continue;
389
+ const tx = cx.weight[x], o0 = top + lefts[x], o1 = top + rights[x], o2 = bottom + lefts[x], o3 = bottom + rights[x], s0 = (1 - tx) * (1 - ty), s1 = tx * (1 - ty), s2 = (1 - tx) * ty, s3 = tx * ty;
390
+ colours[o0] += s0 * values[v];
391
+ colours[o0 + 1] += s0 * values[v + 1];
392
+ colours[o0 + 2] += s0 * values[v + 2];
393
+ colours[o1] += s1 * values[v];
394
+ colours[o1 + 1] += s1 * values[v + 1];
395
+ colours[o1 + 2] += s1 * values[v + 2];
396
+ colours[o2] += s2 * values[v];
397
+ colours[o2 + 1] += s2 * values[v + 1];
398
+ colours[o2 + 2] += s2 * values[v + 2];
399
+ colours[o3] += s3 * values[v];
400
+ colours[o3 + 1] += s3 * values[v + 1];
401
+ colours[o3 + 2] += s3 * values[v + 2];
402
+ }
403
+ }
404
+ const columns = Int32Array.from({ length: width }, (_, x) => x);
405
+ const gramX = blendGram(cx, across, columns), gramY = blendGram(cy, down, lines);
406
+ const scratch = new Float64Array(Math.max(across, down));
407
+ for (let j = 0; j < down; j++)
408
+ for (let c = 0; c < CHANNELS; c++)
409
+ solveTridiagonal(gramX, colours, j * across * CHANNELS + c, CHANNELS, scratch);
410
+ for (let i = 0; i < across; i++)
411
+ for (let c = 0; c < CHANNELS; c++)
412
+ solveTridiagonal(gramY, colours, i * CHANNELS + c, across * CHANNELS, scratch);
413
+ const fittedBound = useBound && finiteWeights && colours.every(Number.isFinite);
414
+ let error = 0;
415
+ for (let r = 0; r < lines.length; r++) {
416
+ const y = lines[r], top = cy.lower[y] * across * CHANNELS, bottom = Math.min(down - 1, cy.lower[y] + 1) * across * CHANNELS, ty = cy.weight[y];
417
+ for (let x = 0; x < width; x++) {
418
+ const tx = cx.weight[x], o0 = top + lefts[x], o1 = top + rights[x], o2 = bottom + lefts[x], o3 = bottom + rights[x], s0 = (1 - tx) * (1 - ty), s1 = tx * (1 - ty), s2 = (1 - tx) * ty, s3 = tx * ty, v = (r * width + x) * CHANNELS;
419
+ const rebuilt0 = s0 * colours[o0] + s1 * colours[o1] + s2 * colours[o2] + s3 * colours[o3];
420
+ const difference0 = values[v] - rebuilt0;
421
+ error += difference0 * difference0;
422
+ const rebuilt1 = s0 * colours[o0 + 1] + s1 * colours[o1 + 1] + s2 * colours[o2 + 1] + s3 * colours[o3 + 1];
423
+ const difference1 = values[v + 1] - rebuilt1;
424
+ error += difference1 * difference1;
425
+ const rebuilt2 = s0 * colours[o0 + 2] + s1 * colours[o1 + 2] + s2 * colours[o2 + 2] + s3 * colours[o3 + 2];
426
+ const difference2 = values[v + 2] - rebuilt2;
427
+ error += difference2 * difference2;
428
+ }
429
+ if (fittedBound) {
430
+ // Remaining squared residuals and the colour code cannot reduce this rendering length.
431
+ const lower = (samples / 2) * Math.log(error / samples + RESIDUAL_FLOOR) + 0 + bound.structure + 0;
432
+ const slack = LENGTH_BOUND_ROUNDING_ULPS * Number.EPSILON * (Math.abs(lower) + Math.abs(bound.best) + 1);
433
+ if (Number.isFinite(lower) && Number.isFinite(slack) && lower - bound.best > slack)
434
+ return { error, colours, discarded: true };
435
+ }
436
+ }
437
+ return { error, colours, discarded: false };
438
+ }
439
+ /** Numerical slack keeps a bound evaluation away from libm and comparison rounding edges. */
440
+ const LENGTH_BOUND_ROUNDING_ULPS = 64;
441
+ /** Residual of the cell colours seen through a Gaussian of `sigma`, after deconvolution. */
442
+ function blurredError(described, moments, cutsX, cutsY, columns, rows, sigma, bound) {
443
+ const { lines, values: pixels, samples, parameter } = described;
444
+ const useBound = bound !== undefined &&
445
+ Number.isFinite(bound.best) &&
446
+ Number.isFinite(bound.structure) &&
447
+ Number.isFinite(samples) &&
448
+ samples > 0 &&
449
+ Number.isFinite(parameter);
450
+ const width = columns.length;
451
+ const across = cutsX.length - 1, down = cutsY.length - 1;
452
+ const wx = blurWeights(cutsX, columns, sigma), wy = blurWeights(cutsY, rows, sigma);
453
+ const colours = new Float64Array(moments.means);
454
+ const correction = new Float64Array(colours.length);
455
+ const finiteHorizontalWeights = wx.every(Number.isFinite);
456
+ const zeroBlend = new Uint8Array(across);
457
+ // One cell of zeros pads each end of a row's vertical blend.
458
+ const blend = new Float64Array((across + 2 * BLUR_REACH) * CHANNELS);
459
+ let error = 0;
460
+ for (let step = 0; step <= DECONVOLUTION_STEPS; step++) {
461
+ // Each pass before the last only corrects the colours; the last only measures the residual.
462
+ const last = step === DECONVOLUTION_STEPS;
463
+ error = 0;
464
+ if (!last)
465
+ correction.fill(0);
466
+ for (let r = 0; r < lines.length; r++) {
467
+ const y = lines[r], j = rows[y];
468
+ blend.fill(0);
469
+ for (let d = -BLUR_REACH; d <= BLUR_REACH; d++) {
470
+ const w = wy[y * BLUR_TAPS + d + BLUR_REACH], jj = j + d;
471
+ if (!w || jj < 0 || jj >= down)
472
+ continue;
473
+ for (let k = 0; k < across * CHANNELS; k++)
474
+ blend[BLUR_REACH * CHANNELS + k] += w * colours[jj * across * CHANNELS + k];
475
+ }
476
+ // Mark exact zero blends only when multiplying them cannot introduce a nonfinite value.
477
+ if (finiteHorizontalWeights)
478
+ for (let i = 0; i < across; i++) {
479
+ const b = i * CHANNELS;
480
+ zeroBlend[i] = Number(blend[b] === 0 &&
481
+ blend[b + 1] === 0 &&
482
+ blend[b + 2] === 0 &&
483
+ blend[b + CHANNELS] === 0 &&
484
+ blend[b + CHANNELS + 1] === 0 &&
485
+ blend[b + CHANNELS + 2] === 0 &&
486
+ blend[b + 2 * CHANNELS] === 0 &&
487
+ blend[b + 2 * CHANNELS + 1] === 0 &&
488
+ blend[b + 2 * CHANNELS + 2] === 0);
489
+ }
490
+ if (last) {
491
+ for (let x = 0; x < width; x++) {
492
+ const i = columns[x], v = (r * width + x) * CHANNELS, t = x * BLUR_TAPS, b = i * CHANNELS, w0 = wx[t], w1 = wx[t + 1], w2 = wx[t + 2];
493
+ if (zeroBlend[i] && pixels[v] === 0 && pixels[v + 1] === 0 && pixels[v + 2] === 0)
494
+ continue;
495
+ let red = 0;
496
+ red += w0 * blend[b];
497
+ red += w1 * blend[b + CHANNELS];
498
+ red += w2 * blend[b + 2 * CHANNELS];
499
+ const dr = pixels[v] - red;
500
+ let green = 0;
501
+ green += w0 * blend[b + 1];
502
+ green += w1 * blend[b + CHANNELS + 1];
503
+ green += w2 * blend[b + 2 * CHANNELS + 1];
504
+ const dg = pixels[v + 1] - green;
505
+ let blue = 0;
506
+ blue += w0 * blend[b + 2];
507
+ blue += w1 * blend[b + CHANNELS + 2];
508
+ blue += w2 * blend[b + 2 * CHANNELS + 2];
509
+ const db = pixels[v + 2] - blue;
510
+ error += dr * dr;
511
+ error += dg * dg;
512
+ error += db * db;
513
+ }
514
+ }
515
+ else {
516
+ for (let x = 0; x < width; x++) {
517
+ const i = columns[x], v = (r * width + x) * CHANNELS, t = x * BLUR_TAPS, b = i * CHANNELS, w0 = wx[t], w1 = wx[t + 1], w2 = wx[t + 2];
518
+ if (zeroBlend[i] && pixels[v] === 0 && pixels[v + 1] === 0 && pixels[v + 2] === 0)
519
+ continue;
520
+ let red = 0;
521
+ red += w0 * blend[b];
522
+ red += w1 * blend[b + CHANNELS];
523
+ red += w2 * blend[b + 2 * CHANNELS];
524
+ const dr = pixels[v] - red;
525
+ let green = 0;
526
+ green += w0 * blend[b + 1];
527
+ green += w1 * blend[b + CHANNELS + 1];
528
+ green += w2 * blend[b + 2 * CHANNELS + 1];
529
+ const dg = pixels[v + 1] - green;
530
+ let blue = 0;
531
+ blue += w0 * blend[b + 2];
532
+ blue += w1 * blend[b + CHANNELS + 2];
533
+ blue += w2 * blend[b + 2 * CHANNELS + 2];
534
+ const db = pixels[v + 2] - blue;
535
+ const cell = (j * across + i) * CHANNELS;
536
+ correction[cell] += dr;
537
+ correction[cell + 1] += dg;
538
+ correction[cell + 2] += db;
539
+ }
540
+ }
541
+ if (last && useBound) {
542
+ // Remaining squared residuals and the color code can only increase this length.
543
+ // Keep the additions in the same order as renderingLength's length() below.
544
+ const lower = (samples / 2) * Math.log(error / samples + RESIDUAL_FLOOR) +
545
+ 0 +
546
+ bound.structure +
547
+ parameter;
548
+ const slack = LENGTH_BOUND_ROUNDING_ULPS *
549
+ Number.EPSILON *
550
+ (Math.abs(lower) + Math.abs(bound.best) + 1);
551
+ if (Number.isFinite(lower) && Number.isFinite(slack) && lower - bound.best > slack)
552
+ return { error, colours, discarded: true };
553
+ }
554
+ }
555
+ if (last)
556
+ break;
557
+ for (let cell = 0; cell < across * down; cell++) {
558
+ const n = moments.counts[cell];
559
+ if (n)
560
+ for (let c = 0; c < CHANNELS; c++)
561
+ colours[cell * CHANNELS + c] += correction[cell * CHANNELS + c] / n;
562
+ }
563
+ }
564
+ return { error, colours, discarded: false };
565
+ }
566
+ /** Description length per sample of the cell colours and residual, cheapest rendering first. */
567
+ function renderingLength(described, cutsX, cutsY) {
568
+ const { content: image, samples, parameter } = described;
569
+ const across = cutsX.length - 1, down = cutsY.length - 1;
570
+ const columns = cellOf(cutsX, image.width), rows = cellOf(cutsY, image.height);
571
+ const moments = cellMoments(described, cutsX, cutsY);
572
+ let cells = 0;
573
+ for (const count of moments.counts)
574
+ if (count)
575
+ cells++;
576
+ const structure = cells * parameter * CELL_PARAMETERS;
577
+ const length = (error, colours, extra = 0) => {
578
+ const variance = error / samples + RESIDUAL_FLOOR;
579
+ return ((samples / 2) * Math.log(variance) +
580
+ colourCost(colours, moments.counts, across, down, variance) +
581
+ structure +
582
+ extra);
583
+ };
584
+ let best = length(moments.flat, moments.means);
585
+ // A slope pays for itself where the residual it removes, at the flat cells' precision, beats
586
+ // its parameters; every cell also says whether it carries one.
587
+ const precision = 2 * (moments.flat / samples + RESIDUAL_FLOOR);
588
+ const slopeCost = SLOPE_PARAMETERS * parameter;
589
+ let planarError = moments.flat, planarCost = cells * Math.LN2;
590
+ for (const removed of moments.slopes)
591
+ if (removed / precision > slopeCost) {
592
+ planarError -= removed;
593
+ planarCost += slopeCost;
594
+ }
595
+ best = Math.min(best, length(planarError, moments.means, planarCost));
596
+ const bilinear = bilinearError(described, cutsX, cutsY, { best, structure });
597
+ if (!bilinear.discarded)
598
+ best = Math.min(best, length(bilinear.error, bilinear.colours));
599
+ const pitch = Math.min(image.width / across, image.height / down);
600
+ for (const sigma of BLUR_WIDTHS) {
601
+ if (sigma > MAX_BLUR_SHARE * pitch)
602
+ break;
603
+ const blurred = blurredError(described, moments, cutsX, cutsY, columns, rows, sigma, {
604
+ best,
605
+ structure,
606
+ });
607
+ if (!blurred.discarded)
608
+ best = Math.min(best, length(blurred.error, blurred.colours, parameter));
609
+ }
610
+ return best / samples;
611
+ }
612
+ /** Description length per sample of the cuts alone. */
613
+ function cutLength(described, cuts) {
614
+ const across = cuts.cutsX.length - 1, down = cuts.cutsY.length - 1;
615
+ const lattice = LATTICE_CUT_PARAMETERS * described.parameter;
616
+ let cost = lattice;
617
+ switch (cuts.model) {
618
+ case CutModel.Lattice:
619
+ break;
620
+ case CutModel.Snapped:
621
+ cost +=
622
+ SNAP_OFFSET_WEIGHT *
623
+ ((across - 1) * Math.log(2 * (cuts.radiusX ?? 0) + 1) +
624
+ (down - 1) * Math.log(2 * (cuts.radiusY ?? 0) + 1));
625
+ break;
626
+ case CutModel.Free:
627
+ cost = MESH_CUT_PARAMETERS * described.parameter * (across + down - 2);
628
+ break;
629
+ }
630
+ return cost / described.samples;
631
+ }
632
+ /** Rendering costs shared by the cut models and phase readings of one description. */
633
+ const renderingLengths = new WeakMap();
634
+ /** Description length per sample (pixel channel of a described row), in nats. */
635
+ export function descriptionLength(described, cuts) {
636
+ const { content, left, top } = described;
637
+ const cutsX = contentCuts(cuts.cutsX, left, content.width), cutsY = contentCuts(cuts.cutsY, top, content.height);
638
+ // The rendering depends on the clipped cuts; the cut model's cost is added separately.
639
+ // Arbitration revisits these cuts for snapped readings, twins and octave evidence.
640
+ const key = `${cutsX.join(",")}|${cutsY.join(",")}`;
641
+ let lengths = renderingLengths.get(described);
642
+ if (!lengths) {
643
+ lengths = new Map();
644
+ renderingLengths.set(described, lengths);
645
+ }
646
+ let rendering = lengths.get(key);
647
+ if (rendering === undefined) {
648
+ rendering = renderingLength(described, cutsX, cutsY);
649
+ lengths.set(key, rendering);
650
+ }
651
+ return rendering + cutLength(described, cuts);
652
+ }
653
+ /**
654
+ * Nats of a pixel's colour coded against a neighbour `step` (squared premultiplied distance) away,
655
+ * beyond the residual floor every sample pays, as `pixelLength` codes every pixel.
656
+ */
657
+ export function colourStepCost(step) {
658
+ return (CHANNELS / 2) * Math.log1p(step / (CHANNELS * RESIDUAL_FLOOR));
659
+ }
660
+ /**
661
+ * Description length per sample of the image as it is: one cell per pixel, which rebuilds it
662
+ * exactly, each pixel coded against its left or upper neighbour.
663
+ */
664
+ export function pixelLength(described) {
665
+ const { content: image, lines, values, samples, parameter } = described;
666
+ const { width } = image;
667
+ let colours = 0;
668
+ for (let r = 0; r < lines.length; r++) {
669
+ const above = r > 0 && lines[r - 1] === lines[r] - 1;
670
+ for (let x = 0; x < width; x++) {
671
+ const v = (r * width + x) * CHANNELS;
672
+ let step = Infinity;
673
+ const distance = (o) => {
674
+ let sum = 0;
675
+ for (let c = 0; c < CHANNELS; c++)
676
+ sum += (values[v + c] - values[o + c]) ** 2;
677
+ return sum;
678
+ };
679
+ if (x > 0)
680
+ step = distance(v - CHANNELS);
681
+ if (above)
682
+ step = Math.min(step, distance(v - width * CHANNELS));
683
+ if (!Number.isFinite(step)) {
684
+ step = 0;
685
+ for (let c = 0; c < CHANNELS; c++)
686
+ step += values[v + c] ** 2;
687
+ }
688
+ colours += colourStepCost(step);
689
+ }
690
+ }
691
+ const cells = lines.length * width;
692
+ return (((samples / 2) * Math.log(RESIDUAL_FLOOR) +
693
+ colours +
694
+ cells * parameter * CELL_PARAMETERS +
695
+ LATTICE_CUT_PARAMETERS * parameter) /
696
+ samples);
697
+ }