Skip to main content

scirs2_ndimage/level_set/
mod.rs

1//! Geodesic active contours and Chan-Vese level-set segmentation.
2//!
3//! Implements:
4//! * **Chan-Vese** (Mumford-Shah functional, piecewise constant) for 2D and 3D.
5//! * **Geodesic active contours** (Caselles 1997, edge-based) for 2D.
6//!
7//! References:
8//! * Chan & Vese, "Active Contours Without Edges," TIP 2001.
9//! * Caselles et al., "Geodesic Active Contours," IJCV 1997.
10
11/// Configuration for level-set segmentation methods.
12#[derive(Debug, Clone)]
13pub struct LevelSetConfig {
14    /// Number of iterations.
15    pub n_iter: usize,
16    /// Time step for explicit updates.
17    pub dt: f64,
18    /// Weight for curvature (smoothing) term.
19    pub smoothing_weight: f64,
20    /// Balloon force coefficient (geodesic method).
21    pub balloon: f64,
22    /// Chan-Vese length penalty (weight of perimeter term).
23    pub mu: f64,
24    /// Chan-Vese area penalty (weight of area term).
25    pub nu: f64,
26    /// Chan-Vese λ₁ (foreground data fidelity weight).
27    pub lambda1: f64,
28    /// Chan-Vese λ₂ (background data fidelity weight).
29    pub lambda2: f64,
30}
31
32impl Default for LevelSetConfig {
33    fn default() -> Self {
34        LevelSetConfig {
35            n_iter: 100,
36            dt: 0.5,
37            smoothing_weight: 1.0,
38            balloon: 0.0,
39            mu: 0.2,
40            nu: 0.0,
41            lambda1: 1.0,
42            lambda2: 1.0,
43        }
44    }
45}
46
47/// Level-set evolution method selector.
48#[non_exhaustive]
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum LevelSetMethod {
51    /// Chan-Vese segmentation (Mumford-Shah piecewise constant).
52    ChanVese,
53    /// Geodesic active contour (edge-based, Caselles 1997).
54    GeodesicActiveContour,
55}
56
57// ---------------------------------------------------------------------------
58// Helpers
59// ---------------------------------------------------------------------------
60
61/// Smoothed Heaviside function.
62#[inline]
63fn heaviside(phi: f64, eps: f64) -> f64 {
64    0.5 * (1.0 + (2.0 / std::f64::consts::PI) * (phi / eps).atan())
65}
66
67/// Smoothed Dirac delta function.
68#[inline]
69fn dirac(phi: f64, eps: f64) -> f64 {
70    eps / (std::f64::consts::PI * (phi * phi + eps * eps))
71}
72
73/// Clamp index to [0, n-1].
74#[inline]
75fn clamp_idx(i: isize, n: usize) -> usize {
76    i.max(0).min(n as isize - 1) as usize
77}
78
79// ---------------------------------------------------------------------------
80// Gradient magnitude
81// ---------------------------------------------------------------------------
82
83/// Compute the gradient magnitude of a 2D image using central differences.
84pub fn compute_gradient_magnitude(image: &[Vec<f64>]) -> Vec<Vec<f64>> {
85    let h = image.len();
86    if h == 0 {
87        return vec![];
88    }
89    let w = image[0].len();
90    let mut grad = vec![vec![0.0f64; w]; h];
91    for i in 0..h {
92        for j in 0..w {
93            let ip = clamp_idx(i as isize + 1, h);
94            let im = clamp_idx(i as isize - 1, h);
95            let jp = clamp_idx(j as isize + 1, w);
96            let jm = clamp_idx(j as isize - 1, w);
97            let gx = (image[ip][j] - image[im][j]) / 2.0;
98            let gy = (image[i][jp] - image[i][jm]) / 2.0;
99            grad[i][j] = (gx * gx + gy * gy).sqrt();
100        }
101    }
102    grad
103}
104
105// ---------------------------------------------------------------------------
106// Curvature (divergence of normalised gradient of φ)
107// ---------------------------------------------------------------------------
108
109/// Compute the mean curvature of the level-set function φ via central differences.
110pub fn compute_curvature(phi: &[Vec<f64>]) -> Vec<Vec<f64>> {
111    let h = phi.len();
112    if h == 0 {
113        return vec![];
114    }
115    let w = phi[0].len();
116    let mut curv = vec![vec![0.0f64; w]; h];
117    let eps = 1e-8;
118    for i in 0..h {
119        for j in 0..w {
120            let ip = clamp_idx(i as isize + 1, h);
121            let im = clamp_idx(i as isize - 1, h);
122            let jp = clamp_idx(j as isize + 1, w);
123            let jm = clamp_idx(j as isize - 1, w);
124
125            let phi_c = phi[i][j];
126            let phi_ip = phi[ip][j];
127            let phi_im = phi[im][j];
128            let phi_jp = phi[i][jp];
129            let phi_jm = phi[i][jm];
130
131            // Partial derivatives
132            let phix = (phi_ip - phi_im) / 2.0;
133            let phiy = (phi_jp - phi_jm) / 2.0;
134            let phixx = phi_ip - 2.0 * phi_c + phi_im;
135            let phiyy = phi_jp - 2.0 * phi_c + phi_jm;
136            // Cross derivative
137            let phi_ipjp = phi[ip][jp];
138            let phi_ipjm = phi[ip][jm];
139            let phi_imjp = phi[im][jp];
140            let phi_imjm = phi[im][jm];
141            let phixy = (phi_ipjp - phi_ipjm - phi_imjp + phi_imjm) / 4.0;
142
143            let norm2 = phix * phix + phiy * phiy + eps;
144            curv[i][j] = (phixx * phiy * phiy - 2.0 * phixy * phix * phiy + phiyy * phix * phix)
145                / (norm2 * norm2.sqrt());
146        }
147    }
148    curv
149}
150
151// ---------------------------------------------------------------------------
152// Helpers for phi reinitialization (simplified fast marching via sign * dist)
153// ---------------------------------------------------------------------------
154
155fn reinitialize_phi(phi: &mut Vec<Vec<f64>>) {
156    let h = phi.len();
157    if h == 0 {
158        return;
159    }
160    let w = phi[0].len();
161    // Simple approximate reinitialization: signed distance to zero crossing
162    // via a 2D distance transform on the sign array.
163    let sign: Vec<Vec<f64>> = (0..h)
164        .map(|i| {
165            (0..w)
166                .map(|j| if phi[i][j] > 0.0 { 1.0 } else { -1.0 })
167                .collect()
168        })
169        .collect();
170    // Euclidean distance transform approximation (Rosenfeld-Pfaltz)
171    let mut dist = vec![vec![f64::INFINITY; w]; h];
172    // Forward pass
173    for i in 0..h {
174        for j in 0..w {
175            // Check if zero crossing nearby: if sign changes relative to neighbor
176            let mut is_border = false;
177            for (di, dj) in &[(-1isize, 0isize), (0, -1)] {
178                let ni = i as isize + di;
179                let nj = j as isize + dj;
180                if ni >= 0 && ni < h as isize && nj >= 0 && nj < w as isize {
181                    if sign[i][j] != sign[ni as usize][nj as usize] {
182                        is_border = true;
183                    }
184                }
185            }
186            if is_border {
187                dist[i][j] = 0.0;
188            }
189        }
190    }
191    // BFS-like distance propagation from borders
192    let mut changed = true;
193    let mut iter = 0;
194    while changed && iter < (h + w) {
195        changed = false;
196        iter += 1;
197        for i in 0..h {
198            for j in 0..w {
199                // Check zero crossing
200                let s = sign[i][j];
201                let mut min_d = dist[i][j];
202                for (di, dj) in &[(-1isize, 0isize), (1, 0), (0, -1isize), (0, 1)] {
203                    let ni = i as isize + di;
204                    let nj = j as isize + dj;
205                    if ni >= 0 && ni < h as isize && nj >= 0 && nj < w as isize {
206                        let ns = sign[ni as usize][nj as usize];
207                        if ns != s {
208                            // Zero crossing between (i,j) and neighbor → distance ~0.5
209                            let d = 0.5;
210                            if d < min_d {
211                                min_d = d;
212                                changed = true;
213                            }
214                        } else {
215                            let d = dist[ni as usize][nj as usize] + 1.0;
216                            if d < min_d {
217                                min_d = d;
218                                changed = true;
219                            }
220                        }
221                    }
222                }
223                dist[i][j] = min_d;
224            }
225        }
226    }
227
228    for i in 0..h {
229        for j in 0..w {
230            phi[i][j] = sign[i][j] * dist[i][j];
231        }
232    }
233}
234
235// ---------------------------------------------------------------------------
236// Chan-Vese 2D
237// ---------------------------------------------------------------------------
238
239/// Chan-Vese level-set segmentation for a 2D image.
240///
241/// The level set φ evolves to minimise:
242///   E = λ₁ ∫(u₀-c₁)²H(φ) + λ₂ ∫(u₀-c₂)²(1-H(φ)) + μ ∫|∇φ| + ν ∫H(φ)
243///
244/// Returns the evolved φ field; φ > 0 = foreground, φ ≤ 0 = background.
245pub fn chan_vese_2d(
246    image: &[Vec<f64>],
247    phi_init: &[Vec<f64>],
248    config: &LevelSetConfig,
249) -> Vec<Vec<f64>> {
250    let h = image.len();
251    if h == 0 {
252        return vec![];
253    }
254    let w = image[0].len();
255    let eps = 1.0_f64;
256
257    let mut phi: Vec<Vec<f64>> = phi_init.iter().map(|row| row.to_vec()).collect();
258
259    for iter in 0..config.n_iter {
260        // Compute c1 and c2
261        let mut sum_fg = 0.0f64;
262        let mut cnt_fg = 0.0f64;
263        let mut sum_bg = 0.0f64;
264        let mut cnt_bg = 0.0f64;
265        for i in 0..h {
266            for j in 0..w {
267                let u = image[i][j];
268                let hv = heaviside(phi[i][j], eps);
269                sum_fg += u * hv;
270                cnt_fg += hv;
271                sum_bg += u * (1.0 - hv);
272                cnt_bg += 1.0 - hv;
273            }
274        }
275        let c1 = if cnt_fg > 1e-12 { sum_fg / cnt_fg } else { 0.0 };
276        let c2 = if cnt_bg > 1e-12 { sum_bg / cnt_bg } else { 0.0 };
277
278        // Curvature
279        let curv = compute_curvature(&phi);
280
281        // Update φ
282        let mut phi_new = phi.clone();
283        for i in 0..h {
284            for j in 0..w {
285                let u = image[i][j];
286                let d_phi = dirac(phi[i][j], eps);
287                let kappa = curv[i][j];
288                let diff_fg = (u - c1) * (u - c1);
289                let diff_bg = (u - c2) * (u - c2);
290                let rhs = config.mu * kappa - config.nu - config.lambda1 * diff_fg
291                    + config.lambda2 * diff_bg;
292                phi_new[i][j] = phi[i][j] + config.dt * d_phi * rhs;
293            }
294        }
295        phi = phi_new;
296
297        // Reinitialize every 20 steps
298        if iter % 20 == 19 {
299            reinitialize_phi(&mut phi);
300        }
301    }
302
303    phi
304}
305
306// ---------------------------------------------------------------------------
307// Chan-Vese 3D
308// ---------------------------------------------------------------------------
309
310fn compute_curvature_3d(phi: &[Vec<Vec<f64>>]) -> Vec<Vec<Vec<f64>>> {
311    let nd = phi.len();
312    if nd == 0 {
313        return vec![];
314    }
315    let nh = phi[0].len();
316    if nh == 0 {
317        return vec![vec![]];
318    }
319    let nw = phi[0][0].len();
320    let eps = 1e-8;
321    let mut curv = vec![vec![vec![0.0f64; nw]; nh]; nd];
322
323    for d in 0..nd {
324        for h in 0..nh {
325            for w in 0..nw {
326                let dp = clamp_idx(d as isize + 1, nd);
327                let dm = clamp_idx(d as isize - 1, nd);
328                let hp = clamp_idx(h as isize + 1, nh);
329                let hm = clamp_idx(h as isize - 1, nh);
330                let wp = clamp_idx(w as isize + 1, nw);
331                let wm = clamp_idx(w as isize - 1, nw);
332
333                let c = phi[d][h][w];
334                let fx = (phi[dp][h][w] - phi[dm][h][w]) / 2.0;
335                let fy = (phi[d][hp][w] - phi[d][hm][w]) / 2.0;
336                let fz = (phi[d][h][wp] - phi[d][h][wm]) / 2.0;
337                let fxx = phi[dp][h][w] - 2.0 * c + phi[dm][h][w];
338                let fyy = phi[d][hp][w] - 2.0 * c + phi[d][hm][w];
339                let fzz = phi[d][h][wp] - 2.0 * c + phi[d][h][wm];
340                let fxy = (phi[dp][hp][w] - phi[dp][hm][w] - phi[dm][hp][w] + phi[dm][hm][w]) / 4.0;
341                let fxz = (phi[dp][h][wp] - phi[dp][h][wm] - phi[dm][h][wp] + phi[dm][h][wm]) / 4.0;
342                let fyz = (phi[d][hp][wp] - phi[d][hp][wm] - phi[d][hm][wp] + phi[d][hm][wm]) / 4.0;
343
344                let norm2 = fx * fx + fy * fy + fz * fz + eps;
345                let numerator = (fxx * (fy * fy + fz * fz)
346                    + fyy * (fx * fx + fz * fz)
347                    + fzz * (fx * fx + fy * fy)
348                    - 2.0 * fxy * fx * fy
349                    - 2.0 * fxz * fx * fz
350                    - 2.0 * fyz * fy * fz);
351                curv[d][h][w] = numerator / (norm2 * norm2.sqrt());
352            }
353        }
354    }
355    curv
356}
357
358/// Chan-Vese level-set segmentation for a 3D volume.
359///
360/// Returns the evolved φ field; φ > 0 = foreground.
361pub fn chan_vese_3d(
362    image: &[Vec<Vec<f64>>],
363    phi_init: &[Vec<Vec<f64>>],
364    config: &LevelSetConfig,
365) -> Vec<Vec<Vec<f64>>> {
366    let nd = image.len();
367    if nd == 0 {
368        return vec![];
369    }
370    let nh = image[0].len();
371    let nw = if nh > 0 { image[0][0].len() } else { 0 };
372    let eps = 1.0_f64;
373
374    let mut phi: Vec<Vec<Vec<f64>>> = phi_init
375        .iter()
376        .map(|plane| plane.iter().map(|row| row.to_vec()).collect())
377        .collect();
378
379    for _iter in 0..config.n_iter {
380        // Compute c1, c2
381        let mut sum_fg = 0.0f64;
382        let mut cnt_fg = 0.0f64;
383        let mut sum_bg = 0.0f64;
384        let mut cnt_bg = 0.0f64;
385        for d in 0..nd {
386            for h in 0..nh {
387                for w in 0..nw {
388                    let u = image[d][h][w];
389                    let hv = heaviside(phi[d][h][w], eps);
390                    sum_fg += u * hv;
391                    cnt_fg += hv;
392                    sum_bg += u * (1.0 - hv);
393                    cnt_bg += 1.0 - hv;
394                }
395            }
396        }
397        let c1 = if cnt_fg > 1e-12 { sum_fg / cnt_fg } else { 0.0 };
398        let c2 = if cnt_bg > 1e-12 { sum_bg / cnt_bg } else { 0.0 };
399
400        let curv = compute_curvature_3d(&phi);
401
402        let mut phi_new = phi.clone();
403        for d in 0..nd {
404            for h in 0..nh {
405                for w in 0..nw {
406                    let u = image[d][h][w];
407                    let d_phi = dirac(phi[d][h][w], eps);
408                    let kappa = curv[d][h][w];
409                    let diff_fg = (u - c1) * (u - c1);
410                    let diff_bg = (u - c2) * (u - c2);
411                    let rhs = config.mu * kappa - config.nu - config.lambda1 * diff_fg
412                        + config.lambda2 * diff_bg;
413                    phi_new[d][h][w] = phi[d][h][w] + config.dt * d_phi * rhs;
414                }
415            }
416        }
417        phi = phi_new;
418    }
419
420    phi
421}
422
423// ---------------------------------------------------------------------------
424// Geodesic active contour 2D
425// ---------------------------------------------------------------------------
426
427/// Geodesic active contour level-set evolution (Caselles 1997) for 2D.
428///
429/// Uses edge stopping function g = 1/(1 + |∇I|²), upwind scheme for advection.
430pub fn geodesic_contour_2d(
431    image: &[Vec<f64>],
432    phi_init: &[Vec<f64>],
433    config: &LevelSetConfig,
434) -> Vec<Vec<f64>> {
435    let h = image.len();
436    if h == 0 {
437        return vec![];
438    }
439    let w = image[0].len();
440
441    // Compute gradient magnitude of image
442    let grad_mag = compute_gradient_magnitude(image);
443
444    // Edge stopping function g(x) = 1 / (1 + |∇I(x)|²)
445    let g: Vec<Vec<f64>> = (0..h)
446        .map(|i| {
447            (0..w)
448                .map(|j| {
449                    let gm = grad_mag[i][j];
450                    1.0 / (1.0 + gm * gm)
451                })
452                .collect()
453        })
454        .collect();
455
456    // Gradient of g (for advection term ∇g · ∇φ)
457    let mut gx = vec![vec![0.0f64; w]; h];
458    let mut gy = vec![vec![0.0f64; w]; h];
459    for i in 0..h {
460        for j in 0..w {
461            let ip = clamp_idx(i as isize + 1, h);
462            let im = clamp_idx(i as isize - 1, h);
463            let jp = clamp_idx(j as isize + 1, w);
464            let jm = clamp_idx(j as isize - 1, w);
465            gx[i][j] = (g[ip][j] - g[im][j]) / 2.0;
466            gy[i][j] = (g[i][jp] - g[i][jm]) / 2.0;
467        }
468    }
469
470    let mut phi: Vec<Vec<f64>> = phi_init.iter().map(|r| r.to_vec()).collect();
471
472    for _iter in 0..config.n_iter {
473        let curv = compute_curvature(&phi);
474
475        let mut phi_new = phi.clone();
476        for i in 0..h {
477            for j in 0..w {
478                let ip = clamp_idx(i as isize + 1, h);
479                let im = clamp_idx(i as isize - 1, h);
480                let jp = clamp_idx(j as isize + 1, w);
481                let jm = clamp_idx(j as isize - 1, w);
482
483                // Upwind differences for |∇φ| approximation
484                let dpx_fwd = phi[ip][j] - phi[i][j];
485                let dpx_bwd = phi[i][j] - phi[im][j];
486                let dpy_fwd = phi[i][jp] - phi[i][j];
487                let dpy_bwd = phi[i][j] - phi[i][jm];
488
489                // Godunov upwind scheme for |∇φ|
490                let grad_phi_norm = {
491                    let ax = dpx_bwd.max(0.0).powi(2) + dpx_fwd.min(0.0).powi(2);
492                    let ay = dpy_bwd.max(0.0).powi(2) + dpy_fwd.min(0.0).powi(2);
493                    (ax + ay).sqrt()
494                };
495
496                // Central differences for ∇φ (advection)
497                let phix_c = (phi[ip][j] - phi[im][j]) / 2.0;
498                let phiy_c = (phi[i][jp] - phi[i][jm]) / 2.0;
499
500                // Advection term: ∇g · ∇φ
501                let advection = gx[i][j] * phix_c + gy[i][j] * phiy_c;
502
503                // Smoothing term: g * κ * |∇φ|
504                let smoothing = g[i][j] * config.smoothing_weight * curv[i][j] * grad_phi_norm;
505
506                // Balloon term: balloon * g * |∇φ|
507                let balloon_term = config.balloon * g[i][j] * grad_phi_norm;
508
509                phi_new[i][j] = phi[i][j] + config.dt * (smoothing + advection + balloon_term);
510            }
511        }
512        phi = phi_new;
513    }
514
515    phi
516}
517
518// ---------------------------------------------------------------------------
519// Utility functions
520// ---------------------------------------------------------------------------
521
522/// Convert a level-set function φ to a binary mask: φ > 0 → true.
523pub fn binary_from_levelset(phi: &[Vec<f64>]) -> Vec<Vec<bool>> {
524    phi.iter()
525        .map(|row| row.iter().map(|&v| v > 0.0).collect())
526        .collect()
527}
528
529/// Initialize a circle-shaped level-set function.
530///
531/// φ(x,y) = radius - √((x-cx)² + (y-cy)²)
532/// Positive inside the circle, negative outside.
533pub fn initialize_circle(
534    height: usize,
535    width: usize,
536    center: (usize, usize),
537    radius: f64,
538) -> Vec<Vec<f64>> {
539    let (cy, cx) = center;
540    (0..height)
541        .map(|i| {
542            (0..width)
543                .map(|j| {
544                    let di = i as f64 - cy as f64;
545                    let dj = j as f64 - cx as f64;
546                    radius - (di * di + dj * dj).sqrt()
547                })
548                .collect()
549        })
550        .collect()
551}
552
553// ---------------------------------------------------------------------------
554// Tests
555// ---------------------------------------------------------------------------
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560
561    fn make_image_bright_center(h: usize, w: usize) -> Vec<Vec<f64>> {
562        let mut img = vec![vec![0.2f64; w]; h];
563        let ch = h / 2;
564        let cw = w / 2;
565        let r = (h.min(w) / 4) as f64;
566        for i in 0..h {
567            for j in 0..w {
568                let di = i as f64 - ch as f64;
569                let dj = j as f64 - cw as f64;
570                if (di * di + dj * dj).sqrt() < r {
571                    img[i][j] = 0.9;
572                }
573            }
574        }
575        img
576    }
577
578    #[test]
579    fn test_initialize_circle() {
580        let phi = initialize_circle(20, 20, (10, 10), 5.0);
581        assert!(phi[10][10] > 0.0, "Center should be inside (phi > 0)");
582        assert!(phi[0][0] < 0.0, "Corner should be outside (phi < 0)");
583    }
584
585    #[test]
586    fn test_binary_from_levelset() {
587        let phi = vec![vec![-1.0, 0.5], vec![0.0, -0.5]];
588        let mask = binary_from_levelset(&phi);
589        assert!(!mask[0][0]); // -1.0 → false
590        assert!(mask[0][1]); //  0.5 → true
591        assert!(!mask[1][0]); //  0.0 → false (not strictly > 0)
592        assert!(!mask[1][1]); // -0.5 → false
593    }
594
595    #[test]
596    fn test_compute_gradient_magnitude_flat() {
597        // Flat image → gradient magnitude should be ~0 everywhere interior
598        let img = vec![vec![1.0f64; 10]; 10];
599        let grad = compute_gradient_magnitude(&img);
600        for row in &grad {
601            for &v in row {
602                assert!(
603                    v.abs() < 1e-10,
604                    "Expected ~0 gradient on flat image, got {}",
605                    v
606                );
607            }
608        }
609    }
610
611    #[test]
612    fn test_compute_gradient_magnitude_edge() {
613        // Step edge: left half 0, right half 1
614        let h = 5;
615        let w = 10;
616        let img: Vec<Vec<f64>> = (0..h)
617            .map(|_| (0..w).map(|j| if j < 5 { 0.0 } else { 1.0 }).collect())
618            .collect();
619        let grad = compute_gradient_magnitude(&img);
620        // At j=4 (boundary), gradient should be > 0
621        assert!(
622            grad[2][4] > 0.0 || grad[2][5] > 0.0,
623            "Expected non-zero gradient at edge"
624        );
625        // Far from edge, gradient should be ~0
626        assert!(grad[2][0].abs() < 1e-10 || grad[2][1].abs() < 1e-10);
627    }
628
629    #[test]
630    fn test_chan_vese_2d_bright_center() {
631        // 10×10 image with bright center; circle initialised around center
632        let img = make_image_bright_center(10, 10);
633        let phi_init = initialize_circle(10, 10, (5, 5), 3.0);
634        let config = LevelSetConfig {
635            n_iter: 30,
636            dt: 0.3,
637            mu: 0.3,
638            lambda1: 1.0,
639            lambda2: 1.0,
640            ..Default::default()
641        };
642        let phi = chan_vese_2d(&img, &phi_init, &config);
643        assert_eq!(phi.len(), 10);
644        assert_eq!(phi[0].len(), 10);
645        // Center should remain positive (foreground)
646        assert!(
647            phi[5][5] > 0.0,
648            "Center expected inside (phi>0), got {}",
649            phi[5][5]
650        );
651        // binary_from_levelset should work
652        let mask = binary_from_levelset(&phi);
653        assert!(mask[5][5], "Center should be foreground");
654    }
655
656    #[test]
657    fn test_chan_vese_3d_basic() {
658        // Tiny 4×4×4 volume
659        let image: Vec<Vec<Vec<f64>>> = (0..4)
660            .map(|_| (0..4).map(|_| vec![0.5f64; 4]).collect())
661            .collect();
662        let phi_init: Vec<Vec<Vec<f64>>> = (0..4)
663            .map(|d| {
664                (0..4)
665                    .map(|h| {
666                        (0..4)
667                            .map(|w| {
668                                let dd = d as f64 - 2.0;
669                                let dh = h as f64 - 2.0;
670                                let dw = w as f64 - 2.0;
671                                1.5 - (dd * dd + dh * dh + dw * dw).sqrt()
672                            })
673                            .collect()
674                    })
675                    .collect()
676            })
677            .collect();
678        let config = LevelSetConfig {
679            n_iter: 5,
680            ..Default::default()
681        };
682        let phi = chan_vese_3d(&image, &phi_init, &config);
683        assert_eq!(phi.len(), 4);
684        assert_eq!(phi[0].len(), 4);
685        assert_eq!(phi[0][0].len(), 4);
686    }
687
688    #[test]
689    fn test_geodesic_contour_edge_stopping_function() {
690        // Flat image → g should be ~1 everywhere
691        let img = vec![vec![0.5f64; 10]; 10];
692        let grad = compute_gradient_magnitude(&img);
693        for row in &grad {
694            for &gm in row {
695                let g = 1.0 / (1.0 + gm * gm);
696                assert!(
697                    (g - 1.0).abs() < 1e-6,
698                    "g on flat image should be ~1, got {}",
699                    g
700                );
701            }
702        }
703
704        // Edge image → g should be < 1 at boundary
705        let img_edge: Vec<Vec<f64>> = (0..10)
706            .map(|_| (0..10).map(|j| if j < 5 { 0.0 } else { 1.0 }).collect())
707            .collect();
708        let grad_edge = compute_gradient_magnitude(&img_edge);
709        let has_large_grad = grad_edge.iter().any(|row| {
710            row.iter().any(|&gm| {
711                let g = 1.0 / (1.0 + gm * gm);
712                g < 0.99
713            })
714        });
715        assert!(has_large_grad, "Expected g < 1 near edge");
716    }
717
718    #[test]
719    fn test_geodesic_contour_2d_runs() {
720        let img = make_image_bright_center(12, 12);
721        let phi_init = initialize_circle(12, 12, (6, 6), 4.0);
722        let config = LevelSetConfig {
723            n_iter: 20,
724            dt: 0.2,
725            smoothing_weight: 1.0,
726            balloon: 0.1,
727            ..Default::default()
728        };
729        let phi = geodesic_contour_2d(&img, &phi_init, &config);
730        assert_eq!(phi.len(), 12);
731        assert_eq!(phi[0].len(), 12);
732    }
733
734    #[test]
735    fn test_compute_curvature_constant_phi() {
736        // Constant φ → curvature numerator involves uniform gradients → should be near zero
737        let phi = vec![vec![2.0f64; 8]; 8];
738        let curv = compute_curvature(&phi);
739        for row in &curv {
740            for &c in row {
741                assert!(
742                    c.abs() < 1e-5,
743                    "Expected ~0 curvature on constant phi, got {}",
744                    c
745                );
746            }
747        }
748    }
749}