use crate::*;
use scirs2_core::ndarray::{Array2, ArrayView2};
#[derive(Debug, Clone)]
pub struct ContourAnalysisExtractor {
min_contour_area: f64,
max_contours: Option<usize>,
chain_approximation: ChainApproximation,
hierarchy_analysis: bool,
binary_threshold: f64,
}
#[derive(Debug, Clone, Copy)]
pub enum ChainApproximation {
None, Simple, TC89L1, TC89KCOS, }
impl ContourAnalysisExtractor {
pub fn new() -> Self {
Self {
min_contour_area: 10.0,
max_contours: None,
chain_approximation: ChainApproximation::Simple,
hierarchy_analysis: true,
binary_threshold: 0.5,
}
}
pub fn min_contour_area(mut self, min_area: f64) -> Self {
self.min_contour_area = min_area;
self
}
pub fn max_contours(mut self, max_contours: Option<usize>) -> Self {
self.max_contours = max_contours;
self
}
pub fn chain_approximation(mut self, method: ChainApproximation) -> Self {
self.chain_approximation = method;
self
}
pub fn hierarchy_analysis(mut self, enable: bool) -> Self {
self.hierarchy_analysis = enable;
self
}
pub fn binary_threshold(mut self, threshold: f64) -> Self {
self.binary_threshold = threshold;
self
}
pub fn extract_features(&self, image: &ArrayView2<f64>) -> SklResult<Vec<f64>> {
let binary_image = self.binarize_image(image);
let contours = self.find_contours(&binary_image)?;
let filtered_contours = self.filter_contours(contours);
let mut features = Vec::new();
features.push(filtered_contours.len() as f64);
let total_area: f64 = filtered_contours.iter().map(|c| c.area).sum();
features.push(total_area);
let total_perimeter: f64 = filtered_contours.iter().map(|c| c.perimeter).sum();
features.push(total_perimeter);
if !filtered_contours.is_empty() {
let areas: Vec<f64> = filtered_contours.iter().map(|c| c.area).collect();
let perimeters: Vec<f64> = filtered_contours.iter().map(|c| c.perimeter).collect();
let solidities: Vec<f64> = filtered_contours.iter().map(|c| c.solidity).collect();
let extents: Vec<f64> = filtered_contours.iter().map(|c| c.extent).collect();
let convexities: Vec<f64> = filtered_contours.iter().map(|c| c.convexity).collect();
features.push(self.mean(&areas)); features.push(self.std_dev(&areas)); features.push(
*areas
.iter()
.max_by(|a, b| a.partial_cmp(b).expect("operation should succeed"))
.expect("operation should succeed"),
); features.push(
*areas
.iter()
.min_by(|a, b| a.partial_cmp(b).expect("operation should succeed"))
.expect("operation should succeed"),
);
features.push(self.mean(&perimeters)); features.push(self.std_dev(&perimeters));
features.push(self.mean(&solidities)); features.push(self.mean(&extents)); features.push(self.mean(&convexities));
let aspect_ratios: Vec<f64> =
filtered_contours.iter().map(|c| c.aspect_ratio).collect();
features.push(self.mean(&aspect_ratios)); features.push(self.std_dev(&aspect_ratios));
let orientations: Vec<f64> = filtered_contours.iter().map(|c| c.orientation).collect();
features.push(self.mean(&orientations)); features.push(self.std_dev(&orientations)); } else {
features.resize(features.len() + 11, 0.0);
}
if self.hierarchy_analysis {
let hierarchy_features = self.analyze_hierarchy(&filtered_contours);
features.extend(hierarchy_features);
}
Ok(features)
}
fn binarize_image(&self, image: &ArrayView2<f64>) -> 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 find_contours(&self, binary_image: &Array2<bool>) -> SklResult<Vec<Contour>> {
let (height, width) = binary_image.dim();
let mut contours = Vec::new();
let mut visited = Array2::<bool>::default((height, width));
for y in 1..height - 1 {
for x in 1..width - 1 {
if binary_image[[y, x]] && !visited[[y, x]] {
if let Some(contour) = self.trace_contour(binary_image, &mut visited, x, y) {
contours.push(contour);
}
}
}
}
Ok(contours)
}
fn trace_contour(
&self,
binary_image: &Array2<bool>,
visited: &mut Array2<bool>,
start_x: usize,
start_y: usize,
) -> Option<Contour> {
let mut points = Vec::new();
let mut x = start_x;
let mut y = start_y;
let directions = [
(-1, -1),
(0, -1),
(1, -1),
(-1, 0),
(1, 0),
(-1, 1),
(0, 1),
(1, 1),
];
let mut direction_index = 0;
let start_point = (x, y);
loop {
visited[[y, x]] = true;
points.push((x as f64, y as f64));
let mut found_next = false;
for i in 0..8 {
let dir_idx = (direction_index + i) % 8;
let (dx, dy) = directions[dir_idx];
let next_x = (x as i32 + dx) as usize;
let next_y = (y as i32 + dy) as usize;
if next_x < binary_image.ncols()
&& next_y < binary_image.nrows()
&& binary_image[[next_y, next_x]]
&& !visited[[next_y, next_x]]
{
x = next_x;
y = next_y;
direction_index = dir_idx;
found_next = true;
break;
}
}
if !found_next || (x, y) == start_point {
break;
}
if points.len() > 10000 {
break;
}
}
if points.len() < 3 {
return None;
}
Some(self.create_contour_from_points(points))
}
fn create_contour_from_points(&self, points: Vec<(f64, f64)>) -> Contour {
let area = self.compute_contour_area(&points);
let perimeter = self.compute_contour_perimeter(&points);
let convex_hull = self.compute_convex_hull(&points);
let convex_area = self.compute_contour_area(&convex_hull);
let bounding_box = self.compute_bounding_box(&points);
let solidity = if convex_area > 0.0 {
area / convex_area
} else {
0.0
};
let extent = if bounding_box.2 * bounding_box.3 > 0.0 {
area / (bounding_box.2 * bounding_box.3)
} else {
0.0
};
let convexity = if perimeter > 0.0 {
self.compute_contour_perimeter(&convex_hull) / perimeter
} else {
0.0
};
let aspect_ratio = if bounding_box.3 > 0.0 {
bounding_box.2 / bounding_box.3
} else {
1.0
};
let orientation = self.compute_orientation(&points);
Contour {
points,
area,
perimeter,
convex_hull,
solidity,
extent,
convexity,
aspect_ratio,
orientation,
bounding_box,
}
}
fn compute_contour_area(&self, points: &[(f64, f64)]) -> f64 {
if points.len() < 3 {
return 0.0;
}
let mut area = 0.0;
let n = points.len();
for i in 0..n {
let j = (i + 1) % n;
area += points[i].0 * points[j].1;
area -= points[j].0 * points[i].1;
}
(area / 2.0).abs()
}
fn compute_contour_perimeter(&self, points: &[(f64, f64)]) -> f64 {
if points.len() < 2 {
return 0.0;
}
let mut perimeter = 0.0;
for i in 0..points.len() {
let j = (i + 1) % points.len();
let dx = points[j].0 - points[i].0;
let dy = points[j].1 - points[i].1;
perimeter += (dx * dx + dy * dy).sqrt();
}
perimeter
}
fn compute_convex_hull(&self, points: &[(f64, f64)]) -> Vec<(f64, f64)> {
if points.len() < 3 {
return points.to_vec();
}
let mut sorted_points = points.to_vec();
let mut min_idx = 0;
for i in 1..sorted_points.len() {
if sorted_points[i].1 < sorted_points[min_idx].1
|| (sorted_points[i].1 == sorted_points[min_idx].1
&& sorted_points[i].0 < sorted_points[min_idx].0)
{
min_idx = i;
}
}
sorted_points.swap(0, min_idx);
let p0 = sorted_points[0];
sorted_points[1..].sort_by(|a, b| {
let cross = self.cross_product(p0, *a, *b);
if cross == 0.0 {
let dist_a = (a.0 - p0.0).powi(2) + (a.1 - p0.1).powi(2);
let dist_b = (b.0 - p0.0).powi(2) + (b.1 - p0.1).powi(2);
dist_a
.partial_cmp(&dist_b)
.expect("operation should succeed")
} else {
cross
.partial_cmp(&0.0)
.expect("operation should succeed")
.reverse()
}
});
let mut hull = Vec::new();
for point in sorted_points {
while hull.len() > 1
&& self.cross_product(hull[hull.len() - 2], hull[hull.len() - 1], point) <= 0.0
{
hull.pop();
}
hull.push(point);
}
hull
}
fn cross_product(&self, o: (f64, f64), a: (f64, f64), b: (f64, f64)) -> f64 {
(a.0 - o.0) * (b.1 - o.1) - (a.1 - o.1) * (b.0 - o.0)
}
fn compute_bounding_box(&self, points: &[(f64, f64)]) -> (f64, f64, f64, f64) {
if points.is_empty() {
return (0.0, 0.0, 0.0, 0.0);
}
let mut min_x = points[0].0;
let mut max_x = points[0].0;
let mut min_y = points[0].1;
let mut max_y = points[0].1;
for &(x, y) in points.iter().skip(1) {
min_x = min_x.min(x);
max_x = max_x.max(x);
min_y = min_y.min(y);
max_y = max_y.max(y);
}
(min_x, min_y, max_x - min_x, max_y - min_y)
}
fn compute_orientation(&self, points: &[(f64, f64)]) -> f64 {
if points.len() < 2 {
return 0.0;
}
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let n = points.len() as f64;
for &(x, y) in points {
sum_x += x;
sum_y += y;
}
let mean_x = sum_x / n;
let mean_y = sum_y / n;
let mut m11 = 0.0;
let mut m20 = 0.0;
let mut m02 = 0.0;
for &(x, y) in points {
let dx = x - mean_x;
let dy = y - mean_y;
m11 += dx * dy;
m20 += dx * dx;
m02 += dy * dy;
}
if m20 == m02 {
0.0
} else {
0.5 * (2.0 * m11).atan2(m20 - m02)
}
}
fn filter_contours(&self, mut contours: Vec<Contour>) -> Vec<Contour> {
contours.retain(|c| c.area >= self.min_contour_area);
contours.sort_by(|a, b| {
b.area
.partial_cmp(&a.area)
.expect("operation should succeed")
});
if let Some(max_count) = self.max_contours {
contours.truncate(max_count);
}
contours
}
fn analyze_hierarchy(&self, contours: &[Contour]) -> Vec<f64> {
let mut hierarchy_features = Vec::new();
let mut hierarchy_levels = Vec::new();
for (i, contour_i) in contours.iter().enumerate() {
let mut level = 0;
for (j, contour_j) in contours.iter().enumerate() {
if i != j && self.is_contour_inside(contour_i, contour_j) {
level += 1;
}
}
hierarchy_levels.push(level as f64);
}
if !hierarchy_levels.is_empty() {
hierarchy_features.push(
*hierarchy_levels
.iter()
.max_by(|a, b| a.partial_cmp(b).expect("operation should succeed"))
.expect("operation should succeed"),
); hierarchy_features.push(self.mean(&hierarchy_levels)); hierarchy_features.push(self.std_dev(&hierarchy_levels)); } else {
hierarchy_features.extend(vec![0.0, 0.0, 0.0]);
}
hierarchy_features
}
fn is_contour_inside(&self, inner: &Contour, outer: &Contour) -> bool {
if inner.area >= outer.area {
return false;
}
let centroid = self.compute_centroid(&inner.points);
self.point_in_polygon(centroid, &outer.points)
}
fn compute_centroid(&self, points: &[(f64, f64)]) -> (f64, f64) {
let n = points.len() as f64;
let sum_x: f64 = points.iter().map(|p| p.0).sum();
let sum_y: f64 = points.iter().map(|p| p.1).sum();
(sum_x / n, sum_y / n)
}
fn point_in_polygon(&self, point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
let mut inside = false;
let n = polygon.len();
let mut j = n - 1;
for i in 0..n {
if ((polygon[i].1 > point.1) != (polygon[j].1 > point.1))
&& (point.0
< (polygon[j].0 - polygon[i].0) * (point.1 - polygon[i].1)
/ (polygon[j].1 - polygon[i].1)
+ polygon[i].0)
{
inside = !inside;
}
j = i;
}
inside
}
fn mean(&self, values: &[f64]) -> f64 {
if values.is_empty() {
0.0
} else {
values.iter().sum::<f64>() / values.len() as f64
}
}
fn std_dev(&self, values: &[f64]) -> f64 {
if values.len() < 2 {
return 0.0;
}
let mean = self.mean(values);
let variance =
values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
variance.sqrt()
}
}
impl Default for ContourAnalysisExtractor {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct Contour {
pub points: Vec<(f64, f64)>,
pub area: f64,
pub perimeter: f64,
pub convex_hull: Vec<(f64, f64)>,
pub solidity: f64, pub extent: f64, pub convexity: f64, pub aspect_ratio: f64, pub orientation: f64, pub bounding_box: (f64, f64, f64, f64), }
#[derive(Debug, Clone)]
pub struct MorphologicalFeaturesExtractor {
kernel_size: usize,
kernel_shape: KernelShape,
operations: Vec<MorphOperation>,
multi_scale: bool,
binary_threshold: f64,
scale_range: (usize, usize),
}
#[derive(Debug, Clone, Copy)]
pub enum KernelShape {
Rectangular,
Elliptical,
Cross,
Diamond,
}
#[derive(Debug, Clone, Copy)]
pub enum MorphOperation {
Erosion,
Dilation,
Opening,
Closing,
Gradient,
TopHat,
BlackHat,
}
impl MorphologicalFeaturesExtractor {
pub fn new() -> Self {
Self {
kernel_size: 3,
kernel_shape: KernelShape::Elliptical,
operations: vec![
MorphOperation::Erosion,
MorphOperation::Dilation,
MorphOperation::Opening,
MorphOperation::Closing,
MorphOperation::Gradient,
],
multi_scale: true,
binary_threshold: 0.5,
scale_range: (3, 15),
}
}
pub fn kernel_size(mut self, size: usize) -> Self {
self.kernel_size = size;
self
}
pub fn kernel_shape(mut self, shape: KernelShape) -> Self {
self.kernel_shape = shape;
self
}
pub fn operations(mut self, operations: Vec<MorphOperation>) -> Self {
self.operations = operations;
self
}
pub fn multi_scale(mut self, enable: bool) -> Self {
self.multi_scale = enable;
self
}
pub fn binary_threshold(mut self, threshold: f64) -> Self {
self.binary_threshold = threshold;
self
}
pub fn scale_range(mut self, range: (usize, usize)) -> Self {
self.scale_range = range;
self
}
pub fn extract_features(&self, image: &ArrayView2<f64>) -> SklResult<Vec<f64>> {
let binary_image = self.binarize_image(image);
let mut features = Vec::new();
if self.multi_scale {
for size in (self.scale_range.0..=self.scale_range.1).step_by(2) {
let scale_features = self.extract_features_at_scale(&binary_image, size)?;
features.extend(scale_features);
}
let granulometry_features = self.compute_granulometry(&binary_image)?;
features.extend(granulometry_features);
} else {
let single_scale_features =
self.extract_features_at_scale(&binary_image, self.kernel_size)?;
features.extend(single_scale_features);
}
Ok(features)
}
fn binarize_image(&self, image: &ArrayView2<f64>) -> 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 extract_features_at_scale(
&self,
binary_image: &Array2<bool>,
kernel_size: usize,
) -> SklResult<Vec<f64>> {
let kernel = self.create_kernel(kernel_size);
let mut features = Vec::new();
for &operation in &self.operations {
let result = self.apply_morphological_operation(binary_image, &kernel, operation)?;
let operation_features = self.compute_operation_statistics(&result);
features.extend(operation_features);
}
Ok(features)
}
fn create_kernel(&self, size: usize) -> Array2<bool> {
let mut kernel = Array2::default((size, size));
let center = size / 2;
match self.kernel_shape {
KernelShape::Rectangular => {
kernel.fill(true);
}
KernelShape::Elliptical => {
let radius_sq = (center as f64).powi(2);
for y in 0..size {
for x in 0..size {
let dx = x as f64 - center as f64;
let dy = y as f64 - center as f64;
kernel[[y, x]] = (dx * dx + dy * dy) <= radius_sq;
}
}
}
KernelShape::Cross => {
for i in 0..size {
kernel[[center, i]] = true;
kernel[[i, center]] = true;
}
}
KernelShape::Diamond => {
for y in 0..size {
for x in 0..size {
let dx = (x as i32 - center as i32).abs();
let dy = (y as i32 - center as i32).abs();
kernel[[y, x]] = dx + dy <= center as i32;
}
}
}
}
kernel
}
fn apply_morphological_operation(
&self,
image: &Array2<bool>,
kernel: &Array2<bool>,
operation: MorphOperation,
) -> SklResult<Array2<bool>> {
match operation {
MorphOperation::Erosion => self.erosion(image, kernel),
MorphOperation::Dilation => self.dilation(image, kernel),
MorphOperation::Opening => {
let eroded = self.erosion(image, kernel)?;
self.dilation(&eroded, kernel)
}
MorphOperation::Closing => {
let dilated = self.dilation(image, kernel)?;
self.erosion(&dilated, kernel)
}
MorphOperation::Gradient => {
let dilated = self.dilation(image, kernel)?;
let eroded = self.erosion(image, kernel)?;
self.subtract(&dilated, &eroded)
}
MorphOperation::TopHat => {
let opened = {
let eroded = self.erosion(image, kernel)?;
self.dilation(&eroded, kernel)?
};
self.subtract(image, &opened)
}
MorphOperation::BlackHat => {
let closed = {
let dilated = self.dilation(image, kernel)?;
self.erosion(&dilated, kernel)?
};
self.subtract(&closed, image)
}
}
}
fn erosion(&self, image: &Array2<bool>, kernel: &Array2<bool>) -> SklResult<Array2<bool>> {
let (img_height, img_width) = image.dim();
let (ker_height, ker_width) = kernel.dim();
let mut result = Array2::default((img_height, img_width));
let ker_center_y = ker_height / 2;
let ker_center_x = ker_width / 2;
for y in 0..img_height {
for x in 0..img_width {
let mut min_val = true;
for ky in 0..ker_height {
for kx in 0..ker_width {
if kernel[[ky, kx]] {
let img_y = y as i32 + ky as i32 - ker_center_y as i32;
let img_x = x as i32 + kx as i32 - ker_center_x as i32;
if img_y >= 0
&& img_y < img_height as i32
&& img_x >= 0
&& img_x < img_width as i32
{
if !image[[img_y as usize, img_x as usize]] {
min_val = false;
break;
}
} else {
min_val = false;
break;
}
}
}
if !min_val {
break;
}
}
result[[y, x]] = min_val;
}
}
Ok(result)
}
fn dilation(&self, image: &Array2<bool>, kernel: &Array2<bool>) -> SklResult<Array2<bool>> {
let (img_height, img_width) = image.dim();
let (ker_height, ker_width) = kernel.dim();
let mut result = Array2::default((img_height, img_width));
let ker_center_y = ker_height / 2;
let ker_center_x = ker_width / 2;
for y in 0..img_height {
for x in 0..img_width {
let mut max_val = false;
for ky in 0..ker_height {
for kx in 0..ker_width {
if kernel[[ky, kx]] {
let img_y = y as i32 + ky as i32 - ker_center_y as i32;
let img_x = x as i32 + kx as i32 - ker_center_x as i32;
if img_y >= 0
&& img_y < img_height as i32
&& img_x >= 0
&& img_x < img_width as i32
&& image[[img_y as usize, img_x as usize]]
{
max_val = true;
break;
}
}
}
if max_val {
break;
}
}
result[[y, x]] = max_val;
}
}
Ok(result)
}
fn subtract(&self, image1: &Array2<bool>, image2: &Array2<bool>) -> SklResult<Array2<bool>> {
let (height, width) = image1.dim();
let mut result = Array2::default((height, width));
for y in 0..height {
for x in 0..width {
result[[y, x]] = image1[[y, x]] && !image2[[y, x]];
}
}
Ok(result)
}
fn compute_operation_statistics(&self, image: &Array2<bool>) -> Vec<f64> {
let mut features = Vec::new();
let true_count = image.iter().filter(|&&x| x).count() as f64;
let total_pixels = (image.nrows() * image.ncols()) as f64;
features.push(true_count); features.push(true_count / total_pixels);
let components = self.count_connected_components(image);
features.push(components as f64);
features
}
fn count_connected_components(&self, image: &Array2<bool>) -> usize {
let (height, width) = image.dim();
let mut visited = Array2::<bool>::default((height, width));
let mut count = 0;
for y in 0..height {
for x in 0..width {
if image[[y, x]] && !visited[[y, x]] {
self.flood_fill(&mut visited, image, x, y);
count += 1;
}
}
}
count
}
fn flood_fill(&self, visited: &mut Array2<bool>, image: &Array2<bool>, x: usize, y: usize) {
let (height, width) = image.dim();
let mut stack = vec![(x, y)];
while let Some((cx, cy)) = stack.pop() {
if visited[[cy, cx]] || !image[[cy, cx]] {
continue;
}
visited[[cy, cx]] = true;
for (dx, dy) in [(-1, 0), (1, 0), (0, -1), (0, 1)].iter() {
let nx = cx as i32 + dx;
let ny = cy as i32 + dy;
if nx >= 0 && nx < width as i32 && ny >= 0 && ny < height as i32 {
let nx = nx as usize;
let ny = ny as usize;
if !visited[[ny, nx]] && image[[ny, nx]] {
stack.push((nx, ny));
}
}
}
}
}
fn compute_granulometry(&self, image: &Array2<bool>) -> SklResult<Vec<f64>> {
let mut granulometry = Vec::new();
let original_area = image.iter().filter(|&&x| x).count() as f64;
for size in (self.scale_range.0..=self.scale_range.1).step_by(2) {
let kernel = self.create_kernel(size);
let opened = {
let eroded = self.erosion(image, &kernel)?;
self.dilation(&eroded, &kernel)?
};
let remaining_area = opened.iter().filter(|&&x| x).count() as f64;
let area_ratio = if original_area > 0.0 {
remaining_area / original_area
} else {
0.0
};
granulometry.push(area_ratio);
}
let mut features = Vec::new();
if !granulometry.is_empty() {
features.push(self.mean(&granulometry)); features.push(self.std_dev(&granulometry));
let mut pattern_spectrum = Vec::new();
for i in 1..granulometry.len() {
pattern_spectrum.push(granulometry[i - 1] - granulometry[i]);
}
if !pattern_spectrum.is_empty() {
features.push(self.mean(&pattern_spectrum)); features.push(self.std_dev(&pattern_spectrum));
let max_idx = pattern_spectrum
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("operation should succeed"))
.map(|(i, _)| i)
.unwrap_or(0);
features.push((max_idx + 1) as f64); } else {
features.extend(vec![0.0, 0.0, 0.0]);
}
} else {
features.extend(vec![0.0, 0.0, 0.0, 0.0, 0.0]);
}
Ok(features)
}
fn mean(&self, values: &[f64]) -> f64 {
if values.is_empty() {
0.0
} else {
values.iter().sum::<f64>() / values.len() as f64
}
}
fn std_dev(&self, values: &[f64]) -> f64 {
if values.len() < 2 {
return 0.0;
}
let mean = self.mean(values);
let variance =
values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
variance.sqrt()
}
}
impl Default for MorphologicalFeaturesExtractor {
fn default() -> Self {
Self::new()
}
}