skeletonize/
lib.rs

1//! A line thinning library for binary images, including edge detection and
2//! threshold functions for preprocessing images into binary images.
3//!
4//! The goal of thinning is to remove excess pixels from the image until the
5//! lines present are one pixel wide, resembling a "skeleton" of the original
6//! pattern.
7//!
8//! The thinning algorithms are based on the papers *Zhang & Suen, 1984* and
9//! *Chen & Hsu, 1988*. See [Reference](#reference).
10//!
11//! ## Usage
12//!
13//! There are three main workflows for thinning images with this library. The
14//! second and third workflows produce binarized images with library functions
15//! before thinning the image.
16//!
17//! The generic [`ForegroundColor`](crate::ForegroundColor) parameter on
18//! [`edge_detection::sobel`][sobel], [`edge_detection::sobel4`][sobel4], and
19//! [`thin_image_edges`](crate::thin_image_edges) specifies what foreground and
20//! background colors the resulting
21//! [`thin_image_edges`](crate::thin_image_edges) image will produce. The
22//! foreground color is the color of the line to be thinned. A foreground color
23//! of white will have a black background and a foreground of black will have a
24//! white background. The generic parameters must match when using an edge
25//! detection function in combination with the thinning function.
26//!
27//! [sobel]: crate::edge_detection::sobel
28//! [sobel4]: crate::edge_detection::sobel4
29//!
30//! An example program can be viewed at `/examples/skeletonize.rs`.
31//!
32//! #### No preprocessing
33//!
34//! The image is already binarized so the edges can be thinned immediately.
35//!
36//! ```
37//! # fn main() -> Result<(), skeletonize::error::SkeletonizeError> {
38//! use skeletonize::{foreground, thin_image_edges, MarkingMethod};
39//!
40//! # let image_buffer = image::ImageBuffer::from_pixel(1, 1, image::Rgb([255, 255, 255]));
41//! # let mut img = image::DynamicImage::ImageRgb8(image_buffer).grayscale();
42//! let method = MarkingMethod::Modified;
43//!
44//! thin_image_edges::<foreground::Black>(&mut img, method, None)?;
45//! # Ok(())
46//! # }
47//! ```
48//!
49//! If this produces poor results and/or takes a long time to run:
50//! - the incorrect foreground color may have been chosen - try using the
51//! opposite color, or
52//! - the image may not be binary and needs to be thresholded.
53//!
54//! #### Edge detection
55//!
56//! Run an edge detection filter on the image and threshold those results before
57//! thinning the lines. Note that the foreground color parameters must match on
58//! the edge detection function and the thinning function.
59//!
60//! ```
61//! # fn main() -> Result<(), skeletonize::error::SkeletonizeError> {
62//! use skeletonize::edge_detection::sobel4;
63//! use skeletonize::{foreground, thin_image_edges, MarkingMethod};
64//!
65//! # let image_buffer = image::ImageBuffer::from_pixel(2, 2, image::Rgb([255, 255, 255]));
66//! # let img = image::DynamicImage::ImageRgb8(image_buffer).grayscale();
67//! let method = MarkingMethod::Modified;
68//! let threshold = Some(0.1);
69//!
70//! let mut filtered = sobel4::<foreground::White>(&img, threshold)?;
71//! thin_image_edges::<foreground::White>(&mut filtered, method, None)?;
72//! # Ok(())
73//! # }
74//! ```
75//!
76//! #### Thresholding
77//!
78//! Threshold the image before thinning, e.g., cleaning up a grayscale image.
79//!
80//! ```
81//! # fn main() -> Result<(), skeletonize::error::SkeletonizeError> {
82//! use skeletonize::{foreground, thin_image_edges, threshold, MarkingMethod};
83//!
84//! # let image_buffer = image::ImageBuffer::from_pixel(2, 2, image::Rgb([255, 255, 255]));
85//! # let mut img = image::DynamicImage::ImageRgb8(image_buffer).grayscale();
86//! let method = MarkingMethod::Modified;
87//! let threshold = 0.1;
88//!
89//! skeletonize::threshold(&mut img, threshold)?;
90//! thin_image_edges::<foreground::Black>(&mut img, method, None)?;
91//! # Ok(())
92//! # }
93//! ```
94//!
95//! ## Reference
96//!
97//! Zhang, T. Y. & Suen, C. Y. (1984). A fast parallel algorithm for thinning
98//! digital patterns. Commun. ACM 27, 3 (March 1984), 236–239.
99//! [DOI:10.1145/357994.358023](https://doi.org/10.1145/357994.358023)
100//!
101//! Chen, Yung-Sheng & Hsu, Wen-Hsing. (1988). A modified fast parallel
102//! algorithm for thinning digital patterns. Pattern Recognition Letters. 7.
103//! 99-106.
104//! [DOI:10.1016/0167-8655(88)90124-9](https://doi.org/10.1016/0167-8655(88)90124-9)
105#![warn(missing_docs, rust_2018_idioms, unsafe_code)]
106
107pub mod edge_detection;
108pub mod error;
109pub mod neighbors;
110mod thinning;
111
112use error::{LumaConversionErrorKind, SkeletonizeError};
113pub use thinning::thin_image_edges;
114
115/// Represents the color of the foreground or features in a binary image. For
116/// example, white text on a black background has a white foreground color and
117/// black background color.
118pub trait ForegroundColor {
119    /// The background color of the image for binarization.
120    const BACKGROUND_COLOR: u8;
121}
122
123/// Implementations of [`ForegroundColor`](crate::ForegroundColor).
124pub mod foreground {
125    /// Black foreground color, represented as `0`.
126    pub struct Black;
127
128    impl crate::ForegroundColor for Black {
129        const BACKGROUND_COLOR: u8 = 255;
130    }
131
132    /// White foreground color, represented by `255`.
133    pub struct White;
134
135    impl crate::ForegroundColor for White {
136        const BACKGROUND_COLOR: u8 = 0;
137    }
138}
139
140/// Classification of pixels in an image used for edge thinning.
141#[derive(Clone, Copy, Debug, PartialEq)]
142#[repr(u8)]
143pub enum Edge {
144    /// The pixel does not contain the foreground color.
145    Empty = 0,
146    /// The pixel contains the foreground color.
147    Filled = 1,
148    /// The pixel is not a valid location within the image.
149    DoesNotExist,
150}
151
152impl Edge {
153    /// Convert the edge status into a `u8` representation.
154    pub fn to_u8(&self) -> u8 {
155        match self {
156            Self::Empty | Self::DoesNotExist => 0,
157            Self::Filled => 1,
158        }
159    }
160}
161
162/// The algorithm that determines which pixels are removed during the edge
163/// thinning process.
164///
165/// ### Reference
166///
167/// <span id="standard"></span>Zhang, T. Y. & Suen, C. Y. (1984). A fast
168/// parallel algorithm for thinning digital patterns. Commun. ACM 27, 3 (March
169/// 1984), 236–239.
170/// [DOI:10.1145/357994.358023](https://doi.org/10.1145/357994.358023)
171///
172/// <span id="modified"></span>Chen, Yung-Sheng & Hsu, Wen-Hsing. (1988). A
173/// modified fast parallel algorithm for thinning digital patterns. Pattern
174/// Recognition Letters. 7. 99-106.
175/// [DOI:10.1016/0167-8655(88)90124-9](https://doi.org/10.1016/0167-8655(88)90124-9)
176#[derive(Clone, Copy, Debug, PartialEq)]
177pub enum MarkingMethod {
178    /// An algorithm based on `Zhang and Suen, 1984`.
179    ///
180    /// See [MarkingMethod](crate::MarkingMethod#standard) for reference.
181    Standard,
182    /// An improved and slightly more complex algorithm than `Standard` based on
183    /// `Chen and Hsu, 1988`. This algorithm improves on the original's
184    /// weaknesses with generally thinner lines and better line connectivity.
185    ///
186    /// See [MarkingMethod](crate::MarkingMethod#modified) for reference.
187    Modified,
188}
189
190impl Default for MarkingMethod {
191    fn default() -> Self {
192        Self::Modified
193    }
194}
195
196/// Create a binary image where values below `threshold` become black and above
197/// become white. `threshold` ranges from 0.0 to 1.0.
198pub fn threshold(img: &mut image::DynamicImage, threshold: f32) -> Result<(), SkeletonizeError> {
199    for pix in img
200        .as_mut_luma8()
201        .ok_or(SkeletonizeError::LumaConversion(
202            LumaConversionErrorKind::ThresholdMutableLuma,
203        ))?
204        .iter_mut()
205    {
206        *pix = if *pix < (threshold * 255.0).round() as u8 {
207            0
208        } else {
209            255
210        };
211    }
212
213    Ok(())
214}