imageproc/
corners.rs

1//! Functions for detecting corners, also known as interest points.
2
3use crate::definitions::{Position, Score};
4use image::{GenericImageView, GrayImage};
5
6/// A location and score for a detected corner.
7/// The scores need not be comparable between different
8/// corner detectors.
9#[derive(Copy, Clone, Debug, PartialEq)]
10pub struct Corner {
11    /// x-coordinate of the corner.
12    pub x: u32,
13    /// y-coordinate of the corner.
14    pub y: u32,
15    /// Score of the detected corner.
16    pub score: f32,
17}
18
19impl Corner {
20    /// A corner at location (x, y) with score `score`.
21    pub fn new(x: u32, y: u32, score: f32) -> Corner {
22        Corner { x, y, score }
23    }
24}
25
26impl Position for Corner {
27    /// x-coordinate of the corner.
28    fn x(&self) -> u32 {
29        self.x
30    }
31
32    /// y-coordinate of the corner.
33    fn y(&self) -> u32 {
34        self.y
35    }
36}
37
38impl Score for Corner {
39    fn score(&self) -> f32 {
40        self.score
41    }
42}
43
44/// Variants of the [FAST](https://en.wikipedia.org/wiki/Features_from_accelerated_segment_test)
45/// corner detector. These classify a point based on its intensity relative to the 16 pixels
46/// in the Bresenham circle of radius 3 around it. A point P with intensity I is detected as a
47/// corner if all pixels in a sufficiently long contiguous section of this circle either
48/// all have intensity greater than I + t or all have intensity less than
49/// I - t, for some user-provided threshold t. The score of a corner is
50/// the greatest threshold for which the given pixel still qualifies as
51/// a corner.
52pub enum Fast {
53    /// Corners require a section of length as least nine.
54    Nine,
55    /// Corners require a section of length as least twelve.
56    Twelve,
57}
58
59/// Finds corners using FAST-12 features. See comment on `Fast`.
60pub fn corners_fast12(image: &GrayImage, threshold: u8) -> Vec<Corner> {
61    let (width, height) = image.dimensions();
62    let mut corners = vec![];
63
64    for y in 0..height {
65        for x in 0..width {
66            if is_corner_fast12(image, threshold, x, y) {
67                let score = fast_corner_score(image, threshold, x, y, Fast::Twelve);
68                corners.push(Corner::new(x, y, score as f32));
69            }
70        }
71    }
72
73    corners
74}
75
76/// Finds corners using FAST-9 features. See comment on Fast enum.
77pub fn corners_fast9(image: &GrayImage, threshold: u8) -> Vec<Corner> {
78    let (width, height) = image.dimensions();
79    let mut corners = vec![];
80
81    for y in 0..height {
82        for x in 0..width {
83            if is_corner_fast9(image, threshold, x, y) {
84                let score = fast_corner_score(image, threshold, x, y, Fast::Nine);
85                corners.push(Corner::new(x, y, score as f32));
86            }
87        }
88    }
89
90    corners
91}
92
93/// The score of a corner detected using the FAST
94/// detector is the largest threshold for which this
95/// pixel is still a corner. We input the threshold at which
96/// the corner was detected as a lower bound on the search.
97/// Note that the corner check uses a strict inequality, so if
98/// the smallest intensity difference between the center pixel
99/// and a corner pixel is n then the corner will have a score of n - 1.
100pub fn fast_corner_score(image: &GrayImage, threshold: u8, x: u32, y: u32, variant: Fast) -> u8 {
101    let mut max = 255u8;
102    let mut min = threshold;
103
104    loop {
105        if max == min {
106            return max;
107        }
108
109        let mean = ((max as u16 + min as u16) / 2u16) as u8;
110        let probe = if max == min + 1 { max } else { mean };
111
112        let is_corner = match variant {
113            Fast::Nine => is_corner_fast9(image, probe, x, y),
114            Fast::Twelve => is_corner_fast12(image, probe, x, y),
115        };
116
117        if is_corner {
118            min = probe;
119        } else {
120            max = probe - 1;
121        }
122    }
123}
124
125// Note [FAST circle labels]
126//
127//          15 00 01
128//       14          02
129//     13              03
130//     12       p      04
131//     11              05
132//       10          06
133//          09 08 07
134
135/// Checks if the given pixel is a corner according to the FAST9 detector.
136/// The current implementation is extremely inefficient.
137// TODO: Make this much faster!
138fn is_corner_fast9(image: &GrayImage, threshold: u8, x: u32, y: u32) -> bool {
139    // UNSAFETY JUSTIFICATION
140    //  Benefit
141    //      Removing all unsafe pixel accesses in this file makes
142    //      bench_is_corner_fast9_9_contiguous_lighter_pixels 60% slower, and
143    //      bench_is_corner_fast12_12_noncontiguous 40% slower
144    //  Correctness
145    //      All pixel accesses in this function, and in the called get_circle,
146    //      access pixels with x-coordinate in the range [x - 3, x + 3] and
147    //      y-coordinate in the range [y - 3, y + 3]. The precondition below
148    //      guarantees that these are within image bounds.
149    let (width, height) = image.dimensions();
150    if x >= u32::max_value() - 3
151        || y >= u32::max_value() - 3
152        || x < 3
153        || y < 3
154        || width <= x + 3
155        || height <= y + 3
156    {
157        return false;
158    }
159
160    // JUSTIFICATION - see comment at the start of this function
161    let c = unsafe { image.unsafe_get_pixel(x, y)[0] };
162    let low_thresh: i16 = c as i16 - threshold as i16;
163    let high_thresh: i16 = c as i16 + threshold as i16;
164
165    // See Note [FAST circle labels]
166    // JUSTIFICATION - see comment at the start of this function
167    let (p0, p4, p8, p12) = unsafe {
168        (
169            image.unsafe_get_pixel(x, y - 3)[0] as i16,
170            image.unsafe_get_pixel(x, y + 3)[0] as i16,
171            image.unsafe_get_pixel(x + 3, y)[0] as i16,
172            image.unsafe_get_pixel(x - 3, y)[0] as i16,
173        )
174    };
175
176    let above = (p0 > high_thresh && p4 > high_thresh)
177        || (p4 > high_thresh && p8 > high_thresh)
178        || (p8 > high_thresh && p12 > high_thresh)
179        || (p12 > high_thresh && p0 > high_thresh);
180
181    let below = (p0 < low_thresh && p4 < low_thresh)
182        || (p4 < low_thresh && p8 < low_thresh)
183        || (p8 < low_thresh && p12 < low_thresh)
184        || (p12 < low_thresh && p0 < low_thresh);
185
186    if !above && !below {
187        return false;
188    }
189
190    // JUSTIFICATION - see comment at the start of this function
191    let pixels = unsafe { get_circle(image, x, y, p0, p4, p8, p12) };
192
193    // above and below could both be true
194    (above && has_bright_span(&pixels, 9, high_thresh))
195        || (below && has_dark_span(&pixels, 9, low_thresh))
196}
197
198/// Checks if the given pixel is a corner according to the FAST12 detector.
199fn is_corner_fast12(image: &GrayImage, threshold: u8, x: u32, y: u32) -> bool {
200    // UNSAFETY JUSTIFICATION
201    //  Benefit
202    //      Removing all unsafe pixel accesses in this file makes
203    //      bench_is_corner_fast9_9_contiguous_lighter_pixels 60% slower, and
204    //      bench_is_corner_fast12_12_noncontiguous 40% slower
205    //  Correctness
206    //      All pixel accesses in this function, and in the called get_circle,
207    //      access pixels with x-coordinate in the range [x - 3, x + 3] and
208    //      y-coordinate in the range [y - 3, y + 3]. The precondition below
209    //      guarantees that these are within image bounds.
210    let (width, height) = image.dimensions();
211    if x >= u32::max_value() - 3
212        || y >= u32::max_value() - 3
213        || x < 3
214        || y < 3
215        || width <= x + 3
216        || height <= y + 3
217    {
218        return false;
219    }
220
221    // JUSTIFICATION - see comment at the start of this function
222    let c = unsafe { image.unsafe_get_pixel(x, y)[0] };
223    let low_thresh: i16 = c as i16 - threshold as i16;
224    let high_thresh: i16 = c as i16 + threshold as i16;
225
226    // See Note [FAST circle labels]
227    // JUSTIFICATION - see comment at the start of this function
228    let (p0, p8) = unsafe {
229        (
230            image.unsafe_get_pixel(x, y - 3)[0] as i16,
231            image.unsafe_get_pixel(x, y + 3)[0] as i16,
232        )
233    };
234
235    let mut above = p0 > high_thresh && p8 > high_thresh;
236    let mut below = p0 < low_thresh && p8 < low_thresh;
237
238    if !above && !below {
239        return false;
240    }
241
242    // JUSTIFICATION - see comment at the start of this function
243    let (p4, p12) = unsafe {
244        (
245            image.unsafe_get_pixel(x + 3, y)[0] as i16,
246            image.unsafe_get_pixel(x - 3, y)[0] as i16,
247        )
248    };
249
250    above = above && ((p4 > high_thresh) || (p12 > high_thresh));
251    below = below && ((p4 < low_thresh) || (p12 < low_thresh));
252
253    if !above && !below {
254        return false;
255    }
256
257    // TODO: Generate a list of pixel offsets once per image,
258    // TODO: and use those offsets directly when reading pixels.
259    // TODO: This is a little tricky as we can't always do it - we'd
260    // TODO: need to distinguish between GenericImages and ImageBuffers.
261    // TODO: We can also reduce the number of checks we do below.
262
263    // JUSTIFICATION - see comment at the start of this function
264    let pixels = unsafe { get_circle(image, x, y, p0, p4, p8, p12) };
265
266    // Exactly one of above or below is true
267    if above {
268        has_bright_span(&pixels, 12, high_thresh)
269    } else {
270        has_dark_span(&pixels, 12, low_thresh)
271    }
272}
273
274/// # Safety
275///
276/// The caller must ensure that:
277///
278///   x + 3 < image.width() &&
279///   x >= 3 &&
280///   y + 3 < image.height() &&
281///   y >= 3
282///
283#[inline]
284unsafe fn get_circle(
285    image: &GrayImage,
286    x: u32,
287    y: u32,
288    p0: i16,
289    p4: i16,
290    p8: i16,
291    p12: i16,
292) -> [i16; 16] {
293    [
294        p0,
295        image.unsafe_get_pixel(x + 1, y - 3)[0] as i16,
296        image.unsafe_get_pixel(x + 2, y - 2)[0] as i16,
297        image.unsafe_get_pixel(x + 3, y - 1)[0] as i16,
298        p4,
299        image.unsafe_get_pixel(x + 3, y + 1)[0] as i16,
300        image.unsafe_get_pixel(x + 2, y + 2)[0] as i16,
301        image.unsafe_get_pixel(x + 1, y + 3)[0] as i16,
302        p8,
303        image.unsafe_get_pixel(x - 1, y + 3)[0] as i16,
304        image.unsafe_get_pixel(x - 2, y + 2)[0] as i16,
305        image.unsafe_get_pixel(x - 3, y + 1)[0] as i16,
306        p12,
307        image.unsafe_get_pixel(x - 3, y - 1)[0] as i16,
308        image.unsafe_get_pixel(x - 2, y - 2)[0] as i16,
309        image.unsafe_get_pixel(x - 1, y - 3)[0] as i16,
310    ]
311}
312
313/// True if the circle has a contiguous section of at least the given length, all
314/// of whose pixels have intensities strictly greater than the threshold.
315fn has_bright_span(circle: &[i16; 16], length: u8, threshold: i16) -> bool {
316    search_span(circle, length, |c| *c > threshold)
317}
318
319/// True if the circle has a contiguous section of at least the given length, all
320/// of whose pixels have intensities strictly less than the threshold.
321fn has_dark_span(circle: &[i16; 16], length: u8, threshold: i16) -> bool {
322    search_span(circle, length, |c| *c < threshold)
323}
324
325/// True if the circle has a contiguous section of at least the given length, all
326/// of whose pixels match f condition.
327fn search_span<F>(circle: &[i16; 16], length: u8, f: F) -> bool
328where
329    F: Fn(&i16) -> bool,
330{
331    if length > 16 {
332        return false;
333    }
334
335    let mut nb_ok = 0u8;
336    let mut nb_ok_start = None;
337
338    for c in circle.iter() {
339        if f(c) {
340            nb_ok += 1;
341            if nb_ok == length {
342                return true;
343            }
344        } else {
345            if nb_ok_start.is_none() {
346                nb_ok_start = Some(nb_ok);
347            }
348            nb_ok = 0;
349        }
350    }
351
352    nb_ok + nb_ok_start.unwrap() >= length
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358    use test::{black_box, Bencher};
359
360    #[test]
361    fn test_is_corner_fast12_12_contiguous_darker_pixels() {
362        let image = gray_image!(
363            10, 10, 00, 00, 00, 10, 10;
364            10, 00, 10, 10, 10, 00, 10;
365            00, 10, 10, 10, 10, 10, 10;
366            00, 10, 10, 10, 10, 10, 10;
367            00, 10, 10, 10, 10, 10, 10;
368            10, 00, 10, 10, 10, 10, 10;
369            10, 10, 00, 00, 00, 10, 10);
370
371        assert_eq!(is_corner_fast12(&image, 8, 3, 3), true);
372    }
373
374    #[test]
375    fn test_is_corner_fast12_12_contiguous_darker_pixels_large_threshold() {
376        let image = gray_image!(
377            10, 10, 00, 00, 00, 10, 10;
378            10, 00, 10, 10, 10, 00, 10;
379            00, 10, 10, 10, 10, 10, 10;
380            00, 10, 10, 10, 10, 10, 10;
381            00, 10, 10, 10, 10, 10, 10;
382            10, 00, 10, 10, 10, 10, 10;
383            10, 10, 00, 00, 00, 10, 10);
384
385        assert_eq!(is_corner_fast12(&image, 15, 3, 3), false);
386    }
387
388    #[test]
389    fn test_is_corner_fast12_12_contiguous_lighter_pixels() {
390        let image = gray_image!(
391            00, 00, 10, 10, 10, 00, 00;
392            00, 10, 00, 00, 00, 10, 00;
393            10, 00, 00, 00, 00, 00, 00;
394            10, 00, 00, 00, 00, 00, 00;
395            10, 00, 00, 00, 00, 00, 00;
396            00, 10, 00, 00, 00, 00, 00;
397            00, 00, 10, 10, 10, 00, 00);
398
399        assert_eq!(is_corner_fast12(&image, 8, 3, 3), true);
400    }
401
402    #[test]
403    fn test_is_corner_fast12_12_noncontiguous() {
404        let image = gray_image!(
405            10, 10, 00, 00, 00, 10, 10;
406            10, 00, 10, 10, 10, 00, 10;
407            00, 10, 10, 10, 10, 10, 10;
408            00, 10, 10, 10, 10, 10, 10;
409            10, 10, 10, 10, 10, 10, 00;
410            10, 00, 10, 10, 10, 10, 10;
411            10, 10, 00, 00, 00, 10, 10);
412
413        assert_eq!(is_corner_fast12(&image, 8, 3, 3), false);
414    }
415
416    #[bench]
417    fn bench_is_corner_fast12_12_noncontiguous(b: &mut Bencher) {
418        let image = black_box(gray_image!(
419            10, 10, 00, 00, 00, 10, 10;
420            10, 00, 10, 10, 10, 00, 10;
421            00, 10, 10, 10, 10, 10, 10;
422            00, 10, 10, 10, 10, 10, 10;
423            10, 10, 10, 10, 10, 10, 00;
424            10, 00, 10, 10, 10, 10, 10;
425            10, 10, 00, 00, 00, 10, 10));
426
427        b.iter(|| black_box(is_corner_fast12(&image, 8, 3, 3)));
428    }
429
430    #[test]
431    fn test_is_corner_fast12_near_image_boundary() {
432        let image = gray_image!(
433            10, 10, 00, 00, 00, 10, 10;
434            10, 00, 10, 10, 10, 00, 10;
435            00, 10, 10, 10, 10, 10, 10;
436            00, 10, 10, 10, 10, 10, 10;
437            00, 10, 10, 10, 10, 10, 10;
438            10, 00, 10, 10, 10, 10, 10;
439            10, 10, 00, 00, 00, 10, 10);
440
441        assert_eq!(is_corner_fast12(&image, 8, 1, 1), false);
442    }
443
444    #[test]
445    fn test_fast_corner_score_12() {
446        let image = gray_image!(
447            10, 10, 00, 00, 00, 10, 10;
448            10, 00, 10, 10, 10, 00, 10;
449            00, 10, 10, 10, 10, 10, 10;
450            00, 10, 10, 10, 10, 10, 10;
451            00, 10, 10, 10, 10, 10, 10;
452            10, 00, 10, 10, 10, 10, 10;
453            10, 10, 00, 00, 00, 10, 10);
454
455        let score = fast_corner_score(&image, 5, 3, 3, Fast::Twelve);
456        assert_eq!(score, 9);
457
458        let score = fast_corner_score(&image, 9, 3, 3, Fast::Twelve);
459        assert_eq!(score, 9);
460    }
461
462    #[test]
463    fn test_is_corner_fast9_9_contiguous_darker_pixels() {
464        let image = gray_image!(
465            10, 10, 00, 00, 00, 10, 10;
466            10, 00, 10, 10, 10, 00, 10;
467            00, 10, 10, 10, 10, 10, 10;
468            00, 10, 10, 10, 10, 10, 10;
469            00, 10, 10, 10, 10, 10, 10;
470            10, 00, 10, 10, 10, 10, 10;
471            10, 10, 10, 10, 10, 10, 10);
472
473        assert_eq!(is_corner_fast9(&image, 8, 3, 3), true);
474    }
475
476    #[test]
477    fn test_is_corner_fast9_9_contiguous_lighter_pixels() {
478        let image = gray_image!(
479            00, 00, 10, 10, 10, 00, 00;
480            00, 10, 00, 00, 00, 10, 00;
481            10, 00, 00, 00, 00, 00, 00;
482            10, 00, 00, 00, 00, 00, 00;
483            10, 00, 00, 00, 00, 00, 00;
484            00, 10, 00, 00, 00, 00, 00;
485            00, 00, 00, 00, 00, 00, 00);
486
487        assert_eq!(is_corner_fast9(&image, 8, 3, 3), true);
488    }
489
490    #[bench]
491    fn bench_is_corner_fast9_9_contiguous_lighter_pixels(b: &mut Bencher) {
492        let image = black_box(gray_image!(
493            00, 00, 10, 10, 10, 00, 00;
494            00, 10, 00, 00, 00, 10, 00;
495            10, 00, 00, 00, 00, 00, 00;
496            10, 00, 00, 00, 00, 00, 00;
497            10, 00, 00, 00, 00, 00, 00;
498            00, 10, 00, 00, 00, 00, 00;
499            00, 00, 00, 00, 00, 00, 00));
500
501        b.iter(|| black_box(is_corner_fast9(&image, 8, 3, 3)));
502    }
503
504    #[test]
505    fn test_is_corner_fast9_12_noncontiguous() {
506        let image = gray_image!(
507            10, 10, 00, 00, 00, 10, 10;
508            10, 00, 10, 10, 10, 00, 10;
509            00, 10, 10, 10, 10, 10, 10;
510            00, 10, 10, 10, 10, 10, 10;
511            10, 10, 10, 10, 10, 10, 00;
512            10, 00, 10, 10, 10, 10, 10;
513            10, 10, 00, 00, 00, 10, 10);
514
515        assert_eq!(is_corner_fast9(&image, 8, 3, 3), false);
516    }
517
518    #[test]
519    fn test_corner_score_fast9() {
520        // 8 pixels with an intensity diff of 20, then 1 with a diff of 10
521        let image = gray_image!(
522            10, 10, 00, 00, 00, 10, 10;
523            10, 00, 10, 10, 10, 00, 10;
524            00, 10, 10, 10, 10, 10, 10;
525            00, 10, 10, 20, 10, 10, 10;
526            00, 10, 10, 10, 10, 10, 10;
527            10, 10, 10, 10, 10, 10, 10;
528            10, 10, 10, 10, 10, 10, 10);
529
530        let score = fast_corner_score(&image, 5, 3, 3, Fast::Nine);
531        assert_eq!(score, 9);
532
533        let score = fast_corner_score(&image, 9, 3, 3, Fast::Nine);
534        assert_eq!(score, 9);
535    }
536}