skeletonize/
edge_detection.rs

1//! Edge detection algorithms for preprocessing images.
2
3use crate::error::{LumaConversionErrorKind, SkeletonizeError};
4use crate::ForegroundColor;
5
6/// Sobel vertical `North` gradient operator.
7#[rustfmt::skip]
8pub const SOBEL_NORTH: [f32; 9] = [
9    1.0, 2.0, 1.0,
10    0.0, 0.0, 0.0,
11    -1.0, -2.0, -1.0,
12];
13/// Sobel vertical `South` gradient operator.
14#[rustfmt::skip]
15pub const SOBEL_SOUTH: [f32; 9] = [
16    -1.0, -2.0, -1.0,
17    0.0, 0.0, 0.0,
18    1.0, 2.0, 1.0,
19];
20/// Sobel horizontal `East` gradient operator.
21#[rustfmt::skip]
22pub const SOBEL_EAST: [f32; 9] = [
23    -1.0, 0.0, 1.0,
24    -2.0, 0.0, 2.0,
25    -1.0, 0.0, 1.0,
26];
27/// Sobel horizontal `West` gradient operator.
28#[rustfmt::skip]
29pub const SOBEL_WEST: [f32; 9] = [
30    1.0, 0.0, -1.0,
31    2.0, 0.0, -2.0,
32    1.0, 0.0, -1.0,
33];
34
35/// Detect edges in an image using [`SOBEL_EAST`](SOBEL_EAST) and
36/// [`SOBEL_NORTH`](SOBEL_NORTH) gradient operators.
37/// The image should not have transparency.
38///
39/// `threshold` is an optional parameter between 0.0 and 1.0 which is used to
40/// binarize the image. Pixels below that `Luma` threshold will be converted
41/// to the background color.
42pub fn sobel<F: ForegroundColor>(
43    img: &image::DynamicImage,
44    threshold: Option<f32>,
45) -> Result<image::DynamicImage, SkeletonizeError> {
46    let mut filter_up = img.filter3x3(&SOBEL_NORTH);
47    let filtered_right = img.filter3x3(&SOBEL_EAST);
48    let mutable_error = SkeletonizeError::LumaConversion(LumaConversionErrorKind::SobelMutableLuma);
49    let immutable_error = SkeletonizeError::LumaConversion(LumaConversionErrorKind::SobelLuma);
50
51    let iter_down = filter_up.as_mut_luma8().ok_or(mutable_error)?.iter_mut();
52    let iter_right = filtered_right.as_luma8().ok_or(immutable_error)?.iter();
53
54    for (g_down, g_right) in iter_down.zip(iter_right) {
55        let res = (f32::from(*g_down) / 255.0).hypot(f32::from(*g_right) / 255.0);
56
57        if let Some(threshold) = threshold {
58            *g_down = if res < threshold {
59                F::BACKGROUND_COLOR
60            } else {
61                !F::BACKGROUND_COLOR
62            }
63        } else {
64            *g_down = (res * 255.0).round() as u8;
65        }
66    }
67
68    // If ForegroundColor is Black and threshold None, edges would stay white
69    // so we need to invert the result before returning it.
70    if threshold.is_none() && F::BACKGROUND_COLOR == 255 {
71        filter_up.invert()
72    }
73
74    Ok(filter_up)
75}
76
77/// Detect edges in an image using four Sobel gradient operators:
78/// [`SOBEL_NORTH`](SOBEL_NORTH), [`SOBEL_SOUTH`](SOBEL_SOUTH),
79/// [`SOBEL_EAST`](SOBEL_EAST), and [`SOBEL_WEST`](SOBEL_WEST).
80/// The image should not have transparency.
81///
82/// `threshold` is an optional parameter between 0.0 and 1.0 which is used to
83/// binarize the image. Pixels below that `Luma` threshold will be converted
84/// to the background color.
85pub fn sobel4<F: ForegroundColor>(
86    img: &image::DynamicImage,
87    threshold: Option<f32>,
88) -> Result<image::DynamicImage, SkeletonizeError> {
89    let mut filter_up = img.filter3x3(&SOBEL_NORTH);
90    let filter_down = img.filter3x3(&SOBEL_SOUTH);
91    let filter_right = img.filter3x3(&SOBEL_EAST);
92    let filter_left = img.filter3x3(&SOBEL_WEST);
93
94    let mutable_error = SkeletonizeError::LumaConversion(LumaConversionErrorKind::SobelMutableLuma);
95    let immutable_error = SkeletonizeError::LumaConversion(LumaConversionErrorKind::SobelLuma);
96
97    let iter_up = filter_up.as_mut_luma8().ok_or(mutable_error)?.iter_mut();
98    let iter_down = filter_down.as_luma8().ok_or(immutable_error)?.iter();
99    let iter_right = filter_right.as_luma8().ok_or(immutable_error)?.iter();
100    let iter_left = filter_left.as_luma8().ok_or(immutable_error)?.iter();
101
102    for (((g_up, g_down), g_left), g_right) in iter_up.zip(iter_down).zip(iter_right).zip(iter_left)
103    {
104        let vertical = (f32::from(*g_up) - f32::from(*g_down)) / 255.0;
105        let horizontal = (f32::from(*g_right) - f32::from(*g_left)) / 255.0;
106        let res = vertical.hypot(horizontal);
107
108        if let Some(threshold) = threshold {
109            *g_up = if res < threshold {
110                F::BACKGROUND_COLOR
111            } else {
112                !F::BACKGROUND_COLOR
113            }
114        } else {
115            *g_up = (res * 255.0).round() as u8;
116        }
117    }
118
119    // If ForegroundColor is Black and threshold None, edges would stay white
120    // so we need to invert the result before returning it.
121    if threshold.is_none() && F::BACKGROUND_COLOR == 255 {
122        filter_up.invert()
123    }
124
125    Ok(filter_up)
126}