libeffectengine/effects/
kuwahara.rs1use std::{io::Cursor, ops::Range, process::exit};
2use wasm_bindgen::prelude::*;
3
4use image::{DynamicImage, GenericImageView, ImageBuffer, ImageFormat, Luma, Rgba};
5
6#[cfg(not(target_arch = "wasm32"))]
7use crate::util::subcommand_help_requested;
8use crate::util::{get_paths, read_image};
9
10#[wasm_bindgen(js_name = kuwahara)]
21pub fn effect() -> Vec<u8> {
22 #[cfg(not(target_arch = "wasm32"))]
23 {
24 if subcommand_help_requested() {
25 print_help();
26 exit(0);
27 }
28 }
29
30 let paths = get_paths();
31 let image_data = read_image(paths.input_path);
32
33 let image = DynamicImage::ImageRgba8(
34 image::load_from_memory(&image_data.data)
35 .expect("Failed to decode image from memory")
36 .to_rgba8(),
37 );
38 let luma8_image = image.to_luma8();
39
40 let mut new_image = ImageBuffer::new(image.width(), image.height());
41
42 let mut window_size: i32 = std::env::args()
43 .nth(4)
44 .or_else(|| Some(String::from("5")))
45 .unwrap()
46 .parse()
47 .unwrap_or_else(|_| 5);
48
49 if window_size < 5 {
50 eprintln!("Window needs to be at least 5 pixels wide.");
51 exit(64);
52 }
53
54 if window_size % 2 == 0 {
55 window_size = window_size + 1;
56 }
57
58 let image_width = image.width() as i32;
59 let image_height = image.height() as i32;
60
61 for x in 0..image_width {
62 for y in 0..image_height {
63 let offset = window_size / 2;
65
66 let q_a = (
68 (x - offset).max(0)..(x + 1).min(image_width),
69 (y - offset).max(0)..(y + 1).min(image_height),
70 );
71
72 let q_b = (
74 x.max(0)..(x + offset + 1).min(image_width),
75 (y - offset).max(0)..(y + 1).min(image_height),
76 );
77
78 let q_c = (
80 (x - offset).max(0)..(x + 1).min(image_width),
81 y.max(0)..(y + offset + 1).min(image_height),
82 );
83
84 let q_d = (
86 x.max(0)..(x + offset + 1).min(image_width),
87 y.max(0)..(y + offset + 1).min(image_height),
88 );
89
90 let std_a = get_std_brightness_deviation_for_pixels(&luma8_image, &q_a);
91 let std_b = get_std_brightness_deviation_for_pixels(&luma8_image, &q_b);
92 let std_c = get_std_brightness_deviation_for_pixels(&luma8_image, &q_c);
93 let std_d = get_std_brightness_deviation_for_pixels(&luma8_image, &q_d);
94
95 let min_std = match [(q_a, std_a), (q_b, std_b), (q_c, std_c), (q_d, std_d)]
96 .iter()
97 .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
98 {
99 Some(x) => x.clone(),
100 None => {
101 eprintln!("???");
102 exit(1);
103 }
104 };
105
106 let mut pixels: Vec<[u8; 4]> = Vec::new();
107 for z in min_std.0.0 {
108 for w in min_std.0.1.clone() {
109 pixels.push(image.get_pixel(z as u32, w as u32).0);
110 }
111 }
112
113 let avg = rgba_average(pixels);
114
115 new_image.put_pixel(x as u32, y as u32, Rgba(avg));
116 }
117 }
118
119 let mut cursor = Cursor::new(Vec::new());
120
121 if image_data.format == ImageFormat::Jpeg {
122 let rgb_image = DynamicImage::ImageRgba8(new_image).into_rgb8();
123 rgb_image
124 .write_to(&mut cursor, image_data.format)
125 .expect("Failed to encode JPEG");
126 } else {
127 new_image
128 .write_to(&mut cursor, image_data.format)
129 .expect("Failed to encode image");
130 }
131
132 return cursor.into_inner();
133}
134
135fn get_std_brightness_deviation_for_pixels(
138 image: &ImageBuffer<Luma<u8>, Vec<u8>>,
139 ranges: &(Range<i32>, Range<i32>),
140) -> u32 {
141 let mut brightnesses: Vec<u32> = Vec::new();
142
143 let x_range = ranges.0.clone();
144 let y_range = ranges.1.clone();
145
146 for x in x_range {
147 for y in y_range.clone() {
148 brightnesses.push(image.get_pixel(x as u32, y as u32).0[0] as u32);
149 }
150 }
151
152 calculate_std_deviation(&brightnesses)
153}
154
155fn rgba_average(colors: Vec<[u8; 4]>) -> [u8; 4] {
157 let folded: [u32; 4] = colors.iter().fold([0, 0, 0, 0], |mut acc, color| {
158 acc[0] += color[0] as u32;
159 acc[1] += color[1] as u32;
160 acc[2] += color[2] as u32;
161 acc[3] += color[3] as u32;
162
163 acc
164 });
165
166 return [
167 (folded[0] / colors.len() as u32) as u8,
168 (folded[1] / colors.len() as u32) as u8,
169 (folded[2] / colors.len() as u32) as u8,
170 (folded[3] / colors.len() as u32) as u8,
171 ];
172}
173
174fn calculate_variance(data: &Vec<u32>) -> u32 {
176 let mean: u32 = data.iter().sum::<u32>() / data.len() as u32;
177 let variance = data
178 .iter()
179 .map(|val| {
180 let diff = mean as i32 - (*val as i32);
181 (diff * diff) as u32
182 })
183 .sum::<u32>()
184 / data.len() as u32;
185
186 variance
187}
188
189fn calculate_std_deviation(data: &Vec<u32>) -> u32 {
191 let variance = calculate_variance(data);
192 variance.isqrt()
193}
194
195#[cfg(not(target_arch = "wasm32"))]
197fn print_help() {
198 println!(
199 r#"
200Kuwahara Filter Effect
201Applies a filter usually used for noise reduction which makes images look
202like they were painted.
203
204USAGE:
205 effectengine-cli kuwahara <INPUT_PATH> <OUTPUT_PATH> [WINDOW_SIZE]
206
207ARGUMENTS:
208 <INPUT_PATH> The path to an input image that should be processed.
209 <OUTPUT_PATH> The path where the resulting image should be saved.
210 Needs to include the filename.
211 [WINDOW_SIZE] Optional. How big the various "paint strokes" should
212 appear. Bigger numbers will mean bigger strokes and
213 less detail. (Default: 5)
214 "#
215 );
216}