@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,252 @@
|
|
|
1
|
+
/** Knot spacing in cells. */
|
|
2
|
+
const KNOT_CELLS = 2;
|
|
3
|
+
/** Smoothness weight per knot, relative to the mean edge weight per knot. */
|
|
4
|
+
const SMOOTHNESS = 4;
|
|
5
|
+
const RIDGE = 1e-9;
|
|
6
|
+
/** Gaps of at least this many cells (one hidden cut or more) may gain or lose a cell. */
|
|
7
|
+
const MIN_RELABEL_CELLS = 2;
|
|
8
|
+
const RELABEL_PASSES = 3;
|
|
9
|
+
/** Relative drop of the penalised error that a changed gap must earn. */
|
|
10
|
+
const RELABEL_GAIN = 0.03;
|
|
11
|
+
/** Range of local pitches the labelling considers, around the proposed pitch. */
|
|
12
|
+
const PITCH_LOW = 0.7;
|
|
13
|
+
const PITCH_HIGH = 1.6;
|
|
14
|
+
/** Relative step between pitch states. */
|
|
15
|
+
const PITCH_RESOLUTION = 0.006;
|
|
16
|
+
/** Standard deviation of one gap's length in pixels between soft (sub-pixel) edges. */
|
|
17
|
+
const GAP_NOISE = 0.45;
|
|
18
|
+
/**
|
|
19
|
+
* Least likelihood of a crisp gap's misfit. Crisp edges are cuts rounded to the raster, so a gap
|
|
20
|
+
* misses its cells by less than a pixel (a triangular density on (-1, 1)); the floor bounds the
|
|
21
|
+
* cost of a stray edge.
|
|
22
|
+
*/
|
|
23
|
+
const ROUNDING_FLOOR = 1e-3;
|
|
24
|
+
/**
|
|
25
|
+
* Standard deviation of the log pitch's wander across the whole axis. Distortions bend the grid
|
|
26
|
+
* over the image, not per cell, so the allowance per cell shrinks as the cells grow in number.
|
|
27
|
+
*/
|
|
28
|
+
const DRIFT = 0.2;
|
|
29
|
+
/**
|
|
30
|
+
* Bounds of the share of cuts that show no edge (neighbouring cells of one colour). A gap of
|
|
31
|
+
* several cells hides a cut, at -log of that share: detailed art, where most cuts show, keeps the
|
|
32
|
+
* coarser of two labellings that fit equally well. Hidden cuts come in flat runs, so each further
|
|
33
|
+
* cell of the gap costs only CELL_COST and long gaps follow the neighbouring pitch.
|
|
34
|
+
*/
|
|
35
|
+
const MIN_HIDDEN = 0.02;
|
|
36
|
+
const MAX_HIDDEN = 0.9;
|
|
37
|
+
const CELL_COST = 0.02;
|
|
38
|
+
export function warpAt(warp, x) {
|
|
39
|
+
const { spacing, values } = warp;
|
|
40
|
+
const last = values.length - 1;
|
|
41
|
+
const t = x / spacing;
|
|
42
|
+
const m = Math.max(0, Math.min(last - 1, Math.floor(t)));
|
|
43
|
+
const a = t - m;
|
|
44
|
+
return values[m] * (1 - a) + values[m + 1] * a;
|
|
45
|
+
}
|
|
46
|
+
/** Local pitch (pixels per cell) at x. */
|
|
47
|
+
export function warpPitch(warp, x) {
|
|
48
|
+
const { spacing, values } = warp;
|
|
49
|
+
const m = Math.max(0, Math.min(values.length - 2, Math.floor(x / spacing)));
|
|
50
|
+
const slope = (values[m + 1] - values[m]) / spacing;
|
|
51
|
+
return slope > 0 ? 1 / slope : Infinity;
|
|
52
|
+
}
|
|
53
|
+
/** Position in [from, to] where u reaches `index` (u is increasing there); NaN when it does not. */
|
|
54
|
+
export function warpInverse(warp, index, from, to) {
|
|
55
|
+
let lo = from, hi = to;
|
|
56
|
+
if (warpAt(warp, lo) > index || warpAt(warp, hi) < index)
|
|
57
|
+
return NaN;
|
|
58
|
+
for (let i = 0; i < 40 && hi - lo > 1e-4; i++) {
|
|
59
|
+
const mid = (lo + hi) / 2;
|
|
60
|
+
if (warpAt(warp, mid) < index)
|
|
61
|
+
lo = mid;
|
|
62
|
+
else
|
|
63
|
+
hi = mid;
|
|
64
|
+
}
|
|
65
|
+
return (lo + hi) / 2;
|
|
66
|
+
}
|
|
67
|
+
/** Solves the symmetric positive definite pentadiagonal system (band Cholesky). */
|
|
68
|
+
export function solveBanded(diag, off1, off2, rhs) {
|
|
69
|
+
const n = diag.length;
|
|
70
|
+
// l0 = L[i][i], l1 = L[i][i-1], l2 = L[i][i-2]
|
|
71
|
+
const l0 = new Float64Array(n), l1 = new Float64Array(n), l2 = new Float64Array(n);
|
|
72
|
+
for (let i = 0; i < n; i++) {
|
|
73
|
+
const a2 = i >= 2 ? off2[i - 2] / l0[i - 2] : 0;
|
|
74
|
+
const a1 = i >= 1 ? (off1[i - 1] - (i >= 2 ? a2 * l1[i - 1] : 0)) / l0[i - 1] : 0;
|
|
75
|
+
l0[i] = Math.sqrt(Math.max(diag[i] - a1 * a1 - a2 * a2, RIDGE));
|
|
76
|
+
l1[i] = a1;
|
|
77
|
+
l2[i] = a2;
|
|
78
|
+
}
|
|
79
|
+
const y = new Float64Array(n);
|
|
80
|
+
for (let i = 0; i < n; i++)
|
|
81
|
+
y[i] = (rhs[i] - (i >= 1 ? l1[i] * y[i - 1] : 0) - (i >= 2 ? l2[i] * y[i - 2] : 0)) / l0[i];
|
|
82
|
+
const x = new Float64Array(n);
|
|
83
|
+
for (let i = n - 1; i >= 0; i--)
|
|
84
|
+
x[i] =
|
|
85
|
+
(y[i] - (i + 1 < n ? l1[i + 1] * x[i + 1] : 0) - (i + 2 < n ? l2[i + 2] * x[i + 2] : 0)) /
|
|
86
|
+
l0[i];
|
|
87
|
+
return x;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Least-squares knot values for edges at integer `targets`, under the smoothness penalty, and the
|
|
91
|
+
* penalised squared error they leave.
|
|
92
|
+
*/
|
|
93
|
+
function solve(length, spacing, edges, targets) {
|
|
94
|
+
const n = Math.max(2, Math.ceil(length / spacing) + 1);
|
|
95
|
+
const diag = new Float64Array(n), off1 = new Float64Array(n), off2 = new Float64Array(n), rhs = new Float64Array(n);
|
|
96
|
+
let total = 0;
|
|
97
|
+
for (let j = 0; j < edges.length; j++) {
|
|
98
|
+
const { position, weight: w } = edges[j];
|
|
99
|
+
const t = position / spacing;
|
|
100
|
+
const m = Math.max(0, Math.min(n - 2, Math.floor(t)));
|
|
101
|
+
const a = t - m;
|
|
102
|
+
diag[m] += w * (1 - a) * (1 - a);
|
|
103
|
+
diag[m + 1] += w * a * a;
|
|
104
|
+
off1[m] += w * a * (1 - a);
|
|
105
|
+
rhs[m] += w * (1 - a) * targets[j];
|
|
106
|
+
rhs[m + 1] += w * a * targets[j];
|
|
107
|
+
total += w;
|
|
108
|
+
}
|
|
109
|
+
const lambda = (SMOOTHNESS * total) / n;
|
|
110
|
+
const c = [1, -2, 1];
|
|
111
|
+
for (let m = 1; m + 1 < n; m++)
|
|
112
|
+
for (let p = 0; p < 3; p++)
|
|
113
|
+
for (let q = p; q < 3; q++) {
|
|
114
|
+
const v = lambda * c[p] * c[q];
|
|
115
|
+
const row = m - 1 + p;
|
|
116
|
+
if (q === p)
|
|
117
|
+
diag[row] += v;
|
|
118
|
+
else if (q - p === 1)
|
|
119
|
+
off1[row] += v;
|
|
120
|
+
else
|
|
121
|
+
off2[row] += v;
|
|
122
|
+
}
|
|
123
|
+
for (let m = 0; m < n; m++)
|
|
124
|
+
diag[m] += RIDGE * (lambda + 1);
|
|
125
|
+
const warp = { spacing, values: solveBanded(diag, off1, off2, rhs) };
|
|
126
|
+
let cost = 0;
|
|
127
|
+
for (let j = 0; j < edges.length; j++) {
|
|
128
|
+
const residual = warpAt(warp, edges[j].position) - targets[j];
|
|
129
|
+
cost += edges[j].weight * residual * residual;
|
|
130
|
+
}
|
|
131
|
+
const v = warp.values;
|
|
132
|
+
for (let m = 1; m + 1 < n; m++) {
|
|
133
|
+
const bend = v[m - 1] - 2 * v[m] + v[m + 1];
|
|
134
|
+
cost += lambda * bend * bend;
|
|
135
|
+
}
|
|
136
|
+
return { warp, cost };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The labelling's random walk lets the pitch wander over a long run without edges, where one cell
|
|
140
|
+
* more or less fits equally well. The warp's smoothness then decides: each gap of several cells
|
|
141
|
+
* tries one cell more and one less (shifting every later edge), kept when the penalised error drops.
|
|
142
|
+
*/
|
|
143
|
+
function relabel(length, spacing, edges, labels) {
|
|
144
|
+
let best = solve(length, spacing, edges, labels);
|
|
145
|
+
for (let pass = 0; pass < RELABEL_PASSES; pass++) {
|
|
146
|
+
let changed = false;
|
|
147
|
+
for (let j = 0; j + 1 < labels.length; j++) {
|
|
148
|
+
const cells = labels[j + 1] - labels[j];
|
|
149
|
+
if (cells < MIN_RELABEL_CELLS)
|
|
150
|
+
continue;
|
|
151
|
+
for (const step of [-1, 1]) {
|
|
152
|
+
if (cells + step < MIN_RELABEL_CELLS)
|
|
153
|
+
continue;
|
|
154
|
+
const trial = labels.slice();
|
|
155
|
+
for (let k = j + 1; k < trial.length; k++)
|
|
156
|
+
trial[k] += step;
|
|
157
|
+
const fit = solve(length, spacing, edges, trial);
|
|
158
|
+
if (fit.cost < best.cost * (1 - RELABEL_GAIN)) {
|
|
159
|
+
best = fit;
|
|
160
|
+
labels = trial;
|
|
161
|
+
changed = true;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!changed)
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
return best.warp;
|
|
170
|
+
}
|
|
171
|
+
/** Cell index of every edge (the first is 0), by the jointly best labelling of the gaps. */
|
|
172
|
+
function labelEdges(positions, proposed, minPitch, crisp) {
|
|
173
|
+
const ratio = 1 + PITCH_RESOLUTION;
|
|
174
|
+
const pitches = [];
|
|
175
|
+
for (let p = Math.max(minPitch, proposed * PITCH_LOW); p <= proposed * PITCH_HIGH; p *= ratio)
|
|
176
|
+
pitches.push(p);
|
|
177
|
+
const states = pitches.length;
|
|
178
|
+
const logs = pitches.map(Math.log);
|
|
179
|
+
const gaps = positions.length - 1;
|
|
180
|
+
const from = new Int16Array(gaps * states);
|
|
181
|
+
const cellsOf = new Int32Array(gaps * states);
|
|
182
|
+
let cost = new Float64Array(states);
|
|
183
|
+
let next = new Float64Array(states);
|
|
184
|
+
const obsScale = 1 / (2 * GAP_NOISE * GAP_NOISE);
|
|
185
|
+
const span = positions[gaps] - positions[0];
|
|
186
|
+
const hidden = Math.min(MAX_HIDDEN, Math.max(MIN_HIDDEN, 1 - gaps / Math.max(1, span / proposed)));
|
|
187
|
+
const shownCost = -Math.log(1 - hidden), hiddenCost = -Math.log(hidden);
|
|
188
|
+
const driftScale = Math.max(1, span / proposed) / (2 * DRIFT * DRIFT);
|
|
189
|
+
for (let j = 0; j < gaps; j++) {
|
|
190
|
+
const gap = positions[j + 1] - positions[j];
|
|
191
|
+
for (let b = 0; b < states; b++) {
|
|
192
|
+
const cells = Math.max(1, Math.round(gap / pitches[b]));
|
|
193
|
+
const misfit = gap - cells * pitches[b];
|
|
194
|
+
let best = Infinity, arg = 0;
|
|
195
|
+
for (let a = 0; a < states; a++) {
|
|
196
|
+
const d = logs[b] - logs[a];
|
|
197
|
+
const between = j > 0 ? (cellsOf[(j - 1) * states + a] + cells) / 2 : cells;
|
|
198
|
+
const value = cost[a] + (d * d * driftScale) / between;
|
|
199
|
+
if (value < best) {
|
|
200
|
+
best = value;
|
|
201
|
+
arg = a;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const observation = crisp
|
|
205
|
+
? -Math.log(Math.max(ROUNDING_FLOOR, 1 - Math.abs(misfit)))
|
|
206
|
+
: misfit * misfit * obsScale;
|
|
207
|
+
const hiding = cells > 1 ? hiddenCost + CELL_COST * (cells - 2) : 0;
|
|
208
|
+
next[b] = best + observation + shownCost + hiding;
|
|
209
|
+
from[j * states + b] = arg;
|
|
210
|
+
cellsOf[j * states + b] = cells;
|
|
211
|
+
}
|
|
212
|
+
[cost, next] = [next, cost];
|
|
213
|
+
}
|
|
214
|
+
let state = 0;
|
|
215
|
+
for (let b = 1; b < states; b++)
|
|
216
|
+
if (cost[b] < cost[state])
|
|
217
|
+
state = b;
|
|
218
|
+
const labels = new Float64Array(positions.length);
|
|
219
|
+
const steps = new Int32Array(gaps);
|
|
220
|
+
for (let j = gaps - 1; j >= 0; j--) {
|
|
221
|
+
steps[j] = cellsOf[j * states + state];
|
|
222
|
+
state = from[j * states + state];
|
|
223
|
+
}
|
|
224
|
+
for (let j = 0; j < gaps; j++)
|
|
225
|
+
labels[j + 1] = labels[j] + steps[j];
|
|
226
|
+
return labels;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Fits u over [0, length] to `edges` with local pitches near `proposed`, none below `minPitch`
|
|
230
|
+
* (where the edges of narrower cells would merge); null with too few edges. `crisp` edges sit on
|
|
231
|
+
* whole pixels of a nearest-neighbour upscale.
|
|
232
|
+
* u(0) is shifted to the integer nearest zero, so a grid that starts on the image border starts at
|
|
233
|
+
* index 0.
|
|
234
|
+
*/
|
|
235
|
+
export function fitWarp(edges, length, proposed, minPitch, crisp) {
|
|
236
|
+
if (edges.length < 3 || !(proposed > 0))
|
|
237
|
+
return null;
|
|
238
|
+
const weights = edges.map((edge) => edge.weight).sort((a, b) => a - b);
|
|
239
|
+
const median = weights[Math.floor(weights.length / 2)] || 1;
|
|
240
|
+
const normalised = edges.map((edge) => ({
|
|
241
|
+
position: edge.position,
|
|
242
|
+
weight: Math.min(2, Math.max(0.25, Math.sqrt(edge.weight / median))),
|
|
243
|
+
}));
|
|
244
|
+
const labels = labelEdges(normalised.map((edge) => edge.position), proposed, minPitch, crisp);
|
|
245
|
+
const span = normalised[normalised.length - 1].position - normalised[0].position;
|
|
246
|
+
const mean = labels[labels.length - 1] > 0 ? span / labels[labels.length - 1] : proposed;
|
|
247
|
+
let warp = relabel(length, Math.max(1, KNOT_CELLS * mean), normalised, labels);
|
|
248
|
+
const shift = Math.round(warpAt(warp, 0));
|
|
249
|
+
if (shift)
|
|
250
|
+
warp = { spacing: warp.spacing, values: warp.values.map((v) => v - shift) };
|
|
251
|
+
return warp;
|
|
252
|
+
}
|
package/dist/grid.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PixelBuffer } from "./base.ts";
|
|
2
|
+
import type { PixelGridGeometry } from "./types.ts";
|
|
3
|
+
export declare const MAX_RECOVERED_GRID_SIZE = 512;
|
|
4
|
+
export declare const MAX_GRID_CELL_SIZE = 128;
|
|
5
|
+
export declare function pixelGridCuts(length: number, step: number, offset?: number): number[];
|
|
6
|
+
export declare function manualPixelGrid(image: Pick<PixelBuffer, "width" | "height">, cellWidth: number, cellHeight: number): PixelGridGeometry;
|
|
7
|
+
/** Even partition of the image into `width` x `height` blocks (clamped to one pixel per block). */
|
|
8
|
+
export declare function blockPartition(image: Pick<PixelBuffer, "width" | "height">, width: number, height: number): PixelGridGeometry;
|
package/dist/grid.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const MAX_RECOVERED_GRID_SIZE = 512;
|
|
2
|
+
export const MAX_GRID_CELL_SIZE = 128;
|
|
3
|
+
const PHASE_SNAP = 0.2;
|
|
4
|
+
const PIXEL_CENTER_OFFSET = 0.5;
|
|
5
|
+
export function pixelGridCuts(length, step, offset = 0) {
|
|
6
|
+
if (!Number.isFinite(step) || step < 1 || !Number.isFinite(offset))
|
|
7
|
+
throw new RangeError("Grid cells must be at least one pixel wide.");
|
|
8
|
+
const phase = ((offset % step) + step) % step;
|
|
9
|
+
const cuts = [0];
|
|
10
|
+
let position = phase > PHASE_SNAP ? phase : phase + step;
|
|
11
|
+
for (; position < length - PHASE_SNAP; position += step) {
|
|
12
|
+
// Quantize one regular lattice to raster edges; never chase individual content edges.
|
|
13
|
+
const cut = Math.max(0, Math.min(length, Math.ceil(position - PIXEL_CENTER_OFFSET)));
|
|
14
|
+
if (cut > cuts[cuts.length - 1] && cut < length)
|
|
15
|
+
cuts.push(cut);
|
|
16
|
+
}
|
|
17
|
+
cuts.push(length);
|
|
18
|
+
return cuts;
|
|
19
|
+
}
|
|
20
|
+
export function manualPixelGrid(image, cellWidth, cellHeight) {
|
|
21
|
+
if (cellWidth > MAX_GRID_CELL_SIZE || cellHeight > MAX_GRID_CELL_SIZE)
|
|
22
|
+
throw new RangeError("Choose cell sizes no larger than 128 pixels.");
|
|
23
|
+
const width = pixelGridCuts(image.width, cellWidth).length - 1, height = pixelGridCuts(image.height, cellHeight).length - 1;
|
|
24
|
+
if (width > MAX_RECOVERED_GRID_SIZE || height > MAX_RECOVERED_GRID_SIZE)
|
|
25
|
+
throw new RangeError("Choose larger cells to restore this image.");
|
|
26
|
+
return { width, height, cellWidth, cellHeight, offsetX: 0, offsetY: 0 };
|
|
27
|
+
}
|
|
28
|
+
/** Even partition of the image into `width` x `height` blocks (clamped to one pixel per block). */
|
|
29
|
+
export function blockPartition(image, width, height) {
|
|
30
|
+
const columns = Math.max(1, Math.min(image.width, Math.round(width)));
|
|
31
|
+
const rows = Math.max(1, Math.min(image.height, Math.round(height)));
|
|
32
|
+
const cuts = (length, count) => Array.from({ length: count + 1 }, (_, index) => Math.round((index * length) / count));
|
|
33
|
+
return {
|
|
34
|
+
width: columns,
|
|
35
|
+
height: rows,
|
|
36
|
+
cellWidth: image.width / columns,
|
|
37
|
+
cellHeight: image.height / rows,
|
|
38
|
+
offsetX: 0,
|
|
39
|
+
offsetY: 0,
|
|
40
|
+
cutsX: cuts(image.width, columns),
|
|
41
|
+
cutsY: cuts(image.height, rows),
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { analyzePixelArt, analyzePixelArtAsync } from "./analyze.ts";
|
|
2
|
+
export { extractPalette, extractPixelationPalette, countDistinctColors, reduceToPalette, } from "./palette.ts";
|
|
3
|
+
export { snapPalette } from "./palette-snap.ts";
|
|
4
|
+
export { estimateTilt, straighten, MAX_TILT_DEGREES } from "./deskew.ts";
|
|
5
|
+
export { resizeNearest } from "./resize.ts";
|
|
6
|
+
export { pixelateImage, pixelateImageAsync } from "./pixelate.ts";
|
|
7
|
+
export { PixelArtClassification, PixelationMethod, PixelationMode, PixelationAlgorithm, CellColorPicker, LineGuideMode, OutlinePrepass, PixelationDither, PaletteColorMode, PixelGridSamplingAxis, } from "./types.ts";
|
|
8
|
+
export { DEFAULT_LINE_GUIDE, readOutlinePrepass, runsStockPixeloe, } from "./outline-prepass.ts";
|
|
9
|
+
export type { ExtractPaletteOptions, PaletteEntry, PixelArtAnalysis, PixelArtAnalysisOptions, PixelArtMetrics, PaletteSnapOptions, PixelateOptions, PixelationSize, PixelationSettings, PixelGridGeometry, PixelGridAnalysis, PixelGridSamplingStage, } from "./types.ts";
|
|
10
|
+
export { defaultPixelationSettings, updatePixelationSettings, applyPixelationGrid, pixelationSizeFromAxis, pixelationSettingsOptions, MAX_PIXELATION_SIZE, PIXELATION_SIZES, PIXELATION_METHOD_OPTIONS, } from "./settings.ts";
|
|
11
|
+
export { manualPixelGrid, MAX_GRID_CELL_SIZE } from "./grid.ts";
|
|
12
|
+
export { removeBackground, modeBorderColor, edgeSeededFloodAlpha, colorKeyDistance, adaptiveTolerance, sampleKeyColor, RemoveBackgroundMode, DEFAULT_REMOVE_BACKGROUND, } from "./remove-background.ts";
|
|
13
|
+
export type { RemoveBackgroundOptions, RemoveBackgroundSettings, } from "./remove-background.ts";
|
|
14
|
+
export { getAlgorithmEntry, getAllAlgorithms, getListedAlgorithms, getGridSources, pixelationMethodOf, detectWithAlgorithm, detectWithAlgorithmAsync, type PixelationAlgorithmEntry, } from "./pixelation-algorithm-registry.ts";
|
|
15
|
+
export { getAllColorPickers, getColorPickerEntry, type CellColorPickerEntry, } from "./cell-color-picker.ts";
|
|
16
|
+
export { preloadWasm, beginAcRlConsensusJob, finishAcRlConsensusJob, beginAcRlConsensusJobAsync, finishAcRlConsensusJobAsync, setAcRlBrowserWorkerFactory, warmAcRlBrowserWorker, acRlWorkerOverlapActive, acRlBrowserWorkerOverlapActive, } from "./grid-ac-rl-consensus.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { analyzePixelArt, analyzePixelArtAsync } from "./analyze.js";
|
|
2
|
+
export { extractPalette, extractPixelationPalette, countDistinctColors, reduceToPalette, } from "./palette.js";
|
|
3
|
+
export { snapPalette } from "./palette-snap.js";
|
|
4
|
+
export { estimateTilt, straighten, MAX_TILT_DEGREES } from "./deskew.js";
|
|
5
|
+
export { resizeNearest } from "./resize.js";
|
|
6
|
+
export { pixelateImage, pixelateImageAsync } from "./pixelate.js";
|
|
7
|
+
export { PixelArtClassification, PixelationMethod, PixelationMode, PixelationAlgorithm, CellColorPicker, LineGuideMode, OutlinePrepass, PixelationDither, PaletteColorMode, PixelGridSamplingAxis, } from "./types.js";
|
|
8
|
+
export { DEFAULT_LINE_GUIDE, readOutlinePrepass, runsStockPixeloe, } from "./outline-prepass.js";
|
|
9
|
+
export { defaultPixelationSettings, updatePixelationSettings, applyPixelationGrid, pixelationSizeFromAxis, pixelationSettingsOptions, MAX_PIXELATION_SIZE, PIXELATION_SIZES, PIXELATION_METHOD_OPTIONS, } from "./settings.js";
|
|
10
|
+
export { manualPixelGrid, MAX_GRID_CELL_SIZE } from "./grid.js";
|
|
11
|
+
export { removeBackground, modeBorderColor, edgeSeededFloodAlpha, colorKeyDistance, adaptiveTolerance, sampleKeyColor, RemoveBackgroundMode, DEFAULT_REMOVE_BACKGROUND, } from "./remove-background.js";
|
|
12
|
+
export { getAlgorithmEntry, getAllAlgorithms, getListedAlgorithms, getGridSources, pixelationMethodOf, detectWithAlgorithm, detectWithAlgorithmAsync, } from "./pixelation-algorithm-registry.js";
|
|
13
|
+
export { getAllColorPickers, getColorPickerEntry, } from "./cell-color-picker.js";
|
|
14
|
+
export { preloadWasm, beginAcRlConsensusJob, finishAcRlConsensusJob, beginAcRlConsensusJobAsync, finishAcRlConsensusJobAsync, setAcRlBrowserWorkerFactory, warmAcRlBrowserWorker, acRlWorkerOverlapActive, acRlBrowserWorkerOverlapActive, } from "./grid-ac-rl-consensus.js";
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 8-bit sRGB <-> CIE Lab exactly as OpenCV's bit-exact `COLOR_RGB2LAB` / `COLOR_LAB2RGB`
|
|
3
|
+
* (modules/imgproc/src/color_lab.cpp, `RGB2Lab_b` and `Lab2RGBinteger`): the same lookup tables
|
|
4
|
+
* (built in single precision like OpenCV's softfloat) and the same fixed-point arithmetic, so the
|
|
5
|
+
* legacy PixelOE port reproduces the reference pixel for pixel. L is 0..255 (L*255/100), a and b
|
|
6
|
+
* are offset by 128.
|
|
7
|
+
*/
|
|
8
|
+
const f32 = Math.fround;
|
|
9
|
+
const GAMMA_SHIFT = 3;
|
|
10
|
+
const LAB_SHIFT = 12;
|
|
11
|
+
const LAB_SHIFT2 = LAB_SHIFT + GAMMA_SHIFT;
|
|
12
|
+
const CBRT_TABLE_SIZE = (256 * 3 * (1 << GAMMA_SHIFT)) / 2;
|
|
13
|
+
const INV_GAMMA_SHIFT = 12;
|
|
14
|
+
const INV_GAMMA_SIZE = 1 << INV_GAMMA_SHIFT;
|
|
15
|
+
const LUT_BASE = 1 << 14;
|
|
16
|
+
const MIN_AB = -8145;
|
|
17
|
+
const AB_TABLE_SIZE = (LUT_BASE * 9) / 4;
|
|
18
|
+
const RGB_SHIFT = LAB_SHIFT + 14 - INV_GAMMA_SHIFT;
|
|
19
|
+
/** D65 white point and the sRGB matrices, as the doubles OpenCV stores. */
|
|
20
|
+
const D65 = [0.950456, 1, 1.088754];
|
|
21
|
+
const RGB_TO_XYZ = [
|
|
22
|
+
0.412453, 0.35758, 0.180423, 0.212671, 0.71516, 0.072169, 0.019334, 0.119193, 0.950227,
|
|
23
|
+
];
|
|
24
|
+
const XYZ_TO_RGB = [
|
|
25
|
+
3.240479, -1.53715, -0.498535, -0.969256, 1.875991, 0.041556, 0.055648, -0.204043, 1.057311,
|
|
26
|
+
];
|
|
27
|
+
/** The one cube-root table entry where softfloat and a correctly rounded cbrt disagree. */
|
|
28
|
+
const CBRT_SOFTFLOAT_ENTRY = 324;
|
|
29
|
+
const L_THRESH = f32(216 / 24389);
|
|
30
|
+
const L_SCALE = f32(841 / 108);
|
|
31
|
+
const L_BIAS = f32(16 / 116);
|
|
32
|
+
/** cvRound: round half to even. */
|
|
33
|
+
function round(v) {
|
|
34
|
+
const r = Math.round(v);
|
|
35
|
+
return r - v === 0.5 && r % 2 !== 0 ? r - 1 : r;
|
|
36
|
+
}
|
|
37
|
+
const descale = (v, n) => (v + (1 << (n - 1))) >> n;
|
|
38
|
+
const clampByte = (v) => (v < 0 ? 0 : v > 255 ? 255 : v);
|
|
39
|
+
function applyGamma(x) {
|
|
40
|
+
return f32(x <= 809 / 20000 ? x / (323 / 25) : ((x + 11 / 200) / (1 + 11 / 200)) ** (12 / 5));
|
|
41
|
+
}
|
|
42
|
+
function applyInvGamma(x) {
|
|
43
|
+
return f32(x <= 7827 / 2500000 ? x * (323 / 25) : x ** (1 / (12 / 5)) * (1 + 11 / 200) - 11 / 200);
|
|
44
|
+
}
|
|
45
|
+
let tables = null;
|
|
46
|
+
function labTables() {
|
|
47
|
+
if (tables)
|
|
48
|
+
return tables;
|
|
49
|
+
const gamma = new Int32Array(256);
|
|
50
|
+
for (let i = 0; i < 256; i++)
|
|
51
|
+
gamma[i] = round(f32(f32(255 * 8) * applyGamma(f32(i / 255))));
|
|
52
|
+
const cbrt = new Int32Array(CBRT_TABLE_SIZE);
|
|
53
|
+
const cbScale = f32(1 / f32(255 * (1 << GAMMA_SHIFT)));
|
|
54
|
+
for (let i = 0; i < CBRT_TABLE_SIZE; i++) {
|
|
55
|
+
const x = f32(cbScale * i);
|
|
56
|
+
const v = x < L_THRESH ? f32(x * L_SCALE + L_BIAS) : f32(Math.cbrt(x));
|
|
57
|
+
cbrt[i] = round(f32((1 << LAB_SHIFT2) * v));
|
|
58
|
+
}
|
|
59
|
+
// OpenCV's softfloat cbrt is not correctly rounded here: it lands one float lower, so the
|
|
60
|
+
// scaled value is just under the .5 tie the correctly rounded cube root hits.
|
|
61
|
+
cbrt[CBRT_SOFTFLOAT_ENTRY] -= 1;
|
|
62
|
+
const forward = new Int32Array(9);
|
|
63
|
+
for (let i = 0; i < 3; i++)
|
|
64
|
+
for (let j = 0; j < 3; j++)
|
|
65
|
+
forward[i * 3 + j] = round(((1 << LAB_SHIFT) * RGB_TO_XYZ[i * 3 + j]) / D65[i]);
|
|
66
|
+
const yf = new Int32Array(512);
|
|
67
|
+
for (let i = 0; i < 256; i++) {
|
|
68
|
+
let y, ify;
|
|
69
|
+
if (i <= 20) {
|
|
70
|
+
y = round(f32(f32(i * LUT_BASE * 20 * 9) / f32(17 * 29 * 29 * 29)));
|
|
71
|
+
ify = round(f32(LUT_BASE * f32(f32(16 / 116) + f32(f32(i * 5) / f32(3 * 17 * 29)))));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const fy = f32(f32(f32(i * 100 * LUT_BASE) / f32(255 * 116)) + f32(f32(16 * LUT_BASE) / f32(116)));
|
|
75
|
+
ify = round(fy);
|
|
76
|
+
y = round(f32(f32(f32(fy * fy) * fy) / f32(LUT_BASE * LUT_BASE)));
|
|
77
|
+
}
|
|
78
|
+
yf[i * 2] = y;
|
|
79
|
+
yf[i * 2 + 1] = ify;
|
|
80
|
+
}
|
|
81
|
+
const abToXz = new Int32Array(AB_TABLE_SIZE);
|
|
82
|
+
for (let i = MIN_AB; i < AB_TABLE_SIZE + MIN_AB; i++)
|
|
83
|
+
abToXz[i - MIN_AB] =
|
|
84
|
+
i <= 3390
|
|
85
|
+
? Math.trunc((i * 108) / 841) - Math.trunc((Math.trunc((LUT_BASE * 16) / 116) * 108) / 841)
|
|
86
|
+
: Math.trunc((Math.trunc((i * i) / LUT_BASE) * i) / LUT_BASE);
|
|
87
|
+
const invGamma = new Uint8Array(INV_GAMMA_SIZE);
|
|
88
|
+
for (let i = 0; i < INV_GAMMA_SIZE; i++)
|
|
89
|
+
invGamma[i] = clampByte(round(f32(255 * applyInvGamma(f32(i / INV_GAMMA_SIZE)))));
|
|
90
|
+
const inverse = new Int32Array(9);
|
|
91
|
+
for (let i = 0; i < 3; i++)
|
|
92
|
+
for (let j = 0; j < 3; j++)
|
|
93
|
+
inverse[i * 3 + j] = round((1 << LAB_SHIFT) * XYZ_TO_RGB[i * 3 + j] * D65[j]);
|
|
94
|
+
tables = { gamma, cbrt, forward, yf, abToXz, invGamma, inverse };
|
|
95
|
+
return tables;
|
|
96
|
+
}
|
|
97
|
+
/** Interleaved RGB (3 bytes per pixel) to interleaved 8-bit Lab. */
|
|
98
|
+
export function rgbToLab8(rgb) {
|
|
99
|
+
const { gamma, cbrt, forward: c } = labTables();
|
|
100
|
+
const out = new Uint8Array(rgb.length);
|
|
101
|
+
const lScale = Math.trunc((116 * 255 + 50) / 100);
|
|
102
|
+
const lShift = -Math.trunc((16 * 255 * (1 << LAB_SHIFT2) + 50) / 100);
|
|
103
|
+
const abShift = 128 * (1 << LAB_SHIFT2);
|
|
104
|
+
for (let i = 0; i < rgb.length; i += 3) {
|
|
105
|
+
const r = gamma[rgb[i]], g = gamma[rgb[i + 1]], b = gamma[rgb[i + 2]];
|
|
106
|
+
const fx = cbrt[descale(r * c[0] + g * c[1] + b * c[2], LAB_SHIFT)];
|
|
107
|
+
const fy = cbrt[descale(r * c[3] + g * c[4] + b * c[5], LAB_SHIFT)];
|
|
108
|
+
const fz = cbrt[descale(r * c[6] + g * c[7] + b * c[8], LAB_SHIFT)];
|
|
109
|
+
out[i] = clampByte(descale(lScale * fy + lShift, LAB_SHIFT2));
|
|
110
|
+
out[i + 1] = clampByte(descale(500 * (fx - fy) + abShift, LAB_SHIFT2));
|
|
111
|
+
out[i + 2] = clampByte(descale(200 * (fy - fz) + abShift, LAB_SHIFT2));
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/** Interleaved 8-bit Lab to interleaved RGB. */
|
|
116
|
+
export function labToRgb8(lab) {
|
|
117
|
+
const { yf, abToXz, invGamma, inverse: c } = labTables();
|
|
118
|
+
const out = new Uint8Array(lab.length);
|
|
119
|
+
const top = INV_GAMMA_SIZE - 1;
|
|
120
|
+
const aOffset = Math.trunc((128 * LUT_BASE) / 500);
|
|
121
|
+
const bOffset = Math.trunc((128 * LUT_BASE) / 200) - 1;
|
|
122
|
+
for (let i = 0; i < lab.length; i += 3) {
|
|
123
|
+
const l = lab[i], a = lab[i + 1], b = lab[i + 2];
|
|
124
|
+
const y = yf[l * 2], ify = yf[l * 2 + 1];
|
|
125
|
+
const adiv = ((5 * a * 53687 + (1 << 7)) >> 13) - aOffset;
|
|
126
|
+
const bdiv = ((b * 41943 + (1 << 4)) >> 9) - bOffset;
|
|
127
|
+
const x = abToXz[ify + adiv - MIN_AB], z = abToXz[ify - bdiv - MIN_AB];
|
|
128
|
+
for (let k = 0; k < 3; k++) {
|
|
129
|
+
const v = descale(c[k * 3] * x + c[k * 3 + 1] * y + c[k * 3 + 2] * z, RGB_SHIFT);
|
|
130
|
+
out[i + k] = invGamma[v < 0 ? 0 : v > top ? top : v];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare function rgbToGray8(rgb: Uint8Array): Uint8Array;
|
|
2
|
+
/** cv::resize INTER_LINEAR for 8-bit images with `cn` interleaved channels. */
|
|
3
|
+
export declare function resizeLinear8(src: Uint8Array, sw: number, sh: number, cn: number, dw: number, dh: number): Uint8Array;
|
|
4
|
+
/** cv::resize INTER_LINEAR for one-channel float64 images, as OpenCV 5's IPP HAL computes it. */
|
|
5
|
+
export declare function resizeLinear64(src: Float64Array, sw: number, sh: number, dw: number, dh: number): Float64Array;
|
|
6
|
+
/** cv::resize INTER_AREA of a one-channel float32 image. Enlarging uses OpenCV's area-weighted bilinear. */
|
|
7
|
+
export declare function resizeArea32(src: Float32Array, sw: number, sh: number, dw: number, dh: number): Float32Array;
|
|
8
|
+
/** cv::resize INTER_AREA of an 8-bit image with `cn` channels, when shrinking. */
|
|
9
|
+
export declare function resizeArea8(src: Uint8Array, sw: number, sh: number, cn: number, dw: number, dh: number): Uint8Array;
|
|
10
|
+
/** cv::resize INTER_NEAREST with `cn` interleaved channels. */
|
|
11
|
+
export declare function resizeNearest<T extends Uint8Array | Float32Array>(src: T, sw: number, sh: number, cn: number, dw: number, dh: number): T;
|
|
12
|
+
/** Kernel size OpenCV derives from sigma for float images. */
|
|
13
|
+
export declare const gaussianSize: (sigma: number) => number;
|
|
14
|
+
/** cv::GaussianBlur of a one-channel float32 image (separable, REFLECT_101, FMA accumulation). */
|
|
15
|
+
export declare function gaussianBlur32(src: Float32Array, w: number, h: number, size: number, sigma: number): Float32Array;
|
|
16
|
+
/** 3x3 rectangle or cross structuring element. */
|
|
17
|
+
export declare enum MorphShape {
|
|
18
|
+
Rect = "rect",
|
|
19
|
+
Cross = "cross"
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* cv::erode / cv::dilate of an interleaved 8-bit image with a 3x3 element, `iterations` times.
|
|
23
|
+
* Pixels outside the image are ignored, as with OpenCV's default border value.
|
|
24
|
+
*/
|
|
25
|
+
export declare function morph8(src: Uint8Array, w: number, h: number, cn: number, shape: MorphShape, erode: boolean, iterations: number): Uint8Array;
|
|
26
|
+
/** NumPy's float32 pairwise summation (np.add.reduce over a contiguous array). */
|
|
27
|
+
export declare function pairwiseSum32(a: ArrayLike<number>, start?: number, n?: number): number;
|
|
28
|
+
/** cv::GaussianBlur of a one-channel 8-bit image with sigma 0 (OpenCV's bit-exact fixed point). */
|
|
29
|
+
export declare function gaussianBlur8(src: Uint8Array, w: number, h: number, size: number): Uint8Array;
|