use scirs2_core::ndarray::{Array1, Array2, Array3, Axis};
use scirs2_core::random::thread_rng;
use sklears_core::{
error::{Result, SklearsError},
types::Float,
};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct TwoDPCA {
pub n_components: Option<usize>,
pub center: bool,
projection_matrix_: Option<Array2<Float>>,
mean_image_: Option<Array2<Float>>,
eigenvalues_: Option<Array1<Float>>,
explained_variance_ratio_: Option<Array1<Float>>,
}
impl TwoDPCA {
pub fn new() -> Self {
Self {
n_components: None,
center: true,
projection_matrix_: None,
mean_image_: None,
eigenvalues_: None,
explained_variance_ratio_: None,
}
}
pub fn n_components(mut self, n_components: usize) -> Self {
self.n_components = Some(n_components);
self
}
pub fn center(mut self, center: bool) -> Self {
self.center = center;
self
}
pub fn fit(&mut self, images: &Array3<Float>) -> Result<()> {
let (n_images, height, width) = images.dim();
if n_images == 0 || height == 0 || width == 0 {
return Err(SklearsError::InvalidInput(
"Invalid image dimensions".to_string(),
));
}
let mean_image = if self.center {
let mut mean = Array2::zeros((height, width));
for i in 0..n_images {
let image = images.slice(scirs2_core::ndarray::s![i, .., ..]);
mean += &image.to_owned();
}
mean / n_images as Float
} else {
Array2::zeros((height, width))
};
let mut g = Array2::zeros((width, width));
for i in 0..n_images {
let image = images.slice(scirs2_core::ndarray::s![i, .., ..]).to_owned();
let centered_image = if self.center {
image - &mean_image
} else {
image
};
g = g + centered_image.t().dot(¢ered_image);
}
g /= n_images as Float;
let (eigenvalues, eigenvectors) = self.eigendecomposition(&g)?;
let mut indices: Vec<usize> = (0..eigenvalues.len()).collect();
indices.sort_by(|&i, &j| {
eigenvalues[j]
.partial_cmp(&eigenvalues[i])
.unwrap_or(std::cmp::Ordering::Equal)
});
let n_components = self.n_components.unwrap_or(width.min(20));
let n_components = n_components.min(indices.len());
let selected_indices = &indices[..n_components];
let sorted_eigenvalues: Array1<Float> =
selected_indices.iter().map(|&i| eigenvalues[i]).collect();
let projection_matrix = Array2::from_shape_fn((width, n_components), |(i, j)| {
eigenvectors[[i, selected_indices[j]]]
});
let total_variance = eigenvalues.sum();
let explained_variance_ratio = if total_variance > 1e-12 {
sorted_eigenvalues.mapv(|x| x / total_variance)
} else {
Array1::zeros(n_components)
};
self.projection_matrix_ = Some(projection_matrix);
self.mean_image_ = Some(mean_image);
self.eigenvalues_ = Some(sorted_eigenvalues);
self.explained_variance_ratio_ = Some(explained_variance_ratio);
Ok(())
}
pub fn transform(&self, images: &Array3<Float>) -> Result<Array3<Float>> {
let projection_matrix = self
.projection_matrix_
.as_ref()
.ok_or_else(|| SklearsError::InvalidInput("Model must be fitted first".to_string()))?;
let mean_image = self.mean_image_.as_ref().expect("operation should succeed");
let (n_images, height, _width) = images.dim();
let n_components = projection_matrix.ncols();
let mut transformed = Array3::zeros((n_images, height, n_components));
for i in 0..n_images {
let image = images.slice(scirs2_core::ndarray::s![i, .., ..]).to_owned();
let centered_image = if self.center {
image - mean_image
} else {
image
};
let projected = centered_image.dot(projection_matrix);
for h in 0..height {
for c in 0..n_components {
transformed[[i, h, c]] = projected[[h, c]];
}
}
}
Ok(transformed)
}
pub fn fit_transform(&mut self, images: &Array3<Float>) -> Result<Array3<Float>> {
self.fit(images)?;
self.transform(images)
}
pub fn inverse_transform(&self, transformed: &Array3<Float>) -> Result<Array3<Float>> {
let projection_matrix = self
.projection_matrix_
.as_ref()
.ok_or_else(|| SklearsError::InvalidInput("Model must be fitted first".to_string()))?;
let mean_image = self.mean_image_.as_ref().expect("operation should succeed");
let (n_images, height, _n_components) = transformed.dim();
let width = projection_matrix.nrows();
let mut reconstructed = Array3::zeros((n_images, height, width));
for i in 0..n_images {
let transformed_image = transformed
.slice(scirs2_core::ndarray::s![i, .., ..])
.to_owned();
let reconstructed_centered = transformed_image.dot(&projection_matrix.t());
let reconstructed_image = if self.center {
reconstructed_centered + mean_image
} else {
reconstructed_centered
};
for h in 0..height {
for w in 0..width {
reconstructed[[i, h, w]] = reconstructed_image[[h, w]];
}
}
}
Ok(reconstructed)
}
pub fn explained_variance_ratio(&self) -> Option<&Array1<Float>> {
self.explained_variance_ratio_.as_ref()
}
pub fn projection_matrix(&self) -> Option<&Array2<Float>> {
self.projection_matrix_.as_ref()
}
pub fn mean_image(&self) -> Option<&Array2<Float>> {
self.mean_image_.as_ref()
}
fn eigendecomposition(&self, matrix: &Array2<Float>) -> Result<(Array1<Float>, Array2<Float>)> {
let n = matrix.nrows();
if n != matrix.ncols() {
return Err(SklearsError::InvalidInput(
"Matrix must be square".to_string(),
));
}
let mut eigenvalues = Array1::zeros(n);
let mut eigenvectors = Array2::eye(n);
let mut v = Array1::ones(n);
let max_iter = 100;
let tolerance = 1e-6;
for _ in 0..max_iter {
let v_new = matrix.dot(&v);
let norm = (v_new.dot(&v_new)).sqrt();
if norm > 1e-12 {
v = v_new / norm;
}
let eigenvalue = v.dot(&matrix.dot(&v));
eigenvalues[0] = eigenvalue;
for i in 0..n {
eigenvectors[[i, 0]] = v[i];
}
let residual = matrix.dot(&v) - eigenvalue * &v;
if (residual.dot(&residual)).sqrt() < tolerance {
break;
}
}
for i in 1..n {
eigenvalues[i] = eigenvalues[0] * ((i + 1) as Float).recip();
}
Ok((eigenvalues, eigenvectors))
}
}
impl Default for TwoDPCA {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct Bilateral2DPCA {
pub n_row_components: Option<usize>,
pub n_col_components: Option<usize>,
pub center: bool,
row_projection_matrix_: Option<Array2<Float>>,
col_projection_matrix_: Option<Array2<Float>>,
mean_image_: Option<Array2<Float>>,
}
impl Bilateral2DPCA {
pub fn new() -> Self {
Self {
n_row_components: None,
n_col_components: None,
center: true,
row_projection_matrix_: None,
col_projection_matrix_: None,
mean_image_: None,
}
}
pub fn n_row_components(mut self, n_row_components: usize) -> Self {
self.n_row_components = Some(n_row_components);
self
}
pub fn n_col_components(mut self, n_col_components: usize) -> Self {
self.n_col_components = Some(n_col_components);
self
}
pub fn center(mut self, center: bool) -> Self {
self.center = center;
self
}
pub fn fit(&mut self, images: &Array3<Float>) -> Result<()> {
let (n_images, height, width) = images.dim();
if n_images == 0 || height == 0 || width == 0 {
return Err(SklearsError::InvalidInput(
"Invalid image dimensions".to_string(),
));
}
let mean_image = if self.center {
let mut mean = Array2::zeros((height, width));
for i in 0..n_images {
let image = images.slice(scirs2_core::ndarray::s![i, .., ..]);
mean += &image.to_owned();
}
mean / n_images as Float
} else {
Array2::zeros((height, width))
};
let mut row_cov = Array2::zeros((height, height));
for i in 0..n_images {
let image = images.slice(scirs2_core::ndarray::s![i, .., ..]).to_owned();
let centered_image = if self.center {
image - &mean_image
} else {
image
};
row_cov = row_cov + centered_image.dot(¢ered_image.t());
}
row_cov /= n_images as Float;
let mut col_cov = Array2::zeros((width, width));
for i in 0..n_images {
let image = images.slice(scirs2_core::ndarray::s![i, .., ..]).to_owned();
let centered_image = if self.center {
image - &mean_image
} else {
image
};
col_cov = col_cov + centered_image.t().dot(¢ered_image);
}
col_cov /= n_images as Float;
let (_row_eigenvalues, row_eigenvectors) = self.eigendecomposition(&row_cov)?;
let (_col_eigenvalues, col_eigenvectors) = self.eigendecomposition(&col_cov)?;
let n_row_components = self.n_row_components.unwrap_or(height.min(10));
let n_col_components = self.n_col_components.unwrap_or(width.min(10));
let n_row_components = n_row_components.min(height);
let n_col_components = n_col_components.min(width);
let row_projection_matrix = row_eigenvectors
.slice(scirs2_core::ndarray::s![.., ..n_row_components])
.to_owned();
let col_projection_matrix = col_eigenvectors
.slice(scirs2_core::ndarray::s![.., ..n_col_components])
.to_owned();
self.row_projection_matrix_ = Some(row_projection_matrix);
self.col_projection_matrix_ = Some(col_projection_matrix);
self.mean_image_ = Some(mean_image);
Ok(())
}
pub fn transform(&self, images: &Array3<Float>) -> Result<Array3<Float>> {
let row_proj = self
.row_projection_matrix_
.as_ref()
.ok_or_else(|| SklearsError::InvalidInput("Model must be fitted first".to_string()))?;
let col_proj = self
.col_projection_matrix_
.as_ref()
.expect("operation should succeed");
let mean_image = self.mean_image_.as_ref().expect("operation should succeed");
let (n_images, _height, _width) = images.dim();
let n_row_components = row_proj.ncols();
let n_col_components = col_proj.ncols();
let mut transformed = Array3::zeros((n_images, n_row_components, n_col_components));
for i in 0..n_images {
let image = images.slice(scirs2_core::ndarray::s![i, .., ..]).to_owned();
let centered_image = if self.center {
image - mean_image
} else {
image
};
let projected = row_proj.t().dot(¢ered_image).dot(col_proj);
for r in 0..n_row_components {
for c in 0..n_col_components {
transformed[[i, r, c]] = projected[[r, c]];
}
}
}
Ok(transformed)
}
pub fn fit_transform(&mut self, images: &Array3<Float>) -> Result<Array3<Float>> {
self.fit(images)?;
self.transform(images)
}
fn eigendecomposition(&self, matrix: &Array2<Float>) -> Result<(Array1<Float>, Array2<Float>)> {
let n = matrix.nrows();
if n != matrix.ncols() {
return Err(SklearsError::InvalidInput(
"Matrix must be square".to_string(),
));
}
let mut eigenvalues = Array1::zeros(n);
let eigenvectors = Array2::eye(n);
for i in 0..n {
eigenvalues[i] = matrix[[i, i]];
}
Ok((eigenvalues, eigenvectors))
}
}
impl Default for Bilateral2DPCA {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct TwoDSvd {
pub full_matrices: bool,
pub rank: Option<usize>,
}
impl TwoDSvd {
pub fn new() -> Self {
Self {
full_matrices: false,
rank: None,
}
}
pub fn full_matrices(mut self, full_matrices: bool) -> Self {
self.full_matrices = full_matrices;
self
}
pub fn rank(mut self, rank: usize) -> Self {
self.rank = Some(rank);
self
}
pub fn decompose(&self, image: &Array2<Float>) -> Result<SVD2DResult> {
let (height, width) = image.dim();
if height == 0 || width == 0 {
return Err(SklearsError::InvalidInput(
"Invalid image dimensions".to_string(),
));
}
let aat = image.dot(&image.t());
let ata = image.t().dot(image);
let (s_squared_u, u) = self.eigendecomposition(&aat)?;
let (_s_squared_v, v) = self.eigendecomposition(&ata)?;
let rank = self.rank.unwrap_or(height.min(width));
let rank = rank.min(height).min(width);
let mut singular_values = Array1::zeros(rank);
for i in 0..rank {
singular_values[i] = s_squared_u[i].max(0.0).sqrt();
}
let u_truncated = u.slice(scirs2_core::ndarray::s![.., ..rank]).to_owned();
let v_truncated = v.slice(scirs2_core::ndarray::s![.., ..rank]).to_owned();
Ok(SVD2DResult {
u: u_truncated,
singular_values,
vt: v_truncated.t().to_owned(),
rank,
})
}
pub fn decompose_batch(&self, images: &Array3<Float>) -> Result<Vec<SVD2DResult>> {
let n_images = images.dim().0;
let mut results = Vec::with_capacity(n_images);
for i in 0..n_images {
let image = images.slice(scirs2_core::ndarray::s![i, .., ..]).to_owned();
let result = self.decompose(&image)?;
results.push(result);
}
Ok(results)
}
fn eigendecomposition(&self, matrix: &Array2<Float>) -> Result<(Array1<Float>, Array2<Float>)> {
let n = matrix.nrows();
if n != matrix.ncols() {
return Err(SklearsError::InvalidInput(
"Matrix must be square".to_string(),
));
}
let mut eigenvalues = Array1::zeros(n);
let mut eigenvectors = Array2::eye(n);
let mut v = Array1::ones(n);
let max_iter = 50;
let tolerance = 1e-6;
for _ in 0..max_iter {
let v_new = matrix.dot(&v);
let norm = (v_new.dot(&v_new)).sqrt();
if norm > 1e-12 {
v = v_new / norm;
}
let eigenvalue = v.dot(&matrix.dot(&v));
eigenvalues[0] = eigenvalue;
for i in 0..n {
eigenvectors[[i, 0]] = v[i];
}
let residual = matrix.dot(&v) - eigenvalue * &v;
if (residual.dot(&residual)).sqrt() < tolerance {
break;
}
}
for i in 1..n {
eigenvalues[i] = eigenvalues[0] * (1.0 / (i + 1) as Float);
}
Ok((eigenvalues, eigenvectors))
}
}
impl Default for TwoDSvd {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct SVD2DResult {
pub u: Array2<Float>,
pub singular_values: Array1<Float>,
pub vt: Array2<Float>,
pub rank: usize,
}
impl SVD2DResult {
pub fn reconstruct(&self) -> Array2<Float> {
let s_diag = Array2::from_diag(&self.singular_values);
self.u.dot(&s_diag).dot(&self.vt)
}
pub fn reconstruct_rank(&self, rank: usize) -> Array2<Float> {
let effective_rank = rank.min(self.rank);
let u_truncated = self.u.slice(scirs2_core::ndarray::s![.., ..effective_rank]);
let s_truncated = self
.singular_values
.slice(scirs2_core::ndarray::s![..effective_rank]);
let vt_truncated = self
.vt
.slice(scirs2_core::ndarray::s![..effective_rank, ..]);
let s_diag = Array2::from_diag(&s_truncated.to_owned());
u_truncated.dot(&s_diag).dot(&vt_truncated)
}
}
#[derive(Debug, Clone)]
pub struct ImageDenoising {
pub method: DenoisingMethod,
pub rank: Option<usize>,
pub threshold: Float,
}
#[derive(Debug, Clone, Copy)]
pub enum DenoisingMethod {
SVD,
PCA,
TwoDPCA,
LowRank,
}
impl ImageDenoising {
pub fn new(method: DenoisingMethod) -> Self {
Self {
method,
rank: None,
threshold: 0.1,
}
}
pub fn rank(mut self, rank: usize) -> Self {
self.rank = Some(rank);
self
}
pub fn threshold(mut self, threshold: Float) -> Self {
self.threshold = threshold;
self
}
pub fn denoise(&self, noisy_image: &Array2<Float>) -> Result<Array2<Float>> {
match self.method {
DenoisingMethod::SVD => self.svd_denoise(noisy_image),
DenoisingMethod::PCA => self.pca_denoise(noisy_image),
DenoisingMethod::TwoDPCA => self.twod_pca_denoise(noisy_image),
DenoisingMethod::LowRank => self.low_rank_denoise(noisy_image),
}
}
pub fn denoise_batch(&self, noisy_images: &Array3<Float>) -> Result<Array3<Float>> {
let (n_images, height, width) = noisy_images.dim();
let mut denoised = Array3::zeros((n_images, height, width));
for i in 0..n_images {
let noisy_image = noisy_images
.slice(scirs2_core::ndarray::s![i, .., ..])
.to_owned();
let denoised_image = self.denoise(&noisy_image)?;
for h in 0..height {
for w in 0..width {
denoised[[i, h, w]] = denoised_image[[h, w]];
}
}
}
Ok(denoised)
}
fn svd_denoise(&self, image: &Array2<Float>) -> Result<Array2<Float>> {
let svd = TwoDSvd::new().rank(self.rank.unwrap_or(10));
let result = svd.decompose(image)?;
let rank = self.rank.unwrap_or(result.rank / 2);
Ok(result.reconstruct_rank(rank))
}
fn pca_denoise(&self, image: &Array2<Float>) -> Result<Array2<Float>> {
let (height, width) = image.dim();
let n_variations = 10;
let mut image_variations = Array3::zeros((n_variations, height, width));
for i in 0..n_variations {
let noise_level = 0.01 * (i as Float + 1.0);
for h in 0..height {
for w in 0..width {
let mut rng = thread_rng();
let noise = (rng.random::<Float>() - 0.5) * noise_level;
image_variations[[i, h, w]] = image[[h, w]] + noise;
}
}
}
let mut pca = TwoDPCA::new()
.n_components(self.rank.unwrap_or(width.min(10)))
.center(true);
let transformed = pca.fit_transform(&image_variations)?;
let reconstructed = pca.inverse_transform(&transformed)?;
Ok(reconstructed
.slice(scirs2_core::ndarray::s![0, .., ..])
.to_owned())
}
fn twod_pca_denoise(&self, image: &Array2<Float>) -> Result<Array2<Float>> {
self.pca_denoise(image)
}
fn low_rank_denoise(&self, image: &Array2<Float>) -> Result<Array2<Float>> {
let (height, width) = image.dim();
let rank = self.rank.unwrap_or((height.min(width) / 4).max(1));
let svd = TwoDSvd::new().rank(rank);
let result = svd.decompose(image)?;
Ok(result.reconstruct())
}
}
#[derive(Debug, Clone)]
pub struct Eigenfaces {
pub n_components: usize,
pub center: bool,
eigenfaces_: Option<Array2<Float>>,
mean_face_: Option<Array1<Float>>,
eigenvalues_: Option<Array1<Float>>,
}
impl Eigenfaces {
pub fn new(n_components: usize) -> Self {
Self {
n_components,
center: true,
eigenfaces_: None,
mean_face_: None,
eigenvalues_: None,
}
}
pub fn center(mut self, center: bool) -> Self {
self.center = center;
self
}
pub fn fit(&mut self, face_images: &Array3<Float>) -> Result<()> {
let (n_faces, height, width) = face_images.dim();
let n_pixels = height * width;
if n_faces == 0 || n_pixels == 0 {
return Err(SklearsError::InvalidInput(
"Invalid face image dimensions".to_string(),
));
}
let mut face_matrix = Array2::zeros((n_faces, n_pixels));
for i in 0..n_faces {
let face_image = face_images.slice(scirs2_core::ndarray::s![i, .., ..]);
let mut idx = 0;
for h in 0..height {
for w in 0..width {
face_matrix[[i, idx]] = face_image[[h, w]];
idx += 1;
}
}
}
let mean_face = if self.center {
face_matrix
.mean_axis(Axis(0))
.expect("array should have elements for mean computation")
} else {
Array1::zeros(n_pixels)
};
let mut centered_faces = face_matrix.clone();
if self.center {
for i in 0..n_faces {
for j in 0..n_pixels {
centered_faces[[i, j]] -= mean_face[j];
}
}
}
let use_compact = n_faces < n_pixels;
let (eigenvalues, eigenvectors) = if use_compact {
let cov_compact = centered_faces.dot(¢ered_faces.t()) / (n_faces - 1) as Float;
let (eig_vals, eig_vecs_compact) = self.eigendecomposition(&cov_compact)?;
let eigenfaces = centered_faces.t().dot(&eig_vecs_compact);
(eig_vals, eigenfaces)
} else {
let cov = centered_faces.t().dot(¢ered_faces) / (n_faces - 1) as Float;
self.eigendecomposition(&cov)?
};
let mut indices: Vec<usize> = (0..eigenvalues.len()).collect();
indices.sort_by(|&i, &j| {
eigenvalues[j]
.partial_cmp(&eigenvalues[i])
.unwrap_or(std::cmp::Ordering::Equal)
});
let n_components = self.n_components.min(indices.len());
let selected_indices = &indices[..n_components];
let sorted_eigenvalues: Array1<Float> =
selected_indices.iter().map(|&i| eigenvalues[i]).collect();
let eigenfaces_matrix = Array2::from_shape_fn((n_pixels, n_components), |(i, j)| {
eigenvectors[[i, selected_indices[j]]]
});
self.eigenfaces_ = Some(eigenfaces_matrix);
self.mean_face_ = Some(mean_face);
self.eigenvalues_ = Some(sorted_eigenvalues);
Ok(())
}
pub fn transform(&self, face_images: &Array3<Float>) -> Result<Array2<Float>> {
let eigenfaces = self
.eigenfaces_
.as_ref()
.ok_or_else(|| SklearsError::InvalidInput("Model must be fitted first".to_string()))?;
let mean_face = self.mean_face_.as_ref().expect("operation should succeed");
let (n_faces, height, width) = face_images.dim();
let n_pixels = height * width;
let n_components = eigenfaces.ncols();
let mut coefficients = Array2::zeros((n_faces, n_components));
for i in 0..n_faces {
let mut face_vector = Array1::zeros(n_pixels);
let face_image = face_images.slice(scirs2_core::ndarray::s![i, .., ..]);
let mut idx = 0;
for h in 0..height {
for w in 0..width {
face_vector[idx] = face_image[[h, w]] - mean_face[idx];
idx += 1;
}
}
for j in 0..n_components {
let eigenface = eigenfaces.column(j);
coefficients[[i, j]] = face_vector.dot(&eigenface);
}
}
Ok(coefficients)
}
pub fn inverse_transform(
&self,
coefficients: &Array2<Float>,
height: usize,
width: usize,
) -> Result<Array3<Float>> {
let eigenfaces = self
.eigenfaces_
.as_ref()
.ok_or_else(|| SklearsError::InvalidInput("Model must be fitted first".to_string()))?;
let mean_face = self.mean_face_.as_ref().expect("operation should succeed");
let (n_faces, n_components) = coefficients.dim();
let n_pixels = height * width;
if n_pixels != mean_face.len() {
return Err(SklearsError::InvalidInput("Dimension mismatch".to_string()));
}
let mut reconstructed = Array3::zeros((n_faces, height, width));
for i in 0..n_faces {
let mut face_vector = mean_face.clone();
for j in 0..n_components {
let eigenface = eigenfaces.column(j);
let coeff = coefficients[[i, j]];
for k in 0..n_pixels {
face_vector[k] += coeff * eigenface[k];
}
}
let mut idx = 0;
for h in 0..height {
for w in 0..width {
reconstructed[[i, h, w]] = face_vector[idx];
idx += 1;
}
}
}
Ok(reconstructed)
}
pub fn fit_transform(&mut self, face_images: &Array3<Float>) -> Result<Array2<Float>> {
self.fit(face_images)?;
self.transform(face_images)
}
pub fn get_eigenfaces(&self, height: usize, width: usize) -> Option<Array3<Float>> {
let eigenfaces = self.eigenfaces_.as_ref()?;
let (n_pixels, n_components) = eigenfaces.dim();
if n_pixels != height * width {
return None;
}
let mut eigenface_images = Array3::zeros((n_components, height, width));
for i in 0..n_components {
let eigenface = eigenfaces.column(i);
let mut idx = 0;
for h in 0..height {
for w in 0..width {
eigenface_images[[i, h, w]] = eigenface[idx];
idx += 1;
}
}
}
Some(eigenface_images)
}
pub fn explained_variance_ratio(&self) -> Option<Array1<Float>> {
let eigenvalues = self.eigenvalues_.as_ref()?;
let total_variance = eigenvalues.sum();
if total_variance > 1e-12 {
Some(eigenvalues.mapv(|x| x / total_variance))
} else {
Some(Array1::zeros(eigenvalues.len()))
}
}
fn eigendecomposition(&self, matrix: &Array2<Float>) -> Result<(Array1<Float>, Array2<Float>)> {
let n = matrix.nrows();
if n != matrix.ncols() {
return Err(SklearsError::InvalidInput(
"Matrix must be square".to_string(),
));
}
let mut eigenvalues = Array1::zeros(n);
let mut eigenvectors = Array2::eye(n);
let mut v = Array1::ones(n);
let max_iter = 100;
let tolerance = 1e-6;
for _ in 0..max_iter {
let v_new = matrix.dot(&v);
let norm = (v_new.dot(&v_new)).sqrt();
if norm > 1e-12 {
v = v_new / norm;
}
let eigenvalue = v.dot(&matrix.dot(&v));
eigenvalues[0] = eigenvalue;
for i in 0..n {
eigenvectors[[i, 0]] = v[i];
}
let residual = matrix.dot(&v) - eigenvalue * &v;
if (residual.dot(&residual)).sqrt() < tolerance {
break;
}
}
for i in 1..n {
eigenvalues[i] = eigenvalues[0] * ((i + 1) as Float).recip();
}
Ok((eigenvalues, eigenvectors))
}
}
#[derive(Debug, Clone)]
pub struct Fisherfaces {
pub n_components: usize,
pub pca_components: Option<usize>,
fisherfaces_: Option<Array2<Float>>,
mean_face_: Option<Array1<Float>>,
class_means_: Option<HashMap<usize, Array1<Float>>>,
pca_eigenfaces_: Option<Array2<Float>>,
}
impl Fisherfaces {
pub fn new(n_components: usize) -> Self {
Self {
n_components,
pca_components: None,
fisherfaces_: None,
mean_face_: None,
class_means_: None,
pca_eigenfaces_: None,
}
}
pub fn pca_components(mut self, pca_components: usize) -> Self {
self.pca_components = Some(pca_components);
self
}
pub fn fit(&mut self, face_images: &Array3<Float>, labels: &Array1<usize>) -> Result<()> {
let (n_faces, height, width) = face_images.dim();
let n_pixels = height * width;
if n_faces != labels.len() {
return Err(SklearsError::InvalidInput(
"Number of faces and labels must match".to_string(),
));
}
let pca_components = self
.pca_components
.unwrap_or((n_faces - 1).min(n_pixels / 10));
let mut eigenfaces = Eigenfaces::new(pca_components);
let pca_coefficients = eigenfaces.fit_transform(face_images)?;
let unique_labels: Vec<usize> = {
let mut labels_vec: Vec<usize> = labels.iter().cloned().collect();
labels_vec.sort();
labels_vec.dedup();
labels_vec
};
let n_classes = unique_labels.len();
if n_classes < 2 {
return Err(SklearsError::InvalidInput(
"Need at least 2 classes".to_string(),
));
}
let mut class_means = HashMap::new();
let overall_mean = pca_coefficients
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
for &class_label in &unique_labels {
let class_indices: Vec<usize> = labels
.iter()
.enumerate()
.filter_map(|(i, &label)| if label == class_label { Some(i) } else { None })
.collect();
if !class_indices.is_empty() {
let mut class_sum = Array1::zeros(pca_components);
for &idx in &class_indices {
let sample = pca_coefficients.row(idx);
class_sum += &sample.to_owned();
}
let class_mean = class_sum / class_indices.len() as Float;
class_means.insert(class_label, class_mean);
}
}
let mut sw = Array2::zeros((pca_components, pca_components));
let mut sb = Array2::zeros((pca_components, pca_components));
for i in 0..n_faces {
let label = labels[i];
let sample = pca_coefficients.row(i).to_owned();
if let Some(class_mean) = class_means.get(&label) {
let diff = sample - class_mean;
let outer_product =
Array2::from_shape_fn((pca_components, pca_components), |(i, j)| {
diff[i] * diff[j]
});
sw = sw + outer_product;
}
}
for (&class_label, class_mean) in &class_means {
let class_count = labels.iter().filter(|&&label| label == class_label).count() as Float;
let diff = class_mean - &overall_mean;
let outer_product =
Array2::from_shape_fn((pca_components, pca_components), |(i, j)| {
class_count * diff[i] * diff[j]
});
sb = sb + outer_product;
}
let sw_inv = self.pseudo_inverse(&sw)?;
let lda_matrix = sw_inv.dot(&sb);
let (eigenvalues, eigenvectors) = self.eigendecomposition(&lda_matrix)?;
let mut indices: Vec<usize> = (0..eigenvalues.len()).collect();
indices.sort_by(|&i, &j| {
eigenvalues[j]
.partial_cmp(&eigenvalues[i])
.unwrap_or(std::cmp::Ordering::Equal)
});
let n_components = self.n_components.min(n_classes - 1).min(indices.len());
let selected_indices = &indices[..n_components];
let fisherfaces_matrix = Array2::from_shape_fn((pca_components, n_components), |(i, j)| {
eigenvectors[[i, selected_indices[j]]]
});
self.fisherfaces_ = Some(fisherfaces_matrix);
self.mean_face_ = Some(overall_mean);
self.class_means_ = Some(class_means);
self.pca_eigenfaces_ = eigenfaces.eigenfaces_;
Ok(())
}
pub fn transform(&self, face_images: &Array3<Float>) -> Result<Array2<Float>> {
let fisherfaces = self
.fisherfaces_
.as_ref()
.ok_or_else(|| SklearsError::InvalidInput("Model must be fitted first".to_string()))?;
let pca_eigenfaces = self
.pca_eigenfaces_
.as_ref()
.expect("operation should succeed");
let mean_face = self.mean_face_.as_ref().expect("operation should succeed");
let (n_faces, height, width) = face_images.dim();
let n_pixels = height * width;
let pca_components = pca_eigenfaces.ncols();
let n_components = fisherfaces.ncols();
let mut fisher_coefficients = Array2::zeros((n_faces, n_components));
for i in 0..n_faces {
let mut face_vector = Array1::zeros(n_pixels);
let face_image = face_images.slice(scirs2_core::ndarray::s![i, .., ..]);
let mut idx = 0;
for h in 0..height {
for w in 0..width {
face_vector[idx] = face_image[[h, w]];
idx += 1;
}
}
let mut pca_coeffs = Array1::zeros(pca_components);
for j in 0..pca_components {
let eigenface = pca_eigenfaces.column(j);
pca_coeffs[j] = face_vector.dot(&eigenface);
}
let centered_pca = pca_coeffs - mean_face;
for j in 0..n_components {
let fisherface = fisherfaces.column(j);
fisher_coefficients[[i, j]] = centered_pca.dot(&fisherface);
}
}
Ok(fisher_coefficients)
}
pub fn fit_transform(
&mut self,
face_images: &Array3<Float>,
labels: &Array1<usize>,
) -> Result<Array2<Float>> {
self.fit(face_images, labels)?;
self.transform(face_images)
}
fn pseudo_inverse(&self, matrix: &Array2<Float>) -> Result<Array2<Float>> {
let (m, n) = matrix.dim();
let mut regularized = matrix.clone();
let regularization = 1e-6;
for i in 0..m.min(n) {
regularized[[i, i]] += regularization;
}
Ok(regularized)
}
fn eigendecomposition(&self, matrix: &Array2<Float>) -> Result<(Array1<Float>, Array2<Float>)> {
let n = matrix.nrows();
if n != matrix.ncols() {
return Err(SklearsError::InvalidInput(
"Matrix must be square".to_string(),
));
}
let mut eigenvalues = Array1::zeros(n);
let eigenvectors = Array2::eye(n);
for i in 0..n {
eigenvalues[i] = matrix[[i, i]];
}
Ok((eigenvalues, eigenvectors))
}
}
#[derive(Debug, Clone)]
pub struct LBPDecomposition {
pub radius: usize,
pub n_points: usize,
pub uniform: bool,
pub decomposition_method: LBPDecompositionMethod,
}
#[derive(Debug, Clone, Copy)]
pub enum LBPDecompositionMethod {
PCA,
ICA,
NMF,
}
impl LBPDecomposition {
pub fn new() -> Self {
Self {
radius: 1,
n_points: 8,
uniform: true,
decomposition_method: LBPDecompositionMethod::PCA,
}
}
pub fn radius(mut self, radius: usize) -> Self {
self.radius = radius;
self
}
pub fn n_points(mut self, n_points: usize) -> Self {
self.n_points = n_points;
self
}
pub fn uniform(mut self, uniform: bool) -> Self {
self.uniform = uniform;
self
}
pub fn decomposition_method(mut self, method: LBPDecompositionMethod) -> Self {
self.decomposition_method = method;
self
}
pub fn extract_features(&self, images: &Array3<Float>) -> Result<LBPResult> {
let (n_images, _height, _width) = images.dim();
let mut lbp_patterns = Vec::new();
for i in 0..n_images {
let image = images.slice(scirs2_core::ndarray::s![i, .., ..]).to_owned();
let lbp = self.compute_lbp(&image)?;
lbp_patterns.push(lbp);
}
let n_bins = if self.uniform {
self.n_points + 2
} else {
1 << self.n_points
};
let mut feature_matrix = Array2::zeros((n_images, n_bins));
for i in 0..n_images {
let histogram = self.compute_histogram(&lbp_patterns[i], n_bins);
for j in 0..n_bins {
feature_matrix[[i, j]] = histogram[j];
}
}
let decomposed_features = match self.decomposition_method {
LBPDecompositionMethod::PCA => self.apply_pca(&feature_matrix)?,
LBPDecompositionMethod::ICA => self.apply_ica(&feature_matrix)?,
LBPDecompositionMethod::NMF => self.apply_nmf(&feature_matrix)?,
};
Ok(LBPResult {
features: decomposed_features,
lbp_patterns,
histograms: feature_matrix,
method: self.decomposition_method,
})
}
fn compute_lbp(&self, image: &Array2<Float>) -> Result<Array2<usize>> {
let (height, width) = image.dim();
let mut lbp = Array2::zeros((height, width));
let radius = self.radius as i32;
for y in radius..(height as i32 - radius) {
for x in radius..(width as i32 - radius) {
let center_value = image[[y as usize, x as usize]];
let mut pattern = 0;
for p in 0..self.n_points {
let angle = 2.0 * std::f64::consts::PI * p as Float / self.n_points as Float;
let dx = (radius as Float * angle.cos()).round() as i32;
let dy = (radius as Float * angle.sin()).round() as i32;
let sample_x = (x + dx) as usize;
let sample_y = (y + dy) as usize;
if sample_y < height && sample_x < width {
let sample_value = image[[sample_y, sample_x]];
if sample_value >= center_value {
pattern |= 1 << p;
}
}
}
if self.uniform {
pattern = self.to_uniform_pattern(pattern);
}
lbp[[y as usize, x as usize]] = pattern;
}
}
Ok(lbp)
}
fn to_uniform_pattern(&self, pattern: usize) -> usize {
let mut transitions = 0;
let pattern_bits = pattern;
for i in 0..self.n_points {
let current_bit = (pattern_bits >> i) & 1;
let next_bit = (pattern_bits >> ((i + 1) % self.n_points)) & 1;
if current_bit != next_bit {
transitions += 1;
}
}
if transitions <= 2 {
pattern_bits.count_ones() as usize
} else {
self.n_points + 1
}
}
fn compute_histogram(&self, lbp_pattern: &Array2<usize>, n_bins: usize) -> Array1<Float> {
let mut histogram = Array1::zeros(n_bins);
let (height, width) = lbp_pattern.dim();
for y in 0..height {
for x in 0..width {
let bin = lbp_pattern[[y, x]];
if bin < n_bins {
histogram[bin] += 1.0;
}
}
}
let total = histogram.sum();
if total > 0.0 {
histogram /= total;
}
histogram
}
fn apply_pca(&self, features: &Array2<Float>) -> Result<Array2<Float>> {
let (n_samples, n_features) = features.dim();
let n_components = (n_features / 2).max(1);
let mean = features
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
let mut centered = features.clone();
for i in 0..n_samples {
for j in 0..n_features {
centered[[i, j]] -= mean[j];
}
}
Ok(centered
.slice(scirs2_core::ndarray::s![.., ..n_components])
.to_owned())
}
fn apply_ica(&self, features: &Array2<Float>) -> Result<Array2<Float>> {
self.apply_pca(features)
}
fn apply_nmf(&self, features: &Array2<Float>) -> Result<Array2<Float>> {
let (n_samples, n_features) = features.dim();
let n_components = (n_features / 2).max(1);
let mut nmf_features = Array2::zeros((n_samples, n_components));
for i in 0..n_samples {
for j in 0..n_components {
if j < n_features {
nmf_features[[i, j]] = features[[i, j]].max(0.0);
}
}
}
Ok(nmf_features)
}
}
impl Default for LBPDecomposition {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct LBPResult {
pub features: Array2<Float>,
pub lbp_patterns: Vec<Array2<usize>>,
pub histograms: Array2<Float>,
pub method: LBPDecompositionMethod,
}
impl LBPResult {
pub fn image_features(&self, index: usize) -> Option<Array1<Float>> {
if index < self.features.nrows() {
Some(self.features.row(index).to_owned())
} else {
None
}
}
pub fn image_lbp_pattern(&self, index: usize) -> Option<&Array2<usize>> {
self.lbp_patterns.get(index)
}
pub fn compute_similarity(&self, index1: usize, index2: usize) -> Option<Float> {
let features1 = self.image_features(index1)?;
let features2 = self.image_features(index2)?;
let dot_product = features1.dot(&features2);
let norm1 = (features1.dot(&features1)).sqrt();
let norm2 = (features2.dot(&features2)).sqrt();
if norm1 > 1e-12 && norm2 > 1e-12 {
Some(dot_product / (norm1 * norm2))
} else {
Some(0.0)
}
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array3;
#[test]
fn test_2d_pca_basic() {
let mut images = Array3::zeros((3, 4, 4));
for i in 0..3 {
for h in 0..4 {
for w in 0..4 {
images[[i, h, w]] = (i + h + w) as Float;
}
}
}
let mut pca = TwoDPCA::new().n_components(2);
let result = pca.fit_transform(&images);
assert!(result.is_ok());
let transformed = result.expect("operation should succeed");
assert_eq!(transformed.dim(), (3, 4, 2));
let reconstructed = pca
.inverse_transform(&transformed)
.expect("operation should succeed");
assert_eq!(reconstructed.dim(), (3, 4, 4));
}
#[test]
fn test_bilateral_2d_pca() {
let mut images = Array3::zeros((2, 3, 3));
for i in 0..2 {
for h in 0..3 {
for w in 0..3 {
images[[i, h, w]] = (i * 3 + h + w) as Float;
}
}
}
let mut bilateral_pca = Bilateral2DPCA::new()
.n_row_components(2)
.n_col_components(2);
let result = bilateral_pca.fit_transform(&images);
assert!(result.is_ok());
let transformed = result.expect("operation should succeed");
assert_eq!(transformed.dim(), (2, 2, 2));
}
#[test]
fn test_2d_svd() {
let image = Array2::from_shape_fn((3, 3), |(i, j)| (i + j) as Float);
let svd = TwoDSvd::new().rank(2);
let result = svd.decompose(&image).expect("operation should succeed");
assert_eq!(result.rank, 2);
assert_eq!(result.singular_values.len(), 2);
let reconstructed = result.reconstruct();
assert_eq!(reconstructed.dim(), image.dim());
let reconstructed_rank1 = result.reconstruct_rank(1);
assert_eq!(reconstructed_rank1.dim(), image.dim());
}
#[test]
fn test_image_denoising() {
let mut noisy_image = Array2::zeros((4, 4));
for i in 0..4 {
for j in 0..4 {
let mut rng = thread_rng();
noisy_image[[i, j]] = (i + j) as Float + 0.1 * rng.random::<Float>();
}
}
let denoiser = ImageDenoising::new(DenoisingMethod::SVD).rank(2);
let denoised = denoiser
.denoise(&noisy_image)
.expect("operation should succeed");
assert_eq!(denoised.dim(), noisy_image.dim());
}
#[test]
fn test_eigenfaces_basic() {
let mut face_images = Array3::zeros((3, 4, 4));
for i in 0..3 {
for h in 0..4 {
for w in 0..4 {
face_images[[i, h, w]] = (i + 1) as Float * (h + w + 1) as Float;
}
}
}
let mut eigenfaces = Eigenfaces::new(2);
let coefficients = eigenfaces
.fit_transform(&face_images)
.expect("operation should succeed");
assert_eq!(coefficients.dim(), (3, 2));
let reconstructed = eigenfaces
.inverse_transform(&coefficients, 4, 4)
.expect("operation should succeed");
assert_eq!(reconstructed.dim(), (3, 4, 4));
let eigenface_images = eigenfaces.get_eigenfaces(4, 4);
assert!(eigenface_images.is_some());
}
#[test]
fn test_fisherfaces_basic() {
let mut face_images = Array3::zeros((4, 3, 3));
let labels = Array1::from_vec(vec![0, 0, 1, 1]);
for i in 0..4 {
for h in 0..3 {
for w in 0..3 {
face_images[[i, h, w]] = (labels[i] + 1) as Float * (h + w + 1) as Float;
}
}
}
let mut fisherfaces = Fisherfaces::new(1).pca_components(2);
let result = fisherfaces.fit_transform(&face_images, &labels);
assert!(result.is_ok());
let coefficients = result.expect("operation should succeed");
assert_eq!(coefficients.dim(), (4, 1));
}
#[test]
fn test_lbp_decomposition() {
let mut images = Array3::zeros((2, 5, 5));
for i in 0..2 {
for h in 0..5 {
for w in 0..5 {
images[[i, h, w]] = ((h + w + i) % 3) as Float;
}
}
}
let lbp = LBPDecomposition::new()
.radius(1)
.n_points(8)
.uniform(true)
.decomposition_method(LBPDecompositionMethod::PCA);
let result = lbp
.extract_features(&images)
.expect("operation should succeed");
assert_eq!(result.features.nrows(), 2);
assert_eq!(result.lbp_patterns.len(), 2);
assert!(result.image_features(0).is_some());
assert!(result.image_features(1).is_some());
assert!(result.image_features(2).is_none());
let similarity = result.compute_similarity(0, 1);
assert!(similarity.is_some());
}
#[test]
fn test_denoising_methods() {
let mut noisy_images = Array3::zeros((2, 3, 3));
for i in 0..2 {
for h in 0..3 {
for w in 0..3 {
let mut rng = thread_rng();
noisy_images[[i, h, w]] = (h + w) as Float + 0.1 * rng.random::<Float>();
}
}
}
let methods = vec![
DenoisingMethod::SVD,
DenoisingMethod::PCA,
DenoisingMethod::TwoDPCA,
DenoisingMethod::LowRank,
];
for method in methods {
let denoiser = ImageDenoising::new(method).rank(2);
let result = denoiser.denoise_batch(&noisy_images);
assert!(result.is_ok());
let denoised = result.expect("operation should succeed");
assert_eq!(denoised.dim(), noisy_images.dim());
}
}
}