1#[derive(Clone, Debug)]
15pub struct FrangiParams {
16 pub scale_range: [f64; 2],
18 pub scale_ratio: f64,
20 pub alpha: f64,
22 pub beta: f64,
24 pub c: f64,
27 pub black_white: bool,
29}
30
31impl Default for FrangiParams {
32 fn default() -> Self {
33 Self {
34 scale_range: [0.5, 6.0],
36 scale_ratio: 0.5,
37 alpha: 0.5,
38 beta: 0.5,
39 c: 500.0,
40 black_white: false, }
42 }
43}
44
45pub struct FrangiResult {
47 pub vesselness: Vec<f64>,
49 pub scale: Vec<f64>,
51}
52
53pub fn frangi_filter_3d(
63 data: &[f64],
64 nx: usize, ny: usize, nz: usize,
65 params: &FrangiParams,
66) -> FrangiResult {
67 frangi_filter_3d_with_progress(data, nx, ny, nz, params, |_, _| {})
68}
69
70fn compute_vesselness(l1: f64, l2: f64, l3: f64, a: f64, b: f64, c2: f64, black_white: bool) -> f64 {
73 let abs_l2 = l2.abs();
74 let abs_l3 = l3.abs();
75
76 if abs_l3 < 1e-10 || abs_l2 < 1e-10 {
78 return 0.0;
79 }
80
81 if black_white {
85 if l2 < 0.0 || l3 < 0.0 {
87 return 0.0;
88 }
89 } else {
90 if l2 > 0.0 || l3 > 0.0 {
92 return 0.0;
93 }
94 }
95
96 let ra = abs_l2 / abs_l3;
99
100 let rb = l1.abs() / (abs_l2 * abs_l3).sqrt();
103
104 let s = (l1 * l1 + l2 * l2 + l3 * l3).sqrt();
107
108 let exp_ra = 1.0 - (-ra * ra / a).exp();
111 let exp_rb = (-rb * rb / b).exp();
112 let exp_s = 1.0 - (-s * s / c2).exp();
113
114 let v = exp_ra * exp_rb * exp_s;
115
116 if v.is_finite() { v.max(0.0).min(1.0) } else { 0.0 }
118}
119
120fn sort_by_abs(a: f64, b: f64, c: f64) -> (f64, f64, f64) {
122 let mut vals = [(a.abs(), a), (b.abs(), b), (c.abs(), c)];
123 vals.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
124 (vals[0].1, vals[1].1, vals[2].1)
125}
126
127#[cfg(test)]
128fn compute_hessian_3d(
129 data: &[f64],
130 nx: usize, ny: usize, nz: usize,
131 sigma: f64,
132) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
133 let n_total = nx * ny * nz;
134
135 let smoothed = if sigma > 0.0 {
137 gaussian_smooth_3d(data, nx, ny, nz, sigma)
138 } else {
139 data.to_vec()
140 };
141
142 let dx = gradient_3d(&smoothed, nx, ny, nz, 'x');
144 let dy = gradient_3d(&smoothed, nx, ny, nz, 'y');
145 let dz = gradient_3d(&smoothed, nx, ny, nz, 'z');
146
147 let dxx = gradient_3d(&dx, nx, ny, nz, 'x');
149 let dxy = gradient_3d(&dx, nx, ny, nz, 'y');
150 let dxz = gradient_3d(&dx, nx, ny, nz, 'z');
151
152 let dyy = gradient_3d(&dy, nx, ny, nz, 'y');
153 let dyz = gradient_3d(&dy, nx, ny, nz, 'z');
154
155 let dzz = gradient_3d(&dz, nx, ny, nz, 'z');
156
157 (dxx, dyy, dzz, dxy, dxz, dyz)
158}
159
160#[cfg(test)]
161fn gradient_3d(data: &[f64], nx: usize, ny: usize, nz: usize, direction: char) -> Vec<f64> {
162 let n_total = nx * ny * nz;
163 let mut grad = vec![0.0f64; n_total];
164
165 let idx = |i: usize, j: usize, k: usize| i + j * nx + k * nx * ny;
167
168 match direction {
169 'x' => {
170 for k in 0..nz {
171 for j in 0..ny {
172 grad[idx(0, j, k)] = data[idx(1, j, k)] - data[idx(0, j, k)];
174
175 for i in 1..nx-1 {
177 grad[idx(i, j, k)] = (data[idx(i+1, j, k)] - data[idx(i-1, j, k)]) / 2.0;
178 }
179
180 if nx > 1 {
182 grad[idx(nx-1, j, k)] = data[idx(nx-1, j, k)] - data[idx(nx-2, j, k)];
183 }
184 }
185 }
186 }
187 'y' => {
188 for k in 0..nz {
189 for i in 0..nx {
190 grad[idx(i, 0, k)] = data[idx(i, 1, k)] - data[idx(i, 0, k)];
192
193 for j in 1..ny-1 {
195 grad[idx(i, j, k)] = (data[idx(i, j+1, k)] - data[idx(i, j-1, k)]) / 2.0;
196 }
197
198 if ny > 1 {
200 grad[idx(i, ny-1, k)] = data[idx(i, ny-1, k)] - data[idx(i, ny-2, k)];
201 }
202 }
203 }
204 }
205 'z' => {
206 for j in 0..ny {
207 for i in 0..nx {
208 grad[idx(i, j, 0)] = data[idx(i, j, 1)] - data[idx(i, j, 0)];
210
211 for k in 1..nz-1 {
213 grad[idx(i, j, k)] = (data[idx(i, j, k+1)] - data[idx(i, j, k-1)]) / 2.0;
214 }
215
216 if nz > 1 {
218 grad[idx(i, j, nz-1)] = data[idx(i, j, nz-1)] - data[idx(i, j, nz-2)];
219 }
220 }
221 }
222 }
223 _ => panic!("Invalid gradient direction"),
224 }
225
226 grad
227}
228
229fn gaussian_smooth_3d(data: &[f64], nx: usize, ny: usize, nz: usize, sigma: f64) -> Vec<f64> {
231 if sigma <= 0.0 {
232 return data.to_vec();
233 }
234
235 let kernel_radius = (3.0 * sigma).ceil() as usize;
237 let kernel_size = 2 * kernel_radius + 1;
238 let mut kernel = vec![0.0f64; kernel_size];
239
240 let mut sum = 0.0;
241 for i in 0..kernel_size {
242 let x = i as f64 - kernel_radius as f64;
243 kernel[i] = (-x * x / (2.0 * sigma * sigma)).exp();
244 sum += kernel[i];
245 }
246
247 for k in kernel.iter_mut() {
249 *k /= sum;
250 }
251
252 let smoothed_x = convolve_1d_direction(data, nx, ny, nz, &kernel, 'x');
255
256 let smoothed_xy = convolve_1d_direction(&smoothed_x, nx, ny, nz, &kernel, 'y');
258
259 let smoothed_xyz = convolve_1d_direction(&smoothed_xy, nx, ny, nz, &kernel, 'z');
261
262 smoothed_xyz
263}
264
265fn convolve_1d_direction(
268 data: &[f64],
269 nx: usize, ny: usize, nz: usize,
270 kernel: &[f64],
271 direction: char,
272) -> Vec<f64> {
273 let n_total = nx * ny * nz;
274 let mut result = vec![0.0f64; n_total];
275 let kernel_radius = (kernel.len() - 1) / 2;
276
277 let idx = |i: usize, j: usize, k: usize| i + j * nx + k * nx * ny;
278
279 let clamp_x = |x: isize| -> usize { x.max(0).min(nx as isize - 1) as usize };
281 let clamp_y = |y: isize| -> usize { y.max(0).min(ny as isize - 1) as usize };
282 let clamp_z = |z: isize| -> usize { z.max(0).min(nz as isize - 1) as usize };
283
284 match direction {
285 'x' => {
286 for k in 0..nz {
287 for j in 0..ny {
288 for i in 0..nx {
289 let mut sum = 0.0;
290
291 for ki in 0..kernel.len() {
292 let offset = ki as isize - kernel_radius as isize;
293 let ni = clamp_x(i as isize + offset);
294 sum += data[idx(ni, j, k)] * kernel[ki];
295 }
296
297 result[idx(i, j, k)] = sum;
298 }
299 }
300 }
301 }
302 'y' => {
303 for k in 0..nz {
304 for j in 0..ny {
305 for i in 0..nx {
306 let mut sum = 0.0;
307
308 for ki in 0..kernel.len() {
309 let offset = ki as isize - kernel_radius as isize;
310 let nj = clamp_y(j as isize + offset);
311 sum += data[idx(i, nj, k)] * kernel[ki];
312 }
313
314 result[idx(i, j, k)] = sum;
315 }
316 }
317 }
318 }
319 'z' => {
320 for k in 0..nz {
321 for j in 0..ny {
322 for i in 0..nx {
323 let mut sum = 0.0;
324
325 for ki in 0..kernel.len() {
326 let offset = ki as isize - kernel_radius as isize;
327 let nk = clamp_z(k as isize + offset);
328 sum += data[idx(i, j, nk)] * kernel[ki];
329 }
330
331 result[idx(i, j, k)] = sum;
332 }
333 }
334 }
335 }
336 _ => panic!("Invalid convolution direction"),
337 }
338
339 result
340}
341
342#[inline(always)]
354fn eigenvalues_3x3_cardano(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> (f64, f64, f64) {
355 let p1 = d * d + e * e + f * f;
356 if p1 < 1e-30 {
357 return (a, b, c);
359 }
360
361 let q = (a + b + c) / 3.0;
362 let p2 = (a - q) * (a - q) + (b - q) * (b - q) + (c - q) * (c - q) + 2.0 * p1;
363 let p = (p2 / 6.0).sqrt();
364
365 let inv_p = 1.0 / p;
367 let b00 = (a - q) * inv_p;
368 let b11 = (b - q) * inv_p;
369 let b22 = (c - q) * inv_p;
370 let b01 = d * inv_p;
371 let b02 = e * inv_p;
372 let b12 = f * inv_p;
373
374 let det_b_half = (b00 * (b11 * b22 - b12 * b12)
376 - b01 * (b01 * b22 - b12 * b02)
377 + b02 * (b01 * b12 - b11 * b02)) / 2.0;
378
379 let r = det_b_half.max(-1.0).min(1.0);
381 let phi = r.acos() / 3.0;
382
383 let eig1 = q + 2.0 * p * phi.cos();
384 let eig3 = q + 2.0 * p * (phi + 2.0 * std::f64::consts::PI / 3.0).cos();
385 let eig2 = 3.0 * q - eig1 - eig3; (eig1, eig2, eig3)
388}
389
390#[allow(dead_code)]
403fn eigenvalues_3x3_symmetric(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> (f64, f64, f64) {
404 let mut v = [[0.0f64; 3]; 3];
406 v[0][0] = a; v[0][1] = d; v[0][2] = e;
407 v[1][0] = d; v[1][1] = b; v[1][2] = f;
408 v[2][0] = e; v[2][1] = f; v[2][2] = c;
409
410 let mut eigenvalues = [0.0f64; 3];
411 let mut e_vec = [0.0f64; 3];
412
413 tred2(&mut v, &mut eigenvalues, &mut e_vec);
415
416 tql2(&mut v, &mut eigenvalues, &mut e_vec);
418
419 (eigenvalues[0], eigenvalues[1], eigenvalues[2])
420}
421
422fn tred2(v: &mut [[f64; 3]; 3], d: &mut [f64; 3], e: &mut [f64; 3]) {
430 const N: usize = 3;
431
432 for j in 0..N {
434 d[j] = v[N - 1][j];
435 }
436
437 for i in (1..N).rev() {
439 let mut scale = 0.0;
441 let mut h = 0.0;
442
443 for k in 0..i {
444 scale += d[k].abs();
445 }
446
447 if scale == 0.0 {
448 e[i] = d[i - 1];
449 for j in 0..i {
450 d[j] = v[i - 1][j];
451 v[i][j] = 0.0;
452 v[j][i] = 0.0;
453 }
454 } else {
455 for k in 0..i {
457 d[k] /= scale;
458 h += d[k] * d[k];
459 }
460
461 let f = d[i - 1];
462 let mut g = h.sqrt();
463 if f > 0.0 {
464 g = -g;
465 }
466 e[i] = scale * g;
467 h -= f * g;
468 d[i - 1] = f - g;
469
470 for j in 0..i {
471 e[j] = 0.0;
472 }
473
474 for j in 0..i {
476 let f = d[j];
477 v[j][i] = f;
478 let mut g = e[j] + v[j][j] * f;
479 for k in (j + 1)..i {
480 g += v[k][j] * d[k];
481 e[k] += v[k][j] * f;
482 }
483 e[j] = g;
484 }
485
486 let mut f = 0.0;
487 for j in 0..i {
488 e[j] /= h;
489 f += e[j] * d[j];
490 }
491
492 let hh = f / (h + h);
493 for j in 0..i {
494 e[j] -= hh * d[j];
495 }
496
497 for j in 0..i {
498 let f = d[j];
499 let g = e[j];
500 for k in j..i {
501 v[k][j] -= f * e[k] + g * d[k];
502 }
503 d[j] = v[i - 1][j];
504 v[i][j] = 0.0;
505 }
506 }
507 d[i] = h;
508 }
509
510 for i in 0..(N - 1) {
512 v[N - 1][i] = v[i][i];
513 v[i][i] = 1.0;
514 let h = d[i + 1];
515 if h != 0.0 {
516 for k in 0..=i {
517 d[k] = v[k][i + 1] / h;
518 }
519 for j in 0..=i {
520 let mut g = 0.0;
521 for k in 0..=i {
522 g += v[k][i + 1] * v[k][j];
523 }
524 for k in 0..=i {
525 v[k][j] -= g * d[k];
526 }
527 }
528 }
529 for k in 0..=i {
530 v[k][i + 1] = 0.0;
531 }
532 }
533
534 for j in 0..N {
535 d[j] = v[N - 1][j];
536 v[N - 1][j] = 0.0;
537 }
538 v[N - 1][N - 1] = 1.0;
539 e[0] = 0.0;
540}
541
542fn tql2(v: &mut [[f64; 3]; 3], d: &mut [f64; 3], e: &mut [f64; 3]) {
550 const N: usize = 3;
551
552 for i in 1..N {
553 e[i - 1] = e[i];
554 }
555 e[N - 1] = 0.0;
556
557 let mut f: f64 = 0.0;
558 let mut tst1: f64 = 0.0;
559 let eps: f64 = 2.0f64.powi(-52);
560
561 for l in 0..N {
562 tst1 = tst1.max(d[l].abs() + e[l].abs());
564 let mut m = l;
565 while m < N {
566 if e[m].abs() <= eps * tst1 {
567 break;
568 }
569 m += 1;
570 }
571
572 if m > l {
574 loop {
575 let g = d[l];
577 let mut p = (d[l + 1] - g) / (2.0 * e[l]);
578 let mut r = hypot(p, 1.0);
579 if p < 0.0 {
580 r = -r;
581 }
582 d[l] = e[l] / (p + r);
583 d[l + 1] = e[l] * (p + r);
584 let dl1 = d[l + 1];
585 let h = g - d[l];
586 for i in (l + 2)..N {
587 d[i] -= h;
588 }
589 f += h;
590
591 p = d[m];
593 let mut c = 1.0;
594 let mut c2 = c;
595 let mut c3 = c;
596 let el1 = e[l + 1];
597 let mut s = 0.0;
598 let mut s2 = 0.0;
599
600 for i in (l..m).rev() {
601 c3 = c2;
602 c2 = c;
603 s2 = s;
604 let g = c * e[i];
605 let h = c * p;
606 r = hypot(p, e[i]);
607 e[i + 1] = s * r;
608 s = e[i] / r;
609 c = p / r;
610 p = c * d[i] - s * g;
611 d[i + 1] = h + s * (c * g + s * d[i]);
612
613 for k in 0..N {
615 let vh = v[k][i + 1];
616 v[k][i + 1] = s * v[k][i] + c * vh;
617 v[k][i] = c * v[k][i] - s * vh;
618 }
619 }
620 p = -s * s2 * c3 * el1 * e[l] / dl1;
621 e[l] = s * p;
622 d[l] = c * p;
623
624 if e[l].abs() <= eps * tst1 {
626 break;
627 }
628 }
629 }
630 d[l] += f;
631 e[l] = 0.0;
632 }
633
634 for i in 0..(N - 1) {
636 let mut k = i;
637 let mut p = d[i];
638 for j in (i + 1)..N {
639 if d[j] < p {
640 k = j;
641 p = d[j];
642 }
643 }
644 if k != i {
645 d[k] = d[i];
646 d[i] = p;
647 for j in 0..N {
648 let temp = v[j][i];
649 v[j][i] = v[j][k];
650 v[j][k] = temp;
651 }
652 }
653 }
654}
655
656#[inline]
658fn hypot(x: f64, y: f64) -> f64 {
659 (x * x + y * y).sqrt()
660}
661
662pub fn frangi_filter_3d_default(
664 data: &[f64],
665 nx: usize, ny: usize, nz: usize,
666) -> Vec<f64> {
667 let params = FrangiParams::default();
668 frangi_filter_3d(data, nx, ny, nz, ¶ms).vesselness
669}
670
671pub fn frangi_filter_3d_with_progress<F>(
678 data: &[f64],
679 nx: usize, ny: usize, nz: usize,
680 params: &FrangiParams,
681 mut progress_callback: F,
682) -> FrangiResult
683where
684 F: FnMut(usize, usize),
685{
686 let n_total = nx * ny * nz;
687
688 let mut sigmas = Vec::new();
690 let mut sigma = params.scale_range[0];
691 while sigma <= params.scale_range[1] {
692 sigmas.push(sigma);
693 sigma += params.scale_ratio;
694 }
695
696 if sigmas.is_empty() {
697 sigmas.push(params.scale_range[0]);
698 }
699
700 let total_scales = sigmas.len();
701
702 let mut best_vesselness = vec![0.0f64; n_total];
704 let mut best_scale = vec![1.0f64; n_total];
705
706 let a = 2.0 * params.alpha * params.alpha;
708 let b = 2.0 * params.beta * params.beta;
709 let c2 = 2.0 * params.c * params.c;
710
711 let sx: usize = 1;
713 let sy: usize = nx;
714 let sz: usize = nx * ny;
715
716 for (scale_idx, &sigma) in sigmas.iter().enumerate() {
718 progress_callback(scale_idx, total_scales);
719
720 let smoothed = if sigma > 0.0 {
722 gaussian_smooth_3d(data, nx, ny, nz, sigma)
723 } else {
724 data.to_vec()
725 };
726
727 let scale_factor = sigma * sigma;
729
730 for k in 2..nz.saturating_sub(2) {
736 for j in 2..ny.saturating_sub(2) {
737 for i in 2..nx.saturating_sub(2) {
738 let idx = i + j * sy + k * sz;
739 let s = smoothed[idx];
740
741 let dxx = (smoothed[idx + 2 * sx] - 2.0 * s + smoothed[idx - 2 * sx]) / 4.0;
743 let dyy = (smoothed[idx + 2 * sy] - 2.0 * s + smoothed[idx - 2 * sy]) / 4.0;
744 let dzz = (smoothed[idx + 2 * sz] - 2.0 * s + smoothed[idx - 2 * sz]) / 4.0;
745
746 let dxy = (smoothed[idx + sx + sy] - smoothed[idx - sx + sy]
748 - smoothed[idx + sx - sy] + smoothed[idx - sx - sy]) / 4.0;
749 let dxz = (smoothed[idx + sx + sz] - smoothed[idx - sx + sz]
750 - smoothed[idx + sx - sz] + smoothed[idx - sx - sz]) / 4.0;
751 let dyz = (smoothed[idx + sy + sz] - smoothed[idx - sy + sz]
752 - smoothed[idx + sy - sz] + smoothed[idx - sy - sz]) / 4.0;
753
754 let hessian_mag = dxx.abs() + dyy.abs() + dzz.abs()
756 + dxy.abs() + dxz.abs() + dyz.abs();
757 if hessian_mag < 1e-20 {
758 continue;
759 }
760
761 let h_xx = dxx * scale_factor;
763 let h_yy = dyy * scale_factor;
764 let h_zz = dzz * scale_factor;
765 let h_xy = dxy * scale_factor;
766 let h_xz = dxz * scale_factor;
767 let h_yz = dyz * scale_factor;
768
769 let (lambda1, lambda2, lambda3) = eigenvalues_3x3_cardano(
771 h_xx, h_yy, h_zz, h_xy, h_xz, h_yz
772 );
773
774 let (l1, l2, l3) = sort_by_abs(lambda1, lambda2, lambda3);
776
777 let vesselness = compute_vesselness(l1, l2, l3, a, b, c2, params.black_white);
779
780 if scale_idx == 0 || vesselness > best_vesselness[idx] {
782 best_vesselness[idx] = vesselness;
783 best_scale[idx] = sigma;
784 }
785 }
786 }
787 }
788 }
789
790 progress_callback(total_scales, total_scales);
791
792 FrangiResult {
793 vesselness: best_vesselness,
794 scale: best_scale,
795 }
796}
797
798#[cfg(test)]
799mod tests {
800 use super::*;
801
802 #[test]
803 fn test_eigenvalues_diagonal() {
804 let (l1, l2, l3) = eigenvalues_3x3_symmetric(1.0, 2.0, 3.0, 0.0, 0.0, 0.0);
806 let mut sorted = vec![l1, l2, l3];
807 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
808
809 assert!((sorted[0] - 1.0).abs() < 1e-10);
810 assert!((sorted[1] - 2.0).abs() < 1e-10);
811 assert!((sorted[2] - 3.0).abs() < 1e-10);
812 }
813
814 #[test]
815 fn test_eigenvalues_identity() {
816 let (l1, l2, l3) = eigenvalues_3x3_symmetric(1.0, 1.0, 1.0, 0.0, 0.0, 0.0);
818
819 assert!((l1 - 1.0).abs() < 1e-10);
820 assert!((l2 - 1.0).abs() < 1e-10);
821 assert!((l3 - 1.0).abs() < 1e-10);
822 }
823
824 #[test]
825 fn test_cardano_matches_householder() {
826 let test_cases = vec![
828 (1.0, 2.0, 3.0, 0.0, 0.0, 0.0), (1.0, 1.0, 1.0, 0.0, 0.0, 0.0), (2.0, 2.0, 1.0, 1.0, 0.0, 0.0), (5.0, 3.0, 1.0, 0.5, -0.3, 0.7), (0.0, 0.0, 0.0, 0.0, 0.0, 0.0), (1e-6, 2e-6, 3e-6, 1e-7, 2e-7, 3e-7), ];
835
836 for (a, b, c, d, e, f) in test_cases {
837 let (h1, h2, h3) = eigenvalues_3x3_symmetric(a, b, c, d, e, f);
838 let (c1, c2, c3) = eigenvalues_3x3_cardano(a, b, c, d, e, f);
839
840 let mut hs = vec![h1, h2, h3];
841 let mut cs = vec![c1, c2, c3];
842 hs.sort_by(|a, b| a.partial_cmp(b).unwrap());
843 cs.sort_by(|a, b| a.partial_cmp(b).unwrap());
844
845 for i in 0..3 {
846 assert!(
847 (hs[i] - cs[i]).abs() < 1e-8,
848 "Mismatch for ({},{},{},{},{},{}): Householder={:?} Cardano={:?}",
849 a, b, c, d, e, f, hs, cs
850 );
851 }
852 }
853 }
854
855 #[test]
856 fn test_gradient_constant() {
857 let data = vec![5.0; 27]; let grad = gradient_3d(&data, 3, 3, 3, 'x');
860
861 for &v in &grad {
862 assert!(v.abs() < 1e-10);
863 }
864 }
865
866 #[test]
867 fn test_frangi_filter_basic() {
868 let data = vec![0.0; 1000]; let params = FrangiParams {
871 scale_range: [1.0, 2.0],
872 scale_ratio: 1.0,
873 ..Default::default()
874 };
875
876 let result = frangi_filter_3d(&data, 10, 10, 10, ¶ms);
877 assert_eq!(result.vesselness.len(), 1000);
878 }
879
880 #[test]
881 fn test_frangi_multi_scale() {
882 let n = 10;
884 let mut data = vec![0.0; n * n * n];
885
886 let cy = n / 2;
888 let cz = n / 2;
889 for x in 0..n {
890 for dy in -1i32..=1 {
891 for dz in -1i32..=1 {
892 let y = (cy as i32 + dy) as usize;
893 let z = (cz as i32 + dz) as usize;
894 if y < n && z < n {
895 data[x + y * n + z * n * n] = 1.0;
896 }
897 }
898 }
899 }
900
901 let params = FrangiParams {
902 scale_range: [0.5, 3.0],
903 scale_ratio: 0.5, alpha: 0.5,
905 beta: 0.5,
906 c: 500.0,
907 black_white: false, };
909
910 let result = frangi_filter_3d(&data, n, n, n, ¶ms);
911
912 assert_eq!(result.vesselness.len(), n * n * n);
913 assert_eq!(result.scale.len(), n * n * n);
914
915 for &v in &result.vesselness {
917 assert!(v.is_finite(), "Vesselness must be finite");
918 assert!(v >= 0.0, "Vesselness must be >= 0");
919 assert!(v <= 1.0, "Vesselness must be <= 1");
920 }
921
922 for &s in &result.scale {
923 assert!(s.is_finite(), "Scale must be finite");
924 assert!(s >= 0.5 && s <= 3.0, "Scale must be in range [0.5, 3.0], got {}", s);
925 }
926 }
927
928 #[test]
929 fn test_frangi_different_beta_c() {
930 let n = 10;
932 let mut data = vec![0.0; n * n * n];
933
934 let cx = n / 2;
936 let cz = n / 2;
937 for y in 0..n {
938 data[cx + y * n + cz * n * n] = 10.0;
939 }
940
941 let params_default = FrangiParams {
942 scale_range: [1.0, 2.0],
943 scale_ratio: 1.0,
944 alpha: 0.5,
945 beta: 0.5,
946 c: 500.0,
947 black_white: false,
948 };
949
950 let params_small_c = FrangiParams {
951 scale_range: [1.0, 2.0],
952 scale_ratio: 1.0,
953 alpha: 0.5,
954 beta: 0.5,
955 c: 1.0, black_white: false,
957 };
958
959 let params_large_beta = FrangiParams {
960 scale_range: [1.0, 2.0],
961 scale_ratio: 1.0,
962 alpha: 0.5,
963 beta: 2.0, c: 500.0,
965 black_white: false,
966 };
967
968 let result_default = frangi_filter_3d(&data, n, n, n, ¶ms_default);
969 let result_small_c = frangi_filter_3d(&data, n, n, n, ¶ms_small_c);
970 let result_large_beta = frangi_filter_3d(&data, n, n, n, ¶ms_large_beta);
971
972 for &v in &result_default.vesselness {
974 assert!(v.is_finite());
975 }
976 for &v in &result_small_c.vesselness {
977 assert!(v.is_finite());
978 }
979 for &v in &result_large_beta.vesselness {
980 assert!(v.is_finite());
981 }
982 }
983
984 #[test]
985 fn test_frangi_black_white() {
986 let n = 10;
988 let mut data = vec![1.0; n * n * n]; let cx = n / 2;
992 let cy = n / 2;
993 for z in 0..n {
994 data[cx + cy * n + z * n * n] = 0.0; }
996
997 let params_bright = FrangiParams {
998 scale_range: [1.0, 2.0],
999 scale_ratio: 1.0,
1000 black_white: false, ..Default::default()
1002 };
1003
1004 let params_dark = FrangiParams {
1005 scale_range: [1.0, 2.0],
1006 scale_ratio: 1.0,
1007 black_white: true, ..Default::default()
1009 };
1010
1011 let result_bright = frangi_filter_3d(&data, n, n, n, ¶ms_bright);
1012 let result_dark = frangi_filter_3d(&data, n, n, n, ¶ms_dark);
1013
1014 for &v in &result_bright.vesselness {
1016 assert!(v.is_finite());
1017 assert!(v >= 0.0 && v <= 1.0);
1018 }
1019 for &v in &result_dark.vesselness {
1020 assert!(v.is_finite());
1021 assert!(v >= 0.0 && v <= 1.0);
1022 }
1023
1024 let sum_bright: f64 = result_bright.vesselness.iter().sum();
1027 let sum_dark: f64 = result_dark.vesselness.iter().sum();
1028 assert!(
1030 (sum_bright - sum_dark).abs() >= 0.0,
1031 "Bright and dark modes should both work"
1032 );
1033 }
1034
1035 #[test]
1036 fn test_frangi_vessel_structure() {
1037 let n = 12;
1040 let mut data = vec![0.0; n * n * n];
1041
1042 let cy = n as f64 / 2.0;
1044 let cz = n as f64 / 2.0;
1045 let sigma_tube = 1.5;
1046 for x in 1..(n - 1) {
1047 for y in 0..n {
1048 for z in 0..n {
1049 let dy = y as f64 - cy;
1050 let dz = z as f64 - cz;
1051 let r2 = dy * dy + dz * dz;
1052 data[x + y * n + z * n * n] = (-r2 / (2.0 * sigma_tube * sigma_tube)).exp();
1053 }
1054 }
1055 }
1056
1057 let params = FrangiParams {
1058 scale_range: [1.0, 2.0],
1059 scale_ratio: 1.0,
1060 alpha: 0.5,
1061 beta: 0.5,
1062 c: 10.0, black_white: false,
1064 };
1065
1066 let result = frangi_filter_3d(&data, n, n, n, ¶ms);
1067
1068 for &v in &result.vesselness {
1070 assert!(v.is_finite());
1071 assert!(v >= 0.0 && v <= 1.0);
1072 }
1073
1074 let max_v: f64 = result.vesselness.iter().cloned().fold(0.0, f64::max);
1076 assert!(
1077 max_v > 0.0,
1078 "Frangi should detect the vessel-like structure, max vesselness = {}",
1079 max_v
1080 );
1081 }
1082
1083 #[test]
1084 fn test_frangi_filter_3d_default() {
1085 let n = 8;
1087 let data = vec![0.0; n * n * n];
1088
1089 let vesselness = frangi_filter_3d_default(&data, n, n, n);
1090 assert_eq!(vesselness.len(), n * n * n);
1091 for &v in &vesselness {
1092 assert!(v.is_finite());
1093 }
1094 }
1095
1096 #[test]
1097 fn test_frangi_with_progress() {
1098 let n = 8;
1099 let data = vec![0.0; n * n * n];
1100 let params = FrangiParams {
1101 scale_range: [1.0, 2.0],
1102 scale_ratio: 1.0,
1103 ..Default::default()
1104 };
1105
1106 let mut progress_calls = Vec::new();
1107 let result = frangi_filter_3d_with_progress(&data, n, n, n, ¶ms, |current, total| {
1108 progress_calls.push((current, total));
1109 });
1110
1111 assert_eq!(result.vesselness.len(), n * n * n);
1112 assert!(!progress_calls.is_empty(), "Progress callback should be called");
1114 let last = progress_calls.last().unwrap();
1116 assert_eq!(last.0, last.1, "Last progress call should indicate completion");
1117 }
1118
1119 #[test]
1120 fn test_sort_by_abs() {
1121 let (a, b, c) = sort_by_abs(3.0, -1.0, 2.0);
1122 assert!((a.abs()) <= b.abs());
1123 assert!((b.abs()) <= c.abs());
1124
1125 let (a, b, c) = sort_by_abs(-5.0, 0.1, -2.0);
1126 assert!((a.abs()) <= b.abs());
1127 assert!((b.abs()) <= c.abs());
1128 }
1129
1130 #[test]
1131 fn test_compute_vesselness_zero_eigenvalues() {
1132 let v = compute_vesselness(0.0, 0.0, 0.0, 0.5, 0.5, 500.0, false);
1134 assert_eq!(v, 0.0);
1135 }
1136
1137 #[test]
1138 fn test_compute_vesselness_bright_vessel() {
1139 let v = compute_vesselness(0.01, -1.0, -1.0, 0.5, 0.5, 500.0, false);
1142 assert!(v.is_finite());
1143 assert!(v >= 0.0);
1144 }
1145
1146 #[test]
1147 fn test_compute_vesselness_dark_vessel() {
1148 let v = compute_vesselness(0.01, 1.0, 1.0, 0.5, 0.5, 500.0, true);
1150 assert!(v.is_finite());
1151 assert!(v >= 0.0);
1152 }
1153
1154 #[test]
1155 fn test_compute_vesselness_wrong_sign() {
1156 let v = compute_vesselness(0.01, 1.0, 1.0, 0.5, 0.5, 500.0, false);
1158 assert_eq!(v, 0.0);
1159
1160 let v = compute_vesselness(0.01, -1.0, -1.0, 0.5, 0.5, 500.0, true);
1162 assert_eq!(v, 0.0);
1163 }
1164
1165 #[test]
1166 fn test_gaussian_smooth_3d() {
1167 let n = 8;
1169 let data = vec![5.0; n * n * n];
1170 let smoothed = gaussian_smooth_3d(&data, n, n, n, 1.0);
1171 for &v in &smoothed {
1172 assert!((v - 5.0).abs() < 1e-6, "Smoothing constant should preserve value, got {}", v);
1173 }
1174 }
1175
1176 #[test]
1177 fn test_gaussian_smooth_3d_zero_sigma() {
1178 let n = 4;
1180 let data: Vec<f64> = (0..n * n * n).map(|i| i as f64).collect();
1181 let smoothed = gaussian_smooth_3d(&data, n, n, n, 0.0);
1182 assert_eq!(smoothed, data);
1183 }
1184
1185 #[test]
1186 fn test_gradient_3d_y_direction() {
1187 let n = 4;
1189 let mut data = vec![0.0; n * n * n];
1190 for k in 0..n {
1192 for j in 0..n {
1193 for i in 0..n {
1194 data[i + j * n + k * n * n] = j as f64;
1195 }
1196 }
1197 }
1198 let grad_y = gradient_3d(&data, n, n, n, 'y');
1199 for k in 0..n {
1201 for j in 1..(n - 1) {
1202 for i in 0..n {
1203 let idx = i + j * n + k * n * n;
1204 assert!(
1205 (grad_y[idx] - 1.0).abs() < 1e-10,
1206 "y gradient at interior should be 1.0, got {}",
1207 grad_y[idx]
1208 );
1209 }
1210 }
1211 }
1212 }
1213
1214 #[test]
1215 fn test_gradient_3d_z_direction() {
1216 let n = 4;
1218 let mut data = vec![0.0; n * n * n];
1219 for k in 0..n {
1221 for j in 0..n {
1222 for i in 0..n {
1223 data[i + j * n + k * n * n] = k as f64;
1224 }
1225 }
1226 }
1227 let grad_z = gradient_3d(&data, n, n, n, 'z');
1228 for k in 1..(n - 1) {
1230 for j in 0..n {
1231 for i in 0..n {
1232 let idx = i + j * n + k * n * n;
1233 assert!(
1234 (grad_z[idx] - 1.0).abs() < 1e-10,
1235 "z gradient at interior should be 1.0, got {}",
1236 grad_z[idx]
1237 );
1238 }
1239 }
1240 }
1241 }
1242
1243 #[test]
1244 fn test_eigenvalues_offdiagonal() {
1245 let (l1, l2, l3) = eigenvalues_3x3_symmetric(2.0, 2.0, 1.0, 1.0, 0.0, 0.0);
1249 let mut sorted = vec![l1, l2, l3];
1250 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
1251
1252 assert!((sorted[0] - 1.0).abs() < 1e-10);
1253 assert!((sorted[1] - 1.0).abs() < 1e-10);
1254 assert!((sorted[2] - 3.0).abs() < 1e-10);
1255 }
1256
1257 #[test]
1258 fn test_frangi_empty_sigmas_fallback() {
1259 let n = 8;
1262 let data = vec![0.0; n * n * n];
1263 let params = FrangiParams {
1264 scale_range: [5.0, 1.0], scale_ratio: 1.0,
1266 ..Default::default()
1267 };
1268
1269 let result = frangi_filter_3d(&data, n, n, n, ¶ms);
1270 assert_eq!(result.vesselness.len(), n * n * n);
1271 }
1272
1273 #[test]
1274 fn test_compute_hessian_3d() {
1275 let n = 8;
1277 let data: Vec<f64> = (0..n * n * n).map(|i| (i as f64 * 0.01).sin()).collect();
1278
1279 let (dxx, dyy, dzz, dxy, dxz, dyz) = compute_hessian_3d(&data, n, n, n, 1.0);
1280
1281 assert_eq!(dxx.len(), n * n * n);
1282 for &v in dxx.iter().chain(dyy.iter()).chain(dzz.iter())
1283 .chain(dxy.iter()).chain(dxz.iter()).chain(dyz.iter()) {
1284 assert!(v.is_finite(), "Hessian components must be finite");
1285 }
1286 }
1287
1288 #[test]
1289 fn test_convolve_1d_all_directions() {
1290 let n = 6;
1292 let data: Vec<f64> = (0..n * n * n).map(|i| (i as f64) * 0.1).collect();
1293 let kernel = vec![0.25, 0.5, 0.25]; let rx = convolve_1d_direction(&data, n, n, n, &kernel, 'x');
1296 let ry = convolve_1d_direction(&data, n, n, n, &kernel, 'y');
1297 let rz = convolve_1d_direction(&data, n, n, n, &kernel, 'z');
1298
1299 assert_eq!(rx.len(), n * n * n);
1300 assert_eq!(ry.len(), n * n * n);
1301 assert_eq!(rz.len(), n * n * n);
1302
1303 for &v in rx.iter().chain(ry.iter()).chain(rz.iter()) {
1304 assert!(v.is_finite());
1305 }
1306 }
1307}