1use rayon::prelude::*;
8use std::f64::consts::PI;
9
10use crate::sample::{Gray, SizeMismatch};
11
12pub const SCALE: f64 = 0.8;
14const SIGMA_PER_SCALE: f64 = 0.6;
16const GRAY_QUANTIZATION: f64 = 2.0;
19const ANGLE_TOLERANCE_DEG: f64 = 22.5;
21const DENSITY_THRESHOLD: f64 = 0.7;
24const LOG_EPSILON: f64 = 0.0;
26const NORM_BINS: usize = 1024;
28
29#[derive(Clone, Copy, Debug, PartialEq)]
31pub struct Segment {
32 pub x1: f64,
34 pub y1: f64,
36 pub x2: f64,
38 pub y2: f64,
40 pub width: f64,
42 pub precision: f64,
44 pub log_nfa: f64,
46}
47
48impl Segment {
49 pub fn angle(&self) -> f64 {
51 let mut a = (self.y2 - self.y1).atan2(self.x2 - self.x1).to_degrees();
52 if a > 90.0 {
53 a -= 180.0;
54 }
55 if a <= -90.0 {
56 a += 180.0;
57 }
58 a
59 }
60
61 pub fn length(&self) -> f64 {
63 (self.x2 - self.x1).hypot(self.y2 - self.y1)
64 }
65}
66
67#[derive(Clone, Copy, Default)]
71struct Px {
72 norm: f32,
73 ux: f32,
74 uy: f32,
75}
76
77#[derive(Clone, Copy)]
79struct Rect {
80 x1: f64,
81 y1: f64,
82 x2: f64,
83 y2: f64,
84 width: f64,
85 dx: f64,
86 dy: f64,
87 prec: f64,
88 p: f64,
89}
90
91pub fn detect<T: Gray>(
98 image: &[T],
99 width: usize,
100 height: usize,
101 scale: f64,
102) -> Result<Vec<Segment>, SizeMismatch> {
103 if image.len() != width * height {
104 return Err(SizeMismatch {
105 expected: width * height,
106 got: image.len(),
107 });
108 }
109 if width < 3 || height < 3 {
110 return Ok(Vec::new());
111 }
112 let converted;
113 let gray: &[f64] = match T::as_f64_slice(image) {
114 Some(direct) => direct,
115 None => {
116 converted = image.iter().map(|s| s.to_gray()).collect::<Vec<f64>>();
117 &converted
118 }
119 };
120 let owned;
121 let (img, w, h) = if scale < 1.0 {
122 owned = downscale(gray, width, height, scale);
123 (owned.0.as_slice(), owned.1, owned.2)
124 } else {
125 (gray, width, height)
126 };
127 if w < 3 || h < 3 {
128 return Ok(Vec::new());
129 }
130 let prec = PI * ANGLE_TOLERANCE_DEG / 180.0;
131 let p = ANGLE_TOLERANCE_DEG / 180.0;
132 let rho = (GRAY_QUANTIZATION / (ANGLE_TOLERANCE_DEG.to_radians()).sin()) as f32;
133 let field = level_line_field(img, w, h);
134 let log_nt = 2.5 * ((w as f64).log10() + (h as f64).log10()) + 11f64.log10();
135 let min_region_pixels = (-log_nt / p.log10()).ceil() as usize;
136
137 let seeds = seeds_by_norm(&field, rho);
138 let mut used: Vec<bool> = field.iter().map(|px| px.norm <= rho).collect();
139 let mut region: Vec<u32> = Vec::new();
140 let mut segments = Vec::new();
141 let back = if scale < 1.0 { 1.0 / scale } else { 1.0 };
142
143 for &seed in &seeds {
144 if used[seed as usize] {
145 continue;
146 }
147 let mut region_angle = grow_region(seed, prec, &field, &mut used, &mut region, w, h);
148 if region.len() < min_region_pixels {
149 continue;
150 }
151 let mut rect = region_to_rect(®ion, &field, region_angle, prec, p, w);
152 let ok = refine(
153 &mut region,
154 &mut region_angle,
155 prec,
156 &mut rect,
157 &mut used,
158 &field,
159 min_region_pixels,
160 w,
161 h,
162 );
163 if !ok {
164 continue;
165 }
166 let log_nfa = improve_rect(&mut rect, &field, rho, log_nt, w, h);
167 if log_nfa <= LOG_EPSILON {
168 continue;
169 }
170 segments.push(Segment {
173 x1: (rect.x1 + 0.5) * back,
174 y1: (rect.y1 + 0.5) * back,
175 x2: (rect.x2 + 0.5) * back,
176 y2: (rect.y2 + 0.5) * back,
177 width: rect.width * back,
178 precision: rect.p,
179 log_nfa,
180 });
181 }
182 segments.sort_by(|a, b| b.length().total_cmp(&a.length()));
183 Ok(segments)
184}
185
186fn downscale(image: &[f64], width: usize, height: usize, scale: f64) -> (Vec<f64>, usize, usize) {
188 let sigma = SIGMA_PER_SCALE / scale;
189 let radius = (sigma * (2.0 * 1000f64.ln()).sqrt()).ceil() as usize;
190 let mut kernel: Vec<f64> = (0..=2 * radius)
191 .map(|i| {
192 let x = i as f64 - radius as f64;
193 (-0.5 * (x / sigma).powi(2)).exp()
194 })
195 .collect();
196 let total: f64 = kernel.iter().sum();
197 for k in &mut kernel {
198 *k /= total;
199 }
200 let clamp = |v: isize, hi: usize| v.clamp(0, hi as isize - 1) as usize;
201 let mut horiz = vec![0.0; width * height];
202 horiz
203 .par_chunks_mut(width)
204 .enumerate()
205 .for_each(|(y, out)| {
206 let row = &image[y * width..(y + 1) * width];
207 for (i, &k) in kernel.iter().enumerate() {
208 let off = i as isize - radius as isize;
209 let (x0, x1) = if off < 0 {
210 ((-off) as usize, width)
211 } else {
212 (0, width - (off as usize).min(width))
213 };
214 for x in x0..x1 {
215 out[x] += row[(x as isize + off) as usize] * k;
216 }
217 let edge = if off < 0 { row[0] } else { row[width - 1] } * k;
218 for slot in out[..x0].iter_mut() {
219 *slot += edge;
220 }
221 for slot in out[x1..].iter_mut() {
222 *slot += edge;
223 }
224 }
225 });
226 let mut blurred = vec![0.0; width * height];
227 blurred
228 .par_chunks_mut(width)
229 .enumerate()
230 .for_each(|(y, out)| {
231 for (i, &k) in kernel.iter().enumerate() {
232 let sy = clamp(y as isize + i as isize - radius as isize, height);
233 let src = &horiz[sy * width..(sy + 1) * width];
234 for (slot, value) in out.iter_mut().zip(src) {
235 *slot += value * k;
236 }
237 }
238 });
239 let new_w = ((width as f64 * scale).floor() as usize).max(2);
240 let new_h = ((height as f64 * scale).floor() as usize).max(2);
241 let cols: Vec<usize> = (0..new_w)
242 .map(|x| ((x as f64 / scale) as usize).min(width - 1))
243 .collect();
244 let mut small = vec![0.0; new_w * new_h];
245 small
246 .par_chunks_mut(new_w)
247 .enumerate()
248 .for_each(|(y, out)| {
249 let sy = ((y as f64 / scale) as usize).min(height - 1);
250 let src = &blurred[sy * width..(sy + 1) * width];
251 for (slot, &sx) in out.iter_mut().zip(&cols) {
252 *slot = src[sx];
253 }
254 });
255 (small, new_w, new_h)
256}
257
258fn level_line_field(image: &[f64], width: usize, height: usize) -> Vec<Px> {
260 let mut field = vec![Px::default(); width * height];
261 field
262 .par_chunks_mut(width)
263 .take(height - 1)
264 .enumerate()
265 .for_each(|(y, out)| {
266 let row = &image[y * width..(y + 1) * width];
267 let below = &image[(y + 1) * width..(y + 2) * width];
268 for x in 0..width - 1 {
269 let a = row[x];
270 let b = row[x + 1];
271 let c = below[x];
272 let d = below[x + 1];
273 let gx = (b + d - a - c) / 2.0;
274 let gy = (c + d - a - b) / 2.0;
275 let norm = gx.hypot(gy);
276 if norm > 0.0 {
277 out[x] = Px {
278 norm: norm as f32,
279 ux: (-gy / norm) as f32,
280 uy: (gx / norm) as f32,
281 };
282 }
283 }
284 });
285 field
286}
287
288fn angle_at(field: &[Px], i: usize) -> f64 {
290 (field[i].uy as f64).atan2(field[i].ux as f64)
291}
292
293fn seeds_by_norm(field: &[Px], rho: f32) -> Vec<u32> {
295 let max = field.iter().map(|p| p.norm).fold(0.0f32, f32::max);
296 if max <= 0.0 {
297 return Vec::new();
298 }
299 let mut bins: Vec<Vec<u32>> = vec![Vec::new(); NORM_BINS];
300 for (i, p) in field.iter().enumerate() {
301 if p.norm > rho {
302 let b = ((p.norm / max) * (NORM_BINS as f32 - 1.0)) as usize;
303 bins[b.min(NORM_BINS - 1)].push(i as u32);
304 }
305 }
306 bins.into_iter().rev().flatten().collect()
307}
308
309fn angle_diff(a: f64, b: f64) -> f64 {
311 signed_angle_diff(a, b).abs()
312}
313
314fn signed_angle_diff(a: f64, b: f64) -> f64 {
316 let mut d = a - b;
317 while d <= -PI {
318 d += 2.0 * PI;
319 }
320 while d > PI {
321 d -= 2.0 * PI;
322 }
323 d
324}
325
326#[allow(clippy::too_many_arguments)]
329fn grow_region(
330 seed: u32,
331 prec: f64,
332 field: &[Px],
333 used: &mut [bool],
334 region: &mut Vec<u32>,
335 width: usize,
336 height: usize,
337) -> f64 {
338 region.clear();
339 region.push(seed);
340 used[seed as usize] = true;
341 let cos_prec = prec.cos();
342 let mut sum_dx = field[seed as usize].ux as f64;
343 let mut sum_dy = field[seed as usize].uy as f64;
344 let mut bar = cos_prec;
345 let offsets: [isize; 8] = [
346 -(width as isize) - 1,
347 -(width as isize),
348 -(width as isize) + 1,
349 -1,
350 1,
351 width as isize - 1,
352 width as isize,
353 width as isize + 1,
354 ];
355 let mut i = 0;
356 while i < region.len() {
357 let p = region[i] as usize;
358 let px = p % width;
359 let py = p / width;
360 if px >= 1 && py >= 1 && px + 1 < width && py + 1 < height {
362 for &off in &offsets {
363 let n = (p as isize + off) as usize;
364 if used[n] {
365 continue;
366 }
367 let px_n = field[n];
368 if px_n.ux as f64 * sum_dx + px_n.uy as f64 * sum_dy < bar {
369 continue;
370 }
371 used[n] = true;
372 region.push(n as u32);
373 sum_dx += px_n.ux as f64;
374 sum_dy += px_n.uy as f64;
375 bar = cos_prec * sum_dx.hypot(sum_dy);
376 }
377 } else {
378 for ny in py.saturating_sub(1)..(py + 2).min(height) {
379 for nx in px.saturating_sub(1)..(px + 2).min(width) {
380 let n = ny * width + nx;
381 if used[n] {
382 continue;
383 }
384 let px_n = field[n];
385 if px_n.ux as f64 * sum_dx + px_n.uy as f64 * sum_dy < bar {
386 continue;
387 }
388 used[n] = true;
389 region.push(n as u32);
390 sum_dx += px_n.ux as f64;
391 sum_dy += px_n.uy as f64;
392 bar = cos_prec * sum_dx.hypot(sum_dy);
393 }
394 }
395 }
396 i += 1;
397 }
398 sum_dy.atan2(sum_dx)
399}
400
401fn region_to_rect(
403 region: &[u32],
404 field: &[Px],
405 region_angle: f64,
406 prec: f64,
407 p: f64,
408 width: usize,
409) -> Rect {
410 let mut cx = 0.0;
411 let mut cy = 0.0;
412 let mut total = 0.0;
413 for &r in region {
414 let w = field[r as usize].norm as f64;
415 cx += (r as usize % width) as f64 * w;
416 cy += (r as usize / width) as f64 * w;
417 total += w;
418 }
419 cx /= total;
420 cy /= total;
421 let theta = region_theta(region, field, cx, cy, region_angle, prec, width);
422 let (dx, dy) = (theta.cos(), theta.sin());
423 let mut l_min = 0.0f64;
424 let mut l_max = 0.0f64;
425 let mut w_min = 0.0f64;
426 let mut w_max = 0.0f64;
427 for &r in region {
428 let rx = (r as usize % width) as f64 - cx;
429 let ry = (r as usize / width) as f64 - cy;
430 let l = rx * dx + ry * dy;
431 let w = -rx * dy + ry * dx;
432 l_min = l_min.min(l);
433 l_max = l_max.max(l);
434 w_min = w_min.min(w);
435 w_max = w_max.max(w);
436 }
437 Rect {
438 x1: cx + l_min * dx,
439 y1: cy + l_min * dy,
440 x2: cx + l_max * dx,
441 y2: cy + l_max * dy,
442 width: (w_max - w_min).max(1.0),
443 dx,
444 dy,
445 prec,
446 p,
447 }
448}
449
450fn region_theta(
452 region: &[u32],
453 field: &[Px],
454 cx: f64,
455 cy: f64,
456 region_angle: f64,
457 prec: f64,
458 width: usize,
459) -> f64 {
460 let mut ixx = 0.0;
461 let mut iyy = 0.0;
462 let mut ixy = 0.0;
463 for &r in region {
464 let w = field[r as usize].norm as f64;
465 let rx = (r as usize % width) as f64 - cx;
466 let ry = (r as usize / width) as f64 - cy;
467 ixx += w * ry * ry;
468 iyy += w * rx * rx;
469 ixy -= w * rx * ry;
470 }
471 let smallest = (ixx + iyy - ((ixx - iyy).powi(2) + 4.0 * ixy * ixy).sqrt()) / 2.0;
472 let mut theta = if (ixx - smallest).abs() > (iyy - smallest).abs() {
473 (smallest - ixx).atan2(ixy)
474 } else {
475 ixy.atan2(smallest - iyy)
476 };
477 if angle_diff(theta, region_angle) > prec {
479 theta += PI;
480 }
481 theta
482}
483
484#[allow(clippy::too_many_arguments)]
486fn refine(
487 region: &mut Vec<u32>,
488 region_angle: &mut f64,
489 prec: f64,
490 rect: &mut Rect,
491 used: &mut [bool],
492 field: &[Px],
493 min_size: usize,
494 width: usize,
495 height: usize,
496) -> bool {
497 if rect_density(region, rect) >= DENSITY_THRESHOLD {
498 return true;
499 }
500 let seed = region[0];
501 let sx = (seed as usize % width) as f64;
502 let sy = (seed as usize / width) as f64;
503 let seed_angle = angle_at(field, seed as usize);
504 let mut sum = 0.0;
505 let mut squares = 0.0;
506 let mut n = 0.0;
507 for &r in region.iter() {
508 let rx = (r as usize % width) as f64;
509 let ry = (r as usize / width) as f64;
510 if (rx - sx).hypot(ry - sy) < rect.width {
511 let d = signed_angle_diff(angle_at(field, r as usize), seed_angle);
512 sum += d;
513 squares += d * d;
514 n += 1.0;
515 }
516 }
517 let mean = sum / n;
518 let variance = squares / n - mean * mean;
519 let tau = 2.0 * variance.max(0.0).sqrt();
520 for &r in region.iter() {
521 used[r as usize] = false;
522 }
523 *region_angle = grow_region(seed, tau, field, used, region, width, height);
524 if region.len() < min_size {
525 return false;
526 }
527 *rect = region_to_rect(region, field, *region_angle, prec, rect.p, width);
528 if rect_density(region, rect) >= DENSITY_THRESHOLD {
529 return true;
530 }
531 reduce_radius(
532 region,
533 region_angle,
534 prec,
535 rect,
536 used,
537 field,
538 min_size,
539 width,
540 )
541}
542
543#[allow(clippy::too_many_arguments)]
545fn reduce_radius(
546 region: &mut Vec<u32>,
547 region_angle: &mut f64,
548 prec: f64,
549 rect: &mut Rect,
550 used: &mut [bool],
551 field: &[Px],
552 min_size: usize,
553 width: usize,
554) -> bool {
555 let seed = region[0];
556 let sx = (seed as usize % width) as f64;
557 let sy = (seed as usize / width) as f64;
558 let mut radius = (sx - rect.x1)
559 .hypot(sy - rect.y1)
560 .max((sx - rect.x2).hypot(sy - rect.y2));
561 loop {
562 if rect_density(region, rect) >= DENSITY_THRESHOLD {
563 return true;
564 }
565 radius *= 0.75;
566 let mut i = 0;
567 while i < region.len() {
568 let r = region[i];
569 let rx = (r as usize % width) as f64;
570 let ry = (r as usize / width) as f64;
571 if (rx - sx).hypot(ry - sy) > radius {
572 used[r as usize] = false;
573 region.swap_remove(i);
574 } else {
575 i += 1;
576 }
577 }
578 if region.len() < min_size {
579 return false;
580 }
581 *rect = region_to_rect(region, field, *region_angle, prec, rect.p, width);
582 }
583}
584
585fn rect_density(region: &[u32], rect: &Rect) -> f64 {
587 let len = (rect.x2 - rect.x1).hypot(rect.y2 - rect.y1);
588 region.len() as f64 / (len * rect.width).max(1.0)
589}
590
591fn rect_counts(rect: &Rect, field: &[Px], rho: f32, width: usize, height: usize) -> (usize, usize) {
593 let cx = (rect.x1 + rect.x2) / 2.0;
594 let cy = (rect.y1 + rect.y2) / 2.0;
595 let half_l = (rect.x2 - rect.x1).hypot(rect.y2 - rect.y1) / 2.0;
596 let half_w = rect.width / 2.0;
597 let reach_x = half_l * rect.dx.abs() + half_w * rect.dy.abs();
598 let reach_y = half_l * rect.dy.abs() + half_w * rect.dx.abs();
599 let x_lo = ((cx - reach_x).floor().max(0.0)) as usize;
600 let y_lo = ((cy - reach_y).floor().max(0.0)) as usize;
601 let x_hi = ((cx + reach_x).ceil() as usize).min(width - 1);
602 let y_hi = ((cy + reach_y).ceil() as usize).min(height - 1);
603 let cos_prec = rect.prec.cos() as f32;
604 let dxf = rect.dx as f32;
605 let dyf = rect.dy as f32;
606 let mut total = 0;
607 let mut aligned = 0;
608 for y in y_lo..=y_hi {
609 for x in x_lo..=x_hi {
610 let rx = x as f64 - cx;
611 let ry = y as f64 - cy;
612 let l = rx * rect.dx + ry * rect.dy;
613 if l.abs() > half_l {
614 continue;
615 }
616 let w = -rx * rect.dy + ry * rect.dx;
617 if w.abs() > half_w {
618 continue;
619 }
620 total += 1;
621 let px = field[y * width + x];
622 if px.norm > rho && px.ux * dxf + px.uy * dyf >= cos_prec {
623 aligned += 1;
624 }
625 }
626 }
627 (total, aligned)
628}
629
630fn rect_log_nfa(
632 rect: &Rect,
633 field: &[Px],
634 rho: f32,
635 log_nt: f64,
636 width: usize,
637 height: usize,
638) -> f64 {
639 let (total, aligned) = rect_counts(rect, field, rho, width, height);
640 log_nfa(total, aligned, rect.p, log_nt)
641}
642
643fn improve_rect(
645 rect: &mut Rect,
646 field: &[Px],
647 rho: f32,
648 log_nt: f64,
649 width: usize,
650 height: usize,
651) -> f64 {
652 let mut best = rect_log_nfa(rect, field, rho, log_nt, width, height);
653 if best > LOG_EPSILON {
654 return best;
655 }
656 let delta = 0.5;
657 let try_variant = |candidate: Rect, best: &mut f64, rect: &mut Rect| {
658 let value = rect_log_nfa(&candidate, field, rho, log_nt, width, height);
659 if value > *best {
660 *best = value;
661 *rect = candidate;
662 }
663 };
664 let mut finer = *rect;
665 for _ in 0..5 {
666 finer.p /= 2.0;
667 finer.prec = finer.p * PI;
668 try_variant(finer, &mut best, rect);
669 }
670 if best > LOG_EPSILON {
671 return best;
672 }
673 let mut slimmer = *rect;
674 for _ in 0..5 {
675 if slimmer.width - delta < 0.5 {
676 break;
677 }
678 slimmer.width -= delta;
679 try_variant(slimmer, &mut best, rect);
680 }
681 if best > LOG_EPSILON {
682 return best;
683 }
684 for side in [1.0, -1.0] {
685 let mut trimmed = *rect;
686 for _ in 0..5 {
687 if trimmed.width - delta < 0.5 {
688 break;
689 }
690 trimmed.x1 += side * -trimmed.dy * delta / 2.0;
691 trimmed.y1 += side * trimmed.dx * delta / 2.0;
692 trimmed.x2 += side * -trimmed.dy * delta / 2.0;
693 trimmed.y2 += side * trimmed.dx * delta / 2.0;
694 trimmed.width -= delta;
695 try_variant(trimmed, &mut best, rect);
696 }
697 if best > LOG_EPSILON {
698 return best;
699 }
700 }
701 let mut finest = *rect;
702 for _ in 0..5 {
703 finest.p /= 2.0;
704 finest.prec = finest.p * PI;
705 try_variant(finest, &mut best, rect);
706 }
707 best
708}
709
710fn log_nfa(n: usize, k: usize, p: f64, log_nt: f64) -> f64 {
712 if n == 0 || k == 0 {
713 return -log_nt;
714 }
715 if n == k {
716 return -log_nt - n as f64 * p.log10();
717 }
718 let nf = n as f64;
719 let kf = k as f64;
720 let odds = p / (1.0 - p);
721 let log1 = log_gamma(nf + 1.0) - log_gamma(kf + 1.0) - log_gamma(nf - kf + 1.0)
722 + kf * p.ln()
723 + (nf - kf) * (1.0 - p).ln();
724 let mut term = log1.exp();
725 if term == 0.0 {
726 if kf > nf * p {
727 return -log1 / std::f64::consts::LN_10 - log_nt;
728 }
729 return -log_nt;
730 }
731 let mut tail = term;
732 for i in k + 1..=n {
733 let bin = (nf - i as f64 + 1.0) / i as f64;
734 let mult = bin * odds;
735 term *= mult;
736 tail += term;
737 if bin < 1.0 {
738 let bound = term * ((1.0 - mult.powi((n - i + 1) as i32)) / (1.0 - mult) - 1.0);
739 if bound < 0.1 * (-tail.log10() - log_nt).abs() * tail {
740 break;
741 }
742 }
743 }
744 -tail.log10() - log_nt
745}
746
747fn log_gamma(x: f64) -> f64 {
751 if x >= 15.0 {
752 return 0.918_938_533_204_673 + (x - 0.5) * x.ln() - x
753 + 0.5 * x * ((x * (1.0 / x).sinh() + 1.0 / (810.0 * x.powi(6))).ln());
754 }
755 const Q: [f64; 7] = [
756 75_122.633_153,
757 80_916.627_895_2,
758 36_308.295_147_7,
759 8_687.245_297_05,
760 1_168.926_494_79,
761 83.867_604_342_4,
762 2.506_628_275_11,
763 ];
764 let mut a = (x + 0.5) * (x + 5.5).ln() - (x + 5.5);
765 let mut b = 0.0;
766 for (n, q) in Q.iter().enumerate() {
767 a -= (x + n as f64).ln();
768 b += q * x.powi(n as i32);
769 }
770 a + b.ln()
771}
772
773#[cfg(test)]
774mod tests {
775 use super::*;
776
777 fn edge_image(width: usize, height: usize, split: usize) -> Vec<f64> {
778 let mut img = vec![50.0; width * height];
779 for y in 0..height {
780 for x in split..width {
781 img[y * width + x] = 200.0;
782 }
783 }
784 img
785 }
786
787 #[test]
788 fn vertical_edge_is_found() {
789 let img = edge_image(120, 120, 60);
790 let segs = detect(&img, 120, 120, SCALE).unwrap();
791 assert!(!segs.is_empty());
792 let best = &segs[0];
793 assert!(best.angle().abs() > 88.0, "angle {}", best.angle());
794 assert!(best.length() > 60.0, "length {}", best.length());
795 assert!(best.x1 > 45.0 && best.x1 < 75.0, "x {}", best.x1);
796 }
797
798 #[test]
799 fn tiny_image_is_empty() {
800 for n in 1..3 {
801 let img = vec![128.0; n * n];
802 assert!(detect(&img, n, n, SCALE).unwrap().is_empty(), "{n}x{n}");
803 }
804 }
805
806 #[test]
807 fn flat_image_is_empty() {
808 let img = vec![128.0; 100 * 100];
809 assert!(detect(&img, 100, 100, SCALE).unwrap().is_empty());
810 }
811}
812
813#[cfg(test)]
814mod bench {
815 use super::*;
816 use std::time::Instant;
817
818 fn load(path: &str) -> (Vec<f64>, usize, usize) {
819 let bytes = std::fs::read(path).unwrap();
820 let w = u64::from_le_bytes(bytes[0..8].try_into().unwrap()) as usize;
821 let h = u64::from_le_bytes(bytes[8..16].try_into().unwrap()) as usize;
822 let img = bytes[16..]
823 .chunks_exact(8)
824 .map(|c| f64::from_le_bytes(c.try_into().unwrap()))
825 .collect();
826 (img, w, h)
827 }
828
829 #[test]
830 #[ignore]
831 fn stages() {
832 let path = std::env::var("LSDETECT_BENCH").unwrap();
833 let (img, w, h) = load(&path);
834 let t = Instant::now();
835 let (small, sw, sh) = downscale(&img, w, h, SCALE);
836 eprintln!("downscale {:7.1?}", t.elapsed());
837 let t = Instant::now();
838 let field = level_line_field(&small, sw, sh);
839 eprintln!("field {:7.1?}", t.elapsed());
840 let rho = (GRAY_QUANTIZATION / (ANGLE_TOLERANCE_DEG.to_radians()).sin()) as f32;
841 let t = Instant::now();
842 let seeds = seeds_by_norm(&field, rho);
843 eprintln!("seeds {:7.1?} ({})", t.elapsed(), seeds.len());
844 let prec = PI * ANGLE_TOLERANCE_DEG / 180.0;
845 let p = ANGLE_TOLERANCE_DEG / 180.0;
846 let log_nt = 2.5 * ((sw as f64).log10() + (sh as f64).log10()) + 11f64.log10();
847 let min_region_pixels = (-log_nt / p.log10()).ceil() as usize;
848 let mut used: Vec<bool> = field.iter().map(|px| px.norm <= rho).collect();
849 let mut region = Vec::new();
850 let (mut t_grow, mut t_rect, mut t_nfa) = (0.0f64, 0.0f64, 0.0f64);
851 let mut kept = 0;
852 for &seed in &seeds {
853 if used[seed as usize] {
854 continue;
855 }
856 let t = Instant::now();
857 let mut region_angle = grow_region(seed, prec, &field, &mut used, &mut region, sw, sh);
858 t_grow += t.elapsed().as_secs_f64();
859 if region.len() < min_region_pixels {
860 continue;
861 }
862 let t = Instant::now();
863 let mut rect = region_to_rect(®ion, &field, region_angle, prec, p, sw);
864 let ok = refine(
865 &mut region,
866 &mut region_angle,
867 prec,
868 &mut rect,
869 &mut used,
870 &field,
871 min_region_pixels,
872 sw,
873 sh,
874 );
875 t_rect += t.elapsed().as_secs_f64();
876 if !ok {
877 continue;
878 }
879 let t = Instant::now();
880 let v = improve_rect(&mut rect, &field, rho, log_nt, sw, sh);
881 t_nfa += t.elapsed().as_secs_f64();
882 if v > LOG_EPSILON {
883 kept += 1;
884 }
885 }
886 eprintln!(
887 "grow {:6.1} ms\nrect+refine {:6.1} ms\nnfa+improve {:6.1} ms\nkept {}",
888 t_grow * 1e3,
889 t_rect * 1e3,
890 t_nfa * 1e3,
891 kept
892 );
893 }
894}
895
896#[cfg(test)]
897mod size_tests {
898 use super::*;
899
900 #[test]
901 fn a_wrong_buffer_length_is_refused() {
902 let image = vec![0.0f64; 8];
903 let result = detect(&image, 3, 3, 1.0);
904 assert_eq!(
905 result.unwrap_err(),
906 SizeMismatch {
907 expected: 9,
908 got: 8
909 }
910 );
911 }
912
913 #[test]
914 fn quantized_grays_share_the_detector_scale() {
915 use crate::sample::Gray;
916 assert_eq!(255u8.to_gray(), 255.0);
917 assert_eq!(65535u16.to_gray(), 255.0);
918 }
919}