pub mod features;
pub mod filters;
pub mod morphology;
pub mod transforms;
use crate::array::Array;
use crate::error::NumRs2Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorSpace {
Grayscale,
Rgb,
Rgba,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BorderMode {
#[default]
Constant,
Reflect,
Replicate,
Wrap,
}
#[derive(Debug, Clone)]
pub struct Image {
data: Array<f64>,
width: usize,
height: usize,
color_space: ColorSpace,
}
impl Image {
pub fn from_grayscale(width: usize, height: usize, data: &[f64]) -> Result<Self, NumRs2Error> {
if data.len() != width * height {
return Err(NumRs2Error::ValueError(format!(
"Data length {} does not match image dimensions {}x{} = {}",
data.len(),
width,
height,
width * height
)));
}
let array = Array::from_vec(data.to_vec()).reshape(&[height, width]);
Ok(Self {
data: array,
width,
height,
color_space: ColorSpace::Grayscale,
})
}
pub fn from_rgb(width: usize, height: usize, data: &[f64]) -> Result<Self, NumRs2Error> {
if data.len() != width * height * 3 {
return Err(NumRs2Error::ValueError(format!(
"Data length {} does not match RGB image dimensions {}x{}x3 = {}",
data.len(),
width,
height,
width * height * 3
)));
}
let array = Array::from_vec(data.to_vec()).reshape(&[height, width, 3]);
Ok(Self {
data: array,
width,
height,
color_space: ColorSpace::Rgb,
})
}
pub fn zeros_grayscale(width: usize, height: usize) -> Self {
Self {
data: Array::zeros(&[height, width]),
width,
height,
color_space: ColorSpace::Grayscale,
}
}
pub fn from_array(data: Array<f64>, color_space: ColorSpace) -> Result<Self, NumRs2Error> {
let shape = data.shape();
match color_space {
ColorSpace::Grayscale => {
if shape.len() != 2 {
return Err(NumRs2Error::ValueError(format!(
"Grayscale image requires 2D array, got {}D",
shape.len()
)));
}
Ok(Self {
width: shape[1],
height: shape[0],
data,
color_space,
})
}
ColorSpace::Rgb => {
if shape.len() != 3 || shape[2] != 3 {
return Err(NumRs2Error::ValueError(format!(
"RGB image requires 3D array with 3 channels, got shape {:?}",
shape
)));
}
Ok(Self {
width: shape[1],
height: shape[0],
data,
color_space,
})
}
ColorSpace::Rgba => {
if shape.len() != 3 || shape[2] != 4 {
return Err(NumRs2Error::ValueError(format!(
"RGBA image requires 3D array with 4 channels, got shape {:?}",
shape
)));
}
Ok(Self {
width: shape[1],
height: shape[0],
data,
color_space,
})
}
}
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn color_space(&self) -> ColorSpace {
self.color_space
}
pub fn data(&self) -> &Array<f64> {
&self.data
}
pub fn data_mut(&mut self) -> &mut Array<f64> {
&mut self.data
}
pub fn channels(&self) -> usize {
match self.color_space {
ColorSpace::Grayscale => 1,
ColorSpace::Rgb => 3,
ColorSpace::Rgba => 4,
}
}
pub fn get_pixel(&self, row: usize, col: usize, channel: usize) -> Result<f64, NumRs2Error> {
if row >= self.height || col >= self.width {
return Err(NumRs2Error::IndexError(format!(
"Pixel ({}, {}) out of bounds for image {}x{}",
row, col, self.height, self.width
)));
}
match self.color_space {
ColorSpace::Grayscale => self.data.get(&[row, col]).map_err(|e| {
NumRs2Error::IndexError(format!("Failed to access pixel ({}, {}): {}", row, col, e))
}),
ColorSpace::Rgb | ColorSpace::Rgba => {
if channel >= self.channels() {
return Err(NumRs2Error::IndexError(format!(
"Channel {} out of bounds for {} channels",
channel,
self.channels()
)));
}
self.data.get(&[row, col, channel]).map_err(|e| {
NumRs2Error::IndexError(format!(
"Failed to access pixel ({}, {}, {}): {}",
row, col, channel, e
))
})
}
}
}
pub fn set_pixel(
&mut self,
row: usize,
col: usize,
channel: usize,
value: f64,
) -> Result<(), NumRs2Error> {
if row >= self.height || col >= self.width {
return Err(NumRs2Error::IndexError(format!(
"Pixel ({}, {}) out of bounds for image {}x{}",
row, col, self.height, self.width
)));
}
match self.color_space {
ColorSpace::Grayscale => self.data.set(&[row, col], value).map_err(|e| {
NumRs2Error::IndexError(format!("Failed to set pixel ({}, {}): {}", row, col, e))
}),
ColorSpace::Rgb | ColorSpace::Rgba => {
if channel >= self.channels() {
return Err(NumRs2Error::IndexError(format!(
"Channel {} out of bounds for {} channels",
channel,
self.channels()
)));
}
self.data.set(&[row, col, channel], value).map_err(|e| {
NumRs2Error::IndexError(format!(
"Failed to set pixel ({}, {}, {}): {}",
row, col, channel, e
))
})
}
}
}
pub fn to_grayscale(&self) -> Result<Self, NumRs2Error> {
match self.color_space {
ColorSpace::Grayscale => Ok(self.clone()),
ColorSpace::Rgb | ColorSpace::Rgba => {
let mut result = Array::zeros(&[self.height, self.width]);
for row in 0..self.height {
for col in 0..self.width {
let r = self.data.get(&[row, col, 0]).map_err(|e| {
NumRs2Error::ComputationError(format!("Red channel access: {}", e))
})?;
let g = self.data.get(&[row, col, 1]).map_err(|e| {
NumRs2Error::ComputationError(format!("Green channel access: {}", e))
})?;
let b = self.data.get(&[row, col, 2]).map_err(|e| {
NumRs2Error::ComputationError(format!("Blue channel access: {}", e))
})?;
let gray = 0.2126 * r + 0.7152 * g + 0.0722 * b;
result.set(&[row, col], gray).map_err(|e| {
NumRs2Error::ComputationError(format!("Setting gray pixel: {}", e))
})?;
}
}
Ok(Self {
data: result,
width: self.width,
height: self.height,
color_space: ColorSpace::Grayscale,
})
}
}
}
pub fn clamp(&self) -> Self {
let clamped = self.data.map(|v| v.clamp(0.0, 1.0));
Self {
data: clamped,
width: self.width,
height: self.height,
color_space: self.color_space,
}
}
pub fn to_vec(&self) -> Vec<f64> {
self.data.to_vec()
}
}
#[derive(Debug, Clone)]
pub enum CvError {
InvalidKernelSize(usize),
DimensionMismatch { expected: String, actual: String },
InvalidParameter(String),
RequiresGrayscale,
ComputationFailed(String),
}
impl std::fmt::Display for CvError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CvError::InvalidKernelSize(s) => {
write!(f, "Invalid kernel size: {} (must be odd and positive)", s)
}
CvError::DimensionMismatch { expected, actual } => {
write!(
f,
"Dimension mismatch: expected {}, got {}",
expected, actual
)
}
CvError::InvalidParameter(msg) => write!(f, "Invalid parameter: {}", msg),
CvError::RequiresGrayscale => write!(f, "Operation requires grayscale image"),
CvError::ComputationFailed(msg) => write!(f, "Computation failed: {}", msg),
}
}
}
impl From<CvError> for NumRs2Error {
fn from(err: CvError) -> Self {
NumRs2Error::InvalidOperation(err.to_string())
}
}
pub use features::{
brute_force_match, fast_corner_detect, harris_corner_detect, non_maximum_suppression,
simple_descriptor, FeatureDescriptor, FeatureMatch, Keypoint,
};
pub use filters::{
bilateral_filter, box_blur, canny_edge_detect, convolve2d, gaussian_blur, laplacian_filter,
median_filter, sobel_x, sobel_y,
};
pub use morphology::{
black_hat, closing, dilation, erosion, morphological_gradient, opening, top_hat,
StructuringElement,
};
pub use transforms::{
affine_transform, crop, flip_horizontal, flip_vertical, pad, resize_bilinear, resize_nearest,
rotate, PadMode,
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_image_creation_grayscale() {
let data = vec![0.5; 10 * 10];
let img = Image::from_grayscale(10, 10, &data);
assert!(img.is_ok());
let img = img.expect("test: grayscale image creation should succeed");
assert_eq!(img.width(), 10);
assert_eq!(img.height(), 10);
assert_eq!(img.channels(), 1);
assert_eq!(img.color_space(), ColorSpace::Grayscale);
}
#[test]
fn test_image_creation_rgb() {
let data = vec![0.5; 10 * 10 * 3];
let img = Image::from_rgb(10, 10, &data);
assert!(img.is_ok());
let img = img.expect("test: RGB image creation should succeed");
assert_eq!(img.width(), 10);
assert_eq!(img.height(), 10);
assert_eq!(img.channels(), 3);
assert_eq!(img.color_space(), ColorSpace::Rgb);
}
#[test]
fn test_image_creation_invalid_size() {
let data = vec![0.5; 50];
let result = Image::from_grayscale(10, 10, &data);
assert!(result.is_err());
}
#[test]
fn test_pixel_access() {
let mut data = vec![0.0; 4 * 4];
data[0] = 1.0;
data[5] = 0.5; let img = Image::from_grayscale(4, 4, &data).expect("test: image creation should succeed");
let v = img
.get_pixel(0, 0, 0)
.expect("test: get_pixel should succeed");
assert!((v - 1.0).abs() < 1e-10);
let v2 = img
.get_pixel(1, 1, 0)
.expect("test: get_pixel should succeed");
assert!((v2 - 0.5).abs() < 1e-10);
}
#[test]
fn test_set_pixel() {
let mut img = Image::zeros_grayscale(4, 4);
img.set_pixel(1, 2, 0, 0.75)
.expect("test: set_pixel should succeed");
let v = img
.get_pixel(1, 2, 0)
.expect("test: get_pixel after set should succeed");
assert!((v - 0.75).abs() < 1e-10);
}
#[test]
fn test_rgb_to_grayscale() {
let data = vec![1.0; 2 * 2 * 3];
let img = Image::from_rgb(2, 2, &data).expect("test: RGB image creation should succeed");
let gray = img
.to_grayscale()
.expect("test: to_grayscale should succeed");
assert_eq!(gray.color_space(), ColorSpace::Grayscale);
let v = gray
.get_pixel(0, 0, 0)
.expect("test: get grayscale pixel should succeed");
assert!((v - 1.0).abs() < 0.01);
}
#[test]
fn test_clamp() {
let data = vec![-0.5, 0.5, 1.5, 0.0];
let img = Image::from_grayscale(2, 2, &data).expect("test: image creation should succeed");
let clamped = img.clamp();
let result = clamped.to_vec();
assert!((result[0] - 0.0).abs() < 1e-10);
assert!((result[1] - 0.5).abs() < 1e-10);
assert!((result[2] - 1.0).abs() < 1e-10);
assert!((result[3] - 0.0).abs() < 1e-10);
}
#[test]
fn test_zeros_grayscale() {
let img = Image::zeros_grayscale(8, 8);
assert_eq!(img.width(), 8);
assert_eq!(img.height(), 8);
for row in 0..8 {
for col in 0..8 {
let v = img
.get_pixel(row, col, 0)
.expect("test: get_pixel should succeed");
assert!((v).abs() < 1e-10);
}
}
}
#[test]
fn test_pixel_out_of_bounds() {
let img = Image::zeros_grayscale(4, 4);
assert!(img.get_pixel(5, 0, 0).is_err());
assert!(img.get_pixel(0, 5, 0).is_err());
}
#[test]
fn test_from_array() {
let arr = Array::zeros(&[10, 10]);
let img = Image::from_array(arr, ColorSpace::Grayscale);
assert!(img.is_ok());
let img = img.expect("test: from_array should succeed");
assert_eq!(img.width(), 10);
assert_eq!(img.height(), 10);
}
}