diff --git a/Cargo.lock b/Cargo.lock index 585ff47..6a5aaec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -305,6 +305,18 @@ dependencies = [ "system-deps", ] +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + [[package]] name = "gif" version = "0.13.1" @@ -429,6 +441,7 @@ version = "0.1.0" dependencies = [ "gtk4", "image", + "rand", "rayon", ] @@ -675,6 +688,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro-crate" version = "3.3.0" @@ -711,6 +733,42 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha", + "rand_core", + "zerocopy", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + [[package]] name = "rayon" version = "1.10.0" @@ -883,6 +941,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "weezl" version = "0.1.8" @@ -971,6 +1038,35 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zune-inflate" version = "0.2.54" diff --git a/Cargo.toml b/Cargo.toml index a635885..ab5da00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" gtk = { package = "gtk4", version = "0.9.6", features=["v4_10"] } image = "0.24" rayon = "1.8" +rand = "0.9.0" # gtk4 = "0.9.6" # show-image = "0.14.0" # image2 = "1.9.2" diff --git a/src/filters.rs b/src/filters.rs new file mode 100644 index 0000000..7570419 --- /dev/null +++ b/src/filters.rs @@ -0,0 +1,413 @@ +use image::{ + Rgb, RgbImage +}; +use rand; +use std::f64::consts::PI; + +pub fn heatmap(image: &mut RgbImage) { + for pixel in image.pixels_mut() { + let intensity = (pixel[0] as u32 + pixel[1] as u32 + pixel[2] as u32) / 3; + + if intensity < 64 { + pixel[0] = 0; + pixel[1] = 0; + pixel[2] = (intensity * 4) as u8; + } else if intensity < 128 { + pixel[0] = 0; + pixel[1] = ((intensity - 64) * 4) as u8; + pixel[2] = 255 - ((intensity - 64) * 4) as u8; + } else if intensity < 192 { + pixel[0] = ((intensity - 128) * 4) as u8; + pixel[1] = 255; + pixel[2] = 0; + } else { + pixel[0] = 255; + pixel[1] = 255 - ((intensity - 192) * 4) as u8; + pixel[2] = 0; + } + } +} + +pub fn pixelate(image: &mut RgbImage, block_size: u32) { + let (width, height) = image.dimensions(); + let mut buffer = image.clone(); + + for y in (0..height).step_by(block_size as usize) { + for x in (0..width).step_by(block_size as usize) { + let mut r = 0; + let mut g = 0; + let mut b = 0; + let mut count = 0; + + for dy in 0..block_size.min(height-y) { + for dx in 0..block_size.min(width-x) { + let px = image.get_pixel(x + dx, y + dy); + r += px[0] as u32; + g += px[1] as u32; + b += px[2] as u32; + count += 1; + } + } + + let avg_r = (r / count) as u8; + let avg_g = (g / count) as u8; + let avg_b = (b / count) as u8; + + for dy in 0..block_size.min(height-y) { + for dx in 0..block_size.min(width-x) { + buffer.put_pixel(x + dx, y + dy, Rgb([avg_r, avg_g, avg_b])); + } + } + } + } + + *image = buffer; +} + +pub fn retro_effect(image: &mut RgbImage) { + let (width, height) = image.dimensions(); + let center_x = width as f32 / 2.0; + let center_y = height as f32 / 2.0; + let max_dist = (center_x.powi(2) + center_y.powi(2)).sqrt(); + + for y in 0..height { + for x in 0..width { + let mut pixel = *image.get_pixel(x, y); + let dx = x as f32 - center_x; + let dy = y as f32 - center_y; + let dist = (dx.powi(2) + dy.powi(2)).sqrt(); + let vignette = 1.0 - (dist / max_dist).powi(2) * 0.7; + + pixel[0] = (pixel[0] as f32 * vignette).clamp(0.0, 255.0) as u8; + pixel[1] = (pixel[1] as f32 * vignette).clamp(0.0, 255.0) as u8; + pixel[2] = (pixel[2] as f32 * vignette).clamp(0.0, 255.0) as u8; + + let noise = (rand::random::() * 20.0) - 10.0; + pixel[0] = (pixel[0] as f32 + noise).clamp(0.0, 255.0) as u8; + pixel[1] = (pixel[1] as f32 + noise).clamp(0.0, 255.0) as u8; + pixel[2] = (pixel[2] as f32 + noise).clamp(0.0, 255.0) as u8; + + image.put_pixel(x, y, pixel); + } + } +} + +pub fn oil_painting(image: &mut RgbImage) { + let (width, height) = image.dimensions(); + let mut buffer = image.clone(); + let radius = 3i32; + let intensity_levels = 8; + + let width_i32 = width as i32; + let height_i32 = height as i32; + + for y in radius..height_i32 - radius { + for x in radius..width_i32 - radius { + let mut intensity_count = vec![0u32; intensity_levels]; + let mut avg_r = vec![0u32; intensity_levels]; + let mut avg_g = vec![0u32; intensity_levels]; + let mut avg_b = vec![0u32; intensity_levels]; + + for dy in -radius..=radius { + for dx in -radius..=radius { + let px_x = (x + dx).clamp(0, width_i32 - 1) as u32; + let px_y = (y + dy).clamp(0, height_i32 - 1) as u32; + + let px = image.get_pixel(px_x, px_y); + let intensity = ((px[0] as u32 + px[1] as u32 + px[2] as u32) / 3) as usize; + let level = (intensity * intensity_levels) / 256; + + let level = level.min(intensity_levels - 1); + + intensity_count[level] += 1; + avg_r[level] += px[0] as u32; + avg_g[level] += px[1] as u32; + avg_b[level] += px[2] as u32; + } + } + + let max_level = intensity_count + .iter() + .enumerate() + .max_by_key(|&(_, count)| count) + .map(|(level, _)| level) + .unwrap_or(0); + + let count = intensity_count[max_level].max(1); + let result_pixel = Rgb([ + (avg_r[max_level] / count) as u8, + (avg_g[max_level] / count) as u8, + (avg_b[max_level] / count) as u8 + ]); + + buffer.put_pixel(x as u32, y as u32, result_pixel); + } + } + + *image = buffer; +} + +pub fn watercolor(image: &mut RgbImage) { + let (width, height) = image.dimensions(); + let mut buffer = image.clone(); + let radius = 2i32; + + for y in (radius as u32)..height-(radius as u32) { + for x in (radius as u32)..width-(radius as u32) { + let mut r = 0u32; + let mut g = 0u32; + let mut b = 0u32; + let mut count = 0u32; + + for dy in -radius..=radius { + for dx in -radius..=radius { + let px_x = x.wrapping_add_signed(dx); + let px_y = y.wrapping_add_signed(dy); + + let px = image.get_pixel(px_x, px_y); + r += px[0] as u32; + g += px[1] as u32; + b += px[2] as u32; + count += 1; + } + } + + buffer.put_pixel(x, y, Rgb([ + (r / count) as u8, + (g / count) as u8, + (b / count) as u8 + ])); + } + } + + *image = buffer; +} + +pub fn sharpen(image: &mut RgbImage) { + let (width, height) = image.dimensions(); + let mut buffer = image.clone(); + let kernel = [ + [ 0.0, -1.0, 0.0], + [-1.0, 5.0, -1.0], + [ 0.0, -1.0, 0.0] + ]; + + for y in 1..height-1 { + for x in 1..width-1 { + let mut r: f32 = 0.0; + let mut g: f32 = 0.0; + let mut b: f32 = 0.0; + + for ky in 0..3 { + for kx in 0..3 { + let px = image.get_pixel(x + kx - 1, y + ky - 1); + let weight = kernel[ky as usize][kx as usize]; + r += px[0] as f32 * weight; + g += px[1] as f32 * weight; + b += px[2] as f32 * weight; + } + } + + buffer.put_pixel(x, y, Rgb([ + r.clamp(0.0, 255.0) as u8, + g.clamp(0.0, 255.0) as u8, + b.clamp(0.0, 255.0) as u8 + ])); + } + } + + *image = buffer; +} + +pub fn sepia(image: &mut RgbImage) { + for pixel in image.pixels_mut() { + let r = pixel[0] as f32 * 0.393 + pixel[1] as f32 * 0.769 + pixel[2] as f32 * 0.189; + let g = pixel[0] as f32 * 0.349 + pixel[1] as f32 * 0.686 + pixel[2] as f32 * 0.168; + let b = pixel[0] as f32 * 0.272 + pixel[1] as f32 * 0.534 + pixel[2] as f32 * 0.131; + + pixel[0] = r.clamp(0.0, 255.0) as u8; + pixel[1] = g.clamp(0.0, 255.0) as u8; + pixel[2] = b.clamp(0.0, 255.0) as u8; + } +} + +pub fn invert(image: &mut RgbImage) { + for pixel in image.pixels_mut() { + pixel[0] = 255 - pixel[0]; + pixel[1] = 255 - pixel[1]; + pixel[2] = 255 - pixel[2]; + } +} + +pub fn gray_world(image: &mut RgbImage) { + let (width, height) = image.dimensions(); + let total_pixels = (width * height) as f32; + + let mut sum_r = 0.0; + let mut sum_g = 0.0; + let mut sum_b = 0.0; + + for pixel in image.pixels() { + sum_r += pixel[0] as f32; + sum_g += pixel[1] as f32; + sum_b += pixel[2] as f32; + } + + let avg_r = sum_r / total_pixels; + let avg_g = sum_g / total_pixels; + let avg_b = sum_b / total_pixels; + + let avg = (avg_r + avg_g + avg_b) / 3.0; + + let kr = avg / avg_r; + let kg = avg / avg_g; + let kb = avg / avg_b; + + for pixel in image.pixels_mut() { + pixel[0] = (pixel[0] as f32 * kr).min(255.0).max(0.0) as u8; + pixel[1] = (pixel[1] as f32 * kg).min(255.0).max(0.0) as u8; + pixel[2] = (pixel[2] as f32 * kb).min(255.0).max(0.0) as u8; + } +} + +pub fn histogram_stretch(image: &mut RgbImage) { + let (width, height) = image.dimensions(); + let total_pixels = width * height; + + if total_pixels == 0 { + return; + } + + let (mut min_r, mut max_r) = (255u8, 0u8); + let (mut min_g, mut max_g) = (255u8, 0u8); + let (mut min_b, mut max_b) = (255u8, 0u8); + + for pixel in image.pixels() { + min_r = min_r.min(pixel[0]); + max_r = max_r.max(pixel[0]); + min_g = min_g.min(pixel[1]); + max_g = max_g.max(pixel[1]); + min_b = min_b.min(pixel[2]); + max_b = max_b.max(pixel[2]); + } + + if min_r == 0 && max_r == 255 && + min_g == 0 && max_g == 255 && + min_b == 0 && max_b == 255 { + return; + } + + for y in 0..height { + for x in 0..width { + let mut pixel = *image.get_pixel(x, y); + + if max_r != min_r { + pixel[0] = ((pixel[0] - min_r) as f32 * 255.0 / (max_r - min_r) as f32).clamp(0.0, 255.0) as u8; + } + if max_g != min_g { + pixel[1] = ((pixel[1] - min_g) as f32 * 255.0 / (max_g - min_g) as f32).clamp(0.0, 255.0) as u8; + } + if max_b != min_b { + pixel[2] = ((pixel[2] - min_b) as f32 * 255.0 / (max_b - min_b) as f32).clamp(0.0, 255.0) as u8; + } + + image.put_pixel(x, y, pixel); + } + } +} + +pub fn emboss(image: &mut RgbImage) { + let kernel = [ + [-2.0, -1.0, 0.0], + [-1.0, 1.0, 1.0], + [ 0.0, 1.0, 2.0] + ]; + + let (width, height) = image.dimensions(); + let mut new_image = image.clone(); + + for y in 1..height-1 { + for x in 1..width-1 { + let mut r: f32 = 0.0; + let mut g: f32 = 0.0; + let mut b: f32 = 0.0; + + for ky in 0..3 { + for kx in 0..3 { + let px = image.get_pixel(x + kx - 1, y + ky - 1); + let weight = kernel[ky as usize][kx as usize]; + r += px[0] as f32 * weight; + g += px[1] as f32 * weight; + b += px[2] as f32 * weight; + } + } + + r = (r + 128.0).clamp(0.0, 255.0); + g = (g + 128.0).clamp(0.0, 255.0); + b = (b + 128.0).clamp(0.0, 255.0); + + new_image.put_pixel(x, y, Rgb([r as u8, g as u8, b as u8])); + } + } + + *image = new_image; +} + +pub fn blur(image: &mut RgbImage) { + let (width, height) = image.dimensions(); + let mut buffer = image.clone(); + + let kernel = [ + [1.0, 4.0, 6.0, 4.0, 1.0], + [4.0, 16.0, 24.0, 16.0, 4.0], + [6.0, 24.0, 36.0, 24.0, 6.0], + [4.0, 16.0, 24.0, 16.0, 4.0], + [1.0, 4.0, 6.0, 4.0, 1.0] + ]; + let kernel_sum: f32 = 256.0; + + for y in 2..height-2 { + for x in 2..width-2 { + let mut r = 0.0; + let mut g = 0.0; + let mut b = 0.0; + + for ky in 0..5 { + for kx in 0..5 { + let px = image.get_pixel(x + kx - 2, y + ky - 2); + let weight = kernel[ky as usize][kx as usize]; + r += px[0] as f32 * weight; + g += px[1] as f32 * weight; + b += px[2] as f32 * weight; + } + } + + let r_val = (r / kernel_sum).clamp(0.0, 255.0) as u8; + let g_val = (g / kernel_sum).clamp(0.0, 255.0) as u8; + let b_val = (b / kernel_sum).clamp(0.0, 255.0) as u8; + + buffer.put_pixel(x, y, Rgb([r_val, g_val, b_val])); + } + } + + *image = buffer; +} + +pub fn shift(image: &mut RgbImage, dx: i32, dy: i32) { + let (width, height) = image.dimensions(); + let mut buffer = RgbImage::new(width, height); + + for y in 0..height { + for x in 0..width { + let new_x = x as i32 - dx; + let new_y = y as i32 - dy; + + if new_x >= 0 && new_x < width as i32 && new_y >= 0 && new_y < height as i32 { + let pixel = *image.get_pixel(new_x as u32, new_y as u32); + buffer.put_pixel(x, y, pixel); + } + } + } + + *image = buffer; +} diff --git a/src/main.rs b/src/main.rs index 5cfe6b2..0876ab7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,8 @@ use std::path::{Path, PathBuf}; use std::rc::Rc; use std::cell::RefCell; +mod filters; + fn main() { let app = Application::builder() .application_id("ru.risdeveau.imagefilters") @@ -40,6 +42,15 @@ fn build_ui(app: &Application) { filter_combo.append_text("Histogram Stretch"); filter_combo.append_text("Emboss"); filter_combo.append_text("Blur"); + filter_combo.append_text("Negative"); + filter_combo.append_text("Heatmap"); + filter_combo.append_text("Pixelate"); + filter_combo.append_text("Retro"); + filter_combo.append_text("Oil"); + filter_combo.append_text("Water"); + filter_combo.append_text("Sharpen"); + filter_combo.append_text("Sepia"); + filter_combo.append_text("Shift"); filter_combo.set_active(Some(0)); let image_data = Rc::new(RefCell::new(None::<(PathBuf, RgbImage)>)); @@ -55,170 +66,25 @@ fn build_ui(app: &Application) { } } - fn gray_world(image: &mut RgbImage) { - let (width, height) = image.dimensions(); - let total_pixels = (width * height) as f32; - - let mut sum_r = 0.0; - let mut sum_g = 0.0; - let mut sum_b = 0.0; - - for pixel in image.pixels() { - sum_r += pixel[0] as f32; - sum_g += pixel[1] as f32; - sum_b += pixel[2] as f32; - } - - let avg_r = sum_r / total_pixels; - let avg_g = sum_g / total_pixels; - let avg_b = sum_b / total_pixels; - - let avg = (avg_r + avg_g + avg_b) / 3.0; - - let kr = avg / avg_r; - let kg = avg / avg_g; - let kb = avg / avg_b; - - for pixel in image.pixels_mut() { - pixel[0] = (pixel[0] as f32 * kr).min(255.0).max(0.0) as u8; - pixel[1] = (pixel[1] as f32 * kg).min(255.0).max(0.0) as u8; - pixel[2] = (pixel[2] as f32 * kb).min(255.0).max(0.0) as u8; - } - } - - fn histogram_stretch(image: &mut RgbImage) { - let (width, height) = image.dimensions(); - let total_pixels = width * height; - - if total_pixels == 0 { - return; - } - - let (mut min_r, mut max_r) = (255u8, 0u8); - let (mut min_g, mut max_g) = (255u8, 0u8); - let (mut min_b, mut max_b) = (255u8, 0u8); - - for pixel in image.pixels() { - min_r = min_r.min(pixel[0]); - max_r = max_r.max(pixel[0]); - min_g = min_g.min(pixel[1]); - max_g = max_g.max(pixel[1]); - min_b = min_b.min(pixel[2]); - max_b = max_b.max(pixel[2]); - } - - if min_r == 0 && max_r == 255 && - min_g == 0 && max_g == 255 && - min_b == 0 && max_b == 255 { - return; - } - - for y in 0..height { - for x in 0..width { - let mut pixel = *image.get_pixel(x, y); - - if max_r != min_r { - pixel[0] = ((pixel[0] - min_r) as f32 * 255.0 / (max_r - min_r) as f32).clamp(0.0, 255.0) as u8; - } - if max_g != min_g { - pixel[1] = ((pixel[1] - min_g) as f32 * 255.0 / (max_g - min_g) as f32).clamp(0.0, 255.0) as u8; - } - if max_b != min_b { - pixel[2] = ((pixel[2] - min_b) as f32 * 255.0 / (max_b - min_b) as f32).clamp(0.0, 255.0) as u8; - } - - image.put_pixel(x, y, pixel); - } - } - } - - fn emboss(image: &mut RgbImage) { - let kernel = [ - [-2.0, -1.0, 0.0], - [-1.0, 1.0, 1.0], - [ 0.0, 1.0, 2.0] - ]; - - let (width, height) = image.dimensions(); - let mut new_image = image.clone(); - - for y in 1..height-1 { - for x in 1..width-1 { - let mut r = 0.0; - let mut g = 0.0; - let mut b = 0.0; - - for ky in 0..3 { - for kx in 0..3 { - let px = image.get_pixel(x + kx - 1, y + ky - 1); - let weight = kernel[ky as usize][kx as usize]; - r += px[0] as f32 * weight; - g += px[1] as f32 * weight; - b += px[2] as f32 * weight; - } - } - - r = (r + 128.0).clamp(0.0, 255.0); - g = (g + 128.0).clamp(0.0, 255.0); - b = (b + 128.0).clamp(0.0, 255.0); - - new_image.put_pixel(x, y, Rgb([r as u8, g as u8, b as u8])); - } - } - - *image = new_image; - } - fn apply_filter(image: &mut RgbImage, filter_name: &str) { match filter_name { - "Gray World" => gray_world(image), - "Histogram Stretch" => histogram_stretch(image), - "Emboss" => emboss(image), - "Blur" => blur(image), + "Gray World" => filters::gray_world(image), + "Histogram Stretch" => filters::histogram_stretch(image), + "Emboss" => filters::emboss(image), + "Blur" => filters::blur(image), + "Negative" => filters::invert(image), + "Heatmap" => filters::heatmap(image), + "Pixelate" => filters::pixelate(image, 8), + "Retro" => filters::retro_effect(image), + "Oil" => filters::oil_painting(image), + "Water" => filters::watercolor(image), + "Sharpen" => filters::sharpen(image), + "Sepia" => filters::sepia(image), + "Shift" => filters::shift(image, 10, 25), _ => {} } } - fn blur(image: &mut RgbImage) { - let (width, height) = image.dimensions(); - let mut buffer = image.clone(); - - let kernel = [ - [1.0, 4.0, 6.0, 4.0, 1.0], - [4.0, 16.0, 24.0, 16.0, 4.0], - [6.0, 24.0, 36.0, 24.0, 6.0], - [4.0, 16.0, 24.0, 16.0, 4.0], - [1.0, 4.0, 6.0, 4.0, 1.0] - ]; - let kernel_sum: f32 = 256.0; - - for y in 2..height-2 { - for x in 2..width-2 { - let mut r = 0.0; - let mut g = 0.0; - let mut b = 0.0; - - for ky in 0..5 { - for kx in 0..5 { - let px = image.get_pixel(x + kx - 2, y + ky - 2); - let weight = kernel[ky as usize][kx as usize]; - r += px[0] as f32 * weight; - g += px[1] as f32 * weight; - b += px[2] as f32 * weight; - } - } - - let r_val = (r / kernel_sum).clamp(0.0, 255.0) as u8; - let g_val = (g / kernel_sum).clamp(0.0, 255.0) as u8; - let b_val = (b / kernel_sum).clamp(0.0, 255.0) as u8; - - buffer.put_pixel(x, y, Rgb([r_val, g_val, b_val])); - } - } - - *image = buffer; - } - { let window = window.clone(); let image_data = image_data.clone();