scirs2_ndimage/level_set/
mod.rs1#[derive(Debug, Clone)]
13pub struct LevelSetConfig {
14 pub n_iter: usize,
16 pub dt: f64,
18 pub smoothing_weight: f64,
20 pub balloon: f64,
22 pub mu: f64,
24 pub nu: f64,
26 pub lambda1: f64,
28 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#[non_exhaustive]
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum LevelSetMethod {
51 ChanVese,
53 GeodesicActiveContour,
55}
56
57#[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#[inline]
69fn dirac(phi: f64, eps: f64) -> f64 {
70 eps / (std::f64::consts::PI * (phi * phi + eps * eps))
71}
72
73#[inline]
75fn clamp_idx(i: isize, n: usize) -> usize {
76 i.max(0).min(n as isize - 1) as usize
77}
78
79pub 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
105pub 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 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 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
151fn 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 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 let mut dist = vec![vec![f64::INFINITY; w]; h];
172 for i in 0..h {
174 for j in 0..w {
175 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 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 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 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
235pub 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 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 let curv = compute_curvature(&phi);
280
281 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 if iter % 20 == 19 {
299 reinitialize_phi(&mut phi);
300 }
301 }
302
303 phi
304}
305
306fn 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
358pub 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 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
423pub 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 let grad_mag = compute_gradient_magnitude(image);
443
444 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 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 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 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 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 let advection = gx[i][j] * phix_c + gy[i][j] * phiy_c;
502
503 let smoothing = g[i][j] * config.smoothing_weight * curv[i][j] * grad_phi_norm;
505
506 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
518pub 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
529pub 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#[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]); assert!(mask[0][1]); assert!(!mask[1][0]); assert!(!mask[1][1]); }
594
595 #[test]
596 fn test_compute_gradient_magnitude_flat() {
597 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 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 assert!(
622 grad[2][4] > 0.0 || grad[2][5] > 0.0,
623 "Expected non-zero gradient at edge"
624 );
625 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 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 assert!(
647 phi[5][5] > 0.0,
648 "Center expected inside (phi>0), got {}",
649 phi[5][5]
650 );
651 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 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 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 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 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}