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 pub fn apply(&self, img: &RgbImage) -> GrayImage {
51 let gray = self.gleam(img);
52
53 let gray = Self::double_the_size(gray);
55 let edges = self.get_edges(&gray);
56 let directions = self.get_gradient_directions(&gray);
57
58 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 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 convert_u32_to_u8_img(swt)
90 }
91
92 fn process_pixel(
94 &self,
95 pos: Position,
96 edges: &GrayImage,
97 directions: &Directions,
98 swt: &mut Image<Luma<u32>>,
99 ) -> Option<Ray> {
100 let (width, height) = edges.dimensions();
102
103 let gradient_direction: f32 = if self.dark_on_bright { -1. } else { 1. };
108
109 let mut ray = Vec::new();
113 ray.push(pos);
114
115 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 debug_assert!(!dir_x.is_nan());
125 debug_assert!(!dir_y.is_nan());
126
127 let mut prev_pos = Position { x: 0, y: 0 };
129 let mut steps_taken: usize = 0;
130 loop {
131 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 (cur_x < 0 || cur_x >= width as _) || (cur_y < 0 || cur_y >= height as _) {
141 return None;
142 }
143
144 let cur_x = cur_x as u32;
146 let cur_y = cur_y as u32;
147
148 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 ray.push(cur_pos);
157
158 let edge = unsafe { edges.unsafe_get_pixel(cur_x, cur_y) }[0];
161 if edge < 128 {
163 continue;
164 }
165
166 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 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 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 #[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 fn get_edges(&self, img: &GrayImage) -> GrayImage {
219 edges::canny(img, self.canny_low, self.canny_high)
220 }
221
222 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 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 #[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
305fn 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}