imageproc/
suppress.rs

1//! Functions for suppressing non-maximal values.
2
3use crate::definitions::{Position, Score};
4use image::{GenericImage, ImageBuffer, Luma, Primitive};
5use std::cmp;
6
7/// Returned image has zeroes for all inputs pixels which do not have the greatest
8/// intensity in the (2 * radius + 1) square block centred on them.
9/// Ties are resolved lexicographically.
10pub fn suppress_non_maximum<I, C>(image: &I, radius: u32) -> ImageBuffer<Luma<C>, Vec<C>>
11where
12    I: GenericImage<Pixel = Luma<C>>,
13    C: Primitive + Ord,
14{
15    let (width, height) = image.dimensions();
16    let mut out: ImageBuffer<Luma<C>, Vec<C>> = ImageBuffer::new(width, height);
17    if width == 0 || height == 0 {
18        return out;
19    }
20
21    // We divide the image into a grid of blocks of size r * r. We find the maximum
22    // value in each block, and then test whether this is in fact the maximum value
23    // in the (2r + 1) * (2r + 1) block centered on it. Any pixel that's not maximal
24    // within its r * r grid cell can't be a local maximum so we need only perform
25    // the (2r + 1) * (2r + 1) search once per r * r grid cell (as opposed to once
26    // per pixel in the naive implementation of this algorithm).
27
28    for y in (0..height).step_by(radius as usize + 1) {
29        for x in (0..width).step_by(radius as usize + 1) {
30            let mut best_x = x;
31            let mut best_y = y;
32            let mut mi = image.get_pixel(x, y)[0];
33
34            // These mins are necessary for when radius > min(width, height)
35            for cy in y..cmp::min(height, y + radius + 1) {
36                for cx in x..cmp::min(width, x + radius + 1) {
37                    let ci = unsafe { image.unsafe_get_pixel(cx, cy)[0] };
38                    if ci < mi {
39                        continue;
40                    }
41                    if ci > mi || (cx, cy) < (best_x, best_y) {
42                        best_x = cx;
43                        best_y = cy;
44                        mi = ci;
45                    }
46                }
47            }
48
49            let x0 = if radius >= best_x { 0 } else { best_x - radius };
50            let x1 = x;
51            let x2 = cmp::min(width, x + radius + 1);
52            let x3 = cmp::min(width, best_x + radius + 1);
53
54            let y0 = if radius >= best_y { 0 } else { best_y - radius };
55            let y1 = y;
56            let y2 = cmp::min(height, y + radius + 1);
57            let y3 = cmp::min(height, best_y + radius + 1);
58
59            // Above initial r * r block
60            let mut failed = contains_greater_value(image, best_x, best_y, mi, y0, y1, x0, x3);
61            // Left of initial r * r block
62            failed |= contains_greater_value(image, best_x, best_y, mi, y1, y2, x0, x1);
63            // Right of initial r * r block
64            failed |= contains_greater_value(image, best_x, best_y, mi, y1, y2, x2, x3);
65            // Below initial r * r block
66            failed |= contains_greater_value(image, best_x, best_y, mi, y2, y3, x0, x3);
67
68            if !failed {
69                unsafe { out.unsafe_put_pixel(best_x, best_y, Luma([mi])) };
70            }
71        }
72    }
73
74    out
75}
76
77/// Returns true if the given block contains a larger value than
78/// the input, or contains an equal value with lexicographically
79/// lesser coordinates.
80fn contains_greater_value<I, C>(
81    image: &I,
82    x: u32,
83    y: u32,
84    v: C,
85    y_lower: u32,
86    y_upper: u32,
87    x_lower: u32,
88    x_upper: u32,
89) -> bool
90where
91    I: GenericImage<Pixel = Luma<C>>,
92    C: Primitive + Ord,
93{
94    for cy in y_lower..y_upper {
95        for cx in x_lower..x_upper {
96            let ci = unsafe { image.unsafe_get_pixel(cx, cy)[0] };
97            if ci < v {
98                continue;
99            }
100            if ci > v || (cx, cy) < (x, y) {
101                return true;
102            }
103        }
104    }
105    false
106}
107
108/// Returns all items which have the highest score in the
109/// (2 * radius + 1) square block centred on them. Ties are resolved lexicographically.
110pub fn local_maxima<T>(ts: &[T], radius: u32) -> Vec<T>
111where
112    T: Position + Score + Copy,
113{
114    let mut ordered_ts = ts.to_vec();
115    ordered_ts.sort_by_key(|&c| (c.y(), c.x()));
116    let height = match ordered_ts.last() {
117        Some(t) => t.y(),
118        None => 0,
119    };
120
121    let mut ts_by_row = vec![vec![]; (height + 1) as usize];
122    for t in &ordered_ts {
123        ts_by_row[t.y() as usize].push(t);
124    }
125
126    let mut max_ts = vec![];
127    for t in &ordered_ts {
128        let cx = t.x();
129        let cy = t.y();
130        let cs = t.score();
131
132        let mut is_max = true;
133        let row_lower = if radius > cy { 0 } else { cy - radius };
134        let row_upper = if cy + radius + 1 > height {
135            height
136        } else {
137            cy + radius + 1
138        };
139        for y in row_lower..row_upper {
140            for c in &ts_by_row[y as usize] {
141                if c.x() + radius < cx {
142                    continue;
143                }
144                if c.x() > cx + radius {
145                    break;
146                }
147                if c.score() > cs {
148                    is_max = false;
149                    break;
150                }
151                if c.score() < cs {
152                    continue;
153                }
154                // Break tiebreaks lexicographically
155                if (c.y(), c.x()) < (cy, cx) {
156                    is_max = false;
157                    break;
158                }
159            }
160            if !is_max {
161                break;
162            }
163        }
164
165        if is_max {
166            max_ts.push(*t);
167        }
168    }
169
170    max_ts
171}
172
173#[cfg(test)]
174mod tests {
175    use super::{local_maxima, suppress_non_maximum};
176    use crate::definitions::{Position, Score};
177    use crate::noise::gaussian_noise_mut;
178    use crate::property_testing::GrayTestImage;
179    use crate::utils::pixel_diff_summary;
180    use image::{GenericImage, GrayImage, ImageBuffer, Luma, Primitive};
181    use quickcheck::{quickcheck, TestResult};
182    use std::cmp;
183    use test::Bencher;
184
185    #[derive(PartialEq, Debug, Copy, Clone)]
186    struct T {
187        x: u32,
188        y: u32,
189        score: f32,
190    }
191
192    impl T {
193        fn new(x: u32, y: u32, score: f32) -> T {
194            T { x, y, score }
195        }
196    }
197
198    impl Position for T {
199        fn x(&self) -> u32 {
200            self.x
201        }
202        fn y(&self) -> u32 {
203            self.y
204        }
205    }
206
207    impl Score for T {
208        fn score(&self) -> f32 {
209            self.score
210        }
211    }
212
213    #[test]
214    fn test_local_maxima() {
215        let ts = vec![
216            // Suppress vertically
217            T::new(0, 0, 8f32),
218            T::new(0, 3, 10f32),
219            T::new(0, 6, 9f32),
220            // Suppress horizontally
221            T::new(5, 5, 10f32),
222            T::new(7, 5, 15f32),
223            // Tiebreak
224            T::new(12, 20, 10f32),
225            T::new(13, 20, 10f32),
226            T::new(13, 21, 10f32),
227        ];
228
229        let expected = vec![
230            T::new(0, 3, 10f32),
231            T::new(7, 5, 15f32),
232            T::new(12, 20, 10f32),
233        ];
234
235        let max = local_maxima(&ts, 3);
236        assert_eq!(max, expected);
237    }
238
239    #[bench]
240    fn bench_local_maxima_dense(b: &mut Bencher) {
241        let mut ts = vec![];
242        for x in 0..20 {
243            for y in 0..20 {
244                let score = (x * y) % 15;
245                ts.push(T::new(x, y, score as f32));
246            }
247        }
248        b.iter(|| local_maxima(&ts, 15));
249    }
250
251    #[bench]
252    fn bench_local_maxima_sparse(b: &mut Bencher) {
253        let mut ts = vec![];
254        for x in 0..20 {
255            for y in 0..20 {
256                ts.push(T::new(50 * x, 50 * y, 50f32));
257            }
258        }
259        b.iter(|| local_maxima(&ts, 15));
260    }
261
262    #[test]
263    fn test_suppress_non_maximum() {
264        let mut image = GrayImage::new(25, 25);
265        // Suppress vertically
266        image.put_pixel(0, 0, Luma([8u8]));
267        image.put_pixel(0, 3, Luma([10u8]));
268        image.put_pixel(0, 6, Luma([9u8]));
269        // Suppress horizontally
270        image.put_pixel(5, 5, Luma([10u8]));
271        image.put_pixel(7, 5, Luma([15u8]));
272        // Tiebreak
273        image.put_pixel(12, 20, Luma([10u8]));
274        image.put_pixel(13, 20, Luma([10u8]));
275        image.put_pixel(13, 21, Luma([10u8]));
276
277        let mut expected = GrayImage::new(25, 25);
278        expected.put_pixel(0, 3, Luma([10u8]));
279        expected.put_pixel(7, 5, Luma([15u8]));
280        expected.put_pixel(12, 20, Luma([10u8]));
281
282        let actual = suppress_non_maximum(&image, 3);
283        assert_pixels_eq!(actual, expected);
284    }
285
286    #[test]
287    fn test_suppress_non_maximum_handles_radius_greater_than_image_side() {
288        // Don't care about output pixels, just want to make sure that
289        // we don't go out of bounds when radius exceeds width or height.
290        let image = GrayImage::new(7, 3);
291        let r = suppress_non_maximum(&image, 5);
292        let image = GrayImage::new(3, 7);
293        let s = suppress_non_maximum(&image, 5);
294        // Use r and s to silence warnings about unused variables.
295        assert!(r.width() == 7);
296        assert!(s.width() == 3);
297    }
298
299    #[bench]
300    fn bench_suppress_non_maximum_increasing_gradient(b: &mut Bencher) {
301        // Increasing gradient in both directions. This can be a worst-case for
302        // early-abort strategies.
303        let img = ImageBuffer::from_fn(40, 20, |x, y| Luma([(x + y) as u8]));
304        b.iter(|| suppress_non_maximum(&img, 7));
305    }
306
307    #[bench]
308    fn bench_suppress_non_maximum_decreasing_gradient(b: &mut Bencher) {
309        let width = 40u32;
310        let height = 20u32;
311        let img = ImageBuffer::from_fn(width, height, |x, y| {
312            Luma([((width - x) + (height - y)) as u8])
313        });
314        b.iter(|| suppress_non_maximum(&img, 7));
315    }
316
317    #[bench]
318    fn bench_suppress_non_maximum_noise_7(b: &mut Bencher) {
319        let mut img: GrayImage = ImageBuffer::new(40, 20);
320        gaussian_noise_mut(&mut img, 128f64, 30f64, 1);
321        b.iter(|| suppress_non_maximum(&img, 7));
322    }
323
324    #[bench]
325    fn bench_suppress_non_maximum_noise_3(b: &mut Bencher) {
326        let mut img: GrayImage = ImageBuffer::new(40, 20);
327        gaussian_noise_mut(&mut img, 128f64, 30f64, 1);
328        b.iter(|| suppress_non_maximum(&img, 3));
329    }
330
331    #[bench]
332    fn bench_suppress_non_maximum_noise_1(b: &mut Bencher) {
333        let mut img: GrayImage = ImageBuffer::new(40, 20);
334        gaussian_noise_mut(&mut img, 128f64, 30f64, 1);
335        b.iter(|| suppress_non_maximum(&img, 1));
336    }
337
338    /// Reference implementation of suppress_non_maximum. Used to validate
339    /// the (presumably faster) actual implementation.
340    fn suppress_non_maximum_reference<I, C>(image: &I, radius: u32) -> ImageBuffer<Luma<C>, Vec<C>>
341    where
342        I: GenericImage<Pixel = Luma<C>>,
343        C: Primitive + Ord,
344    {
345        let (width, height) = image.dimensions();
346        let mut out = ImageBuffer::new(width, height);
347        out.copy_from(image, 0, 0).unwrap();
348
349        let iradius = radius as i32;
350        let iheight = height as i32;
351        let iwidth = width as i32;
352
353        // We update zero values from out as we go, so to check intensities
354        // we need to read values from the input image.
355        for y in 0..height {
356            for x in 0..width {
357                let intensity = image.get_pixel(x, y)[0];
358                let mut is_max = true;
359
360                let y_lower = cmp::max(0, y as i32 - iradius);
361                let y_upper = cmp::min(y as i32 + iradius + 1, iheight);
362                let x_lower = cmp::max(0, x as i32 - iradius);
363                let x_upper = cmp::min(x as i32 + iradius + 1, iwidth);
364
365                for py in y_lower..y_upper {
366                    for px in x_lower..x_upper {
367                        let v = image.get_pixel(px as u32, py as u32)[0];
368                        // Handle intensity tiebreaks lexicographically
369                        let candidate_is_lexically_earlier = (px as u32, py as u32) < (x, y);
370                        if v > intensity || (v == intensity && candidate_is_lexically_earlier) {
371                            is_max = false;
372                            break;
373                        }
374                    }
375                }
376
377                if !is_max {
378                    out.put_pixel(x, y, Luma([C::zero()]));
379                }
380            }
381        }
382
383        out
384    }
385
386    #[test]
387    fn test_suppress_non_maximum_matches_reference_implementation() {
388        fn prop(image: GrayTestImage) -> TestResult {
389            let expected = suppress_non_maximum_reference(&image.0, 3);
390            let actual = suppress_non_maximum(&image.0, 3);
391            match pixel_diff_summary(&actual, &expected) {
392                None => TestResult::passed(),
393                Some(err) => TestResult::error(err),
394            }
395        }
396        quickcheck(prop as fn(GrayTestImage) -> TestResult);
397    }
398
399    #[test]
400    fn test_step() {
401        assert_eq!((0u32..5).step_by(4).collect::<Vec<u32>>(), vec![0, 4]);
402        assert_eq!((0u32..4).step_by(4).collect::<Vec<u32>>(), vec![0]);
403        assert_eq!((4u32..4).step_by(4).collect::<Vec<u32>>(), vec![]);
404    }
405}