@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.
- package/LICENSE +21 -0
- package/NOTICE.md +12 -0
- package/README.md +238 -2
- package/README.zh-CN.md +209 -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,158 @@
|
|
|
1
|
+
import { pixelGridCuts } from "./grid.js";
|
|
2
|
+
/**
|
|
3
|
+
* Edge coherence of a grid: how much of the image's colour steps fall on its cuts. Each step
|
|
4
|
+
* between two neighbouring pixels counts with the cosine of its phase inside its cell, so a step on
|
|
5
|
+
* a cut counts fully, a step at a cell middle counts against the grid, and steps spread evenly over
|
|
6
|
+
* a cell of two or more whole pixels add up to nothing. The score of a grid is the smaller of its
|
|
7
|
+
* two axes' shares.
|
|
8
|
+
*
|
|
9
|
+
* A soft or noisy upscale describes slightly better through a lattice at a half or third of its
|
|
10
|
+
* pitch: the finer cells follow the blur and noise inside each source pixel. Such a lattice puts
|
|
11
|
+
* most of its cuts inside source pixels, where the image has no edges. Auto does not read a grid
|
|
12
|
+
* the edges reject in either of two ways, which also spares it the cost of reading the grid:
|
|
13
|
+
*
|
|
14
|
+
* - Rigid: the steps summed over the whole image, against the grid's own phase. A grid is rejected
|
|
15
|
+
* when a proposal of at least COARSER_PITCH times its pitch on both axes reaches MIN_COHERENCE and
|
|
16
|
+
* the grid keeps less than COHERENCE_SHARE of it. Only a coarser proposal can reject a grid this
|
|
17
|
+
* way: a warped, drifting or re-blocked grid scores low on one rigid lattice, and its true count
|
|
18
|
+
* must still compete with the lattices near it.
|
|
19
|
+
* - Local: the steps summed over BANDS strips, scored in runs of RUN_CELLS cells at whatever phase
|
|
20
|
+
* the run takes, so a grid whose phase drifts across the image keeps its coherence. When some
|
|
21
|
+
* grid is crisp in the rigid sense (MIN_COHERENCE) and the best local coherence reaches
|
|
22
|
+
* MIN_LOCAL_COHERENCE, a grid that keeps less than COHERENCE_SHARE of it is rejected.
|
|
23
|
+
* Of the grids left, only the MAX_COHERENT_GRIDS of highest local coherence are read: the rest
|
|
24
|
+
* are near-duplicate counts that cost a reading each and do not win.
|
|
25
|
+
*
|
|
26
|
+
* Only grids of at least MIN_REJECTING_CELLS cells per axis reject others: on a small sprite, a
|
|
27
|
+
* lattice of a few large cells lines up with its outline by chance.
|
|
28
|
+
*
|
|
29
|
+
* A bilinear or blurred upscale spreads every edge over its cells; no grid reaches the minimums,
|
|
30
|
+
* and every proposal is read.
|
|
31
|
+
*/
|
|
32
|
+
const STRIDE = 4;
|
|
33
|
+
const CHANNELS = 4;
|
|
34
|
+
const ALPHA_CHANNEL = 3;
|
|
35
|
+
const ALPHA_SCALE = 255;
|
|
36
|
+
/** Share of the coherence that rejects a grid a grid must keep to be read. */
|
|
37
|
+
const COHERENCE_SHARE = 0.6;
|
|
38
|
+
/** Rigid coherence a grid needs to reject others. */
|
|
39
|
+
const MIN_COHERENCE = 0.2;
|
|
40
|
+
/** Local coherence the best grid needs before local coherence rejects others. */
|
|
41
|
+
const MIN_LOCAL_COHERENCE = 0.3;
|
|
42
|
+
/** Cells per axis a grid needs to reject others. */
|
|
43
|
+
const MIN_REJECTING_CELLS = 12;
|
|
44
|
+
/** Pitch ratio, per axis, from which a proposal counts as coarser. */
|
|
45
|
+
const COARSER_PITCH = 1.5;
|
|
46
|
+
/** Grids read at most when local coherence selects them. */
|
|
47
|
+
const MAX_COHERENT_GRIDS = 10;
|
|
48
|
+
/** Strips across each axis the local steps are summed over. */
|
|
49
|
+
const BANDS = 8;
|
|
50
|
+
/** Cells along a strip that share one phase in the local score. */
|
|
51
|
+
const RUN_CELLS = 16;
|
|
52
|
+
/**
|
|
53
|
+
* Pixels per cell, per axis, a grid needs for the check: cells of one pixel put every step on a
|
|
54
|
+
* cut. Finer grids are always read and reject none.
|
|
55
|
+
*/
|
|
56
|
+
const MIN_COHERENCE_PITCH = 2;
|
|
57
|
+
/** Colour step (premultiplied RGB and alpha) into each column and row, per strip. */
|
|
58
|
+
function stepProfiles(image) {
|
|
59
|
+
const { width, height, data } = image;
|
|
60
|
+
const x = Array.from({ length: BANDS }, () => new Float64Array(width)), y = Array.from({ length: BANDS }, () => new Float64Array(height));
|
|
61
|
+
const columnBand = Int32Array.from({ length: width }, (_, column) => Math.floor((column * BANDS) / width));
|
|
62
|
+
// One row of premultiplied colours at a time, with the row above it.
|
|
63
|
+
let above = new Float64Array(width * CHANNELS), current = new Float64Array(width * CHANNELS);
|
|
64
|
+
for (let row = 0; row < height; row++) {
|
|
65
|
+
const across = x[Math.floor((row * BANDS) / height)];
|
|
66
|
+
for (let column = 0, o = row * width * STRIDE; column < width; column++, o += STRIDE) {
|
|
67
|
+
const alpha = data[o + ALPHA_CHANNEL] / ALPHA_SCALE, at = column * CHANNELS;
|
|
68
|
+
current[at] = data[o] * alpha;
|
|
69
|
+
current[at + 1] = data[o + 1] * alpha;
|
|
70
|
+
current[at + 2] = data[o + 2] * alpha;
|
|
71
|
+
current[at + ALPHA_CHANNEL] = data[o + ALPHA_CHANNEL];
|
|
72
|
+
}
|
|
73
|
+
for (let column = 0, at = 0; column < width; column++, at += CHANNELS) {
|
|
74
|
+
let right = 0, down = 0;
|
|
75
|
+
for (let c = 0; c < CHANNELS; c++) {
|
|
76
|
+
if (column)
|
|
77
|
+
right += Math.abs(current[at + c] - current[at - CHANNELS + c]);
|
|
78
|
+
if (row)
|
|
79
|
+
down += Math.abs(current[at + c] - above[at + c]);
|
|
80
|
+
}
|
|
81
|
+
across[column] += right;
|
|
82
|
+
y[columnBand[column]][row] += down;
|
|
83
|
+
}
|
|
84
|
+
[above, current] = [current, above];
|
|
85
|
+
}
|
|
86
|
+
return { x, y };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Share of one axis's steps on the cuts, weighted by the cosine of their phase in the cell. With
|
|
90
|
+
* `run`, the steps of every `run` cells of each strip add up as phasors and only their length
|
|
91
|
+
* counts, so each run is scored at its own phase.
|
|
92
|
+
*/
|
|
93
|
+
function axisCoherence(strips, cuts, run) {
|
|
94
|
+
let aligned = 0, total = 0;
|
|
95
|
+
const cells = cuts.length - 1;
|
|
96
|
+
const length = run ?? cells;
|
|
97
|
+
for (const profile of strips)
|
|
98
|
+
for (let first = 0; first < cells; first += length) {
|
|
99
|
+
let real = 0, imaginary = 0;
|
|
100
|
+
for (let cell = first; cell < Math.min(cells, first + length); cell++) {
|
|
101
|
+
const from = Math.max(0, Math.ceil(cuts[cell])), to = Math.min(profile.length, Math.ceil(cuts[cell + 1]));
|
|
102
|
+
const size = cuts[cell + 1] - cuts[cell];
|
|
103
|
+
for (let p = from; p < to; p++) {
|
|
104
|
+
const angle = (2 * Math.PI * (p - cuts[cell])) / size;
|
|
105
|
+
real += profile[p] * Math.cos(angle);
|
|
106
|
+
if (run)
|
|
107
|
+
imaginary += profile[p] * Math.sin(angle);
|
|
108
|
+
total += profile[p];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
aligned += run ? Math.hypot(real, imaginary) : real;
|
|
112
|
+
}
|
|
113
|
+
return total ? aligned / total : 0;
|
|
114
|
+
}
|
|
115
|
+
const sum = (strips) => {
|
|
116
|
+
const total = new Float64Array(strips[0].length);
|
|
117
|
+
for (const strip of strips)
|
|
118
|
+
for (let p = 0; p < strip.length; p++)
|
|
119
|
+
total[p] += strip[p];
|
|
120
|
+
return [total];
|
|
121
|
+
};
|
|
122
|
+
/** The proposals Auto reads, per proposer: those the image's edges do not reject (see above). */
|
|
123
|
+
export function coherentGrids(image, proposals) {
|
|
124
|
+
if (!proposals.some((grids) => grids.length))
|
|
125
|
+
return proposals.map((grids) => [...grids]);
|
|
126
|
+
const bands = stepProfiles(image);
|
|
127
|
+
const whole = { x: sum(bands.x), y: sum(bands.y) };
|
|
128
|
+
const scored = proposals.map((grids) => grids.map((grid) => {
|
|
129
|
+
const cutsX = grid.cutsX ?? pixelGridCuts(image.width, grid.cellWidth, grid.offsetX), cutsY = grid.cutsY ?? pixelGridCuts(image.height, grid.cellHeight, grid.offsetY);
|
|
130
|
+
const pitchX = image.width / (cutsX.length - 1), pitchY = image.height / (cutsY.length - 1);
|
|
131
|
+
const checked = pitchX >= MIN_COHERENCE_PITCH && pitchY >= MIN_COHERENCE_PITCH;
|
|
132
|
+
return {
|
|
133
|
+
grid,
|
|
134
|
+
pitchX,
|
|
135
|
+
pitchY,
|
|
136
|
+
checked,
|
|
137
|
+
coherence: checked
|
|
138
|
+
? Math.min(axisCoherence(whole.x, cutsX), axisCoherence(whole.y, cutsY))
|
|
139
|
+
: 0,
|
|
140
|
+
local: checked
|
|
141
|
+
? Math.min(axisCoherence(bands.x, cutsX, RUN_CELLS), axisCoherence(bands.y, cutsY, RUN_CELLS))
|
|
142
|
+
: 0,
|
|
143
|
+
};
|
|
144
|
+
}));
|
|
145
|
+
const all = scored.flat();
|
|
146
|
+
const large = all.filter(({ grid }) => Math.min(grid.width, grid.height) >= MIN_REJECTING_CELLS);
|
|
147
|
+
const rejecters = large.filter(({ coherence }) => coherence >= MIN_COHERENCE);
|
|
148
|
+
const bestLocal = rejecters.length ? Math.max(...large.map(({ local }) => local)) : 0;
|
|
149
|
+
const localFloor = bestLocal >= MIN_LOCAL_COHERENCE ? COHERENCE_SHARE * bestLocal : -Infinity;
|
|
150
|
+
const supported = (grid) => grid.local >= localFloor &&
|
|
151
|
+
!rejecters.some((rejecter) => rejecter.pitchX >= COARSER_PITCH * grid.pitchX &&
|
|
152
|
+
rejecter.pitchY >= COARSER_PITCH * grid.pitchY &&
|
|
153
|
+
grid.coherence < COHERENCE_SHARE * rejecter.coherence);
|
|
154
|
+
let read = new Set(all.filter((grid) => grid.checked && supported(grid)));
|
|
155
|
+
if (localFloor > -Infinity && read.size > MAX_COHERENT_GRIDS)
|
|
156
|
+
read = new Set([...read].sort((a, b) => b.local - a.local).slice(0, MAX_COHERENT_GRIDS));
|
|
157
|
+
return scored.map((grids) => grids.filter((grid) => !grid.checked || read.has(grid)).map(({ grid }) => grid));
|
|
158
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PixelBuffer } from "./base.ts";
|
|
2
|
+
import { type PixelGridAnalysis } from "./types.ts";
|
|
3
|
+
/** The cuts of one axis along its step profile at `pitch`; null when the axis has no content. */
|
|
4
|
+
export declare function edgePath(profile: Float64Array, pitch: number): number[] | null;
|
|
5
|
+
/**
|
|
6
|
+
* Edge-path meshes at the pitches of the given proposals, strongest proposals first, one per pitch
|
|
7
|
+
* pair (see above).
|
|
8
|
+
*/
|
|
9
|
+
export declare function edgePathGrids(image: PixelBuffer, proposals: readonly PixelGridAnalysis[]): PixelGridAnalysis[];
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { pixelGridCuts } from "./grid.js";
|
|
2
|
+
import { PixelationAlgorithm } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Edge paths: a mesh whose cuts follow the colour edges at a pitch another proposer found.
|
|
5
|
+
*
|
|
6
|
+
* A generator does not draw its cells at one width. Along an axis the cells of a generated sprite
|
|
7
|
+
* are 0.8 to 1.2 times the pitch, and the width wanders across the image, so every lattice falls
|
|
8
|
+
* out of step with the art somewhere and cuts through its pixels. The edge mesh (grid-detect) only
|
|
9
|
+
* keeps cuts on sharp, consistent edges, which a soft generated image rarely has everywhere.
|
|
10
|
+
*
|
|
11
|
+
* Per axis, the colour steps into each column (or row) are summed over the image, and the cuts are
|
|
12
|
+
* the path through them, from the first strong step to the last, that gathers the most step energy
|
|
13
|
+
* (EDGE_GAIN per cut, relative to the mean step) at cell widths between MIN_WIDTH and MAX_WIDTH of
|
|
14
|
+
* the pitch. A width away from the pitch costs PITCH_WEIGHT, a change of width from one cell to
|
|
15
|
+
* the next SMOOTH_WEIGHT, both on the square of the change as a share of the pitch, so across a
|
|
16
|
+
* stretch of one colour the path keeps the pitch and at an edge it moves onto it. Outside the
|
|
17
|
+
* content the cuts continue at the pitch. Arbitration reads the paths as free meshes: each cut pays
|
|
18
|
+
* for itself, so a path wins only where the art does not keep one pitch. A path already sits on
|
|
19
|
+
* the edges, so it is read as placed, without strip shifts or a phase field.
|
|
20
|
+
*/
|
|
21
|
+
const STRIDE = 4;
|
|
22
|
+
const CHANNELS = 4;
|
|
23
|
+
const ALPHA_CHANNEL = 3;
|
|
24
|
+
const ALPHA_SCALE = 255;
|
|
25
|
+
const MIN_WIDTH = 0.75;
|
|
26
|
+
const MAX_WIDTH = 1.25;
|
|
27
|
+
const PITCH_WEIGHT = 10;
|
|
28
|
+
const SMOOTH_WEIGHT = 8;
|
|
29
|
+
/** Share of the strongest step a step needs to start or end the content. */
|
|
30
|
+
const CONTENT_STEP = 0.05;
|
|
31
|
+
/** A border cell narrower than this share of the pitch joins its neighbour. */
|
|
32
|
+
const SLIVER = 0.5;
|
|
33
|
+
/** Pitches of at least this many pixels are followed; finer ones have no room to move. */
|
|
34
|
+
const MIN_PITCH = 4;
|
|
35
|
+
/** Cells per axis a path needs. */
|
|
36
|
+
const MIN_CELLS = 8;
|
|
37
|
+
/** Pitch pairs within this many pixels on both axes share one path. */
|
|
38
|
+
const PITCH_TOLERANCE = 0.5;
|
|
39
|
+
/** Paths proposed at most: each costs a reading. */
|
|
40
|
+
const MAX_PATHS = 3;
|
|
41
|
+
/** Colour step into every column and row, summed over the image (premultiplied RGB and alpha). */
|
|
42
|
+
function stepProfiles(image) {
|
|
43
|
+
const { width, height, data } = image;
|
|
44
|
+
const x = new Float64Array(width), y = new Float64Array(height);
|
|
45
|
+
let above = new Float64Array(width * CHANNELS), current = new Float64Array(width * CHANNELS);
|
|
46
|
+
for (let row = 0; row < height; row++) {
|
|
47
|
+
for (let column = 0, o = row * width * STRIDE; column < width; column++, o += STRIDE) {
|
|
48
|
+
const alpha = data[o + ALPHA_CHANNEL] / ALPHA_SCALE, at = column * CHANNELS;
|
|
49
|
+
current[at] = data[o] * alpha;
|
|
50
|
+
current[at + 1] = data[o + 1] * alpha;
|
|
51
|
+
current[at + 2] = data[o + 2] * alpha;
|
|
52
|
+
current[at + ALPHA_CHANNEL] = data[o + ALPHA_CHANNEL];
|
|
53
|
+
}
|
|
54
|
+
for (let column = 0, at = 0; column < width; column++, at += CHANNELS) {
|
|
55
|
+
let right = 0, down = 0;
|
|
56
|
+
for (let c = 0; c < CHANNELS; c++) {
|
|
57
|
+
if (column)
|
|
58
|
+
right += Math.abs(current[at + c] - current[at - CHANNELS + c]);
|
|
59
|
+
if (row)
|
|
60
|
+
down += Math.abs(current[at + c] - above[at + c]);
|
|
61
|
+
}
|
|
62
|
+
x[column] += right;
|
|
63
|
+
y[row] += down;
|
|
64
|
+
}
|
|
65
|
+
[above, current] = [current, above];
|
|
66
|
+
}
|
|
67
|
+
return { x, y };
|
|
68
|
+
}
|
|
69
|
+
/** The cuts of one axis along its step profile at `pitch`; null when the axis has no content. */
|
|
70
|
+
export function edgePath(profile, pitch) {
|
|
71
|
+
const length = profile.length;
|
|
72
|
+
let peak = 0;
|
|
73
|
+
for (let p = 1; p < length; p++)
|
|
74
|
+
peak = Math.max(peak, profile[p]);
|
|
75
|
+
if (!peak)
|
|
76
|
+
return null;
|
|
77
|
+
let first = -1, last = -1;
|
|
78
|
+
for (let p = 1; p < length; p++)
|
|
79
|
+
if (profile[p] >= CONTENT_STEP * peak) {
|
|
80
|
+
if (first < 0)
|
|
81
|
+
first = p;
|
|
82
|
+
last = p;
|
|
83
|
+
}
|
|
84
|
+
const narrow = Math.max(2, Math.round(MIN_WIDTH * pitch)), wide = Math.max(narrow, Math.round(MAX_WIDTH * pitch));
|
|
85
|
+
if (first < 0 || last - first < narrow)
|
|
86
|
+
return null;
|
|
87
|
+
let mean = 0;
|
|
88
|
+
for (let p = first; p <= last; p++)
|
|
89
|
+
mean += profile[p];
|
|
90
|
+
mean /= last - first + 1;
|
|
91
|
+
const gain = (p) => (p < length ? profile[p] / mean - 1 : -1);
|
|
92
|
+
const widths = wide - narrow + 1;
|
|
93
|
+
const span = last - first;
|
|
94
|
+
// score[q][w]: best path from `first` to a cut at first + q whose last cell is narrow + w wide.
|
|
95
|
+
const score = new Float64Array((span + wide + 1) * widths).fill(-Infinity);
|
|
96
|
+
const from = new Int32Array((span + wide + 1) * widths).fill(-1);
|
|
97
|
+
const cost = (w) => PITCH_WEIGHT * ((narrow + w - pitch) / pitch) ** 2;
|
|
98
|
+
for (let w = 0; w < widths; w++) {
|
|
99
|
+
const q = narrow + w;
|
|
100
|
+
if (q <= span + wide)
|
|
101
|
+
score[q * widths + w] = gain(first) + gain(first + q) - cost(w);
|
|
102
|
+
}
|
|
103
|
+
for (let q = narrow; q <= span; q++)
|
|
104
|
+
for (let w = 0; w < widths; w++) {
|
|
105
|
+
const here = score[q * widths + w];
|
|
106
|
+
if (here === -Infinity)
|
|
107
|
+
continue;
|
|
108
|
+
for (let next = 0; next < widths; next++) {
|
|
109
|
+
const to = q + narrow + next;
|
|
110
|
+
if (to > span + wide)
|
|
111
|
+
break;
|
|
112
|
+
const change = (next - w) / pitch;
|
|
113
|
+
const value = here + gain(first + to) - cost(next) - SMOOTH_WEIGHT * change * change;
|
|
114
|
+
if (value > score[to * widths + next]) {
|
|
115
|
+
score[to * widths + next] = value;
|
|
116
|
+
from[to * widths + next] = q * widths + w;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let end = -1, best = -Infinity;
|
|
121
|
+
// The path ends on the last content step, or within one narrow cell past it.
|
|
122
|
+
for (let q = span; q <= Math.min(span + narrow, span + wide); q++)
|
|
123
|
+
for (let w = 0; w < widths; w++)
|
|
124
|
+
if (score[q * widths + w] > best) {
|
|
125
|
+
best = score[q * widths + w];
|
|
126
|
+
end = q * widths + w;
|
|
127
|
+
}
|
|
128
|
+
if (end < 0)
|
|
129
|
+
return null;
|
|
130
|
+
const inner = [];
|
|
131
|
+
for (let at = end; at >= 0; at = from[at]) {
|
|
132
|
+
const q = Math.floor(at / widths);
|
|
133
|
+
inner.push(first + q);
|
|
134
|
+
if (from[at] < 0)
|
|
135
|
+
inner.push(first);
|
|
136
|
+
}
|
|
137
|
+
inner.reverse();
|
|
138
|
+
// Continue at the pitch to both ends of the axis.
|
|
139
|
+
const cuts = inner.filter((c) => c > 0 && c < length);
|
|
140
|
+
while (cuts[0] - pitch > 0)
|
|
141
|
+
cuts.unshift(cuts[0] - pitch);
|
|
142
|
+
while (cuts[cuts.length - 1] + pitch < length)
|
|
143
|
+
cuts.push(cuts[cuts.length - 1] + pitch);
|
|
144
|
+
if (cuts[0] < SLIVER * pitch)
|
|
145
|
+
cuts.shift();
|
|
146
|
+
if (length - cuts[cuts.length - 1] < SLIVER * pitch)
|
|
147
|
+
cuts.pop();
|
|
148
|
+
return [0, ...cuts.map((c) => Math.round(c * 100) / 100), length];
|
|
149
|
+
}
|
|
150
|
+
const pitchOf = (cuts) => (cuts[cuts.length - 1] - cuts[0]) / (cuts.length - 1);
|
|
151
|
+
/**
|
|
152
|
+
* Edge-path meshes at the pitches of the given proposals, strongest proposals first, one per pitch
|
|
153
|
+
* pair (see above).
|
|
154
|
+
*/
|
|
155
|
+
export function edgePathGrids(image, proposals) {
|
|
156
|
+
const pitches = [];
|
|
157
|
+
for (const grid of [...proposals].sort((a, b) => b.confidence - a.confidence)) {
|
|
158
|
+
if (!grid.reliable)
|
|
159
|
+
continue;
|
|
160
|
+
const x = pitchOf(grid.cutsX ?? pixelGridCuts(image.width, grid.cellWidth, grid.offsetX)), y = pitchOf(grid.cutsY ?? pixelGridCuts(image.height, grid.cellHeight, grid.offsetY));
|
|
161
|
+
if (Math.min(x, y) < MIN_PITCH)
|
|
162
|
+
continue;
|
|
163
|
+
if (pitches.some((p) => Math.abs(p.x - x) <= PITCH_TOLERANCE && Math.abs(p.y - y) <= PITCH_TOLERANCE))
|
|
164
|
+
continue;
|
|
165
|
+
pitches.push({ x, y, confidence: grid.confidence });
|
|
166
|
+
if (pitches.length === MAX_PATHS)
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
if (!pitches.length)
|
|
170
|
+
return [];
|
|
171
|
+
const profiles = stepProfiles(image);
|
|
172
|
+
const grids = [];
|
|
173
|
+
const seen = new Set();
|
|
174
|
+
for (const { x, y, confidence } of pitches) {
|
|
175
|
+
const cutsX = edgePath(profiles.x, x), cutsY = edgePath(profiles.y, y);
|
|
176
|
+
if (!cutsX || !cutsY)
|
|
177
|
+
continue;
|
|
178
|
+
const columns = cutsX.length - 1, rows = cutsY.length - 1;
|
|
179
|
+
if (Math.min(columns, rows) < MIN_CELLS)
|
|
180
|
+
continue;
|
|
181
|
+
const key = `${cutsX.join(",")}|${cutsY.join(",")}`;
|
|
182
|
+
if (seen.has(key))
|
|
183
|
+
continue;
|
|
184
|
+
seen.add(key);
|
|
185
|
+
grids.push({
|
|
186
|
+
width: columns,
|
|
187
|
+
height: rows,
|
|
188
|
+
cellWidth: image.width / columns,
|
|
189
|
+
cellHeight: image.height / rows,
|
|
190
|
+
offsetX: 0,
|
|
191
|
+
offsetY: 0,
|
|
192
|
+
cutsX,
|
|
193
|
+
cutsY,
|
|
194
|
+
confidence,
|
|
195
|
+
reliable: true,
|
|
196
|
+
algorithm: PixelationAlgorithm.EdgeGrid,
|
|
197
|
+
placed: true,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return grids;
|
|
201
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PixelBuffer } from "./base.ts";
|
|
2
|
+
import { type PixelGridAnalysis } from "./types.ts";
|
|
3
|
+
/** How the native cells were resampled onto the frame. */
|
|
4
|
+
export declare enum FrameModel {
|
|
5
|
+
/** Bilinear-like: the profile kinks at each cell's sample point. */
|
|
6
|
+
Soft = "soft",
|
|
7
|
+
/** Nearest-like: colour steps on each cell boundary. */
|
|
8
|
+
Hard = "hard"
|
|
9
|
+
}
|
|
10
|
+
interface AxisComb {
|
|
11
|
+
first: number;
|
|
12
|
+
significance: Float64Array;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Projection of the profile onto the comb of every count from `first` to `last`, in units of its
|
|
16
|
+
* standard deviation were the profile structureless: positive when the energy gathers on the
|
|
17
|
+
* sample points (soft) or on the cell boundaries (hard).
|
|
18
|
+
*/
|
|
19
|
+
export declare function combSignificance(profile: Float64Array, first: number, last: number, model: FrameModel): AxisComb;
|
|
20
|
+
/**
|
|
21
|
+
* The frame-aligned lattice of each of `models` that passes the significance, reconstruction,
|
|
22
|
+
* alias and subdivision tests. Cells are square up to SQUARE_TOLERANCE.
|
|
23
|
+
*/
|
|
24
|
+
export declare function detectFrameLattices(image: PixelBuffer, models?: readonly FrameModel[]): PixelGridAnalysis[];
|
|
25
|
+
export {};
|