scirs2_ndimage/segmentation/
thresholding.rs1use crate::error::{NdimageError, NdimageResult};
6use crate::utils::safe_f64_to_float;
7use scirs2_core::ndarray::{Array, Dimension, Ix2};
8use scirs2_core::numeric::{Float, FromPrimitive, NumAssign};
9
10#[allow(dead_code)]
12fn safe_usize_to_float<T: Float + FromPrimitive>(value: usize) -> NdimageResult<T> {
13 T::from_usize(value).ok_or_else(|| {
14 NdimageError::ComputationError(format!("Failed to convert usize {} to float type", value))
15 })
16}
17
18#[allow(dead_code)]
44pub fn threshold_binary<T, D>(image: &Array<T, D>, threshold: T) -> NdimageResult<Array<T, D>>
45where
46 T: Float + NumAssign + std::fmt::Debug + std::ops::DivAssign + 'static,
47 D: Dimension + 'static,
48{
49 let result = image.mapv(|val| if val > threshold { T::one() } else { T::zero() });
51
52 Ok(result)
53}
54
55#[allow(dead_code)]
84pub fn otsu_threshold<T, D>(image: &Array<T, D>, bins: usize) -> NdimageResult<(Array<T, D>, T)>
85where
86 T: Float + NumAssign + std::fmt::Debug + std::ops::DivAssign + FromPrimitive + 'static,
87 D: Dimension + 'static,
88{
89 let nbins = bins;
90
91 let mut min_val = Float::infinity();
93 let mut max_val = Float::neg_infinity();
94
95 for &val in image.iter() {
96 if val < min_val {
97 min_val = val;
98 }
99 if val > max_val {
100 max_val = val;
101 }
102 }
103
104 if min_val == max_val {
106 let binary = threshold_binary(image, min_val)?;
108 return Ok((binary, min_val));
109 }
110
111 let mut hist = vec![0; nbins];
113 let bin_width = (max_val - min_val) / safe_usize_to_float(nbins)?;
114
115 for &val in image.iter() {
116 let bin = ((val - min_val) / bin_width).to_usize().unwrap_or(0);
117 let bin_index = std::cmp::min(bin, nbins - 1);
118 hist[bin_index] += 1;
119 }
120
121 let total_pixels = image.len();
123
124 let mut cum_sum = vec![0; nbins];
126 cum_sum[0] = hist[0];
127 for i in 1..nbins {
128 cum_sum[i] = cum_sum[i - 1] + hist[i];
129 }
130
131 let mut cum_val = vec![T::zero(); nbins];
133 for i in 0..nbins {
134 if i > 0 {
135 cum_val[i] = cum_val[i - 1] + safe_usize_to_float(i * hist[i])?;
136 } else {
137 cum_val[i] = safe_usize_to_float(i * hist[i])?
138 }
139 }
140
141 let mut max_var = T::zero();
143 let mut threshold_idx = 0;
144
145 for i in 0..(nbins - 1) {
146 let bg_pixels = cum_sum[i];
147 let fg_pixels = total_pixels - bg_pixels;
148
149 if bg_pixels == 0 || fg_pixels == 0 {
151 continue;
152 }
153
154 let bg_mean = cum_val[i] / safe_usize_to_float::<T>(bg_pixels)?;
155 let fg_mean = (cum_val[nbins - 1] - cum_val[i]) / safe_usize_to_float::<T>(fg_pixels)?;
156
157 let variance = safe_usize_to_float::<T>(bg_pixels * fg_pixels)?
159 * (bg_mean - fg_mean)
160 * (bg_mean - fg_mean);
161
162 if variance > max_var {
164 max_var = variance;
165 threshold_idx = i;
166 }
167 }
168
169 let threshold = min_val + safe_usize_to_float::<T>(threshold_idx)? * bin_width;
171
172 let binary = threshold_binary(image, threshold)?;
174
175 Ok((binary, threshold))
176}
177
178#[derive(Debug, Clone, Copy)]
209pub enum AdaptiveMethod {
210 Mean,
211 Gaussian,
212}
213
214#[allow(dead_code)]
215pub fn adaptive_threshold<T>(
216 image: &Array<T, Ix2>,
217 block_size: usize,
218 method: AdaptiveMethod,
219 c: T,
220) -> NdimageResult<Array<bool, Ix2>>
221where
222 T: Float + NumAssign + std::fmt::Debug + FromPrimitive,
223{
224 if block_size % 2 == 0 || block_size < 3 {
226 return Err(NdimageError::InvalidInput(
227 "block_size must be odd and at least 3".to_string(),
228 ));
229 }
230
231 let shape = image.raw_dim();
232 let (rows, cols) = (shape[0], shape[1]);
233 let mut result = Array::from_elem(shape, false);
234 let radius = block_size / 2;
235
236 for i in 0..rows {
238 for j in 0..cols {
239 let start_row = i.saturating_sub(radius);
241 let end_row = std::cmp::min(i + radius + 1, rows);
242 let start_col = j.saturating_sub(radius);
243 let end_col = std::cmp::min(j + radius + 1, cols);
244
245 let neighborhood = image.slice(scirs2_core::ndarray::s![
247 start_row..end_row,
248 start_col..end_col
249 ]);
250
251 let threshold = match method {
253 AdaptiveMethod::Mean => {
254 let sum = neighborhood.iter().fold(T::zero(), |acc, &x| acc + x);
256 sum / safe_usize_to_float(neighborhood.len())? - c
257 }
258 AdaptiveMethod::Gaussian => {
259 let center_row = i - start_row;
262 let center_col = j - start_col;
263
264 let mut weighted_sum = T::zero();
265 let mut weight_sum = T::zero();
266
267 for (idx, &val) in neighborhood.indexed_iter() {
268 let dist_sq = (idx.0 as isize - center_row as isize).pow(2)
269 + (idx.1 as isize - center_col as isize).pow(2);
270 let dist = safe_usize_to_float::<T>(dist_sq as usize)?.sqrt();
271
272 let sigma =
274 safe_usize_to_float::<T>(radius)? / safe_f64_to_float::<T>(2.0)?;
275 let weight =
276 (-dist * dist / (safe_f64_to_float::<T>(2.0)? * sigma * sigma)).exp();
277
278 weighted_sum += val * weight;
279 weight_sum += weight;
280 }
281
282 weighted_sum / weight_sum - c
283 }
284 };
285
286 result[(i, j)] = image[(i, j)] > threshold;
288 }
289 }
290
291 Ok(result)
292}