1use image::{GrayImage, ImageBuffer};
2use std::cmp::Ordering;
3
4use crate::utils::{
5 box_filter_3x3::{sum_row_3, sum_row_3_f64, sum_rows_3, sum_rows_3_f64},
6 fast_gradients::compute_gradients_into,
7};
8
9#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct Corner {
16 pub x: u32,
18 pub y: u32,
20 pub score: f32,
22}
23
24impl Corner {
25 pub fn position(&self) -> (f32, f32) {
27 (self.x as f32, self.y as f32)
28 }
29}
30
31impl From<(u32, u32, f32)> for Corner {
32 fn from((x, y, score): (u32, u32, f32)) -> Self {
33 Self { x, y, score }
34 }
35}
36
37impl From<Corner> for (u32, u32, f32) {
38 fn from(corner: Corner) -> Self {
39 (corner.x, corner.y, corner.score)
40 }
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub struct Grid {
48 pub cols: u32,
50 pub rows: u32,
52 pub max_per_cell: u32,
54}
55
56pub fn good_features_to_track(
76 image: &GrayImage,
77 quality_level: f32,
78 min_distance: u32,
79) -> Vec<Corner> {
80 let mut ctx = DetectorContext::new();
81 ctx.detect(image, quality_level, min_distance);
82 ctx.detector.take_results()
83}
84
85pub fn good_features_to_track_grid(
115 image: &GrayImage,
116 grid: Grid,
117 quality_level: f32,
118 min_distance: u32,
119 existing_points: &[(f32, f32)],
120) -> Vec<Corner> {
121 let mut ctx = DetectorContext::new();
122 ctx.detect_grid(image, grid, quality_level, min_distance, existing_points);
123 ctx.detector.take_results()
124}
125
126#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
130pub struct DetectorCapacity {
131 pub width: u32,
133 pub height: u32,
135 pub min_distance: u32,
138 pub grid_cells: u32,
140 pub existing_points: usize,
142}
143
144impl DetectorCapacity {
145 fn max_pixels(&self) -> usize {
146 self.width as usize * self.height as usize
147 }
148
149 fn max_candidates(&self) -> usize {
151 self.width.saturating_sub(2) as usize * self.height.saturating_sub(2) as usize
152 }
153
154 fn max_spaced(&self) -> usize {
158 let candidates = self.max_candidates();
159 if self.min_distance < 2 {
160 return candidates;
161 }
162 let side = self.min_distance as f64 / std::f64::consts::SQRT_2;
163 let cells = |length: u32| (length as f64 / side).ceil() as usize + 1;
164 candidates.min(cells(self.width) * cells(self.height))
165 }
166}
167
168pub(crate) fn reserve_total<T>(buffer: &mut Vec<T>, total: usize) {
170 buffer.reserve(total.saturating_sub(buffer.len()));
171}
172
173#[derive(Default)]
187pub struct DetectorContext {
188 detector: Detector<i32>,
189}
190
191impl DetectorContext {
192 pub fn new() -> Self {
195 Self::default()
196 }
197
198 pub fn with_capacity(capacity: DetectorCapacity) -> Self {
200 let mut context = Self::default();
201 context.reserve(capacity);
202 context
203 }
204
205 pub fn reserve(&mut self, capacity: DetectorCapacity) {
208 self.detector.reserve(&capacity);
209 }
210
211 pub fn detect(
214 &mut self,
215 image: &GrayImage,
216 quality_level: f32,
217 min_distance: u32,
218 ) -> &[Corner] {
219 self.detector
220 .detect(image, Threshold::relative(quality_level), min_distance)
221 }
222
223 pub fn detect_grid(
230 &mut self,
231 image: &GrayImage,
232 grid: Grid,
233 quality_level: f32,
234 min_distance: u32,
235 existing_points: &[(f32, f32)],
236 ) -> &[Corner] {
237 self.detector.detect_grid(
238 image,
239 grid,
240 Threshold::relative(quality_level),
241 min_distance,
242 existing_points,
243 )
244 }
245}
246
247pub(crate) trait DetectorImage: Copy {
250 type Sum: TensorSum;
251
252 fn dimensions(self) -> (u32, u32);
253
254 fn gradients_into(
256 self,
257 grad_x: &mut [<Self::Sum as TensorSum>::Gradient],
258 grad_y: &mut [<Self::Sum as TensorSum>::Gradient],
259 );
260}
261
262impl DetectorImage for &GrayImage {
263 type Sum = i32;
264
265 fn dimensions(self) -> (u32, u32) {
266 ImageBuffer::dimensions(self)
267 }
268
269 fn gradients_into(self, grad_x: &mut [i16], grad_y: &mut [i16]) {
270 compute_gradients_into(self, grad_x, grad_y);
271 }
272}
273
274#[derive(Clone, Copy, Debug, PartialEq)]
277pub(crate) struct Threshold {
278 pub(crate) absolute: f32,
279 pub(crate) relative: f32,
280}
281
282impl Threshold {
283 fn relative(quality_level: f32) -> Self {
285 Self {
286 absolute: 0.0,
287 relative: quality_level,
288 }
289 }
290
291 #[cfg_attr(target_arch = "wasm32", inline(never))]
298 #[cfg_attr(not(target_arch = "wasm32"), inline(always))]
299 fn floor(&self, scores: &[f32], width: usize, height: usize) -> f32 {
300 let relative = if self.relative > 0.0 {
301 quality_floor(scores, width, height, self.relative)
302 } else {
303 0.0
304 };
305 self.absolute.max(relative)
306 }
307
308 fn keeps(&self, score: f32, strongest: f32) -> bool {
310 score >= self.absolute && score >= self.relative * strongest
313 }
314}
315
316#[derive(Default)]
319pub(crate) struct Detector<T: TensorSum> {
320 planes: Planes<T>,
321 selection: Selection,
322}
323
324impl<T: TensorSum> Detector<T> {
325 pub(crate) fn reserve(&mut self, capacity: &DetectorCapacity) {
326 self.planes.reserve(capacity);
327 self.selection.reserve(capacity);
328 }
329
330 pub(crate) fn detect<I: DetectorImage<Sum = T>>(
333 &mut self,
334 image: I,
335 threshold: Threshold,
336 min_distance: u32,
337 ) -> &[Corner] {
338 self.detect_candidates(image, threshold);
339 let (width, height) = image.dimensions();
340 self.selection
341 .spaced(&self.planes.candidates, min_distance, width, height)
342 }
343
344 pub(crate) fn detect_grid<I: DetectorImage<Sum = T>>(
350 &mut self,
351 image: I,
352 grid: Grid,
353 threshold: Threshold,
354 min_distance: u32,
355 existing_points: &[(f32, f32)],
356 ) -> &[Corner] {
357 assert!(grid.cols > 0 && grid.rows > 0, "grid must be non-empty");
358 if grid.max_per_cell == 0 {
359 return self.selection.clear();
360 }
361 self.detect_candidates(image, threshold);
362 let (width, height) = image.dimensions();
363 self.selection.grid(
364 &self.planes.candidates,
365 width,
366 height,
367 grid,
368 min_distance,
369 existing_points,
370 )
371 }
372
373 pub(crate) fn take_results(&mut self) -> Vec<Corner> {
375 std::mem::take(&mut self.selection.results)
376 }
377
378 #[cfg(test)]
380 pub(crate) fn scores(&self) -> &[f32] {
381 &self.planes.scores
382 }
383
384 fn detect_candidates<I: DetectorImage<Sum = T>>(&mut self, image: I, threshold: Threshold) {
388 self.planes.candidates.clear();
389 let (width, height) = image.dimensions();
390 if width < 3 || height < 3 {
391 return;
392 }
393
394 self.planes.run(image, threshold);
395 let candidates = &mut self.planes.candidates;
396 filter_by_threshold(candidates, threshold);
397 sort_by_score(candidates);
398 }
399}
400
401fn sort_by_score(candidates: &mut [(u32, u32, f32)]) {
404 candidates.sort_unstable_by(|a, b| {
405 b.2.partial_cmp(&a.2)
406 .unwrap_or(Ordering::Equal)
407 .then(a.1.cmp(&b.1))
408 .then(a.0.cmp(&b.0))
409 });
410}
411
412#[derive(Default)]
415struct Planes<T: TensorSum> {
416 grad_x: Vec<T::Gradient>,
417 grad_y: Vec<T::Gradient>,
418 tensor: TensorRows<T>,
419 scores: Vec<f32>,
420 maxima_rows: [Vec<f32>; 3],
422 candidates: Vec<(u32, u32, f32)>,
423}
424
425impl<T: TensorSum> Planes<T> {
426 fn reserve(&mut self, capacity: &DetectorCapacity) {
427 let (width, pixels) = (capacity.width as usize, capacity.max_pixels());
428 reserve_total(&mut self.grad_x, pixels);
429 reserve_total(&mut self.grad_y, pixels);
430 reserve_total(&mut self.scores, pixels);
431 self.tensor.reserve(width);
432 for row in &mut self.maxima_rows {
433 reserve_total(row, width.saturating_sub(2));
434 }
435 reserve_total(&mut self.candidates, capacity.max_candidates());
436 }
437
438 fn run<I: DetectorImage<Sum = T>>(&mut self, image: I, threshold: Threshold) {
445 #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
446 if is_x86_feature_detected!("avx2") {
447 return unsafe { run_avx2(self, image, threshold) };
449 }
450 run_impl(self, image, threshold)
451 }
452}
453
454#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
455#[target_feature(enable = "avx2")]
456unsafe fn run_avx2<I: DetectorImage>(planes: &mut Planes<I::Sum>, image: I, threshold: Threshold) {
457 run_impl(planes, image, threshold)
458}
459
460#[inline(always)]
461fn run_impl<I: DetectorImage>(p: &mut Planes<I::Sum>, image: I, threshold: Threshold) {
462 let (w, h) = image.dimensions();
463 let (w, h) = (w as usize, h as usize);
464 let n = w * h;
465 p.grad_x.resize(n, Default::default());
466 p.grad_y.resize(n, Default::default());
467 p.scores.resize(n, 0.0);
468
469 image.gradients_into(&mut p.grad_x, &mut p.grad_y);
470 min_eigenvalue_scores(&p.grad_x, &p.grad_y, w, &mut p.tensor, &mut p.scores);
471 let floor = threshold.floor(&p.scores, w, h);
472 local_maxima(
473 &p.scores,
474 w,
475 h,
476 floor,
477 &mut p.maxima_rows,
478 &mut p.candidates,
479 );
480}
481
482fn filter_by_threshold(candidates: &mut Vec<(u32, u32, f32)>, threshold: Threshold) {
484 let strongest = candidates.iter().fold(0.0f32, |max, p| max.max(p.2));
485 candidates.retain(|&(_, _, score)| threshold.keeps(score, strongest));
486}
487
488fn enforce_spacing<O: From<(u32, u32, f32)>>(
491 candidates: &[(u32, u32, f32)],
492 min_distance: u32,
493 width: u32,
494 height: u32,
495 occupancy: &mut OccupancyGrid,
496 out: &mut Vec<O>,
497) {
498 out.clear();
499 if candidates.is_empty() {
500 return;
501 }
502 if min_distance == 0 {
503 out.extend(candidates.iter().map(|&candidate| O::from(candidate)));
504 return;
505 }
506 occupancy.reset(width, height, min_distance);
507 for &(x, y, q) in candidates {
508 let (xf, yf) = (x as f32, y as f32);
509 if !occupancy.has_neighbor_within(xf, yf, min_distance as f32) {
510 occupancy.insert(xf, yf);
513 out.push(O::from((x, y, q)));
514 }
515 }
516}
517
518#[derive(Default)]
521struct Selection {
522 results: Vec<Corner>,
523 occupancy: OccupancyGrid,
524 cell_count: Vec<u32>,
525}
526
527impl Selection {
528 fn reserve(&mut self, capacity: &DetectorCapacity) {
529 let spaced = capacity.max_spaced();
530 reserve_total(&mut self.results, spaced);
531 self.occupancy
532 .reserve(capacity, capacity.existing_points + spaced);
533 reserve_total(&mut self.cell_count, capacity.grid_cells as usize);
534 }
535
536 fn clear(&mut self) -> &[Corner] {
537 self.results.clear();
538 &self.results
539 }
540
541 fn spaced(
543 &mut self,
544 candidates: &[(u32, u32, f32)],
545 min_distance: u32,
546 width: u32,
547 height: u32,
548 ) -> &[Corner] {
549 enforce_spacing(
550 candidates,
551 min_distance,
552 width,
553 height,
554 &mut self.occupancy,
555 &mut self.results,
556 );
557 &self.results
558 }
559
560 fn grid(
562 &mut self,
563 candidates: &[(u32, u32, f32)],
564 width: u32,
565 height: u32,
566 grid: Grid,
567 min_distance: u32,
568 existing_points: &[(f32, f32)],
569 ) -> &[Corner] {
570 self.results.clear();
571 if candidates.is_empty() {
572 return &self.results;
573 }
574 let Grid {
575 cols: grid_cols,
576 rows: grid_rows,
577 max_per_cell,
578 } = grid;
579
580 let cell_of = |x: f32, y: f32| -> usize {
582 let col = ((x / width as f32) * grid_cols as f32) as u32;
583 let row = ((y / height as f32) * grid_rows as f32) as u32;
584 let col = col.min(grid_cols - 1);
585 let row = row.min(grid_rows - 1);
586 (row * grid_cols + col) as usize
587 };
588
589 self.cell_count.clear();
591 self.cell_count.resize((grid_cols * grid_rows) as usize, 0);
592 self.occupancy.reset(width, height, min_distance);
594
595 for &(ex, ey) in existing_points {
596 self.cell_count[cell_of(ex, ey)] += 1;
597 self.occupancy.insert(ex, ey);
598 }
599
600 let min_distance = min_distance as f32;
601 for &(x, y, q) in candidates {
602 let (xf, yf) = (x as f32, y as f32);
603 let cell = cell_of(xf, yf);
604
605 if self.cell_count[cell] >= max_per_cell {
606 continue;
607 }
608 if self.occupancy.has_neighbor_within(xf, yf, min_distance) {
609 continue;
610 }
611
612 self.occupancy.insert(xf, yf);
613 self.cell_count[cell] += 1;
614 self.results.push(Corner { x, y, score: q });
615 }
616
617 &self.results
618 }
619}
620
621#[derive(Default)]
628struct OccupancyGrid {
629 cell_size: u32,
630 cols: u32,
631 rows: u32,
632 heads: Vec<u32>,
633 next: Vec<u32>,
634 points: Vec<(f32, f32)>,
635}
636
637const NO_POINT: u32 = u32::MAX;
638
639impl OccupancyGrid {
640 fn reserve(&mut self, capacity: &DetectorCapacity, points: usize) {
643 let cell_size = capacity.min_distance.max(1);
644 let cells = |length: u32| length.div_ceil(cell_size).max(1) as usize;
645 reserve_total(
646 &mut self.heads,
647 cells(capacity.width) * cells(capacity.height),
648 );
649 reserve_total(&mut self.next, points);
650 reserve_total(&mut self.points, points);
651 }
652
653 fn reset(&mut self, width: u32, height: u32, min_distance: u32) {
654 self.cell_size = min_distance.max(1);
655 self.cols = width.div_ceil(self.cell_size).max(1);
656 self.rows = height.div_ceil(self.cell_size).max(1);
657 self.heads.clear();
658 self.heads
659 .resize((self.cols * self.rows) as usize, NO_POINT);
660 self.next.clear();
661 self.points.clear();
662 }
663
664 fn cell(&self, x: f32, y: f32) -> (u32, u32) {
665 let col = (x.max(0.0) as u32 / self.cell_size).min(self.cols - 1);
666 let row = (y.max(0.0) as u32 / self.cell_size).min(self.rows - 1);
667 (col, row)
668 }
669
670 fn insert(&mut self, x: f32, y: f32) {
671 let (col, row) = self.cell(x, y);
672 let cell = (row * self.cols + col) as usize;
673 let index = self.points.len() as u32;
674 self.points.push((x, y));
675 self.next.push(self.heads[cell]);
676 self.heads[cell] = index;
677 }
678
679 fn has_neighbor_within(&self, x: f32, y: f32, min_distance: f32) -> bool {
680 if min_distance <= 0.0 {
681 return false;
682 }
683
684 let min_dist_sq = min_distance * min_distance;
685 let (col, row) = self.cell(x, y);
686
687 for drow in -1..=1 {
688 for dcol in -1..=1 {
689 let cx = col as i32 + dcol;
690 let cy = row as i32 + drow;
691 if cx < 0 || cy < 0 || cx >= self.cols as i32 || cy >= self.rows as i32 {
692 continue;
693 }
694
695 let mut index = self.heads[(cy as u32 * self.cols + cx as u32) as usize];
696 while index != NO_POINT {
697 let (px, py) = self.points[index as usize];
698 let dist_sq = (x - px).powi(2) + (y - py).powi(2);
699 if dist_sq < min_dist_sq {
700 return true;
701 }
702 index = self.next[index as usize];
703 }
704 }
705 }
706
707 false
708 }
709}
710
711pub(crate) trait TensorSum: Copy + Default {
715 type Gradient: Copy + Default;
716
717 fn products(gx: Self::Gradient, gy: Self::Gradient) -> (Self, Self, Self);
719
720 fn sum_row_3(src: &[Self], dst: &mut [Self]);
722
723 fn sum_rows_3(
725 above: Option<&[Self]>,
726 center: &[Self],
727 below: Option<&[Self]>,
728 dst: &mut [Self],
729 );
730
731 fn scaled_min_eigenvalue(a: Self, b: Self, c: Self, normalization: f32) -> f32;
734}
735
736impl TensorSum for i32 {
737 type Gradient = i16;
738
739 #[inline(always)]
740 fn products(gx: i16, gy: i16) -> (i32, i32, i32) {
741 let (ix, iy) = (gx as i32, gy as i32);
742 (ix * ix, iy * iy, ix * iy)
743 }
744
745 #[inline(always)]
746 fn sum_row_3(src: &[i32], dst: &mut [i32]) {
747 sum_row_3(src, dst);
748 }
749
750 #[inline(always)]
751 fn sum_rows_3(above: Option<&[i32]>, center: &[i32], below: Option<&[i32]>, dst: &mut [i32]) {
752 sum_rows_3(above, center, below, dst);
753 }
754
755 #[inline(always)]
756 fn scaled_min_eigenvalue(a: i32, b: i32, c: i32, normalization: f32) -> f32 {
757 scaled_min_eigenvalue(a, b, c, normalization)
758 }
759}
760
761impl TensorSum for f64 {
762 type Gradient = f32;
763
764 #[inline(always)]
765 fn products(gx: f32, gy: f32) -> (f64, f64, f64) {
766 let (ix, iy) = (gx as f64, gy as f64);
767 (ix * ix, iy * iy, ix * iy)
768 }
769
770 #[inline(always)]
771 fn sum_row_3(src: &[f64], dst: &mut [f64]) {
772 sum_row_3_f64(src, dst);
773 }
774
775 #[inline(always)]
776 fn sum_rows_3(above: Option<&[f64]>, center: &[f64], below: Option<&[f64]>, dst: &mut [f64]) {
777 sum_rows_3_f64(above, center, below, dst);
778 }
779
780 #[inline(always)]
781 fn scaled_min_eigenvalue(a: f64, b: f64, c: f64, normalization: f32) -> f32 {
782 let determinant = a * b - c * c;
788 let (trace, difference, c) = ((a + b) as f32, (a - b) as f32, c as f32);
789 let largest = (trace + (difference * difference + 4.0 * c * c).sqrt()) * 0.5;
790 let score = determinant as f32 / (largest * normalization);
791 if determinant > 0.0 { score } else { 0.0 }
792 }
793}
794
795#[derive(Default)]
798struct TensorRow<T> {
799 xx: Vec<T>,
800 yy: Vec<T>,
801 xy: Vec<T>,
802}
803
804impl<T: TensorSum> TensorRow<T> {
805 #[cfg(test)]
806 fn new(width: usize) -> Self {
807 let mut row = Self::default();
808 row.resize(width);
809 row
810 }
811
812 fn resize(&mut self, width: usize) {
813 for plane in [&mut self.xx, &mut self.yy, &mut self.xy] {
814 plane.resize(width, T::default());
815 }
816 }
817
818 fn reserve(&mut self, width: usize) {
819 for plane in [&mut self.xx, &mut self.yy, &mut self.xy] {
820 reserve_total(plane, width);
821 }
822 }
823
824 fn planes(&self) -> [&[T]; 3] {
825 [&self.xx, &self.yy, &self.xy]
826 }
827
828 fn planes_mut(&mut self) -> [&mut [T]; 3] {
829 [&mut self.xx, &mut self.yy, &mut self.xy]
830 }
831
832 #[inline(always)]
834 fn sum_rows(&mut self, above: Option<&Self>, center: &Self, below: Option<&Self>) {
835 let (above, below) = (above.map(Self::planes), below.map(Self::planes));
836 for (k, dst) in self.planes_mut().into_iter().enumerate() {
837 let (a, b) = (above.map(|p| p[k]), below.map(|p| p[k]));
838 T::sum_rows_3(a, center.planes()[k], b, dst);
839 }
840 }
841}
842
843#[derive(Default)]
847struct TensorRows<T> {
848 products: TensorRow<T>,
849 ring: [TensorRow<T>; 3],
850 window: TensorRow<T>,
851}
852
853impl<T: TensorSum> TensorRows<T> {
854 fn reserve(&mut self, width: usize) {
855 let Self {
856 products,
857 ring,
858 window,
859 } = self;
860 for row in [products, window].into_iter().chain(ring) {
861 row.reserve(width);
862 }
863 }
864}
865
866#[inline(always)]
870fn gradient_products_row<T: TensorSum>(
871 gx: &[T::Gradient],
872 gy: &[T::Gradient],
873 products: &mut TensorRow<T>,
874) {
875 let rows = products
876 .xx
877 .iter_mut()
878 .zip(&mut products.yy)
879 .zip(&mut products.xy)
880 .zip(gx.iter().zip(gy));
881 for (((xx, yy), xy), (&ix, &iy)) in rows {
882 (*xx, *yy, *xy) = T::products(ix, iy);
883 }
884}
885
886#[inline(always)]
888fn horizontal_tensor_row<T: TensorSum>(
889 gx: &[T::Gradient],
890 gy: &[T::Gradient],
891 products: &mut TensorRow<T>,
892 sums: &mut TensorRow<T>,
893) {
894 gradient_products_row(gx, gy, products);
895 T::sum_row_3(&products.xx, &mut sums.xx);
896 T::sum_row_3(&products.yy, &mut sums.yy);
897 T::sum_row_3(&products.xy, &mut sums.xy);
898}
899
900#[inline(always)]
905fn min_eigenvalue_scores<T: TensorSum>(
906 gx: &[T::Gradient],
907 gy: &[T::Gradient],
908 width: usize,
909 rows: &mut TensorRows<T>,
910 scores: &mut [f32],
911) {
912 if width == 0 || scores.is_empty() {
913 return;
914 }
915 let height = scores.len() / width;
916 let TensorRows {
917 products,
918 ring,
919 window,
920 } = rows;
921 for row in [&mut *products, &mut *window]
922 .into_iter()
923 .chain(ring.iter_mut())
924 {
925 row.resize(width);
926 }
927
928 let row = |y: usize| y * width..(y + 1) * width;
929 horizontal_tensor_row(&gx[row(0)], &gy[row(0)], products, &mut ring[0]);
930 for y in 0..height {
931 if y + 1 < height {
932 let next = &mut ring[(y + 1) % 3];
933 horizontal_tensor_row(&gx[row(y + 1)], &gy[row(y + 1)], products, next);
934 }
935 let above = (y > 0).then(|| &ring[(y + 2) % 3]);
936 let below = (y + 1 < height).then(|| &ring[(y + 1) % 3]);
937 window.sum_rows(above, &ring[y % 3], below);
938 score_row(window, y as u32, height as u32, &mut scores[row(y)]);
939 }
940}
941
942#[inline(always)]
944fn score_row<T: TensorSum>(window: &TensorRow<T>, y: u32, height: u32, scores: &mut [f32]) {
945 let width = scores.len();
946 let rows = 1 + u32::from(y > 0) + u32::from(y + 1 < height);
947 let normalization = |cols: u32| (32 * 32 * cols * rows) as f32;
950 let interior = normalization(3);
951 let sums = window.xx.iter().zip(&window.yy).zip(&window.xy);
952 for (score, ((&a, &b), &c)) in scores.iter_mut().zip(sums) {
953 *score = T::scaled_min_eigenvalue(a, b, c, interior);
954 }
955 let edge = normalization(if width == 1 { 1 } else { 2 });
957 for x in [0, width - 1] {
958 scores[x] = T::scaled_min_eigenvalue(window.xx[x], window.yy[x], window.xy[x], edge);
959 }
960}
961
962#[inline(always)]
965fn scaled_min_eigenvalue(a: i32, b: i32, c: i32, normalization: f32) -> f32 {
966 let determinant = a as i64 * b as i64 - c as i64 * c as i64;
972 let (trace, difference, c) = ((a + b) as f32, (a - b) as f32, c as f32);
973 let largest = (trace + (difference * difference + 4.0 * c * c).sqrt()) * 0.5;
974 let score = determinant as f32 / (largest * normalization);
977 if determinant > 0 { score } else { 0.0 }
978}
979
980#[inline(always)]
987fn quality_floor(scores: &[f32], width: usize, height: usize, quality_level: f32) -> f32 {
988 let (mut interior, mut border) = (0.0f32, 0.0f32);
989 for (y, row) in scores.chunks_exact(width).enumerate() {
990 if y == 0 || y + 1 == height {
991 border = max_of(row, border);
992 } else {
993 interior = max_of(&row[1..width - 1], interior);
994 border = max_score(max_score(border, row[0]), row[width - 1]);
995 }
996 }
997 if interior >= border {
998 quality_level * interior
999 } else {
1000 0.0
1001 }
1002}
1003
1004#[inline(always)]
1013fn local_maxima(
1014 scores: &[f32],
1015 width: usize,
1016 height: usize,
1017 floor: f32,
1018 rows: &mut [Vec<f32>; 3],
1019 maxima: &mut Vec<(u32, u32, f32)>,
1020) {
1021 if width < 3 || height < 3 {
1022 return;
1023 }
1024 for row in rows.iter_mut() {
1025 row.resize(width - 2, 0.0);
1026 }
1027 let row = |y: usize| &scores[y * width..(y + 1) * width];
1028 max_row_3(row(0), &mut rows[0]);
1031 max_row_3(row(1), &mut rows[1]);
1032 for y in 1..height - 1 {
1033 max_row_3(row(y + 1), &mut rows[(y + 1) % 3]);
1034 let (above, center, below) = (&rows[(y + 2) % 3], &rows[y % 3], &rows[(y + 1) % 3]);
1035 let interior = &row(y)[1..width - 1];
1036 let groups = interior
1037 .as_chunks::<MAXIMA_GROUP>()
1038 .0
1039 .iter()
1040 .zip(above.as_chunks().0)
1041 .zip(center.as_chunks().0)
1042 .zip(below.as_chunks().0);
1043 let mut start = 0;
1044 for (((s, a), c), b) in groups {
1045 let mut mask = kept_mask(s, a, c, b, floor);
1046 while mask != 0 {
1047 let i = start + mask.trailing_zeros() as usize;
1048 maxima.push((i as u32 + 1, y as u32, interior[i]));
1049 mask &= mask - 1;
1050 }
1051 start += MAXIMA_GROUP;
1052 }
1053 for i in start..interior.len() {
1054 let neighborhood = max_score(max_score(above[i], center[i]), below[i]);
1055 if is_kept(interior[i], neighborhood, floor) {
1056 maxima.push((i as u32 + 1, y as u32, interior[i]));
1057 }
1058 }
1059 }
1060}
1061
1062const MAXIMA_GROUP: usize = 8;
1064
1065#[inline(always)]
1069fn is_kept(score: f32, neighborhood: f32, floor: f32) -> bool {
1070 (score > 0.0) & (score >= floor) & (score == neighborhood)
1071}
1072
1073#[inline(always)]
1076fn kept_mask(
1077 scores: &[f32; MAXIMA_GROUP],
1078 above: &[f32; MAXIMA_GROUP],
1079 center: &[f32; MAXIMA_GROUP],
1080 below: &[f32; MAXIMA_GROUP],
1081 floor: f32,
1082) -> u32 {
1083 let pixels = scores.iter().zip(above).zip(center).zip(below);
1084 let mask = |(k, (((&score, &above), ¢er), &below))| {
1085 let neighborhood = max_score(max_score(above, center), below);
1086 u32::from(is_kept(score, neighborhood, floor)) << k
1087 };
1088 pixels.enumerate().map(mask).fold(0, |all, bit| all | bit)
1089}
1090
1091#[inline(always)]
1093fn max_row_3(src: &[f32], dst: &mut [f32]) {
1094 let windows = src.iter().zip(&src[1..]).zip(&src[2..]);
1095 for (value, ((&left, ¢er), &right)) in dst.iter_mut().zip(windows) {
1096 *value = max_score(max_score(left, center), right);
1097 }
1098}
1099
1100#[inline(always)]
1104fn max_score(a: f32, b: f32) -> f32 {
1105 if cfg!(target_arch = "aarch64") {
1106 a.max(b)
1107 } else if a > b {
1108 a
1109 } else {
1110 b
1111 }
1112}
1113
1114#[inline(always)]
1117fn max_of(scores: &[f32], init: f32) -> f32 {
1118 let mut lanes = [init; 8];
1119 let chunks = scores.chunks_exact(lanes.len());
1120 let rest = chunks.remainder();
1121 for chunk in chunks {
1122 for (lane, &score) in lanes.iter_mut().zip(chunk) {
1123 *lane = max_score(*lane, score);
1124 }
1125 }
1126 let max = |max, &score: &f32| max_score(max, score);
1127 rest.iter().chain(&lanes).fold(init, max)
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132 use super::*;
1133 use crate::utils::box_filter_3x3::box_sum_3x3_in_place;
1134 use image::{ImageBuffer, Luma};
1135
1136 fn spaced(candidates: &[(u32, u32, f32)], min_distance: u32) -> Vec<(u32, u32, f32)> {
1137 let mut occupancy = OccupancyGrid::default();
1138 let mut out = Vec::new();
1139 enforce_spacing(candidates, min_distance, 30, 30, &mut occupancy, &mut out);
1140 out
1141 }
1142
1143 fn maxima(scores: &[f32], width: usize, height: usize, floor: f32) -> Vec<(u32, u32, f32)> {
1144 let mut rows = Default::default();
1145 let mut out = Vec::new();
1146 local_maxima(scores, width, height, floor, &mut rows, &mut out);
1147 out
1148 }
1149
1150 #[test]
1151 fn minimum_eigenvalues_match_analytic_tensors() {
1152 let tensors = [
1153 (4, 9, 0, 4.0),
1154 (5, 5, 4, 1.0),
1155 (5, 5, -4, 1.0),
1156 (4, 9, 6, 0.0),
1157 (0, 0, 0, 0.0),
1158 (16_129, 16_129, 0, 16_129.0),
1159 (16_129, 16_129, 16_129, 0.0),
1160 (0, 0, 1, 0.0),
1162 ];
1163 for tensor in tensors {
1164 let score = scaled_min_eigenvalue(tensor.0, tensor.1, tensor.2, 1.0);
1165 assert!(score.is_finite());
1166 assert!(
1167 (score - tensor.3).abs() <= 1e-6 * tensor.3.max(1.0),
1168 "wrong score for {tensor:?}"
1169 );
1170 }
1171 }
1172
1173 type Plane = ImageBuffer<Luma<i32>, Vec<i32>>;
1174
1175 fn scores_of_window_planes(a: &Plane, b: &Plane, c: &Plane) -> Vec<f32> {
1177 let (width, height) = a.dimensions();
1178 let mut scores = vec![0.0; (width * height) as usize];
1179 let mut window = TensorRow::<i32>::new(width as usize);
1180 for y in 0..height {
1181 let row = (y * width) as usize..((y + 1) * width) as usize;
1182 window.xx.copy_from_slice(&a.as_raw()[row.clone()]);
1183 window.yy.copy_from_slice(&b.as_raw()[row.clone()]);
1184 window.xy.copy_from_slice(&c.as_raw()[row.clone()]);
1185 score_row(&window, y, height, &mut scores[row]);
1186 }
1187 scores
1188 }
1189
1190 #[test]
1191 fn raw_products_preserve_fractional_normalized_gradients_and_extremes() {
1192 let gx = [1i16, -31, 32, 4080, -4080];
1193 let gy = [-1i16, 1, 31, -4080, -4080];
1194 let mut products = TensorRow::<i32>::new(5);
1195 gradient_products_row(&gx, &gy, &mut products);
1196 assert_eq!(products.xx, [1, 961, 1024, 16_646_400, 16_646_400]);
1197 assert_eq!(products.yy, [1, 1, 961, 16_646_400, 16_646_400]);
1198 assert_eq!(products.xy, [-1, -31, 992, -16_646_400, 16_646_400]);
1199 }
1200
1201 #[test]
1202 fn row_pipeline_matches_full_plane_box_sums_for_small_and_odd_sizes() {
1203 let mut rows = TensorRows::<i32>::default();
1206 for width in 1..8 {
1207 for height in 1..8 {
1208 let gradient = |seed: u32| {
1209 ImageBuffer::from_fn(width, height, |x, y| {
1210 let hash = (x * 7 + y * 13 + seed).wrapping_mul(2_654_435_761) >> 19;
1211 Luma([(hash % 8161) as i16 - 4080])
1212 })
1213 };
1214 let (gx, gy) = (gradient(1), gradient(2));
1215 let plane = |product: fn(i32, i32) -> i32| {
1216 let mut plane = ImageBuffer::from_fn(width, height, |x, y| {
1217 let (ix, iy) = (gx.get_pixel(x, y)[0], gy.get_pixel(x, y)[0]);
1218 Luma([product(ix as i32, iy as i32)])
1219 });
1220 box_sum_3x3_in_place(&mut plane);
1221 plane
1222 };
1223 let expected = scores_of_window_planes(
1224 &plane(|ix, _| ix * ix),
1225 &plane(|_, iy| iy * iy),
1226 &plane(|ix, iy| ix * iy),
1227 );
1228 let mut scores = vec![0.0; (width * height) as usize];
1229 min_eigenvalue_scores(
1230 gx.as_raw(),
1231 gy.as_raw(),
1232 width as usize,
1233 &mut rows,
1234 &mut scores,
1235 );
1236 assert_eq!(scores, expected, "{width}x{height}");
1237 }
1238 }
1239 }
1240
1241 #[test]
1242 fn normalization_preserves_tensor_mean_at_borders_and_small_dimensions() {
1243 for (width, height) in [(1, 1), (1, 5), (5, 1), (2, 2), (3, 3), (7, 9)] {
1244 let mut a = ImageBuffer::from_pixel(width, height, Luma([1024]));
1245 let mut b = ImageBuffer::from_pixel(width, height, Luma([2048]));
1246 let c = ImageBuffer::new(width, height);
1247 box_sum_3x3_in_place(&mut a);
1248 box_sum_3x3_in_place(&mut b);
1249 let scores = scores_of_window_planes(&a, &b, &c);
1250 assert_eq!(scores.len(), (width * height) as usize);
1251 assert!(scores.iter().all(|&score| score == 1.0));
1252 }
1253 }
1254
1255 #[test]
1256 fn fractional_window_mean_is_not_truncated() {
1257 let a = ImageBuffer::from_pixel(3, 3, Luma([1]));
1258 let b = a.clone();
1259 let c = ImageBuffer::new(3, 3);
1260 let scores = scores_of_window_planes(&a, &b, &c);
1261 let expected = 1.0 / (32.0 * 32.0 * 9.0);
1262 assert_eq!(scores[4], expected);
1263 }
1264
1265 fn brute_force_maxima(scores: &[f32], width: usize, height: usize) -> Vec<(u32, u32, f32)> {
1267 let mut maxima = Vec::new();
1268 for y in 1..height.saturating_sub(1) {
1269 for x in 1..width.saturating_sub(1) {
1270 let score = scores[y * width + x];
1271 let neighbor = |nx: usize, ny: usize| scores[ny * width + nx];
1272 let is_max =
1273 (y - 1..=y + 1).all(|ny| (x - 1..=x + 1).all(|nx| neighbor(nx, ny) <= score));
1274 if score > 0.0 && is_max {
1275 maxima.push((x as u32, y as u32, score));
1276 }
1277 }
1278 }
1279 maxima
1280 }
1281
1282 #[test]
1283 fn local_maxima_match_brute_force_with_ties_and_floors() {
1284 let mut state = 0x2545_f491u32;
1285 for (width, height) in [(3, 3), (4, 3), (3, 7), (8, 5), (10, 4), (17, 11), (27, 6)] {
1288 for levels in [2, 4, 1000] {
1289 let scores: Vec<f32> = (0..width * height)
1291 .map(|_| {
1292 state ^= state << 13;
1293 state ^= state >> 17;
1294 state ^= state << 5;
1295 (state % levels) as f32
1296 })
1297 .collect();
1298 let expected = brute_force_maxima(&scores, width, height);
1299 assert_eq!(maxima(&scores, width, height, 0.0), expected);
1300 for floor in [0.5, 1.0, 3.0, 999.0] {
1301 let mut above = expected.clone();
1302 above.retain(|p| p.2 >= floor);
1303 assert_eq!(maxima(&scores, width, height, floor), above);
1304 }
1305 }
1306 }
1307 }
1308
1309 #[test]
1310 fn local_maxima_exclude_borders_zeros_and_dominated_pixels() {
1311 let width = 6;
1314 let mut scores = vec![0f32; width * 5];
1315 scores[width + 1] = 5.0;
1316 scores[width + 2] = 5.0;
1317 scores[3 * width + 4] = 7.0;
1318 scores[3 * width + 3] = 6.0; scores[4 * width] = 100.0; assert_eq!(
1321 maxima(&scores, width, 5, 0.0),
1322 vec![(1, 1, 5.0), (2, 1, 5.0), (4, 3, 7.0)]
1323 );
1324 }
1325
1326 #[test]
1327 fn quality_floor_is_skipped_when_the_maximum_is_only_on_the_border() {
1328 #[rustfmt::skip]
1331 let scores = [
1332 9.0, 0.0, 0.0, 0.0, 0.0,
1333 0.0, 5.0, 0.0, 0.0, 0.0,
1334 0.0, 0.0, 0.0, 2.0, 0.0,
1335 0.0, 0.0, 0.0, 0.0, 0.0,
1336 ];
1337 assert_eq!(quality_floor(&scores, 5, 4, 0.5), 0.0);
1338 assert_eq!(maxima(&scores, 5, 4, 0.0), [(3, 2, 2.0)]);
1339
1340 let mut interior = scores;
1341 interior[0] = 1.0;
1342 assert_eq!(quality_floor(&interior, 5, 4, 0.5), 2.5);
1343 assert_eq!(maxima(&interior, 5, 4, 2.5), [(1, 1, 5.0)]);
1344 }
1345
1346 #[test]
1347 fn nearly_rank_one_tensor_keeps_its_small_positive_eigenvalue() {
1348 let (a, b, c) = (33_284_641, 33_268_325, 33_276_482);
1350 assert_eq!(a as i64 * b as i64 - c as i64 * c as i64, 1);
1351 let score = scaled_min_eigenvalue(a, b, c, 1.0) as f64;
1352 let expected = 1.0 / (a as f64 + b as f64);
1353 assert!((score - expected).abs() <= expected * 1e-6);
1354 }
1355
1356 #[test]
1357 fn maximum_product_sum_is_finite_and_rank_one_stays_zero() {
1358 let max = 9 * 4080 * 4080;
1359 let score = scaled_min_eigenvalue(max, max, 0, 1.0);
1360 assert!((score - max as f32).abs() <= max as f32 * 1e-6);
1361 assert_eq!(scaled_min_eigenvalue(max, max, max, 1.0), 0.0);
1362 assert_eq!(scaled_min_eigenvalue(max, max, -max, 1.0), 0.0);
1363 }
1364
1365 #[test]
1366 fn f32_score_matches_f64_eigenvalue_of_window_tensors() {
1367 let mut state = 0x9e37_79b9u32;
1369 let mut next = || {
1370 state ^= state << 13;
1371 state ^= state >> 17;
1372 state ^= state << 5;
1373 (state % 8161) as i32 - 4080
1374 };
1375 for trial in 0..20_000 {
1376 let (dx, dy) = (next(), next());
1377 let (mut a, mut b, mut c) = (0, 0, 0);
1378 for _ in 0..9 {
1379 let jitter = if trial % 2 == 0 { 4080 } else { 3 };
1380 let scale = next().signum();
1381 let ix = (scale * dx + next() % jitter).clamp(-4080, 4080);
1382 let iy = (scale * dy + next() % jitter).clamp(-4080, 4080);
1383 (a, b, c) = (a + ix * ix, b + iy * iy, c + ix * iy);
1384 }
1385 let determinant = a as i64 * b as i64 - c as i64 * c as i64;
1386 let (af, bf, cf) = (a as f64, b as f64, c as f64);
1387 let largest = (af + bf + ((af - bf).powi(2) + 4.0 * cf * cf).sqrt()) / 2.0;
1388 let expected = (determinant.max(0) as f64 / largest) as f32;
1389 let score = scaled_min_eigenvalue(a, b, c, 1.0);
1390 assert!(
1391 (score - expected).abs() <= expected * 1e-6,
1392 "{a} {b} {c}: {score} vs {expected}"
1393 );
1394 }
1395 }
1396
1397 #[test]
1398 fn spacing_keeps_all_accepted_points_in_each_cell() {
1399 let candidates = [(10, 10, 3.0), (19, 19, 2.0), (10, 11, 1.0)];
1400 assert_eq!(spaced(&candidates, 10), candidates[..2]);
1401 }
1402
1403 #[test]
1404 fn zero_spacing_keeps_all_candidates_and_large_spacing_does_not_overflow() {
1405 let candidates = [(1, 1, 3.0), (1, 2, 2.0), (20, 20, 1.0)];
1406 assert_eq!(spaced(&candidates, 0), candidates);
1407 assert_eq!(spaced(&candidates, u32::MAX), candidates[..1]);
1408 }
1409
1410 #[test]
1411 fn reservation_covers_plateaus_and_densest_spacing() {
1412 let capacity = DetectorCapacity {
1414 width: 9,
1415 height: 7,
1416 ..Default::default()
1417 };
1418 let mut planes = Planes::<i32>::default();
1419 planes.reserve(&capacity);
1420 let buffer = planes.candidates.as_ptr();
1421 let scores = vec![1.0; 9 * 7];
1422 local_maxima(
1423 &scores,
1424 9,
1425 7,
1426 0.0,
1427 &mut planes.maxima_rows,
1428 &mut planes.candidates,
1429 );
1430 assert_eq!(planes.candidates.len(), capacity.max_candidates());
1431 assert_eq!(planes.candidates.as_ptr(), buffer);
1432
1433 for min_distance in [0, 1, 2, 3, 5, 8, 13, 100] {
1435 let capacity = DetectorCapacity {
1436 width: 61,
1437 height: 47,
1438 min_distance,
1439 ..Default::default()
1440 };
1441 let candidates: Vec<_> = (1..46)
1442 .flat_map(|y| (1..60).map(move |x| (x, y, 1.0)))
1443 .collect();
1444 let mut selection = Selection::default();
1445 selection.reserve(&capacity);
1446 let buffer = selection.results.as_ptr();
1447 let kept = selection.spaced(&candidates, min_distance, 61, 47).len();
1448 assert!(kept <= capacity.max_spaced(), "{min_distance}: {kept}");
1449 assert_eq!(selection.results.as_ptr(), buffer);
1450 }
1451 }
1452
1453 #[test]
1454 fn context_reuse_gives_identical_results_to_free_functions() {
1455 let image = GrayImage::from_fn(64, 48, |x, y| {
1456 image::Luma([((x * 31 + y * 17 + (x ^ y) * 13) & 255) as u8])
1457 });
1458 let mut ctx = DetectorContext::new();
1459 for _ in 0..2 {
1460 assert_eq!(
1461 ctx.detect(&image, 0.01, 5),
1462 good_features_to_track(&image, 0.01, 5)
1463 );
1464 let existing = [(10.0, 10.0), (40.0, 30.0)];
1465 let grid = Grid {
1466 cols: 4,
1467 rows: 3,
1468 max_per_cell: 3,
1469 };
1470 assert_eq!(
1471 ctx.detect_grid(&image, grid, 0.01, 6, &existing),
1472 good_features_to_track_grid(&image, grid, 0.01, 6, &existing)
1473 );
1474 }
1475 }
1476
1477 #[test]
1478 fn threshold_requires_both_parts() {
1479 let both = Threshold {
1480 absolute: 2.0,
1481 relative: 0.1,
1482 };
1483 assert!(both.keeps(10.0, 100.0) && !both.keeps(9.9, 100.0));
1484 assert!(both.keeps(2.0, 5.0) && !both.keeps(1.9, 5.0));
1485 assert!(!Threshold::relative(f32::NAN).keeps(1.0, 1.0));
1488 assert!(Threshold::relative(-1.0).keeps(1e-30, 1.0));
1489 }
1490
1491 fn reference(a: f64, b: f64, c: f64) -> f64 {
1493 let determinant = a * b - c * c;
1494 let largest = (a + b + ((a - b).powi(2) + 4.0 * c * c).sqrt()) / 2.0;
1495 if determinant > 0.0 {
1496 determinant / largest
1497 } else {
1498 0.0
1499 }
1500 }
1501
1502 #[test]
1503 fn f64_tensor_scores_match_reference_for_real_valued_gradients() {
1504 let mut state = 0x2545_f491u32;
1505 let mut next = || {
1506 state ^= state << 13;
1507 state ^= state >> 17;
1508 state ^= state << 5;
1509 state as f32 / u32::MAX as f32 * 2.0 - 1.0
1510 };
1511 for scale in [1e-6f32, 1e-3, 0.37, 41.0, 5e8] {
1516 for collinear in [false, true] {
1517 let (dx, dy) = (next(), next());
1518 let (mut a, mut b, mut c) = (0.0f64, 0.0f64, 0.0f64);
1519 for _ in 0..9 {
1520 let jitter = if collinear { 1e-3 } else { 1.0 };
1521 let t = next();
1522 let gx = (t * dx + next() * jitter) * scale;
1523 let gy = (t * dy + next() * jitter) * scale;
1524 let (xx, yy, xy) = <f64 as TensorSum>::products(gx, gy);
1525 (a, b, c) = (a + xx, b + yy, c + xy);
1526 }
1527 let score = <f64 as TensorSum>::scaled_min_eigenvalue(a, b, c, 1.0);
1528 let expected = reference(a, b, c);
1529 assert!(
1530 (score as f64 - expected).abs() <= expected * 1e-5,
1531 "scale {scale}, collinear {collinear}: {score} vs {expected}"
1532 );
1533 }
1534 }
1535 }
1536}