Skip to main content

libeffectengine/effects/
kuwahara.rs

1use 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/// Applies an implementation of the Kuwahara filter to the given image.
11///
12/// The Kuwahara filter is usually used for noise reduction while retaining image
13/// quality, but it can be used for artistic purposes because it makes image look
14/// "painted". It does this by grabbing small squares from an image, dividing it
15/// up into four quadrants, then computing the standard brightness deviation for all
16/// pixels inside each quadrant. Whichever quadrant has the lowest deviation then
17/// gets used in the next step, where the average color from said quadrant is
18/// computed and then applied to the current pixel, which is the center where all
19/// four quadrants overlap.
20#[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 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        eprintln!("Window needs to have an odd width.");
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            // Offset from the center pixel to the edge of the window
64            let offset = window_size / 2;
65
66            // Quadrant A: Top-Left
67            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            // Quadrant B: Top-Right
73            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            // Quadrant C: Bottom-Left
79            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            // Quadrant D: Bottom-Right
85            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
135/// Calculates the standard deviation for pixels from an image
136/// within a given square.
137fn 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
155/// Calculates the average RGB color from the given colors.
156fn 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
174/// Calculates the variance for a vector of numbers.
175fn 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
189/// Calculates the standard deviation for a vector of numbers.
190fn calculate_std_deviation(data: &Vec<u32>) -> u32 {
191    let variance = calculate_variance(data);
192    variance.isqrt()
193}
194
195/// Prints the help text for this effect.
196#[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}