@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,13 @@
|
|
|
1
|
+
import type { PixelBuffer } from "./base.ts";
|
|
2
|
+
import { type PixelGridAnalysis, type PixelGridSamplingStage } from "./types.ts";
|
|
3
|
+
/** Internal image view. Its data stays in the source; only offset reads apply local shifts. */
|
|
4
|
+
export interface GridImageView extends PixelBuffer {
|
|
5
|
+
offset?: (x: number, y: number) => number;
|
|
6
|
+
sampling?: readonly PixelGridSamplingStage[];
|
|
7
|
+
}
|
|
8
|
+
/** Compose inverse steps in reverse order, keeping clamping at each intermediate image. */
|
|
9
|
+
export declare function shiftedGridImage(image: GridImageView, stage: PixelGridSamplingStage): GridImageView;
|
|
10
|
+
/** Materialize only when a consumer needs a regular RGBA buffer, such as final coloring. */
|
|
11
|
+
export declare function materializeGridImage(image: GridImageView): PixelBuffer;
|
|
12
|
+
/** Use the same rotation and local inverse mapping selected by Auto during final recovery. */
|
|
13
|
+
export declare function prepareGridSampling(image: PixelBuffer, grid: PixelGridAnalysis): PixelBuffer;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { straighten } from "./deskew.js";
|
|
2
|
+
import { PixelGridSamplingAxis, } from "./types.js";
|
|
3
|
+
const STRIDE = 4;
|
|
4
|
+
/** Retain Auto's existing confidence gate for local correction during coloring. */
|
|
5
|
+
const SAMPLING_MIN_CONFIDENCE = 0.35;
|
|
6
|
+
/** Bounded coordinate metadata, with no corrected RGBA allocation. */
|
|
7
|
+
const MAX_SHIFT_LOOKUP_ENTRIES = 1 << 18;
|
|
8
|
+
const MAX_INT32_COORDINATE = 2 ** 31 - 1;
|
|
9
|
+
/** Integer coordinates take the last interval starting at or before them, including ties. */
|
|
10
|
+
function axisIndex(boundaries, length) {
|
|
11
|
+
const index = new Int32Array(length);
|
|
12
|
+
for (let band = 1; band + 1 < boundaries.length; band++)
|
|
13
|
+
index.fill(band, Math.max(0, Math.ceil(boundaries[band])), band + 2 === boundaries.length ? length : Math.max(0, Math.ceil(boundaries[band + 1])));
|
|
14
|
+
return index;
|
|
15
|
+
}
|
|
16
|
+
/** Compose inverse steps in reverse order, keeping clamping at each intermediate image. */
|
|
17
|
+
export function shiftedGridImage(image, stage) {
|
|
18
|
+
const { width, height, data } = image;
|
|
19
|
+
const vertical = stage.axis === PixelGridSamplingAxis.Vertical;
|
|
20
|
+
const bands = axisIndex(stage.bands, vertical ? height : width);
|
|
21
|
+
const across = axisIndex(stage.across, vertical ? width : height);
|
|
22
|
+
const previous = image.offset;
|
|
23
|
+
const shifts = stage.shifts;
|
|
24
|
+
let offset;
|
|
25
|
+
if (vertical) {
|
|
26
|
+
offset = previous
|
|
27
|
+
? (x, y) => previous(x, Math.max(0, Math.min(height - 1, y + shifts[bands[y]][across[x]])))
|
|
28
|
+
: (x, y) => (Math.max(0, Math.min(height - 1, y + shifts[bands[y]][across[x]])) * width + x) * STRIDE;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
offset = previous
|
|
32
|
+
? (x, y) => previous(Math.max(0, Math.min(width - 1, x + shifts[bands[x]][across[y]])), y)
|
|
33
|
+
: (x, y) => (y * width + Math.max(0, Math.min(width - 1, x + shifts[bands[x]][across[y]]))) * STRIDE;
|
|
34
|
+
}
|
|
35
|
+
const length = vertical ? height : width;
|
|
36
|
+
const perpendicular = vertical ? width : height;
|
|
37
|
+
const intervals = stage.across.length - 1;
|
|
38
|
+
// Keep original reads for fractional/non-index coordinates and unsupported shift inputs.
|
|
39
|
+
const original = offset;
|
|
40
|
+
const matrixValid = stage.bands.length >= 2 &&
|
|
41
|
+
stage.across.length >= 2 &&
|
|
42
|
+
Array.isArray(stage.shifts) &&
|
|
43
|
+
stage.shifts.length === stage.bands.length - 1 &&
|
|
44
|
+
Array.from(stage.shifts).every((row) => row?.length === intervals);
|
|
45
|
+
if (matrixValid &&
|
|
46
|
+
Number.isSafeInteger(length) &&
|
|
47
|
+
length <= MAX_INT32_COORDINATE &&
|
|
48
|
+
Number.isSafeInteger(perpendicular) &&
|
|
49
|
+
length + 2 * stage.shifts.length + 2 * perpendicular + intervals <= MAX_SHIFT_LOOKUP_ENTRIES) {
|
|
50
|
+
const ids = new Int32Array(intervals);
|
|
51
|
+
const bySignature = new Map();
|
|
52
|
+
const patterns = [];
|
|
53
|
+
let valid = true;
|
|
54
|
+
for (let k = 0; k < intervals; k++) {
|
|
55
|
+
const pattern = stage.shifts.map((row) => row[k]);
|
|
56
|
+
if (!pattern.every(Number.isSafeInteger)) {
|
|
57
|
+
valid = false;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
const signature = pattern.join(",");
|
|
61
|
+
let id = bySignature.get(signature);
|
|
62
|
+
if (id === undefined) {
|
|
63
|
+
if ((patterns.length + 1) * (length + 2 * pattern.length) + 2 * perpendicular + intervals >
|
|
64
|
+
MAX_SHIFT_LOOKUP_ENTRIES) {
|
|
65
|
+
valid = false;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
id = patterns.length;
|
|
69
|
+
bySignature.set(signature, id);
|
|
70
|
+
patterns.push(pattern);
|
|
71
|
+
}
|
|
72
|
+
ids[k] = id;
|
|
73
|
+
}
|
|
74
|
+
if (valid) {
|
|
75
|
+
const maps = patterns.map((pattern) => {
|
|
76
|
+
const coordinates = new Int32Array(length);
|
|
77
|
+
for (let p = 0; p < length; p++)
|
|
78
|
+
coordinates[p] = Math.max(0, Math.min(length - 1, p + pattern[bands[p]]));
|
|
79
|
+
return coordinates;
|
|
80
|
+
});
|
|
81
|
+
const byLine = Array.from({ length: perpendicular }, (_, p) => maps[ids[across[p]]]);
|
|
82
|
+
if (vertical) {
|
|
83
|
+
offset = previous
|
|
84
|
+
? (x, y) => {
|
|
85
|
+
const at = byLine[x]?.[y];
|
|
86
|
+
return at === undefined ? original(x, y) : previous(x, at);
|
|
87
|
+
}
|
|
88
|
+
: (x, y) => {
|
|
89
|
+
const at = byLine[x]?.[y];
|
|
90
|
+
return at === undefined ? original(x, y) : (at * width + x) * STRIDE;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
offset = previous
|
|
95
|
+
? (x, y) => {
|
|
96
|
+
const at = byLine[y]?.[x];
|
|
97
|
+
return at === undefined ? original(x, y) : previous(at, y);
|
|
98
|
+
}
|
|
99
|
+
: (x, y) => {
|
|
100
|
+
const at = byLine[y]?.[x];
|
|
101
|
+
return at === undefined ? original(x, y) : (y * width + at) * STRIDE;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { width, height, data, sampling: [...(image.sampling ?? []), stage], offset };
|
|
107
|
+
}
|
|
108
|
+
/** Materialize only when a consumer needs a regular RGBA buffer, such as final coloring. */
|
|
109
|
+
export function materializeGridImage(image) {
|
|
110
|
+
const offset = image.offset;
|
|
111
|
+
if (!offset)
|
|
112
|
+
return image;
|
|
113
|
+
const data = new Uint8ClampedArray(image.width * image.height * STRIDE);
|
|
114
|
+
for (let y = 0; y < image.height; y++)
|
|
115
|
+
for (let x = 0; x < image.width; x++) {
|
|
116
|
+
const from = offset(x, y);
|
|
117
|
+
const to = (y * image.width + x) * STRIDE;
|
|
118
|
+
for (let channel = 0; channel < STRIDE; channel++)
|
|
119
|
+
data[to + channel] = image.data[from + channel];
|
|
120
|
+
}
|
|
121
|
+
return { width: image.width, height: image.height, data };
|
|
122
|
+
}
|
|
123
|
+
/** Use the same rotation and local inverse mapping selected by Auto during final recovery. */
|
|
124
|
+
export function prepareGridSampling(image, grid) {
|
|
125
|
+
const upright = grid.rotation ? straighten(image, grid.rotation) : image;
|
|
126
|
+
if (!grid.sampling?.length || grid.confidence < SAMPLING_MIN_CONFIDENCE)
|
|
127
|
+
return upright;
|
|
128
|
+
let view = upright;
|
|
129
|
+
for (const stage of grid.sampling)
|
|
130
|
+
view = shiftedGridImage(view, stage);
|
|
131
|
+
return materializeGridImage(view);
|
|
132
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type GridImageView } from "./grid-sampling.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Pitch factors at which the chance evidence is measured: a quarter octave either side, clear of
|
|
4
|
+
* the grid's octaves and of its neighbouring cell counts.
|
|
5
|
+
*/
|
|
6
|
+
export declare const OFF_PITCH_FACTORS: readonly [number, number];
|
|
7
|
+
/** Per described line, prefix sums along it of the premultiplied colour and of its squared norm. */
|
|
8
|
+
export interface LinePrefix {
|
|
9
|
+
length: number;
|
|
10
|
+
/** The described lines, ascending. */
|
|
11
|
+
lines: Int32Array;
|
|
12
|
+
sums: Float64Array;
|
|
13
|
+
}
|
|
14
|
+
export declare function linePrefix(image: GridImageView, vertical: boolean): LinePrefix;
|
|
15
|
+
/** The image's line prefixes along each axis, computed once for every grid read on it. */
|
|
16
|
+
export declare class StripImage {
|
|
17
|
+
readonly image: GridImageView;
|
|
18
|
+
private readonly prefixes;
|
|
19
|
+
constructor(image: GridImageView);
|
|
20
|
+
prefix(vertical: boolean): LinePrefix;
|
|
21
|
+
}
|
|
22
|
+
/** Squared deviation from their mean of the pixels [from, to) of one line. */
|
|
23
|
+
export declare function intervalEnergy(sums: Float64Array, base: number, from: number, to: number): number;
|
|
24
|
+
/** A grid read through strip shifts. */
|
|
25
|
+
export interface StripShear {
|
|
26
|
+
/** The image with every strip moved back by its shift. */
|
|
27
|
+
straight: GridImageView;
|
|
28
|
+
/** Nats the shifts cost: each strip names one of its options. */
|
|
29
|
+
cost: number;
|
|
30
|
+
/** The image straightened by the anti-phase search, for the twin. */
|
|
31
|
+
twin: () => GridImageView;
|
|
32
|
+
}
|
|
33
|
+
/** The grid's strip shifts, or null when no strip moves. */
|
|
34
|
+
export declare function stripShear(strips: StripImage, cutsX: readonly number[], cutsY: readonly number[]): StripShear | null;
|
|
35
|
+
/** Uniform cuts over the same extent at `factor` times the pitch. */
|
|
36
|
+
export declare function offPitch(cuts: readonly number[], factor: number): number[] | null;
|
|
37
|
+
export type ChanceEvidenceStop = (lowerBound: number) => boolean;
|
|
38
|
+
/** Raw per-axis/per-factor measurements; totals depend on the caller's incumbent. */
|
|
39
|
+
export type ChanceEvidenceFragments = (number | undefined)[];
|
|
40
|
+
/**
|
|
41
|
+
* Phase evidence the strip search finds without the grid, per sample: on each axis, the most it
|
|
42
|
+
* finds at the off-pitch factors, averaged over the two axes.
|
|
43
|
+
*/
|
|
44
|
+
export declare function chanceEvidence(strips: StripImage, cutsX: readonly number[], cutsY: readonly number[], stopIfCannotWin?: ChanceEvidenceStop, fragments?: ChanceEvidenceFragments): number;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { RESIDUAL_FLOOR, describedLines, } from "./grid-description-length.js";
|
|
2
|
+
import { shiftedGridImage } from "./grid-sampling.js";
|
|
3
|
+
import { PixelGridSamplingAxis } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Strip shifts of a grid. Every strip of cells (a row of cells for the column cuts, a column of
|
|
6
|
+
* cells for the row cuts) may move the cuts across it by whole pixels, within half a cell, as a
|
|
7
|
+
* warped, sheared or row-jittered upscale displaces them. Each strip takes the shift that leaves
|
|
8
|
+
* the least variance inside its cells, and moving every strip back by its shift straightens the
|
|
9
|
+
* grid. The column cuts are settled first, the row cuts then on the straightened columns. The
|
|
10
|
+
* variance is read on the lines a description covers, so a large image costs no more than its
|
|
11
|
+
* description.
|
|
12
|
+
*
|
|
13
|
+
* The same search with every strip confined to the anti-phase of its shift (a quarter to three
|
|
14
|
+
* quarters of a cell away) straightens the twin. Because every strip picks its shift from the
|
|
15
|
+
* image, the search finds some phase evidence on any content; `chanceEvidence` measures how much
|
|
16
|
+
* at pitches the grid does not have.
|
|
17
|
+
*/
|
|
18
|
+
const STRIDE = 4;
|
|
19
|
+
const CHANNELS = 3;
|
|
20
|
+
const ALPHA_SCALE = 255;
|
|
21
|
+
/** The anti-phase window of a shift, in cells. */
|
|
22
|
+
const ANTI_PHASE_FROM = 0.25;
|
|
23
|
+
const ANTI_PHASE_TO = 0.75;
|
|
24
|
+
const HALF = 0.5;
|
|
25
|
+
/**
|
|
26
|
+
* Pitch factors at which the chance evidence is measured: a quarter octave either side, clear of
|
|
27
|
+
* the grid's octaves and of its neighbouring cell counts.
|
|
28
|
+
*/
|
|
29
|
+
export const OFF_PITCH_FACTORS = [2 ** 0.25, 2 ** -0.25];
|
|
30
|
+
/** Cells and pixels per cell an off-pitch lattice needs to be searched. */
|
|
31
|
+
const MIN_OFF_PITCH_CELLS = 3;
|
|
32
|
+
const MIN_OFF_PITCH = 1.5;
|
|
33
|
+
const TIE = 1e-9;
|
|
34
|
+
export function linePrefix(image, vertical) {
|
|
35
|
+
const { width, height, data, offset } = image;
|
|
36
|
+
const length = vertical ? height : width;
|
|
37
|
+
const lines = describedLines(vertical ? width : height, length);
|
|
38
|
+
const sums = new Float64Array(lines.length * (length + 1) * (CHANNELS + 1));
|
|
39
|
+
for (let k = 0; k < lines.length; k++) {
|
|
40
|
+
const line = lines[k], base = k * (length + 1) * (CHANNELS + 1);
|
|
41
|
+
for (let p = 0; p < length; p++) {
|
|
42
|
+
const o = offset
|
|
43
|
+
? vertical
|
|
44
|
+
? offset(line, p)
|
|
45
|
+
: offset(p, line)
|
|
46
|
+
: (vertical ? p * width + line : line * width + p) * STRIDE;
|
|
47
|
+
const alpha = data[o + CHANNELS] / ALPHA_SCALE;
|
|
48
|
+
const at = base + (p + 1) * (CHANNELS + 1), previous = base + p * (CHANNELS + 1);
|
|
49
|
+
if (alpha === 0) {
|
|
50
|
+
sums[at] = sums[previous];
|
|
51
|
+
sums[at + 1] = sums[previous + 1];
|
|
52
|
+
sums[at + 2] = sums[previous + 2];
|
|
53
|
+
sums[at + CHANNELS] = sums[previous + CHANNELS];
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const red = data[o] * alpha, green = data[o + 1] * alpha, blue = data[o + 2] * alpha;
|
|
57
|
+
sums[at] = sums[previous] + red;
|
|
58
|
+
sums[at + 1] = sums[previous + 1] + green;
|
|
59
|
+
sums[at + 2] = sums[previous + 2] + blue;
|
|
60
|
+
let square = 0;
|
|
61
|
+
square += red * red;
|
|
62
|
+
square += green * green;
|
|
63
|
+
square += blue * blue;
|
|
64
|
+
sums[at + CHANNELS] = sums[previous + CHANNELS] + square;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { length, lines, sums };
|
|
68
|
+
}
|
|
69
|
+
/** The image's line prefixes along each axis, computed once for every grid read on it. */
|
|
70
|
+
export class StripImage {
|
|
71
|
+
image;
|
|
72
|
+
prefixes = [null, null];
|
|
73
|
+
constructor(image) {
|
|
74
|
+
this.image = image;
|
|
75
|
+
}
|
|
76
|
+
prefix(vertical) {
|
|
77
|
+
const axis = Number(vertical);
|
|
78
|
+
return (this.prefixes[axis] ??= linePrefix(this.image, vertical));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Squared deviation from their mean of the pixels [from, to) of one line. */
|
|
82
|
+
export function intervalEnergy(sums, base, from, to) {
|
|
83
|
+
if (to <= from)
|
|
84
|
+
return 0;
|
|
85
|
+
const count = to - from, start = base + from * (CHANNELS + 1), end = base + to * (CHANNELS + 1);
|
|
86
|
+
let energy = sums[end + CHANNELS] - sums[start + CHANNELS];
|
|
87
|
+
const red = sums[end] - sums[start], green = sums[end + 1] - sums[start + 1], blue = sums[end + 2] - sums[start + 2];
|
|
88
|
+
if (energy === 0 && red === 0 && green === 0 && blue === 0)
|
|
89
|
+
return energy;
|
|
90
|
+
energy -= (red * red) / count;
|
|
91
|
+
energy -= (green * green) / count;
|
|
92
|
+
energy -= (blue * blue) / count;
|
|
93
|
+
return energy;
|
|
94
|
+
}
|
|
95
|
+
const pitchOf = (cuts) => (cuts[cuts.length - 1] - cuts[0]) / (cuts.length - 1);
|
|
96
|
+
/** Index of the first described line at or after `line`. */
|
|
97
|
+
function lineIndex(lines, line) {
|
|
98
|
+
let low = 0, high = lines.length;
|
|
99
|
+
while (low < high) {
|
|
100
|
+
const middle = (low + high) >> 1;
|
|
101
|
+
if (lines[middle] < line)
|
|
102
|
+
low = middle + 1;
|
|
103
|
+
else
|
|
104
|
+
high = middle;
|
|
105
|
+
}
|
|
106
|
+
return low;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* For each strip of lines between consecutive `across` cuts, the shift of the inner `cuts` that
|
|
110
|
+
* leaves the least energy inside the cells. With `anchor`, each strip's shift is confined to the
|
|
111
|
+
* anti-phase window of the anchor's.
|
|
112
|
+
*/
|
|
113
|
+
function stripShifts(prefix, cuts, across, anchor) {
|
|
114
|
+
const { length, lines, sums } = prefix;
|
|
115
|
+
const cells = cuts.length - 1;
|
|
116
|
+
const pitch = pitchOf(cuts);
|
|
117
|
+
const low = -Math.floor((pitch - 1) * HALF), high = Math.floor(pitch * HALF);
|
|
118
|
+
const shifts = [];
|
|
119
|
+
const positions = new Float64Array(cells + 1);
|
|
120
|
+
let energy = 0;
|
|
121
|
+
for (let k = 0; k + 1 < across.length; k++) {
|
|
122
|
+
const from = anchor ? Math.ceil(anchor[k] + ANTI_PHASE_FROM * pitch) : low, to = anchor ? Math.floor(anchor[k] + ANTI_PHASE_TO * pitch) : high;
|
|
123
|
+
const first = lineIndex(lines, across[k]), last = lineIndex(lines, across[k + 1]);
|
|
124
|
+
let best = from, least = Infinity;
|
|
125
|
+
for (let shift = from; shift <= to; shift++) {
|
|
126
|
+
for (let i = 1; i <= cells; i++) {
|
|
127
|
+
const at = i === cells ? length : Math.max(0, Math.min(length, cuts[i] + shift));
|
|
128
|
+
positions[i] = Math.max(positions[i - 1], at);
|
|
129
|
+
}
|
|
130
|
+
let sum = 0;
|
|
131
|
+
for (let line = first; line < last; line++) {
|
|
132
|
+
const base = line * (length + 1) * (CHANNELS + 1);
|
|
133
|
+
for (let i = 1; i <= cells; i++)
|
|
134
|
+
sum += intervalEnergy(sums, base, positions[i - 1], positions[i]);
|
|
135
|
+
}
|
|
136
|
+
const nearer = !anchor && Math.abs(shift) < Math.abs(best);
|
|
137
|
+
if (sum < least - TIE || (sum <= least + TIE && nearer)) {
|
|
138
|
+
best = shift;
|
|
139
|
+
least = sum;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
shifts.push(best);
|
|
143
|
+
energy += least;
|
|
144
|
+
}
|
|
145
|
+
return { shifts, energy, options: high - low + 1 };
|
|
146
|
+
}
|
|
147
|
+
/** The image with each strip moved back by its shift: out(p) = in(p + shift). */
|
|
148
|
+
function unshear(image, vertical, across, shifts) {
|
|
149
|
+
return shiftedGridImage(image, {
|
|
150
|
+
axis: vertical ? PixelGridSamplingAxis.Vertical : PixelGridSamplingAxis.Horizontal,
|
|
151
|
+
bands: [0, vertical ? image.height : image.width],
|
|
152
|
+
across,
|
|
153
|
+
shifts: [shifts],
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const moved = (shifts) => shifts.some(Boolean);
|
|
157
|
+
/** The grid's strip shifts, or null when no strip moves. */
|
|
158
|
+
export function stripShear(strips, cutsX, cutsY) {
|
|
159
|
+
const { image } = strips;
|
|
160
|
+
const columns = stripShifts(strips.prefix(false), cutsX, cutsY);
|
|
161
|
+
const once = moved(columns.shifts) ? unshear(image, false, cutsY, columns.shifts) : image;
|
|
162
|
+
const rows = stripShifts(once === image ? strips.prefix(true) : linePrefix(once, true), cutsY, cutsX);
|
|
163
|
+
if (!moved(columns.shifts) && !moved(rows.shifts))
|
|
164
|
+
return null;
|
|
165
|
+
const straight = moved(rows.shifts) ? unshear(once, true, cutsX, rows.shifts) : once;
|
|
166
|
+
const cost = (cutsY.length - 1) * Math.log(columns.options) + (cutsX.length - 1) * Math.log(rows.options);
|
|
167
|
+
const twin = () => {
|
|
168
|
+
const columnTwin = stripShifts(strips.prefix(false), cutsX, cutsY, columns.shifts);
|
|
169
|
+
const twinOnce = unshear(image, false, cutsY, columnTwin.shifts);
|
|
170
|
+
const rowTwin = stripShifts(linePrefix(twinOnce, true), cutsY, cutsX, rows.shifts);
|
|
171
|
+
return unshear(twinOnce, true, cutsX, rowTwin.shifts);
|
|
172
|
+
};
|
|
173
|
+
return { straight, cost, twin };
|
|
174
|
+
}
|
|
175
|
+
/** Uniform cuts over the same extent at `factor` times the pitch. */
|
|
176
|
+
export function offPitch(cuts, factor) {
|
|
177
|
+
const first = cuts[0], last = cuts[cuts.length - 1];
|
|
178
|
+
const cells = Math.round((cuts.length - 1) / factor);
|
|
179
|
+
if (cells < MIN_OFF_PITCH_CELLS || (last - first) / cells < MIN_OFF_PITCH)
|
|
180
|
+
return null;
|
|
181
|
+
return Array.from({ length: cells + 1 }, (_, i) => Math.round(first + ((last - first) * i) / cells));
|
|
182
|
+
}
|
|
183
|
+
/** Phase evidence, per sample, of one axis's strip search: its shifts against their anti-phase. */
|
|
184
|
+
function axisEvidence(prefix, cuts, across) {
|
|
185
|
+
const samples = prefix.lines.length * prefix.length * CHANNELS;
|
|
186
|
+
const best = stripShifts(prefix, cuts, across);
|
|
187
|
+
const anti = stripShifts(prefix, cuts, across, best.shifts);
|
|
188
|
+
return (HALF *
|
|
189
|
+
Math.log((anti.energy / samples + RESIDUAL_FLOOR) / (best.energy / samples + RESIDUAL_FLOOR)));
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Phase evidence the strip search finds without the grid, per sample: on each axis, the most it
|
|
193
|
+
* finds at the off-pitch factors, averaged over the two axes.
|
|
194
|
+
*/
|
|
195
|
+
export function chanceEvidence(strips, cutsX, cutsY, stopIfCannotWin, fragments) {
|
|
196
|
+
const axes = [false, true];
|
|
197
|
+
let total = 0, remaining = axes.length * OFF_PITCH_FACTORS.length, slot = 0;
|
|
198
|
+
for (const vertical of axes) {
|
|
199
|
+
const cuts = vertical ? cutsY : cutsX, across = vertical ? cutsX : cutsY;
|
|
200
|
+
let most = 0;
|
|
201
|
+
for (const factor of OFF_PITCH_FACTORS) {
|
|
202
|
+
const off = offPitch(cuts, factor);
|
|
203
|
+
if (off) {
|
|
204
|
+
let value = fragments?.[slot];
|
|
205
|
+
if (value === undefined) {
|
|
206
|
+
value = axisEvidence(strips.prefix(vertical), off, across);
|
|
207
|
+
if (fragments)
|
|
208
|
+
fragments[slot] = value;
|
|
209
|
+
}
|
|
210
|
+
most = Math.max(most, value);
|
|
211
|
+
}
|
|
212
|
+
slot++;
|
|
213
|
+
remaining--;
|
|
214
|
+
if (stopIfCannotWin && remaining > 0) {
|
|
215
|
+
const lowerBound = (total + most) * HALF;
|
|
216
|
+
if (Number.isFinite(lowerBound) && lowerBound >= 0 && stopIfCannotWin(lowerBound))
|
|
217
|
+
return Infinity;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
total += most;
|
|
221
|
+
}
|
|
222
|
+
return total * HALF;
|
|
223
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { PixelBuffer } from "./base.ts";
|
|
2
|
+
/** A flat-coloured blocky sprite: background, then overlapping rectangles and ellipses. */
|
|
3
|
+
export declare function sprite(size: number, seed?: number): PixelBuffer;
|
|
4
|
+
/**
|
|
5
|
+
* Detailed pixel art: a dithered two-tone sky, outlined blobs with a lit and a shaded tone,
|
|
6
|
+
* single-pixel highlights and scattered speckles, so many neighbouring cells share a colour while
|
|
7
|
+
* others change every pixel.
|
|
8
|
+
*/
|
|
9
|
+
export declare function detailedSprite(width: number, height: number, seed?: number): PixelBuffer;
|
|
10
|
+
/** Non-integer square nearest upscale: cells alternate between two integer widths. */
|
|
11
|
+
export declare function fractionalUpscale(source: PixelBuffer, scale: number): PixelBuffer;
|
|
12
|
+
/** Nearest upscale whose cell size drifts smoothly across the image (a non-uniform grid). */
|
|
13
|
+
export declare function driftUpscale(source: PixelBuffer, scale: number, drift: number, seed?: number): PixelBuffer;
|
|
14
|
+
/** Nearest upscale whose every inner cut moves by up to `jitter` cells, as pixel-bench's jitter. */
|
|
15
|
+
export declare function jitterUpscale(source: PixelBuffer, scale: number, jitter: number, seed?: number): PixelBuffer;
|
|
16
|
+
export declare function checkerboard(cell: number, cells: number): PixelBuffer;
|
|
17
|
+
/** Nearest-neighbour upscale by a possibly fractional factor. */
|
|
18
|
+
export declare function upscaleNearest(source: PixelBuffer, fx: number, fy?: number): PixelBuffer;
|
|
19
|
+
export declare function upscaleBilinear(source: PixelBuffer, scale: number): PixelBuffer;
|
|
20
|
+
/** Bilinear resample with pixel-centre alignment (as image libraries do), by a possibly fractional factor. */
|
|
21
|
+
export declare function resampleBilinear(source: PixelBuffer, scale: number): PixelBuffer;
|
|
22
|
+
/** pixel-bench's soft_bilinear: a centred bilinear upscale, then a Gaussian blur. */
|
|
23
|
+
export declare function softBilinear(source: PixelBuffer, scale: number, blur?: number): PixelBuffer;
|
|
24
|
+
/**
|
|
25
|
+
* pixel-bench's mush: a centred bilinear upscale displaced by a smooth random field of up to
|
|
26
|
+
* `amplitude` pixels (correlated over about a cell), a soft-focus blend and a Gaussian blur.
|
|
27
|
+
*/
|
|
28
|
+
export declare function mushWarp(source: PixelBuffer, scale: number, seed?: number, amplitude?: number, blur?: number): PixelBuffer;
|
|
29
|
+
/**
|
|
30
|
+
* pixel-bench's warp on a nearest upscale: a smooth shear that moves every row along x and every
|
|
31
|
+
* column along y by two sine waves of up to `amplitude` pixels.
|
|
32
|
+
*/
|
|
33
|
+
export declare function warpUpscale(source: PixelBuffer, scale: number, amplitude: number, seed?: number): PixelBuffer;
|
|
34
|
+
/**
|
|
35
|
+
* pixel-bench's row_jitter: a nearest upscale whose every row of cells moves along x, and every
|
|
36
|
+
* column of cells along y, by up to `amplitude` pixels.
|
|
37
|
+
*/
|
|
38
|
+
export declare function rowJitterUpscale(source: PixelBuffer, scale: number, amplitude: number, seed?: number): PixelBuffer;
|
|
39
|
+
/**
|
|
40
|
+
* pixel-bench's block_reset: a nearest upscale split into `blocks` by `blocks` parts, each of which
|
|
41
|
+
* samples the source at its own phase, up to half a cell either way on both axes.
|
|
42
|
+
*/
|
|
43
|
+
export declare function blockResetUpscale(source: PixelBuffer, scale: number, blocks: number, seed?: number): PixelBuffer;
|
|
44
|
+
/**
|
|
45
|
+
* pixel-bench's cell_texture: a nearest upscale under a smooth multiplicative texture (correlated
|
|
46
|
+
* over a fifth of a cell) of standard deviation `amplitude`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function texturedUpscale(source: PixelBuffer, scale: number, amplitude: number, seed?: number): PixelBuffer;
|
|
49
|
+
/** A soft "AI upscale": centred bilinear, a further blur and mild noise. */
|
|
50
|
+
export declare function mush(source: PixelBuffer, scale: number, seed?: number): PixelBuffer;
|
|
51
|
+
/** Separable box blur with the given radius (radius 1 smears each edge over about 3 pixels). */
|
|
52
|
+
export declare function boxBlur(source: PixelBuffer, radius: number): PixelBuffer;
|
|
53
|
+
export declare function gradient(width: number, height: number): PixelBuffer;
|
|
54
|
+
/** Smooth, photo-like colour field: low-frequency waves plus a few soft blobs, no hard edges. */
|
|
55
|
+
export declare function smoothPhoto(width: number, height: number): PixelBuffer;
|
|
56
|
+
/** Textured "photo": noise at several scales, so it has edges everywhere. */
|
|
57
|
+
export declare function texturePhoto(width: number, height: number, seed?: number): PixelBuffer;
|
|
58
|
+
export declare function addNoise(source: PixelBuffer, amplitude: number, seed?: number): PixelBuffer;
|
|
59
|
+
/** Cells of 15 to 17 pixels with a margin, like a hand-resized sprite. */
|
|
60
|
+
export declare function jitterWithMargin(source: PixelBuffer, margin?: number, seed?: number): PixelBuffer;
|
|
61
|
+
/** A smooth backdrop with hard-edged, anti-aliased shapes at arbitrary positions (no pixel grid). */
|
|
62
|
+
export declare function vectorShapes(width: number, height: number, seed?: number): PixelBuffer;
|
|
63
|
+
/**
|
|
64
|
+
* A generated sprite: `art` on a clear margin of `margin` cells, drawn with cells of 1 ± `spread`
|
|
65
|
+
* of `pitch`, each on its own, then softened by a box blur of `blur` pixels. No lattice fits it.
|
|
66
|
+
*/
|
|
67
|
+
export declare function generatedSprite(art: PixelBuffer, pitch: number, spread: number, margin?: number, blur?: number, seed?: number): PixelBuffer;
|
|
68
|
+
/** A sprite on a clear background: `sprite` with its background colour made transparent. */
|
|
69
|
+
export declare function clearSprite(size: number, seed?: number): PixelBuffer;
|