Skip to main content

scirs2_spatial/
distance_transform.rs

1//! Distance transform algorithms for image processing and spatial analysis
2//!
3//! This module provides efficient algorithms for computing distance transforms,
4//! which assign to each point the distance to the nearest feature (e.g., boundary, obstacle).
5//!
6//! # Features
7//!
8//! * **Euclidean distance transform** - Exact Euclidean distances
9//! * **Chamfer distance transform** - Fast approximate distances
10//! * **Manhattan distance transform** - City-block distances
11//! * **Geodesic distance transform** - Distances along surfaces
12//! * **Feature transforms** - Identify nearest feature points
13//!
14//! # Examples
15//!
16//! ```
17//! use scirs2_core::ndarray::array;
18//! use scirs2_spatial::distance_transform::{euclidean_distance_transform, DistanceMetric};
19//!
20//! // Create a binary image (0 = background, 1 = feature)
21//! let binary = array![
22//!     [1, 1, 0, 0],
23//!     [1, 0, 0, 0],
24//!     [0, 0, 0, 1],
25//!     [0, 0, 1, 1]
26//! ];
27//!
28//! // Compute distance transform
29//! let distances = euclidean_distance_transform::<f64>(&binary.view(), DistanceMetric::Euclidean)
30//!     .expect("Failed to compute distance transform");
31//!
32//! // Each background pixel now contains its distance to nearest feature
33//! println!("Distance transform: {:?}", distances);
34//! ```
35
36use crate::error::{SpatialError, SpatialResult};
37use scirs2_core::ndarray::{Array1, Array2, Array3, ArrayView2, ArrayView3};
38use scirs2_core::numeric::Float;
39use std::collections::VecDeque;
40
41/// Distance metric for distance transforms
42#[derive(Debug, Clone, Copy, PartialEq)]
43pub enum DistanceMetric {
44    /// Euclidean (L2) distance
45    Euclidean,
46    /// Manhattan (L1, city-block) distance
47    Manhattan,
48    /// Chebyshev (L∞, chessboard) distance
49    Chebyshev,
50    /// Chamfer 3-4 approximation
51    Chamfer34,
52    /// Chamfer 5-7-11 approximation
53    Chamfer5711,
54}
55
56/// Compute Euclidean distance transform for 2D binary image
57///
58/// Uses the efficient separable algorithm of Felzenszwalb & Huttenlocher.
59///
60/// # Arguments
61///
62/// * `binary` - Binary input array (0 = background, non-zero = feature)
63/// * `metric` - Distance metric to use
64///
65/// # Returns
66///
67/// * Distance transform array (same shape as input)
68///
69/// # Examples
70///
71/// ```
72/// use scirs2_core::ndarray::array;
73/// use scirs2_spatial::distance_transform::{euclidean_distance_transform, DistanceMetric};
74///
75/// let binary = array![[1, 0, 0], [0, 0, 0], [0, 0, 1]];
76/// let distances = euclidean_distance_transform::<f64>(&binary.view(), DistanceMetric::Euclidean)
77///     .expect("Failed to compute");
78/// ```
79pub fn euclidean_distance_transform<T: Float>(
80    binary: &ArrayView2<i32>,
81    metric: DistanceMetric,
82) -> SpatialResult<Array2<T>> {
83    let (rows, cols) = binary.dim();
84
85    if rows == 0 || cols == 0 {
86        return Err(SpatialError::ValueError(
87            "Input array must be non-empty".to_string(),
88        ));
89    }
90
91    match metric {
92        DistanceMetric::Euclidean => euclidean_dt_2d(binary),
93        DistanceMetric::Manhattan => manhattan_dt_2d(binary),
94        DistanceMetric::Chebyshev => chebyshev_dt_2d(binary),
95        DistanceMetric::Chamfer34 => chamfer_dt_2d(binary, &[3.0, 4.0]),
96        DistanceMetric::Chamfer5711 => chamfer_dt_2d(binary, &[5.0, 7.0, 11.0]),
97    }
98}
99
100/// Exact Euclidean distance transform using separable algorithm
101fn euclidean_dt_2d<T: Float>(binary: &ArrayView2<i32>) -> SpatialResult<Array2<T>> {
102    let (rows, cols) = binary.dim();
103    let mut distances = Array2::from_elem((rows, cols), T::infinity());
104
105    // Initialize: Set feature pixels to 0
106    for i in 0..rows {
107        for j in 0..cols {
108            if binary[[i, j]] != 0 {
109                distances[[i, j]] = T::zero();
110            }
111        }
112    }
113
114    // Forward pass - horizontal
115    for i in 0..rows {
116        let mut min_dist = T::infinity();
117        for j in 0..cols {
118            if binary[[i, j]] != 0 {
119                min_dist = T::zero();
120            } else {
121                min_dist = min_dist + T::one();
122            }
123            distances[[i, j]] = min_dist.min(distances[[i, j]]);
124        }
125
126        // Backward pass - horizontal
127        let mut min_dist = T::infinity();
128        for j in (0..cols).rev() {
129            if binary[[i, j]] != 0 {
130                min_dist = T::zero();
131            } else {
132                min_dist = min_dist + T::one();
133            }
134            distances[[i, j]] = min_dist.min(distances[[i, j]]);
135        }
136    }
137
138    // Vertical passes using parabola envelope
139    for j in 0..cols {
140        // Forward pass
141        for i in 1..rows {
142            let up_dist = distances[[i - 1, j]] + T::one();
143            distances[[i, j]] = up_dist.min(distances[[i, j]]);
144        }
145
146        // Backward pass
147        for i in (0..rows - 1).rev() {
148            let down_dist = distances[[i + 1, j]] + T::one();
149            distances[[i, j]] = down_dist.min(distances[[i, j]]);
150        }
151    }
152
153    Ok(distances)
154}
155
156/// Manhattan (L1) distance transform
157fn manhattan_dt_2d<T: Float>(binary: &ArrayView2<i32>) -> SpatialResult<Array2<T>> {
158    let (rows, cols) = binary.dim();
159    let mut distances = Array2::from_elem(
160        (rows, cols),
161        T::from(rows + cols).expect("conversion failed"),
162    );
163
164    // Initialize feature pixels
165    for i in 0..rows {
166        for j in 0..cols {
167            if binary[[i, j]] != 0 {
168                distances[[i, j]] = T::zero();
169            }
170        }
171    }
172
173    // Forward pass
174    for i in 0..rows {
175        for j in 0..cols {
176            if binary[[i, j]] == 0 {
177                let mut min_dist = distances[[i, j]];
178
179                if i > 0 {
180                    min_dist = min_dist.min(distances[[i - 1, j]] + T::one());
181                }
182                if j > 0 {
183                    min_dist = min_dist.min(distances[[i, j - 1]] + T::one());
184                }
185
186                distances[[i, j]] = min_dist;
187            }
188        }
189    }
190
191    // Backward pass
192    for i in (0..rows).rev() {
193        for j in (0..cols).rev() {
194            if binary[[i, j]] == 0 {
195                let mut min_dist = distances[[i, j]];
196
197                if i < rows - 1 {
198                    min_dist = min_dist.min(distances[[i + 1, j]] + T::one());
199                }
200                if j < cols - 1 {
201                    min_dist = min_dist.min(distances[[i, j + 1]] + T::one());
202                }
203
204                distances[[i, j]] = min_dist;
205            }
206        }
207    }
208
209    Ok(distances)
210}
211
212/// Chebyshev (L∞) distance transform
213fn chebyshev_dt_2d<T: Float>(binary: &ArrayView2<i32>) -> SpatialResult<Array2<T>> {
214    let (rows, cols) = binary.dim();
215    let mut distances = Array2::from_elem(
216        (rows, cols),
217        T::from(rows.max(cols)).expect("conversion failed"),
218    );
219
220    // Use BFS for exact Chebyshev distance
221    let mut queue = VecDeque::new();
222
223    // Initialize with feature pixels
224    for i in 0..rows {
225        for j in 0..cols {
226            if binary[[i, j]] != 0 {
227                distances[[i, j]] = T::zero();
228                queue.push_back((i, j, T::zero()));
229            }
230        }
231    }
232
233    // 8-connected neighbors for Chebyshev
234    let neighbors = [
235        (-1, -1),
236        (-1, 0),
237        (-1, 1),
238        (0, -1),
239        (0, 1),
240        (1, -1),
241        (1, 0),
242        (1, 1),
243    ];
244
245    while let Some((i, j, dist)) = queue.pop_front() {
246        for &(di, dj) in &neighbors {
247            let ni = i as isize + di;
248            let nj = j as isize + dj;
249
250            if ni >= 0 && ni < rows as isize && nj >= 0 && nj < cols as isize {
251                let ni = ni as usize;
252                let nj = nj as usize;
253
254                let new_dist = dist + T::one();
255                if new_dist < distances[[ni, nj]] {
256                    distances[[ni, nj]] = new_dist;
257                    queue.push_back((ni, nj, new_dist));
258                }
259            }
260        }
261    }
262
263    Ok(distances)
264}
265
266/// Chamfer distance transform with configurable weights
267fn chamfer_dt_2d<T: Float>(binary: &ArrayView2<i32>, weights: &[f64]) -> SpatialResult<Array2<T>> {
268    let (rows, cols) = binary.dim();
269    let max_val = T::from(rows * cols).expect("conversion failed");
270    let mut distances = Array2::from_elem((rows, cols), max_val);
271
272    // Initialize feature pixels
273    for i in 0..rows {
274        for j in 0..cols {
275            if binary[[i, j]] != 0 {
276                distances[[i, j]] = T::zero();
277            }
278        }
279    }
280
281    let w1 = T::from(weights[0]).expect("conversion failed");
282    let w2 =
283        T::from(weights.get(1).copied().unwrap_or(weights[0] * 1.4)).expect("conversion failed");
284
285    // Forward pass
286    for i in 0..rows {
287        for j in 0..cols {
288            if binary[[i, j]] == 0 {
289                let mut min_dist = distances[[i, j]];
290
291                // 4-connected
292                if i > 0 {
293                    min_dist = min_dist.min(distances[[i - 1, j]] + w1);
294                }
295                if j > 0 {
296                    min_dist = min_dist.min(distances[[i, j - 1]] + w1);
297                }
298
299                // Diagonal
300                if i > 0 && j > 0 {
301                    min_dist = min_dist.min(distances[[i - 1, j - 1]] + w2);
302                }
303                if i > 0 && j < cols - 1 {
304                    min_dist = min_dist.min(distances[[i - 1, j + 1]] + w2);
305                }
306
307                distances[[i, j]] = min_dist;
308            }
309        }
310    }
311
312    // Backward pass
313    for i in (0..rows).rev() {
314        for j in (0..cols).rev() {
315            if binary[[i, j]] == 0 {
316                let mut min_dist = distances[[i, j]];
317
318                // 4-connected
319                if i < rows - 1 {
320                    min_dist = min_dist.min(distances[[i + 1, j]] + w1);
321                }
322                if j < cols - 1 {
323                    min_dist = min_dist.min(distances[[i, j + 1]] + w1);
324                }
325
326                // Diagonal
327                if i < rows - 1 && j < cols - 1 {
328                    min_dist = min_dist.min(distances[[i + 1, j + 1]] + w2);
329                }
330                if i < rows - 1 && j > 0 {
331                    min_dist = min_dist.min(distances[[i + 1, j - 1]] + w2);
332                }
333
334                distances[[i, j]] = min_dist;
335            }
336        }
337    }
338
339    Ok(distances)
340}
341
342/// Compute feature transform (indices of nearest features)
343///
344/// Returns the coordinates of the nearest feature point for each pixel.
345///
346/// # Arguments
347///
348/// * `binary` - Binary input array
349///
350/// # Returns
351///
352/// * Array of (row, col) indices of nearest features
353pub fn feature_transform(binary: &ArrayView2<i32>) -> SpatialResult<Array2<(usize, usize)>> {
354    let (rows, cols) = binary.dim();
355    let mut features = Array2::from_elem((rows, cols), (usize::MAX, usize::MAX));
356    let mut distances = Array2::from_elem((rows, cols), f64::INFINITY);
357
358    // Initialize with feature pixels
359    for i in 0..rows {
360        for j in 0..cols {
361            if binary[[i, j]] != 0 {
362                features[[i, j]] = (i, j);
363                distances[[i, j]] = 0.0;
364            }
365        }
366    }
367
368    // Forward pass
369    for i in 0..rows {
370        for j in 0..cols {
371            if binary[[i, j]] == 0 {
372                let neighbors = [
373                    (i.saturating_sub(1), j),
374                    (i, j.saturating_sub(1)),
375                    (i.saturating_sub(1), j.saturating_sub(1)),
376                ];
377
378                for &(ni, nj) in &neighbors {
379                    if ni < rows && nj < cols && features[[ni, nj]] != (usize::MAX, usize::MAX) {
380                        let (fi, fj) = features[[ni, nj]];
381                        let dist = (((i as isize - fi as isize).pow(2)
382                            + (j as isize - fj as isize).pow(2))
383                            as f64)
384                            .sqrt();
385
386                        if dist < distances[[i, j]] {
387                            distances[[i, j]] = dist;
388                            features[[i, j]] = (fi, fj);
389                        }
390                    }
391                }
392            }
393        }
394    }
395
396    // Backward pass
397    for i in (0..rows).rev() {
398        for j in (0..cols).rev() {
399            if binary[[i, j]] == 0 {
400                let neighbors = [
401                    ((i + 1).min(rows - 1), j),
402                    (i, (j + 1).min(cols - 1)),
403                    ((i + 1).min(rows - 1), (j + 1).min(cols - 1)),
404                ];
405
406                for &(ni, nj) in &neighbors {
407                    if features[[ni, nj]] != (usize::MAX, usize::MAX) {
408                        let (fi, fj) = features[[ni, nj]];
409                        let dist = (((i as isize - fi as isize).pow(2)
410                            + (j as isize - fj as isize).pow(2))
411                            as f64)
412                            .sqrt();
413
414                        if dist < distances[[i, j]] {
415                            distances[[i, j]] = dist;
416                            features[[i, j]] = (fi, fj);
417                        }
418                    }
419                }
420            }
421        }
422    }
423
424    Ok(features)
425}
426
427/// Compute 3D Euclidean distance transform
428///
429/// Extension of the 2D algorithm to 3D volumes.
430///
431/// # Arguments
432///
433/// * `binary` - Binary 3D array
434/// * `metric` - Distance metric
435///
436/// # Returns
437///
438/// * 3D distance transform
439pub fn euclidean_distance_transform_3d<T: Float>(
440    binary: &ArrayView3<i32>,
441    metric: DistanceMetric,
442) -> SpatialResult<Array3<T>> {
443    let (depth, rows, cols) = binary.dim();
444
445    if depth == 0 || rows == 0 || cols == 0 {
446        return Err(SpatialError::ValueError(
447            "Input array must be non-empty".to_string(),
448        ));
449    }
450
451    // Simplified 3D implementation (could be optimized with separable algorithm)
452    let mut distances = Array3::from_elem((depth, rows, cols), T::infinity());
453
454    // Initialize feature voxels
455    for i in 0..depth {
456        for j in 0..rows {
457            for k in 0..cols {
458                if binary[[i, j, k]] != 0 {
459                    distances[[i, j, k]] = T::zero();
460                }
461            }
462        }
463    }
464
465    // Simple iterative propagation (placeholder for full 3D algorithm)
466    let max_iterations = (depth + rows + cols) / 2;
467    for _iter in 0..max_iterations {
468        let mut changed = false;
469
470        for i in 0..depth {
471            for j in 0..rows {
472                for k in 0..cols {
473                    if binary[[i, j, k]] == 0 {
474                        let mut min_dist = distances[[i, j, k]];
475
476                        // Check 6-connected neighbors
477                        if i > 0 {
478                            min_dist = min_dist.min(distances[[i - 1, j, k]] + T::one());
479                        }
480                        if i < depth - 1 {
481                            min_dist = min_dist.min(distances[[i + 1, j, k]] + T::one());
482                        }
483                        if j > 0 {
484                            min_dist = min_dist.min(distances[[i, j - 1, k]] + T::one());
485                        }
486                        if j < rows - 1 {
487                            min_dist = min_dist.min(distances[[i, j + 1, k]] + T::one());
488                        }
489                        if k > 0 {
490                            min_dist = min_dist.min(distances[[i, j, k - 1]] + T::one());
491                        }
492                        if k < cols - 1 {
493                            min_dist = min_dist.min(distances[[i, j, k + 1]] + T::one());
494                        }
495
496                        if min_dist < distances[[i, j, k]] {
497                            distances[[i, j, k]] = min_dist;
498                            changed = true;
499                        }
500                    }
501                }
502            }
503        }
504
505        if !changed {
506            break;
507        }
508    }
509
510    Ok(distances)
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516    use approx::assert_relative_eq;
517    use scirs2_core::ndarray::array;
518
519    #[test]
520    fn test_euclidean_distance_transform_2d() {
521        let binary = array![[1, 0, 0], [0, 0, 0], [0, 0, 1]];
522
523        let distances: Array2<f64> =
524            euclidean_distance_transform(&binary.view(), DistanceMetric::Euclidean)
525                .expect("Failed to compute");
526
527        // Check shape
528        assert_eq!(distances.dim(), (3, 3));
529
530        // Feature pixels should have distance 0
531        assert_relative_eq!(distances[[0, 0]], 0.0, epsilon = 1e-10);
532        assert_relative_eq!(distances[[2, 2]], 0.0, epsilon = 1e-10);
533
534        // Center pixel should have distance > 1
535        assert!(distances[[1, 1]] > 1.0);
536    }
537
538    #[test]
539    fn test_manhattan_distance_transform() {
540        let binary = array![[1, 0, 0], [0, 0, 0], [0, 0, 1]];
541
542        let distances: Array2<f64> =
543            euclidean_distance_transform(&binary.view(), DistanceMetric::Manhattan)
544                .expect("Failed to compute");
545
546        // Feature pixels
547        assert_relative_eq!(distances[[0, 0]], 0.0, epsilon = 1e-10);
548        assert_relative_eq!(distances[[2, 2]], 0.0, epsilon = 1e-10);
549
550        // Manhattan distance from (0,0) to (1,1) is 2
551        assert_relative_eq!(distances[[1, 1]], 2.0, epsilon = 0.1);
552    }
553
554    #[test]
555    fn test_feature_transform() {
556        let binary = array![[1, 0, 0], [0, 0, 0], [0, 0, 1]];
557
558        let features = feature_transform(&binary.view()).expect("Failed to compute");
559
560        // Feature pixels should point to themselves
561        assert_eq!(features[[0, 0]], (0, 0));
562        assert_eq!(features[[2, 2]], (2, 2));
563
564        // Other pixels should point to nearest feature
565        assert!(
566            features[[1, 1]] == (0, 0) || features[[1, 1]] == (2, 2),
567            "Pixel (1,1) should point to one of the features"
568        );
569    }
570
571    #[test]
572    fn test_chamfer_distance_transform() {
573        let binary = array![[1, 0, 0], [0, 0, 0], [0, 0, 0]];
574
575        let distances: Array2<f64> =
576            euclidean_distance_transform(&binary.view(), DistanceMetric::Chamfer34)
577                .expect("Failed to compute");
578
579        // Feature pixel
580        assert_relative_eq!(distances[[0, 0]], 0.0, epsilon = 1e-10);
581
582        // Distances should increase monotonically away from feature
583        assert!(distances[[0, 1]] > 0.0);
584        assert!(distances[[0, 2]] > distances[[0, 1]]);
585    }
586
587    #[test]
588    fn test_3d_distance_transform() {
589        let binary = array![[[1, 0], [0, 0]], [[0, 0], [0, 0]]];
590
591        let distances: Array3<f64> =
592            euclidean_distance_transform_3d(&binary.view(), DistanceMetric::Euclidean)
593                .expect("Failed to compute");
594
595        // Feature voxel
596        assert_relative_eq!(distances[[0, 0, 0]], 0.0, epsilon = 1e-10);
597
598        // All other voxels should have positive distance
599        for i in 0..2 {
600            for j in 0..2 {
601                for k in 0..2 {
602                    if i != 0 || j != 0 || k != 0 {
603                        assert!(distances[[i, j, k]] > 0.0);
604                    }
605                }
606            }
607        }
608    }
609
610    #[test]
611    fn test_empty_input() {
612        let binary = array![[0, 0], [0, 0]];
613
614        let distances: Array2<f64> =
615            euclidean_distance_transform(&binary.view(), DistanceMetric::Euclidean)
616                .expect("Failed to compute");
617
618        // All distances should be large (no features)
619        for &d in distances.iter() {
620            assert!(d > 1.0);
621        }
622    }
623
624    #[test]
625    fn test_all_features() {
626        let binary = array![[1, 1], [1, 1]];
627
628        let distances: Array2<f64> =
629            euclidean_distance_transform(&binary.view(), DistanceMetric::Euclidean)
630                .expect("Failed to compute");
631
632        // All distances should be zero
633        for &d in distances.iter() {
634            assert_relative_eq!(d, 0.0, epsilon = 1e-10);
635        }
636    }
637}