@xprite/pixelate 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +12 -0
  3. package/README.md +238 -2
  4. package/README.zh-CN.md +209 -0
  5. package/dist/analyze.d.ts +8 -0
  6. package/dist/analyze.js +217 -0
  7. package/dist/base.d.ts +18 -0
  8. package/dist/base.js +42 -0
  9. package/dist/cell-color-picker.d.ts +14 -0
  10. package/dist/cell-color-picker.js +278 -0
  11. package/dist/color-lab.d.ts +3 -0
  12. package/dist/color-lab.js +31 -0
  13. package/dist/color-step.d.ts +33 -0
  14. package/dist/color-step.js +62 -0
  15. package/dist/contours.d.ts +4 -0
  16. package/dist/contours.js +66 -0
  17. package/dist/denoise.d.ts +3 -0
  18. package/dist/denoise.js +37 -0
  19. package/dist/deskew.d.ts +24 -0
  20. package/dist/deskew.js +202 -0
  21. package/dist/grid-ac-rl-consensus.d.ts +127 -0
  22. package/dist/grid-ac-rl-consensus.js +497 -0
  23. package/dist/grid-arbitrate.d.ts +52 -0
  24. package/dist/grid-arbitrate.js +366 -0
  25. package/dist/grid-cut-snap.d.ts +15 -0
  26. package/dist/grid-cut-snap.js +77 -0
  27. package/dist/grid-description-length.d.ts +47 -0
  28. package/dist/grid-description-length.js +697 -0
  29. package/dist/grid-detect.d.ts +8 -0
  30. package/dist/grid-detect.js +665 -0
  31. package/dist/grid-edge-coherence.d.ts +4 -0
  32. package/dist/grid-edge-coherence.js +158 -0
  33. package/dist/grid-edge-path.d.ts +9 -0
  34. package/dist/grid-edge-path.js +201 -0
  35. package/dist/grid-frame-lattice.d.ts +25 -0
  36. package/dist/grid-frame-lattice.js +420 -0
  37. package/dist/grid-frequency-spectrum.d.ts +11 -0
  38. package/dist/grid-frequency-spectrum.js +89 -0
  39. package/dist/grid-period-boundary.d.ts +13 -0
  40. package/dist/grid-period-boundary.js +255 -0
  41. package/dist/grid-period-comb.d.ts +4 -0
  42. package/dist/grid-period-comb.js +230 -0
  43. package/dist/grid-period-distance.d.ts +7 -0
  44. package/dist/grid-period-distance.js +233 -0
  45. package/dist/grid-phase-field.d.ts +11 -0
  46. package/dist/grid-phase-field.js +446 -0
  47. package/dist/grid-pitch-lattice.d.ts +4 -0
  48. package/dist/grid-pitch-lattice.js +255 -0
  49. package/dist/grid-pitch-search.d.ts +11 -0
  50. package/dist/grid-pitch-search.js +233 -0
  51. package/dist/grid-sample.d.ts +4 -0
  52. package/dist/grid-sample.js +164 -0
  53. package/dist/grid-sampling.d.ts +13 -0
  54. package/dist/grid-sampling.js +132 -0
  55. package/dist/grid-strip-shift.d.ts +44 -0
  56. package/dist/grid-strip-shift.js +223 -0
  57. package/dist/grid-test-images.d.ts +69 -0
  58. package/dist/grid-test-images.js +547 -0
  59. package/dist/grid-warp.d.ts +38 -0
  60. package/dist/grid-warp.js +252 -0
  61. package/dist/grid.d.ts +8 -0
  62. package/dist/grid.js +43 -0
  63. package/dist/index.d.ts +16 -0
  64. package/dist/index.js +14 -0
  65. package/dist/line-guided/cv-lab.d.ts +4 -0
  66. package/dist/line-guided/cv-lab.js +134 -0
  67. package/dist/line-guided/cv-ops.d.ts +29 -0
  68. package/dist/line-guided/cv-ops.js +759 -0
  69. package/dist/line-guided/pixeloe-entry.d.ts +15 -0
  70. package/dist/line-guided/pixeloe-entry.js +122 -0
  71. package/dist/line-guided/pixeloe-legacy.d.ts +31 -0
  72. package/dist/line-guided/pixeloe-legacy.js +301 -0
  73. package/dist/line-guided/xdog.d.ts +7 -0
  74. package/dist/line-guided/xdog.js +84 -0
  75. package/dist/most-frequent.d.ts +3 -0
  76. package/dist/most-frequent.js +56 -0
  77. package/dist/outline-prepass.d.ts +37 -0
  78. package/dist/outline-prepass.js +56 -0
  79. package/dist/palette-snap.d.ts +4 -0
  80. package/dist/palette-snap.js +371 -0
  81. package/dist/palette.d.ts +21 -0
  82. package/dist/palette.js +280 -0
  83. package/dist/pixelate.d.ts +15 -0
  84. package/dist/pixelate.js +214 -0
  85. package/dist/pixelation-algorithm-registry.d.ts +34 -0
  86. package/dist/pixelation-algorithm-registry.js +716 -0
  87. package/dist/pixeloe-prepass.d.ts +12 -0
  88. package/dist/pixeloe-prepass.js +527 -0
  89. package/dist/quantization.d.ts +6 -0
  90. package/dist/quantization.js +145 -0
  91. package/dist/remove-background.d.ts +40 -0
  92. package/dist/remove-background.js +229 -0
  93. package/dist/resize.d.ts +3 -0
  94. package/dist/resize.js +86 -0
  95. package/dist/sampling.d.ts +7 -0
  96. package/dist/sampling.js +301 -0
  97. package/dist/settings.d.ts +36 -0
  98. package/dist/settings.js +181 -0
  99. package/dist/types.d.ts +255 -0
  100. package/dist/types.js +87 -0
  101. package/licenses/PixelOE-Apache-2.0.txt +201 -0
  102. package/licenses/pixel-art-fixer-MIT.txt +21 -0
  103. package/licenses/proper-pixel-art-MIT.txt +21 -0
  104. package/licenses/tensorhead-dithering-MIT.txt +21 -0
  105. package/package.json +62 -4
  106. package/wasm/ac-rl-detect-browser-worker.js +45 -0
  107. package/wasm/ac-rl-detect-worker.mjs +51 -0
  108. package/wasm/pixelfixer_detect.d.ts +65 -0
  109. package/wasm/pixelfixer_detect.js +334 -0
  110. package/wasm/pixelfixer_detect_bg.wasm +0 -0
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Grid arbitration via description-length (MDL).
3
+ *
4
+ * GridEvidenceReader shares the image description, line prefixes and distinct readings across
5
+ * arbitration, corroboration and consensus selection for one evaluation view.
6
+ */
7
+ import { MAX_RECOVERED_GRID_SIZE, pixelGridCuts } from "./grid.js";
8
+ import { snapLatticeCuts } from "./grid-cut-snap.js";
9
+ import { CutModel, describeImage, descriptionLength, pixelLength, } from "./grid-description-length.js";
10
+ import { fieldChanceEvidence, phaseField } from "./grid-phase-field.js";
11
+ import { chanceEvidence, StripImage, stripShear, } from "./grid-strip-shift.js";
12
+ import { PixelationAlgorithm, } from "./types.js";
13
+ /**
14
+ * One decision over every proposed grid and the passthrough. Each proposal is read in the ways
15
+ * its cuts allow, and the reading that scores best stands: an edge mesh as placed; a lattice as
16
+ * cut or with its cuts snapped to the nearest edges (leaving every cut in place is one of the
17
+ * snaps, so the offsets have to pay for themselves); and either of them through strip shifts (see
18
+ * grid-strip-shift) or a phase field (see grid-phase-field), described on the straightened image
19
+ * and paying for the shifts. Every reading carries three numbers:
20
+ *
21
+ * - its description length of the input (see grid-description-length);
22
+ * - the length of its twin: the same model with every cut moved half a cell, onto the cell
23
+ * middles. A real grid is phase-locked, so its twin describes the input markedly worse; a
24
+ * grid the content ignores (photos, gradients, a finer or coarser octave of the true grid that
25
+ * only partly lines up) describes it about as well at either phase. Cuts placed on edges
26
+ * chose their positions from the input, so their twin settles on edges the same way; it may
27
+ * not settle on the reading's own cuts, or it would no longer stand at the other phase. The
28
+ * twin of a strip-shifted reading shifts every strip into the anti-phase of its own shift.
29
+ * - its chance evidence: the phase evidence its strip search finds at pitches the content does
30
+ * not have, which the reading does not count as its own (none for unshifted readings).
31
+ *
32
+ * A reading scores its length less PHASE_EVIDENCE_WEIGHT times its phase evidence (the twin's
33
+ * length less its own and its chance evidence), so among lattices that describe the image about
34
+ * equally, the one the content is locked to wins. The passthrough carries no phase evidence and
35
+ * scores the shorter of two descriptions without a recovered grid, less NULL_MARGIN:
36
+ *
37
+ * - the image as it is, one cell per pixel (native pixel art and drawings), and
38
+ * - the best length any proposal reaches at the worse of its two phases, less the phase evidence
39
+ * its search finds by chance: what a lattice explains whatever its phase (smooth photos and
40
+ * gradients), or with shifts that find a phase on any content, is no evidence of a grid.
41
+ *
42
+ * Phase evidence has to come from every cut, which also settles octaves. Split a reading's cuts
43
+ * into OCTAVE_CLASSES (every second cut, every third cut) and moving one class to the cell
44
+ * middles should lose about its share of the twin's evidence. A finer octave of a coarser grid,
45
+ * such as a lattice at half the pitch of a textured upscale, has a class inside the coarser
46
+ * cells, which moves for free: the shortfall of the least evident class from its share is
47
+ * evidence the reading does not own, and it adds to the reading's score.
48
+ *
49
+ * Geometries are admitted by the strongest proposal's confidence; every cut model for an
50
+ * admitted geometry competes on the same score, independently of its proposer's confidence.
51
+ */
52
+ const PHASE_EVIDENCE_WEIGHT = 1.1;
53
+ /** Nats per sample the passthrough is preferred by. */
54
+ const NULL_MARGIN = 0.03;
55
+ /**
56
+ * Cells per axis a grid needs: with fewer, a few large features can line up with the cuts by
57
+ * chance and pass for phase evidence.
58
+ */
59
+ const MIN_CELLS = 8;
60
+ const STRONG_GRID_CONFIDENCE = 0.9;
61
+ const DISTINCT_GEOMETRY_CONFIDENCE_GAP = 0.5;
62
+ /** Every how many cuts the octave classes take. */
63
+ const OCTAVE_CLASSES = [2, 3];
64
+ const HALF = 0.5;
65
+ export const phaseScore = (reading) => reading.length - PHASE_EVIDENCE_WEIGHT * (reading.twin - reading.length - reading.chance);
66
+ /** The cuts moved half a cell: one cut at each cell middle, inside the same extent. */
67
+ export function twinCuts(cuts) {
68
+ const last = cuts[cuts.length - 1];
69
+ const twin = [0];
70
+ for (let i = 0; i + 1 < cuts.length; i++) {
71
+ const middle = Math.round((cuts[i] + cuts[i + 1]) * HALF);
72
+ if (middle > twin[twin.length - 1] && middle < last)
73
+ twin.push(middle);
74
+ }
75
+ twin.push(last);
76
+ return twin;
77
+ }
78
+ /** The cuts with every `period`th inner cut, from the `phase`th, moved to its cell's middle. */
79
+ export function classTwinCuts(cuts, period, phase) {
80
+ const twin = [...cuts];
81
+ const cells = cuts.length - 1;
82
+ for (let i = 1; i < cells; i++)
83
+ if (i % period === phase)
84
+ twin[i] = Math.round((cuts[i] + cuts[i + 1]) * HALF);
85
+ for (let i = 1; i <= cells; i++)
86
+ if (twin[i] <= twin[i - 1])
87
+ twin[i] = twin[i - 1] + 1;
88
+ return twin;
89
+ }
90
+ /** Phase evidence, per sample, the reading's cuts miss from their least evident class. */
91
+ export function octaveShortfall(frame, cutsX, cutsY) {
92
+ const length = (x, y) => descriptionLength(frame, { cutsX: x, cutsY: y, model: CutModel.Lattice });
93
+ const own = length(cutsX, cutsY);
94
+ const evidence = length(twinCuts(cutsX), twinCuts(cutsY)) - own;
95
+ let shortfall = 0;
96
+ for (const period of OCTAVE_CLASSES) {
97
+ let least = Infinity;
98
+ for (let phase = 0; phase < period; phase++)
99
+ least = Math.min(least, length(classTwinCuts(cutsX, period, phase), classTwinCuts(cutsY, period, phase)) - own);
100
+ shortfall = Math.max(shortfall, evidence - period * least);
101
+ }
102
+ return shortfall;
103
+ }
104
+ function cutsOf(image, grid) {
105
+ return {
106
+ cutsX: [...(grid.cutsX ?? pixelGridCuts(image.width, grid.cellWidth, grid.offsetX))],
107
+ cutsY: [...(grid.cutsY ?? pixelGridCuts(image.height, grid.cellHeight, grid.offsetY))],
108
+ };
109
+ }
110
+ const keyOf = (cutsX, cutsY) => `${cutsX.join(",")}|${cutsY.join(",")}`;
111
+ /** The twin of cuts placed on edges: settled on edges the same way, but never on its own cuts. */
112
+ function settledTwin(image, cutsX, cutsY, model) {
113
+ const settled = snapLatticeCuts(image, twinCuts(cutsX), twinCuts(cutsY), {
114
+ x: cutsX,
115
+ y: cutsY,
116
+ });
117
+ return {
118
+ cutsX: settled.x.cuts,
119
+ cutsY: settled.y.cuts,
120
+ model,
121
+ radiusX: settled.x.radius,
122
+ radiusY: settled.y.radius,
123
+ };
124
+ }
125
+ /** The proposal as placed or snapped, with the twin to measure it against. */
126
+ function placedReading(image, described, grid, cutsX, cutsY) {
127
+ if (grid.algorithm === PixelationAlgorithm.EdgeGrid) {
128
+ const cuts = { cutsX, cutsY, model: CutModel.Free };
129
+ return {
130
+ cuts,
131
+ length: descriptionLength(described, cuts),
132
+ twin: () => settledTwin(image, cutsX, cutsY, CutModel.Free),
133
+ };
134
+ }
135
+ const lattice = { cutsX, cutsY, model: CutModel.Lattice };
136
+ const latticeLength = descriptionLength(described, lattice);
137
+ const snap = snapLatticeCuts(image, cutsX, cutsY);
138
+ const snapped = {
139
+ cutsX: snap.x.cuts,
140
+ cutsY: snap.y.cuts,
141
+ model: CutModel.Snapped,
142
+ radiusX: snap.x.radius,
143
+ radiusY: snap.y.radius,
144
+ };
145
+ const snappedLength = descriptionLength(described, snapped);
146
+ if (latticeLength <= snappedLength)
147
+ return {
148
+ cuts: lattice,
149
+ length: latticeLength,
150
+ twin: () => ({ cutsX: twinCuts(cutsX), cutsY: twinCuts(cutsY), model: CutModel.Lattice }),
151
+ };
152
+ return {
153
+ cuts: snapped,
154
+ length: snappedLength,
155
+ twin: () => settledTwin(image, snap.x.cuts, snap.y.cuts, CutModel.Snapped),
156
+ };
157
+ }
158
+ /** The correction search and sampled colors depend on geometry, independently of the cut model. */
159
+ function shiftedReadings(strips, cutsX, cutsY) {
160
+ const field = phaseField(strips, cutsX, cutsY);
161
+ const shifted = [
162
+ {
163
+ shear: stripShear(strips, cutsX, cutsY),
164
+ chance: (stop, fragments) => chanceEvidence(strips, cutsX, cutsY, stop, fragments),
165
+ },
166
+ {
167
+ shear: field,
168
+ chance: (stop, fragments) => fieldChanceEvidence(strips, cutsX, cutsY, field.bandsX, field.bandsY, stop, fragments),
169
+ },
170
+ ];
171
+ return shifted.flatMap(({ shear, chance }) => {
172
+ if (!shear)
173
+ return [];
174
+ let frame, twin, evidence, fragments;
175
+ return [
176
+ {
177
+ shear,
178
+ frame: () => (frame ??= describeImage(shear.straight)),
179
+ twin: () => (twin ??= describeImage(shear.twin())),
180
+ chance: (stop) => {
181
+ if (evidence !== undefined)
182
+ return evidence;
183
+ const measured = chance(stop, (fragments ??= []));
184
+ // A stopped measurement is not a reusable exact value for another cut model.
185
+ if (measured !== Infinity)
186
+ evidence = measured;
187
+ return measured;
188
+ },
189
+ },
190
+ ];
191
+ });
192
+ }
193
+ function readingOf(strips, described, grid, cutsX, cutsY, shifted) {
194
+ const placed = placedReading(strips.image, described, grid, cutsX, cutsY);
195
+ let best = {
196
+ cuts: placed.cuts,
197
+ length: placed.length,
198
+ twin: descriptionLength(described, placed.twin()),
199
+ chance: 0,
200
+ frame: () => described,
201
+ sampling: strips.image.sampling,
202
+ };
203
+ const cuts = {
204
+ cutsX,
205
+ cutsY,
206
+ model: grid.algorithm === PixelationAlgorithm.EdgeGrid ? CutModel.Free : CutModel.Lattice,
207
+ };
208
+ for (const { shear, frame, twin, chance } of shifted()) {
209
+ const straight = frame();
210
+ const cost = shear.cost / straight.samples;
211
+ const reading = {
212
+ cuts,
213
+ length: descriptionLength(straight, cuts) + cost,
214
+ twin: descriptionLength(twin(), cuts) + cost,
215
+ chance: 0,
216
+ frame,
217
+ sampling: shear.straight.sampling,
218
+ };
219
+ // Chance evidence only adds to a score, so it is measured only for a reading that could win.
220
+ const optimistic = phaseScore(reading), incumbent = phaseScore(best);
221
+ if (optimistic >= incumbent)
222
+ continue;
223
+ const canBound = Number.isFinite(reading.length) &&
224
+ Number.isFinite(reading.twin) &&
225
+ Number.isFinite(optimistic) &&
226
+ Number.isFinite(incumbent);
227
+ reading.chance = chance(canBound
228
+ ? (lowerBound) => phaseScore({ length: reading.length, twin: reading.twin, chance: lowerBound }) >=
229
+ incumbent
230
+ : undefined);
231
+ if (phaseScore(reading) < incumbent)
232
+ best = reading;
233
+ }
234
+ return best;
235
+ }
236
+ /** Reuse one evaluation view's descriptions and distinct readings during Auto's decision. */
237
+ export class GridEvidenceReader {
238
+ image;
239
+ described;
240
+ strips;
241
+ pixels;
242
+ readings = new Map();
243
+ constructor(image) {
244
+ this.image = image;
245
+ this.described = describeImage(image);
246
+ this.strips = new StripImage(image);
247
+ this.pixels = pixelLength(this.described);
248
+ }
249
+ read(grids) {
250
+ const { image, described, strips } = this;
251
+ const seen = new Set();
252
+ const readings = [];
253
+ // Admission is scoped to geometry; every model of admitted cuts is evaluated.
254
+ const ordered = grids
255
+ .map((grid) => {
256
+ const { cutsX, cutsY } = cutsOf(image, grid);
257
+ const model = grid.algorithm === PixelationAlgorithm.EdgeGrid ? CutModel.Free : CutModel.Lattice;
258
+ const geometry = keyOf(cutsX, cutsY);
259
+ return { grid, cutsX, cutsY, geometry, key: `${model}:${geometry}` };
260
+ })
261
+ .sort((a, b) => b.grid.confidence - a.grid.confidence ||
262
+ a.key.localeCompare(b.key) ||
263
+ (a.grid.algorithm ?? "").localeCompare(b.grid.algorithm ?? "") ||
264
+ a.grid.cellWidth - b.grid.cellWidth ||
265
+ a.grid.cellHeight - b.grid.cellHeight ||
266
+ a.grid.offsetX - b.grid.offsetX ||
267
+ a.grid.offsetY - b.grid.offsetY);
268
+ // Only pending models of the same geometry need shared auxiliary descriptions. Release
269
+ // those once both models are read; persistent readings retain only their winning frame.
270
+ const pending = new Map();
271
+ const corrections = new Map();
272
+ for (const { geometry, key } of ordered) {
273
+ if (this.readings.has(key))
274
+ continue;
275
+ const models = pending.get(geometry) ?? new Set();
276
+ models.add(key);
277
+ pending.set(geometry, models);
278
+ }
279
+ const admittedGeometries = new Set();
280
+ let bestConfidence = -Infinity;
281
+ for (const { grid, cutsX, cutsY, geometry, key } of ordered) {
282
+ const columns = cutsX.length - 1, rows = cutsY.length - 1;
283
+ if (Math.min(columns, rows) < MIN_CELLS || Math.max(columns, rows) > MAX_RECOVERED_GRID_SIZE)
284
+ continue;
285
+ if (seen.has(key))
286
+ continue;
287
+ seen.add(key);
288
+ if (!admittedGeometries.has(geometry) &&
289
+ readings.length > 0 &&
290
+ bestConfidence >= STRONG_GRID_CONFIDENCE &&
291
+ bestConfidence - grid.confidence >= DISTINCT_GEOMETRY_CONFIDENCE_GAP)
292
+ continue;
293
+ admittedGeometries.add(geometry);
294
+ let reading = this.readings.get(key);
295
+ if (!reading) {
296
+ reading = readingOf(strips, described, grid, cutsX, cutsY, () => {
297
+ if (grid.placed)
298
+ return [];
299
+ let shifted = corrections.get(geometry);
300
+ if (!shifted) {
301
+ shifted = shiftedReadings(strips, cutsX, cutsY);
302
+ if (pending.get(geometry).size > 1)
303
+ corrections.set(geometry, shifted);
304
+ }
305
+ return shifted;
306
+ });
307
+ this.readings.set(key, reading);
308
+ const models = pending.get(geometry);
309
+ models.delete(key);
310
+ if (!models.size)
311
+ corrections.delete(geometry);
312
+ }
313
+ const { cuts, length, twin, chance, frame, sampling } = reading;
314
+ readings.push({
315
+ grid: {
316
+ ...grid,
317
+ width: columns,
318
+ height: rows,
319
+ cutsX: [...cuts.cutsX],
320
+ cutsY: [...cuts.cutsY],
321
+ sampling,
322
+ },
323
+ length,
324
+ twin,
325
+ chance,
326
+ frame,
327
+ });
328
+ bestConfidence = Math.max(bestConfidence, grid.confidence);
329
+ }
330
+ return { readings, pixels: this.pixels };
331
+ }
332
+ }
333
+ /**
334
+ * The best-scoring grid among the proposals, or null when the passthrough scores lower. The
335
+ * winner's confidence is the larger of its proposer's and the share of its twin's length that
336
+ * its phase saves.
337
+ */
338
+ export function arbitrateGrids(image, grids, evidence = new GridEvidenceReader(image)) {
339
+ const { readings, pixels } = evidence.read(grids);
340
+ let passthrough = pixels;
341
+ for (const reading of readings)
342
+ passthrough = Math.min(passthrough, Math.max(reading.length, reading.twin - reading.chance));
343
+ let best = null;
344
+ let score = passthrough - NULL_MARGIN;
345
+ // The octave shortfall only adds to a score, so readings are checked best phase score first
346
+ // and the search stops at the first that cannot win even without it.
347
+ for (const reading of [...readings].sort((a, b) => phaseScore(a) - phaseScore(b))) {
348
+ const phase = phaseScore(reading);
349
+ if (phase >= score)
350
+ break;
351
+ const { cutsX, cutsY } = reading.grid;
352
+ const total = phase + octaveShortfall(reading.frame(), cutsX, cutsY);
353
+ if (total < score) {
354
+ best = reading;
355
+ score = total;
356
+ }
357
+ }
358
+ if (!best)
359
+ return null;
360
+ const share = Math.max(0, Math.min(1, (best.twin - best.length) / best.twin));
361
+ return {
362
+ grid: { ...best.grid, confidence: Math.max(best.grid.confidence, share), reliable: true },
363
+ length: best.length,
364
+ score,
365
+ };
366
+ }
@@ -0,0 +1,15 @@
1
+ import type { PixelBuffer } from "./base.ts";
2
+ export interface SnappedAxis {
3
+ cuts: number[];
4
+ /** Pixels each cut could move either way. */
5
+ radius: number;
6
+ }
7
+ /** Positions per axis a snapped cut may not take. */
8
+ export interface AvoidedCuts {
9
+ x: readonly number[];
10
+ y: readonly number[];
11
+ }
12
+ export declare function snapLatticeCuts(image: PixelBuffer, cutsX: readonly number[], cutsY: readonly number[], avoid?: AvoidedCuts): {
13
+ x: SnappedAxis;
14
+ y: SnappedAxis;
15
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * A lattice whose cuts each moved to the strongest colour step within `radius` pixels: the cell
3
+ * count of a regular grid with the cut positions of the raster that rounded it. A cut with no
4
+ * stronger step nearby, as in a flat border, stays where the lattice put it. A cut only moves
5
+ * where the cell before it stays within CELL_SPREAD of the pitch, so a strong edge inside a cell
6
+ * (a highlight, an outline) cannot squeeze one cell and stretch the next.
7
+ */
8
+ const CHANNELS = 3;
9
+ const STRIDE = 4;
10
+ const ALPHA_CHANNEL = 3;
11
+ const ALPHA_SCALE = 255;
12
+ /** Share of a cell a cut may move. */
13
+ const SNAP_SHARE = 0.25;
14
+ /** Share of the pitch a snapped cell may differ from it. */
15
+ const CELL_SPREAD = 0.25;
16
+ const NO_STEP = -1;
17
+ /**
18
+ * Colour step between each pixel column (or row) and the one before it, summed along the line, on
19
+ * premultiplied colour and alpha: a clear pixel's hidden colour (a removed background) is no edge.
20
+ */
21
+ function computeStepProfile(image, horizontal) {
22
+ const { width, height, data } = image;
23
+ const length = horizontal ? width : height;
24
+ const profile = new Float64Array(length);
25
+ for (let y = 0; y < height; y++)
26
+ for (let x = 0; x < width; x++) {
27
+ const along = horizontal ? x : y;
28
+ if (along === 0)
29
+ continue;
30
+ const o = (y * width + x) * STRIDE;
31
+ const p = horizontal ? o - STRIDE : o - width * STRIDE;
32
+ const a = data[o + ALPHA_CHANNEL] / ALPHA_SCALE, b = data[p + ALPHA_CHANNEL] / ALPHA_SCALE;
33
+ let step = Math.abs(data[o + ALPHA_CHANNEL] - data[p + ALPHA_CHANNEL]);
34
+ for (let c = 0; c < CHANNELS; c++)
35
+ step += Math.abs(data[o + c] * a - data[p + c] * b);
36
+ profile[along] += step;
37
+ }
38
+ return profile;
39
+ }
40
+ /** Every reading of one image snaps against the same profiles. */
41
+ const profiles = new WeakMap();
42
+ function stepProfiles(image) {
43
+ let cached = profiles.get(image);
44
+ if (!cached) {
45
+ cached = { x: computeStepProfile(image, true), y: computeStepProfile(image, false) };
46
+ profiles.set(image, cached);
47
+ }
48
+ return cached;
49
+ }
50
+ function snapAxis(profile, cuts, avoid) {
51
+ const cells = cuts.length - 1;
52
+ const pitch = (cuts[cells] - cuts[0]) / cells;
53
+ const radius = Math.max(1, Math.floor(pitch * SNAP_SHARE));
54
+ const avoided = new Set(avoid);
55
+ const strength = (at) => (avoided.has(at) ? NO_STEP : profile[at]);
56
+ // Rounded outwards: at a small pitch, whole pixels must still cover the cells' own spread.
57
+ const narrowest = Math.max(1, Math.floor((1 - CELL_SPREAD) * pitch)), widest = Math.ceil((1 + CELL_SPREAD) * pitch);
58
+ const snapped = [...cuts];
59
+ for (let i = 1; i < cells; i++) {
60
+ const low = Math.max(cuts[i] - radius, snapped[i - 1] + narrowest), high = Math.min(cuts[i] + radius, cuts[i + 1] - 1, snapped[i - 1] + widest);
61
+ if (low > high)
62
+ continue;
63
+ let best = Math.min(Math.max(cuts[i], low), high);
64
+ for (let at = low; at <= high; at++)
65
+ if (strength(at) > strength(best))
66
+ best = at;
67
+ snapped[i] = best;
68
+ }
69
+ return { cuts: snapped, radius };
70
+ }
71
+ export function snapLatticeCuts(image, cutsX, cutsY, avoid = { x: [], y: [] }) {
72
+ const profile = stepProfiles(image);
73
+ return {
74
+ x: snapAxis(profile.x, cutsX, avoid.x),
75
+ y: snapAxis(profile.y, cutsY, avoid.y),
76
+ };
77
+ }
@@ -0,0 +1,47 @@
1
+ import type { GridImageView } from "./grid-sampling.ts";
2
+ export declare const RESIDUAL_FLOOR = 16;
3
+ export declare enum CutModel {
4
+ /** One lattice per axis. */
5
+ Lattice = "lattice",
6
+ /** A lattice whose cuts each moved within a radius. */
7
+ Snapped = "snapped",
8
+ /** Every cut placed on its own. */
9
+ Free = "free"
10
+ }
11
+ export interface GridCuts {
12
+ cutsX: readonly number[];
13
+ cutsY: readonly number[];
14
+ model: CutModel;
15
+ /** Pixels a snapped cut could move either way, per axis. */
16
+ radiusX?: number;
17
+ radiusY?: number;
18
+ }
19
+ /** The rows a description covers and their premultiplied colours, shared by every grid. */
20
+ export interface DescribedImage {
21
+ /** The input inside its frame. */
22
+ content: GridImageView;
23
+ /** Where the content starts in the input. */
24
+ left: number;
25
+ top: number;
26
+ lines: Int32Array;
27
+ /** Premultiplied RGB of each described row, row after row. */
28
+ values: Float64Array;
29
+ samples: number;
30
+ /** Cost of one parameter, in nats. */
31
+ parameter: number;
32
+ }
33
+ /** The lines a description covers: every one, or as many evenly spread as PIXEL_BUDGET allows. */
34
+ export declare function describedLines(lines: number, length: number): Int32Array;
35
+ export declare function describeImage(input: GridImageView): DescribedImage;
36
+ /** Description length per sample (pixel channel of a described row), in nats. */
37
+ export declare function descriptionLength(described: DescribedImage, cuts: GridCuts): number;
38
+ /**
39
+ * Nats of a pixel's colour coded against a neighbour `step` (squared premultiplied distance) away,
40
+ * beyond the residual floor every sample pays, as `pixelLength` codes every pixel.
41
+ */
42
+ export declare function colourStepCost(step: number): number;
43
+ /**
44
+ * Description length per sample of the image as it is: one cell per pixel, which rebuilds it
45
+ * exactly, each pixel coded against its left or upper neighbour.
46
+ */
47
+ export declare function pixelLength(described: DescribedImage): number;