1use super::ring::RingOffsets;
3use crate::{ChessParams, ResponseMap};
4
5#[cfg(feature = "rayon")]
6use rayon::prelude::*;
7
8#[cfg(feature = "simd")]
9use core::simd::Simd;
10
11#[cfg(feature = "simd")]
12use std::simd::prelude::{SimdFloat, SimdInt, SimdUint};
13
14#[cfg(feature = "simd")]
15const LANES: usize = 16;
16
17#[cfg(feature = "simd")]
18type U8s = Simd<u8, LANES>;
19
20#[cfg(feature = "simd")]
21type I16s = Simd<i16, LANES>;
22
23#[cfg(feature = "simd")]
24type I32s = Simd<i32, LANES>;
25
26#[cfg(feature = "simd")]
27type F32s = Simd<f32, LANES>;
28
29#[cfg(feature = "tracing")]
30use tracing::instrument;
31
32#[derive(Clone, Copy, Debug)]
40pub struct Roi {
41 x0: usize,
42 y0: usize,
43 x1: usize,
44 y1: usize,
45}
46
47impl Roi {
48 pub fn new(x0: usize, y0: usize, x1: usize, y1: usize) -> Option<Self> {
50 if x0 < x1 && y0 < y1 {
51 Some(Self { x0, y0, x1, y1 })
52 } else {
53 None
54 }
55 }
56
57 #[inline]
59 pub fn x0(&self) -> usize {
60 self.x0
61 }
62 #[inline]
64 pub fn y0(&self) -> usize {
65 self.y0
66 }
67 #[inline]
69 pub fn x1(&self) -> usize {
70 self.x1
71 }
72 #[inline]
74 pub fn y1(&self) -> usize {
75 self.y1
76 }
77}
78
79#[inline]
80fn ring_from_params(params: &ChessParams) -> (RingOffsets, &'static [(i32, i32); 16]) {
81 let ring = params.ring();
82 (ring, ring.offsets())
83}
84
85#[cfg_attr(
162 feature = "tracing",
163 instrument(level = "info", skip(img, params), fields(w, h))
164)]
165pub fn chess_response_u8(img: &[u8], w: usize, h: usize, params: &ChessParams) -> ResponseMap {
166 assert_eq!(
167 img.len(),
168 w * h,
169 "chess_response_u8: img.len() ({}) must equal w*h ({w} * {h} = {})",
170 img.len(),
171 w * h,
172 );
173 #[cfg(feature = "rayon")]
175 {
176 compute_response_parallel(img, w, h, params)
177 }
178 #[cfg(not(feature = "rayon"))]
179 {
180 compute_response_sequential(img, w, h, params)
181 }
182}
183
184#[cfg(all(test, feature = "simd"))]
187fn chess_response_u8_scalar(img: &[u8], w: usize, h: usize, params: &ChessParams) -> ResponseMap {
188 compute_response_sequential_scalar(img, w, h, params)
189}
190
191#[cfg_attr(
208 feature = "tracing",
209 instrument(
210 level = "debug",
211 skip(img, params),
212 fields(img_w, img_h, roi_w = roi.x1 - roi.x0, roi_h = roi.y1 - roi.y0)
213 )
214)]
215pub fn chess_response_u8_patch(
216 img: &[u8],
217 img_w: usize,
218 img_h: usize,
219 params: &ChessParams,
220 roi: Roi,
221) -> ResponseMap {
222 assert_eq!(
223 img.len(),
224 img_w * img_h,
225 "chess_response_u8_patch: img.len() ({}) must equal img_w*img_h ({img_w} * {img_h} = {})",
226 img.len(),
227 img_w * img_h,
228 );
229 let Roi { x0, y0, x1, y1 } = roi;
230
231 let x0 = x0.min(img_w);
233 let y0 = y0.min(img_h);
234 let x1 = x1.min(img_w);
235 let y1 = y1.min(img_h);
236
237 if x1 <= x0 || y1 <= y0 {
238 return ResponseMap {
239 w: 0,
240 h: 0,
241 data: Vec::new(),
242 };
243 }
244
245 let patch_w = x1 - x0;
246 let patch_h = y1 - y0;
247 let mut data = vec![0.0f32; patch_w * patch_h];
248
249 let (ring_kind, ring) = ring_from_params(params);
250 let r = ring_kind.radius() as i32;
251
252 let gx0 = r as usize;
254 let gy0 = r as usize;
255 let gx1 = img_w - r as usize;
256 let gy1 = img_h - r as usize;
257
258 for py in 0..patch_h {
259 let gy = y0 + py;
260 if gy < gy0 || gy >= gy1 {
261 continue;
262 }
263
264 let row_gx0 = x0.max(gx0);
266 let row_gx1 = x1.min(gx1);
267 if row_gx0 >= row_gx1 {
268 continue;
269 }
270
271 let row = &mut data[py * patch_w..(py + 1) * patch_w];
272 let rel_start = row_gx0 - x0;
273 let rel_end = row_gx1 - x0;
274 let dst_row = &mut row[rel_start..rel_end];
275
276 #[cfg(feature = "simd")]
277 {
278 compute_row_range_simd(img, img_w, gy as i32, ring, dst_row, row_gx0, row_gx1);
279 }
280
281 #[cfg(not(feature = "simd"))]
282 {
283 compute_row_range_scalar(img, img_w, gy as i32, ring, dst_row, row_gx0, row_gx1);
284 }
285 }
286
287 ResponseMap {
288 w: patch_w,
289 h: patch_h,
290 data,
291 }
292}
293
294#[cfg(not(feature = "rayon"))]
295fn compute_response_sequential(
296 img: &[u8],
297 w: usize,
298 h: usize,
299 params: &ChessParams,
300) -> ResponseMap {
301 let (ring_kind, ring) = ring_from_params(params);
302 let r = ring_kind.radius() as i32;
303
304 let mut data = vec![0.0f32; w * h];
305
306 let x0 = r as usize;
308 let y0 = r as usize;
309 let x1 = w - r as usize;
310 let y1 = h - r as usize;
311
312 for y in y0..y1 {
313 let row = &mut data[y * w..(y + 1) * w];
314 let dst_row = &mut row[x0..x1];
315
316 #[cfg(feature = "simd")]
317 {
318 compute_row_range_simd(img, w, y as i32, ring, dst_row, x0, x1);
319 }
320
321 #[cfg(not(feature = "simd"))]
322 {
323 compute_row_range_scalar(img, w, y as i32, ring, dst_row, x0, x1);
324 }
325 }
326
327 ResponseMap { w, h, data }
328}
329
330#[cfg(all(test, feature = "simd"))]
331fn compute_response_sequential_scalar(
332 img: &[u8],
333 w: usize,
334 h: usize,
335 params: &ChessParams,
336) -> ResponseMap {
337 let (ring_kind, ring) = ring_from_params(params);
338 let r = ring_kind.radius() as i32;
339
340 let mut data = vec![0.0f32; w * h];
341
342 let x0 = r as usize;
344 let y0 = r as usize;
345 let x1 = w - r as usize;
346 let y1 = h - r as usize;
347
348 for y in y0..y1 {
349 let row = &mut data[y * w..(y + 1) * w];
350 let dst_row = &mut row[x0..x1];
351 compute_row_range_scalar(img, w, y as i32, ring, dst_row, x0, x1);
352 }
353
354 ResponseMap { w, h, data }
355}
356
357#[cfg(feature = "rayon")]
358fn compute_response_parallel(img: &[u8], w: usize, h: usize, params: &ChessParams) -> ResponseMap {
359 let (ring_kind, ring) = ring_from_params(params);
360 let r = ring_kind.radius() as i32;
361 let mut data = vec![0.0f32; w * h];
362
363 let x0 = r as usize;
365 let y0 = r as usize;
366 let x1 = w - r as usize;
367 let y1 = h - r as usize;
368
369 data.par_chunks_mut(w).enumerate().for_each(|(y, row)| {
372 let y_i = y as i32;
373 if y_i < y0 as i32 || y_i >= y1 as i32 {
374 return;
375 }
376
377 let dst_row = &mut row[x0..x1];
378
379 #[cfg(feature = "simd")]
380 {
381 compute_row_range_simd(img, w, y_i, ring, dst_row, x0, x1);
382 }
383
384 #[cfg(not(feature = "simd"))]
385 {
386 compute_row_range_scalar(img, w, y_i, ring, dst_row, x0, x1);
387 }
388 });
389
390 ResponseMap { w, h, data }
391}
392
393#[cfg(not(feature = "rayon"))]
398#[cfg_attr(not(feature = "rayon"), allow(dead_code))]
399fn compute_response_parallel(img: &[u8], w: usize, h: usize, params: &ChessParams) -> ResponseMap {
400 compute_response_sequential(img, w, h, params)
401}
402
403#[inline(always)]
417fn chess_response_at_u8(img: &[u8], w: usize, x: i32, y: i32, ring: &[(i32, i32); 16]) -> f32 {
418 let mut s = [0i32; 16];
420 for k in 0..16 {
421 let (dx, dy) = ring[k];
422 let xx = (x + dx) as usize;
423 let yy = (y + dy) as usize;
424 s[k] = img[yy * w + xx] as i32;
425 }
426
427 let mut sr = 0i32;
429 for k in 0..4 {
430 let a = s[k] + s[k + 8];
431 let b = s[k + 4] + s[k + 12];
432 sr += (a - b).abs();
433 }
434
435 let mut dr = 0i32;
437 for k in 0..8 {
438 dr += (s[k] - s[k + 8]).abs();
439 }
440
441 let sum_ring: i32 = s.iter().sum();
443 let mu_n = sum_ring as f32 / 16.0;
444
445 let c = img[(y as usize) * w + (x as usize)] as f32;
447 let n = img[((y - 1) as usize) * w + (x as usize)] as f32;
448 let s0 = img[((y + 1) as usize) * w + (x as usize)] as f32;
449 let e = img[(y as usize) * w + ((x + 1) as usize)] as f32;
450 let w0 = img[(y as usize) * w + ((x - 1) as usize)] as f32;
451 let mu_l = (c + n + s0 + e + w0) / 5.0;
452
453 let mr = (mu_n - mu_l).abs();
454
455 (sr as f32) - (dr as f32) - 16.0 * mr
456}
457
458#[cfg(any(not(feature = "simd"), test))]
463fn compute_row_range_scalar(
464 img: &[u8],
465 w: usize,
466 y: i32,
467 ring: &[(i32, i32); 16],
468 dst_row: &mut [f32],
469 x_start: usize,
470 x_end: usize,
471) {
472 for (offset, x) in (x_start..x_end).enumerate() {
473 dst_row[offset] = chess_response_at_u8(img, w, x as i32, y, ring);
474 }
475}
476
477#[cfg(feature = "simd")]
478fn compute_row_range_simd(
479 img: &[u8],
480 w: usize,
481 y: i32,
482 ring: &[(i32, i32); 16],
483 dst_row: &mut [f32],
484 x_start: usize,
485 x_end: usize,
486) {
487 let y_usize = y as usize;
488
489 let mut ring_bases: [isize; 16] = [0; 16];
491 for k in 0..16 {
492 let (dx, dy) = ring[k];
493 let yy = (y + dy) as usize;
494 ring_bases[k] = (yy * w) as isize + dx as isize;
495 }
496
497 let mut x = x_start;
498
499 while x + LANES <= x_end {
500 let mut s: [I16s; 16] = [I16s::splat(0); 16];
502
503 for k in 0..16 {
504 let base = (ring_bases[k] + x as isize) as usize;
505
506 let v_u8 = U8s::from_slice(&img[base..base + LANES]);
508 s[k] = v_u8.cast::<i16>();
509 }
510
511 let mut sum_ring_v = I32s::splat(0);
513 for &v in &s {
514 sum_ring_v += v.cast::<i32>();
515 }
516
517 let mut sr_v = I32s::splat(0);
519 for k in 0..4 {
520 let a = s[k].cast::<i32>() + s[k + 8].cast::<i32>();
521 let b = s[k + 4].cast::<i32>() + s[k + 12].cast::<i32>();
522 sr_v += (a - b).abs();
523 }
524
525 let mut dr_v = I32s::splat(0);
527 for k in 0..8 {
528 let a = s[k].cast::<i32>();
529 let b = s[k + 8].cast::<i32>();
530 dr_v += (a - b).abs();
531 }
532
533 let row_c = y_usize * w + x;
540 let c_v = U8s::from_slice(&img[row_c..row_c + LANES]).cast::<i16>();
541 let n_v = U8s::from_slice(&img[row_c - w..row_c - w + LANES]).cast::<i16>();
542 let s_v = U8s::from_slice(&img[row_c + w..row_c + w + LANES]).cast::<i16>();
543 let e_v = U8s::from_slice(&img[row_c + 1..row_c + 1 + LANES]).cast::<i16>();
544 let w_v = U8s::from_slice(&img[row_c - 1..row_c - 1 + LANES]).cast::<i16>();
545
546 let local_sum = c_v + n_v + s_v + e_v + w_v;
550
551 let mu_n = sum_ring_v.cast::<f32>() / F32s::splat(16.0);
552 let mu_l = local_sum.cast::<f32>() / F32s::splat(5.0);
553 let mr = (mu_n - mu_l).abs();
554
555 let resp = sr_v.cast::<f32>() - dr_v.cast::<f32>() - F32s::splat(16.0) * mr;
559 let px = x - x_start;
560 resp.copy_to_slice(&mut dst_row[px..px + LANES]);
561
562 x += LANES;
563 }
564
565 while x < x_end {
567 let px = x - x_start;
568 let resp = chess_response_at_u8(img, w, x as i32, y, ring);
569 dst_row[px] = resp;
570 x += 1;
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577 use crate::detect::chess::ring::RING5;
578
579 fn idx(w: usize, x: usize, y: usize) -> usize {
580 y * w + x
581 }
582
583 #[test]
584 #[should_panic(expected = "chess_response_u8: img.len()")]
585 fn chess_response_u8_panics_on_dimension_mismatch() {
586 let img = vec![0u8; 10];
587 let params = ChessParams::default();
588 let _ = chess_response_u8(&img, 4, 4, ¶ms);
589 }
590
591 #[test]
592 #[should_panic(expected = "chess_response_u8_patch: img.len()")]
593 fn chess_response_u8_patch_panics_on_dimension_mismatch() {
594 let img = vec![0u8; 10];
595 let params = ChessParams::default();
596 let roi = Roi::new(0, 0, 2, 2).unwrap();
597 let _ = chess_response_u8_patch(&img, 4, 4, ¶ms, roi);
598 }
599
600 #[test]
601 fn response_matches_manual_ring_layout() {
602 let params = ChessParams::default();
603 let w = 11usize;
604 let h = 11usize;
605 let cx = 5usize;
606 let cy = 5usize;
607 let mut img = vec![0u8; w * h];
608
609 for (i, (dx, dy)) in RING5.iter().enumerate() {
611 let x = (cx as i32 + dx) as usize;
612 let y = (cy as i32 + dy) as usize;
613 img[idx(w, x, y)] = i as u8;
614 }
615
616 for (dx, dy, v) in [
618 (0, 0, 10u8),
619 (0, -1, 20u8),
620 (0, 1, 30u8),
621 (1, 0, 40u8),
622 (-1, 0, 50u8),
623 ] {
624 let x = (cx as i32 + dx) as usize;
625 let y = (cy as i32 + dy) as usize;
626 img[idx(w, x, y)] = v;
627 }
628
629 let resp = chess_response_u8(&img, w, h, ¶ms);
630 let center = resp.at(cx, cy);
631
632 let expected = -392.0_f32;
634 assert!(
635 (center - expected).abs() < 1e-3,
636 "expected center response {expected}, got {center}"
637 );
638
639 for (i, v) in resp.data().iter().enumerate() {
640 if i == idx(w, cx, cy) {
641 continue;
642 }
643 assert!(
644 v.abs() < 1e-6,
645 "non-center response should stay zero (idx={i}, val={v})"
646 );
647 }
648 }
649
650 #[cfg(feature = "simd")]
651 #[test]
652 fn simd_matches_scalar_reasonably() {
653 let params = ChessParams::default();
654 let img = image::GrayImage::from_fn(256, 256, |x, y| image::Luma([(x ^ y) as u8]));
655 let w = img.width() as usize;
656 let h = img.height() as usize;
657
658 let ref_map = chess_response_u8_scalar(img.as_raw(), w, h, ¶ms);
659 let simd_map = chess_response_u8(img.as_raw(), w, h, ¶ms);
660
661 let eps = 1e-3_f32;
662 for (a, b) in ref_map.data().iter().zip(simd_map.data().iter()) {
663 assert!((a - b).abs() <= eps, "diff: {a} vs {b}");
664 }
665 }
666
667 #[cfg(all(feature = "simd", feature = "rayon"))]
668 #[test]
669 fn simd_parallel_matches_scalar() {
670 let params = ChessParams::default();
671 let img = image::GrayImage::from_fn(192, 192, |x, y| {
672 image::Luma([(x.wrapping_mul(7) ^ y) as u8])
673 });
674 let w = img.width() as usize;
675 let h = img.height() as usize;
676
677 let ref_map = chess_response_u8_scalar(img.as_raw(), w, h, ¶ms);
678 let simd_map = chess_response_u8(img.as_raw(), w, h, ¶ms);
679
680 let eps = 1e-3_f32;
681 for (a, b) in ref_map.data().iter().zip(simd_map.data().iter()) {
682 assert!((a - b).abs() <= eps, "diff: {a} vs {b}");
683 }
684 }
685}