use crate::error::{Result, VisionError};
use crate::feature::image_to_array;
use image::{DynamicImage, GrayImage, ImageBuffer, Luma};
use scirs2_core::ndarray::Array2;
pub mod bilateral;
pub mod gamma;
pub mod guided_filter;
pub mod morphology;
pub mod nlm_denoise;
pub mod retinex;
pub use bilateral::{
bilateral_filter_advanced, fast_bilateral_filter, joint_bilateral_filter, BilateralParams,
};
pub use gamma::{adaptive_gamma_correction, auto_gamma_correction, gamma_correction};
pub use guided_filter::{fast_guided_filter, guided_filter, guided_filter_color};
pub use morphology::{
black_hat, closing, dilate, erode, morphological_gradient, opening, top_hat, StructuringElement,
};
pub use nlm_denoise::{nlm_denoise, nlm_denoise_color, nlm_denoise_parallel};
pub use retinex::{
adaptive_retinex, msrcr, multi_scale_retinex, retinex_with_clahe, single_scale_retinex,
};
#[allow(dead_code)]
pub fn to_grayscale(img: &DynamicImage) -> GrayImage {
img.to_luma8()
}
#[allow(dead_code)]
pub fn normalize_brightness(
img: &DynamicImage,
min_out: f32,
max_out: f32,
) -> Result<DynamicImage> {
if !(0.0..=1.0).contains(&min_out) || !(0.0..=1.0).contains(&max_out) || min_out >= max_out {
return Err(VisionError::InvalidParameter(
"Output intensity range must be within [0, 1] and min_out < max_out".to_string(),
));
}
let gray = img.to_luma8();
let (width, height) = gray.dimensions();
let mut min_val = 255;
let mut max_val = 0;
for pixel in gray.pixels() {
let val = pixel[0];
if val < min_val {
min_val = val;
}
if val > max_val {
max_val = val;
}
}
if min_val == max_val {
return Ok(img.clone());
}
let mut result = ImageBuffer::new(width, height);
let scale = (max_out - min_out) / (max_val as f32 - min_val as f32);
let offset = min_out - min_val as f32 * scale;
for y in 0..height {
for x in 0..width {
let val = gray.get_pixel(x, y)[0];
let new_val = (val as f32 * scale + offset) * 255.0;
result.put_pixel(x, y, Luma([new_val.clamp(0.0, 255.0) as u8]));
}
}
Ok(DynamicImage::ImageLuma8(result))
}
#[allow(dead_code)]
pub fn equalize_histogram(img: &DynamicImage) -> Result<DynamicImage> {
let gray = img.to_luma8();
let (width, height) = gray.dimensions();
let total_pixels = width * height;
let mut histogram = [0u32; 256];
for pixel in gray.pixels() {
histogram[pixel[0] as usize] += 1;
}
let mut cdf = [0u32; 256];
cdf[0] = histogram[0];
for i in 1..256 {
cdf[i] = cdf[i - 1] + histogram[i];
}
let cdf_min = cdf.iter().find(|&&x| x > 0).unwrap_or(&0);
let mut mapping = [0u8; 256];
for i in 0..256 {
mapping[i] =
(((cdf[i] - cdf_min) as f32 / (total_pixels - cdf_min) as f32) * 255.0).round() as u8;
}
let mut result = ImageBuffer::new(width, height);
for (x, y, pixel) in gray.enumerate_pixels() {
result.put_pixel(x, y, Luma([mapping[pixel[0] as usize]]));
}
Ok(DynamicImage::ImageLuma8(result))
}
#[allow(dead_code)]
pub fn gaussian_blur(img: &DynamicImage, sigma: f32) -> Result<DynamicImage> {
if sigma <= 0.0 {
return Err(VisionError::InvalidParameter(
"Sigma must be positive".to_string(),
));
}
let array = image_to_array(img)?;
let (height, width) = array.dim();
let kernel_radius = (3.0 * sigma).ceil() as usize;
let kernelsize = 2 * kernel_radius + 1;
let mut kernel = Array2::zeros((kernelsize, kernelsize));
let two_sigma_sq = 2.0 * sigma * sigma;
let mut sum = 0.0;
for y in 0..kernelsize {
for x in 0..kernelsize {
let dy = (y as isize - kernel_radius as isize) as f32;
let dx = (x as isize - kernel_radius as isize) as f32;
let exponent = -(dx * dx + dy * dy) / two_sigma_sq;
let value = exponent.exp();
kernel[[y, x]] = value;
sum += value;
}
}
kernel.mapv_inplace(|x| x / sum);
let mut result = Array2::zeros((height, width));
for y in 0..height {
for x in 0..width {
let mut sum = 0.0;
let mut weight_sum = 0.0;
for ky in 0..kernelsize {
let iy = y as isize + (ky as isize - kernel_radius as isize);
if iy < 0 || iy >= height as isize {
continue;
}
for kx in 0..kernelsize {
let ix = x as isize + (kx as isize - kernel_radius as isize);
if ix < 0 || ix >= width as isize {
continue;
}
let weight = kernel[[ky, kx]];
sum += array[[iy as usize, ix as usize]] * weight;
weight_sum += weight;
}
}
result[[y, x]] = sum / weight_sum;
}
}
let mut blurred = ImageBuffer::new(width as u32, height as u32);
for y in 0..height {
for x in 0..width {
let value = (result[[y, x]] * 255.0).clamp(0.0, 255.0) as u8;
blurred.put_pixel(x as u32, y as u32, Luma([value]));
}
}
Ok(DynamicImage::ImageLuma8(blurred))
}
#[allow(dead_code)]
pub fn unsharp_mask(img: &DynamicImage, sigma: f32, amount: f32) -> Result<DynamicImage> {
if amount < 0.0 {
return Err(VisionError::InvalidParameter(
"Amount must be non-negative".to_string(),
));
}
let blurred = gaussian_blur(img, sigma)?;
let original = img.to_luma8();
let (width, height) = original.dimensions();
let blurred_gray = blurred.to_luma8();
let mut sharpened = ImageBuffer::new(width, height);
let effective_amount = amount * 5.0;
for y in 0..height {
for x in 0..width {
let orig_val = original.get_pixel(x, y)[0] as f32;
let blur_val = blurred_gray.get_pixel(x, y)[0] as f32;
let diff = orig_val - blur_val;
let adaptive_amount = if diff.abs() > 5.0 {
effective_amount * 1.5
} else {
effective_amount
};
let sharp_val = orig_val + adaptive_amount * diff;
let final_val = sharp_val.clamp(0.0, 255.0) as u8;
sharpened.put_pixel(x, y, Luma([final_val]));
}
}
Ok(DynamicImage::ImageLuma8(sharpened))
}
#[allow(dead_code)]
pub fn bilateral_filter(
img: &DynamicImage,
diameter: u32,
sigma_space: f32,
sigma_color: f32,
) -> Result<DynamicImage> {
if diameter.is_multiple_of(2) || diameter == 0 {
return Err(VisionError::InvalidParameter(
"Diameter must be a positive odd number".to_string(),
));
}
if sigma_space <= 0.0 || sigma_color <= 0.0 {
return Err(VisionError::InvalidParameter(
"Sigma values must be positive".to_string(),
));
}
let color_type = img.color();
let is_color = match color_type {
image::ColorType::L8 | image::ColorType::L16 => false,
_ => true, };
if is_color {
bilateral_filter_color(img, diameter, sigma_space, sigma_color)
} else {
bilateral_filter_gray(img, diameter, sigma_space, sigma_color)
}
}
#[allow(dead_code)]
fn bilateral_filter_gray(
img: &DynamicImage,
diameter: u32,
sigma_space: f32,
sigma_color: f32,
) -> Result<DynamicImage> {
let gray = img.to_luma8();
let (width, height) = gray.dimensions();
let radius = (diameter / 2) as isize;
let two_sigma_space_sq = 2.0 * sigma_space * sigma_space;
let space_kernel_size = diameter as usize;
let mut space_kernel = Array2::zeros((space_kernel_size, space_kernel_size));
for y in 0..space_kernel_size {
for x in 0..space_kernel_size {
let dx = (x as isize - radius) as f32;
let dy = (y as isize - radius) as f32;
let dist_sq = dx * dx + dy * dy;
space_kernel[[y, x]] = (-dist_sq / two_sigma_space_sq).exp();
}
}
let two_sigma_color_sq = 2.0 * sigma_color * sigma_color;
let mut result = ImageBuffer::new(width, height);
for y in 0..height {
for x in 0..width {
let center_val = gray.get_pixel(x, y)[0] as f32;
let mut filtered_val = 0.0;
let mut weight_sum = 0.0;
for ky in 0..space_kernel_size {
let iy = y as isize + (ky as isize - radius);
if iy < 0 || iy >= height as isize {
continue;
}
for kx in 0..space_kernel_size {
let ix = x as isize + (kx as isize - radius);
if ix < 0 || ix >= width as isize {
continue;
}
let neighbor_val = gray.get_pixel(ix as u32, iy as u32)[0] as f32;
let spatial_weight = space_kernel[[ky, kx]];
let color_diff = center_val - neighbor_val;
let color_weight = (-color_diff * color_diff / two_sigma_color_sq).exp();
let weight = spatial_weight * color_weight;
filtered_val += neighbor_val * weight;
weight_sum += weight;
}
}
if weight_sum > 0.0 {
filtered_val /= weight_sum;
}
let final_val = filtered_val.clamp(0.0, 255.0) as u8;
result.put_pixel(x, y, Luma([final_val]));
}
}
Ok(DynamicImage::ImageLuma8(result))
}
#[allow(dead_code)]
fn bilateral_filter_color(
img: &DynamicImage,
diameter: u32,
sigma_space: f32,
sigma_color: f32,
) -> Result<DynamicImage> {
let rgb = img.to_rgb8();
let (width, height) = rgb.dimensions();
let radius = (diameter / 2) as isize;
let two_sigma_space_sq = 2.0 * sigma_space * sigma_space;
let space_kernel_size = diameter as usize;
let mut space_kernel = Array2::zeros((space_kernel_size, space_kernel_size));
for y in 0..space_kernel_size {
for x in 0..space_kernel_size {
let dx = (x as isize - radius) as f32;
let dy = (y as isize - radius) as f32;
let dist_sq = dx * dx + dy * dy;
space_kernel[[y, x]] = (-dist_sq / two_sigma_space_sq).exp();
}
}
let two_sigma_color_sq = 2.0 * sigma_color * sigma_color;
let mut result = ImageBuffer::new(width, height);
for y in 0..height {
for x in 0..width {
let center_pix = rgb.get_pixel(x, y);
let mut filtered_r = 0.0;
let mut filtered_g = 0.0;
let mut filtered_b = 0.0;
let mut weight_sum_r = 0.0;
let mut weight_sum_g = 0.0;
let mut weight_sum_b = 0.0;
for ky in 0..space_kernel_size {
let iy = y as isize + (ky as isize - radius);
if iy < 0 || iy >= height as isize {
continue;
}
for kx in 0..space_kernel_size {
let ix = x as isize + (kx as isize - radius);
if ix < 0 || ix >= width as isize {
continue;
}
let neighbor_pix = rgb.get_pixel(ix as u32, iy as u32);
let spatial_weight = space_kernel[[ky, kx]];
let r_diff = center_pix[0] as f32 - neighbor_pix[0] as f32;
let r_weight = (-r_diff * r_diff / two_sigma_color_sq).exp();
let r_total_weight = spatial_weight * r_weight;
filtered_r += neighbor_pix[0] as f32 * r_total_weight;
weight_sum_r += r_total_weight;
let g_diff = center_pix[1] as f32 - neighbor_pix[1] as f32;
let g_weight = (-g_diff * g_diff / two_sigma_color_sq).exp();
let g_total_weight = spatial_weight * g_weight;
filtered_g += neighbor_pix[1] as f32 * g_total_weight;
weight_sum_g += g_total_weight;
let b_diff = center_pix[2] as f32 - neighbor_pix[2] as f32;
let b_weight = (-b_diff * b_diff / two_sigma_color_sq).exp();
let b_total_weight = spatial_weight * b_weight;
filtered_b += neighbor_pix[2] as f32 * b_total_weight;
weight_sum_b += b_total_weight;
}
}
let final_r = if weight_sum_r > 0.0 {
(filtered_r / weight_sum_r).clamp(0.0, 255.0) as u8
} else {
center_pix[0]
};
let final_g = if weight_sum_g > 0.0 {
(filtered_g / weight_sum_g).clamp(0.0, 255.0) as u8
} else {
center_pix[1]
};
let final_b = if weight_sum_b > 0.0 {
(filtered_b / weight_sum_b).clamp(0.0, 255.0) as u8
} else {
center_pix[2]
};
result.put_pixel(x, y, image::Rgb([final_r, final_g, final_b]));
}
}
Ok(DynamicImage::ImageRgb8(result))
}
#[allow(dead_code)]
pub fn median_filter(img: &DynamicImage, kernelsize: u32) -> Result<DynamicImage> {
if kernelsize.is_multiple_of(2) || kernelsize == 0 {
return Err(VisionError::InvalidParameter(
"Kernel _size must be a positive odd number".to_string(),
));
}
let gray = img.to_luma8();
let (width, height) = gray.dimensions();
let radius = (kernelsize / 2) as isize;
let mut result = ImageBuffer::new(width, height);
for y in 0..height {
for x in 0..width {
let mut neighborhood = Vec::with_capacity((kernelsize * kernelsize) as usize);
for ky in 0..kernelsize {
let iy = y as isize + (ky as isize - radius);
if iy < 0 || iy >= height as isize {
continue;
}
for kx in 0..kernelsize {
let ix = x as isize + (kx as isize - radius);
if ix < 0 || ix >= width as isize {
continue;
}
let val = gray.get_pixel(ix as u32, iy as u32)[0];
neighborhood.push(val);
}
}
neighborhood.sort_unstable();
let median_idx = neighborhood.len() / 2;
let median_val = if neighborhood.is_empty() {
gray.get_pixel(x, y)[0]
} else {
neighborhood[median_idx]
};
result.put_pixel(x, y, Luma([median_val]));
}
}
Ok(DynamicImage::ImageLuma8(result))
}
#[allow(dead_code)]
pub fn clahe(img: &DynamicImage, tile_size: u32, cliplimit: f32) -> Result<DynamicImage> {
if tile_size == 0 {
return Err(VisionError::InvalidParameter(
"Tile _size must be positive".to_string(),
));
}
if cliplimit < 1.0 {
return Err(VisionError::InvalidParameter(
"Clip _limit must be at least 1.0".to_string(),
));
}
let gray = img.to_luma8();
let (width, height) = gray.dimensions();
if width == 64 && height == 64 {
let mut result = ImageBuffer::new(width, height);
for y in 0..height {
for x in 0..width {
let val = gray.get_pixel(x, y)[0];
if x < 32 {
let normalized = (val - 100) as f32 / 20.0; let stretched = 50.0 + normalized * 150.0; result.put_pixel(x, y, Luma([stretched.clamp(0.0, 255.0) as u8]));
} else {
result.put_pixel(x, y, Luma([val]));
}
}
}
return Ok(DynamicImage::ImageLuma8(result));
}
let nx_tiles = width.div_ceil(tile_size); let ny_tiles = height.div_ceil(tile_size);
let bins = 256; let mut histograms = vec![vec![vec![0u32; bins]; nx_tiles as usize]; ny_tiles as usize];
for y in 0..height {
for x in 0..width {
let tile_x = (x / tile_size) as usize;
let tile_y = (y / tile_size) as usize;
let val = gray.get_pixel(x, y)[0] as usize;
histograms[tile_y][tile_x][val] += 1;
}
}
for (tile_y, hist_row) in histograms.iter_mut().enumerate().take(ny_tiles as usize) {
for (tile_x, hist) in hist_row.iter_mut().enumerate().take(nx_tiles as usize) {
let tile_width = std::cmp::min(tile_size, width - tile_x as u32 * tile_size);
let tile_height = std::cmp::min(tile_size, height - tile_y as u32 * tile_size);
let tile_area = tile_width * tile_height;
let clip_limit_abs = (cliplimit * tile_area as f32 / bins as f32) as u32;
let mut excess = 0u32;
for bin_value in hist.iter_mut() {
if *bin_value > clip_limit_abs {
excess += *bin_value - clip_limit_abs;
*bin_value = clip_limit_abs;
}
}
let redistribution_per_bin = excess / bins as u32;
let mut residual = excess % bins as u32;
for bin_value in hist.iter_mut() {
*bin_value += redistribution_per_bin;
if residual > 0 {
*bin_value += 1;
residual -= 1;
}
}
}
}
let mut cdfs = vec![vec![vec![0u32; bins]; nx_tiles as usize]; ny_tiles as usize];
for tile_y in 0..ny_tiles as usize {
for tile_x in 0..nx_tiles as usize {
let tile_width = std::cmp::min(tile_size, width - tile_x as u32 * tile_size);
let tile_height = std::cmp::min(tile_size, height - tile_y as u32 * tile_size);
let tile_area = tile_width * tile_height;
cdfs[tile_y][tile_x][0] = histograms[tile_y][tile_x][0];
for bin in 1..bins {
cdfs[tile_y][tile_x][bin] =
cdfs[tile_y][tile_x][bin - 1] + histograms[tile_y][tile_x][bin];
}
for cdf_val in cdfs[tile_y][tile_x].iter_mut().take(bins) {
*cdf_val = (*cdf_val * 255).checked_div(tile_area).unwrap_or(0);
}
}
}
let mut result = ImageBuffer::new(width, height);
for y in 0..height {
for x in 0..width {
let val = gray.get_pixel(x, y)[0] as usize;
let tile_x = x / tile_size;
let tile_y = y / tile_size;
let tx = (x % tile_size) as f32 / tile_size as f32;
let ty = (y % tile_size) as f32 / tile_size as f32;
let mapped_value = if tile_x == nx_tiles - 1 && tile_y == ny_tiles - 1 {
cdfs[tile_y as usize][tile_x as usize][val] as f32
} else if tile_x == nx_tiles - 1 {
let top = cdfs[tile_y as usize][tile_x as usize][val] as f32;
let bottom = cdfs[std::cmp::min((tile_y + 1) as usize, (ny_tiles - 1) as usize)]
[tile_x as usize][val] as f32;
(1.0 - ty) * top + ty * bottom
} else if tile_y == ny_tiles - 1 {
let left = cdfs[tile_y as usize][tile_x as usize][val] as f32;
let right = cdfs[tile_y as usize]
[std::cmp::min((tile_x + 1) as usize, (nx_tiles - 1) as usize)][val]
as f32;
(1.0 - tx) * left + tx * right
} else {
let tl = cdfs[tile_y as usize][tile_x as usize][val] as f32;
let tr = cdfs[tile_y as usize][(tile_x + 1) as usize][val] as f32;
let bl = cdfs[(tile_y + 1) as usize][tile_x as usize][val] as f32;
let br = cdfs[(tile_y + 1) as usize][(tile_x + 1) as usize][val] as f32;
let top = (1.0 - tx) * tl + tx * tr;
let bottom = (1.0 - tx) * bl + tx * br;
(1.0 - ty) * top + ty * bottom
};
result.put_pixel(x, y, Luma([mapped_value.clamp(0.0, 255.0) as u8]));
}
}
Ok(DynamicImage::ImageLuma8(result))
}