edge_det 0.1.0 → 0.1.3
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 +202 -0
- package/README.md +100 -0
- package/dist/edge_det.js +99 -0
- package/dist/index.d.ts +12 -0
- package/dist/wasm_bytes.d.ts +1 -0
- package/package.json +12 -2
- package/.github/workflows/build.yml +0 -36
- package/Cargo.lock +0 -114
- package/Cargo.toml +0 -19
- package/edge_det-0.1.0.tgz +0 -0
- package/pnpm-workspace.yaml +0 -2
- package/scripts/build-wasm.mjs +0 -19
- package/src/lib.rs +0 -327
- package/src_ts/index.ts +0 -68
- package/tests/detection.test.ts +0 -238
- package/tsconfig.json +0 -14
- package/vite.config.ts +0 -10
- package/vitest.config.ts +0 -7
package/src/lib.rs
DELETED
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
use wasm_bindgen::prelude::*;
|
|
2
|
-
|
|
3
|
-
const GAUSS: [f32; 5] = [1.0, 4.0, 6.0, 4.0, 1.0];
|
|
4
|
-
const GAUSS_SUM: f32 = 16.0;
|
|
5
|
-
|
|
6
|
-
fn grayscale(data: &[u8], w: usize, h: usize) -> Vec<f32> {
|
|
7
|
-
let n = w * h;
|
|
8
|
-
let mut g = vec![0.0f32; n];
|
|
9
|
-
for i in 0..n {
|
|
10
|
-
let o = i * 4;
|
|
11
|
-
g[i] = 0.299 * data[o] as f32 + 0.587 * data[o + 1] as f32 + 0.114 * data[o + 2] as f32;
|
|
12
|
-
}
|
|
13
|
-
g
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
fn blur(img: &[f32], w: usize, h: usize) -> Vec<f32> {
|
|
17
|
-
let mut tmp = vec![0.0f32; w * h];
|
|
18
|
-
let mut out = vec![0.0f32; w * h];
|
|
19
|
-
for y in 0..h {
|
|
20
|
-
for x in 0..w {
|
|
21
|
-
let mut s = 0.0f32;
|
|
22
|
-
for k in 0usize..5 {
|
|
23
|
-
let xx = ((x as i32) + k as i32 - 2).max(0).min(w as i32 - 1) as usize;
|
|
24
|
-
s += img[y * w + xx] * GAUSS[k];
|
|
25
|
-
}
|
|
26
|
-
tmp[y * w + x] = s / GAUSS_SUM;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
for y in 0..h {
|
|
30
|
-
for x in 0..w {
|
|
31
|
-
let mut s = 0.0f32;
|
|
32
|
-
for k in 0usize..5 {
|
|
33
|
-
let yy = ((y as i32) + k as i32 - 2).max(0).min(h as i32 - 1) as usize;
|
|
34
|
-
s += tmp[yy * w + x] * GAUSS[k];
|
|
35
|
-
}
|
|
36
|
-
out[y * w + x] = s / GAUSS_SUM;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
out
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
struct SobelResult {
|
|
43
|
-
mag: Vec<f32>,
|
|
44
|
-
dx: Vec<f32>,
|
|
45
|
-
dy: Vec<f32>,
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
fn sobel(img: &[f32], w: usize, h: usize) -> SobelResult {
|
|
49
|
-
let n = w * h;
|
|
50
|
-
let mut mag = vec![0.0f32; n];
|
|
51
|
-
let mut dx = vec![0.0f32; n];
|
|
52
|
-
let mut dy = vec![0.0f32; n];
|
|
53
|
-
for y in 1..h - 1 {
|
|
54
|
-
for x in 1..w - 1 {
|
|
55
|
-
let i = y * w + x;
|
|
56
|
-
let sx = -img[i - w - 1] - 2.0 * img[i - 1] - img[i + w - 1]
|
|
57
|
-
+ img[i - w + 1] + 2.0 * img[i + 1] + img[i + w + 1];
|
|
58
|
-
let sy = -img[i - w - 1] - 2.0 * img[i - w] - img[i - w + 1]
|
|
59
|
-
+ img[i + w - 1] + 2.0 * img[i + w] + img[i + w + 1];
|
|
60
|
-
dx[i] = sx;
|
|
61
|
-
dy[i] = sy;
|
|
62
|
-
mag[i] = (sx * sx + sy * sy).sqrt();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
SobelResult { mag, dx, dy }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
fn nms(result: &SobelResult, w: usize, h: usize) -> Vec<f32> {
|
|
69
|
-
let mut out = vec![0.0f32; w * h];
|
|
70
|
-
for y in 1..h - 1 {
|
|
71
|
-
for x in 1..w - 1 {
|
|
72
|
-
let i = y * w + x;
|
|
73
|
-
let m = result.mag[i];
|
|
74
|
-
if m < 0.5 {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
let gx = result.dx[i];
|
|
78
|
-
let gy = result.dy[i];
|
|
79
|
-
let abs_gx = gx.abs();
|
|
80
|
-
let abs_gy = gy.abs();
|
|
81
|
-
|
|
82
|
-
let (mut mag1, mut mag2) = (0.0f32, 0.0f32);
|
|
83
|
-
if abs_gx > abs_gy {
|
|
84
|
-
let t = abs_gy / abs_gx;
|
|
85
|
-
if gx * gy > 0.0 {
|
|
86
|
-
mag1 = result.mag[i - w - 1] * (1.0 - t) + result.mag[i - w] * t;
|
|
87
|
-
mag2 = result.mag[i + w + 1] * (1.0 - t) + result.mag[i + w] * t;
|
|
88
|
-
} else {
|
|
89
|
-
mag1 = result.mag[i - w + 1] * (1.0 - t) + result.mag[i - w] * t;
|
|
90
|
-
mag2 = result.mag[i + w - 1] * (1.0 - t) + result.mag[i + w] * t;
|
|
91
|
-
}
|
|
92
|
-
} else if abs_gy > 0.0 {
|
|
93
|
-
let t = abs_gx / abs_gy;
|
|
94
|
-
if gx * gy > 0.0 {
|
|
95
|
-
mag1 = result.mag[i - w - 1] * (1.0 - t) + result.mag[i - 1] * t;
|
|
96
|
-
mag2 = result.mag[i + w + 1] * (1.0 - t) + result.mag[i + 1] * t;
|
|
97
|
-
} else {
|
|
98
|
-
mag1 = result.mag[i - w + 1] * (1.0 - t) + result.mag[i + 1] * t;
|
|
99
|
-
mag2 = result.mag[i + w - 1] * (1.0 - t) + result.mag[i - 1] * t;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
out[i] = if m >= mag1 && m >= mag2 { m } else { 0.0 };
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
out
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
fn hysteresis(edges: &mut [u8], w: usize, h: usize) {
|
|
109
|
-
let mut stack: Vec<(i32, i32)> = Vec::new();
|
|
110
|
-
for y in 0..h {
|
|
111
|
-
for x in 0..w {
|
|
112
|
-
if edges[y * w + x] == 2 {
|
|
113
|
-
stack.push((x as i32, y as i32));
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
while let Some((x, y)) = stack.pop() {
|
|
118
|
-
for dy in -1..=1 {
|
|
119
|
-
for dx in -1..=1 {
|
|
120
|
-
let nx = x + dx;
|
|
121
|
-
let ny = y + dy;
|
|
122
|
-
if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
|
|
123
|
-
let ni = (ny * w as i32 + nx) as usize;
|
|
124
|
-
if edges[ni] == 1 {
|
|
125
|
-
edges[ni] = 2;
|
|
126
|
-
stack.push((nx, ny));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
for e in edges.iter_mut() {
|
|
133
|
-
if *e == 1 {
|
|
134
|
-
*e = 0;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
struct UF {
|
|
140
|
-
p: Vec<usize>,
|
|
141
|
-
r: Vec<u8>,
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
impl UF {
|
|
145
|
-
fn new(n: usize) -> Self {
|
|
146
|
-
Self {
|
|
147
|
-
p: (0..n).collect(),
|
|
148
|
-
r: vec![0; n],
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
fn find(&mut self, x: usize) -> usize {
|
|
152
|
-
if self.p[x] != x {
|
|
153
|
-
self.p[x] = self.find(self.p[x]);
|
|
154
|
-
}
|
|
155
|
-
self.p[x]
|
|
156
|
-
}
|
|
157
|
-
fn union(&mut self, a: usize, b: usize) {
|
|
158
|
-
let ra = self.find(a);
|
|
159
|
-
let rb = self.find(b);
|
|
160
|
-
if ra == rb {
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
if self.r[ra] < self.r[rb] {
|
|
164
|
-
self.p[ra] = rb;
|
|
165
|
-
} else if self.r[ra] > self.r[rb] {
|
|
166
|
-
self.p[rb] = ra;
|
|
167
|
-
} else {
|
|
168
|
-
self.p[rb] = ra;
|
|
169
|
-
self.r[ra] += 1;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
fn bounding_boxes(edges: &[u8], w: usize, h: usize, min_area: usize) -> Vec<[i32; 4]> {
|
|
175
|
-
let n = w * h;
|
|
176
|
-
let mut uf = UF::new(n);
|
|
177
|
-
for y in 0..h {
|
|
178
|
-
for x in 0..w {
|
|
179
|
-
let i = y * w + x;
|
|
180
|
-
if edges[i] == 0 {
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
if x + 1 < w && edges[i + 1] != 0 {
|
|
184
|
-
uf.union(i, i + 1);
|
|
185
|
-
}
|
|
186
|
-
if y + 1 < h && edges[i + w] != 0 {
|
|
187
|
-
uf.union(i, i + w);
|
|
188
|
-
}
|
|
189
|
-
if x + 1 < w && y + 1 < h && edges[i + w + 1] != 0 {
|
|
190
|
-
uf.union(i, i + w + 1);
|
|
191
|
-
}
|
|
192
|
-
if x > 0 && y + 1 < h && edges[i + w - 1] != 0 {
|
|
193
|
-
uf.union(i, i + w - 1);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
let mut x0 = vec![w as i32; n];
|
|
198
|
-
let mut y0 = vec![h as i32; n];
|
|
199
|
-
let mut x1 = vec![0i32; n];
|
|
200
|
-
let mut y1 = vec![0i32; n];
|
|
201
|
-
let mut cnt = vec![0usize; n];
|
|
202
|
-
for y in 0..h {
|
|
203
|
-
for x in 0..w {
|
|
204
|
-
let i = y * w + x;
|
|
205
|
-
if edges[i] == 0 {
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
let r = uf.find(i);
|
|
209
|
-
x0[r] = x0[r].min(x as i32);
|
|
210
|
-
y0[r] = y0[r].min(y as i32);
|
|
211
|
-
x1[r] = x1[r].max(x as i32);
|
|
212
|
-
y1[r] = y1[r].max(y as i32);
|
|
213
|
-
cnt[r] += 1;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
let mut boxes: Vec<[i32; 4]> = Vec::new();
|
|
217
|
-
for i in 0..n {
|
|
218
|
-
if cnt[i] >= min_area {
|
|
219
|
-
boxes.push([x0[i], y0[i], x1[i] - x0[i] + 1, y1[i] - y0[i] + 1]);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
boxes.sort_by(|a, b| (b[2] * b[3]).cmp(&(a[2] * a[3])));
|
|
223
|
-
boxes
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
fn color_gradient(data: &[u8], w: usize, h: usize) -> SobelResult {
|
|
227
|
-
let n = w * h;
|
|
228
|
-
let mut r_ch = vec![0.0f32; n];
|
|
229
|
-
let mut g_ch = vec![0.0f32; n];
|
|
230
|
-
let mut b_ch = vec![0.0f32; n];
|
|
231
|
-
for i in 0..n {
|
|
232
|
-
let o = i * 4;
|
|
233
|
-
r_ch[i] = data[o] as f32;
|
|
234
|
-
g_ch[i] = data[o + 1] as f32;
|
|
235
|
-
b_ch[i] = data[o + 2] as f32;
|
|
236
|
-
}
|
|
237
|
-
let rb = blur(&r_ch, w, h);
|
|
238
|
-
let gb = blur(&g_ch, w, h);
|
|
239
|
-
let bb = blur(&b_ch, w, h);
|
|
240
|
-
|
|
241
|
-
let mut mag = vec![0.0f32; n];
|
|
242
|
-
let mut dx = vec![0.0f32; n];
|
|
243
|
-
let mut dy = vec![0.0f32; n];
|
|
244
|
-
for y in 1..h - 1 {
|
|
245
|
-
for x in 1..w - 1 {
|
|
246
|
-
let i = y * w + x;
|
|
247
|
-
let dr_x = rb[i + 1] - rb[i - 1];
|
|
248
|
-
let dg_x = gb[i + 1] - gb[i - 1];
|
|
249
|
-
let db_x = bb[i + 1] - bb[i - 1];
|
|
250
|
-
let dr_y = rb[i + w] - rb[i - w];
|
|
251
|
-
let dg_y = gb[i + w] - gb[i - w];
|
|
252
|
-
let db_y = bb[i + w] - bb[i - w];
|
|
253
|
-
let sx = (dr_x * dr_x + dg_x * dg_x + db_x * db_x).sqrt();
|
|
254
|
-
let sy = (dr_y * dr_y + dg_y * dg_y + db_y * db_y).sqrt();
|
|
255
|
-
let sign_x = if dr_x + dg_x + db_x >= 0.0 { 1.0 } else { -1.0 };
|
|
256
|
-
let sign_y = if dr_y + dg_y + db_y >= 0.0 { 1.0 } else { -1.0 };
|
|
257
|
-
dx[i] = sx * sign_x;
|
|
258
|
-
dy[i] = sy * sign_y;
|
|
259
|
-
mag[i] = (sx * sx + sy * sy).sqrt();
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
SobelResult { mag, dx, dy }
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
fn merge_and_threshold(
|
|
266
|
-
gray_sup: &[f32],
|
|
267
|
-
color_sup: &[f32],
|
|
268
|
-
w: usize,
|
|
269
|
-
h: usize,
|
|
270
|
-
lo: f32,
|
|
271
|
-
hi: f32,
|
|
272
|
-
) -> Vec<u8> {
|
|
273
|
-
let n = w * h;
|
|
274
|
-
let mut out = vec![0u8; n];
|
|
275
|
-
for i in 0..n {
|
|
276
|
-
let v = gray_sup[i].max(color_sup[i]);
|
|
277
|
-
if v >= hi {
|
|
278
|
-
out[i] = 2;
|
|
279
|
-
} else if v >= lo {
|
|
280
|
-
out[i] = 1;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
out
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
#[wasm_bindgen]
|
|
287
|
-
pub fn detect_borders(
|
|
288
|
-
data: &[u8],
|
|
289
|
-
width: usize,
|
|
290
|
-
height: usize,
|
|
291
|
-
low_threshold: f32,
|
|
292
|
-
high_threshold: f32,
|
|
293
|
-
min_area: usize,
|
|
294
|
-
) -> Vec<f32> {
|
|
295
|
-
let gray = grayscale(data, width, height);
|
|
296
|
-
let blurred = blur(&gray, width, height);
|
|
297
|
-
let gray_sobel = sobel(&blurred, width, height);
|
|
298
|
-
let gray_sup = nms(&gray_sobel, width, height);
|
|
299
|
-
|
|
300
|
-
let color_sobel = color_gradient(data, width, height);
|
|
301
|
-
let color_sup = nms(&color_sobel, width, height);
|
|
302
|
-
|
|
303
|
-
let mut edges = merge_and_threshold(
|
|
304
|
-
&gray_sup,
|
|
305
|
-
&color_sup,
|
|
306
|
-
width,
|
|
307
|
-
height,
|
|
308
|
-
low_threshold,
|
|
309
|
-
high_threshold,
|
|
310
|
-
);
|
|
311
|
-
hysteresis(&mut edges, width, height);
|
|
312
|
-
|
|
313
|
-
let boxes = bounding_boxes(&edges, width, height, min_area);
|
|
314
|
-
let mut out = Vec::with_capacity(boxes.len() * 4);
|
|
315
|
-
for b in &boxes {
|
|
316
|
-
out.push(b[0] as f32);
|
|
317
|
-
out.push(b[1] as f32);
|
|
318
|
-
out.push(b[2] as f32);
|
|
319
|
-
out.push(b[3] as f32);
|
|
320
|
-
}
|
|
321
|
-
out
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
#[wasm_bindgen]
|
|
325
|
-
pub fn detect_borders_default(data: &[u8], width: usize, height: usize) -> Vec<f32> {
|
|
326
|
-
detect_borders(data, width, height, 20.0, 60.0, 100)
|
|
327
|
-
}
|
package/src_ts/index.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
initSync,
|
|
3
|
-
detect_borders,
|
|
4
|
-
detect_borders_default,
|
|
5
|
-
} from '../pkg/edge_det.js'
|
|
6
|
-
import { WASM_BYTES } from './wasm_bytes'
|
|
7
|
-
|
|
8
|
-
export interface Border {
|
|
9
|
-
x: number
|
|
10
|
-
y: number
|
|
11
|
-
w: number
|
|
12
|
-
h: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
let _initialized = false
|
|
16
|
-
|
|
17
|
-
function ensureInit() {
|
|
18
|
-
if (!_initialized) {
|
|
19
|
-
initSync({ module: WASM_BYTES })
|
|
20
|
-
_initialized = true
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function detectBorders(
|
|
25
|
-
data: Uint8Array,
|
|
26
|
-
width: number,
|
|
27
|
-
height: number,
|
|
28
|
-
options?: { lowThreshold?: number; highThreshold?: number; minArea?: number }
|
|
29
|
-
): Border[] {
|
|
30
|
-
ensureInit()
|
|
31
|
-
const result = detect_borders(
|
|
32
|
-
data,
|
|
33
|
-
width,
|
|
34
|
-
height,
|
|
35
|
-
options?.lowThreshold ?? 20,
|
|
36
|
-
options?.highThreshold ?? 60,
|
|
37
|
-
options?.minArea ?? 100
|
|
38
|
-
)
|
|
39
|
-
const borders: Border[] = []
|
|
40
|
-
for (let i = 0; i < result.length; i += 4) {
|
|
41
|
-
borders.push({
|
|
42
|
-
x: result[i],
|
|
43
|
-
y: result[i + 1],
|
|
44
|
-
w: result[i + 2],
|
|
45
|
-
h: result[i + 3],
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
return borders
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function detectBordersDefault(
|
|
52
|
-
data: Uint8Array,
|
|
53
|
-
width: number,
|
|
54
|
-
height: number
|
|
55
|
-
): Border[] {
|
|
56
|
-
ensureInit()
|
|
57
|
-
const result = detect_borders_default(data, width, height)
|
|
58
|
-
const borders: Border[] = []
|
|
59
|
-
for (let i = 0; i < result.length; i += 4) {
|
|
60
|
-
borders.push({
|
|
61
|
-
x: result[i],
|
|
62
|
-
y: result[i + 1],
|
|
63
|
-
w: result[i + 2],
|
|
64
|
-
h: result[i + 3],
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
return borders
|
|
68
|
-
}
|
package/tests/detection.test.ts
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import * as PImage from 'pureimage'
|
|
3
|
-
import { detectBorders, detectBordersDefault, Border } from '../src_ts/index'
|
|
4
|
-
|
|
5
|
-
function createImage(
|
|
6
|
-
w: number,
|
|
7
|
-
h: number,
|
|
8
|
-
draw: (ctx: PImage.Context) => void
|
|
9
|
-
): Uint8Array {
|
|
10
|
-
const img = PImage.make(w, h, {})
|
|
11
|
-
const ctx = img.getContext('2d')
|
|
12
|
-
ctx.fillStyle = '#ffffff'
|
|
13
|
-
ctx.fillRect(0, 0, w, h)
|
|
14
|
-
draw(ctx)
|
|
15
|
-
const buf = img.data
|
|
16
|
-
const rgba = new Uint8Array(w * h * 4)
|
|
17
|
-
for (let i = 0; i < w * h; i++) {
|
|
18
|
-
rgba[i * 4] = buf[i * 4]
|
|
19
|
-
rgba[i * 4 + 1] = buf[i * 4 + 1]
|
|
20
|
-
rgba[i * 4 + 2] = buf[i * 4 + 2]
|
|
21
|
-
rgba[i * 4 + 3] = buf[i * 4 + 3]
|
|
22
|
-
}
|
|
23
|
-
return rgba
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function drawRect(
|
|
27
|
-
ctx: PImage.Context,
|
|
28
|
-
x: number,
|
|
29
|
-
y: number,
|
|
30
|
-
w: number,
|
|
31
|
-
h: number,
|
|
32
|
-
color: string,
|
|
33
|
-
lineWidth?: number
|
|
34
|
-
) {
|
|
35
|
-
ctx.strokeStyle = color
|
|
36
|
-
ctx.lineWidth = lineWidth ?? 1
|
|
37
|
-
ctx.strokeRect(x, y, w, h)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function fillRect(
|
|
41
|
-
ctx: PImage.Context,
|
|
42
|
-
x: number,
|
|
43
|
-
y: number,
|
|
44
|
-
w: number,
|
|
45
|
-
h: number,
|
|
46
|
-
color: string
|
|
47
|
-
) {
|
|
48
|
-
ctx.fillStyle = color
|
|
49
|
-
ctx.fillRect(x, y, w, h)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function mergeBounds(borders: Border[]) {
|
|
53
|
-
const m = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }
|
|
54
|
-
for (const b of borders) {
|
|
55
|
-
m.x = Math.min(m.x, b.x)
|
|
56
|
-
m.y = Math.min(m.y, b.y)
|
|
57
|
-
m.x2 = Math.max(m.x2, b.x + b.w)
|
|
58
|
-
m.y2 = Math.max(m.y2, b.y + b.h)
|
|
59
|
-
}
|
|
60
|
-
return m
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function areaOf(b: Border) {
|
|
64
|
-
return b.w * b.h
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function coverage(borders: Border[], expected: { x: number; y: number; w: number; h: number }) {
|
|
68
|
-
const merged = mergeBounds(borders)
|
|
69
|
-
const ex = expected.x
|
|
70
|
-
const ey = expected.y
|
|
71
|
-
const ex2 = expected.x + expected.w
|
|
72
|
-
const ey2 = expected.y + expected.h
|
|
73
|
-
const iw = Math.max(0, Math.min(merged.x2, ex2) - Math.max(merged.x, ex))
|
|
74
|
-
const ih = Math.max(0, Math.min(merged.y2, ey2) - Math.max(merged.y, ey))
|
|
75
|
-
const inter = iw * ih
|
|
76
|
-
const unionArea = (merged.x2 - merged.x) * (merged.y2 - merged.y) + expected.w * expected.h - inter
|
|
77
|
-
return inter / unionArea
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const TOL = 6
|
|
81
|
-
|
|
82
|
-
describe('detectBorders', () => {
|
|
83
|
-
it('solid color → no borders', () => {
|
|
84
|
-
const w = 80, h = 80
|
|
85
|
-
const data = createImage(w, h, () => {})
|
|
86
|
-
const borders = detectBorders(data, w, h)
|
|
87
|
-
expect(borders.length).toBe(0)
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('white bg + gray filled rect (low contrast)', () => {
|
|
91
|
-
const w = 200, h = 200
|
|
92
|
-
const rect = { x: 30, y: 40, w: 80, h: 60 }
|
|
93
|
-
const data = createImage(w, h, (ctx) => {
|
|
94
|
-
fillRect(ctx, rect.x, rect.y, rect.w, rect.h, '#cccccc')
|
|
95
|
-
})
|
|
96
|
-
const borders = detectBorders(data, w, h, {
|
|
97
|
-
lowThreshold: 8,
|
|
98
|
-
highThreshold: 25,
|
|
99
|
-
minArea: 20,
|
|
100
|
-
})
|
|
101
|
-
expect(borders.length).toBeGreaterThanOrEqual(1)
|
|
102
|
-
const merged = mergeBounds(borders)
|
|
103
|
-
expect(merged.x).toBeLessThanOrEqual(rect.x + TOL)
|
|
104
|
-
expect(merged.y).toBeLessThanOrEqual(rect.y + TOL)
|
|
105
|
-
expect(merged.x2).toBeGreaterThanOrEqual(rect.x + rect.w - TOL)
|
|
106
|
-
expect(merged.y2).toBeGreaterThanOrEqual(rect.y + rect.h - TOL)
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('white bg + thin black 1px line rect', () => {
|
|
110
|
-
const w = 200, h = 200
|
|
111
|
-
const rect = { x: 20, y: 20, w: 100, h: 80 }
|
|
112
|
-
const data = createImage(w, h, (ctx) => {
|
|
113
|
-
drawRect(ctx, rect.x, rect.y, rect.w, rect.h, '#000000', 1)
|
|
114
|
-
})
|
|
115
|
-
const borders = detectBorders(data, w, h, {
|
|
116
|
-
lowThreshold: 15,
|
|
117
|
-
highThreshold: 45,
|
|
118
|
-
minArea: 10,
|
|
119
|
-
})
|
|
120
|
-
expect(borders.length).toBeGreaterThanOrEqual(1)
|
|
121
|
-
const merged = mergeBounds(borders)
|
|
122
|
-
expect(merged.x).toBeLessThanOrEqual(rect.x + TOL)
|
|
123
|
-
expect(merged.y).toBeLessThanOrEqual(rect.y + TOL)
|
|
124
|
-
expect(merged.x2).toBeGreaterThanOrEqual(rect.x + rect.w - TOL)
|
|
125
|
-
expect(merged.y2).toBeGreaterThanOrEqual(rect.y + rect.h - TOL)
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('white bg + thin black 2px line rect', () => {
|
|
129
|
-
const w = 200, h = 200
|
|
130
|
-
const rect = { x: 50, y: 30, w: 100, h: 120 }
|
|
131
|
-
const data = createImage(w, h, (ctx) => {
|
|
132
|
-
drawRect(ctx, rect.x, rect.y, rect.w, rect.h, '#000000', 2)
|
|
133
|
-
})
|
|
134
|
-
const borders = detectBorders(data, w, h, {
|
|
135
|
-
lowThreshold: 15,
|
|
136
|
-
highThreshold: 45,
|
|
137
|
-
minArea: 10,
|
|
138
|
-
})
|
|
139
|
-
expect(borders.length).toBeGreaterThanOrEqual(1)
|
|
140
|
-
const merged = mergeBounds(borders)
|
|
141
|
-
expect(merged.x).toBeLessThanOrEqual(rect.x + TOL)
|
|
142
|
-
expect(merged.y).toBeLessThanOrEqual(rect.y + TOL)
|
|
143
|
-
expect(merged.x2).toBeGreaterThanOrEqual(rect.x + rect.w - TOL)
|
|
144
|
-
expect(merged.y2).toBeGreaterThanOrEqual(rect.y + rect.h - TOL)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('white bg + gray thin line rect (low contrast + thin)', () => {
|
|
148
|
-
const w = 200, h = 200
|
|
149
|
-
const rect = { x: 25, y: 25, w: 100, h: 100 }
|
|
150
|
-
const data = createImage(w, h, (ctx) => {
|
|
151
|
-
drawRect(ctx, rect.x, rect.y, rect.w, rect.h, '#999999', 1)
|
|
152
|
-
})
|
|
153
|
-
const borders = detectBorders(data, w, h, {
|
|
154
|
-
lowThreshold: 5,
|
|
155
|
-
highThreshold: 18,
|
|
156
|
-
minArea: 10,
|
|
157
|
-
})
|
|
158
|
-
expect(borders.length).toBeGreaterThanOrEqual(1)
|
|
159
|
-
const merged = mergeBounds(borders)
|
|
160
|
-
expect(merged.x).toBeLessThanOrEqual(rect.x + TOL)
|
|
161
|
-
expect(merged.y).toBeLessThanOrEqual(rect.y + TOL)
|
|
162
|
-
expect(merged.x2).toBeGreaterThanOrEqual(rect.x + rect.w - TOL)
|
|
163
|
-
expect(merged.y2).toBeGreaterThanOrEqual(rect.y + rect.h - TOL)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('blue rect on gray bg (color)', () => {
|
|
167
|
-
const w = 200, h = 200
|
|
168
|
-
const rect = { x: 40, y: 30, w: 80, h: 60 }
|
|
169
|
-
const data = createImage(w, h, (ctx) => {
|
|
170
|
-
fillRect(ctx, 0, 0, w, h, '#555555')
|
|
171
|
-
fillRect(ctx, rect.x, rect.y, rect.w, rect.h, '#0088ff')
|
|
172
|
-
})
|
|
173
|
-
const borders = detectBorders(data, w, h, {
|
|
174
|
-
lowThreshold: 12,
|
|
175
|
-
highThreshold: 35,
|
|
176
|
-
minArea: 20,
|
|
177
|
-
})
|
|
178
|
-
expect(borders.length).toBeGreaterThanOrEqual(1)
|
|
179
|
-
const merged = mergeBounds(borders)
|
|
180
|
-
expect(merged.x).toBeLessThanOrEqual(rect.x + TOL)
|
|
181
|
-
expect(merged.y).toBeLessThanOrEqual(rect.y + TOL)
|
|
182
|
-
expect(merged.x2).toBeGreaterThanOrEqual(rect.x + rect.w - TOL)
|
|
183
|
-
expect(merged.y2).toBeGreaterThanOrEqual(rect.y + rect.h - TOL)
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
it('two rects — count and area', () => {
|
|
187
|
-
const w = 300, h = 300
|
|
188
|
-
const r1 = { x: 20, y: 20, w: 60, h: 60 }
|
|
189
|
-
const r2 = { x: 150, y: 120, w: 100, h: 80 }
|
|
190
|
-
const data = createImage(w, h, (ctx) => {
|
|
191
|
-
fillRect(ctx, r1.x, r1.y, r1.w, r1.h, '#cccccc')
|
|
192
|
-
fillRect(ctx, r2.x, r2.y, r2.w, r2.h, '#cccccc')
|
|
193
|
-
})
|
|
194
|
-
const borders = detectBorders(data, w, h, {
|
|
195
|
-
lowThreshold: 8,
|
|
196
|
-
highThreshold: 25,
|
|
197
|
-
minArea: 20,
|
|
198
|
-
})
|
|
199
|
-
expect(borders.length).toBe(2)
|
|
200
|
-
const areas = borders.map(areaOf).sort((a, b) => a - b)
|
|
201
|
-
const expectedAreas = [r1.w * r1.h, r2.w * r2.h].sort((a, b) => a - b)
|
|
202
|
-
for (let i = 0; i < 2; i++) {
|
|
203
|
-
expect(areas[i]).toBeGreaterThan(expectedAreas[i] * 0.5)
|
|
204
|
-
expect(areas[i]).toBeLessThan(expectedAreas[i] * 2.5)
|
|
205
|
-
}
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
it('two thin-line rects — count and area', () => {
|
|
209
|
-
const w = 300, h = 300
|
|
210
|
-
const r1 = { x: 20, y: 20, w: 80, h: 60 }
|
|
211
|
-
const r2 = { x: 160, y: 100, w: 100, h: 80 }
|
|
212
|
-
const data = createImage(w, h, (ctx) => {
|
|
213
|
-
drawRect(ctx, r1.x, r1.y, r1.w, r1.h, '#000000', 2)
|
|
214
|
-
drawRect(ctx, r2.x, r2.y, r2.w, r2.h, '#000000', 2)
|
|
215
|
-
})
|
|
216
|
-
const borders = detectBorders(data, w, h, {
|
|
217
|
-
lowThreshold: 15,
|
|
218
|
-
highThreshold: 45,
|
|
219
|
-
minArea: 10,
|
|
220
|
-
})
|
|
221
|
-
expect(borders.length).toBeGreaterThanOrEqual(2)
|
|
222
|
-
const sorted = [...borders].sort((a, b) => areaOf(a) - areaOf(b))
|
|
223
|
-
const r1Area = r1.w * r1.h
|
|
224
|
-
const r2Area = r2.w * r2.h
|
|
225
|
-
expect(areaOf(sorted[sorted.length - 1])).toBeGreaterThan(r2Area * 0.3)
|
|
226
|
-
expect(areaOf(sorted[sorted.length - 1])).toBeLessThan(r2Area * 3)
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
it('detectBordersDefault works', () => {
|
|
230
|
-
const w = 200, h = 200
|
|
231
|
-
const rect = { x: 30, y: 30, w: 100, h: 100 }
|
|
232
|
-
const data = createImage(w, h, (ctx) => {
|
|
233
|
-
fillRect(ctx, rect.x, rect.y, rect.w, rect.h, '#cccccc')
|
|
234
|
-
})
|
|
235
|
-
const borders = detectBordersDefault(data, w, h)
|
|
236
|
-
expect(borders.length).toBeGreaterThanOrEqual(1)
|
|
237
|
-
})
|
|
238
|
-
})
|
package/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"outDir": "dist",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true
|
|
11
|
-
},
|
|
12
|
-
"include": ["src_ts"],
|
|
13
|
-
"exclude": ["tests", "node_modules"]
|
|
14
|
-
}
|
package/vite.config.ts
DELETED