@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,15 @@
|
|
|
1
|
+
import type { PixelBuffer } from "../base.ts";
|
|
2
|
+
import { LineGuideMode, type PixelationSize } from "../types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A recent PixelOE result for `image` under `variant` (the step and its settings), else computed
|
|
5
|
+
* and remembered. Callers get their own copy of the pixels.
|
|
6
|
+
*/
|
|
7
|
+
export declare function rememberPixeloe(image: PixelBuffer, variant: string, compute: () => PixelBuffer): PixelBuffer;
|
|
8
|
+
/** Stock PixelOE pixelize (optionally line-guided) of the whole image to `size`. */
|
|
9
|
+
export declare function runPixeloe(image: PixelBuffer, grid: PixelationSize, guide?: LineGuideMode): PixelBuffer;
|
|
10
|
+
/**
|
|
11
|
+
* The binarised XDoG line mask at the image's own size (1 = line), for the image -> image PixelOE
|
|
12
|
+
* + lines pre-pass. Made like the stock guide (Otsu cut of the XDoG drawing at no more than 512 px on
|
|
13
|
+
* the long side), then enlarged to the image by nearest neighbour. Null when the drawing is uniform.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sourceLineMask(image: PixelBuffer): Float32Array | null;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { resizeArea8 } from "./cv-ops.js";
|
|
2
|
+
import { legacyPixeloe, legacyPixeloeSizes, } from "./pixeloe-legacy.js";
|
|
3
|
+
import { otsuThreshold, xdogLines } from "./xdog.js";
|
|
4
|
+
import { resizeBox } from "../resize.js";
|
|
5
|
+
import { LineGuideMode } from "../types.js";
|
|
6
|
+
/**
|
|
7
|
+
* The line drawing is made at no more than this long side (or the working size, if larger): the
|
|
8
|
+
* XDoG parameters were tuned on sources of this size, where its 0.8 px blur finds drawn outlines
|
|
9
|
+
* rather than texture.
|
|
10
|
+
*/
|
|
11
|
+
const LINE_SOURCE_MAX = 512;
|
|
12
|
+
const OPAQUE = 255;
|
|
13
|
+
const ALPHA_CUTOFF = 128;
|
|
14
|
+
const PAPER = 255;
|
|
15
|
+
/** Recent results, so changing only colour settings does not rerun PixelOE. */
|
|
16
|
+
const MEMO_SIZE = 4;
|
|
17
|
+
/** Larger results (full-size pre-passes of big photos, 8 MB and up) are not kept. */
|
|
18
|
+
const MAX_REMEMBERED_PIXELS = 2_097_152;
|
|
19
|
+
const memo = [];
|
|
20
|
+
const FNV_OFFSET = 0x811c9dc5;
|
|
21
|
+
const FNV_PRIME = 0x01000193;
|
|
22
|
+
const MIX_PRIME = 0x9e3779b1;
|
|
23
|
+
/** Two independent 32-bit hashes of the pixels (FNV-1a and a multiplicative mix). */
|
|
24
|
+
function fingerprint(data) {
|
|
25
|
+
const bytes = data.byteOffset % 4 ? data.slice() : data;
|
|
26
|
+
const words = new Uint32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >> 2);
|
|
27
|
+
let a = FNV_OFFSET, b = 0;
|
|
28
|
+
for (let i = 0; i < words.length; i++) {
|
|
29
|
+
a = Math.imul(a ^ words[i], FNV_PRIME);
|
|
30
|
+
b = Math.imul(b + words[i], MIX_PRIME) ^ (b >>> 15);
|
|
31
|
+
}
|
|
32
|
+
return `${(a >>> 0).toString(36)}${(b >>> 0).toString(36)}`;
|
|
33
|
+
}
|
|
34
|
+
/** RGB composited over white (transparent pixels would otherwise read as dark outlines). */
|
|
35
|
+
function opaqueRgb(image) {
|
|
36
|
+
const { data } = image;
|
|
37
|
+
const rgb = new Uint8Array(image.width * image.height * 3);
|
|
38
|
+
let opaque = true;
|
|
39
|
+
for (let p = 0, i = 0; i < data.length; i += 4, p += 3) {
|
|
40
|
+
const alpha = data[i + 3];
|
|
41
|
+
if (alpha !== OPAQUE)
|
|
42
|
+
opaque = false;
|
|
43
|
+
for (let c = 0; c < 3; c++)
|
|
44
|
+
rgb[p + c] =
|
|
45
|
+
alpha === OPAQUE
|
|
46
|
+
? data[i + c]
|
|
47
|
+
: Math.round((data[i + c] * alpha + PAPER * (OPAQUE - alpha)) / OPAQUE);
|
|
48
|
+
}
|
|
49
|
+
return { rgb, opaque };
|
|
50
|
+
}
|
|
51
|
+
function lineGuide(rgb, width, height, workLongSide, mode) {
|
|
52
|
+
const limit = Math.max(LINE_SOURCE_MAX, workLongSide);
|
|
53
|
+
const scale = Math.min(1, limit / Math.max(width, height));
|
|
54
|
+
const lw = Math.max(1, Math.round(width * scale)), lh = Math.max(1, Math.round(height * scale));
|
|
55
|
+
const source = scale < 1 ? resizeArea8(rgb, width, height, 3, lw, lh) : rgb;
|
|
56
|
+
const lines = xdogLines(source, lw, lh);
|
|
57
|
+
return { lines, width: lw, height: lh, threshold: otsuThreshold(lines), mode };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A recent PixelOE result for `image` under `variant` (the step and its settings), else computed
|
|
61
|
+
* and remembered. Callers get their own copy of the pixels.
|
|
62
|
+
*/
|
|
63
|
+
export function rememberPixeloe(image, variant, compute) {
|
|
64
|
+
const key = `${image.width}x${image.height}:${fingerprint(image.data)}:${variant}`;
|
|
65
|
+
const known = memo.find((entry) => entry.key === key);
|
|
66
|
+
if (known)
|
|
67
|
+
return { ...known.result, data: known.result.data.slice() };
|
|
68
|
+
const result = compute();
|
|
69
|
+
if (result.width * result.height > MAX_REMEMBERED_PIXELS)
|
|
70
|
+
return result;
|
|
71
|
+
memo.unshift({ key, result: { ...result, data: result.data.slice() } });
|
|
72
|
+
memo.length = Math.min(memo.length, MEMO_SIZE);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
/** Stock PixelOE pixelize (optionally line-guided) of the whole image to `size`. */
|
|
76
|
+
export function runPixeloe(image, grid, guide) {
|
|
77
|
+
return rememberPixeloe(image, `${grid.width}x${grid.height}:${guide ?? ""}`, () => pixeloe(image, grid, guide));
|
|
78
|
+
}
|
|
79
|
+
function pixeloe(image, grid, guide) {
|
|
80
|
+
const { width, height } = image;
|
|
81
|
+
const { rgb, opaque } = opaqueRgb(image);
|
|
82
|
+
const sizes = legacyPixeloeSizes(width, height, grid.width, grid.height);
|
|
83
|
+
const result = legacyPixeloe(rgb, width, height, {
|
|
84
|
+
width: grid.width,
|
|
85
|
+
height: grid.height,
|
|
86
|
+
guide: guide === undefined
|
|
87
|
+
? undefined
|
|
88
|
+
: lineGuide(rgb, width, height, Math.max(...sizes.work), guide),
|
|
89
|
+
});
|
|
90
|
+
const data = new Uint8ClampedArray(result.width * result.height * 4);
|
|
91
|
+
const alpha = opaque ? null : resizeBox(image, result.width, result.height, "straight").data;
|
|
92
|
+
for (let p = 0, i = 0; p < result.rgb.length; p += 3, i += 4) {
|
|
93
|
+
data[i] = result.rgb[p];
|
|
94
|
+
data[i + 1] = result.rgb[p + 1];
|
|
95
|
+
data[i + 2] = result.rgb[p + 2];
|
|
96
|
+
// Pixel art keeps hard edges: a cell is either drawn or empty.
|
|
97
|
+
data[i + 3] = alpha && alpha[i + 3] < ALPHA_CUTOFF ? 0 : OPAQUE;
|
|
98
|
+
}
|
|
99
|
+
return { width: result.width, height: result.height, data };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The binarised XDoG line mask at the image's own size (1 = line), for the image -> image PixelOE
|
|
103
|
+
* + lines pre-pass. Made like the stock guide (Otsu cut of the XDoG drawing at no more than 512 px on
|
|
104
|
+
* the long side), then enlarged to the image by nearest neighbour. Null when the drawing is uniform.
|
|
105
|
+
*/
|
|
106
|
+
export function sourceLineMask(image) {
|
|
107
|
+
const { width, height } = image;
|
|
108
|
+
const { rgb } = opaqueRgb(image);
|
|
109
|
+
const { lines, width: lw, height: lh, threshold, } = lineGuide(rgb, width, height, 0, LineGuideMode.Union);
|
|
110
|
+
if (threshold === null)
|
|
111
|
+
return null;
|
|
112
|
+
const cut = Math.fround(threshold);
|
|
113
|
+
const mask = new Float32Array(width * height);
|
|
114
|
+
for (let y = 0; y < height; y++) {
|
|
115
|
+
const row = Math.min(lh - 1, Math.floor(((y + 0.5) * lh) / height)) * lw;
|
|
116
|
+
for (let x = 0; x < width; x++) {
|
|
117
|
+
const v = lines[row + Math.min(lw - 1, Math.floor(((x + 0.5) * lw) / width))];
|
|
118
|
+
mask[y * width + x] = Math.fround(v / 255) < cut ? 1 : 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return mask;
|
|
122
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { LineGuideMode } from "../types.ts";
|
|
2
|
+
export interface LegacyPixeloeOptions {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
thickness?: number;
|
|
6
|
+
/** Line image (one byte per pixel, 255 = paper), its size and its 0..1 line threshold. */
|
|
7
|
+
guide?: LineGuide;
|
|
8
|
+
}
|
|
9
|
+
export interface LineGuide {
|
|
10
|
+
lines: Uint8Array;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
/** Values below `threshold * 255` are line; null when the drawing has no lines at all. */
|
|
14
|
+
threshold: number | null;
|
|
15
|
+
mode: LineGuideMode;
|
|
16
|
+
}
|
|
17
|
+
export interface PixeloeSizes {
|
|
18
|
+
work: [number, number];
|
|
19
|
+
out: [number, number];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Use PixelOE's own sizing when some `target_size` gives exactly the requested output (then the
|
|
23
|
+
* result is the library's), otherwise work at the requested size x patch size.
|
|
24
|
+
*/
|
|
25
|
+
export declare function legacyPixeloeSizes(w: number, h: number, ow: number, oh: number): PixeloeSizes;
|
|
26
|
+
/** Interleaved RGB at the source size to interleaved RGB at the requested output size. */
|
|
27
|
+
export declare function legacyPixeloe(rgb: Uint8Array, w: number, h: number, options: LegacyPixeloeOptions): {
|
|
28
|
+
rgb: Uint8Array;
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
};
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { labToRgb8, rgbToLab8 } from "./cv-lab.js";
|
|
2
|
+
import { MorphShape, gaussianBlur32, gaussianBlur8, morph8, pairwiseSum32, resizeArea32, resizeLinear64, resizeLinear8, resizeNearest, } from "./cv-ops.js";
|
|
3
|
+
import { LineGuideMode } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* PixelOE's legacy `pixelize()` (Apache-2.0, KohakuBlueleaf/PixelOE, `src/pixeloe/legacy`): resize
|
|
6
|
+
* to target x patch size, contrast-aware outline expansion, colour match, contrast-based downscale.
|
|
7
|
+
* Ported operation for operation from the OpenCV / NumPy / torch original with
|
|
8
|
+
* mode="contrast", patch_size=6, avg_scale=9, dist_scale=4 and no colour quantisation, so it matches
|
|
9
|
+
* the Python reference pixel for pixel. The expansion weight can be replaced by a line guide.
|
|
10
|
+
*/
|
|
11
|
+
const PIXELOE_PATCH_SIZE = 6;
|
|
12
|
+
const PIXELOE_DEFAULT_THICKNESS = 2;
|
|
13
|
+
const AVG_SCALE = 9;
|
|
14
|
+
const DIST_SCALE = 4;
|
|
15
|
+
const WEIGHT_STRIDE = Math.floor(PIXELOE_PATCH_SIZE / 4) * 2;
|
|
16
|
+
const COLOR_LEVELS = 5;
|
|
17
|
+
/** A guide mask cell counts as line when more than this share of it is line at working size. */
|
|
18
|
+
const MASK_COVERAGE = 0.2;
|
|
19
|
+
const ORIGINAL_MIX = 0.25;
|
|
20
|
+
const ORIGINAL_SLOPE = 5;
|
|
21
|
+
const f32 = Math.fround;
|
|
22
|
+
/** pixelize() sizes for one `target_size`: working image and contrast-downscale output. */
|
|
23
|
+
function pixeloeTargetSizes(w, h, target) {
|
|
24
|
+
const ratio = w / h;
|
|
25
|
+
const s = Math.sqrt((target ** 2 * PIXELOE_PATCH_SIZE ** 2) / ratio);
|
|
26
|
+
const work = [Math.trunc(s * ratio), Math.trunc(s)];
|
|
27
|
+
const r2 = work[0] / work[1];
|
|
28
|
+
const t = Math.sqrt(target ** 2 / r2);
|
|
29
|
+
return { work, out: [Math.trunc(t * r2), Math.trunc(t)] };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Largest working image (pixels). Outputs above ~330 px a side would otherwise need several
|
|
33
|
+
* hundred MB in a browser worker; they work at a smaller patch instead.
|
|
34
|
+
*/
|
|
35
|
+
const MAX_WORK_PIXELS = 2048 * 2048;
|
|
36
|
+
/**
|
|
37
|
+
* Use PixelOE's own sizing when some `target_size` gives exactly the requested output (then the
|
|
38
|
+
* result is the library's), otherwise work at the requested size x patch size.
|
|
39
|
+
*/
|
|
40
|
+
export function legacyPixeloeSizes(w, h, ow, oh) {
|
|
41
|
+
const guess = Math.round(Math.sqrt(ow * oh));
|
|
42
|
+
for (const target of [Math.max(ow, oh), guess, guess - 1, guess + 1, guess - 2, guess + 2]) {
|
|
43
|
+
if (target < 1)
|
|
44
|
+
continue;
|
|
45
|
+
const sizes = pixeloeTargetSizes(w, h, target);
|
|
46
|
+
const [ww, wh] = sizes.work;
|
|
47
|
+
if (sizes.out[0] === ow &&
|
|
48
|
+
sizes.out[1] === oh &&
|
|
49
|
+
ww > 0 &&
|
|
50
|
+
wh > 0 &&
|
|
51
|
+
ww * wh <= MAX_WORK_PIXELS)
|
|
52
|
+
return sizes;
|
|
53
|
+
}
|
|
54
|
+
const patch = Math.max(1, Math.min(PIXELOE_PATCH_SIZE, Math.floor(Math.sqrt(MAX_WORK_PIXELS / (ow * oh)))));
|
|
55
|
+
return { work: [ow * patch, oh * patch], out: [ow, oh] };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* utils.apply_chunk_torch: one statistic per `stride` block over an edge-padded `kernel` window.
|
|
59
|
+
* Every plane PixelOE takes statistics of holds 8-bit values (Lab bytes, or them / 255), so the
|
|
60
|
+
* window is gathered as bytes and the statistic reads a histogram instead of sorting.
|
|
61
|
+
*/
|
|
62
|
+
function chunkStat(data, w, h, kernel, stride, stat) {
|
|
63
|
+
const shift = Math.max(kernel - stride, 0);
|
|
64
|
+
const pad = shift >> 1;
|
|
65
|
+
const out = new Uint8Array(w * h);
|
|
66
|
+
const values = new Uint8Array(kernel * kernel);
|
|
67
|
+
const histogram = new Uint16Array(BYTE_VALUES);
|
|
68
|
+
const bw = Math.floor(w / stride), bh = Math.floor(h / stride);
|
|
69
|
+
const columns = new Int32Array(kernel);
|
|
70
|
+
for (let by = 0; by < bh; by++)
|
|
71
|
+
for (let bx = 0; bx < bw; bx++) {
|
|
72
|
+
for (let i = 0; i < kernel; i++)
|
|
73
|
+
columns[i] = Math.min(w - 1, Math.max(0, bx * stride - pad + i));
|
|
74
|
+
let n = 0;
|
|
75
|
+
for (let j = 0; j < kernel; j++) {
|
|
76
|
+
const row = Math.min(h - 1, Math.max(0, by * stride - pad + j)) * w;
|
|
77
|
+
for (let i = 0; i < kernel; i++)
|
|
78
|
+
values[n++] = data[row + columns[i]];
|
|
79
|
+
}
|
|
80
|
+
const v = stat(values, histogram);
|
|
81
|
+
for (let y = by * stride; y < by * stride + stride; y++)
|
|
82
|
+
out.fill(v, y * w + bx * stride, y * w + bx * stride + stride);
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
const BYTE_VALUES = 256;
|
|
87
|
+
/** Counts `values` into `histogram`; `release` empties it again for the next window. */
|
|
88
|
+
function count(values, histogram) {
|
|
89
|
+
for (let i = 0; i < values.length; i++)
|
|
90
|
+
histogram[values[i]]++;
|
|
91
|
+
}
|
|
92
|
+
function release(values, histogram) {
|
|
93
|
+
for (let i = 0; i < values.length; i++)
|
|
94
|
+
histogram[values[i]] = 0;
|
|
95
|
+
}
|
|
96
|
+
/** The value at sorted position `rank` of the counted window. */
|
|
97
|
+
function ranked(histogram, rank) {
|
|
98
|
+
let seen = 0;
|
|
99
|
+
for (let v = 0; v < BYTE_VALUES; v++) {
|
|
100
|
+
seen += histogram[v];
|
|
101
|
+
if (seen > rank)
|
|
102
|
+
return v;
|
|
103
|
+
}
|
|
104
|
+
return BYTE_VALUES - 1;
|
|
105
|
+
}
|
|
106
|
+
/** torch.median: the lower median. */
|
|
107
|
+
const lowerMedian = (values, histogram) => {
|
|
108
|
+
count(values, histogram);
|
|
109
|
+
const median = ranked(histogram, (values.length - 1) >> 1);
|
|
110
|
+
release(values, histogram);
|
|
111
|
+
return median;
|
|
112
|
+
};
|
|
113
|
+
const minimum = (values) => {
|
|
114
|
+
let v = values[0];
|
|
115
|
+
for (let i = 1; i < values.length; i++)
|
|
116
|
+
if (values[i] < v)
|
|
117
|
+
v = values[i];
|
|
118
|
+
return v;
|
|
119
|
+
};
|
|
120
|
+
const maximum = (values) => {
|
|
121
|
+
let v = values[0];
|
|
122
|
+
for (let i = 1; i < values.length; i++)
|
|
123
|
+
if (values[i] > v)
|
|
124
|
+
v = values[i];
|
|
125
|
+
return v;
|
|
126
|
+
};
|
|
127
|
+
const sigmoid = (x) => 1 / (1 + Math.exp(-x));
|
|
128
|
+
/** outline.expansion_weight(img, k=patch, stride, 9, 4) on interleaved RGB. */
|
|
129
|
+
function expansionWeight(rgb, w, h) {
|
|
130
|
+
const lab = rgbToLab8(rgb);
|
|
131
|
+
const lum = new Uint8Array(w * h);
|
|
132
|
+
for (let i = 0; i < lum.length; i++)
|
|
133
|
+
lum[i] = lab[i * 3];
|
|
134
|
+
const k = PIXELOE_PATCH_SIZE;
|
|
135
|
+
// The reference takes these statistics of L / 255; the order statistics commute with it.
|
|
136
|
+
const median = chunkStat(lum, w, h, k * 2, WEIGHT_STRIDE, lowerMedian);
|
|
137
|
+
const high = chunkStat(lum, w, h, k, WEIGHT_STRIDE, maximum);
|
|
138
|
+
const low = chunkStat(lum, w, h, k, WEIGHT_STRIDE, minimum);
|
|
139
|
+
const raw = new Float64Array(w * h);
|
|
140
|
+
for (let i = 0; i < raw.length; i++) {
|
|
141
|
+
const avg = median[i] / 255, bright = high[i] / 255 - avg, dark = avg - low[i] / 255;
|
|
142
|
+
raw[i] = sigmoid((avg - 0.5) * AVG_SCALE - (bright - dark) * DIST_SCALE);
|
|
143
|
+
}
|
|
144
|
+
const hw = Math.floor(w / WEIGHT_STRIDE), hh = Math.floor(h / WEIGHT_STRIDE);
|
|
145
|
+
const weight = resizeLinear64(resizeLinear64(raw, w, h, hw, hh), hw, hh, w, h);
|
|
146
|
+
let lo = Infinity, hi = -Infinity;
|
|
147
|
+
for (const v of weight) {
|
|
148
|
+
if (v < lo)
|
|
149
|
+
lo = v;
|
|
150
|
+
if (v > hi)
|
|
151
|
+
hi = v;
|
|
152
|
+
}
|
|
153
|
+
for (let i = 0; i < weight.length; i++)
|
|
154
|
+
weight[i] = (weight[i] - lo) / hi;
|
|
155
|
+
return weight;
|
|
156
|
+
}
|
|
157
|
+
/** outline.outline_expansion with a given weight (erode = dilate = thickness). */
|
|
158
|
+
function outlineExpansion(rgb, w, h, weight, thickness) {
|
|
159
|
+
const eroded = morph8(rgb, w, h, 3, MorphShape.Rect, true, thickness);
|
|
160
|
+
const dilated = morph8(rgb, w, h, 3, MorphShape.Rect, false, thickness);
|
|
161
|
+
const mixed = new Uint8Array(rgb.length);
|
|
162
|
+
for (let p = 0; p < w * h; p++) {
|
|
163
|
+
const wt = weight[p];
|
|
164
|
+
const orig = sigmoid((wt - 0.5) * ORIGINAL_SLOPE) * ORIGINAL_MIX;
|
|
165
|
+
for (let c = 0; c < 3; c++) {
|
|
166
|
+
const i = p * 3 + c;
|
|
167
|
+
const v = eroded[i] * wt + dilated[i] * (1 - wt);
|
|
168
|
+
mixed[i] = Math.trunc(v * (1 - orig) + rgb[i] * orig);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const once = morph8(mixed, w, h, 3, MorphShape.Cross, true, thickness);
|
|
172
|
+
const twice = morph8(once, w, h, 3, MorphShape.Cross, false, thickness * 2);
|
|
173
|
+
return morph8(twice, w, h, 3, MorphShape.Cross, true, thickness);
|
|
174
|
+
}
|
|
175
|
+
function labStats(lab) {
|
|
176
|
+
const n = lab.length;
|
|
177
|
+
const mean = f32(pairwiseSum32(lab) / n);
|
|
178
|
+
const sq = new Float32Array(n);
|
|
179
|
+
for (let i = 0; i < n; i++) {
|
|
180
|
+
const d = f32(lab[i] - mean);
|
|
181
|
+
sq[i] = f32(d * d);
|
|
182
|
+
}
|
|
183
|
+
return { mean, std: f32(Math.sqrt(f32(pairwiseSum32(sq) / n))) };
|
|
184
|
+
}
|
|
185
|
+
/** color.match_color(source, target, level=5) on interleaved RGB. */
|
|
186
|
+
function matchColor(source, target, w, h) {
|
|
187
|
+
const toFloat = (rgb) => {
|
|
188
|
+
const lab = rgbToLab8(rgb);
|
|
189
|
+
const out = new Float32Array(lab.length);
|
|
190
|
+
for (let i = 0; i < lab.length; i++)
|
|
191
|
+
out[i] = lab[i] / 255;
|
|
192
|
+
return out;
|
|
193
|
+
};
|
|
194
|
+
const s = toFloat(source), t = toFloat(target);
|
|
195
|
+
const ss = labStats(s), ts = labStats(t);
|
|
196
|
+
const shifted = new Uint8Array(s.length);
|
|
197
|
+
for (let i = 0; i < s.length; i++) {
|
|
198
|
+
const v = f32(f32(f32(f32(s[i] - ss.mean) / ss.std) * ts.std) + ts.mean);
|
|
199
|
+
shifted[i] = Math.trunc(Math.min(255, Math.max(0, f32(v * 255))));
|
|
200
|
+
}
|
|
201
|
+
const rgb = labToRgb8(shifted);
|
|
202
|
+
const out = new Uint8Array(rgb.length);
|
|
203
|
+
const n = w * h;
|
|
204
|
+
for (let c = 0; c < 3; c++) {
|
|
205
|
+
let inp = new Float32Array(n);
|
|
206
|
+
let tgt = new Uint8Array(n);
|
|
207
|
+
for (let i = 0; i < n; i++) {
|
|
208
|
+
inp[i] = rgb[i * 3 + c];
|
|
209
|
+
tgt[i] = target[i * 3 + c];
|
|
210
|
+
}
|
|
211
|
+
const high = new Float32Array(n);
|
|
212
|
+
for (let level = 1; level <= COLOR_LEVELS; level++) {
|
|
213
|
+
const size = 2 * 2 ** level + 1;
|
|
214
|
+
const low = gaussianBlur32(inp, w, h, size, 0);
|
|
215
|
+
for (let i = 0; i < n; i++)
|
|
216
|
+
high[i] = f32(high[i] + f32(inp[i] - low[i]));
|
|
217
|
+
inp = low;
|
|
218
|
+
tgt = gaussianBlur8(tgt, w, h, size);
|
|
219
|
+
}
|
|
220
|
+
for (let i = 0; i < n; i++)
|
|
221
|
+
out[i * 3 + c] = Math.trunc(Math.min(255, Math.max(0, f32(high[i] + tgt[i]))));
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
/** downscale.contrast_based.find_pixel over one patch of L values. */
|
|
226
|
+
const contrastPick = (values, histogram) => {
|
|
227
|
+
const mid = values[values.length >> 1];
|
|
228
|
+
count(values, histogram);
|
|
229
|
+
const med = ranked(histogram, (values.length - 1) >> 1);
|
|
230
|
+
release(values, histogram);
|
|
231
|
+
let sum = 0, mini = values[0], maxi = values[0];
|
|
232
|
+
for (let i = 0; i < values.length; i++) {
|
|
233
|
+
const v = values[i];
|
|
234
|
+
sum += v;
|
|
235
|
+
if (v < mini)
|
|
236
|
+
mini = v;
|
|
237
|
+
if (v > maxi)
|
|
238
|
+
maxi = v;
|
|
239
|
+
}
|
|
240
|
+
const mu = f32(sum / values.length);
|
|
241
|
+
if (med < mu && maxi - med > med - mini)
|
|
242
|
+
return mini;
|
|
243
|
+
if (med > mu && maxi - med < med - mini)
|
|
244
|
+
return maxi;
|
|
245
|
+
return mid;
|
|
246
|
+
};
|
|
247
|
+
/** downscale.contrast_based.contrast_based_downscale to an exact output size. */
|
|
248
|
+
function contrastDownscale(rgb, w, h, ow, oh) {
|
|
249
|
+
const patch = Math.max(Math.floor(h / oh), Math.floor(w / ow));
|
|
250
|
+
const lab = rgbToLab8(rgb);
|
|
251
|
+
const planes = [0, 1, 2].map((c) => {
|
|
252
|
+
const plane = new Uint8Array(w * h);
|
|
253
|
+
for (let i = 0; i < plane.length; i++)
|
|
254
|
+
plane[i] = lab[i * 3 + c];
|
|
255
|
+
return plane;
|
|
256
|
+
});
|
|
257
|
+
const picked = [
|
|
258
|
+
chunkStat(planes[0], w, h, patch, patch, contrastPick),
|
|
259
|
+
chunkStat(planes[1], w, h, patch, patch, lowerMedian),
|
|
260
|
+
chunkStat(planes[2], w, h, patch, patch, lowerMedian),
|
|
261
|
+
];
|
|
262
|
+
const merged = new Uint8Array(w * h * 3);
|
|
263
|
+
for (let i = 0; i < w * h; i++)
|
|
264
|
+
for (let c = 0; c < 3; c++)
|
|
265
|
+
merged[i * 3 + c] = picked[c][i];
|
|
266
|
+
return resizeNearest(labToRgb8(merged), w, h, 3, ow, oh);
|
|
267
|
+
}
|
|
268
|
+
/** Line pixels (value below `threshold * 255`) as the guide mask at working size. */
|
|
269
|
+
function guideMask(lines, threshold, w, h, ww, wh) {
|
|
270
|
+
// NumPy compares a float32 array with a Python float in float32 (NEP 50), so both bounds are too.
|
|
271
|
+
const cut = f32(threshold), coverage = f32(MASK_COVERAGE);
|
|
272
|
+
const binary = new Float32Array(lines.length);
|
|
273
|
+
for (let i = 0; i < lines.length; i++)
|
|
274
|
+
binary[i] = f32(lines[i] / 255) < cut ? 1 : 0;
|
|
275
|
+
const area = resizeArea32(binary, w, h, ww, wh);
|
|
276
|
+
return area.map((v) => (v > coverage ? 1 : 0));
|
|
277
|
+
}
|
|
278
|
+
/** Interleaved RGB at the source size to interleaved RGB at the requested output size. */
|
|
279
|
+
export function legacyPixeloe(rgb, w, h, options) {
|
|
280
|
+
const thickness = options.thickness ?? PIXELOE_DEFAULT_THICKNESS;
|
|
281
|
+
const { work: [ww, wh], out: [ow, oh], } = legacyPixeloeSizes(w, h, options.width, options.height);
|
|
282
|
+
const work = resizeLinear8(rgb, w, h, 3, ww, wh);
|
|
283
|
+
let expanded = work;
|
|
284
|
+
if (thickness > 0) {
|
|
285
|
+
const guide = options.guide;
|
|
286
|
+
// Lines only replaces the contrast weight outright, so it is not computed at all then.
|
|
287
|
+
let weight = guide?.mode === LineGuideMode.Replace ? [] : expansionWeight(work, ww, wh);
|
|
288
|
+
if (guide) {
|
|
289
|
+
const mask = guide.threshold === null
|
|
290
|
+
? new Float32Array(ww * wh)
|
|
291
|
+
: guideMask(guide.lines, guide.threshold, guide.width, guide.height, ww, wh);
|
|
292
|
+
weight =
|
|
293
|
+
guide.mode === LineGuideMode.Replace
|
|
294
|
+
? mask
|
|
295
|
+
: Float64Array.from(weight, (v, i) => Math.max(v, mask[i]));
|
|
296
|
+
}
|
|
297
|
+
expanded = outlineExpansion(work, ww, wh, weight, thickness);
|
|
298
|
+
}
|
|
299
|
+
const matched = matchColor(expanded, work, ww, wh);
|
|
300
|
+
return { rgb: contrastDownscale(matched, ww, wh, ow, oh), width: ow, height: oh };
|
|
301
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** One byte per pixel: 255 = no line, lower = line. */
|
|
2
|
+
export declare function xdogLines(rgb: Uint8Array, width: number, height: number): Uint8Array;
|
|
3
|
+
/**
|
|
4
|
+
* skimage `threshold_otsu` of `line / 255` (256 bins over the value range), as a 0..1 threshold.
|
|
5
|
+
* Returns null when the image has a single value.
|
|
6
|
+
*/
|
|
7
|
+
export declare function otsuThreshold(line: Uint8Array): number | null;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { gaussianBlur32, gaussianSize, rgbToGray8, } from "./cv-ops.js";
|
|
2
|
+
/**
|
|
3
|
+
* XDoG line drawing (Winnemöller 2012) with the parameters of the reference: sigma 0.8, k 1.6,
|
|
4
|
+
* p 20, eps 0, phi 10, on the 8-bit grey image. White (255) is paper, darker values are lines.
|
|
5
|
+
*/
|
|
6
|
+
const XDOG_SIGMA = 0.8;
|
|
7
|
+
const XDOG_K = 1.6;
|
|
8
|
+
const XDOG_P = 20;
|
|
9
|
+
const XDOG_PHI = 10;
|
|
10
|
+
const f32 = Math.fround;
|
|
11
|
+
/** One byte per pixel: 255 = no line, lower = line. */
|
|
12
|
+
export function xdogLines(rgb, width, height) {
|
|
13
|
+
const gray = rgbToGray8(rgb);
|
|
14
|
+
const g = new Float32Array(gray.length);
|
|
15
|
+
for (let i = 0; i < g.length; i++)
|
|
16
|
+
g[i] = f32(gray[i] / 255);
|
|
17
|
+
const sigmaB = XDOG_SIGMA * XDOG_K;
|
|
18
|
+
const a = gaussianBlur32(g, width, height, gaussianSize(XDOG_SIGMA), XDOG_SIGMA);
|
|
19
|
+
const b = gaussianBlur32(g, width, height, gaussianSize(sigmaB), sigmaB);
|
|
20
|
+
const out = new Uint8Array(g.length);
|
|
21
|
+
for (let i = 0; i < g.length; i++) {
|
|
22
|
+
const d = f32(f32((1 + XDOG_P) * a[i]) - f32(XDOG_P * b[i]));
|
|
23
|
+
const v = d >= 0 ? 1 : f32(1 + f32(Math.tanh(f32(XDOG_PHI * d))));
|
|
24
|
+
out[i] = Math.trunc(f32(Math.min(1, Math.max(0, v)) * 255));
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* skimage `threshold_otsu` of `line / 255` (256 bins over the value range), as a 0..1 threshold.
|
|
30
|
+
* Returns null when the image has a single value.
|
|
31
|
+
*/
|
|
32
|
+
export function otsuThreshold(line) {
|
|
33
|
+
let lo = 255, hi = 0;
|
|
34
|
+
for (const v of line) {
|
|
35
|
+
if (v < lo)
|
|
36
|
+
lo = v;
|
|
37
|
+
if (v > hi)
|
|
38
|
+
hi = v;
|
|
39
|
+
}
|
|
40
|
+
if (lo === hi)
|
|
41
|
+
return null;
|
|
42
|
+
const bins = 256;
|
|
43
|
+
const first = lo / 255, last = hi / 255;
|
|
44
|
+
const counts = new Float64Array(bins);
|
|
45
|
+
const norm = bins / (last - first);
|
|
46
|
+
for (const v of line) {
|
|
47
|
+
const x = v / 255;
|
|
48
|
+
let i = Math.floor((x - first) * norm);
|
|
49
|
+
if (i === bins)
|
|
50
|
+
i--;
|
|
51
|
+
counts[i]++;
|
|
52
|
+
}
|
|
53
|
+
const width = (last - first) / bins;
|
|
54
|
+
const centres = new Float64Array(bins);
|
|
55
|
+
for (let i = 0; i < bins; i++) {
|
|
56
|
+
const left = first + i * width, right = i === bins - 1 ? last : first + (i + 1) * width;
|
|
57
|
+
centres[i] = (left + right) / 2;
|
|
58
|
+
}
|
|
59
|
+
const w1 = new Float64Array(bins), m1 = new Float64Array(bins), w2 = new Float64Array(bins), m2 = new Float64Array(bins);
|
|
60
|
+
let cw = 0, cs = 0;
|
|
61
|
+
for (let i = 0; i < bins; i++) {
|
|
62
|
+
cw += counts[i];
|
|
63
|
+
cs += counts[i] * centres[i];
|
|
64
|
+
w1[i] = cw;
|
|
65
|
+
m1[i] = cs / cw;
|
|
66
|
+
}
|
|
67
|
+
cw = 0;
|
|
68
|
+
cs = 0;
|
|
69
|
+
for (let i = bins - 1; i >= 0; i--) {
|
|
70
|
+
cw += counts[i];
|
|
71
|
+
cs += counts[i] * centres[i];
|
|
72
|
+
w2[i] = cw;
|
|
73
|
+
m2[i] = cs / cw;
|
|
74
|
+
}
|
|
75
|
+
let best = -1, index = 0;
|
|
76
|
+
for (let i = 0; i < bins - 1; i++) {
|
|
77
|
+
const v = w1[i] * w2[i + 1] * (m1[i] - m2[i + 1]) ** 2;
|
|
78
|
+
if (v > best) {
|
|
79
|
+
best = v;
|
|
80
|
+
index = i;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return centres[index];
|
|
84
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { UINT8_MAX } from "./base.js";
|
|
2
|
+
import { assertDimension, assertPixelCount } from "./base.js";
|
|
3
|
+
const CHANNELS = 4;
|
|
4
|
+
const RGB_BASE = 256;
|
|
5
|
+
const EMPTY_SAMPLE = -1;
|
|
6
|
+
const MAX_SAMPLES_PER_AXIS = 64;
|
|
7
|
+
/** Choose a cell's most frequent RGB, counting transparent pixels as empty samples. */
|
|
8
|
+
export function resizeMostFrequent(source, width, height) {
|
|
9
|
+
assertDimension(source.width, "source width");
|
|
10
|
+
assertDimension(source.height, "source height");
|
|
11
|
+
assertDimension(width, "width");
|
|
12
|
+
assertDimension(height, "height");
|
|
13
|
+
assertPixelCount(width, height);
|
|
14
|
+
if (source.data.length !== source.width * source.height * CHANNELS)
|
|
15
|
+
throw new Error("Invalid pixel buffer.");
|
|
16
|
+
const data = new Uint8ClampedArray(width * height * CHANNELS);
|
|
17
|
+
for (let row = 0; row < height; row++) {
|
|
18
|
+
const top = Math.floor((row * source.height) / height);
|
|
19
|
+
const bottom = Math.min(source.height, Math.max(top + 1, Math.floor(((row + 1) * source.height) / height)));
|
|
20
|
+
const yStep = Math.ceil((bottom - top) / MAX_SAMPLES_PER_AXIS);
|
|
21
|
+
for (let column = 0; column < width; column++) {
|
|
22
|
+
const left = Math.floor((column * source.width) / width);
|
|
23
|
+
const right = Math.min(source.width, Math.max(left + 1, Math.floor(((column + 1) * source.width) / width)));
|
|
24
|
+
const xStep = Math.ceil((right - left) / MAX_SAMPLES_PER_AXIS);
|
|
25
|
+
const counts = new Map();
|
|
26
|
+
let alphaSum = 0;
|
|
27
|
+
let samples = 0;
|
|
28
|
+
for (let y = top; y < bottom; y += yStep)
|
|
29
|
+
for (let x = left; x < right; x += xStep) {
|
|
30
|
+
const offset = (y * source.width + x) * CHANNELS;
|
|
31
|
+
alphaSum += source.data[offset + 3];
|
|
32
|
+
samples++;
|
|
33
|
+
const key = source.data[offset + 3] === 0
|
|
34
|
+
? EMPTY_SAMPLE
|
|
35
|
+
: (source.data[offset] * RGB_BASE + source.data[offset + 1]) * RGB_BASE +
|
|
36
|
+
source.data[offset + 2];
|
|
37
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
38
|
+
}
|
|
39
|
+
let selected = EMPTY_SAMPLE;
|
|
40
|
+
let largest = 0;
|
|
41
|
+
for (const [color, count] of counts)
|
|
42
|
+
if (count > largest) {
|
|
43
|
+
selected = color;
|
|
44
|
+
largest = count;
|
|
45
|
+
}
|
|
46
|
+
if (selected === EMPTY_SAMPLE)
|
|
47
|
+
continue;
|
|
48
|
+
const offset = (row * width + column) * CHANNELS;
|
|
49
|
+
data[offset] = Math.floor(selected / (RGB_BASE * RGB_BASE));
|
|
50
|
+
data[offset + 1] = Math.floor(selected / RGB_BASE) % RGB_BASE;
|
|
51
|
+
data[offset + 2] = selected % RGB_BASE;
|
|
52
|
+
data[offset + 3] = Math.min(UINT8_MAX, Math.round(alphaSum / samples));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { width, height, data };
|
|
56
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { PixelBuffer } from "./base.ts";
|
|
2
|
+
import { CellColorPicker, LineGuideMode, OutlinePrepass, PixelationMode, type PixelGridGeometry } from "./types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Whether these options run stock PixelOE end to end (its contrast downscale and colour match to the
|
|
5
|
+
* output size) instead of a pre-pass followed by the picker: a PixelOE pre-pass with the Contrast
|
|
6
|
+
* picker, when the cells are not a grid recovered from the source (a plain convert, or Custom size's
|
|
7
|
+
* even blocks). This is the PixelOE reference output.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runsStockPixeloe(options: {
|
|
10
|
+
prepass?: OutlinePrepass;
|
|
11
|
+
colorPicker?: CellColorPicker;
|
|
12
|
+
mode?: PixelationMode;
|
|
13
|
+
}, grid?: PixelGridGeometry): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The image -> image pre-pass (the original PixelOE pre-pass, optionally line-guided). Recent
|
|
16
|
+
* results are reused, so a colour or picker change on the same image does not rerun it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function applyOutlinePrepass(image: PixelBuffer, cell: number, prepass: OutlinePrepass, lineGuide?: LineGuideMode): PixelBuffer;
|
|
19
|
+
/**
|
|
20
|
+
* The pre-pass every source opens with, whether or not a grid is found: PixelOE + lines (with the
|
|
21
|
+
* Contrast picker). None, the plain grid recovery, is one choice away in the controls.
|
|
22
|
+
*/
|
|
23
|
+
export declare const DEFAULT_OUTLINE_PREPASS = OutlinePrepass.PixeloeLines;
|
|
24
|
+
/**
|
|
25
|
+
* The line guide PixelOE + lines starts with: Lines only (the line mask replaces PixelOE's outline
|
|
26
|
+
* weight). Add to outlines barely differs from plain PixelOE on line art, while Lines only keeps
|
|
27
|
+
* continuous drawn lines (e.g. an eye line) through the downscale.
|
|
28
|
+
*/
|
|
29
|
+
export declare const DEFAULT_LINE_GUIDE = LineGuideMode.Replace;
|
|
30
|
+
/**
|
|
31
|
+
* A pre-pass from saved settings: the current value, or the old `pixeloePrepass` checkbox
|
|
32
|
+
* (true -> PixelOE, false -> None).
|
|
33
|
+
*/
|
|
34
|
+
export declare function readOutlinePrepass(saved: {
|
|
35
|
+
prepass?: unknown;
|
|
36
|
+
pixeloePrepass?: unknown;
|
|
37
|
+
}): OutlinePrepass | undefined;
|