use crate::*;
use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
#[derive(Debug, Clone)]
pub struct ShapeDescriptorExtractor {
max_moment_order: usize,
binary_threshold: f64,
normalize: bool,
central_moments: bool,
hu_moments: bool,
advanced_properties: bool,
}
impl ShapeDescriptorExtractor {
pub fn new() -> Self {
Self {
max_moment_order: 3,
binary_threshold: 0.5,
normalize: true,
central_moments: true,
hu_moments: true,
advanced_properties: true,
}
}
pub fn max_moment_order(mut self, order: usize) -> Self {
self.max_moment_order = order.clamp(1, 10); self
}
pub fn binary_threshold(mut self, threshold: f64) -> Self {
self.binary_threshold = threshold.clamp(0.0, 1.0);
self
}
pub fn normalize_moments(mut self, normalize: bool) -> Self {
self.normalize = normalize;
self
}
pub fn central_moments(mut self, central: bool) -> Self {
self.central_moments = central;
self
}
pub fn hu_moments(mut self, hu: bool) -> Self {
self.hu_moments = hu;
self
}
pub fn advanced_properties(mut self, advanced: bool) -> Self {
self.advanced_properties = advanced;
self
}
pub fn extract_features(&self, image: &ArrayView2<Float>) -> SklResult<Array1<Float>> {
let (height, width) = image.dim();
if height < 3 || width < 3 {
return Err(SklearsError::InvalidInput(
"Image too small for shape analysis (minimum 3x3)".to_string(),
));
}
let binary_image = self.to_binary(image);
let area = self.compute_area(&binary_image);
if area == 0.0 {
return Ok(Array1::zeros(self.estimate_feature_count()));
}
let centroid = self.compute_centroid(&binary_image, area);
let perimeter = self.compute_perimeter(&binary_image);
let moments = self.compute_geometric_moments(&binary_image, centroid)?;
let mut features = Vec::new();
features.extend(self.extract_basic_properties(area, centroid, perimeter, &binary_image));
features.extend(moments);
if self.hu_moments {
let hu_moments = self.compute_hu_moments(&binary_image, centroid)?;
features.extend(hu_moments);
}
if self.advanced_properties {
let advanced = self.extract_advanced_properties(&binary_image, area);
features.extend(advanced);
}
Ok(Array1::from_vec(features))
}
fn to_binary(&self, image: &ArrayView2<Float>) -> Array2<bool> {
let (height, width) = image.dim();
let mut binary = Array2::default((height, width));
for y in 0..height {
for x in 0..width {
binary[[y, x]] = image[[y, x]] > self.binary_threshold;
}
}
binary
}
fn compute_area(&self, binary_image: &Array2<bool>) -> f64 {
binary_image.iter().filter(|&&pixel| pixel).count() as f64
}
fn compute_centroid(&self, binary_image: &Array2<bool>, area: f64) -> (f64, f64) {
if area == 0.0 {
return (0.0, 0.0);
}
let (height, width) = binary_image.dim();
let mut sum_x = 0.0;
let mut sum_y = 0.0;
for y in 0..height {
for x in 0..width {
if binary_image[[y, x]] {
sum_x += x as f64;
sum_y += y as f64;
}
}
}
(sum_x / area, sum_y / area)
}
fn compute_perimeter(&self, binary_image: &Array2<bool>) -> f64 {
let (height, width) = binary_image.dim();
let mut perimeter = 0.0;
for y in 1..height - 1 {
for x in 1..width - 1 {
if binary_image[[y, x]] {
let neighbors = [
binary_image[[y - 1, x]], binary_image[[y + 1, x]], binary_image[[y, x - 1]], binary_image[[y, x + 1]], ];
if neighbors.iter().any(|&n| !n) {
perimeter += 1.0;
}
}
}
}
perimeter
}
fn compute_geometric_moments(
&self,
binary_image: &Array2<bool>,
centroid: (f64, f64),
) -> SklResult<Vec<f64>> {
let (height, width) = binary_image.dim();
let (cx, cy) = centroid;
let mut moments = Vec::new();
for p in 0..=self.max_moment_order {
for q in 0..=self.max_moment_order {
if p + q <= self.max_moment_order {
let mut m_pq = 0.0;
let mut mu_pq = 0.0;
for y in 0..height {
for x in 0..width {
if binary_image[[y, x]] {
let x_f = x as f64;
let y_f = y as f64;
m_pq += x_f.powi(p as i32) * y_f.powi(q as i32);
if self.central_moments {
mu_pq += (x_f - cx).powi(p as i32) * (y_f - cy).powi(q as i32);
}
}
}
}
moments.push(m_pq);
if self.central_moments {
if self.normalize && p + q >= 2 {
let m00 = self.compute_area(binary_image);
if m00 > 0.0 {
let gamma = ((p + q) as f64 / 2.0) + 1.0;
let eta_pq = mu_pq / m00.powf(gamma);
moments.push(eta_pq);
} else {
moments.push(0.0);
}
} else {
moments.push(mu_pq);
}
}
}
}
}
Ok(moments)
}
fn extract_basic_properties(
&self,
area: f64,
centroid: (f64, f64),
perimeter: f64,
binary_image: &Array2<bool>,
) -> Vec<f64> {
let mut features = vec![
area, centroid.0, centroid.1, perimeter,
];
let compactness = if perimeter > 0.0 {
(4.0 * std::f64::consts::PI * area) / (perimeter * perimeter)
} else {
0.0
};
let aspect_ratio = self.compute_aspect_ratio(binary_image);
let extent = self.compute_extent(binary_image, area);
let solidity = self.compute_solidity(binary_image, area);
features.push(compactness);
features.push(aspect_ratio);
features.push(extent);
features.push(solidity);
features
}
fn compute_aspect_ratio(&self, binary_image: &Array2<bool>) -> f64 {
let (min_x, max_x, min_y, max_y) = self.find_bounding_box(binary_image);
let width = (max_x - min_x + 1) as f64;
let height = (max_y - min_y + 1) as f64;
if height > 0.0 {
width / height
} else {
1.0
}
}
fn compute_extent(&self, binary_image: &Array2<bool>, area: f64) -> f64 {
let (min_x, max_x, min_y, max_y) = self.find_bounding_box(binary_image);
let bbox_width = (max_x - min_x + 1) as f64;
let bbox_height = (max_y - min_y + 1) as f64;
let bbox_area = bbox_width * bbox_height;
if bbox_area > 0.0 {
area / bbox_area
} else {
0.0
}
}
fn compute_solidity(&self, binary_image: &Array2<bool>, area: f64) -> f64 {
let extent = self.compute_extent(binary_image, area);
let irregularity_penalty = self.compute_shape_irregularity(binary_image);
(extent * (1.0 - irregularity_penalty)).clamp(0.0, 1.0)
}
fn find_bounding_box(&self, binary_image: &Array2<bool>) -> (usize, usize, usize, usize) {
let (height, width) = binary_image.dim();
let mut min_x = width;
let mut max_x = 0;
let mut min_y = height;
let mut max_y = 0;
for y in 0..height {
for x in 0..width {
if binary_image[[y, x]] {
min_x = min_x.min(x);
max_x = max_x.max(x);
min_y = min_y.min(y);
max_y = max_y.max(y);
}
}
}
if min_x == width {
(0, 0, 0, 0)
} else {
(min_x, max_x, min_y, max_y)
}
}
fn compute_shape_irregularity(&self, binary_image: &Array2<bool>) -> f64 {
let (height, width) = binary_image.dim();
let mut boundary_variation = 0.0;
let mut boundary_points = 0;
for y in 1..height - 1 {
for x in 1..width - 1 {
if binary_image[[y, x]] {
let neighbors = [
binary_image[[y - 1, x - 1]],
binary_image[[y - 1, x]],
binary_image[[y - 1, x + 1]],
binary_image[[y, x - 1]],
binary_image[[y, x + 1]],
binary_image[[y + 1, x - 1]],
binary_image[[y + 1, x]],
binary_image[[y + 1, x + 1]],
];
let neighbor_count = neighbors.iter().filter(|&&n| n).count();
if neighbor_count < 8 {
boundary_variation += (8 - neighbor_count) as f64 / 8.0;
boundary_points += 1;
}
}
}
}
if boundary_points > 0 {
(boundary_variation / boundary_points as f64).min(1.0)
} else {
0.0
}
}
fn compute_hu_moments(
&self,
binary_image: &Array2<bool>,
centroid: (f64, f64),
) -> SklResult<Vec<f64>> {
let mu_00 = self.compute_area(binary_image);
if mu_00 == 0.0 {
return Ok(vec![0.0; 7]);
}
let mu_20 = self.compute_central_moment(binary_image, centroid, 2, 0);
let mu_02 = self.compute_central_moment(binary_image, centroid, 0, 2);
let mu_11 = self.compute_central_moment(binary_image, centroid, 1, 1);
let mu_30 = self.compute_central_moment(binary_image, centroid, 3, 0);
let mu_03 = self.compute_central_moment(binary_image, centroid, 0, 3);
let mu_21 = self.compute_central_moment(binary_image, centroid, 2, 1);
let mu_12 = self.compute_central_moment(binary_image, centroid, 1, 2);
let eta_20 = mu_20 / mu_00.powi(2);
let eta_02 = mu_02 / mu_00.powi(2);
let eta_11 = mu_11 / mu_00.powi(2);
let eta_30 = mu_30 / mu_00.powf(2.5);
let eta_03 = mu_03 / mu_00.powf(2.5);
let eta_21 = mu_21 / mu_00.powf(2.5);
let eta_12 = mu_12 / mu_00.powf(2.5);
let hu1 = eta_20 + eta_02;
let hu2 = (eta_20 - eta_02).powi(2) + 4.0 * eta_11.powi(2);
let hu3 = (eta_30 - 3.0 * eta_12).powi(2) + (3.0 * eta_21 - eta_03).powi(2);
let hu4 = (eta_30 + eta_12).powi(2) + (eta_21 + eta_03).powi(2);
let hu5 = (eta_30 - 3.0 * eta_12)
* (eta_30 + eta_12)
* ((eta_30 + eta_12).powi(2) - 3.0 * (eta_21 + eta_03).powi(2))
+ (3.0 * eta_21 - eta_03)
* (eta_21 + eta_03)
* (3.0 * (eta_30 + eta_12).powi(2) - (eta_21 + eta_03).powi(2));
let hu6 = (eta_20 - eta_02) * ((eta_30 + eta_12).powi(2) - (eta_21 + eta_03).powi(2))
+ 4.0 * eta_11 * (eta_30 + eta_12) * (eta_21 + eta_03);
let hu7 = (3.0 * eta_21 - eta_03)
* (eta_30 + eta_12)
* ((eta_30 + eta_12).powi(2) - 3.0 * (eta_21 + eta_03).powi(2))
- (eta_30 - 3.0 * eta_12)
* (eta_21 + eta_03)
* (3.0 * (eta_30 + eta_12).powi(2) - (eta_21 + eta_03).powi(2));
Ok(vec![hu1, hu2, hu3, hu4, hu5, hu6, hu7])
}
fn compute_central_moment(
&self,
binary_image: &Array2<bool>,
centroid: (f64, f64),
p: usize,
q: usize,
) -> f64 {
let (height, width) = binary_image.dim();
let (cx, cy) = centroid;
let mut moment = 0.0;
for y in 0..height {
for x in 0..width {
if binary_image[[y, x]] {
let x_f = x as f64;
let y_f = y as f64;
moment += (x_f - cx).powi(p as i32) * (y_f - cy).powi(q as i32);
}
}
}
moment
}
fn extract_advanced_properties(&self, binary_image: &Array2<bool>, area: f64) -> Vec<f64> {
let mut features = Vec::new();
let orientation = self.compute_orientation(binary_image);
features.push(orientation);
let eccentricity = self.compute_eccentricity(binary_image);
features.push(eccentricity);
let convexity = self.compute_convexity_approximation(binary_image, area);
features.push(convexity);
let complexity = self.compute_shape_complexity(binary_image);
features.push(complexity);
features
}
fn compute_orientation(&self, binary_image: &Array2<bool>) -> f64 {
let area = self.compute_area(binary_image);
if area == 0.0 {
return 0.0;
}
let centroid = self.compute_centroid(binary_image, area);
let mu_20 = self.compute_central_moment(binary_image, centroid, 2, 0);
let mu_02 = self.compute_central_moment(binary_image, centroid, 0, 2);
let mu_11 = self.compute_central_moment(binary_image, centroid, 1, 1);
if (mu_20 - mu_02).abs() < f64::EPSILON {
std::f64::consts::PI / 4.0 } else {
0.5 * (2.0 * mu_11 / (mu_20 - mu_02)).atan()
}
}
fn compute_eccentricity(&self, binary_image: &Array2<bool>) -> f64 {
let area = self.compute_area(binary_image);
if area == 0.0 {
return 0.0;
}
let centroid = self.compute_centroid(binary_image, area);
let mu_20 = self.compute_central_moment(binary_image, centroid, 2, 0) / area;
let mu_02 = self.compute_central_moment(binary_image, centroid, 0, 2) / area;
let mu_11 = self.compute_central_moment(binary_image, centroid, 1, 1) / area;
let trace = mu_20 + mu_02;
let det = mu_20 * mu_02 - mu_11 * mu_11;
if det <= 0.0 || trace <= 0.0 {
return 0.0;
}
let discriminant = (trace * trace - 4.0 * det).max(0.0).sqrt();
let lambda1 = (trace + discriminant) / 2.0;
let lambda2 = (trace - discriminant) / 2.0;
if lambda1 <= 0.0 {
0.0
} else {
(1.0 - lambda2 / lambda1).max(0.0).sqrt()
}
}
fn compute_convexity_approximation(&self, binary_image: &Array2<bool>, area: f64) -> f64 {
if area == 0.0 {
return 0.0;
}
let perimeter = self.compute_perimeter(binary_image);
if perimeter == 0.0 {
return 1.0;
}
let ideal_perimeter = 2.0 * (std::f64::consts::PI * area).sqrt();
(ideal_perimeter / perimeter).min(1.0)
}
fn compute_shape_complexity(&self, binary_image: &Array2<bool>) -> f64 {
self.compute_shape_irregularity(binary_image)
}
fn estimate_feature_count(&self) -> usize {
let mut count = 8;
let moment_count = (self.max_moment_order + 1) * (self.max_moment_order + 2) / 2;
count += moment_count;
if self.central_moments && self.normalize {
count += moment_count; }
if self.hu_moments {
count += 7; }
if self.advanced_properties {
count += 4; }
count
}
}
impl Default for ShapeDescriptorExtractor {
fn default() -> Self {
Self::new()
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
#[test]
fn test_shape_extractor_creation() {
let extractor = ShapeDescriptorExtractor::new();
assert_eq!(extractor.max_moment_order, 3);
assert_eq!(extractor.binary_threshold, 0.5);
assert!(extractor.normalize);
assert!(extractor.central_moments);
assert!(extractor.hu_moments);
}
#[test]
fn test_shape_extractor_builder() {
let extractor = ShapeDescriptorExtractor::new()
.max_moment_order(2)
.binary_threshold(0.3)
.normalize_moments(false)
.hu_moments(false);
assert_eq!(extractor.max_moment_order, 2);
assert_eq!(extractor.binary_threshold, 0.3);
assert!(!extractor.normalize);
assert!(!extractor.hu_moments);
}
#[test]
fn test_extract_features_small_image() {
let image = Array2::from_shape_vec((2, 2), vec![0.0, 1.0, 1.0, 0.0])
.expect("operation should succeed");
let extractor = ShapeDescriptorExtractor::new();
let result = extractor.extract_features(&image.view());
assert!(result.is_err()); }
#[test]
fn test_extract_features_valid_image() {
let image = Array2::from_shape_vec((10, 10), (0..100).map(|x| x as f64 / 100.0).collect())
.expect("operation should succeed");
let extractor = ShapeDescriptorExtractor::new();
let features = extractor
.extract_features(&image.view())
.expect("operation should succeed");
assert!(!features.is_empty());
}
#[test]
fn test_binary_conversion() {
let extractor = ShapeDescriptorExtractor::new().binary_threshold(0.5);
let image =
Array2::from_shape_vec((3, 3), vec![0.0, 0.3, 0.7, 0.2, 0.6, 0.9, 0.1, 0.4, 0.8])
.expect("operation should succeed");
let binary = extractor.to_binary(&image.view());
assert!(!binary[[0, 0]]); assert!(!binary[[0, 1]]); assert!(binary[[0, 2]]); assert!(binary[[1, 2]]); }
#[test]
fn test_area_computation() {
let extractor = ShapeDescriptorExtractor::new();
let binary = Array2::from_shape_vec(
(3, 3),
vec![true, false, true, false, true, false, true, false, true],
)
.expect("operation should succeed");
let area = extractor.compute_area(&binary);
assert_eq!(area, 5.0); }
#[test]
fn test_centroid_computation() {
let extractor = ShapeDescriptorExtractor::new();
let binary = Array2::from_shape_vec(
(3, 3),
vec![true, false, false, false, true, false, false, false, true],
)
.expect("operation should succeed");
let area = 3.0;
let centroid = extractor.compute_centroid(&binary, area);
assert!((centroid.0 - 1.0).abs() < 1e-10);
assert!((centroid.1 - 1.0).abs() < 1e-10);
}
#[test]
fn test_perimeter_computation() {
let extractor = ShapeDescriptorExtractor::new();
let binary = Array2::from_shape_vec(
(5, 5),
vec![
false, false, false, false, false, false, true, true, true, false, false, true,
true, true, false, false, true, true, true, false, false, false, false, false,
false,
],
)
.expect("operation should succeed");
let perimeter = extractor.compute_perimeter(&binary);
assert!(perimeter > 0.0); }
#[test]
fn test_bounding_box() {
let extractor = ShapeDescriptorExtractor::new();
let binary = Array2::from_shape_vec(
(5, 5),
vec![
false, false, false, false, false, false, true, false, false, false, false, false,
false, true, false, false, false, false, false, false, false, false, false, false,
false,
],
)
.expect("operation should succeed");
let (min_x, max_x, min_y, max_y) = extractor.find_bounding_box(&binary);
assert_eq!(min_x, 1); assert_eq!(max_x, 3); assert_eq!(min_y, 1); assert_eq!(max_y, 2); }
#[test]
fn test_aspect_ratio() {
let extractor = ShapeDescriptorExtractor::new();
let binary = Array2::from_shape_vec(
(4, 6),
vec![
false, true, true, true, true, false, false, true, true, true, true, false, false,
false, false, false, false, false, false, false, false, false, false, false,
],
)
.expect("operation should succeed");
let aspect_ratio = extractor.compute_aspect_ratio(&binary);
assert!(aspect_ratio > 1.0); }
#[test]
fn test_central_moment() {
let extractor = ShapeDescriptorExtractor::new();
let binary = Array2::from_shape_vec(
(3, 3),
vec![true, false, false, false, true, false, false, false, true],
)
.expect("operation should succeed");
let centroid = (1.0, 1.0);
let mu_20 = extractor.compute_central_moment(&binary, centroid, 2, 0);
assert!(mu_20.is_finite());
}
#[test]
fn test_empty_image() {
let extractor = ShapeDescriptorExtractor::new();
let binary =
Array2::from_shape_vec((5, 5), vec![false; 25]).expect("operation should succeed");
let area = extractor.compute_area(&binary);
assert_eq!(area, 0.0);
let centroid = extractor.compute_centroid(&binary, area);
assert_eq!(centroid, (0.0, 0.0));
}
#[test]
fn test_hu_moments() {
let extractor = ShapeDescriptorExtractor::new();
let binary = Array2::from_shape_vec(
(5, 5),
vec![
false, false, true, false, false, false, true, true, true, false, true, true, true,
true, true, false, true, true, true, false, false, false, true, false, false,
],
)
.expect("operation should succeed");
let area = extractor.compute_area(&binary);
let centroid = extractor.compute_centroid(&binary, area);
let hu_moments = extractor
.compute_hu_moments(&binary, centroid)
.expect("operation should succeed");
assert_eq!(hu_moments.len(), 7);
assert!(hu_moments.iter().all(|x| x.is_finite()));
}
}