@xprite/pixelate 0.0.0-stage → 0.1.1

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 +270 -2
  4. package/README.zh-CN.md +240 -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,255 @@
1
+ import { MAX_GRID_CELL_SIZE, MAX_RECOVERED_GRID_SIZE, pixelGridCuts, } from "./grid.js";
2
+ import { PixelationAlgorithm } from "./types.js";
3
+ /**
4
+ * Boundary run-length comb period detection for painterly and similar inputs where soft pitch
5
+ * search and edge detection may fail. Counts run lengths of the same boundary color along the
6
+ * image edges, scores uniform step hypotheses by how well they comb those runs, and proposes
7
+ * lattices whose periods match the detected step on each axis.
8
+ *
9
+ * This is an additional candidate source that feeds the arbiter alongside edge/uniform/pitch
10
+ * detectors; it does not replace them. The arbiter's min-DL still chooses the final grid among
11
+ * all proposals and the passthrough baseline.
12
+ */
13
+ const MIN_STEP = 2.0;
14
+ const MIN_CELLS = 4;
15
+ const MIN_GRID_CELLS = 16;
16
+ const MAX_STEP_SEARCH = 48; // Expanded to cover larger periods
17
+ const STEP_INCREMENT = 0.05; // Finer granularity for better precision
18
+ /** Number of top-scoring periods to consider per axis. */
19
+ const MAX_CANDIDATES_PER_AXIS = 8;
20
+ /** Exponential weight decay for successive comb teeth (k0 in the reference). */
21
+ const DECAY_CONSTANT = 12.0;
22
+ /** Maximum teeth count (comb order) when scoring a period. */
23
+ const MAX_COMB_ORDER = 24;
24
+ /**
25
+ * Extract run lengths of consecutive similar colors across sampled lines.
26
+ * Uses a more permissive threshold to catch soft edges in painterly inputs.
27
+ */
28
+ function extractColorRuns(image, horizontal) {
29
+ const { width, height, data } = image;
30
+ const scanLength = horizontal ? width : height;
31
+ const crossLength = horizontal ? height : width;
32
+ // Sample more lines for better statistics
33
+ const maxSampledLines = 128;
34
+ const stride = Math.max(1, Math.floor(crossLength / maxSampledLines));
35
+ const runs = [];
36
+ // Looser threshold for painterly/blurred edges
37
+ const COLOR_THRESHOLD = 30;
38
+ for (let line = 0; line < crossLength; line += stride) {
39
+ let runLength = 0;
40
+ let lastR = -1, lastG = -1, lastB = -1, lastA = -1;
41
+ for (let i = 0; i < scanLength; i++) {
42
+ const pixelIndex = horizontal ? (line * width + i) * 4 : (i * width + line) * 4;
43
+ const r = data[pixelIndex];
44
+ const g = data[pixelIndex + 1];
45
+ const b = data[pixelIndex + 2];
46
+ const a = data[pixelIndex + 3];
47
+ // Check if color is similar to previous pixel
48
+ let similar = false;
49
+ if (lastR >= 0) {
50
+ const dist = Math.sqrt((r - lastR) ** 2 + (g - lastG) ** 2 + (b - lastB) ** 2);
51
+ const alphaDiff = Math.abs(a - lastA);
52
+ similar = dist <= COLOR_THRESHOLD && alphaDiff <= 40;
53
+ }
54
+ if (i === 0 || similar) {
55
+ runLength++;
56
+ }
57
+ else {
58
+ if (runLength > 0)
59
+ runs.push(runLength);
60
+ runLength = 1;
61
+ }
62
+ lastR = r;
63
+ lastG = g;
64
+ lastB = b;
65
+ lastA = a;
66
+ }
67
+ if (runLength > 0)
68
+ runs.push(runLength);
69
+ }
70
+ return runs;
71
+ }
72
+ /**
73
+ * Score a period hypothesis using a comb-anticomb approach on run lengths.
74
+ * Comb peaks at k*step, anticomb at k*step ± 0.5*step.
75
+ * A true period will have strong alignment at multiples of the step.
76
+ */
77
+ function runHistogram(runs) {
78
+ const tally = new Map();
79
+ let longest = 0;
80
+ for (const run of runs) {
81
+ tally.set(run, (tally.get(run) || 0) + 1);
82
+ longest = Math.max(longest, run);
83
+ }
84
+ return { lengths: [...tally.keys()], counts: [...tally.values()], total: runs.length, longest };
85
+ }
86
+ function scorePeriod(histogram, step) {
87
+ const { lengths, counts, total: totalRuns, longest: maxRun } = histogram;
88
+ if (totalRuns < 2 || step < MIN_STEP)
89
+ return -1;
90
+ const maxK = Math.min(MAX_COMB_ORDER, Math.floor(maxRun / step));
91
+ if (maxK < 2)
92
+ return -1;
93
+ let combScore = 0;
94
+ let antiCombScore = 0;
95
+ let totalWeight = 0;
96
+ // Comb-anticomb scoring: measure alignment vs offset
97
+ for (let k = 1; k <= maxK; k++) {
98
+ const target = k * step;
99
+ const weight = Math.exp(-(k - 1) / DECAY_CONSTANT);
100
+ totalWeight += weight;
101
+ // Window for "near" the target
102
+ const window = Math.max(0.5, step * 0.12);
103
+ // Comb: count runs near k*step
104
+ let combCount = 0;
105
+ // Anticomb: count runs near k*step ± 0.5*step
106
+ let antiCombCount = 0;
107
+ const offsetLeft = target - 0.5 * step;
108
+ const offsetRight = target + 0.5 * step;
109
+ for (let h = 0; h < lengths.length; h++) {
110
+ const runLength = lengths[h], count = counts[h];
111
+ const distTarget = Math.abs(runLength - target);
112
+ const distLeft = Math.abs(runLength - offsetLeft);
113
+ const distRight = Math.abs(runLength - offsetRight);
114
+ if (distTarget <= window) {
115
+ combCount += count;
116
+ }
117
+ if (distLeft <= window || distRight <= window) {
118
+ antiCombCount += count;
119
+ }
120
+ }
121
+ combScore += weight * combCount;
122
+ antiCombScore += weight * antiCombCount;
123
+ }
124
+ // Normalize by total weight and runs
125
+ if (totalWeight === 0 || totalRuns === 0)
126
+ return -1;
127
+ const normalizedComb = combScore / (totalWeight * totalRuns);
128
+ const normalizedAntiComb = antiCombScore / (totalWeight * totalRuns);
129
+ // Return comb - anticomb difference
130
+ return normalizedComb - normalizedAntiComb;
131
+ }
132
+ /**
133
+ * Search for the best period (step size) on one axis by scoring many candidates
134
+ * and returning the top scorers.
135
+ */
136
+ function searchAxisPeriod(runs, maxStep) {
137
+ const candidates = [];
138
+ const histogram = runHistogram(runs);
139
+ for (let step = MIN_STEP; step <= maxStep; step += STEP_INCREMENT) {
140
+ const score = scorePeriod(histogram, step);
141
+ if (score > 0) {
142
+ candidates.push({ step, score });
143
+ }
144
+ }
145
+ // Sort by score descending and take top candidates
146
+ candidates.sort((a, b) => b.score - a.score);
147
+ return candidates.slice(0, MAX_CANDIDATES_PER_AXIS);
148
+ }
149
+ /**
150
+ * Detect a grid using boundary run-length comb period search. Returns multiple
151
+ * candidates (one per top-scoring period combination) for the arbiter to evaluate.
152
+ */
153
+ export function detectBoundaryPeriodGrids(image) {
154
+ const { width, height } = image;
155
+ const minStep = Math.max(MIN_STEP, Math.max(width, height) / MAX_RECOVERED_GRID_SIZE);
156
+ const maxStepX = Math.min(MAX_STEP_SEARCH, Math.min(MAX_GRID_CELL_SIZE, width / MIN_CELLS));
157
+ const maxStepY = Math.min(MAX_STEP_SEARCH, Math.min(MAX_GRID_CELL_SIZE, height / MIN_CELLS));
158
+ if (maxStepX < minStep || maxStepY < minStep)
159
+ return [];
160
+ // Extract color runs across sampled lines
161
+ const xRuns = extractColorRuns(image, true);
162
+ const yRuns = extractColorRuns(image, false);
163
+ if (xRuns.length < 2 || yRuns.length < 2)
164
+ return [];
165
+ // Search for periods on each axis
166
+ const xCandidates = searchAxisPeriod(xRuns, maxStepX);
167
+ const yCandidates = searchAxisPeriod(yRuns, maxStepY);
168
+ if (xCandidates.length === 0 || yCandidates.length === 0)
169
+ return [];
170
+ // Generate grid proposals from period combinations.
171
+ // For each axis step, emit covering lattices at floor/round/ceil cell counts so a step
172
+ // that lands between two integers (e.g. 826/2.35 ≈ 351.5) still proposes the true count.
173
+ const grids = [];
174
+ const seen = new Set();
175
+ /**
176
+ * Covering cell counts for a period: always the nearest integer, plus the other side when
177
+ * length/step is near halfway (so 826/2.35 ≈ 351.5 still proposes 352). Far-from-half
178
+ * fractions (545/2.35 ≈ 231.91) keep only the rounded count — emitting the farther floor
179
+ * let a lower-residual wrong height win on painterly inputs.
180
+ */
181
+ const countNeighbors = (length, step) => {
182
+ const exact = length / step;
183
+ if (!Number.isFinite(exact) || exact <= 0)
184
+ return [];
185
+ const rounded = Math.round(exact);
186
+ const neighbors = [rounded];
187
+ const frac = exact - Math.floor(exact);
188
+ if (frac > 0.35 && frac < 0.65) {
189
+ const other = frac >= 0.5 ? Math.floor(exact) : Math.ceil(exact);
190
+ if (other > 0 && other !== rounded)
191
+ neighbors.push(other);
192
+ }
193
+ return neighbors;
194
+ };
195
+ for (const xCand of xCandidates.slice(0, 3)) {
196
+ for (const yCand of yCandidates.slice(0, 3)) {
197
+ const confidence = Math.min(xCand.score, yCand.score);
198
+ for (const cols of countNeighbors(width, xCand.step)) {
199
+ for (const rows of countNeighbors(height, yCand.step)) {
200
+ if (cols < MIN_CELLS ||
201
+ rows < MIN_CELLS ||
202
+ cols * rows < MIN_GRID_CELLS ||
203
+ cols > MAX_RECOVERED_GRID_SIZE ||
204
+ rows > MAX_RECOVERED_GRID_SIZE) {
205
+ continue;
206
+ }
207
+ const key = `${cols}x${rows}`;
208
+ if (seen.has(key))
209
+ continue;
210
+ seen.add(key);
211
+ // Image-covering lattice for the cell counts (not the raw float step).
212
+ const coverWidth = width / cols;
213
+ const coverHeight = height / rows;
214
+ const cutsX = pixelGridCuts(width, coverWidth, 0);
215
+ const cutsY = pixelGridCuts(height, coverHeight, 0);
216
+ grids.push({
217
+ width: cols,
218
+ height: rows,
219
+ cellWidth: coverWidth,
220
+ cellHeight: coverHeight,
221
+ offsetX: 0,
222
+ offsetY: 0,
223
+ cutsX,
224
+ cutsY,
225
+ confidence,
226
+ reliable: true,
227
+ algorithm: PixelationAlgorithm.UniformGrid,
228
+ });
229
+ }
230
+ }
231
+ }
232
+ }
233
+ return grids;
234
+ }
235
+ /**
236
+ * Returns the top-scored boundary period grid, or unreliable when none found.
237
+ * This is the single-hypothesis interface for registry entries; the multi-hypothesis
238
+ * version above feeds the arbiter's candidate pool.
239
+ */
240
+ export function detectBoundaryPeriodGrid(image) {
241
+ const grids = detectBoundaryPeriodGrids(image);
242
+ if (grids.length === 0) {
243
+ return {
244
+ width: 1,
245
+ height: 1,
246
+ cellWidth: image.width,
247
+ cellHeight: image.height,
248
+ offsetX: 0,
249
+ offsetY: 0,
250
+ confidence: 0,
251
+ reliable: false,
252
+ };
253
+ }
254
+ return grids[0];
255
+ }
@@ -0,0 +1,4 @@
1
+ import type { PixelBuffer } from "./base.ts";
2
+ import { type PixelGridAnalysis } from "./types.ts";
3
+ /** Lattices covering the image at the cell counts of the strongest steps of either axis. */
4
+ export declare function combCountGrids(image: PixelBuffer): PixelGridAnalysis[];
@@ -0,0 +1,230 @@
1
+ import { blockPartition, MAX_RECOVERED_GRID_SIZE } from "./grid.js";
2
+ import { PixelationAlgorithm } from "./types.js";
3
+ /**
4
+ * Cell counts read from the period of the colour steps, for upscales whose cells keep a steady
5
+ * count but not a steady phase (warped, re-blocked or locally shifted grids), where no single
6
+ * lattice lines up with the whole image.
7
+ *
8
+ * Each axis is read through the first and second differences of the colour, summed over short
9
+ * bands of lines and over all lines. Every band's autocorrelation is normalised on its own, so a
10
+ * band whose cuts are shifted against another's still peaks at the same lag; their mean is
11
+ * phase-free. A step scores the autocorrelation on its multiples against the midpoints between
12
+ * them (a comb), less half the best score of its own fractions, so the fundamental of a harmonic
13
+ * train outranks its multiples. The strongest steps propose lattices that cover the whole image at
14
+ * the counts the steps round to.
15
+ */
16
+ const CHANNELS = 3;
17
+ const STRIDE = 4;
18
+ const ALPHA_SCALE = 255;
19
+ const ALPHA_CHANNEL = 3;
20
+ /** Lines summed into one band of the profile. */
21
+ const BAND_LINES = 16;
22
+ /** Bands read per axis; taller images read evenly spread bands. */
23
+ const MAX_BANDS = 64;
24
+ const MIN_STEP = 2.5;
25
+ const MAX_STEP = 64;
26
+ /** The longest step is this fraction of the axis, so its comb holds several teeth. */
27
+ const MIN_PERIODS = 4;
28
+ /** Relative spacing of the steps the comb scores. */
29
+ const STEP_RATIO = 1.0025;
30
+ const COMB_TEETH = 24;
31
+ /** Lags the autocorrelation covers, in multiples of the longest step. */
32
+ const LAG_PERIODS = 12;
33
+ /** Teeth weigh exp(-(k - 1) / COMB_DECAY), so the near multiples count most. */
34
+ const COMB_DECAY = 12;
35
+ const HALF = 0.5;
36
+ const SUBHARMONICS = [2, 3, 4, 5];
37
+ const SUBHARMONIC_PENALTY = 0.5;
38
+ /** Steps kept per axis. */
39
+ const STEPS_PER_AXIS = 3;
40
+ /** Half-width of the window a refined tooth is looked for in, as a share of the step. */
41
+ const TOOTH_WINDOW = 0.3;
42
+ const MIN_TOOTH_WINDOW = 2;
43
+ const MIN_TOOTH_TOLERANCE = 1.5;
44
+ const REFINE_PASSES = 3;
45
+ /** A refined step stays within this ratio of the step the comb found. */
46
+ const REFINE_LIMIT = 1.25;
47
+ const MIN_GRID_CELLS = 2;
48
+ var Difference;
49
+ (function (Difference) {
50
+ Difference[Difference["First"] = 1] = "First";
51
+ Difference[Difference["Second"] = 2] = "Second";
52
+ })(Difference || (Difference = {}));
53
+ /** Absolute colour difference of the given order at every pixel along each line of an axis. */
54
+ function differences(image, horizontal, order) {
55
+ const { width, height, data } = image;
56
+ const length = horizontal ? width : height;
57
+ const lines = horizontal ? height : width;
58
+ const out = new Float64Array(lines * length);
59
+ const value = (along, line, c) => {
60
+ const o = (horizontal ? line * width + along : along * width + line) * STRIDE;
61
+ return (data[o + c] * data[o + ALPHA_CHANNEL]) / ALPHA_SCALE;
62
+ };
63
+ const end = order === Difference.Second ? length - 1 : length;
64
+ for (let line = 0; line < lines; line++)
65
+ for (let i = 1; i < end; i++) {
66
+ let energy = 0;
67
+ for (let c = 0; c < CHANNELS; c++) {
68
+ const here = value(i, line, c), before = value(i - 1, line, c);
69
+ energy += Math.abs(order === Difference.Second ? value(i + 1, line, c) - 2 * here + before : here - before);
70
+ }
71
+ out[line * length + i] = energy;
72
+ }
73
+ return { lines, length, data: out };
74
+ }
75
+ /** Mean over bands of `band` lines of each band's unbiased autocorrelation, normalised at lag 0. */
76
+ function bandAutocorrelation(feature, band, maxLag) {
77
+ const { lines, length, data } = feature;
78
+ const size = Math.min(band, lines);
79
+ const bands = Math.max(1, Math.floor(lines / size));
80
+ const read = Math.min(bands, MAX_BANDS);
81
+ const sum = new Float64Array(maxLag + 1);
82
+ const profile = new Float64Array(length);
83
+ let used = 0;
84
+ for (let k = 0; k < read; k++) {
85
+ const first = Math.floor((k * bands) / read) * size;
86
+ profile.fill(0);
87
+ for (let line = first; line < first + size; line++)
88
+ for (let i = 0; i < length; i++)
89
+ profile[i] += data[line * length + i];
90
+ let mean = 0;
91
+ for (let i = 0; i < length; i++)
92
+ mean += profile[i];
93
+ mean /= length;
94
+ let energy = 0;
95
+ for (let i = 0; i < length; i++) {
96
+ profile[i] -= mean;
97
+ energy += profile[i] * profile[i];
98
+ }
99
+ if (!energy)
100
+ continue;
101
+ used++;
102
+ for (let lag = 0; lag <= maxLag; lag++) {
103
+ let s = 0;
104
+ for (let i = 0; i + lag < length; i++)
105
+ s += profile[i] * profile[i + lag];
106
+ sum[lag] += (s / energy) * (length / (length - lag));
107
+ }
108
+ }
109
+ if (used)
110
+ for (let lag = 0; lag <= maxLag; lag++)
111
+ sum[lag] /= used;
112
+ return sum;
113
+ }
114
+ function at(values, position) {
115
+ const i = Math.max(0, Math.min(values.length - 2, Math.floor(position)));
116
+ const t = position - i;
117
+ return values[i] * (1 - t) + values[i + 1] * t;
118
+ }
119
+ /** Weighted mean of the autocorrelation on the multiples of `step` less that between them. */
120
+ function comb(correlation, step) {
121
+ const teeth = Math.min(Math.floor((correlation.length - 2) / step), COMB_TEETH);
122
+ if (teeth < 2)
123
+ return -Infinity;
124
+ let sum = 0, weights = 0;
125
+ for (let k = 1; k <= teeth; k++) {
126
+ const weight = Math.exp(-(k - 1) / COMB_DECAY);
127
+ const between = HALF * (at(correlation, (k - HALF) * step) + at(correlation, (k + HALF) * step));
128
+ sum += weight * (at(correlation, k * step) - between);
129
+ weights += weight;
130
+ }
131
+ return sum / weights;
132
+ }
133
+ /** The step of the line through the peaks of the autocorrelation near each multiple of `step`. */
134
+ function refine(correlation, step) {
135
+ const n = correlation.length;
136
+ const teeth = Math.min(Math.floor((n - 3) / step), COMB_TEETH);
137
+ const peaks = [];
138
+ const window = Math.max(MIN_TOOTH_WINDOW, Math.floor(TOOTH_WINDOW * step));
139
+ for (let k = 1; k <= teeth; k++) {
140
+ const low = Math.max(1, Math.floor(k * step - window)), high = Math.min(n - 2, Math.ceil(k * step + window));
141
+ let i = low;
142
+ for (let j = low; j <= high; j++)
143
+ if (correlation[j] > correlation[i])
144
+ i = j;
145
+ if (i <= low || i >= high)
146
+ continue;
147
+ const curvature = correlation[i - 1] - 2 * correlation[i] + correlation[i + 1];
148
+ const shift = curvature ? (correlation[i - 1] - correlation[i + 1]) / (2 * curvature) : 0;
149
+ peaks.push({
150
+ position: i + Math.max(-1, Math.min(1, shift)),
151
+ order: k,
152
+ weight: Math.max(Number.EPSILON, correlation[i]),
153
+ });
154
+ }
155
+ let refined = step;
156
+ const tolerance = Math.max(MIN_TOOTH_TOLERANCE, TOOTH_WINDOW * step);
157
+ for (let pass = 0; pass < REFINE_PASSES; pass++) {
158
+ let numerator = 0, denominator = 0, kept = 0;
159
+ for (const { position, order, weight } of peaks)
160
+ if (Math.abs(position - order * refined) <= tolerance) {
161
+ numerator += weight * position * order;
162
+ denominator += weight * order * order;
163
+ kept++;
164
+ }
165
+ if (kept < 2)
166
+ break;
167
+ refined = numerator / denominator;
168
+ }
169
+ return refined >= step / REFINE_LIMIT && refined <= step * REFINE_LIMIT ? refined : step;
170
+ }
171
+ /** The strongest cell steps along one axis, strongest first. */
172
+ function combSteps(image, horizontal) {
173
+ const length = horizontal ? image.width : image.height;
174
+ const longest = Math.min(MAX_STEP, length / MIN_PERIODS);
175
+ if (longest <= MIN_STEP)
176
+ return [];
177
+ const maxLag = Math.min(length - 2, Math.ceil(longest * LAG_PERIODS));
178
+ const correlation = new Float64Array(maxLag + 1);
179
+ const readings = [];
180
+ for (const order of [Difference.First, Difference.Second]) {
181
+ const feature = differences(image, horizontal, order);
182
+ for (const band of [BAND_LINES, feature.lines])
183
+ readings.push(bandAutocorrelation(feature, band, maxLag));
184
+ }
185
+ for (const reading of readings)
186
+ for (let lag = 0; lag <= maxLag; lag++)
187
+ correlation[lag] += reading[lag] / readings.length;
188
+ const steps = [];
189
+ for (let step = MIN_STEP; step <= longest; step *= STEP_RATIO)
190
+ steps.push(step);
191
+ const raw = steps.map((step) => comb(correlation, step));
192
+ const rawAt = (step) => {
193
+ const index = Math.round(Math.log(step / MIN_STEP) / Math.log(STEP_RATIO));
194
+ return index < 0 ? -Infinity : raw[Math.min(raw.length - 1, index)];
195
+ };
196
+ const selective = steps.map((step, i) => {
197
+ let fraction = 0;
198
+ for (const m of SUBHARMONICS)
199
+ if (step / m >= MIN_STEP)
200
+ fraction = Math.max(fraction, rawAt(step / m));
201
+ return raw[i] - SUBHARMONIC_PENALTY * fraction;
202
+ });
203
+ const peaks = [];
204
+ for (let i = 1; i + 1 < steps.length; i++)
205
+ if (selective[i] > selective[i - 1] && selective[i] >= selective[i + 1])
206
+ peaks.push(i);
207
+ peaks.sort((a, b) => selective[b] - selective[a]);
208
+ return peaks.slice(0, STEPS_PER_AXIS).map((i) => refine(correlation, steps[i]));
209
+ }
210
+ /** Lattices covering the image at the cell counts of the strongest steps of either axis. */
211
+ export function combCountGrids(image) {
212
+ const steps = [...combSteps(image, true), ...combSteps(image, false)];
213
+ const grids = [];
214
+ const seen = new Set();
215
+ const limit = (count, length) => count >= MIN_GRID_CELLS && count <= Math.min(length, MAX_RECOVERED_GRID_SIZE);
216
+ for (const step of steps) {
217
+ const columns = Math.round(image.width / step), rows = Math.round(image.height / step);
218
+ const key = `${columns}x${rows}`;
219
+ if (seen.has(key) || !limit(columns, image.width) || !limit(rows, image.height))
220
+ continue;
221
+ seen.add(key);
222
+ grids.push({
223
+ ...blockPartition(image, columns, rows),
224
+ confidence: 0,
225
+ reliable: true,
226
+ algorithm: PixelationAlgorithm.UniformGrid,
227
+ });
228
+ }
229
+ return grids;
230
+ }
@@ -0,0 +1,7 @@
1
+ import type { PixelBuffer } from "./base.ts";
2
+ import { type PixelGridAnalysis } from "./types.ts";
3
+ /**
4
+ * The lattice covering the image at the cell counts of the axes' standing steps; an axis without
5
+ * one takes the other's.
6
+ */
7
+ export declare function distanceCountGrids(image: PixelBuffer): PixelGridAnalysis[];