@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.
- package/LICENSE +21 -0
- package/NOTICE.md +12 -0
- package/README.md +270 -2
- package/README.zh-CN.md +240 -0
- package/dist/analyze.d.ts +8 -0
- package/dist/analyze.js +217 -0
- package/dist/base.d.ts +18 -0
- package/dist/base.js +42 -0
- package/dist/cell-color-picker.d.ts +14 -0
- package/dist/cell-color-picker.js +278 -0
- package/dist/color-lab.d.ts +3 -0
- package/dist/color-lab.js +31 -0
- package/dist/color-step.d.ts +33 -0
- package/dist/color-step.js +62 -0
- package/dist/contours.d.ts +4 -0
- package/dist/contours.js +66 -0
- package/dist/denoise.d.ts +3 -0
- package/dist/denoise.js +37 -0
- package/dist/deskew.d.ts +24 -0
- package/dist/deskew.js +202 -0
- package/dist/grid-ac-rl-consensus.d.ts +127 -0
- package/dist/grid-ac-rl-consensus.js +497 -0
- package/dist/grid-arbitrate.d.ts +52 -0
- package/dist/grid-arbitrate.js +366 -0
- package/dist/grid-cut-snap.d.ts +15 -0
- package/dist/grid-cut-snap.js +77 -0
- package/dist/grid-description-length.d.ts +47 -0
- package/dist/grid-description-length.js +697 -0
- package/dist/grid-detect.d.ts +8 -0
- package/dist/grid-detect.js +665 -0
- package/dist/grid-edge-coherence.d.ts +4 -0
- package/dist/grid-edge-coherence.js +158 -0
- package/dist/grid-edge-path.d.ts +9 -0
- package/dist/grid-edge-path.js +201 -0
- package/dist/grid-frame-lattice.d.ts +25 -0
- package/dist/grid-frame-lattice.js +420 -0
- package/dist/grid-frequency-spectrum.d.ts +11 -0
- package/dist/grid-frequency-spectrum.js +89 -0
- package/dist/grid-period-boundary.d.ts +13 -0
- package/dist/grid-period-boundary.js +255 -0
- package/dist/grid-period-comb.d.ts +4 -0
- package/dist/grid-period-comb.js +230 -0
- package/dist/grid-period-distance.d.ts +7 -0
- package/dist/grid-period-distance.js +233 -0
- package/dist/grid-phase-field.d.ts +11 -0
- package/dist/grid-phase-field.js +446 -0
- package/dist/grid-pitch-lattice.d.ts +4 -0
- package/dist/grid-pitch-lattice.js +255 -0
- package/dist/grid-pitch-search.d.ts +11 -0
- package/dist/grid-pitch-search.js +233 -0
- package/dist/grid-sample.d.ts +4 -0
- package/dist/grid-sample.js +164 -0
- package/dist/grid-sampling.d.ts +13 -0
- package/dist/grid-sampling.js +132 -0
- package/dist/grid-strip-shift.d.ts +44 -0
- package/dist/grid-strip-shift.js +223 -0
- package/dist/grid-test-images.d.ts +69 -0
- package/dist/grid-test-images.js +547 -0
- package/dist/grid-warp.d.ts +38 -0
- package/dist/grid-warp.js +252 -0
- package/dist/grid.d.ts +8 -0
- package/dist/grid.js +43 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +14 -0
- package/dist/line-guided/cv-lab.d.ts +4 -0
- package/dist/line-guided/cv-lab.js +134 -0
- package/dist/line-guided/cv-ops.d.ts +29 -0
- package/dist/line-guided/cv-ops.js +759 -0
- package/dist/line-guided/pixeloe-entry.d.ts +15 -0
- package/dist/line-guided/pixeloe-entry.js +122 -0
- package/dist/line-guided/pixeloe-legacy.d.ts +31 -0
- package/dist/line-guided/pixeloe-legacy.js +301 -0
- package/dist/line-guided/xdog.d.ts +7 -0
- package/dist/line-guided/xdog.js +84 -0
- package/dist/most-frequent.d.ts +3 -0
- package/dist/most-frequent.js +56 -0
- package/dist/outline-prepass.d.ts +37 -0
- package/dist/outline-prepass.js +56 -0
- package/dist/palette-snap.d.ts +4 -0
- package/dist/palette-snap.js +371 -0
- package/dist/palette.d.ts +21 -0
- package/dist/palette.js +280 -0
- package/dist/pixelate.d.ts +15 -0
- package/dist/pixelate.js +214 -0
- package/dist/pixelation-algorithm-registry.d.ts +34 -0
- package/dist/pixelation-algorithm-registry.js +716 -0
- package/dist/pixeloe-prepass.d.ts +12 -0
- package/dist/pixeloe-prepass.js +527 -0
- package/dist/quantization.d.ts +6 -0
- package/dist/quantization.js +145 -0
- package/dist/remove-background.d.ts +40 -0
- package/dist/remove-background.js +229 -0
- package/dist/resize.d.ts +3 -0
- package/dist/resize.js +86 -0
- package/dist/sampling.d.ts +7 -0
- package/dist/sampling.js +301 -0
- package/dist/settings.d.ts +36 -0
- package/dist/settings.js +181 -0
- package/dist/types.d.ts +255 -0
- package/dist/types.js +87 -0
- package/licenses/PixelOE-Apache-2.0.txt +201 -0
- package/licenses/pixel-art-fixer-MIT.txt +21 -0
- package/licenses/proper-pixel-art-MIT.txt +21 -0
- package/licenses/tensorhead-dithering-MIT.txt +21 -0
- package/package.json +62 -4
- package/wasm/ac-rl-detect-browser-worker.js +45 -0
- package/wasm/ac-rl-detect-worker.mjs +51 -0
- package/wasm/pixelfixer_detect.d.ts +65 -0
- package/wasm/pixelfixer_detect.js +334 -0
- package/wasm/pixelfixer_detect_bg.wasm +0 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { blockPartition, MAX_RECOVERED_GRID_SIZE } from "./grid.js";
|
|
2
|
+
import { PixelationAlgorithm } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Cell counts read from the distances between colour boundaries along each line. A boundary moves
|
|
5
|
+
* with the phase of its line, but the distance between two boundaries of one line does not, so
|
|
6
|
+
* the distances keep the pitch of a warped or row-shifted upscale whatever its phase does along
|
|
7
|
+
* the image.
|
|
8
|
+
*
|
|
9
|
+
* Boundaries are the sub-pixel peaks of the colour step along a line, after the step is summed
|
|
10
|
+
* over a few neighbouring lines, so that a cell edge, which runs across many lines, outlasts
|
|
11
|
+
* noise. The distances from every boundary to the next few of its line count, so a spurious
|
|
12
|
+
* boundary inside a cell, which puts one distance off the lattice, still leaves the distance
|
|
13
|
+
* across it on the lattice. A step scores the mean cosine of 2π times distance over step: the
|
|
14
|
+
* multiples of the step add up and the other distances cancel. Every fraction of the true step
|
|
15
|
+
* divides its multiples too and scores about as high, so among the steps near the best score the
|
|
16
|
+
* longest one that is itself a common distance stands for its axis, at the cell count it rounds to.
|
|
17
|
+
*/
|
|
18
|
+
const STRIDE = 4;
|
|
19
|
+
const ALPHA_SCALE = 255;
|
|
20
|
+
const ALPHA_CHANNEL = 3;
|
|
21
|
+
/** Lines the colour step is summed over, centred on the line read. */
|
|
22
|
+
const COHERENCE_LINES = 7;
|
|
23
|
+
/** Lines read per axis; longer axes read evenly spread lines. */
|
|
24
|
+
const MAX_LINES = 384;
|
|
25
|
+
/** A boundary's step is at least EDGE_FLOOR and EDGE_SHARE of the EDGE_QUANTILE of the steps. */
|
|
26
|
+
const EDGE_FLOOR = 20;
|
|
27
|
+
const EDGE_SHARE = 0.1;
|
|
28
|
+
const EDGE_QUANTILE = 0.95;
|
|
29
|
+
/** Boundaries ahead of each boundary whose distances count. */
|
|
30
|
+
const MAX_LAG = 4;
|
|
31
|
+
const MIN_DISTANCE = 2;
|
|
32
|
+
const MAX_DISTANCE = 64;
|
|
33
|
+
const BIN = 0.25;
|
|
34
|
+
const MIN_DISTANCES = 50;
|
|
35
|
+
const MIN_STEP = 2.05;
|
|
36
|
+
const MAX_STEP = 48;
|
|
37
|
+
/** The longest step is this fraction of the axis, so that several cells hold it. */
|
|
38
|
+
const MIN_PERIODS = 4;
|
|
39
|
+
/** Relative spacing of the steps scored. */
|
|
40
|
+
const STEP_RATIO = 1.0025;
|
|
41
|
+
/** Peaks within this share of the best score tie with it. */
|
|
42
|
+
const TIE_SHARE = 0.7;
|
|
43
|
+
/** Share of the distances a standing step needs within FUNDAMENTAL_WINDOW of its own length. */
|
|
44
|
+
const FUNDAMENTAL_SHARE = 0.04;
|
|
45
|
+
const FUNDAMENTAL_WINDOW = 0.18;
|
|
46
|
+
const MIN_FUNDAMENTAL_WINDOW = 0.6;
|
|
47
|
+
/** Refinement: a fine search within REFINE_SPAN of the step, then least squares on the multiples. */
|
|
48
|
+
const REFINE_SPAN = 0.06;
|
|
49
|
+
const REFINE_RATIO = 1.0005;
|
|
50
|
+
const REFINE_BIN = 0.05;
|
|
51
|
+
const REFINE_PASSES = 2;
|
|
52
|
+
/** A distance counts for a multiple while within this share of a step of it. */
|
|
53
|
+
const REFINE_TOLERANCE = 0.3;
|
|
54
|
+
const MIN_GRID_CELLS = 2;
|
|
55
|
+
/** Colour step summed over COHERENCE_LINES lines, at each position of each line read. */
|
|
56
|
+
function lineSteps(image, horizontal) {
|
|
57
|
+
const { width, height, data } = image;
|
|
58
|
+
const length = horizontal ? width : height, count = horizontal ? height : width;
|
|
59
|
+
const lines = Math.min(count, MAX_LINES);
|
|
60
|
+
const steps = new Float64Array(lines * (length - 1));
|
|
61
|
+
const half = (COHERENCE_LINES - 1) >> 1;
|
|
62
|
+
// Adjacent coherence windows overlap; keep each source line only until its last use.
|
|
63
|
+
const cached = new Map();
|
|
64
|
+
for (let k = 0; k < lines; k++) {
|
|
65
|
+
const centre = Math.floor(((k + 0.5) * count) / lines);
|
|
66
|
+
const first = Math.max(0, centre - half);
|
|
67
|
+
const last = Math.min(count - 1, centre + half);
|
|
68
|
+
for (const line of cached.keys())
|
|
69
|
+
if (line < first)
|
|
70
|
+
cached.delete(line);
|
|
71
|
+
for (let line = first; line <= last; line++) {
|
|
72
|
+
let values = cached.get(line);
|
|
73
|
+
if (!values) {
|
|
74
|
+
values = new Float64Array(length - 1);
|
|
75
|
+
const stride = horizontal ? STRIDE : width * STRIDE;
|
|
76
|
+
let a = (horizontal ? line * width : line) * STRIDE;
|
|
77
|
+
for (let i = 0; i + 1 < length; i++) {
|
|
78
|
+
const b = a + stride;
|
|
79
|
+
const alphaA = data[a + ALPHA_CHANNEL] / ALPHA_SCALE, alphaB = data[b + ALPHA_CHANNEL] / ALPHA_SCALE;
|
|
80
|
+
let step = Math.abs(data[a + ALPHA_CHANNEL] - data[b + ALPHA_CHANNEL]);
|
|
81
|
+
step += Math.abs(data[a] * alphaA - data[b] * alphaB);
|
|
82
|
+
step += Math.abs(data[a + 1] * alphaA - data[b + 1] * alphaB);
|
|
83
|
+
step += Math.abs(data[a + 2] * alphaA - data[b + 2] * alphaB);
|
|
84
|
+
values[i] = step;
|
|
85
|
+
a = b;
|
|
86
|
+
}
|
|
87
|
+
cached.set(line, values);
|
|
88
|
+
}
|
|
89
|
+
// Preserve the original ascending source-line summation order for every output step.
|
|
90
|
+
for (let i = 0; i + 1 < length; i++)
|
|
91
|
+
steps[k * (length - 1) + i] += values[i];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { lines, length: length - 1, steps };
|
|
95
|
+
}
|
|
96
|
+
/** Histogram, in BIN-wide bins, of the distances between boundaries of the same line. */
|
|
97
|
+
function boundaryDistances(image, horizontal) {
|
|
98
|
+
const { lines, length, steps } = lineSteps(image, horizontal);
|
|
99
|
+
const histogram = new Float64Array(Math.ceil(MAX_DISTANCE / BIN) + 1);
|
|
100
|
+
const positive = steps.filter((step) => step > 0).sort();
|
|
101
|
+
if (!positive.length)
|
|
102
|
+
return histogram;
|
|
103
|
+
const threshold = Math.max(EDGE_FLOOR, EDGE_SHARE *
|
|
104
|
+
positive[Math.min(positive.length - 1, Math.floor(EDGE_QUANTILE * positive.length))]);
|
|
105
|
+
const boundaries = [];
|
|
106
|
+
for (let k = 0; k < lines; k++) {
|
|
107
|
+
const row = k * length;
|
|
108
|
+
boundaries.length = 0;
|
|
109
|
+
for (let i = 0; i < length; i++) {
|
|
110
|
+
const here = steps[row + i], left = i > 0 ? steps[row + i - 1] : 0, right = i + 1 < length ? steps[row + i + 1] : 0;
|
|
111
|
+
if (here <= threshold || here <= left || here < right)
|
|
112
|
+
continue;
|
|
113
|
+
const curvature = left - 2 * here + right;
|
|
114
|
+
const shift = curvature ? (0.5 * (left - right)) / curvature : 0;
|
|
115
|
+
boundaries.push(i + Math.max(-0.5, Math.min(0.5, shift)));
|
|
116
|
+
}
|
|
117
|
+
for (let a = 0; a < boundaries.length; a++)
|
|
118
|
+
for (let b = a + 1; b <= Math.min(boundaries.length - 1, a + MAX_LAG); b++) {
|
|
119
|
+
const distance = boundaries[b] - boundaries[a];
|
|
120
|
+
if (distance >= MIN_DISTANCE && distance <= MAX_DISTANCE)
|
|
121
|
+
histogram[Math.round(distance / BIN)]++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return histogram;
|
|
125
|
+
}
|
|
126
|
+
/** Mean of cos(2π distance / step) over the distances of a histogram with bins `bin` wide. */
|
|
127
|
+
function combScore(histogram, bin, step, weighted = false) {
|
|
128
|
+
let sum = 0, total = 0;
|
|
129
|
+
for (let i = 0; i < histogram.length; i++) {
|
|
130
|
+
if (!histogram[i])
|
|
131
|
+
continue;
|
|
132
|
+
const distance = i * bin;
|
|
133
|
+
const weight = histogram[i] * (weighted ? distance / step : 1);
|
|
134
|
+
sum += weight * Math.cos((2 * Math.PI * distance) / step);
|
|
135
|
+
total += weight;
|
|
136
|
+
}
|
|
137
|
+
return total ? sum / total : 0;
|
|
138
|
+
}
|
|
139
|
+
/** The step refined by a fine search, then by least squares over the multiples it explains. */
|
|
140
|
+
function refine(histogram, step) {
|
|
141
|
+
const fine = new Float64Array(Math.ceil(MAX_DISTANCE / REFINE_BIN) + 1);
|
|
142
|
+
for (let i = 0; i < histogram.length; i++)
|
|
143
|
+
if (histogram[i])
|
|
144
|
+
fine[Math.round((i * BIN) / REFINE_BIN)] += histogram[i];
|
|
145
|
+
let best = step, bestScore = -Infinity;
|
|
146
|
+
for (let s = step * (1 - REFINE_SPAN); s <= step * (1 + REFINE_SPAN); s *= REFINE_RATIO) {
|
|
147
|
+
const score = combScore(fine, REFINE_BIN, s, true);
|
|
148
|
+
if (score > bestScore) {
|
|
149
|
+
bestScore = score;
|
|
150
|
+
best = s;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (let pass = 0; pass < REFINE_PASSES; pass++) {
|
|
154
|
+
let numerator = 0, denominator = 0;
|
|
155
|
+
for (let i = 0; i < fine.length; i++) {
|
|
156
|
+
if (!fine[i])
|
|
157
|
+
continue;
|
|
158
|
+
const distance = i * REFINE_BIN;
|
|
159
|
+
const order = Math.round(distance / best);
|
|
160
|
+
if (order < 1)
|
|
161
|
+
continue;
|
|
162
|
+
const weight = fine[i] *
|
|
163
|
+
order *
|
|
164
|
+
Math.max(0, 1 - Math.abs(distance - order * best) / (REFINE_TOLERANCE * best));
|
|
165
|
+
numerator += weight * order * distance;
|
|
166
|
+
denominator += weight * order * order;
|
|
167
|
+
}
|
|
168
|
+
if (denominator <= 0)
|
|
169
|
+
break;
|
|
170
|
+
best = numerator / denominator;
|
|
171
|
+
}
|
|
172
|
+
return best;
|
|
173
|
+
}
|
|
174
|
+
/** The standing step of an axis, or null when its boundaries are too few or keep no step. */
|
|
175
|
+
function axisStep(image, horizontal) {
|
|
176
|
+
const length = horizontal ? image.width : image.height;
|
|
177
|
+
const longest = Math.min(MAX_STEP, length / MIN_PERIODS);
|
|
178
|
+
if (longest <= MIN_STEP)
|
|
179
|
+
return null;
|
|
180
|
+
const histogram = boundaryDistances(image, horizontal);
|
|
181
|
+
let total = 0;
|
|
182
|
+
for (const count of histogram)
|
|
183
|
+
total += count;
|
|
184
|
+
if (total < MIN_DISTANCES)
|
|
185
|
+
return null;
|
|
186
|
+
const steps = [];
|
|
187
|
+
for (let step = MIN_STEP; step <= longest; step *= STEP_RATIO)
|
|
188
|
+
steps.push(step);
|
|
189
|
+
const scores = steps.map((step) => combScore(histogram, BIN, step));
|
|
190
|
+
const peaks = [];
|
|
191
|
+
for (let i = 1; i + 1 < steps.length; i++)
|
|
192
|
+
if (scores[i] > scores[i - 1] && scores[i] >= scores[i + 1] && scores[i] > 0)
|
|
193
|
+
peaks.push(i);
|
|
194
|
+
if (!peaks.length)
|
|
195
|
+
return null;
|
|
196
|
+
peaks.sort((a, b) => scores[b] - scores[a]);
|
|
197
|
+
const top = scores[peaks[0]];
|
|
198
|
+
const support = (step) => {
|
|
199
|
+
const window = Math.max(MIN_FUNDAMENTAL_WINDOW, FUNDAMENTAL_WINDOW * step);
|
|
200
|
+
let near = 0;
|
|
201
|
+
for (let i = 0; i < histogram.length; i++)
|
|
202
|
+
if (Math.abs(i * BIN - step) < window)
|
|
203
|
+
near += histogram[i];
|
|
204
|
+
return near / total;
|
|
205
|
+
};
|
|
206
|
+
const tied = peaks
|
|
207
|
+
.filter((i) => scores[i] >= TIE_SHARE * top)
|
|
208
|
+
.sort((a, b) => steps[b] - steps[a])
|
|
209
|
+
.find((i) => support(steps[i]) >= FUNDAMENTAL_SHARE);
|
|
210
|
+
return refine(histogram, steps[tied ?? peaks[0]]);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The lattice covering the image at the cell counts of the axes' standing steps; an axis without
|
|
214
|
+
* one takes the other's.
|
|
215
|
+
*/
|
|
216
|
+
export function distanceCountGrids(image) {
|
|
217
|
+
const across = axisStep(image, true), down = axisStep(image, false);
|
|
218
|
+
const x = across ?? down, y = down ?? across;
|
|
219
|
+
if (x === null || y === null)
|
|
220
|
+
return [];
|
|
221
|
+
const columns = Math.round(image.width / x), rows = Math.round(image.height / y);
|
|
222
|
+
const fits = (count, length) => count >= MIN_GRID_CELLS && count <= Math.min(length, MAX_RECOVERED_GRID_SIZE);
|
|
223
|
+
if (!fits(columns, image.width) || !fits(rows, image.height))
|
|
224
|
+
return [];
|
|
225
|
+
return [
|
|
226
|
+
{
|
|
227
|
+
...blockPartition(image, columns, rows),
|
|
228
|
+
confidence: 0,
|
|
229
|
+
reliable: true,
|
|
230
|
+
algorithm: PixelationAlgorithm.UniformGrid,
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ChanceEvidenceFragments, type ChanceEvidenceStop, type StripImage, type StripShear } from "./grid-strip-shift.ts";
|
|
2
|
+
/** The grid read through phase fields, or null when no line of any band moves. */
|
|
3
|
+
export declare function phaseField(strips: StripImage, cutsX: readonly number[], cutsY: readonly number[]): (StripShear & {
|
|
4
|
+
bandsX: number;
|
|
5
|
+
bandsY: number;
|
|
6
|
+
}) | null;
|
|
7
|
+
/**
|
|
8
|
+
* Phase evidence the field search finds without the grid, per sample: on each axis, the most it
|
|
9
|
+
* finds at the off-pitch factors with the same number of bands, averaged over the two axes.
|
|
10
|
+
*/
|
|
11
|
+
export declare function fieldChanceEvidence(strips: StripImage, cutsX: readonly number[], cutsY: readonly number[], bandsX: number, bandsY: number, stopIfCannotWin?: ChanceEvidenceStop, fragments?: ChanceEvidenceFragments): number;
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { colourStepCost, RESIDUAL_FLOOR, } from "./grid-description-length.js";
|
|
2
|
+
import { shiftedGridImage } from "./grid-sampling.js";
|
|
3
|
+
import { intervalEnergy, linePrefix, OFF_PITCH_FACTORS, offPitch, } from "./grid-strip-shift.js";
|
|
4
|
+
import { PixelGridSamplingAxis } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Phase fields of a grid: the lattice keeps its pitch, but its phase may drift along the image and
|
|
7
|
+
* differ from one part of it to another, as a warped upscale or a sheet of separately placed
|
|
8
|
+
* sprites displaces it. Split the cells across an axis into bands; along every band, each
|
|
9
|
+
* described line moves the cuts by a whole pixel shift, and the shift holds from line to line
|
|
10
|
+
* unless a change pays for itself. Each band names its first shift, and every change names its
|
|
11
|
+
* line and either a step of one pixel or a new shift, so a field that follows noise pays for it
|
|
12
|
+
* while a smooth warp pays little. Moving lines apart drops the input pixels between them, and
|
|
13
|
+
* moving a line past the image's end drops the pixels it leaves; the straightened image does not
|
|
14
|
+
* hold them, so the field pays for coding each on its own. The shifts are chosen by dynamic
|
|
15
|
+
* programming on the variance left inside the cells, coded at the residual variance, and on the
|
|
16
|
+
* pixels they drop; the split into bands is the one whose field codes the axis shortest. The
|
|
17
|
+
* column cuts are settled first, the row cuts then on the straightened columns.
|
|
18
|
+
*
|
|
19
|
+
* The twin confines every line's shift to the anti-phase of its own (a quarter to three quarters
|
|
20
|
+
* of a cell away), and `fieldChanceEvidence` measures the phase evidence the same search finds at
|
|
21
|
+
* pitches the grid does not have.
|
|
22
|
+
*/
|
|
23
|
+
const CHANNELS = 3;
|
|
24
|
+
const HALF = 0.5;
|
|
25
|
+
const ANTI_PHASE_FROM = 0.25;
|
|
26
|
+
const ANTI_PHASE_TO = 0.75;
|
|
27
|
+
/** Bands of cells an axis may be split into; each band follows its own shifts. */
|
|
28
|
+
const BAND_COUNTS = [1, 2, 3, 4];
|
|
29
|
+
/** Cells a band needs, so that its shifts are read on more than a few features. */
|
|
30
|
+
const MIN_BAND_CELLS = 8;
|
|
31
|
+
/** Dynamic programming passes; each codes the field at the residual variance of the last. */
|
|
32
|
+
const REFITS = 2;
|
|
33
|
+
/**
|
|
34
|
+
* Cells a shift may reach either way. Beyond half a cell the cuts repeat, but a warp that moves the
|
|
35
|
+
* content further must be followed there, or the straightened lines would slip a cell apart.
|
|
36
|
+
*/
|
|
37
|
+
const REACH = 1;
|
|
38
|
+
const TIE = 1e-9;
|
|
39
|
+
const dropTables = new WeakMap();
|
|
40
|
+
/**
|
|
41
|
+
* Per described line, the cumulative nats of coding each pixel's colour against its neighbour
|
|
42
|
+
* (see colourStepCost). Where a dropped pixel lies follows from the shifts, and the straightened
|
|
43
|
+
* image holds a repeated pixel in its place, which already pays the residual floor.
|
|
44
|
+
*/
|
|
45
|
+
function dropTable(prefix) {
|
|
46
|
+
const cached = dropTables.get(prefix);
|
|
47
|
+
if (cached)
|
|
48
|
+
return cached;
|
|
49
|
+
const { length, lines, sums } = prefix;
|
|
50
|
+
const drops = new Float64Array(lines.length * (length + 1));
|
|
51
|
+
for (let k = 0; k < lines.length; k++) {
|
|
52
|
+
// linePrefix accumulates nonnegative squared premultiplied RGB; a zero sum codes a zero line.
|
|
53
|
+
if (sums[(k + 1) * (length + 1) * (CHANNELS + 1) - 1] === 0)
|
|
54
|
+
continue;
|
|
55
|
+
const base = k * (length + 1) * (CHANNELS + 1), row = k * (length + 1);
|
|
56
|
+
let previousRed = 0, previousGreen = 0, previousBlue = 0;
|
|
57
|
+
for (let p = 0; p < length; p++) {
|
|
58
|
+
const at = base + (p + 1) * (CHANNELS + 1), before = at - (CHANNELS + 1);
|
|
59
|
+
const red = sums[at] - sums[before];
|
|
60
|
+
const green = sums[at + 1] - sums[before + 1];
|
|
61
|
+
const blue = sums[at + 2] - sums[before + 2];
|
|
62
|
+
let neighbourRed = previousRed, neighbourGreen = previousGreen, neighbourBlue = previousBlue;
|
|
63
|
+
if (p === 0) {
|
|
64
|
+
const neighbour = Math.min(length - 1, 1);
|
|
65
|
+
const neighbourAt = base + (neighbour + 1) * (CHANNELS + 1), neighbourBefore = neighbourAt - (CHANNELS + 1);
|
|
66
|
+
neighbourRed = sums[neighbourAt] - sums[neighbourBefore];
|
|
67
|
+
neighbourGreen = sums[neighbourAt + 1] - sums[neighbourBefore + 1];
|
|
68
|
+
neighbourBlue = sums[neighbourAt + 2] - sums[neighbourBefore + 2];
|
|
69
|
+
}
|
|
70
|
+
let step = 0;
|
|
71
|
+
step += (red - neighbourRed) ** 2;
|
|
72
|
+
step += (green - neighbourGreen) ** 2;
|
|
73
|
+
step += (blue - neighbourBlue) ** 2;
|
|
74
|
+
drops[row + p + 1] = drops[row + p] + colourStepCost(step);
|
|
75
|
+
previousRed = red;
|
|
76
|
+
previousGreen = green;
|
|
77
|
+
previousBlue = blue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
dropTables.set(prefix, drops);
|
|
81
|
+
return drops;
|
|
82
|
+
}
|
|
83
|
+
function energyTable(prefix, cuts, first, last) {
|
|
84
|
+
const { length, lines, sums: line } = prefix;
|
|
85
|
+
const cells = cuts.length - 1, span = last - first + 1;
|
|
86
|
+
// Every bandEnergy caller uses bandsOf for a supported split, including the anti-field.
|
|
87
|
+
// Keep all cell additions, storing only the prefixes those splits can query.
|
|
88
|
+
const needed = new Set([0, cells]);
|
|
89
|
+
for (const count of BAND_COUNTS) {
|
|
90
|
+
if (count > 1 && cells < count * MIN_BAND_CELLS)
|
|
91
|
+
break;
|
|
92
|
+
for (const boundary of bandsOf(cells, count))
|
|
93
|
+
needed.add(boundary);
|
|
94
|
+
}
|
|
95
|
+
const boundaries = [...needed].sort((a, b) => a - b);
|
|
96
|
+
const bandStride = boundaries.length;
|
|
97
|
+
const bandSlots = new Int32Array(cells + 1).fill(-1);
|
|
98
|
+
for (let slot = 0; slot < bandStride; slot++)
|
|
99
|
+
bandSlots[boundaries[slot]] = slot;
|
|
100
|
+
const sums = new Float64Array(lines.length * span * bandStride);
|
|
101
|
+
// Shifted cut bounds are identical on every described line.
|
|
102
|
+
const froms = new Float64Array(span * cells), tos = new Float64Array(span * cells);
|
|
103
|
+
for (let j = 0; j < span; j++) {
|
|
104
|
+
const shift = first + j;
|
|
105
|
+
for (let i = 0; i < cells; i++) {
|
|
106
|
+
const from = i === 0 ? 0 : Math.max(0, Math.min(length, cuts[i] + shift));
|
|
107
|
+
const to = i + 1 === cells ? length : Math.max(0, Math.min(length, cuts[i + 1] + shift));
|
|
108
|
+
froms[j * cells + i] = from;
|
|
109
|
+
tos[j * cells + i] = Math.max(from, to);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Different shifts/cells often request the exact same source interval. Evaluate it once,
|
|
113
|
+
// then retain the original line/shift/cell order of additions into the cumulative table.
|
|
114
|
+
const ids = new Int32Array(froms.length);
|
|
115
|
+
const unique = new Map();
|
|
116
|
+
const uniqueFroms = [], uniqueTos = [];
|
|
117
|
+
for (let i = 0; i < froms.length; i++) {
|
|
118
|
+
const key = `${froms[i]},${tos[i]}`;
|
|
119
|
+
let id = unique.get(key);
|
|
120
|
+
if (id === undefined) {
|
|
121
|
+
id = uniqueFroms.length;
|
|
122
|
+
unique.set(key, id);
|
|
123
|
+
uniqueFroms.push(froms[i]);
|
|
124
|
+
uniqueTos.push(tos[i]);
|
|
125
|
+
}
|
|
126
|
+
ids[i] = id;
|
|
127
|
+
}
|
|
128
|
+
const intervalEnergies = new Float64Array(uniqueFroms.length);
|
|
129
|
+
// Preserve non-integer typed-array lookup behaviour for fractional cell boundaries.
|
|
130
|
+
const zeroLineSafe = uniqueFroms.every((from, i) => Number.isInteger(from) &&
|
|
131
|
+
Number.isInteger(uniqueTos[i]) &&
|
|
132
|
+
from >= 0 &&
|
|
133
|
+
from <= length &&
|
|
134
|
+
uniqueTos[i] >= 0 &&
|
|
135
|
+
uniqueTos[i] <= length);
|
|
136
|
+
for (let k = 0; k < lines.length; k++) {
|
|
137
|
+
// linePrefix accumulates nonnegative squared premultiplied RGB; a zero sum codes a zero line.
|
|
138
|
+
if (zeroLineSafe && line[(k + 1) * (length + 1) * (CHANNELS + 1) - 1] === 0)
|
|
139
|
+
continue;
|
|
140
|
+
const base = k * (length + 1) * (CHANNELS + 1);
|
|
141
|
+
for (let i = 0; i < intervalEnergies.length; i++)
|
|
142
|
+
intervalEnergies[i] = intervalEnergy(line, base, uniqueFroms[i], uniqueTos[i]);
|
|
143
|
+
for (let j = 0; j < span; j++) {
|
|
144
|
+
const row = (k * span + j) * bandStride, bounds = j * cells;
|
|
145
|
+
let total = 0, cell = 0;
|
|
146
|
+
for (let slot = 1; slot < bandStride; slot++) {
|
|
147
|
+
for (; cell < boundaries[slot]; cell++)
|
|
148
|
+
total += intervalEnergies[ids[bounds + cell]];
|
|
149
|
+
sums[row + slot] = total;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
prefix,
|
|
155
|
+
cuts,
|
|
156
|
+
cells,
|
|
157
|
+
first,
|
|
158
|
+
span,
|
|
159
|
+
sums,
|
|
160
|
+
bandSlots,
|
|
161
|
+
bandStride,
|
|
162
|
+
drops: dropTable(prefix),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const bandEnergy = (table, k, j, from, to) => {
|
|
166
|
+
const row = (k * table.span + j) * table.bandStride;
|
|
167
|
+
return table.sums[row + table.bandSlots[to]] - table.sums[row + table.bandSlots[from]];
|
|
168
|
+
};
|
|
169
|
+
/** Nats of coding the pixels [from, to) of line k on their own. */
|
|
170
|
+
function dropped(table, k, from, to) {
|
|
171
|
+
const { length } = table.prefix;
|
|
172
|
+
const low = Math.max(0, Math.min(length, from)), high = Math.max(0, Math.min(length, to));
|
|
173
|
+
if (high <= low)
|
|
174
|
+
return 0;
|
|
175
|
+
const row = k * (length + 1);
|
|
176
|
+
return table.drops[row + high] - table.drops[row + low];
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The shift of every line along the cells [from, to) that codes them shortest, each pixel's
|
|
180
|
+
* variance costing `scale` nats and the pixels a shift drops `drop` nats. `window` confines a
|
|
181
|
+
* line's shifts. Shifts a whole cell apart cut the lines alike, so on equal cuts the path keeps to
|
|
182
|
+
* the smaller shift.
|
|
183
|
+
*/
|
|
184
|
+
function bandPath(table, from, to, scale, change, window, drop) {
|
|
185
|
+
const lines = table.prefix.lines.length, { span, first } = table;
|
|
186
|
+
const back = new Int32Array(lines * span);
|
|
187
|
+
let previous = new Float64Array(span), current = new Float64Array(span), best = Infinity, bestAt = -1;
|
|
188
|
+
const tieBias = Float64Array.from({ length: span }, (_, j) => Math.abs(first + j) * TIE);
|
|
189
|
+
for (let k = 0; k < lines; k++) {
|
|
190
|
+
const [low, high] = window(k);
|
|
191
|
+
let nextBest = Infinity, nextBestAt = -1;
|
|
192
|
+
const jumpCost = best + change.jump;
|
|
193
|
+
for (let j = 0; j < span; j++) {
|
|
194
|
+
const shift = first + j;
|
|
195
|
+
if (shift < low || shift > high) {
|
|
196
|
+
current[j] = Infinity;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const energy = bandEnergy(table, k, j, from, to) * scale + drop(k, shift) + tieBias[j];
|
|
200
|
+
if (k === 0) {
|
|
201
|
+
current[j] = energy;
|
|
202
|
+
if (current[j] < nextBest) {
|
|
203
|
+
nextBest = current[j];
|
|
204
|
+
nextBestAt = j;
|
|
205
|
+
}
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
let cost = previous[j], source = j;
|
|
209
|
+
if (j > 0 && previous[j - 1] + change.step < cost) {
|
|
210
|
+
cost = previous[j - 1] + change.step;
|
|
211
|
+
source = j - 1;
|
|
212
|
+
}
|
|
213
|
+
if (j + 1 < span && previous[j + 1] + change.step < cost) {
|
|
214
|
+
cost = previous[j + 1] + change.step;
|
|
215
|
+
source = j + 1;
|
|
216
|
+
}
|
|
217
|
+
if (jumpCost < cost) {
|
|
218
|
+
cost = jumpCost;
|
|
219
|
+
source = bestAt;
|
|
220
|
+
}
|
|
221
|
+
current[j] = cost + energy;
|
|
222
|
+
if (current[j] < nextBest) {
|
|
223
|
+
nextBest = current[j];
|
|
224
|
+
nextBestAt = j;
|
|
225
|
+
}
|
|
226
|
+
back[k * span + j] = source;
|
|
227
|
+
}
|
|
228
|
+
const scratch = previous;
|
|
229
|
+
previous = current;
|
|
230
|
+
current = scratch;
|
|
231
|
+
best = nextBest;
|
|
232
|
+
bestAt = nextBestAt;
|
|
233
|
+
}
|
|
234
|
+
let at = 0;
|
|
235
|
+
for (let j = 1; j < span; j++)
|
|
236
|
+
if (previous[j] < previous[at])
|
|
237
|
+
at = j;
|
|
238
|
+
const shifts = new Int32Array(lines);
|
|
239
|
+
let energy = 0, cost = 0;
|
|
240
|
+
for (let k = lines - 1; k >= 0; k--) {
|
|
241
|
+
shifts[k] = first + at;
|
|
242
|
+
energy += bandEnergy(table, k, at, from, to);
|
|
243
|
+
cost += drop(k, shifts[k]);
|
|
244
|
+
if (k > 0) {
|
|
245
|
+
const before = back[k * span + at];
|
|
246
|
+
if (before !== at)
|
|
247
|
+
cost += Math.abs(before - at) === 1 ? change.step : change.jump;
|
|
248
|
+
at = before;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return { shifts, energy, cost };
|
|
252
|
+
}
|
|
253
|
+
const pitchOf = (cuts) => (cuts[cuts.length - 1] - cuts[0]) / (cuts.length - 1);
|
|
254
|
+
function shiftRange(cuts) {
|
|
255
|
+
const pitch = pitchOf(cuts);
|
|
256
|
+
const reach = Math.round(pitch * REACH);
|
|
257
|
+
return { low: -reach, high: reach, pitch };
|
|
258
|
+
}
|
|
259
|
+
function bandsOf(cells, count) {
|
|
260
|
+
return Array.from({ length: count + 1 }, (_, b) => Math.round((b * cells) / count));
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The field of one band split. Without `anchor` every shift lies within REACH; with it, every
|
|
264
|
+
* line's shift lies in the anti-phase window of the anchor's. A line's pixels between where the
|
|
265
|
+
* band before it ends and where its own band starts reading, or before the first band and past
|
|
266
|
+
* the last, drop out of the straightened image; the bands are settled in order, each paying for
|
|
267
|
+
* the pixels it drops after the one before.
|
|
268
|
+
*/
|
|
269
|
+
function splitField(table, bands, variance, anchor) {
|
|
270
|
+
const { low, high, pitch } = table.range;
|
|
271
|
+
const { length } = table.prefix;
|
|
272
|
+
const options = high - low + 1;
|
|
273
|
+
const line = Math.log(Math.max(2, table.prefix.lines.length)) + Math.LN2;
|
|
274
|
+
const change = { step: line + Math.LN2, jump: line + Math.log(options - 1) };
|
|
275
|
+
const shifts = [];
|
|
276
|
+
const stationaryWindow = [low, high];
|
|
277
|
+
let energy = 0, cost = 0;
|
|
278
|
+
for (let b = 0; b + 1 < bands.length; b++) {
|
|
279
|
+
const window = anchor
|
|
280
|
+
? (k) => [
|
|
281
|
+
Math.ceil(anchor.shifts[b][k] + ANTI_PHASE_FROM * pitch),
|
|
282
|
+
Math.floor(anchor.shifts[b][k] + ANTI_PHASE_TO * pitch),
|
|
283
|
+
]
|
|
284
|
+
: () => stationaryWindow;
|
|
285
|
+
const start = b === 0 ? 0 : table.cuts[bands[b]], before = shifts[b - 1], last = b + 2 === bands.length;
|
|
286
|
+
const drop = (k, shift) => dropped(table, k, before ? start + before[k] : 0, start + shift) +
|
|
287
|
+
(last && shift < 0 ? dropped(table, k, length + shift, length) : 0);
|
|
288
|
+
const path = bandPath(table, bands[b], bands[b + 1], 1 / (2 * variance), change, window, drop);
|
|
289
|
+
shifts.push(path.shifts);
|
|
290
|
+
energy += path.energy;
|
|
291
|
+
cost += Math.log(options) + path.cost;
|
|
292
|
+
}
|
|
293
|
+
return { bands: [...bands], shifts, energy, cost };
|
|
294
|
+
}
|
|
295
|
+
function rangedTable(prefix, cuts) {
|
|
296
|
+
const range = shiftRange(cuts);
|
|
297
|
+
const last = range.high + Math.floor(ANTI_PHASE_TO * range.pitch);
|
|
298
|
+
return { ...energyTable(prefix, cuts, range.low, last), range };
|
|
299
|
+
}
|
|
300
|
+
const samplesOf = (prefix) => prefix.lines.length * prefix.length * CHANNELS;
|
|
301
|
+
/** Description length, in nats, of an axis's residual and shifts under a field. */
|
|
302
|
+
const fieldLength = (field, samples) => (samples / 2) * Math.log(field.energy / samples + RESIDUAL_FLOOR) + field.cost;
|
|
303
|
+
/** The band split, and its field, that codes the axis shortest. */
|
|
304
|
+
function bestField(table, bandCounts = BAND_COUNTS) {
|
|
305
|
+
const samples = samplesOf(table.prefix);
|
|
306
|
+
let still = 0;
|
|
307
|
+
for (let k = 0; k < table.prefix.lines.length; k++)
|
|
308
|
+
still += bandEnergy(table, k, -table.first, 0, table.cells);
|
|
309
|
+
let best = null;
|
|
310
|
+
for (const count of bandCounts) {
|
|
311
|
+
if (count > 1 && table.cells < count * MIN_BAND_CELLS)
|
|
312
|
+
break;
|
|
313
|
+
const bands = bandsOf(table.cells, count);
|
|
314
|
+
let variance = still / samples + RESIDUAL_FLOOR;
|
|
315
|
+
let field = splitField(table, bands, variance);
|
|
316
|
+
for (let pass = 1; pass < REFITS; pass++) {
|
|
317
|
+
variance = field.energy / samples + RESIDUAL_FLOOR;
|
|
318
|
+
field = splitField(table, bands, variance);
|
|
319
|
+
}
|
|
320
|
+
if (!best || fieldLength(field, samples) < fieldLength(best, samples))
|
|
321
|
+
best = field;
|
|
322
|
+
}
|
|
323
|
+
return best;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The image with each field line moved back by its shift. Reads interpolate by the original
|
|
327
|
+
* nearest described line rule; an exact midpoint chooses the later line.
|
|
328
|
+
*/
|
|
329
|
+
function unfield(image, vertical, cuts, lines, field) {
|
|
330
|
+
const count = vertical ? image.width : image.height;
|
|
331
|
+
const across = [0];
|
|
332
|
+
for (let i = 1; i < lines.length; i++)
|
|
333
|
+
across.push(Math.ceil((lines[i - 1] + lines[i]) / 2));
|
|
334
|
+
across.push(count);
|
|
335
|
+
return shiftedGridImage(image, {
|
|
336
|
+
axis: vertical ? PixelGridSamplingAxis.Vertical : PixelGridSamplingAxis.Horizontal,
|
|
337
|
+
bands: field.bands.map((cell) => cuts[cell]),
|
|
338
|
+
across,
|
|
339
|
+
shifts: field.shifts,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const moved = (field) => field.shifts.some((shifts) => shifts.some(Boolean));
|
|
343
|
+
/** The field of a table confined to the anti-phase of `anchor`'s shifts, for the twin. */
|
|
344
|
+
function antiField(table, anchor) {
|
|
345
|
+
const variance = anchor.energy / samplesOf(table.prefix) + RESIDUAL_FLOOR;
|
|
346
|
+
return splitField(table, anchor.bands, variance, anchor);
|
|
347
|
+
}
|
|
348
|
+
/** Only the most recent x geometry may retain its normal/anti row prefixes. */
|
|
349
|
+
const lastColumns = new WeakMap();
|
|
350
|
+
function columnView(strips, cutsX, table, field) {
|
|
351
|
+
const once = moved(field)
|
|
352
|
+
? unfield(strips.image, false, cutsX, table.prefix.lines, field)
|
|
353
|
+
: strips.image;
|
|
354
|
+
return {
|
|
355
|
+
once,
|
|
356
|
+
rowPrefix: once === strips.image ? strips.prefix(true) : linePrefix(once, true),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function columnsOf(strips, cutsX) {
|
|
360
|
+
const key = cutsX.join(",");
|
|
361
|
+
const cached = lastColumns.get(strips);
|
|
362
|
+
if (cached?.key === key)
|
|
363
|
+
return cached;
|
|
364
|
+
const table = rangedTable(strips.prefix(false), cutsX);
|
|
365
|
+
const field = bestField(table);
|
|
366
|
+
const entry = { key, table, field, ...columnView(strips, cutsX, table, field) };
|
|
367
|
+
lastColumns.set(strips, entry);
|
|
368
|
+
return entry;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Keep this closure separate from columnsOf/phaseField: retained readings must not retain an
|
|
372
|
+
* evicted cache entry's normal/anti line prefixes. Only the last WeakMap entry owns those.
|
|
373
|
+
*/
|
|
374
|
+
function fieldTwin(strips, cutsX, cutsY, columnTable, columns, rows) {
|
|
375
|
+
return () => {
|
|
376
|
+
const cached = lastColumns.get(strips);
|
|
377
|
+
const anti = cached?.table === columnTable
|
|
378
|
+
? (cached.anti ??= columnView(strips, cutsX, columnTable, antiField(columnTable, columns)))
|
|
379
|
+
: columnView(strips, cutsX, columnTable, antiField(columnTable, columns));
|
|
380
|
+
const rowTable = rangedTable(anti.rowPrefix, cutsY);
|
|
381
|
+
const rowField = antiField(rowTable, rows);
|
|
382
|
+
return moved(rowField)
|
|
383
|
+
? unfield(anti.once, true, cutsY, rowTable.prefix.lines, rowField)
|
|
384
|
+
: anti.once;
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/** The grid read through phase fields, or null when no line of any band moves. */
|
|
388
|
+
export function phaseField(strips, cutsX, cutsY) {
|
|
389
|
+
const { table: columnTable, field: columns, once, rowPrefix } = columnsOf(strips, cutsX);
|
|
390
|
+
const rowTable = rangedTable(rowPrefix, cutsY);
|
|
391
|
+
const rows = bestField(rowTable);
|
|
392
|
+
const straight = moved(rows) ? unfield(once, true, cutsY, rowTable.prefix.lines, rows) : once;
|
|
393
|
+
if (!moved(columns) && !moved(rows))
|
|
394
|
+
return null;
|
|
395
|
+
return {
|
|
396
|
+
straight,
|
|
397
|
+
cost: columns.cost + rows.cost,
|
|
398
|
+
twin: fieldTwin(strips, cutsX, cutsY, columnTable, columns, rows),
|
|
399
|
+
bandsX: columns.bands.length - 1,
|
|
400
|
+
bandsY: rows.bands.length - 1,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/** Phase evidence, per sample, of one axis's field: its shifts against their anti-phase. */
|
|
404
|
+
function axisEvidence(prefix, cuts, bands) {
|
|
405
|
+
const table = rangedTable(prefix, cuts);
|
|
406
|
+
const samples = samplesOf(prefix);
|
|
407
|
+
const best = bestField(table, [
|
|
408
|
+
Math.min(bands, Math.max(1, Math.floor(table.cells / MIN_BAND_CELLS))),
|
|
409
|
+
]);
|
|
410
|
+
const anti = antiField(table, best);
|
|
411
|
+
return (HALF *
|
|
412
|
+
Math.log((anti.energy / samples + RESIDUAL_FLOOR) / (best.energy / samples + RESIDUAL_FLOOR)));
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Phase evidence the field search finds without the grid, per sample: on each axis, the most it
|
|
416
|
+
* finds at the off-pitch factors with the same number of bands, averaged over the two axes.
|
|
417
|
+
*/
|
|
418
|
+
export function fieldChanceEvidence(strips, cutsX, cutsY, bandsX, bandsY, stopIfCannotWin, fragments) {
|
|
419
|
+
const axes = [false, true];
|
|
420
|
+
let total = 0, remaining = axes.length * OFF_PITCH_FACTORS.length, slot = 0;
|
|
421
|
+
for (const vertical of axes) {
|
|
422
|
+
const cuts = vertical ? cutsY : cutsX;
|
|
423
|
+
let most = 0;
|
|
424
|
+
for (const factor of OFF_PITCH_FACTORS) {
|
|
425
|
+
const off = offPitch(cuts, factor);
|
|
426
|
+
if (off) {
|
|
427
|
+
let value = fragments?.[slot];
|
|
428
|
+
if (value === undefined) {
|
|
429
|
+
value = axisEvidence(strips.prefix(vertical), off, vertical ? bandsY : bandsX);
|
|
430
|
+
if (fragments)
|
|
431
|
+
fragments[slot] = value;
|
|
432
|
+
}
|
|
433
|
+
most = Math.max(most, value);
|
|
434
|
+
}
|
|
435
|
+
slot++;
|
|
436
|
+
remaining--;
|
|
437
|
+
if (stopIfCannotWin && remaining > 0) {
|
|
438
|
+
const lowerBound = (total + most) * HALF;
|
|
439
|
+
if (Number.isFinite(lowerBound) && lowerBound >= 0 && stopIfCannotWin(lowerBound))
|
|
440
|
+
return Infinity;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
total += most;
|
|
444
|
+
}
|
|
445
|
+
return total * HALF;
|
|
446
|
+
}
|