optical_flow_lk/float_features.rs
1use crate::features::{Corner, Detector, DetectorCapacity, DetectorImage, Grid, Threshold};
2use crate::float_image::FloatImageView;
3use crate::utils::float_gradients::scharr_f32_into;
4
5/// Corner score threshold of the f32 detector.
6///
7/// Scores are minimum eigenvalues of the mean 3x3 structure tensor of Scharr
8/// gradients normalized by 32, in (input unit / pixel)^2: (°C / px)^2 for a
9/// thermal image in °C. They scale with the square of the image contrast and
10/// do not depend on a constant offset.
11///
12/// A local maximum is kept when its score is positive and at least
13/// `max(absolute, relative * strongest)`, where `strongest` is the largest
14/// local-maximum score in the whole image. The reference is global for grid
15/// detection as well, not per cell. The relative part is disabled (zero)
16/// unless set with [`with_relative`](Self::with_relative).
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct ScoreThreshold {
19 absolute: f32,
20 relative: f32,
21}
22
23impl ScoreThreshold {
24 /// Keeps local maxima scoring at least `min_score`, in
25 /// (input unit / pixel)^2.
26 ///
27 /// # Panics
28 /// Panics if `min_score` is negative, NaN or infinite.
29 #[must_use]
30 pub fn absolute(min_score: f32) -> Self {
31 assert!(
32 min_score.is_finite() && min_score >= 0.0,
33 "absolute score threshold must be finite and non-negative, got {min_score}"
34 );
35 Self {
36 absolute: min_score,
37 relative: 0.0,
38 }
39 }
40
41 /// Additionally requires at least `quality_level` times the strongest
42 /// local maximum's score, as the 8-bit detector's `quality_level` does.
43 ///
44 /// # Panics
45 /// Panics unless `0 <= quality_level <= 1`.
46 #[must_use]
47 pub fn with_relative(self, quality_level: f32) -> Self {
48 assert!(
49 (0.0..=1.0).contains(&quality_level),
50 "relative score threshold must be within 0..=1, got {quality_level}"
51 );
52 Self {
53 relative: quality_level,
54 ..self
55 }
56 }
57
58 /// The absolute part, in (input unit / pixel)^2.
59 pub fn absolute_part(&self) -> f32 {
60 self.absolute
61 }
62
63 /// The relative part; zero when disabled.
64 pub fn relative_part(&self) -> f32 {
65 self.relative
66 }
67
68 fn threshold(self) -> Threshold {
69 Threshold {
70 absolute: self.absolute,
71 relative: self.relative,
72 }
73 }
74}
75
76/// Shi-Tomasi corner detector for f32 images in physical units, e.g. thermal
77/// frames in °C. The counterpart of [`DetectorContext`](crate::DetectorContext),
78/// owning every buffer it touches.
79///
80/// The pipeline matches the 8-bit detector: Scharr gradients, the mean 3x3
81/// structure tensor, its minimum eigenvalue, 3x3 non-maximum suppression,
82/// sorting and spacing. Gradients are computed in f32 and the tensor is
83/// summed in f64, so neither quantization nor cancellation limits weak
84/// structure. For an image whose values are integers, the result equals the
85/// 8-bit detector's on the same values bit for bit, as long as the tensor
86/// determinant stays below 2^53, which holds for all but extreme 8-bit
87/// contrast. See [`ScoreThreshold`] for the score unit and the threshold.
88///
89/// Conventions, shared with the 8-bit detector:
90/// - Border pixels have zero gradient but count as window samples, and the
91/// outermost pixels are never returned.
92/// - A pixel is a local maximum when no 8-neighbor scores higher, so equal
93/// neighbors on a plateau are all kept.
94/// - Results are sorted by descending score, then by row, then by column.
95///
96/// Buffers grow on demand and are reused. Create the context with
97/// [`with_capacity`](Self::with_capacity) to keep allocation out of the
98/// per-frame path; calls within the [`DetectorCapacity`] then perform no heap
99/// allocation, whatever the image content.
100#[derive(Default)]
101pub struct FloatDetectorContext {
102 detector: Detector<f64>,
103}
104
105impl FloatDetectorContext {
106 /// Creates an empty context. Buffers grow to fit on the first detection
107 /// and are reused thereafter.
108 pub fn new() -> Self {
109 Self::default()
110 }
111
112 /// Creates a context with every buffer allocated for `capacity`.
113 pub fn with_capacity(capacity: DetectorCapacity) -> Self {
114 let mut context = Self::default();
115 context.reserve(capacity);
116 context
117 }
118
119 /// Allocates every buffer for `capacity` now, so that later calls within
120 /// it do not allocate. Existing capacity is kept.
121 pub fn reserve(&mut self, capacity: DetectorCapacity) {
122 self.detector.reserve(&capacity);
123 }
124
125 /// Detects corners scoring at least `threshold`, at least `min_distance`
126 /// pixels apart (zero disables spacing), greedily by descending score.
127 ///
128 /// Returns [`Corner`]s from the context's own result buffer; the slice is
129 /// valid until the next call. Images narrower or shorter than three
130 /// pixels, and flat images, yield no corners.
131 pub fn detect(
132 &mut self,
133 image: FloatImageView<'_>,
134 threshold: ScoreThreshold,
135 min_distance: u32,
136 ) -> &[Corner] {
137 self.detector
138 .detect(image, threshold.threshold(), min_distance)
139 }
140
141 /// Detects up to `grid.max_per_cell` corners per cell of `grid`, counting
142 /// `existing_points` against each cell's budget and keeping `min_distance`
143 /// to them, as
144 /// [`good_features_to_track_grid`](crate::good_features_to_track_grid) does.
145 ///
146 /// # Panics
147 /// Panics if `grid.cols` or `grid.rows` is zero.
148 pub fn detect_grid(
149 &mut self,
150 image: FloatImageView<'_>,
151 grid: Grid,
152 threshold: ScoreThreshold,
153 min_distance: u32,
154 existing_points: &[(f32, f32)],
155 ) -> &[Corner] {
156 self.detector.detect_grid(
157 image,
158 grid,
159 threshold.threshold(),
160 min_distance,
161 existing_points,
162 )
163 }
164}
165
166impl DetectorImage for FloatImageView<'_> {
167 type Sum = f64;
168
169 fn dimensions(self) -> (u32, u32) {
170 (self.width(), self.height())
171 }
172
173 #[inline(always)]
174 fn gradients_into(self, grad_x: &mut [f32], grad_y: &mut [f32]) {
175 let (width, height) = (self.width() as usize, self.height() as usize);
176 scharr_f32_into(self.pixels(), width, height, grad_x, grad_y);
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn threshold_passes_both_parts_to_the_detector() {
186 let absolute = ScoreThreshold::absolute(2.0);
187 assert_eq!(absolute.relative_part(), 0.0);
188 let both = absolute.with_relative(0.1);
189 assert_eq!((both.absolute_part(), both.relative_part()), (2.0, 0.1));
190 assert_eq!(
191 both.threshold(),
192 Threshold {
193 absolute: 2.0,
194 relative: 0.1
195 }
196 );
197 }
198
199 #[test]
200 #[should_panic(expected = "finite and non-negative")]
201 fn negative_absolute_threshold_panics() {
202 let _ = ScoreThreshold::absolute(-1e-9);
203 }
204
205 #[test]
206 #[should_panic(expected = "finite and non-negative")]
207 fn nan_absolute_threshold_panics() {
208 let _ = ScoreThreshold::absolute(f32::NAN);
209 }
210
211 #[test]
212 #[should_panic(expected = "within 0..=1")]
213 fn relative_threshold_above_one_panics() {
214 let _ = ScoreThreshold::absolute(0.0).with_relative(1.5);
215 }
216
217 #[test]
218 #[should_panic(expected = "within 0..=1")]
219 fn nan_relative_threshold_panics() {
220 let _ = ScoreThreshold::absolute(0.0).with_relative(f32::NAN);
221 }
222
223 #[test]
224 fn extreme_permitted_values_keep_scores_finite() {
225 let max = FloatImageView::MAX_ABS_VALUE;
226 let (width, height) = (8u32, 8u32);
227 let pixels: Vec<f32> = (0..width * height)
228 .map(|i| {
229 if (i / width + i % width) % 2 == 0 {
230 max
231 } else {
232 -max
233 }
234 })
235 .collect();
236 let checker: Vec<f32> = (0..width * height)
237 .map(|i| {
238 if (i / width < 4) == (i % width < 4) {
239 max
240 } else {
241 -max
242 }
243 })
244 .collect();
245 for pixels in [pixels, checker] {
246 let view = FloatImageView::new(&pixels, width, height).unwrap();
247 let mut context = FloatDetectorContext::new();
248 context.detect(view, ScoreThreshold::absolute(0.0), 0);
249 assert!(
250 context
251 .detector
252 .scores()
253 .iter()
254 .all(|s| s.is_finite() && *s >= 0.0)
255 );
256 }
257 }
258}