@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,420 @@
1
+ import { MAX_GRID_CELL_SIZE, MAX_RECOVERED_GRID_SIZE, pixelGridCuts, } from "./grid.js";
2
+ import { gridFrequencySpectrum } from "./grid-frequency-spectrum.js";
3
+ import { PixelationAlgorithm } from "./types.js";
4
+ /**
5
+ * Whole-frame lattices. An upscale of a whole image maps native cell i of n onto
6
+ * [i, i + 1) * length / n. A soft resample (bilinear, blurred, AI) kinks at each cell's sample
7
+ * point (i + 0.5) * length / n - 0.5; a nearest one steps on each boundary i * length / n. The
8
+ * curvature energy (soft) or the step energy (hard) along each axis is projected onto that comb
9
+ * for every integer count n. The projection is phase-locked to the frame, so it needs no pitch or
10
+ * phase search, integrates every line of the image, and stays exact for cells below two pixels,
11
+ * where the comb lies above the Nyquist rate and an autocorrelation only sees its alias.
12
+ *
13
+ * Sampled at whole pixels, soft kinks at count n and hard steps at count length - n are the
14
+ * same signal. Below a two-pixel pitch a lattice therefore also has to explain the image better
15
+ * under its model than the alias does under the other. Hard steps at counts n and length - n
16
+ * project alike too; the boundary edges the two counts do not share decide between them.
17
+ *
18
+ * A count is accepted when the projection stands out from the profile's own fluctuation on both
19
+ * axes, a reconstruction from the lattice's sample points beats the same reconstruction half a
20
+ * cell out of step across the frame, and the lattice is no harmonic or subdivision of a coarser
21
+ * grid. A hard lattice's boundaries must also gather the steps.
22
+ */
23
+ const CHANNELS = 4;
24
+ const ALPHA_CHANNEL = 3;
25
+ const ALPHA_SCALE = 255;
26
+ /** Below this pitch a lattice is no longer told from a blurred native image. */
27
+ const MIN_PITCH = 1.5;
28
+ /** As the soft arbiter: fewer cells per axis can line up with large smooth features. */
29
+ const MIN_CELLS = 8;
30
+ const MAX_SAMPLED_LINES = 1024;
31
+ /** Relative pitch difference (plus one cell) the two axes of one square-celled lattice may show. */
32
+ const SQUARE_TOLERANCE = 0.02;
33
+ /** Joint projection, in standard deviations of a structureless profile, a lattice must reach. */
34
+ const MIN_SIGNIFICANCE = 5;
35
+ const MAX_HARMONICS = 8;
36
+ /** Share of the out-of-step reconstruction error the lattice must remove. */
37
+ const MIN_GAIN = 0.1;
38
+ /** Cells the out-of-step lattices gain or lose across the frame. */
39
+ const OUT_OF_STEP = 0.5;
40
+ const RECONSTRUCTION_ROWS = 256;
41
+ /** Pitch below which the comb lies above the Nyquist rate and has an alias. */
42
+ const ALIAS_PITCH = 2;
43
+ /** Mean step inside the groups of a k-fold split, against the step between groups. */
44
+ const SPLIT_STEP_RATIO = 0.25;
45
+ /** Step energy on the finer alias's own boundaries, against the shared ones, that keeps it. */
46
+ const ALIAS_CONTRAST = 0.4;
47
+ /**
48
+ * Share of the step energy a hard lattice's boundaries must gather beyond an even spread: a soft
49
+ * image's broad features also step periodically at a few coarse counts.
50
+ */
51
+ const MIN_STEP_EXCESS = 0.15;
52
+ /** How the native cells were resampled onto the frame. */
53
+ export var FrameModel;
54
+ (function (FrameModel) {
55
+ /** Bilinear-like: the profile kinks at each cell's sample point. */
56
+ FrameModel["Soft"] = "soft";
57
+ /** Nearest-like: colour steps on each cell boundary. */
58
+ FrameModel["Hard"] = "hard";
59
+ })(FrameModel || (FrameModel = {}));
60
+ var EdgeKind;
61
+ (function (EdgeKind) {
62
+ EdgeKind[EdgeKind["Shared"] = 0] = "Shared";
63
+ EdgeKind[EdgeKind["Exclusive"] = 1] = "Exclusive";
64
+ EdgeKind[EdgeKind["Neither"] = 2] = "Neither";
65
+ })(EdgeKind || (EdgeKind = {}));
66
+ const EDGE_KINDS = 3;
67
+ var Resample;
68
+ (function (Resample) {
69
+ Resample[Resample["Bilinear"] = 0] = "Bilinear";
70
+ Resample[Resample["Nearest"] = 1] = "Nearest";
71
+ })(Resample || (Resample = {}));
72
+ const resampleOf = (model) => model === FrameModel.Soft ? Resample.Bilinear : Resample.Nearest;
73
+ const premultiplied = (data, o, c) => c === ALPHA_CHANNEL ? data[o + c] : (data[o + c] * data[o + ALPHA_CHANNEL]) / ALPHA_SCALE;
74
+ /**
75
+ * Mean energy along sampled lines of the squared difference of every channel: the second
76
+ * difference centred on pixel i (soft), or the step from pixel i - 1 to pixel i (hard).
77
+ */
78
+ function energyProfile(image, horizontal, model) {
79
+ const { width, height, data } = image;
80
+ const length = horizontal ? width : height;
81
+ const cross = horizontal ? height : width;
82
+ const stride = Math.max(1, Math.ceil(cross / MAX_SAMPLED_LINES));
83
+ const unit = (horizontal ? 1 : width) * CHANNELS;
84
+ const soft = model === FrameModel.Soft;
85
+ const profile = new Float64Array(length);
86
+ let lines = 0;
87
+ for (let line = 0; line < cross; line += stride, lines++) {
88
+ const start = (horizontal ? line * width : line) * CHANNELS;
89
+ for (let i = 1; i < length - (soft ? 1 : 0); i++) {
90
+ const o = start + i * unit;
91
+ let energy = 0;
92
+ for (let c = 0; c < CHANNELS; c++) {
93
+ const d = soft
94
+ ? premultiplied(data, o - unit, c) -
95
+ 2 * premultiplied(data, o, c) +
96
+ premultiplied(data, o + unit, c)
97
+ : premultiplied(data, o, c) - premultiplied(data, o - unit, c);
98
+ energy += d * d;
99
+ }
100
+ profile[i] += energy;
101
+ }
102
+ }
103
+ for (let i = 0; i < length; i++)
104
+ profile[i] /= lines;
105
+ return profile;
106
+ }
107
+ /**
108
+ * Projection of the profile onto the comb of every count from `first` to `last`, in units of its
109
+ * standard deviation were the profile structureless: positive when the energy gathers on the
110
+ * sample points (soft) or on the cell boundaries (hard).
111
+ */
112
+ export function combSignificance(profile, first, last, model) {
113
+ const length = profile.length;
114
+ const soft = model === FrameModel.Soft;
115
+ // The soft profile lives on pixels 1..length-2, the hard one on the edges 1..length-1.
116
+ const end = soft ? length - 1 : length;
117
+ const centred = new Float64Array(length);
118
+ let mean = 0;
119
+ for (let i = 1; i < end; i++)
120
+ mean += profile[i];
121
+ mean /= end - 1;
122
+ let energy = 0;
123
+ for (let i = 1; i < end; i++) {
124
+ centred[i] = profile[i] - mean;
125
+ energy += centred[i] ** 2;
126
+ }
127
+ const noise = Math.sqrt(energy / 2);
128
+ const significance = new Float64Array(Math.max(0, last - first + 1));
129
+ if (noise <= 0)
130
+ return { first, significance };
131
+ const spectrum = gridFrequencySpectrum(centred);
132
+ for (let count = first; count <= last; count++) {
133
+ // F[k] uses exp(-i theta). Re(F[k] exp(-i phi)) therefore keeps the signed
134
+ // cosine projection, including the soft comb's half-pixel phase phi = pi k / length.
135
+ const phase = soft ? (Math.PI * count) / length : 0;
136
+ const sum = spectrum.real[count] * Math.cos(phase) + spectrum.imaginary[count] * Math.sin(phase);
137
+ significance[count - first] = ((soft ? -1 : 1) * sum) / noise;
138
+ }
139
+ return { first, significance };
140
+ }
141
+ /**
142
+ * Harmonic sum of the comb at `count`: the pulses also project onto every multiple k * count,
143
+ * with sign (-1)^(k + 1) for kinks on the sample points and +1 for steps on the boundaries. The
144
+ * best normalised partial sum over the first harmonics prefers the fundamental over its
145
+ * multiples (which miss harmonics) and rejects its fractions (whose harmonics fall between the
146
+ * pulses).
147
+ */
148
+ function harmonicSum(comb, count, model) {
149
+ const { first, significance } = comb;
150
+ const last = first + significance.length - 1;
151
+ let partial = 0, best = -Infinity;
152
+ for (let k = 1; k <= MAX_HARMONICS && k * count <= last; k++) {
153
+ const sign = model === FrameModel.Hard || k % 2 ? 1 : -1;
154
+ partial += sign * significance[k * count - first];
155
+ best = Math.max(best, partial / Math.sqrt(k));
156
+ }
157
+ return best;
158
+ }
159
+ /**
160
+ * Whether the counts are a whole multiple of counts whose own comb is significant, of either
161
+ * sign: the lattice then repeats a coarser grid's harmonic, which the edge detectors and the
162
+ * arbiter judge instead.
163
+ */
164
+ function isHarmonic(x, y, columns, rows) {
165
+ for (let k = 2; k <= MAX_HARMONICS; k++) {
166
+ if (columns % k || rows % k || columns / k < x.first || rows / k < y.first)
167
+ continue;
168
+ const fundamental = (Math.abs(x.significance[columns / k - x.first]) +
169
+ Math.abs(y.significance[rows / k - y.first])) /
170
+ Math.SQRT2;
171
+ if (fundamental >= MIN_SIGNIFICANCE)
172
+ return true;
173
+ }
174
+ return false;
175
+ }
176
+ /** Sample positions of `cells` (possibly fractional) frame-aligned cells, clamped to the axis. */
177
+ function samplePoints(length, cells) {
178
+ const points = new Float64Array(Math.ceil(cells));
179
+ for (let i = 0; i < points.length; i++)
180
+ points[i] = Math.min(length - 1, Math.max(0, ((i + 0.5) * length) / cells - 0.5));
181
+ return points;
182
+ }
183
+ /** The image read bilinearly at the sample points of `columns` x `rows` frame-aligned cells. */
184
+ function latticeSamples(image, columns, rows) {
185
+ const { width, height, data } = image;
186
+ const xs = samplePoints(width, columns), ys = samplePoints(height, rows);
187
+ const values = new Float64Array(xs.length * ys.length * CHANNELS);
188
+ const at = (x, y) => (y * width + x) * CHANNELS;
189
+ for (let j = 0; j < ys.length; j++) {
190
+ const y0 = Math.floor(ys[j]), y1 = Math.min(height - 1, y0 + 1), ty = ys[j] - y0;
191
+ for (let i = 0; i < xs.length; i++) {
192
+ const x0 = Math.floor(xs[i]), x1 = Math.min(width - 1, x0 + 1), tx = xs[i] - x0;
193
+ for (let c = 0; c < CHANNELS; c++)
194
+ values[(j * xs.length + i) * CHANNELS + c] =
195
+ (premultiplied(data, at(x0, y0), c) * (1 - tx) +
196
+ premultiplied(data, at(x1, y0), c) * tx) *
197
+ (1 - ty) +
198
+ (premultiplied(data, at(x0, y1), c) * (1 - tx) +
199
+ premultiplied(data, at(x1, y1), c) * tx) *
200
+ ty;
201
+ }
202
+ }
203
+ return { columns, rows, across: xs.length, down: ys.length, values };
204
+ }
205
+ /** Lower sample index, and weight of the next one, that each pixel of an axis reads. */
206
+ function spread(length, cells, points, resample) {
207
+ const index = new Int32Array(length), weight = new Float64Array(length);
208
+ for (let p = 0; p < length; p++) {
209
+ const u = Math.min(points - 1, Math.max(0, ((p + 0.5) * cells) / length - 0.5));
210
+ if (resample === Resample.Nearest) {
211
+ index[p] = Math.round(u);
212
+ continue;
213
+ }
214
+ index[p] = Math.min(points - 2, Math.floor(u));
215
+ weight[p] = u - index[p];
216
+ }
217
+ return { index, weight };
218
+ }
219
+ /** Mean squared error, on sampled rows, of the image against its rebuild from the samples. */
220
+ function reconstructionError(image, lattice, resample) {
221
+ const { width, height, data } = image;
222
+ const { across, down, values } = lattice;
223
+ const sx = spread(width, lattice.columns, across, resample), sy = spread(height, lattice.rows, down, resample);
224
+ const stride = Math.max(1, Math.floor(height / RECONSTRUCTION_ROWS));
225
+ let error = 0, count = 0;
226
+ for (let y = 0; y < height; y += stride) {
227
+ const top = sy.index[y] * across, bottom = Math.min(down - 1, sy.index[y] + 1) * across, ty = sy.weight[y];
228
+ for (let x = 0; x < width; x++) {
229
+ const left = sx.index[x], right = Math.min(across - 1, left + 1), tx = sx.weight[x];
230
+ const o = (y * width + x) * CHANNELS;
231
+ for (let c = 0; c < CHANNELS; c++) {
232
+ const s = (cell) => values[cell * CHANNELS + c];
233
+ const value = (s(top + left) * (1 - tx) + s(top + right) * tx) * (1 - ty) +
234
+ (s(bottom + left) * (1 - tx) + s(bottom + right) * tx) * ty;
235
+ error += (value - premultiplied(data, o, c)) ** 2;
236
+ }
237
+ count++;
238
+ }
239
+ }
240
+ return count ? error / count : 0;
241
+ }
242
+ /** The out-of-step reconstruction error share that the lattice itself avoids. */
243
+ function reconstructionGain(image, lattice, model) {
244
+ const { columns, rows } = lattice;
245
+ const resample = resampleOf(model);
246
+ const aligned = reconstructionError(image, lattice, resample);
247
+ const outOfStep = (cells) => reconstructionError(image, latticeSamples(image, cells, (rows * cells) / columns), resample);
248
+ const off = (outOfStep(columns + OUT_OF_STEP) + outOfStep(columns - OUT_OF_STEP)) / 2;
249
+ return off > 0 ? 1 - aligned / off : 0;
250
+ }
251
+ /** Whether the other model at the alias counts explains the image at least as well. */
252
+ function aliasExplains(image, lattice, model) {
253
+ const { width, height } = image;
254
+ const { columns, rows } = lattice;
255
+ const aliasColumns = width / columns < ALIAS_PITCH ? width - columns : columns, aliasRows = height / rows < ALIAS_PITCH ? height - rows : rows;
256
+ if (aliasColumns === columns && aliasRows === rows)
257
+ return false;
258
+ const other = model === FrameModel.Soft ? FrameModel.Hard : FrameModel.Soft;
259
+ return (reconstructionError(image, latticeSamples(image, aliasColumns, aliasRows), resampleOf(other)) <=
260
+ reconstructionError(image, lattice, resampleOf(model)));
261
+ }
262
+ /**
263
+ * Whether the samples group into k x k blocks (2 <= k <= MAX_HARMONICS) on both axes: a blurred
264
+ * edge kinks on both sides of a coarse boundary, so the comb can also lock to a subdivision of a
265
+ * coarse grid. Steps between neighbouring samples then stay small inside each group and gather
266
+ * on one residue; on a true lattice every residue steps alike.
267
+ */
268
+ function splitsCoarserCells(lattice) {
269
+ const { across, down, values } = lattice;
270
+ const step = (a, b) => {
271
+ let sum = 0;
272
+ for (let c = 0; c < CHANNELS; c++)
273
+ sum += Math.abs(values[a + c] - values[b + c]);
274
+ return sum;
275
+ };
276
+ const splits = (residues) => {
277
+ const sorted = Array.from(residues).sort((a, b) => a - b);
278
+ const inside = sorted.slice(0, -1).reduce((a, b) => a + b, 0) / (sorted.length - 1);
279
+ return inside < SPLIT_STEP_RATIO * sorted[sorted.length - 1];
280
+ };
281
+ for (let k = 2; k <= MAX_HARMONICS; k++) {
282
+ if (across % k || down % k)
283
+ continue;
284
+ const horizontal = new Float64Array(k), vertical = new Float64Array(k);
285
+ for (let j = 0; j < down; j++)
286
+ for (let i = 0; i < across; i++) {
287
+ const o = (j * across + i) * CHANNELS;
288
+ if (i + 1 < across)
289
+ horizontal[i % k] += step(o, o + CHANNELS);
290
+ if (j + 1 < down)
291
+ vertical[j % k] += step(o, o + across * CHANNELS);
292
+ }
293
+ if (splits(horizontal) && splits(vertical))
294
+ return true;
295
+ }
296
+ return false;
297
+ }
298
+ const isSquare = (width, height, columns, rows) => Math.abs(rows - (columns * height) / width) <=
299
+ ((columns * height) / width) * SQUARE_TOLERANCE + 1;
300
+ const countRange = (length) => ({
301
+ first: Math.max(MIN_CELLS, Math.ceil(length / MAX_GRID_CELL_SIZE)),
302
+ last: Math.min(MAX_RECOVERED_GRID_SIZE, Math.floor(length / MIN_PITCH)),
303
+ });
304
+ const inRange = (count, range) => count >= range.first && count <= range.last;
305
+ /** Pixel edges where a nearest resample of `count` frame-aligned cells starts each next cell. */
306
+ function boundaryEdges(length, count) {
307
+ const edges = new Uint8Array(length);
308
+ for (let i = 1; i < count; i++)
309
+ edges[Math.ceil((i * length) / count - 0.5)] = 1;
310
+ return edges;
311
+ }
312
+ /**
313
+ * Step energy on the boundary edges of `fine` cells that are no boundary of `coarse` cells, as a
314
+ * share of the energy on the boundaries both share, each above the energy on edges neither has:
315
+ * blur or jitter spreads steps onto the edges beside them alike, whichever lattice is true.
316
+ */
317
+ function exclusiveContrast(profile, fine, coarse) {
318
+ const length = profile.length;
319
+ const own = boundaryEdges(length, fine), rival = boundaryEdges(length, coarse);
320
+ const sum = new Float64Array(EDGE_KINDS), edges = new Float64Array(EDGE_KINDS);
321
+ for (let e = 1; e < length; e++) {
322
+ const kind = own[e] ? (rival[e] ? EdgeKind.Shared : EdgeKind.Exclusive) : EdgeKind.Neither;
323
+ sum[kind] += profile[e];
324
+ edges[kind]++;
325
+ }
326
+ if (!edges[EdgeKind.Exclusive] || !edges[EdgeKind.Shared] || !edges[EdgeKind.Neither])
327
+ return 0;
328
+ const mean = (kind) => sum[kind] / edges[kind];
329
+ const floor = mean(EdgeKind.Neither);
330
+ const shared = mean(EdgeKind.Shared) - floor;
331
+ return shared > 0 ? (mean(EdgeKind.Exclusive) - floor) / shared : 0;
332
+ }
333
+ /**
334
+ * Steps on the boundaries of count n and of count length - n fall on whole pixels alike, so their
335
+ * combs are identical. The boundaries of the coarser count are nearly all boundaries of the finer
336
+ * one, so the finer count stands only when the steps also fill its own boundaries.
337
+ */
338
+ function stepAlias(image, profiles, counts) {
339
+ const { width, height } = image;
340
+ const alias = { columns: width - counts.columns, rows: height - counts.rows };
341
+ if (!inRange(alias.columns, countRange(width)) ||
342
+ !inRange(alias.rows, countRange(height)) ||
343
+ !isSquare(width, height, alias.columns, alias.rows))
344
+ return counts;
345
+ const [coarse, fine] = alias.columns < counts.columns ? [alias, counts] : [counts, alias];
346
+ const contrast = (exclusiveContrast(profiles.x, fine.columns, coarse.columns) +
347
+ exclusiveContrast(profiles.y, fine.rows, coarse.rows)) /
348
+ 2;
349
+ return contrast >= ALIAS_CONTRAST ? fine : coarse;
350
+ }
351
+ /**
352
+ * Mean step energy on the boundary edges of `count` cells above the mean of all edges, as a share
353
+ * of the excess a nearest resample reaches when every step lies on a boundary.
354
+ */
355
+ function stepExcess(profile, count) {
356
+ const length = profile.length;
357
+ const boundaries = boundaryEdges(length, count);
358
+ let on = 0, all = 0;
359
+ for (let e = 1; e < length; e++) {
360
+ all += profile[e];
361
+ if (boundaries[e])
362
+ on += profile[e];
363
+ }
364
+ const pitch = length / count;
365
+ return all > 0 ? (((on / (count - 1)) * (length - 1)) / all - 1) / (pitch - 1) : 0;
366
+ }
367
+ function fitFrame(image, model) {
368
+ const { width, height } = image;
369
+ const rangeX = countRange(width), rangeY = countRange(height);
370
+ if (rangeX.last < rangeX.first || rangeY.last < rangeY.first)
371
+ return null;
372
+ const profiles = { x: energyProfile(image, true, model), y: energyProfile(image, false, model) };
373
+ const x = combSignificance(profiles.x, rangeX.first, rangeX.last, model), y = combSignificance(profiles.y, rangeY.first, rangeY.last, model);
374
+ const harmonicsY = Array.from({ length: rangeY.last - rangeY.first + 1 }, (_, i) => harmonicSum(y, rangeY.first + i, model));
375
+ let best = { joint: -Infinity, columns: 0, rows: 0 };
376
+ for (let columns = rangeX.first; columns <= rangeX.last; columns++) {
377
+ const ideal = (columns * height) / width;
378
+ const low = Math.max(rangeY.first, Math.floor(ideal * (1 - SQUARE_TOLERANCE)) - 1), high = Math.min(rangeY.last, Math.ceil(ideal * (1 + SQUARE_TOLERANCE)) + 1);
379
+ const along = harmonicSum(x, columns, model);
380
+ for (let rows = low; rows <= high; rows++) {
381
+ const joint = (along + harmonicsY[rows - rangeY.first]) / Math.SQRT2;
382
+ if (joint > best.joint)
383
+ best = { joint, columns, rows };
384
+ }
385
+ }
386
+ if (best.joint < MIN_SIGNIFICANCE)
387
+ return null;
388
+ const hard = model === FrameModel.Hard;
389
+ const { columns, rows } = hard ? stepAlias(image, profiles, best) : best;
390
+ if (isHarmonic(x, y, columns, rows))
391
+ return null;
392
+ if (hard &&
393
+ (stepExcess(profiles.x, columns) + stepExcess(profiles.y, rows)) / 2 < MIN_STEP_EXCESS)
394
+ return null;
395
+ const lattice = latticeSamples(image, columns, rows);
396
+ const gain = reconstructionGain(image, lattice, model);
397
+ if (gain < MIN_GAIN || aliasExplains(image, lattice, model) || splitsCoarserCells(lattice))
398
+ return null;
399
+ const cellWidth = width / columns, cellHeight = height / rows;
400
+ return {
401
+ width: columns,
402
+ height: rows,
403
+ cellWidth,
404
+ cellHeight,
405
+ offsetX: 0,
406
+ offsetY: 0,
407
+ cutsX: pixelGridCuts(width, cellWidth),
408
+ cutsY: pixelGridCuts(height, cellHeight),
409
+ confidence: Math.min(1, gain),
410
+ reliable: true,
411
+ algorithm: PixelationAlgorithm.UniformGrid,
412
+ };
413
+ }
414
+ /**
415
+ * The frame-aligned lattice of each of `models` that passes the significance, reconstruction,
416
+ * alias and subdivision tests. Cells are square up to SQUARE_TOLERANCE.
417
+ */
418
+ export function detectFrameLattices(image, models = [FrameModel.Soft, FrameModel.Hard]) {
419
+ return models.flatMap((model) => fitFrame(image, model) ?? []);
420
+ }
@@ -0,0 +1,11 @@
1
+ interface Spectrum {
2
+ real: Float64Array;
3
+ imaginary: Float64Array;
4
+ }
5
+ /**
6
+ * Unnormalized DFT, F[k] = sum input[j] exp(-2 pi i j k / length), at the original
7
+ * integer frequencies. Bluestein pads only its convolution, so prime and odd image sizes keep
8
+ * the same frequency-to-cell-count relationship as a direct projection.
9
+ */
10
+ export declare function gridFrequencySpectrum(input: Float64Array): Spectrum;
11
+ export {};
@@ -0,0 +1,89 @@
1
+ const TAU = 2 * Math.PI;
2
+ /** In-place radix-2 transform; inverse includes the convolution's normalization. */
3
+ function radix2(real, imaginary, inverse = false) {
4
+ const length = real.length;
5
+ for (let index = 1, reversed = 0; index < length; index++) {
6
+ let bit = length / 2;
7
+ while (reversed & bit) {
8
+ reversed ^= bit;
9
+ bit /= 2;
10
+ }
11
+ reversed ^= bit;
12
+ if (index < reversed) {
13
+ const r = real[index], i = imaginary[index];
14
+ real[index] = real[reversed];
15
+ imaginary[index] = imaginary[reversed];
16
+ real[reversed] = r;
17
+ imaginary[reversed] = i;
18
+ }
19
+ }
20
+ const direction = inverse ? 1 : -1;
21
+ for (let size = 2; size <= length; size *= 2) {
22
+ const half = size / 2;
23
+ for (let offset = 0; offset < half; offset++) {
24
+ const angle = (direction * TAU * offset) / size;
25
+ const cosine = Math.cos(angle), sine = Math.sin(angle);
26
+ for (let index = offset; index < length; index += size) {
27
+ const other = index + half;
28
+ const r = real[other] * cosine - imaginary[other] * sine;
29
+ const i = real[other] * sine + imaginary[other] * cosine;
30
+ real[other] = real[index] - r;
31
+ imaginary[other] = imaginary[index] - i;
32
+ real[index] += r;
33
+ imaginary[index] += i;
34
+ }
35
+ }
36
+ }
37
+ if (inverse)
38
+ for (let index = 0; index < length; index++) {
39
+ real[index] /= length;
40
+ imaginary[index] /= length;
41
+ }
42
+ }
43
+ /**
44
+ * Unnormalized DFT, F[k] = sum input[j] exp(-2 pi i j k / length), at the original
45
+ * integer frequencies. Bluestein pads only its convolution, so prime and odd image sizes keep
46
+ * the same frequency-to-cell-count relationship as a direct projection.
47
+ */
48
+ export function gridFrequencySpectrum(input) {
49
+ const length = input.length;
50
+ if (length === 0)
51
+ return { real: new Float64Array(), imaginary: new Float64Array() };
52
+ if ((length & (length - 1)) === 0) {
53
+ const real = input.slice(), imaginary = new Float64Array(length);
54
+ radix2(real, imaginary);
55
+ return { real, imaginary };
56
+ }
57
+ let padded = 1;
58
+ while (padded < 2 * length - 1)
59
+ padded *= 2;
60
+ const real = new Float64Array(padded), imaginary = new Float64Array(padded), kernelReal = new Float64Array(padded), kernelImaginary = new Float64Array(padded), cosine = new Float64Array(length), sine = new Float64Array(length);
61
+ for (let index = 0; index < length; index++) {
62
+ // The chirp is periodic in index² modulo 2 * length. Reduce before evaluating trig.
63
+ const angle = (Math.PI * ((index * index) % (2 * length))) / length;
64
+ cosine[index] = Math.cos(angle);
65
+ sine[index] = Math.sin(angle);
66
+ real[index] = input[index] * cosine[index];
67
+ imaginary[index] = -input[index] * sine[index];
68
+ kernelReal[index] = cosine[index];
69
+ kernelImaginary[index] = sine[index];
70
+ if (index) {
71
+ kernelReal[padded - index] = cosine[index];
72
+ kernelImaginary[padded - index] = sine[index];
73
+ }
74
+ }
75
+ radix2(real, imaginary);
76
+ radix2(kernelReal, kernelImaginary);
77
+ for (let index = 0; index < padded; index++) {
78
+ const r = real[index] * kernelReal[index] - imaginary[index] * kernelImaginary[index];
79
+ imaginary[index] = real[index] * kernelImaginary[index] + imaginary[index] * kernelReal[index];
80
+ real[index] = r;
81
+ }
82
+ radix2(real, imaginary, true);
83
+ const resultReal = new Float64Array(length), resultImaginary = new Float64Array(length);
84
+ for (let index = 0; index < length; index++) {
85
+ resultReal[index] = real[index] * cosine[index] + imaginary[index] * sine[index];
86
+ resultImaginary[index] = imaginary[index] * cosine[index] - real[index] * sine[index];
87
+ }
88
+ return { real: resultReal, imaginary: resultImaginary };
89
+ }
@@ -0,0 +1,13 @@
1
+ import type { PixelBuffer } from "./base.ts";
2
+ import { type PixelGridAnalysis } from "./types.ts";
3
+ /**
4
+ * Detect a grid using boundary run-length comb period search. Returns multiple
5
+ * candidates (one per top-scoring period combination) for the arbiter to evaluate.
6
+ */
7
+ export declare function detectBoundaryPeriodGrids(image: PixelBuffer): PixelGridAnalysis[];
8
+ /**
9
+ * Returns the top-scored boundary period grid, or unreliable when none found.
10
+ * This is the single-hypothesis interface for registry entries; the multi-hypothesis
11
+ * version above feeds the arbiter's candidate pool.
12
+ */
13
+ export declare function detectBoundaryPeriodGrid(image: PixelBuffer): PixelGridAnalysis;