stroke_width_transform/
lib.rs

1use image::{
2    imageops::{resize, FilterType},
3    GenericImage, GenericImageView, GrayImage, ImageBuffer, Luma, Pixel, RgbImage,
4};
5use imageproc::{definitions::Image, edges, gradients};
6use num_traits::{NumCast, Pow, ToPrimitive};
7
8#[derive(Debug)]
9struct Directions {
10    x: Image<Luma<f32>>,
11    y: Image<Luma<f32>>,
12}
13
14#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
15struct Position {
16    x: u32,
17    y: u32,
18}
19
20type Ray = Vec<Position>;
21
22pub struct StrokeWidthTransform {
23    one_over_gamma: f32,
24    dark_on_bright: bool,
25    canny_low: f32,
26    canny_high: f32,
27}
28
29impl Default for StrokeWidthTransform {
30    fn default() -> Self {
31        let gamma = 2.2;
32        Self {
33            one_over_gamma: 1.0 / gamma,
34            dark_on_bright: true,
35            canny_low: 20.,
36            canny_high: 75.,
37        }
38    }
39}
40
41impl StrokeWidthTransform {
42    pub fn default_bright_on_dark() -> Self {
43        Self {
44            dark_on_bright: false,
45            ..Self::default()
46        }
47    }
48
49    /// Applies the Stroke Width Transformation to the image.
50    pub fn apply(&self, img: &RgbImage) -> GrayImage {
51        let gray = self.gleam(img);
52
53        // Temporarily increase the image size for edge detection to work (better).
54        let gray = Self::double_the_size(gray);
55        let edges = self.get_edges(&gray);
56        let directions = self.get_gradient_directions(&gray);
57
58        // The grayscale image is not required anymore; we can free some memory.
59        drop(gray);
60
61        self.transform(edges, directions)
62    }
63
64    fn transform(&self, edges: GrayImage, directions: Directions) -> GrayImage {
65        let mut rays: Vec<Ray> = Vec::new();
66
67        let (width, height) = edges.dimensions();
68        let mut swt: Image<Luma<u32>> = ImageBuffer::new(width, height);
69
70        for y in 0..height {
71            for x in 0..width {
72                let edge = unsafe { edges.unsafe_get_pixel(x, y) };
73                // TODO: Verify edge value range, should be either 0 or 255.
74                if edge[0] < 128 {
75                    continue;
76                }
77
78                if let Some(ray) =
79                    self.process_pixel(Position { x, y }, &edges, &directions, &mut swt)
80                {
81                    rays.push(ray);
82                }
83            }
84        }
85
86        // Next-generation println! debugging:
87        // swt.save("swt-out.jpg");
88
89        convert_u32_to_u8_img(swt)
90    }
91
92    /// Obtains the stroke width starting from the specified position.
93    fn process_pixel(
94        &self,
95        pos: Position,
96        edges: &GrayImage,
97        directions: &Directions,
98        swt: &mut Image<Luma<u32>>,
99    ) -> Option<Ray> {
100        // Keep track of the image dimensions for boundary tests.
101        let (width, height) = edges.dimensions();
102
103        // The direction in which we travel the gradient depends on the type of text
104        // we want to find. For dark text on light background, follow the opposite
105        // direction (into the dark are); for light text on dark background, follow
106        // the gradient as is.
107        let gradient_direction: f32 = if self.dark_on_bright { -1. } else { 1. };
108
109        // Starting from the current pixel we will shoot a ray into the direction
110        // of the pixel's gradient and keep track of all pixels in that direction
111        // that still lie on an edge.
112        let mut ray = Vec::new();
113        ray.push(pos);
114
115        // Obtain the direction to step into.
116        // TODO: Obtain arctan of directions initially, then obtain dir_x and dir_y using cos and sin here.
117        //       See below for another use of the directions.
118        let dir_x = unsafe { directions.x.unsafe_get_pixel(pos.x, pos.y) }[0];
119        let dir_y = unsafe { directions.y.unsafe_get_pixel(pos.x, pos.y) }[0];
120
121        // Since some pixels have no gradient, normalization of the gradient
122        // is a division by zero for them, resulting in NaN. These values
123        // should not bother us since we explicitly tested for an edge before.
124        debug_assert!(!dir_x.is_nan());
125        debug_assert!(!dir_y.is_nan());
126
127        // Traverse the pixels along the direction.
128        let mut prev_pos = Position { x: 0, y: 0 };
129        let mut steps_taken: usize = 0;
130        loop {
131            // Advance to the next pixel on the line.
132            steps_taken += 1;
133            let cur_x =
134                (pos.x as f32 + gradient_direction * dir_x * steps_taken as f32).floor() as i64;
135            let cur_y =
136                (pos.y as f32 + gradient_direction * dir_y * steps_taken as f32).floor() as i64;
137
138            // If we reach the edge of the image without crossing a stroke edge,
139            // we discard the result.
140            if (cur_x < 0 || cur_x >= width as _) || (cur_y < 0 || cur_y >= height as _) {
141                return None;
142            }
143
144            // The cast is safe because we know the position lies within the image range.
145            let cur_x = cur_x as u32;
146            let cur_y = cur_y as u32;
147
148            // If the step width was too small, continue;
149            let cur_pos = Position { x: cur_x, y: cur_y };
150            if cur_pos == prev_pos {
151                continue;
152            }
153            prev_pos = cur_pos;
154
155            // The point is either on the line or the end of it, so we register it.
156            ray.push(cur_pos);
157
158            // If that pixel is not an edge, we are still on the line and
159            // need to continue scanning.
160            let edge = unsafe { edges.unsafe_get_pixel(cur_x, cur_y) }[0];
161            // TODO: Verify edge value range, should be either 0 or 255.
162            if edge < 128 {
163                continue;
164            }
165
166            // If this edge is pointed in a direction approximately opposite of the
167            // one we started in, it is approximately parallel. This means we
168            // just found the other side of the stroke.
169            // The original paper suggests the gradients need to be opposite +/- PI/6.
170            // Since the dot product is the cosine of the enclosed angle and
171            // cos(pi/6) = 0.8660254037844387, we can discard all values that exceed
172            // this threshold.
173            // TODO: arctan + cos and sin.
174            let cur_dir_x = unsafe { directions.x.unsafe_get_pixel(cur_x, cur_y) }[0];
175            let cur_dir_y = unsafe { directions.y.unsafe_get_pixel(cur_x, cur_y) }[0];
176            let dot_product = dir_x * cur_dir_x + dir_y * cur_dir_y;
177            if dot_product >= -0.866 {
178                return None;
179            }
180
181            // Paint each of the pixels on the ray with their determined stroke width.
182            let delta_x = cur_pos.x as i64 - pos.x as i64;
183            let delta_y = cur_pos.y as i64 - pos.y as i64;
184            let stroke_width = ((delta_x * delta_x + delta_y * delta_y) as f32)
185                .sqrt()
186                .floor() as u32;
187
188            for p in ray.iter() {
189                unsafe {
190                    swt.unsafe_put_pixel(p.x, p.y, [stroke_width].into());
191                }
192            }
193
194            return Some(ray);
195        }
196    }
197
198    /// Doubles the size of the image.
199    /// This is a workaround for the fact that we don't have control over the Gaussian filter
200    /// kernel size in `edges::canny`. Because we do know that blurring is applied, we
201    /// apply simple filtering only when up-sampling.
202    fn double_the_size(img: GrayImage) -> GrayImage {
203        let (width, height) = img.dimensions();
204        resize(&img, width * 2, height * 2, FilterType::Triangle)
205    }
206
207    /// Opposite of `double_the_size`
208    #[allow(unused)]
209    fn halve_the_size<I>(img: Image<I>) -> Image<I>
210    where
211        I: Pixel + 'static,
212    {
213        let (width, height) = img.dimensions();
214        resize(&img, width / 2, height / 2, FilterType::Gaussian)
215    }
216
217    /// Detects edges.
218    fn get_edges(&self, img: &GrayImage) -> GrayImage {
219        edges::canny(img, self.canny_low, self.canny_high)
220    }
221
222    /// Detects image gradients.
223    fn get_gradient_directions(&self, img: &GrayImage) -> Directions {
224        let grad_x = gradients::horizontal_scharr(img);
225        let grad_y = gradients::vertical_scharr(img);
226
227        let (width, height) = img.dimensions();
228        debug_assert_eq!(width, grad_x.dimensions().0);
229        debug_assert_eq!(height, grad_x.dimensions().1);
230
231        let mut out_x: Image<Luma<f32>> = ImageBuffer::new(width, height);
232        let mut out_y: Image<Luma<f32>> = ImageBuffer::new(width, height);
233
234        for y in 0..height {
235            for x in 0..width {
236                let gx = unsafe { grad_x.unsafe_get_pixel(x, y) };
237                let gy = unsafe { grad_y.unsafe_get_pixel(x, y) };
238
239                let gx = gx[0].to_f32().unwrap();
240                let gy = gy[0].to_f32().unwrap();
241
242                let inv_norm = 1. / (gx * gx + gy * gy).sqrt();
243                let gx = gx * inv_norm;
244                let gy = gy * inv_norm;
245
246                unsafe {
247                    out_x.unsafe_put_pixel(x, y, [gx].into());
248                    out_y.unsafe_put_pixel(x, y, [gy].into());
249                }
250            }
251        }
252
253        Directions { x: out_x, y: out_y }
254    }
255
256    /// Implements Gleam grayscale conversion from
257    /// Kanan & Cottrell 2012: "Color-to-Grayscale: Does the Method Matter in Image Recognition?"
258    /// http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0029740
259    fn gleam(&self, image: &RgbImage) -> GrayImage {
260        let (width, height) = image.dimensions();
261        let mut out: ImageBuffer<Luma<u8>, Vec<u8>> = ImageBuffer::new(width, height);
262
263        for y in 0..height {
264            for x in 0..width {
265                let rgb = unsafe { image.unsafe_get_pixel(x, y) };
266
267                let r = self.gamma(u8_to_f32(rgb[0]));
268                let g = self.gamma(u8_to_f32(rgb[1]));
269                let b = self.gamma(u8_to_f32(rgb[2]));
270                let l = mean(r, g, b);
271                let p = f32_to_u8(l);
272
273                unsafe { out.unsafe_put_pixel(x, y, [p].into()) }
274            }
275        }
276
277        out
278    }
279
280    /// Applies a gamma transformation to the input.
281    #[inline]
282    fn gamma(&self, x: f32) -> f32 {
283        x.pow(self.one_over_gamma)
284    }
285}
286
287#[inline]
288fn u8_to_f32(x: u8) -> f32 {
289    const SCALE_U8_TO_F32: f32 = 1.0 / 255.0;
290    x.to_f32().unwrap() * SCALE_U8_TO_F32
291}
292
293#[inline]
294fn f32_to_u8(x: f32) -> u8 {
295    const SCALE_F32_TO_U8: f32 = 255.0;
296    NumCast::from((x * SCALE_F32_TO_U8).clamp(0.0, 255.0)).unwrap()
297}
298
299#[inline]
300fn mean(r: f32, g: f32, b: f32) -> f32 {
301    const ONE_THIRD: f32 = 1.0 / 3.0;
302    (r + g + b) * ONE_THIRD
303}
304
305/// Helper function to map u32 value range to u8 value range.
306fn convert_u32_to_u8_img(image: Image<Luma<u32>>) -> GrayImage {
307    let (width, height) = image.dimensions();
308    let mut out: GrayImage = ImageBuffer::new(width, height);
309
310    let max_value = image.pixels().fold(0u32, |max, px| max.max(px[0]));
311    let scaler = if max_value > 0 {
312        255. / (max_value as f32)
313    } else {
314        0.
315    };
316
317    for y in 0..height {
318        for x in 0..width {
319            let pixel = unsafe { image.unsafe_get_pixel(x, y) };
320            let v = pixel[0].to_f32().unwrap();
321            let scaled = (v * scaler).to_u8().unwrap();
322            unsafe { out.unsafe_put_pixel(x, y, [scaled].into()) }
323        }
324    }
325
326    out
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn u8_to_f32_works() {
335        assert_eq!(u8_to_f32(0), 0.);
336        assert_eq!(u8_to_f32(255), 1.);
337    }
338
339    #[test]
340    fn f32_to_u8_works() {
341        assert_eq!(f32_to_u8(1.0), 255);
342        assert_eq!(f32_to_u8(2.0), 255);
343        assert_eq!(f32_to_u8(-1.0), 0);
344    }
345
346    #[test]
347    fn gamma_works() {
348        let swt = StrokeWidthTransform {
349            one_over_gamma: 2.,
350            ..StrokeWidthTransform::default()
351        };
352        assert_eq!(swt.gamma(1.0), 1.0);
353        assert_eq!(swt.gamma(2.0), 4.0);
354    }
355
356    #[test]
357    fn mean_works() {
358        assert_eq!(mean(-1., 0., 1.), 0.);
359        assert_eq!(mean(1., 2., 3.), 2.);
360        assert_eq!(mean(0., 0., 1.), 1. / 3.);
361    }
362}