@xprite/pixelate 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +12 -0
- package/README.md +238 -2
- package/README.zh-CN.md +209 -0
- package/dist/analyze.d.ts +8 -0
- package/dist/analyze.js +217 -0
- package/dist/base.d.ts +18 -0
- package/dist/base.js +42 -0
- package/dist/cell-color-picker.d.ts +14 -0
- package/dist/cell-color-picker.js +278 -0
- package/dist/color-lab.d.ts +3 -0
- package/dist/color-lab.js +31 -0
- package/dist/color-step.d.ts +33 -0
- package/dist/color-step.js +62 -0
- package/dist/contours.d.ts +4 -0
- package/dist/contours.js +66 -0
- package/dist/denoise.d.ts +3 -0
- package/dist/denoise.js +37 -0
- package/dist/deskew.d.ts +24 -0
- package/dist/deskew.js +202 -0
- package/dist/grid-ac-rl-consensus.d.ts +127 -0
- package/dist/grid-ac-rl-consensus.js +497 -0
- package/dist/grid-arbitrate.d.ts +52 -0
- package/dist/grid-arbitrate.js +366 -0
- package/dist/grid-cut-snap.d.ts +15 -0
- package/dist/grid-cut-snap.js +77 -0
- package/dist/grid-description-length.d.ts +47 -0
- package/dist/grid-description-length.js +697 -0
- package/dist/grid-detect.d.ts +8 -0
- package/dist/grid-detect.js +665 -0
- package/dist/grid-edge-coherence.d.ts +4 -0
- package/dist/grid-edge-coherence.js +158 -0
- package/dist/grid-edge-path.d.ts +9 -0
- package/dist/grid-edge-path.js +201 -0
- package/dist/grid-frame-lattice.d.ts +25 -0
- package/dist/grid-frame-lattice.js +420 -0
- package/dist/grid-frequency-spectrum.d.ts +11 -0
- package/dist/grid-frequency-spectrum.js +89 -0
- package/dist/grid-period-boundary.d.ts +13 -0
- package/dist/grid-period-boundary.js +255 -0
- package/dist/grid-period-comb.d.ts +4 -0
- package/dist/grid-period-comb.js +230 -0
- package/dist/grid-period-distance.d.ts +7 -0
- package/dist/grid-period-distance.js +233 -0
- package/dist/grid-phase-field.d.ts +11 -0
- package/dist/grid-phase-field.js +446 -0
- package/dist/grid-pitch-lattice.d.ts +4 -0
- package/dist/grid-pitch-lattice.js +255 -0
- package/dist/grid-pitch-search.d.ts +11 -0
- package/dist/grid-pitch-search.js +233 -0
- package/dist/grid-sample.d.ts +4 -0
- package/dist/grid-sample.js +164 -0
- package/dist/grid-sampling.d.ts +13 -0
- package/dist/grid-sampling.js +132 -0
- package/dist/grid-strip-shift.d.ts +44 -0
- package/dist/grid-strip-shift.js +223 -0
- package/dist/grid-test-images.d.ts +69 -0
- package/dist/grid-test-images.js +547 -0
- package/dist/grid-warp.d.ts +38 -0
- package/dist/grid-warp.js +252 -0
- package/dist/grid.d.ts +8 -0
- package/dist/grid.js +43 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +14 -0
- package/dist/line-guided/cv-lab.d.ts +4 -0
- package/dist/line-guided/cv-lab.js +134 -0
- package/dist/line-guided/cv-ops.d.ts +29 -0
- package/dist/line-guided/cv-ops.js +759 -0
- package/dist/line-guided/pixeloe-entry.d.ts +15 -0
- package/dist/line-guided/pixeloe-entry.js +122 -0
- package/dist/line-guided/pixeloe-legacy.d.ts +31 -0
- package/dist/line-guided/pixeloe-legacy.js +301 -0
- package/dist/line-guided/xdog.d.ts +7 -0
- package/dist/line-guided/xdog.js +84 -0
- package/dist/most-frequent.d.ts +3 -0
- package/dist/most-frequent.js +56 -0
- package/dist/outline-prepass.d.ts +37 -0
- package/dist/outline-prepass.js +56 -0
- package/dist/palette-snap.d.ts +4 -0
- package/dist/palette-snap.js +371 -0
- package/dist/palette.d.ts +21 -0
- package/dist/palette.js +280 -0
- package/dist/pixelate.d.ts +15 -0
- package/dist/pixelate.js +214 -0
- package/dist/pixelation-algorithm-registry.d.ts +34 -0
- package/dist/pixelation-algorithm-registry.js +716 -0
- package/dist/pixeloe-prepass.d.ts +12 -0
- package/dist/pixeloe-prepass.js +527 -0
- package/dist/quantization.d.ts +6 -0
- package/dist/quantization.js +145 -0
- package/dist/remove-background.d.ts +40 -0
- package/dist/remove-background.js +229 -0
- package/dist/resize.d.ts +3 -0
- package/dist/resize.js +86 -0
- package/dist/sampling.d.ts +7 -0
- package/dist/sampling.js +301 -0
- package/dist/settings.d.ts +36 -0
- package/dist/settings.js +181 -0
- package/dist/types.d.ts +255 -0
- package/dist/types.js +87 -0
- package/licenses/PixelOE-Apache-2.0.txt +201 -0
- package/licenses/pixel-art-fixer-MIT.txt +21 -0
- package/licenses/proper-pixel-art-MIT.txt +21 -0
- package/licenses/tensorhead-dithering-MIT.txt +21 -0
- package/package.json +62 -4
- package/wasm/ac-rl-detect-browser-worker.js +45 -0
- package/wasm/ac-rl-detect-worker.mjs +51 -0
- package/wasm/pixelfixer_detect.d.ts +65 -0
- package/wasm/pixelfixer_detect.js +334 -0
- package/wasm/pixelfixer_detect_bg.wasm +0 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The few OpenCV / NumPy image operations the legacy PixelOE pipeline and XDoG use, reproduced with
|
|
3
|
+
* the same rounding and accumulation order as OpenCV 5 (modules/imgproc resize.cpp, filter.simd.hpp,
|
|
4
|
+
* smooth.dispatch.cpp, morph) and NumPy's pairwise sums. Images are row-major; `cn` interleaved
|
|
5
|
+
* channels where noted.
|
|
6
|
+
*/
|
|
7
|
+
const f32 = Math.fround;
|
|
8
|
+
/** Single-precision fused multiply-add, as the AVX2 kernels OpenCV dispatches to. */
|
|
9
|
+
const fma32 = (a, b, c) => f32(a * b + c);
|
|
10
|
+
const RESIZE_COEF_SCALE = 2048;
|
|
11
|
+
const VELTKAMP_SPLIT = 134217729; // 2^27 + 1
|
|
12
|
+
const bitsView = new DataView(new ArrayBuffer(8));
|
|
13
|
+
/** Moves `v` one ulp towards `towards` when `v` has an even mantissa (round-to-odd step). */
|
|
14
|
+
function roundToOdd(v, towards) {
|
|
15
|
+
if (towards === 0)
|
|
16
|
+
return v;
|
|
17
|
+
bitsView.setFloat64(0, v);
|
|
18
|
+
let high = bitsView.getUint32(0), low = bitsView.getUint32(4);
|
|
19
|
+
if ((low & 1) === 1)
|
|
20
|
+
return v;
|
|
21
|
+
if (towards > 0 === v > 0) {
|
|
22
|
+
low = (low + 1) >>> 0;
|
|
23
|
+
if (low === 0)
|
|
24
|
+
high++;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
if (low === 0)
|
|
28
|
+
high--;
|
|
29
|
+
low = (low - 1) >>> 0;
|
|
30
|
+
}
|
|
31
|
+
bitsView.setUint32(0, high);
|
|
32
|
+
bitsView.setUint32(4, low);
|
|
33
|
+
return bitsView.getFloat64(0);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Correctly rounded `a * b + c` (IEEE fused multiply-add) for finite, normal-range inputs,
|
|
37
|
+
* via exact products/sums and Boldo–Melquiond round-to-odd.
|
|
38
|
+
*/
|
|
39
|
+
function fma64(a, b, c) {
|
|
40
|
+
const p = a * b;
|
|
41
|
+
let t = VELTKAMP_SPLIT * a;
|
|
42
|
+
const ah = t - (t - a), al = a - ah;
|
|
43
|
+
t = VELTKAMP_SPLIT * b;
|
|
44
|
+
const bh = t - (t - b), bl = b - bh;
|
|
45
|
+
const pl = ah * bh - p + ah * bl + al * bh + al * bl;
|
|
46
|
+
const sh = c + p;
|
|
47
|
+
const sb = sh - c;
|
|
48
|
+
const sl = c - (sh - sb) + (p - sb);
|
|
49
|
+
const vh = sl + pl;
|
|
50
|
+
const vb = vh - sl;
|
|
51
|
+
const vl = sl - (vh - vb) + (pl - vb);
|
|
52
|
+
return sh + (vh === 0 ? vl : roundToOdd(vh, vl));
|
|
53
|
+
}
|
|
54
|
+
/** cvRound: round half to even. */
|
|
55
|
+
function cvRound(v) {
|
|
56
|
+
const r = Math.round(v);
|
|
57
|
+
return r - v === 0.5 && r % 2 !== 0 ? r - 1 : r;
|
|
58
|
+
}
|
|
59
|
+
const saturateByte = (v) => {
|
|
60
|
+
const r = cvRound(v);
|
|
61
|
+
return r < 0 ? 0 : r > 255 ? 255 : r;
|
|
62
|
+
};
|
|
63
|
+
const saturateShort = (v) => Math.max(-32768, Math.min(32767, cvRound(v)));
|
|
64
|
+
/** COLOR_RGB2GRAY on 8-bit RGB (fixed point, 14-bit coefficients). */
|
|
65
|
+
/** OpenCV 5 `RGB2Gray<uchar>` 15-bit coefficients (verified over every 24-bit colour). */
|
|
66
|
+
const GRAY_SHIFT = 15;
|
|
67
|
+
const GRAY_R = 9798;
|
|
68
|
+
const GRAY_G = 19235;
|
|
69
|
+
const GRAY_B = 3735;
|
|
70
|
+
export function rgbToGray8(rgb) {
|
|
71
|
+
const out = new Uint8Array(rgb.length / 3);
|
|
72
|
+
for (let i = 0, j = 0; j < out.length; i += 3, j++)
|
|
73
|
+
out[j] =
|
|
74
|
+
(rgb[i] * GRAY_R + rgb[i + 1] * GRAY_G + rgb[i + 2] * GRAY_B + (1 << (GRAY_SHIFT - 1))) >>
|
|
75
|
+
GRAY_SHIFT;
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function linearTaps(source, target, clampFraction) {
|
|
79
|
+
const scale = 1 / (target / source);
|
|
80
|
+
const offset = new Int32Array(target);
|
|
81
|
+
const first = new Float32Array(target);
|
|
82
|
+
const second = new Float32Array(target);
|
|
83
|
+
let limit = target;
|
|
84
|
+
for (let d = 0; d < target; d++) {
|
|
85
|
+
let f = f32((d + 0.5) * scale - 0.5);
|
|
86
|
+
let s = Math.floor(f);
|
|
87
|
+
f = f32(f - s);
|
|
88
|
+
if (clampFraction && s < 0) {
|
|
89
|
+
f = 0;
|
|
90
|
+
s = 0;
|
|
91
|
+
}
|
|
92
|
+
if (s + 1 >= source) {
|
|
93
|
+
limit = Math.min(limit, d);
|
|
94
|
+
if (clampFraction && s >= source - 1) {
|
|
95
|
+
f = 0;
|
|
96
|
+
s = source - 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
offset[d] = s;
|
|
100
|
+
first[d] = f32(1 - f);
|
|
101
|
+
second[d] = f;
|
|
102
|
+
}
|
|
103
|
+
return { offset, first, second, limit };
|
|
104
|
+
}
|
|
105
|
+
const isTwice = (source, target) => {
|
|
106
|
+
const scale = 1 / (target / source);
|
|
107
|
+
return Math.abs(scale - 2) < Number.EPSILON;
|
|
108
|
+
};
|
|
109
|
+
/** cv::resize INTER_LINEAR for 8-bit images with `cn` interleaved channels. */
|
|
110
|
+
export function resizeLinear8(src, sw, sh, cn, dw, dh) {
|
|
111
|
+
if (sw === dw && sh === dh)
|
|
112
|
+
return src.slice();
|
|
113
|
+
if (isTwice(sw, dw) && isTwice(sh, dh))
|
|
114
|
+
return halve8(src, sw, sh, cn, dw, dh);
|
|
115
|
+
const xs = linearTaps(sw, dw, true);
|
|
116
|
+
const ys = linearTaps(sh, dh, false);
|
|
117
|
+
const ax0 = new Int32Array(dw), ax1 = new Int32Array(dw);
|
|
118
|
+
for (let d = 0; d < dw; d++) {
|
|
119
|
+
ax0[d] = saturateShort(xs.first[d] * RESIZE_COEF_SCALE);
|
|
120
|
+
ax1[d] = saturateShort(xs.second[d] * RESIZE_COEF_SCALE);
|
|
121
|
+
}
|
|
122
|
+
const rowCache = new Map();
|
|
123
|
+
const row = (sy) => {
|
|
124
|
+
let r = rowCache.get(sy);
|
|
125
|
+
if (r)
|
|
126
|
+
return r;
|
|
127
|
+
r = new Int32Array(dw * cn);
|
|
128
|
+
const base = sy * sw * cn;
|
|
129
|
+
for (let d = 0; d < dw; d++) {
|
|
130
|
+
const sx = xs.offset[d] * cn;
|
|
131
|
+
for (let c = 0; c < cn; c++)
|
|
132
|
+
r[d * cn + c] =
|
|
133
|
+
d < xs.limit
|
|
134
|
+
? src[base + sx + c] * ax0[d] + src[base + sx + cn + c] * ax1[d]
|
|
135
|
+
: src[base + sx + c] * RESIZE_COEF_SCALE;
|
|
136
|
+
}
|
|
137
|
+
if (rowCache.size > 4)
|
|
138
|
+
rowCache.delete(rowCache.keys().next().value);
|
|
139
|
+
rowCache.set(sy, r);
|
|
140
|
+
return r;
|
|
141
|
+
};
|
|
142
|
+
const out = new Uint8Array(dw * dh * cn);
|
|
143
|
+
for (let d = 0; d < dh; d++) {
|
|
144
|
+
const sy = ys.offset[d];
|
|
145
|
+
const s0 = row(Math.min(sh - 1, Math.max(0, sy)));
|
|
146
|
+
const s1 = row(Math.min(sh - 1, Math.max(0, sy + 1)));
|
|
147
|
+
const b0 = saturateShort(ys.first[d] * RESIZE_COEF_SCALE);
|
|
148
|
+
const b1 = saturateShort(ys.second[d] * RESIZE_COEF_SCALE);
|
|
149
|
+
const o = d * dw * cn;
|
|
150
|
+
for (let i = 0; i < dw * cn; i++) {
|
|
151
|
+
const v = (((b0 * (s0[i] >> 4)) >> 16) + ((b1 * (s1[i] >> 4)) >> 16) + 2) >> 2;
|
|
152
|
+
out[o + i] = v < 0 ? 0 : v > 255 ? 255 : v;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
/** OpenCV's exact 2x INTER_LINEAR downscale, which it runs as the vectorised area average. */
|
|
158
|
+
function halve8(src, sw, sh, cn, dw, dh) {
|
|
159
|
+
const out = new Uint8Array(dw * dh * cn);
|
|
160
|
+
for (let y = 0; y < dh; y++)
|
|
161
|
+
for (let x = 0; x < dw; x++)
|
|
162
|
+
for (let c = 0; c < cn; c++) {
|
|
163
|
+
const a = (2 * y * sw + 2 * x) * cn + c;
|
|
164
|
+
out[(y * dw + x) * cn + c] =
|
|
165
|
+
(src[a] + src[a + cn] + src[a + sw * cn] + src[a + sw * cn + cn] + 2) >> 2;
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
/** cv::resize INTER_LINEAR for one-channel float64 images, as OpenCV 5's IPP HAL computes it. */
|
|
170
|
+
export function resizeLinear64(src, sw, sh, dw, dh) {
|
|
171
|
+
if (sw === dw && sh === dh)
|
|
172
|
+
return src.slice();
|
|
173
|
+
const xs = linearTaps64(sw, dw);
|
|
174
|
+
const ys = linearTaps64(sh, dh);
|
|
175
|
+
const rows = new Float64Array(sh * dw);
|
|
176
|
+
for (let y = 0; y < sh; y++)
|
|
177
|
+
for (let d = 0; d < dw; d++) {
|
|
178
|
+
const p = src[y * sw + xs.offset[d]];
|
|
179
|
+
const q = src[y * sw + Math.min(sw - 1, xs.offset[d] + 1)];
|
|
180
|
+
rows[y * dw + d] = fma64(q - p, xs.fraction[d], p);
|
|
181
|
+
}
|
|
182
|
+
const out = new Float64Array(dw * dh);
|
|
183
|
+
for (let d = 0; d < dh; d++) {
|
|
184
|
+
const s0 = ys.offset[d] * dw;
|
|
185
|
+
const s1 = Math.min(sh - 1, ys.offset[d] + 1) * dw;
|
|
186
|
+
const f = ys.fraction[d];
|
|
187
|
+
for (let x = 0; x < dw; x++) {
|
|
188
|
+
const p = rows[s0 + x];
|
|
189
|
+
out[d * dw + x] = fma64(rows[s1 + x] - p, f, p);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Taps of the IPP HAL's float64 linear resize, which OpenCV 5 wheels use for CV_64F:
|
|
196
|
+
* double-precision fractions `fma(d + 0.5, source / target, -0.5)` and taps blended as
|
|
197
|
+
* `fma(q - p, fraction, p)` (verified bit-exact against cv2 5.0 on random float64 images).
|
|
198
|
+
*/
|
|
199
|
+
function linearTaps64(source, target) {
|
|
200
|
+
const scale = source / target;
|
|
201
|
+
const offset = new Int32Array(target);
|
|
202
|
+
const fraction = new Float64Array(target);
|
|
203
|
+
for (let d = 0; d < target; d++) {
|
|
204
|
+
const f = fma64(d + 0.5, scale, -0.5);
|
|
205
|
+
let s = Math.floor(f);
|
|
206
|
+
let frac = f - s;
|
|
207
|
+
if (s < 0) {
|
|
208
|
+
s = 0;
|
|
209
|
+
frac = 0;
|
|
210
|
+
}
|
|
211
|
+
if (s >= source - 1) {
|
|
212
|
+
s = source - 1;
|
|
213
|
+
frac = 0;
|
|
214
|
+
}
|
|
215
|
+
offset[d] = s;
|
|
216
|
+
fraction[d] = frac;
|
|
217
|
+
}
|
|
218
|
+
return { offset, fraction };
|
|
219
|
+
}
|
|
220
|
+
function areaTaps(source, target, scale) {
|
|
221
|
+
const taps = [];
|
|
222
|
+
for (let d = 0; d < target; d++) {
|
|
223
|
+
const fs1 = d * scale;
|
|
224
|
+
const fs2 = fs1 + scale;
|
|
225
|
+
const cell = Math.min(scale, source - fs1);
|
|
226
|
+
let s2 = Math.floor(fs2);
|
|
227
|
+
let s1 = Math.ceil(fs1);
|
|
228
|
+
s2 = Math.min(s2, source - 1);
|
|
229
|
+
s1 = Math.min(s1, s2);
|
|
230
|
+
if (s1 - fs1 > 1e-3)
|
|
231
|
+
taps.push({ d, s: s1 - 1, alpha: f32((s1 - fs1) / cell) });
|
|
232
|
+
for (let s = s1; s < s2; s++)
|
|
233
|
+
taps.push({ d, s, alpha: f32(1 / cell) });
|
|
234
|
+
if (fs2 - s2 > 1e-3)
|
|
235
|
+
taps.push({ d, s: s2, alpha: f32(Math.min(Math.min(fs2 - s2, 1), cell) / cell) });
|
|
236
|
+
}
|
|
237
|
+
return taps;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* cv::resize INTER_AREA when shrinking (`round` true stores 8-bit results, false keeps float32).
|
|
241
|
+
* Integer factors take OpenCV's fast path; other factors its weighted-area tables.
|
|
242
|
+
*/
|
|
243
|
+
function resizeAreaDown(src, sw, sh, cn, dw, dh, round) {
|
|
244
|
+
const scaleX = 1 / (dw / sw), scaleY = 1 / (dh / sh);
|
|
245
|
+
const ix = Math.round(scaleX), iy = Math.round(scaleY);
|
|
246
|
+
const out = new Float32Array(dw * dh * cn);
|
|
247
|
+
const store = (v) => (round ? saturateByte(v) : v);
|
|
248
|
+
if (Math.abs(scaleX - ix) < Number.EPSILON && Math.abs(scaleY - iy) < Number.EPSILON) {
|
|
249
|
+
const area = ix * iy;
|
|
250
|
+
const inv = f32(1 / area);
|
|
251
|
+
const values = new Float64Array(area);
|
|
252
|
+
for (let y = 0; y < dh; y++)
|
|
253
|
+
for (let x = 0; x < dw; x++)
|
|
254
|
+
for (let c = 0; c < cn; c++) {
|
|
255
|
+
if (ix === 2 && iy === 2) {
|
|
256
|
+
const a = (2 * y * sw + 2 * x) * cn + c;
|
|
257
|
+
const s = src[a] + src[a + cn] + src[a + sw * cn] + src[a + sw * cn + cn];
|
|
258
|
+
out[(y * dw + x) * cn + c] = round ? (s + 2) >> 2 : f32(f32(s) * 0.25);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
let n = 0;
|
|
262
|
+
for (let sy = 0; sy < iy; sy++)
|
|
263
|
+
for (let sx = 0; sx < ix; sx++)
|
|
264
|
+
values[n++] = src[((y * iy + sy) * sw + x * ix + sx) * cn + c];
|
|
265
|
+
let sum = 0;
|
|
266
|
+
let k = 0;
|
|
267
|
+
for (; k <= area - 4; k += 4)
|
|
268
|
+
sum = f32(sum + f32(f32(f32(values[k] + values[k + 1]) + values[k + 2]) + values[k + 3]));
|
|
269
|
+
for (; k < area; k++)
|
|
270
|
+
sum = f32(sum + values[k]);
|
|
271
|
+
out[(y * dw + x) * cn + c] = store(f32(sum * inv));
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
const xt = areaTaps(sw, dw, scaleX);
|
|
276
|
+
const yt = areaTaps(sh, dh, scaleY);
|
|
277
|
+
const width = dw * cn;
|
|
278
|
+
const buf = new Float32Array(width);
|
|
279
|
+
const sum = new Float32Array(width);
|
|
280
|
+
// The column taps as flat tables (destination and source element offsets, weight).
|
|
281
|
+
const into = Int32Array.from(xt, (tap) => tap.d * cn);
|
|
282
|
+
const from = Int32Array.from(xt, (tap) => tap.s * cn);
|
|
283
|
+
const weights = Float64Array.from(xt, (tap) => tap.alpha);
|
|
284
|
+
let prev = yt[0]?.d ?? 0;
|
|
285
|
+
for (const { d: dy, s: sy, alpha: beta } of yt) {
|
|
286
|
+
buf.fill(0);
|
|
287
|
+
const base = sy * sw * cn;
|
|
288
|
+
if (cn === 3)
|
|
289
|
+
for (let t = 0; t < into.length; t++) {
|
|
290
|
+
const d = into[t], s = base + from[t], alpha = weights[t];
|
|
291
|
+
// Float32 stores round each sum, as f32(buf + f32(src * alpha)).
|
|
292
|
+
buf[d] = buf[d] + f32(src[s] * alpha);
|
|
293
|
+
buf[d + 1] = buf[d + 1] + f32(src[s + 1] * alpha);
|
|
294
|
+
buf[d + 2] = buf[d + 2] + f32(src[s + 2] * alpha);
|
|
295
|
+
}
|
|
296
|
+
else
|
|
297
|
+
for (let t = 0; t < into.length; t++) {
|
|
298
|
+
const d = into[t], s = base + from[t], alpha = weights[t];
|
|
299
|
+
for (let c = 0; c < cn; c++)
|
|
300
|
+
buf[d + c] = f32(buf[d + c] + f32(src[s + c] * alpha));
|
|
301
|
+
}
|
|
302
|
+
if (dy !== prev) {
|
|
303
|
+
for (let i = 0; i < width; i++)
|
|
304
|
+
out[prev * width + i] = store(sum[i]);
|
|
305
|
+
for (let i = 0; i < width; i++)
|
|
306
|
+
sum[i] = f32(buf[i] * beta);
|
|
307
|
+
prev = dy;
|
|
308
|
+
}
|
|
309
|
+
else
|
|
310
|
+
for (let i = 0; i < width; i++)
|
|
311
|
+
sum[i] = f32(sum[i] + f32(buf[i] * beta));
|
|
312
|
+
}
|
|
313
|
+
for (let i = 0; i < width; i++)
|
|
314
|
+
out[prev * width + i] = store(sum[i]);
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
/** cv::resize INTER_AREA of a one-channel float32 image. Enlarging uses OpenCV's area-weighted bilinear. */
|
|
318
|
+
export function resizeArea32(src, sw, sh, dw, dh) {
|
|
319
|
+
if (sw === dw && sh === dh)
|
|
320
|
+
return src.slice();
|
|
321
|
+
if (dw <= sw && dh <= sh)
|
|
322
|
+
return resizeAreaDown(src, sw, sh, 1, dw, dh, false);
|
|
323
|
+
return resizeAreaUp32(src, sw, sh, dw, dh);
|
|
324
|
+
}
|
|
325
|
+
/** cv::resize INTER_AREA of an 8-bit image with `cn` channels, when shrinking. */
|
|
326
|
+
export function resizeArea8(src, sw, sh, cn, dw, dh) {
|
|
327
|
+
if (sw === dw && sh === dh)
|
|
328
|
+
return src.slice();
|
|
329
|
+
return Uint8Array.from(resizeAreaDown(src, sw, sh, cn, dw, dh, true));
|
|
330
|
+
}
|
|
331
|
+
function areaUpTaps(source, target) {
|
|
332
|
+
const scale = 1 / (target / source);
|
|
333
|
+
const inv = target / source;
|
|
334
|
+
const offset = new Int32Array(target);
|
|
335
|
+
const first = new Float32Array(target);
|
|
336
|
+
const second = new Float32Array(target);
|
|
337
|
+
for (let d = 0; d < target; d++) {
|
|
338
|
+
let s = Math.floor(d * scale);
|
|
339
|
+
let f = f32(d + 1 - (s + 1) * inv);
|
|
340
|
+
f = f <= 0 ? 0 : f32(f - Math.floor(f));
|
|
341
|
+
if (s < 0) {
|
|
342
|
+
f = 0;
|
|
343
|
+
s = 0;
|
|
344
|
+
}
|
|
345
|
+
if (s >= source - 1) {
|
|
346
|
+
f = 0;
|
|
347
|
+
s = source - 1;
|
|
348
|
+
}
|
|
349
|
+
offset[d] = s;
|
|
350
|
+
first[d] = f32(1 - f);
|
|
351
|
+
second[d] = f;
|
|
352
|
+
}
|
|
353
|
+
return { offset, first, second };
|
|
354
|
+
}
|
|
355
|
+
function resizeAreaUp32(src, sw, sh, dw, dh) {
|
|
356
|
+
const xs = areaUpTaps(sw, dw), ys = areaUpTaps(sh, dh);
|
|
357
|
+
const out = new Float32Array(dw * dh);
|
|
358
|
+
for (let y = 0; y < dh; y++) {
|
|
359
|
+
const y0 = ys.offset[y], y1 = Math.min(sh - 1, y0 + 1);
|
|
360
|
+
const b0 = ys.first[y], b1 = ys.second[y];
|
|
361
|
+
for (let x = 0; x < dw; x++) {
|
|
362
|
+
const x0 = xs.offset[x], x1 = Math.min(sw - 1, x0 + 1);
|
|
363
|
+
const a0 = xs.first[x], a1 = xs.second[x];
|
|
364
|
+
const r0 = f32(f32(src[y0 * sw + x0] * a0) + f32(src[y0 * sw + x1] * a1));
|
|
365
|
+
const r1 = f32(f32(src[y1 * sw + x0] * a0) + f32(src[y1 * sw + x1] * a1));
|
|
366
|
+
out[y * dw + x] = f32(f32(r0 * b0) + f32(r1 * b1));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
/** cv::resize INTER_NEAREST with `cn` interleaved channels. */
|
|
372
|
+
export function resizeNearest(src, sw, sh, cn, dw, dh) {
|
|
373
|
+
const out = new src.constructor(dw * dh * cn);
|
|
374
|
+
const ifx = 1 / (dw / sw), ify = 1 / (dh / sh);
|
|
375
|
+
for (let y = 0; y < dh; y++) {
|
|
376
|
+
const sy = Math.min(Math.floor(y * ify), sh - 1);
|
|
377
|
+
for (let x = 0; x < dw; x++) {
|
|
378
|
+
const sx = Math.min(Math.floor(x * ifx), sw - 1);
|
|
379
|
+
for (let c = 0; c < cn; c++)
|
|
380
|
+
out[(y * dw + x) * cn + c] = src[(sy * sw + sx) * cn + c];
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
/** OpenCV's fixed binomial kernels for small sizes when no sigma is given. */
|
|
386
|
+
const SMALL_KERNELS = {
|
|
387
|
+
3: [0.25, 0.5, 0.25],
|
|
388
|
+
5: [0.0625, 0.25, 0.375, 0.25, 0.0625],
|
|
389
|
+
7: [0.03125, 0.109375, 0.21875, 0.28125, 0.21875, 0.109375, 0.03125],
|
|
390
|
+
9: [4, 13, 30, 51, 60, 51, 30, 13, 4].map((v) => v / 256),
|
|
391
|
+
};
|
|
392
|
+
/** cv::getGaussianKernel as float32. */
|
|
393
|
+
function gaussianKernel(size, sigma) {
|
|
394
|
+
if (sigma <= 0 && SMALL_KERNELS[size])
|
|
395
|
+
return Float32Array.from(SMALL_KERNELS[size]);
|
|
396
|
+
const s = sigma > 0 ? sigma : size * 0.15 + 0.35;
|
|
397
|
+
const scale = -0.125 / (s * s);
|
|
398
|
+
const half = (size - 1) >> 1;
|
|
399
|
+
const values = [];
|
|
400
|
+
let sum = 0;
|
|
401
|
+
for (let i = 0, x = 1 - size; i < half; i++, x += 2) {
|
|
402
|
+
const t = Math.exp(x * x * scale);
|
|
403
|
+
values.push(t);
|
|
404
|
+
sum += t;
|
|
405
|
+
}
|
|
406
|
+
sum = sum * 2 + 1;
|
|
407
|
+
const mul = 1 / sum;
|
|
408
|
+
const kernel = new Float32Array(size);
|
|
409
|
+
for (let i = 0; i < half; i++)
|
|
410
|
+
kernel[i] = kernel[size - 1 - i] = values[i] * mul;
|
|
411
|
+
kernel[half] = mul;
|
|
412
|
+
return kernel;
|
|
413
|
+
}
|
|
414
|
+
/** Kernel size OpenCV derives from sigma for float images. */
|
|
415
|
+
export const gaussianSize = (sigma) => cvRound(sigma * 4 * 2 + 1) | 1;
|
|
416
|
+
/** BORDER_REFLECT_101 index. */
|
|
417
|
+
function reflect101(i, n) {
|
|
418
|
+
if (n === 1)
|
|
419
|
+
return 0;
|
|
420
|
+
while (i < 0 || i >= n)
|
|
421
|
+
i = i < 0 ? -i : 2 * n - 2 - i;
|
|
422
|
+
return i;
|
|
423
|
+
}
|
|
424
|
+
/** Source index of every position of a row padded by `r` on each side (BORDER_REFLECT_101). */
|
|
425
|
+
function reflectedIndices(n, r) {
|
|
426
|
+
const indices = new Int32Array(n + 2 * r);
|
|
427
|
+
for (let i = -r; i < n + r; i++)
|
|
428
|
+
indices[i + r] = reflect101(i, n);
|
|
429
|
+
return indices;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* One row of the float32 horizontal pass: `out[x]` is `fround(p * k + s)` chained over the taps in
|
|
433
|
+
* order from s = 0. Eight pixels run side by side so their chains overlap; each keeps its own order.
|
|
434
|
+
*/
|
|
435
|
+
function fmaRow32(padded, k, out, base, w) {
|
|
436
|
+
const size = k.length;
|
|
437
|
+
let x = 0;
|
|
438
|
+
for (; x + 7 < w; x += 8) {
|
|
439
|
+
let s0 = 0, s1 = 0, s2 = 0, s3 = 0, s4 = 0, s5 = 0, s6 = 0, s7 = 0;
|
|
440
|
+
for (let i = 0; i < size; i++) {
|
|
441
|
+
const q = k[i], p = x + i;
|
|
442
|
+
s0 = f32(padded[p] * q + s0);
|
|
443
|
+
s1 = f32(padded[p + 1] * q + s1);
|
|
444
|
+
s2 = f32(padded[p + 2] * q + s2);
|
|
445
|
+
s3 = f32(padded[p + 3] * q + s3);
|
|
446
|
+
s4 = f32(padded[p + 4] * q + s4);
|
|
447
|
+
s5 = f32(padded[p + 5] * q + s5);
|
|
448
|
+
s6 = f32(padded[p + 6] * q + s6);
|
|
449
|
+
s7 = f32(padded[p + 7] * q + s7);
|
|
450
|
+
}
|
|
451
|
+
const o = base + x;
|
|
452
|
+
out[o] = s0;
|
|
453
|
+
out[o + 1] = s1;
|
|
454
|
+
out[o + 2] = s2;
|
|
455
|
+
out[o + 3] = s3;
|
|
456
|
+
out[o + 4] = s4;
|
|
457
|
+
out[o + 5] = s5;
|
|
458
|
+
out[o + 6] = s6;
|
|
459
|
+
out[o + 7] = s7;
|
|
460
|
+
}
|
|
461
|
+
for (; x < w; x++) {
|
|
462
|
+
let s = 0;
|
|
463
|
+
for (let i = 0; i < size; i++)
|
|
464
|
+
s = f32(padded[x + i] * k[i] + s);
|
|
465
|
+
out[base + x] = s;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* One row of the float32 vertical pass: the centre row times `k[r]`, then for each distance the
|
|
470
|
+
* rounded sum of the rows above and below, fused in, nearest first. Eight pixels side by side.
|
|
471
|
+
*/
|
|
472
|
+
function symmetricColumn32(rows, k, r, ups, downs, out, o, w) {
|
|
473
|
+
const centre = k[r];
|
|
474
|
+
let x = 0;
|
|
475
|
+
for (; x + 7 < w; x += 8) {
|
|
476
|
+
const c = o + x;
|
|
477
|
+
let s0 = f32(rows[c] * centre), s1 = f32(rows[c + 1] * centre), s2 = f32(rows[c + 2] * centre), s3 = f32(rows[c + 3] * centre), s4 = f32(rows[c + 4] * centre), s5 = f32(rows[c + 5] * centre), s6 = f32(rows[c + 6] * centre), s7 = f32(rows[c + 7] * centre);
|
|
478
|
+
for (let i = 1; i <= r; i++) {
|
|
479
|
+
const q = k[r + i], u = ups[i] + x, d = downs[i] + x;
|
|
480
|
+
s0 = f32(f32(rows[d] + rows[u]) * q + s0);
|
|
481
|
+
s1 = f32(f32(rows[d + 1] + rows[u + 1]) * q + s1);
|
|
482
|
+
s2 = f32(f32(rows[d + 2] + rows[u + 2]) * q + s2);
|
|
483
|
+
s3 = f32(f32(rows[d + 3] + rows[u + 3]) * q + s3);
|
|
484
|
+
s4 = f32(f32(rows[d + 4] + rows[u + 4]) * q + s4);
|
|
485
|
+
s5 = f32(f32(rows[d + 5] + rows[u + 5]) * q + s5);
|
|
486
|
+
s6 = f32(f32(rows[d + 6] + rows[u + 6]) * q + s6);
|
|
487
|
+
s7 = f32(f32(rows[d + 7] + rows[u + 7]) * q + s7);
|
|
488
|
+
}
|
|
489
|
+
out[c] = s0;
|
|
490
|
+
out[c + 1] = s1;
|
|
491
|
+
out[c + 2] = s2;
|
|
492
|
+
out[c + 3] = s3;
|
|
493
|
+
out[c + 4] = s4;
|
|
494
|
+
out[c + 5] = s5;
|
|
495
|
+
out[c + 6] = s6;
|
|
496
|
+
out[c + 7] = s7;
|
|
497
|
+
}
|
|
498
|
+
for (; x < w; x++) {
|
|
499
|
+
let s = f32(rows[o + x] * centre);
|
|
500
|
+
for (let i = 1; i <= r; i++)
|
|
501
|
+
s = f32(f32(rows[downs[i] + x] + rows[ups[i] + x]) * k[r + i] + s);
|
|
502
|
+
out[o + x] = s;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/** cv::GaussianBlur of a one-channel float32 image (separable, REFLECT_101, FMA accumulation). */
|
|
506
|
+
export function gaussianBlur32(src, w, h, size, sigma) {
|
|
507
|
+
const k = gaussianKernel(w === 1 ? 1 : size, sigma);
|
|
508
|
+
const r = (size - 1) >> 1;
|
|
509
|
+
// A one-pixel-wide image has a one-tap kernel but still reads `size` taps (as undefined).
|
|
510
|
+
const taps = k.length === size ? k : Float32Array.from({ length: size }, (_, i) => k[i] ?? NaN);
|
|
511
|
+
const rows = new Float32Array(w * h);
|
|
512
|
+
const padded = new Float32Array(w + 2 * r);
|
|
513
|
+
const columns = reflectedIndices(w, r);
|
|
514
|
+
for (let y = 0; y < h; y++) {
|
|
515
|
+
const base = y * w;
|
|
516
|
+
for (let i = 0; i < padded.length; i++)
|
|
517
|
+
padded[i] = src[base + columns[i]];
|
|
518
|
+
if (size > 5) {
|
|
519
|
+
fmaRow32(padded, taps, rows, base, w);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
// SymmRowSmallVec_32f: ((left + right) * k1, then centre, then the outer pair) fused.
|
|
523
|
+
const k0 = taps[r], k1 = taps[r + 1], k2 = taps[r + 2];
|
|
524
|
+
for (let x = 0; x < w; x++) {
|
|
525
|
+
const c = x + r;
|
|
526
|
+
let s = fma32(padded[c], k0, f32(f32(padded[c - 1] + padded[c + 1]) * k1));
|
|
527
|
+
if (size === 5)
|
|
528
|
+
s = fma32(f32(padded[c + 2] + padded[c - 2]), k2, s);
|
|
529
|
+
rows[base + x] = s;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const out = new Float32Array(w * h);
|
|
533
|
+
const lines = reflectedIndices(h, r);
|
|
534
|
+
const ups = new Int32Array(r + 1), downs = new Int32Array(r + 1);
|
|
535
|
+
for (let y = 0; y < h; y++) {
|
|
536
|
+
for (let i = 1; i <= r; i++) {
|
|
537
|
+
ups[i] = lines[y - i + r] * w;
|
|
538
|
+
downs[i] = lines[y + i + r] * w;
|
|
539
|
+
}
|
|
540
|
+
symmetricColumn32(rows, taps, r, ups, downs, out, y * w, w);
|
|
541
|
+
}
|
|
542
|
+
return out;
|
|
543
|
+
}
|
|
544
|
+
/** 3x3 rectangle or cross structuring element. */
|
|
545
|
+
export var MorphShape;
|
|
546
|
+
(function (MorphShape) {
|
|
547
|
+
MorphShape["Rect"] = "rect";
|
|
548
|
+
MorphShape["Cross"] = "cross";
|
|
549
|
+
})(MorphShape || (MorphShape = {}));
|
|
550
|
+
/**
|
|
551
|
+
* cv::erode / cv::dilate of an interleaved 8-bit image with a 3x3 element, `iterations` times.
|
|
552
|
+
* Pixels outside the image are ignored, as with OpenCV's default border value.
|
|
553
|
+
*/
|
|
554
|
+
export function morph8(src, w, h, cn, shape, erode, iterations) {
|
|
555
|
+
// A dilation is the erosion of the inverted image, inverted back: max(a, b) = 255 - min(255 - a, 255 - b).
|
|
556
|
+
if (!erode)
|
|
557
|
+
return invert8(erode8(invert8(src), w, h, cn, shape, iterations));
|
|
558
|
+
return erode8(src, w, h, cn, shape, iterations);
|
|
559
|
+
}
|
|
560
|
+
function invert8(src) {
|
|
561
|
+
const out = new Uint8Array(src.length);
|
|
562
|
+
for (let i = 0; i < src.length; i++)
|
|
563
|
+
out[i] = 255 - src[i];
|
|
564
|
+
return out;
|
|
565
|
+
}
|
|
566
|
+
/** Minimum of an element and its left and right neighbours that lie inside the row. */
|
|
567
|
+
function rowEdgeMin(cur, i, row, end, cn) {
|
|
568
|
+
let v = cur[i];
|
|
569
|
+
if (i - cn >= row && cur[i - cn] < v)
|
|
570
|
+
v = cur[i - cn];
|
|
571
|
+
if (i + cn < end && cur[i + cn] < v)
|
|
572
|
+
v = cur[i + cn];
|
|
573
|
+
return v;
|
|
574
|
+
}
|
|
575
|
+
function erode8(src, w, h, cn, shape, iterations) {
|
|
576
|
+
const stride = w * cn;
|
|
577
|
+
// Clamping a neighbour index to the image repeats a pixel already in the window, which is the
|
|
578
|
+
// same as ignoring pixels outside the image.
|
|
579
|
+
let cur = src;
|
|
580
|
+
const across = new Uint8Array(src.length);
|
|
581
|
+
for (let it = 0; it < iterations; it++) {
|
|
582
|
+
for (let y = 0; y < h; y++) {
|
|
583
|
+
const row = y * stride, end = row + stride;
|
|
584
|
+
// The first and last pixel of a row have one neighbour; the rest two.
|
|
585
|
+
const head = Math.min(end, row + cn), tail = Math.max(head, end - cn);
|
|
586
|
+
for (let i = row; i < head; i++)
|
|
587
|
+
across[i] = rowEdgeMin(cur, i, row, end, cn);
|
|
588
|
+
for (let i = head; i < tail; i++) {
|
|
589
|
+
let v = cur[i - cn];
|
|
590
|
+
if (cur[i] < v)
|
|
591
|
+
v = cur[i];
|
|
592
|
+
if (cur[i + cn] < v)
|
|
593
|
+
v = cur[i + cn];
|
|
594
|
+
across[i] = v;
|
|
595
|
+
}
|
|
596
|
+
for (let i = tail; i < end; i++)
|
|
597
|
+
across[i] = rowEdgeMin(cur, i, row, end, cn);
|
|
598
|
+
}
|
|
599
|
+
const next = new Uint8Array(src.length);
|
|
600
|
+
// Rect: the horizontal result of the rows above and below; cross: only their centre pixel.
|
|
601
|
+
const vertical = shape === MorphShape.Rect ? across : cur;
|
|
602
|
+
for (let y = 0; y < h; y++) {
|
|
603
|
+
const up = Math.max(0, y - 1) * stride - y * stride, down = Math.min(h - 1, y + 1) * stride - y * stride;
|
|
604
|
+
for (let i = y * stride, end = i + stride; i < end; i++) {
|
|
605
|
+
let v = across[i];
|
|
606
|
+
if (vertical[i + up] < v)
|
|
607
|
+
v = vertical[i + up];
|
|
608
|
+
if (vertical[i + down] < v)
|
|
609
|
+
v = vertical[i + down];
|
|
610
|
+
next[i] = v;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
cur = next;
|
|
614
|
+
}
|
|
615
|
+
return cur;
|
|
616
|
+
}
|
|
617
|
+
const PAIRWISE_BLOCK = 128;
|
|
618
|
+
/** NumPy's float32 pairwise summation (np.add.reduce over a contiguous array). */
|
|
619
|
+
export function pairwiseSum32(a, start = 0, n = a.length) {
|
|
620
|
+
if (n < 8) {
|
|
621
|
+
let r = 0;
|
|
622
|
+
for (let i = 0; i < n; i++)
|
|
623
|
+
r = f32(r + a[start + i]);
|
|
624
|
+
return r;
|
|
625
|
+
}
|
|
626
|
+
if (n <= PAIRWISE_BLOCK) {
|
|
627
|
+
// Eight float32 lanes, as NumPy's unrolled loop.
|
|
628
|
+
let r0 = f32(a[start]), r1 = f32(a[start + 1]), r2 = f32(a[start + 2]), r3 = f32(a[start + 3]), r4 = f32(a[start + 4]), r5 = f32(a[start + 5]), r6 = f32(a[start + 6]), r7 = f32(a[start + 7]);
|
|
629
|
+
let i = 8;
|
|
630
|
+
for (; i < n - (n % 8); i += 8) {
|
|
631
|
+
const at = start + i;
|
|
632
|
+
r0 = f32(r0 + a[at]);
|
|
633
|
+
r1 = f32(r1 + a[at + 1]);
|
|
634
|
+
r2 = f32(r2 + a[at + 2]);
|
|
635
|
+
r3 = f32(r3 + a[at + 3]);
|
|
636
|
+
r4 = f32(r4 + a[at + 4]);
|
|
637
|
+
r5 = f32(r5 + a[at + 5]);
|
|
638
|
+
r6 = f32(r6 + a[at + 6]);
|
|
639
|
+
r7 = f32(r7 + a[at + 7]);
|
|
640
|
+
}
|
|
641
|
+
let res = f32(f32(f32(r0 + r1) + f32(r2 + r3)) + f32(f32(r4 + r5) + f32(r6 + r7)));
|
|
642
|
+
for (; i < n; i++)
|
|
643
|
+
res = f32(res + a[start + i]);
|
|
644
|
+
return res;
|
|
645
|
+
}
|
|
646
|
+
let n2 = Math.floor(n / 2);
|
|
647
|
+
n2 -= n2 % 8;
|
|
648
|
+
return f32(pairwiseSum32(a, start, n2) + pairwiseSum32(a, start + n2, n - n2));
|
|
649
|
+
}
|
|
650
|
+
/** cv::GaussianBlur's bit-exact 8-bit kernel: 8 fraction bits, rounded with error diffusion. */
|
|
651
|
+
function fixedKernel8(size) {
|
|
652
|
+
const k = Float64Array.from(SMALL_KERNELS[size] ?? gaussianKernel64(size, size * 0.15 + 0.35));
|
|
653
|
+
const out = new Int32Array(size);
|
|
654
|
+
const half = size >> 1;
|
|
655
|
+
let err = 0, sum = 0;
|
|
656
|
+
for (let i = 0; i < half; i++) {
|
|
657
|
+
const adjusted = k[i] * 256 + err;
|
|
658
|
+
const v = cvRound(adjusted);
|
|
659
|
+
err = adjusted - v;
|
|
660
|
+
out[i] = out[size - 1 - i] = v;
|
|
661
|
+
sum += v;
|
|
662
|
+
}
|
|
663
|
+
out[half] = 256 - 2 * sum;
|
|
664
|
+
return out;
|
|
665
|
+
}
|
|
666
|
+
function gaussianKernel64(size, sigma) {
|
|
667
|
+
const scale = -0.125 / (sigma * sigma);
|
|
668
|
+
const half = (size - 1) >> 1;
|
|
669
|
+
const values = [];
|
|
670
|
+
let sum = 0;
|
|
671
|
+
for (let i = 0, x = 1 - size; i < half; i++, x += 2) {
|
|
672
|
+
const t = Math.exp(x * x * scale);
|
|
673
|
+
values.push(t);
|
|
674
|
+
sum += t;
|
|
675
|
+
}
|
|
676
|
+
const mul = 1 / (sum * 2 + 1);
|
|
677
|
+
const kernel = new Float64Array(size);
|
|
678
|
+
for (let i = 0; i < half; i++)
|
|
679
|
+
kernel[i] = kernel[size - 1 - i] = values[i] * mul;
|
|
680
|
+
kernel[half] = mul;
|
|
681
|
+
return kernel;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Integer sums `k[r] * src[c] + k[r + i] * (src[c + lefts[i]] + src[c + rights[i]])` for the `w`
|
|
685
|
+
* pixels c = centre + x, eight side by side. Integer sums are exact in any order, so mirrored taps
|
|
686
|
+
* (the kernel is symmetric) share a multiply and zero taps are skipped.
|
|
687
|
+
*/
|
|
688
|
+
function symmetricSums(src, k, r, centre, lefts, rights, out, w) {
|
|
689
|
+
const middle = k[r];
|
|
690
|
+
let x = 0;
|
|
691
|
+
for (; x + 7 < w; x += 8) {
|
|
692
|
+
const c = centre + x;
|
|
693
|
+
let s0 = middle * src[c], s1 = middle * src[c + 1], s2 = middle * src[c + 2], s3 = middle * src[c + 3], s4 = middle * src[c + 4], s5 = middle * src[c + 5], s6 = middle * src[c + 6], s7 = middle * src[c + 7];
|
|
694
|
+
for (let i = 1; i <= r; i++) {
|
|
695
|
+
const q = k[r + i];
|
|
696
|
+
if (!q)
|
|
697
|
+
continue;
|
|
698
|
+
const a = c + lefts[i], b = c + rights[i];
|
|
699
|
+
s0 += q * (src[a] + src[b]);
|
|
700
|
+
s1 += q * (src[a + 1] + src[b + 1]);
|
|
701
|
+
s2 += q * (src[a + 2] + src[b + 2]);
|
|
702
|
+
s3 += q * (src[a + 3] + src[b + 3]);
|
|
703
|
+
s4 += q * (src[a + 4] + src[b + 4]);
|
|
704
|
+
s5 += q * (src[a + 5] + src[b + 5]);
|
|
705
|
+
s6 += q * (src[a + 6] + src[b + 6]);
|
|
706
|
+
s7 += q * (src[a + 7] + src[b + 7]);
|
|
707
|
+
}
|
|
708
|
+
out[x] = s0;
|
|
709
|
+
out[x + 1] = s1;
|
|
710
|
+
out[x + 2] = s2;
|
|
711
|
+
out[x + 3] = s3;
|
|
712
|
+
out[x + 4] = s4;
|
|
713
|
+
out[x + 5] = s5;
|
|
714
|
+
out[x + 6] = s6;
|
|
715
|
+
out[x + 7] = s7;
|
|
716
|
+
}
|
|
717
|
+
for (; x < w; x++) {
|
|
718
|
+
const c = centre + x;
|
|
719
|
+
let sum = middle * src[c];
|
|
720
|
+
for (let i = 1; i <= r; i++)
|
|
721
|
+
sum += k[r + i] * (src[c + lefts[i]] + src[c + rights[i]]);
|
|
722
|
+
out[x] = sum;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/** cv::GaussianBlur of a one-channel 8-bit image with sigma 0 (OpenCV's bit-exact fixed point). */
|
|
726
|
+
export function gaussianBlur8(src, w, h, size) {
|
|
727
|
+
const k = fixedKernel8(size);
|
|
728
|
+
const r = size >> 1;
|
|
729
|
+
const rows = new Int32Array(w * h);
|
|
730
|
+
const padded = new Int32Array(w + 2 * r);
|
|
731
|
+
const columns = reflectedIndices(w, r);
|
|
732
|
+
const sums = new Int32Array(w);
|
|
733
|
+
const lefts = new Int32Array(r + 1), rights = new Int32Array(r + 1);
|
|
734
|
+
for (let i = 1; i <= r; i++) {
|
|
735
|
+
lefts[i] = -i;
|
|
736
|
+
rights[i] = i;
|
|
737
|
+
}
|
|
738
|
+
for (let y = 0; y < h; y++) {
|
|
739
|
+
const base = y * w;
|
|
740
|
+
for (let i = 0; i < padded.length; i++)
|
|
741
|
+
padded[i] = src[base + columns[i]];
|
|
742
|
+
symmetricSums(padded, k, r, r, lefts, rights, sums, w);
|
|
743
|
+
for (let x = 0; x < w; x++)
|
|
744
|
+
rows[base + x] = sums[x] > 0xffff ? 0xffff : sums[x];
|
|
745
|
+
}
|
|
746
|
+
const out = new Uint8Array(w * h);
|
|
747
|
+
const lines = reflectedIndices(h, r);
|
|
748
|
+
for (let y = 0; y < h; y++) {
|
|
749
|
+
const o = y * w;
|
|
750
|
+
for (let i = 1; i <= r; i++) {
|
|
751
|
+
lefts[i] = lines[y - i + r] * w - o;
|
|
752
|
+
rights[i] = lines[y + i + r] * w - o;
|
|
753
|
+
}
|
|
754
|
+
symmetricSums(rows, k, r, o, lefts, rights, sums, w);
|
|
755
|
+
for (let x = 0; x < w; x++)
|
|
756
|
+
out[o + x] = Math.min(255, (sums[x] + (1 << 15)) >>> 16);
|
|
757
|
+
}
|
|
758
|
+
return out;
|
|
759
|
+
}
|