imageproc/
gradients.rs

1//! Functions for computing gradients of image intensities.
2
3use crate::definitions::{HasBlack, Image};
4use crate::filter::filter3x3;
5use crate::map::{ChannelMap, WithChannel};
6use image::{GenericImage, GenericImageView, GrayImage, Luma, Pixel};
7use itertools::multizip;
8
9/// Sobel filter for detecting vertical gradients.
10///
11/// Used by the [`vertical_sobel`](fn.vertical_sobel.html) function.
12#[rustfmt::skip]
13pub static VERTICAL_SOBEL: [i32; 9] = [
14    -1, -2, -1,
15     0,  0,  0,
16     1,  2,  1];
17
18/// Sobel filter for detecting horizontal gradients.
19///
20/// Used by the [`horizontal_sobel`](fn.horizontal_sobel.html) function.
21#[rustfmt::skip]
22pub static HORIZONTAL_SOBEL: [i32; 9] = [
23    -1, 0, 1,
24    -2, 0, 2,
25    -1, 0, 1];
26
27/// Scharr filter for detecting vertical gradients.
28///
29/// Used by the [`vertical_scharr`](fn.vertical_scharr.html) function.
30#[rustfmt::skip]
31pub static VERTICAL_SCHARR: [i32; 9] = [
32    -3, -10,  -3,
33     0,   0,   0,
34     3,  10,   3];
35
36/// Scharr filter for detecting horizontal gradients.
37///
38/// Used by the [`horizontal_scharr`](fn.horizontal_scharr.html) function.
39#[rustfmt::skip]
40pub static HORIZONTAL_SCHARR: [i32; 9] = [
41     -3,  0,   3,
42    -10,  0,  10,
43     -3,  0,   3];
44
45/// Prewitt filter for detecting vertical gradients.
46///
47/// Used by the [`vertical_prewitt`](fn.vertical_prewitt.html) function.
48#[rustfmt::skip]
49pub static VERTICAL_PREWITT: [i32; 9] = [
50    -1, -1, -1,
51     0,  0,  0,
52     1,  1,  1];
53
54/// Prewitt filter for detecting horizontal gradients.
55///
56/// Used by the [`horizontal_prewitt`](fn.horizontal_prewitt.html) function.
57#[rustfmt::skip]
58pub static HORIZONTAL_PREWITT: [i32; 9] = [
59    -1, 0, 1,
60    -1, 0, 1,
61    -1, 0, 1];
62
63/// Convolves an image with the [`HORIZONTAL_SOBEL`](static.HORIZONTAL_SOBEL.html)
64/// kernel to detect horizontal gradients.
65pub fn horizontal_sobel(image: &GrayImage) -> Image<Luma<i16>> {
66    filter3x3(image, &HORIZONTAL_SOBEL)
67}
68
69/// Convolves an image with the [`VERTICAL_SOBEL`](static.VERTICAL_SOBEL.html)
70/// kernel to detect vertical gradients.
71pub fn vertical_sobel(image: &GrayImage) -> Image<Luma<i16>> {
72    filter3x3(image, &VERTICAL_SOBEL)
73}
74
75/// Convolves an image with the [`HORIZONTAL_SCHARR`](static.HORIZONTAL_SCHARR.html)
76/// kernel to detect horizontal gradients.
77pub fn horizontal_scharr(image: &GrayImage) -> Image<Luma<i16>> {
78    filter3x3(image, &HORIZONTAL_SCHARR)
79}
80
81/// Convolves an image with the [`VERTICAL_SCHARR`](static.VERTICAL_SCHARR.html)
82/// kernel to detect vertical gradients.
83pub fn vertical_scharr(image: &GrayImage) -> Image<Luma<i16>> {
84    filter3x3(image, &VERTICAL_SCHARR)
85}
86
87/// Convolves an image with the [`HORIZONTAL_PREWITT`](static.HORIZONTAL_PREWITT.html)
88/// kernel to detect horizontal gradients.
89pub fn horizontal_prewitt(image: &GrayImage) -> Image<Luma<i16>> {
90    filter3x3(image, &HORIZONTAL_PREWITT)
91}
92
93/// Convolves an image with the [`VERTICAL_PREWITT`](static.VERTICAL_PREWITT.html)
94/// kernel to detect vertical gradients.
95pub fn vertical_prewitt(image: &GrayImage) -> Image<Luma<i16>> {
96    filter3x3(image, &VERTICAL_PREWITT)
97}
98
99/// Returns the magnitudes of gradients in an image using Sobel filters.
100pub fn sobel_gradients(image: &GrayImage) -> Image<Luma<u16>> {
101    gradients(image, &HORIZONTAL_SOBEL, &VERTICAL_SOBEL, |p| p)
102}
103
104/// Computes per-channel gradients using Sobel filters and calls `f`
105/// to compute each output pixel.
106///
107/// # Examples
108/// ```
109/// # extern crate image;
110/// # #[macro_use]
111/// # extern crate imageproc;
112/// # fn main() {
113/// use imageproc::gradients::{sobel_gradient_map};
114/// use image::Luma;
115/// use std::cmp;
116///
117/// // A shallow horizontal gradient in the red
118/// // channel, a steeper vertical gradient in the
119/// // blue channel, constant in the green channel.
120/// let input = rgb_image!(
121///     [0, 5, 0], [1, 5, 0], [2, 5, 0];
122///     [0, 5, 2], [1, 5, 2], [2, 5, 2];
123///     [0, 5, 4], [1, 5, 4], [2, 5, 4]
124/// );
125///
126/// // Computing independent per-channel gradients.
127/// let channel_gradient = rgb_image!(type: u16,
128///     [ 4,  0,  8], [ 8,  0,  8], [ 4,  0,  8];
129///     [ 4,  0, 16], [ 8,  0, 16], [ 4,  0, 16];
130///     [ 4,  0,  8], [ 8,  0,  8], [ 4,  0,  8]
131/// );
132///
133/// assert_pixels_eq!(
134///     sobel_gradient_map(&input, |p| p),
135///     channel_gradient
136/// );
137///
138/// // Defining the gradient of an RGB image to be the
139/// // mean of its per-channel gradients.
140/// let mean_gradient = gray_image!(type: u16,
141///     4, 5, 4;
142///     6, 8, 6;
143///     4, 5, 4
144/// );
145///
146/// assert_pixels_eq!(
147///     sobel_gradient_map(&input, |p| {
148///         let mean = (p[0] + p[1] + p[2]) / 3;
149///         Luma([mean])
150///     }),
151///     mean_gradient
152/// );
153///
154/// // Defining the gradient of an RGB image to be the pixelwise
155/// // maximum of its per-channel gradients.
156/// let max_gradient = gray_image!(type: u16,
157///      8,  8,  8;
158///     16, 16, 16;
159///      8,  8,  8
160/// );
161///
162/// assert_pixels_eq!(
163///     sobel_gradient_map(&input, |p| {
164///         let max = cmp::max(cmp::max(p[0], p[1]), p[2]);
165///         Luma([max])
166///     }),
167///     max_gradient
168/// );
169/// # }
170pub fn sobel_gradient_map<P, F, Q>(image: &Image<P>, f: F) -> Image<Q>
171where
172    P: Pixel<Subpixel = u8> + WithChannel<u16> + WithChannel<i16>,
173    Q: Pixel,
174    ChannelMap<P, u16>: HasBlack,
175    F: Fn(ChannelMap<P, u16>) -> Q,
176{
177    gradients(image, &HORIZONTAL_SOBEL, &VERTICAL_SOBEL, f)
178}
179
180/// Returns the magnitudes of gradients in an image using Prewitt filters.
181pub fn prewitt_gradients(image: &GrayImage) -> Image<Luma<u16>> {
182    gradients(image, &HORIZONTAL_PREWITT, &VERTICAL_PREWITT, |p| p)
183}
184
185// TODO: Returns directions as well as magnitudes.
186// TODO: Support filtering without allocating a fresh image - filtering functions could
187// TODO: take some kind of pixel-sink. This would allow us to compute gradient magnitudes
188// TODO: and directions without allocating intermediates for vertical and horizontal gradients.
189fn gradients<P, F, Q>(
190    image: &Image<P>,
191    horizontal_kernel: &[i32; 9],
192    vertical_kernel: &[i32; 9],
193    f: F,
194) -> Image<Q>
195where
196    P: Pixel<Subpixel = u8> + WithChannel<u16> + WithChannel<i16>,
197    Q: Pixel,
198    ChannelMap<P, u16>: HasBlack,
199    F: Fn(ChannelMap<P, u16>) -> Q,
200{
201    let horizontal: Image<ChannelMap<P, i16>> = filter3x3::<_, _, i16>(image, horizontal_kernel);
202    let vertical: Image<ChannelMap<P, i16>> = filter3x3::<_, _, i16>(image, vertical_kernel);
203
204    let (width, height) = image.dimensions();
205    let mut out = Image::<Q>::new(width, height);
206
207    // This would be more concise using itertools::multizip over image pixels, but that increased runtime by around 20%
208    for y in 0..height {
209        for x in 0..width {
210            // JUSTIFICATION
211            //  Benefit
212            //      Using checked indexing here makes this sobel_gradients 1.1x slower,
213            //      as measured by bench_sobel_gradients
214            //  Correctness
215            //      x and y are in bounds for image by construction,
216            //      vertical and horizontal are the result of calling filter3x3 on image,
217            //      and filter3x3 returns an image of the same size as its input
218            let (h, v) = unsafe {
219                (
220                    horizontal.unsafe_get_pixel(x, y),
221                    vertical.unsafe_get_pixel(x, y),
222                )
223            };
224            let mut p = ChannelMap::<P, u16>::black();
225
226            for (h, v, p) in multizip((h.channels(), v.channels(), p.channels_mut())) {
227                *p = gradient_magnitude(*h as f32, *v as f32);
228            }
229
230            // JUSTIFICATION
231            //  Benefit
232            //      Using checked indexing here makes this sobel_gradients 1.1x slower,
233            //      as measured by bench_sobel_gradients
234            //  Correctness
235            //      x and y are in bounds for image by construction,
236            //      and out has the same dimensions
237            unsafe {
238                out.unsafe_put_pixel(x, y, f(p));
239            }
240        }
241    }
242
243    out
244}
245
246#[inline]
247fn gradient_magnitude(dx: f32, dy: f32) -> u16 {
248    (dx.powi(2) + dy.powi(2)).sqrt() as u16
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::utils::gray_bench_image;
255    use image::{ImageBuffer, Luma};
256    use test::{black_box, Bencher};
257
258    #[rustfmt::skip::macros(gray_image)]
259    #[test]
260    fn test_gradients_constant_image() {
261        let image = ImageBuffer::from_pixel(5, 5, Luma([15u8]));
262        let expected = ImageBuffer::from_pixel(5, 5, Luma([0i16]));
263        assert_pixels_eq!(horizontal_sobel(&image), expected);
264        assert_pixels_eq!(vertical_sobel(&image), expected);
265        assert_pixels_eq!(horizontal_scharr(&image), expected);
266        assert_pixels_eq!(vertical_scharr(&image), expected);
267        assert_pixels_eq!(horizontal_prewitt(&image), expected);
268        assert_pixels_eq!(vertical_prewitt(&image), expected);
269    }
270
271    #[test]
272    fn test_horizontal_sobel_gradient_image() {
273        let image = gray_image!(
274            3, 2, 1;
275            6, 5, 4;
276            9, 8, 7);
277
278        let expected = gray_image!(type: i16,
279            -4, -8, -4;
280            -4, -8, -4;
281            -4, -8, -4);
282
283        let filtered = horizontal_sobel(&image);
284        assert_pixels_eq!(filtered, expected);
285    }
286
287    #[test]
288    fn test_vertical_sobel_gradient_image() {
289        let image = gray_image!(
290            3, 6, 9;
291            2, 5, 8;
292            1, 4, 7);
293
294        let expected = gray_image!(type: i16,
295            -4, -4, -4;
296            -8, -8, -8;
297            -4, -4, -4);
298
299        let filtered = vertical_sobel(&image);
300        assert_pixels_eq!(filtered, expected);
301    }
302
303    #[test]
304    fn test_horizontal_scharr_gradient_image() {
305        let image = gray_image!(
306            3, 2, 1;
307            6, 5, 4;
308            9, 8, 7);
309
310        let expected = gray_image!(type: i16,
311            -16, -32, -16;
312            -16, -32, -16;
313            -16, -32, -16);
314
315        let filtered = horizontal_scharr(&image);
316        assert_pixels_eq!(filtered, expected);
317    }
318
319    #[test]
320    fn test_vertical_scharr_gradient_image() {
321        let image = gray_image!(
322            3, 6, 9;
323            2, 5, 8;
324            1, 4, 7);
325
326        let expected = gray_image!(type: i16,
327            -16, -16, -16;
328            -32, -32, -32;
329            -16, -16, -16);
330
331        let filtered = vertical_scharr(&image);
332        assert_pixels_eq!(filtered, expected);
333    }
334
335    #[test]
336    fn test_horizontal_prewitt_gradient_image() {
337        let image = gray_image!(
338            3, 2, 1;
339            6, 5, 4;
340            9, 8, 7);
341
342        let expected = gray_image!(type: i16,
343            -3, -6, -3;
344            -3, -6, -3;
345            -3, -6, -3);
346
347        let filtered = horizontal_prewitt(&image);
348        assert_pixels_eq!(filtered, expected);
349    }
350
351    #[test]
352    fn test_vertical_prewitt_gradient_image() {
353        let image = gray_image!(
354            3, 6, 9;
355            2, 5, 8;
356            1, 4, 7);
357
358        let expected = gray_image!(type: i16,
359            -3, -3, -3;
360            -6, -6, -6;
361            -3, -3, -3);
362
363        let filtered = vertical_prewitt(&image);
364        assert_pixels_eq!(filtered, expected);
365    }
366
367    #[bench]
368    fn bench_sobel_gradients(b: &mut Bencher) {
369        let image = gray_bench_image(500, 500);
370        b.iter(|| {
371            let gradients = sobel_gradients(&image);
372            black_box(gradients);
373        });
374    }
375}