1use crate::Grid;
53use super::icosphere::bet2_icosa;
54#[cfg(test)]
55use super::icosphere::create_icosphere;
56use super::mesh::{bet2_vertex_normals, build_neighbor_matrix, self_intersection_heuristic};
57
58struct BetParameters {
60 t2: f64, #[allow(dead_code)]
68 t98: f64, t: f64, tm: f64, #[allow(dead_code)]
72 cog: [f64; 3], cog_mm: [f64; 3], radius: f64, }
76
77fn estimate_brain_parameters(
79 data: &[f64],
80 nx: usize, ny: usize, nz: usize,
81 voxel_size: &[f64; 3],
82) -> BetParameters {
83 let nonzero: Vec<f64> = data.iter().copied().filter(|&v| v > 0.0).collect();
85
86 if nonzero.is_empty() {
87 let cog = [(nx as f64) / 2.0, (ny as f64) / 2.0, (nz as f64) / 2.0];
88 let cog_mm = [cog[0] * voxel_size[0], cog[1] * voxel_size[1], cog[2] * voxel_size[2]];
89 return BetParameters { t2: 0.0, t98: 1.0, t: 0.1, tm: 0.5, cog, cog_mm, radius: 50.0 };
90 }
91
92 let (t2, t98) = find_thresholds(data);
94 let t = t2 + 0.1 * (t98 - t2);
95
96 let mut sum_x = 0.0;
98 let mut sum_y = 0.0;
99 let mut sum_z = 0.0;
100 let mut sum_weight = 0.0;
101 let mut n_voxels = 0usize;
102
103 for k in 0..nz {
105 for j in 0..ny {
106 for i in 0..nx {
107 let idx = i + j * nx + k * nx * ny;
108 let val = data[idx];
109 if val > t {
110 let c = (val - t2).min(t98 - t2);
112 sum_x += (i as f64) * c;
113 sum_y += (j as f64) * c;
114 sum_z += (k as f64) * c;
115 sum_weight += c;
116 n_voxels += 1;
117 }
118 }
119 }
120 }
121
122 let cog = if sum_weight > 0.0 {
123 [sum_x / sum_weight, sum_y / sum_weight, sum_z / sum_weight]
124 } else {
125 [(nx as f64) / 2.0, (ny as f64) / 2.0, (nz as f64) / 2.0]
126 };
127
128 let voxel_volume = voxel_size[0] * voxel_size[1] * voxel_size[2];
130 let brain_volume = (n_voxels as f64) * voxel_volume;
131 let radius = (3.0 * brain_volume / (4.0 * std::f64::consts::PI)).powf(1.0 / 3.0);
132
133 let cog_mm = [cog[0] * voxel_size[0], cog[1] * voxel_size[1], cog[2] * voxel_size[2]];
136 let radius_sq = radius * radius;
137
138 let mut within_brain_values: Vec<f64> = Vec::new();
139 for k in 0..nz {
140 for j in 0..ny {
141 for i in 0..nx {
142 let idx = i + j * nx + k * nx * ny;
143 let val = data[idx];
144 if val > t2 && val < t98 {
146 let px = (i as f64) * voxel_size[0];
148 let py = (j as f64) * voxel_size[1];
149 let pz = (k as f64) * voxel_size[2];
150 let dx = px - cog_mm[0];
151 let dy = py - cog_mm[1];
152 let dz = pz - cog_mm[2];
153 let dist_sq = dx * dx + dy * dy + dz * dz;
154 if dist_sq < radius_sq {
155 within_brain_values.push(val);
156 }
157 }
158 }
159 }
160 }
161
162 let tm = if within_brain_values.is_empty() {
164 (t2 + t98) / 2.0 } else {
166 within_brain_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
168 let med = within_brain_values.len() / 2;
169 within_brain_values[med.max(1) - 1]
170 };
171
172 BetParameters { t2, t98, t, tm, cog, cog_mm, radius }
173}
174
175fn find_thresholds(data: &[f64]) -> (f64, f64) {
181 const BINS: i64 = 1000;
182 const MAX_PASSES: i32 = 10;
183 let vol: Vec<f32> = data.iter().map(|&v| v as f32).collect();
184 let vmin = vol.iter().copied().fold(f32::INFINITY, f32::min);
185 let vmax = vol.iter().copied().fold(f32::NEG_INFINITY, f32::max);
186 let (mut min, mut max) = (vmin, vmax);
187 let (mut top_bin, mut bottom_bin, mut pass) = (0i64, 0i64, 1);
188 let (mut lowest_bin, mut highest_bin) = (0i64, BINS - 1);
189 let (mut thresh2, mut thresh98) = (0.0f32, 0.0f32);
190 let mut hist = vec![0i64; BINS as usize];
191 while pass == 1 || ((thresh98 - thresh2) as f64) < ((max - min) as f64) / 10.0 {
192 if pass > 1 {
193 bottom_bin = (bottom_bin - 1).max(0);
194 top_bin = (top_bin + 1).min(BINS - 1);
195 let tmpmin = (min as f64 + (bottom_bin as f64 / BINS as f64) * (max - min) as f64) as f32;
196 max = (min as f64 + ((top_bin + 1) as f64 / BINS as f64) * (max - min) as f64) as f32;
197 min = tmpmin;
198 }
199 if pass == MAX_PASSES || min == max {
200 min = vmin;
201 max = vmax;
202 }
203 hist.iter_mut().for_each(|h| *h = 0);
205 let mut validsize: i64 = if min == max { -1 } else { 0 };
206 if min != max {
207 let fa = BINS as f64 / (max - min) as f64;
208 let fb = (BINS as f64 * (-(min as f64))) / (max - min) as f64;
209 for &v in &vol {
210 let b = ((fa * v as f64 + fb) as i64).clamp(0, BINS - 1);
211 hist[b as usize] += 1;
212 validsize += 1;
213 }
214 }
215 if validsize < 1 {
216 return (min as f64, max as f64);
217 }
218 if pass == MAX_PASSES {
219 validsize -= hist[lowest_bin as usize] + hist[highest_bin as usize];
220 lowest_bin += 1;
221 highest_bin -= 1;
222 }
223 if validsize < 0 {
224 thresh2 = min;
225 thresh98 = min;
226 break;
227 }
228 let fa = (max - min) as f64 / BINS as f64;
229 let mut count = 0i64;
230 bottom_bin = lowest_bin;
231 while count < validsize / 50 {
232 count += hist[bottom_bin as usize];
233 bottom_bin += 1;
234 }
235 bottom_bin -= 1;
236 thresh2 = min + (bottom_bin as f64 * fa) as f32;
237 count = 0;
238 top_bin = highest_bin;
239 while count < validsize / 50 {
240 count += hist[top_bin as usize];
241 top_bin -= 1;
242 }
243 top_bin += 1;
244 thresh98 = min + ((top_bin + 1) as f64 * fa) as f32;
245 if pass == MAX_PASSES {
246 break;
247 }
248 pass += 1;
249 }
250 (thresh2 as f64, thresh98 as f64)
251}
252
253fn sample_intensities_fsl(
265 data: &[f64],
266 nx: usize, ny: usize, nz: usize,
267 point_mm: &[f64; 3],
268 normal: &[f64; 3],
269 voxel_size: &[f64; 3],
270 t2: f64,
271 t: f64,
272 tm: f64,
273) -> (f64, f64, bool) {
274 const D1: f64 = 7.0;
277 const D2: f64 = 3.0;
278 let (xdim, ydim, zdim) = (voxel_size[0], voxel_size[1], voxel_size[2]);
279 let dscale = xdim.min(ydim).min(zdim).min(1.0);
280 let in_bounds = |i: i64, j: i64, k: i64| i >= 0 && j >= 0 && k >= 0 && (i as usize) < nx && (j as usize) < ny && (k as usize) < nz;
281 let n_total = (nx * ny * nz) as i64;
286 let value = |i: i64, j: i64, k: i64| {
287 let lin = (k * ny as i64 + j) * nx as i64 + i;
288 if (0..n_total).contains(&lin) { data[lin as usize] } else { 0.0 }
289 };
290 let mut i_min = tm;
291 let mut i_max = t;
292 let p = [point_mm[0] - normal[0], point_mm[1] - normal[1], point_mm[2] - normal[2]]; let (mut iv, mut jv, mut kv) = (p[0] / xdim + 0.5, p[1] / ydim + 0.5, p[2] / zdim + 0.5);
294 if !in_bounds(iv as i64, jv as i64, kv as i64) {
295 return (i_min, i_max, false);
296 }
297 let im = value(iv as i64, jv as i64, kv as i64);
298 i_min = i_min.min(im);
299 i_max = i_max.max(im);
300 let (mut nxv, mut nyv, mut nzv) = (normal[0] / xdim, normal[1] / ydim, normal[2] / zdim);
301 let (i2, j2, k2) = ((iv - (D1 - 1.0) * nxv) as i64, (jv - (D1 - 1.0) * nyv) as i64, (kv - (D1 - 1.0) * nzv) as i64);
302 nxv *= dscale;
303 nyv *= dscale;
304 nzv *= dscale;
305 if !in_bounds(i2, j2, k2) {
306 return (i_min, i_max, false);
307 }
308 let im = value(i2, j2, k2);
309 i_min = i_min.min(im);
310 let mut gi = 2.0;
311 while gi < D1 {
312 iv -= nxv;
313 jv -= nyv;
314 kv -= nzv;
315 let im = value(iv as i64, jv as i64, kv as i64);
316 i_min = i_min.min(im);
317 if gi < D2 {
318 i_max = i_max.max(im);
319 }
320 gi += dscale;
321 }
322 i_min = i_min.max(t2);
323 i_max = i_max.min(tm);
324 (i_min, i_max, true)
325}
326
327fn surface_to_mask(
332 vertices_mm: &[[f64; 3]],
333 faces: &[[usize; 3]],
334 nx: usize, ny: usize, nz: usize,
335 voxel_size: &[f64; 3],
336) -> Vec<u8> {
337 let (xdim, ydim, zdim) = (voxel_size[0], voxel_size[1], voxel_size[2]);
342 let mininc = xdim.min(ydim).min(zdim) * 0.5;
343 let mut grid: Vec<u8> = vec![1; nx * ny * nz];
344 let idx = |i: usize, j: usize, k: usize| i + j * nx + k * nx * ny;
345 let set0 = |p: [f64; 3], grid: &mut Vec<u8>| {
346 let (i, j, k) = ((p[0] / xdim + 0.5).floor(), (p[1] / ydim + 0.5).floor(), (p[2] / zdim + 0.5).floor());
347 if i >= 0.0 && j >= 0.0 && k >= 0.0 && (i as usize) < nx && (j as usize) < ny && (k as usize) < nz {
348 grid[idx(i as usize, j as usize, k as usize)] = 0;
349 }
350 };
351 let unit = |a: [f64; 3], b: [f64; 3]| -> ([f64; 3], f64) {
352 let v = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
353 let d = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
354 if d > 0.0 { ([v[0] / d, v[1] / d, v[2] / d], d) } else { (v, d) }
355 };
356 for &[i0, i1, i2] in faces {
357 let (v0, v1, v2) = (vertices_mm[i0], vertices_mm[i1], vertices_mm[i2]);
358 let (n, d) = unit(v0, v1);
359 let mut j = 0.0;
360 while j <= d {
361 let p = [v1[0] + j * n[0], v1[1] + j * n[1], v1[2] + j * n[2]];
362 let (m, dd) = unit(p, v2);
364 let mut t = 0.0;
365 while t <= dd {
366 set0([v2[0] + t * m[0], v2[1] + t * m[1], v2[2] + t * m[2]], &mut grid);
367 t += mininc;
368 }
369 j += mininc;
370 }
371 }
372 let mut c = [0.0f64; 3];
373 for v in vertices_mm {
374 c[0] += v[0];
375 c[1] += v[1];
376 c[2] += v[2];
377 }
378 let inv = 1.0 / vertices_mm.len() as f64;
379 let (cx, cy, cz) = ((c[0] * inv / xdim) as i64, (c[1] * inv / ydim) as i64, (c[2] * inv / zdim) as i64);
380 let mut stack: Vec<(i64, i64, i64)> = vec![(cx, cy, cz)];
381 let inside = |x: i64, y: i64, z: i64| x >= 0 && y >= 0 && z >= 0 && (x as usize) < nx && (y as usize) < ny && (z as usize) < nz;
382 while let Some((x, y, z)) = stack.pop() {
383 if !inside(x, y, z) {
384 continue;
385 }
386 grid[idx(x as usize, y as usize, z as usize)] = 0;
387 for (dx, dy, dz) in [(-1, 0, 0), (0, -1, 0), (0, 0, -1), (1, 0, 0), (0, 1, 0), (0, 0, 1)] {
388 let (a, b, c) = (x + dx, y + dy, z + dz);
389 if inside(a, b, c) && grid[idx(a as usize, b as usize, c as usize)] == 1 {
390 stack.push((a, b, c));
391 }
392 }
393 }
394 grid.iter().map(|&v| (v == 0) as u8).collect()
395}
396
397
398const SELF_INTERSECTION_THRESHOLD: f64 = 4000.0;
400
401const MAX_PASSES: usize = 10;
403
404fn evolution_pass(
408 data: &[f64],
409 nx: usize, ny: usize, nz: usize,
410 voxel_size: &[f64; 3],
411 bp: &BetParameters,
412 vertices: &mut [[f64; 3]],
413 faces: &[[usize; 3]],
414 neighbor_matrix: &[Vec<usize>],
415 neighbor_counts: &[usize],
416 bt: f64,
417 smoothness_factor: f64,
418 gradient_threshold: f64,
419 iterations: usize,
420 pass: usize,
421 progress_callback: &mut Option<&mut dyn FnMut(usize, usize)>,
422) {
423 let n_vertices = vertices.len();
424
425 let rmin = 3.33 * smoothness_factor;
427 let rmax = 10.0 * smoothness_factor;
428 let e = (1.0 / rmin + 1.0 / rmax) / 2.0;
429 let f = 6.0 / (1.0 / rmin - 1.0 / rmax);
430 let normal_max_update_fraction = 0.5;
431 let lambda_fit = 0.1;
432
433 let mut l = 0.0;
436 let mean_neighbour_distance = |vs: &[[f64; 3]]| -> f64 {
437 let mut total = 0.0;
438 for i in 0..vs.len() {
439 let mut li = 0.0;
440 for j in 0..neighbor_counts[i] {
441 let q = vs[neighbor_matrix[i][j]];
442 li += ((q[0] - vs[i][0]).powi(2) + (q[1] - vs[i][1]).powi(2) + (q[2] - vs[i][2]).powi(2)).sqrt();
443 }
444 total += li / neighbor_counts[i] as f64;
445 }
446 total / vs.len() as f64
447 };
448
449 let base_increase = if pass > 0 { 10.0_f64.powi((pass + 1) as i32) } else { 1.0 };
451
452 let progress_interval = (iterations / 20).max(1);
454
455 let mut sample_fail_count: usize = 0;
457
458 for iteration in 0..iterations {
459 if let Some(ref mut cb) = progress_callback {
461 if iteration % progress_interval == 0 {
462 cb(iteration, iterations);
463 }
464 }
465
466 let incfactor = if pass > 0 && iteration > (0.75 * iterations as f64) as usize {
468 let t = iteration as f64 / iterations as f64;
469 4.0 * (1.0 - t) * (base_increase - 1.0) + 1.0
470 } else {
471 base_increase
472 };
473
474 if iteration == 50 || iteration % 100 == 0 {
475 l = mean_neighbour_distance(vertices);
476 }
477 let normals = bet2_vertex_normals(vertices, faces);
479
480 let mut updates: Vec<[f64; 3]> = vec![[0.0, 0.0, 0.0]; n_vertices];
482
483 for i in 0..n_vertices {
484 let v = vertices[i];
485 let n = normals[i];
486
487 let mut mean_neighbor = [0.0, 0.0, 0.0];
489 let count = neighbor_counts[i];
490 for j in 0..count {
491 let ni = neighbor_matrix[i][j];
492 mean_neighbor[0] += vertices[ni][0];
493 mean_neighbor[1] += vertices[ni][1];
494 mean_neighbor[2] += vertices[ni][2];
495 }
496 if count > 0 {
497 mean_neighbor[0] /= count as f64;
498 mean_neighbor[1] /= count as f64;
499 mean_neighbor[2] /= count as f64;
500 }
501
502 let dv = [mean_neighbor[0] - v[0], mean_neighbor[1] - v[1], mean_neighbor[2] - v[2]];
504
505 let dv_dot_n = dv[0] * n[0] + dv[1] * n[1] + dv[2] * n[2];
507
508 let sn = [dv_dot_n * n[0], dv_dot_n * n[1], dv_dot_n * n[2]];
510
511 let st = [dv[0] - sn[0], dv[1] - sn[1], dv[2] - sn[2]];
513
514 let u1 = [st[0] * 0.5, st[1] * 0.5, st[2] * 0.5];
516
517 let sn_mag = dv_dot_n.abs();
519 let rinv = (2.0 * sn_mag) / (l * l);
520 let mut f2 = (1.0 + (f * (rinv - e)).tanh()) * 0.5;
521
522 if pass > 0 && dv_dot_n > 0.0 {
524 f2 *= incfactor;
525 f2 = f2.min(1.0);
526 }
527
528 let u2 = [f2 * sn[0], f2 * sn[1], f2 * sn[2]];
529
530 let (i_min, i_max, sample_ok) = sample_intensities_fsl(
532 data, nx, ny, nz, &v, &n, voxel_size, bp.t2, bp.t, bp.tm
533 );
534
535 let u3 = if sample_ok {
537 let local_bt = if gradient_threshold.abs() > 1e-10 {
539 let z_offset = (v[2] - bp.cog_mm[2]) / bp.radius;
541 (bt + gradient_threshold * z_offset).clamp(0.0, 1.0)
542 } else {
543 bt
544 };
545
546 let t_l = (i_max - bp.t2) * local_bt + bp.t2;
548 let f3 = if i_max - bp.t2 > 0.0 {
549 2.0 * (i_min - t_l) / (i_max - bp.t2)
550 } else {
551 2.0 * (i_min - t_l)
552 };
553 let f3 = f3 * normal_max_update_fraction * lambda_fit * l;
554
555 [f3 * n[0], f3 * n[1], f3 * n[2]]
556 } else {
557 sample_fail_count += 1;
559 [0.0, 0.0, 0.0]
560 };
561
562 updates[i] = [u1[0] + u2[0] + u3[0], u1[1] + u2[1] + u3[1], u1[2] + u2[2] + u3[2]];
564 }
565
566 for i in 0..n_vertices {
568 vertices[i][0] += updates[i][0];
569 vertices[i][1] += updates[i][1];
570 vertices[i][2] += updates[i][2];
571 }
572
573 }
574
575 if sample_fail_count > 0 {
577 let total_samples = iterations * n_vertices;
578 let fail_pct = 100.0 * sample_fail_count as f64 / total_samples as f64;
579 eprintln!("[BET] Sampling fallback used: {} / {} ({:.1}%)",
580 sample_fail_count, total_samples, fail_pct);
581 }
582}
583
584#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
586#[derive(Clone, Debug, PartialEq)]
587pub struct BetParams {
588 pub fractional_intensity: f64,
590 pub smoothness: f64,
592 pub gradient_threshold: f64,
594 pub iterations: usize,
596 pub subdivisions: usize,
598 pub voxel_scale: f64,
612}
613
614impl Default for BetParams {
615 fn default() -> Self {
616 Self {
617 fractional_intensity: 0.5,
618 smoothness: 1.0,
619 gradient_threshold: 0.0,
620 iterations: 1000,
621 subdivisions: 4,
622 voxel_scale: 1.0,
623 }
624 }
625}
626
627#[derive(Clone, Copy, Debug, PartialEq, Eq)]
633pub enum FslLeftRightOrder {
634 Radiological,
636 Neurological,
638}
639
640impl FslLeftRightOrder {
641 pub fn from_affine(affine: Option<&[f64; 16]>) -> Self {
645 match affine {
646 Some(a) => {
647 let det = a[0] * (a[5] * a[10] - a[6] * a[9]) - a[1] * (a[4] * a[10] - a[6] * a[8])
648 + a[2] * (a[4] * a[9] - a[5] * a[8]);
649 if det >= 1e-12 { Self::Neurological } else { Self::Radiological }
650 }
651 None => Self::Radiological,
652 }
653 }
654}
655
656pub fn run_bet<F>(
669 data: &[f64],
670 grid: &Grid,
671 params: &BetParams,
672 order: FslLeftRightOrder,
673 progress: F,
674) -> Vec<u8>
675where
676 F: FnMut(usize, usize),
677{
678 match order {
679 FslLeftRightOrder::Radiological => run_bet_as_stored(data, grid, params, progress),
680 FslLeftRightOrder::Neurological => {
681 let (nx, ny, nz) = grid.dims;
682 let flip_x = |i: usize| (nx - 1 - i % nx) + nx * (i / nx);
683 let flipped: Vec<f64> = (0..nx * ny * nz).map(|i| data[flip_x(i)]).collect();
684 let mask = run_bet_as_stored(&flipped, grid, params, progress);
685 (0..nx * ny * nz).map(|i| mask[flip_x(i)]).collect()
686 }
687 }
688}
689
690fn run_bet_as_stored<F>(
691 data: &[f64],
692 grid: &Grid,
693 params: &BetParams,
694 mut progress: F,
695) -> Vec<u8>
696where
697 F: FnMut(usize, usize),
698{
699 let fractional_intensity = params.fractional_intensity as f32 as f64;
701 let smoothness_factor = params.smoothness as f32 as f64;
702 let gradient_threshold = params.gradient_threshold as f32 as f64;
703 let iterations = params.iterations;
704 let subdivisions = params.subdivisions;
705 let (nx, ny, nz) = grid.dims;
706 let scale = if params.voxel_scale > 0.0 { params.voxel_scale } else { 1.0 };
711 let voxel_size = [grid.vsx() * scale, grid.vsy() * scale, grid.vsz() * scale];
712
713 progress(0, iterations);
715 let bp = estimate_brain_parameters(data, nx, ny, nz, &voxel_size);
716
717 let (unit_vertices, faces) = bet2_icosa(subdivisions + 1);
720 let n_vertices = unit_vertices.len();
721
722 let initial_radius_mm = bp.radius * 0.5;
725
726 let initial_vertices: Vec<[f64; 3]> = unit_vertices
727 .iter()
728 .map(|v| [
729 v[0] * initial_radius_mm + bp.cog_mm[0],
730 v[1] * initial_radius_mm + bp.cog_mm[1],
731 v[2] * initial_radius_mm + bp.cog_mm[2],
732 ])
733 .collect();
734
735 let (neighbor_matrix, neighbor_counts) = build_neighbor_matrix(n_vertices, &faces, 6);
737
738 let bt = fractional_intensity.powf(0.275);
741
742 let mut vertices = initial_vertices.clone();
744 let mut pass = 0;
745
746 loop {
747 let mut cb: Option<&mut dyn FnMut(usize, usize)> = Some(&mut progress);
749 evolution_pass(
750 data, nx, ny, nz, &voxel_size, &bp,
751 &mut vertices, &faces,
752 &neighbor_matrix, &neighbor_counts,
753 bt, smoothness_factor, gradient_threshold,
754 iterations, pass,
755 &mut cb,
756 );
757
758 let si_score = self_intersection_heuristic(&vertices, &initial_vertices, &faces, &voxel_size);
760 let has_self_intersection = si_score > SELF_INTERSECTION_THRESHOLD;
761
762 if has_self_intersection {
763 eprintln!("[BET] Self-intersection detected (score={:.0}, threshold={}), pass {}",
764 si_score, SELF_INTERSECTION_THRESHOLD, pass + 1);
765 }
766
767 if !has_self_intersection || pass >= MAX_PASSES {
769 if pass > 0 {
770 eprintln!("[BET] Completed after {} recovery pass(es)", pass);
771 }
772 break;
773 }
774
775 vertices = initial_vertices.clone();
777 pass += 1;
778 }
779
780 progress(iterations, iterations);
782
783 surface_to_mask(&vertices, &faces, nx, ny, nz, &voxel_size)
785}
786
787#[cfg(test)]
788mod tests {
789 use super::*;
790
791 fn make_sphere_volume(n: usize, radius: f64, intensity: f64) -> (Vec<f64>, usize, usize, usize) {
795 let center = (n as f64) / 2.0;
796 let mut data = vec![0.0; n * n * n];
797 for k in 0..n {
798 for j in 0..n {
799 for i in 0..n {
800 let di = (i as f64) - center;
801 let dj = (j as f64) - center;
802 let dk = (k as f64) - center;
803 let dist = (di * di + dj * dj + dk * dk).sqrt();
804 if dist <= radius {
805 data[i + j * n + k * n * n] = intensity * (1.0 - 0.5 * dist / radius);
807 }
808 }
809 }
810 }
811 (data, n, n, n)
812 }
813
814 #[test]
815 fn test_estimate_brain_parameters() {
816 let nx = 10;
817 let ny = 10;
818 let nz = 10;
819 let mut data = vec![0.0; nx * ny * nz];
820
821 for k in 0..nz {
824 for j in 0..ny {
825 for i in 0..nx {
826 let di = (i as f64) - 5.0;
827 let dj = (j as f64) - 5.0;
828 let dk = (k as f64) - 5.0;
829 let dist = (di*di + dj*dj + dk*dk).sqrt();
830 if dist <= 4.0 {
831 data[i + j * nx + k * nx * ny] = 150.0 - dist * 25.0;
833 }
834 }
835 }
836 }
837
838 let bp = estimate_brain_parameters(&data, nx, ny, nz, &[1.0, 1.0, 1.0]);
839
840 assert!(bp.t2 >= 0.0);
841 assert!(bp.t98 >= bp.t2); assert!((bp.cog[0] - 5.0).abs() < 1.0);
843 assert!((bp.cog[1] - 5.0).abs() < 1.0);
844 assert!((bp.cog[2] - 5.0).abs() < 1.0);
845 assert!(bp.radius > 0.0);
846 assert!(bp.tm > bp.t2 && bp.tm < bp.t98); assert!((bp.cog_mm[0] - bp.cog[0]).abs() < 1e-10);
849 }
850
851 #[test]
852 fn test_estimate_brain_parameters_empty_data() {
853 let nx = 4;
855 let ny = 4;
856 let nz = 4;
857 let data = vec![0.0; nx * ny * nz];
858 let bp = estimate_brain_parameters(&data, nx, ny, nz, &[1.0, 1.0, 1.0]);
859
860 assert!((bp.t2 - 0.0).abs() < 1e-10);
861 assert!((bp.t98 - 1.0).abs() < 1e-10);
862 assert!((bp.t - 0.1).abs() < 1e-10);
863 assert!((bp.tm - 0.5).abs() < 1e-10);
864 assert!((bp.cog[0] - 2.0).abs() < 1e-10);
865 assert!((bp.cog[1] - 2.0).abs() < 1e-10);
866 assert!((bp.cog[2] - 2.0).abs() < 1e-10);
867 assert!((bp.radius - 50.0).abs() < 1e-10);
868 }
869
870 #[test]
871 fn test_estimate_brain_parameters_anisotropic_voxels() {
872 let (data, nx, ny, nz) = make_sphere_volume(12, 4.0, 200.0);
873 let voxel_size = [2.0, 2.0, 2.0];
874 let bp = estimate_brain_parameters(&data, nx, ny, nz, &voxel_size);
875
876 assert!((bp.cog_mm[0] - bp.cog[0] * voxel_size[0]).abs() < 1e-10);
878 assert!((bp.cog_mm[1] - bp.cog[1] * voxel_size[1]).abs() < 1e-10);
879 assert!((bp.cog_mm[2] - bp.cog[2] * voxel_size[2]).abs() < 1e-10);
880 assert!(bp.radius > 0.0);
881 assert!(bp.t98 > bp.t2);
882 }
883
884 #[test]
885 fn test_power_transform() {
886 let f = 0.5_f64;
888 let bt = f.powf(0.275);
889 assert!((bt - 0.826).abs() < 0.01);
891 }
892
893 #[test]
894 fn test_sample_intensities_fsl_basic() {
895 let n = 16;
897 let data = vec![100.0; n * n * n];
898 let voxel_size = [1.0, 1.0, 1.0];
899 let point_mm = [8.0, 8.0, 8.0];
901 let normal = [0.0, 0.0, 1.0]; let t2 = 10.0;
904 let t = 20.0;
905 let tm = 100.0;
906
907 let (i_min, i_max, ok) = sample_intensities_fsl(
908 &data, n, n, n, &point_mm, &normal, &voxel_size, t2, t, tm,
909 );
910
911 assert!(ok, "Sampling should succeed for center point");
912 assert!(i_min.is_finite());
913 assert!(i_max.is_finite());
914 assert!(i_min >= t2, "i_min={} should be >= t2={}", i_min, t2);
916 assert!(i_max <= tm, "i_max={} should be <= tm={}", i_max, tm);
918 }
919
920 #[test]
921 fn test_sample_intensities_fsl_out_of_bounds() {
922 let n = 8;
924 let data = vec![100.0; n * n * n];
925 let voxel_size = [1.0, 1.0, 1.0];
926 let point_mm = [0.5, 0.5, 0.5];
928 let normal = [-1.0, 0.0, 0.0]; let (_, _, _ok) = sample_intensities_fsl(
931 &data, n, n, n, &point_mm, &normal, &voxel_size, 10.0, 20.0, 100.0,
932 );
933
934 let point_mm2 = [0.5, 0.5, 0.5];
937 let normal2 = [1.0, 0.0, 0.0]; let (_, _, ok2) = sample_intensities_fsl(
939 &data, n, n, n, &point_mm2, &normal2, &voxel_size, 10.0, 20.0, 100.0,
940 );
941 assert!(!ok2, "Sampling should fail when initial step goes out of bounds");
942 }
943
944 #[test]
945 fn test_sample_intensities_fsl_varying_intensity() {
946 let n = 16;
948 let mut data = vec![0.0; n * n * n];
949 for k in 0..n {
950 for j in 0..n {
951 for i in 0..n {
952 data[i + j * n + k * n * n] = 50.0 + (k as f64) * 10.0;
953 }
954 }
955 }
956 let voxel_size = [1.0, 1.0, 1.0];
957 let point_mm = [8.0, 8.0, 8.0];
958 let normal = [0.0, 0.0, -1.0];
960
961 let t2 = 50.0;
962 let t = 55.0;
963 let tm = 120.0;
964
965 let (i_min, i_max, ok) = sample_intensities_fsl(
966 &data, n, n, n, &point_mm, &normal, &voxel_size, t2, t, tm,
967 );
968
969 assert!(ok, "Sampling should succeed");
970 assert!(i_min.is_finite() && i_max.is_finite());
971 assert!(i_min >= t2);
972 assert!(i_max <= tm);
973 }
974
975 #[test]
976 fn test_surface_to_mask_small_sphere() {
977 let (unit_verts, faces) = create_icosphere(2);
979 let n = 16;
980 let center = (n as f64) / 2.0;
981 let radius = 5.0; let voxel_size = [1.0, 1.0, 1.0];
983
984 let vertices_mm: Vec<[f64; 3]> = unit_verts
986 .iter()
987 .map(|v| [
988 v[0] * radius + center,
989 v[1] * radius + center,
990 v[2] * radius + center,
991 ])
992 .collect();
993
994 let mask = surface_to_mask(&vertices_mm, &faces, n, n, n, &voxel_size);
995
996 assert_eq!(mask.len(), n * n * n);
997
998 let idx = |i: usize, j: usize, k: usize| i + j * n + k * n * n;
1000 assert_eq!(mask[idx(8, 8, 8)], 1, "Center should be brain");
1001
1002 assert_eq!(mask[idx(0, 0, 0)], 0, "Corner should be background");
1004 assert_eq!(mask[idx(15, 15, 15)], 0, "Corner should be background");
1005
1006 let brain_count: usize = mask.iter().map(|&v| v as usize).sum();
1008 assert!(brain_count > 0, "Should have some brain voxels");
1009 assert!(brain_count > 100 && brain_count < 2000,
1012 "Brain voxels ({}) should be roughly sphere-like", brain_count);
1013 }
1014
1015 #[test]
1016 fn test_surface_to_mask_anisotropic_voxels() {
1017 let (unit_verts, faces) = create_icosphere(1);
1018 let n = 16;
1019 let voxel_size = [2.0, 2.0, 2.0];
1020 let center_mm = [(n as f64) * voxel_size[0] / 2.0,
1021 (n as f64) * voxel_size[1] / 2.0,
1022 (n as f64) * voxel_size[2] / 2.0];
1023 let radius_mm = 8.0;
1024
1025 let vertices_mm: Vec<[f64; 3]> = unit_verts
1026 .iter()
1027 .map(|v| [
1028 v[0] * radius_mm + center_mm[0],
1029 v[1] * radius_mm + center_mm[1],
1030 v[2] * radius_mm + center_mm[2],
1031 ])
1032 .collect();
1033
1034 let mask = surface_to_mask(&vertices_mm, &faces, n, n, n, &voxel_size);
1035 assert_eq!(mask.len(), n * n * n);
1036
1037 let brain_count: usize = mask.iter().map(|&v| v as usize).sum();
1038 assert!(brain_count > 0, "Should have brain voxels with anisotropic voxels");
1039 }
1040
1041 #[test]
1042 fn test_run_bet_small_synthetic_volume() {
1043 let (data, nx, ny, nz) = make_sphere_volume(16, 6.0, 200.0);
1045 let grid = Grid::new(nx, ny, nz, 1.0, 1.0, 1.0);
1046
1047 let mask = run_bet(
1048 &data,
1049 &grid,
1050 &BetParams { fractional_intensity: 0.5, smoothness: 1.0, gradient_threshold: 0.0, iterations: 50, subdivisions: 1, voxel_scale: 1.0 },
1051 FslLeftRightOrder::Radiological, |_, _| {},
1052 );
1053
1054 assert_eq!(mask.len(), nx * ny * nz);
1055
1056 assert!(mask.iter().all(|&v| v == 0 || v == 1), "Mask should be binary");
1058
1059 let brain_count: usize = mask.iter().map(|&v| v as usize).sum();
1061 assert!(brain_count > 0, "BET should extract some brain voxels");
1062
1063 let idx = |i: usize, j: usize, k: usize| i + j * nx + k * nx * ny;
1065 assert_eq!(mask[idx(8, 8, 8)], 1, "Center of sphere should be brain");
1066 }
1067
1068 #[test]
1072 fn test_voxel_scale_matches_an_equally_scaled_grid() {
1073 let (data, nx, ny, nz) = make_sphere_volume(16, 6.0, 200.0);
1074 let base = BetParams {
1075 fractional_intensity: 0.5, smoothness: 1.0, gradient_threshold: 0.0,
1076 iterations: 50, subdivisions: 1, voxel_scale: 1.0,
1077 };
1078
1079 let scaled_grid = Grid::new(nx, ny, nz, 3.0, 4.5, 6.0);
1081 let via_grid = run_bet(&data, &scaled_grid, &base, FslLeftRightOrder::Radiological, |_, _| {});
1082
1083 let native_grid = Grid::new(nx, ny, nz, 0.3, 0.45, 0.6);
1084 let via_param = run_bet(
1085 &data, &native_grid,
1086 &BetParams { voxel_scale: 10.0, ..base.clone() },
1087 FslLeftRightOrder::Radiological, |_, _| {},
1088 );
1089
1090 assert_eq!(via_param, via_grid, "voxel_scale must equal scaling the grid itself");
1091 }
1092
1093 #[test]
1096 fn test_non_positive_voxel_scale_is_treated_as_unscaled() {
1097 let (data, nx, ny, nz) = make_sphere_volume(16, 6.0, 200.0);
1098 let grid = Grid::new(nx, ny, nz, 1.0, 1.0, 1.0);
1099 let base = BetParams {
1100 fractional_intensity: 0.5, smoothness: 1.0, gradient_threshold: 0.0,
1101 iterations: 50, subdivisions: 1, voxel_scale: 1.0,
1102 };
1103 let unscaled = run_bet(&data, &grid, &base, FslLeftRightOrder::Radiological, |_, _| {});
1104
1105 for bad in [0.0, -1.0] {
1106 let mask = run_bet(&data, &grid, &BetParams { voxel_scale: bad, ..base.clone() }, FslLeftRightOrder::Radiological, |_, _| {});
1107 assert_eq!(mask, unscaled, "voxel_scale {bad} should behave as 1.0");
1108 }
1109 }
1110
1111 #[test]
1112 fn test_run_bet_with_gradient_threshold() {
1113 let (data, nx, ny, nz) = make_sphere_volume(16, 6.0, 200.0);
1115 let grid = Grid::new(nx, ny, nz, 1.0, 1.0, 1.0);
1116
1117 let mask = run_bet(
1118 &data,
1119 &grid,
1120 &BetParams { fractional_intensity: 0.5, smoothness: 1.0, gradient_threshold: 0.3, iterations: 50, subdivisions: 1, voxel_scale: 1.0 },
1121 FslLeftRightOrder::Radiological, |_, _| {},
1122 );
1123
1124 assert_eq!(mask.len(), nx * ny * nz);
1125 assert!(mask.iter().all(|&v| v == 0 || v == 1));
1126 let brain_count: usize = mask.iter().map(|&v| v as usize).sum();
1127 assert!(brain_count > 0, "BET with gradient should extract brain voxels");
1128 }
1129
1130 #[test]
1131 fn test_run_bet_with_progress() {
1132 let (data, nx, ny, nz) = make_sphere_volume(16, 6.0, 200.0);
1134 let grid = Grid::new(nx, ny, nz, 1.0, 1.0, 1.0);
1135
1136 let mut progress_calls = Vec::new();
1137 let mask = run_bet(
1138 &data,
1139 &grid,
1140 &BetParams { fractional_intensity: 0.5, smoothness: 1.0, gradient_threshold: 0.0, iterations: 50, subdivisions: 1, voxel_scale: 1.0 },
1141 FslLeftRightOrder::Radiological, |current, total| {
1142 progress_calls.push((current, total));
1143 },
1144 );
1145
1146 assert_eq!(mask.len(), nx * ny * nz);
1147 assert!(mask.iter().all(|&v| v == 0 || v == 1));
1148
1149 assert!(progress_calls.len() >= 2,
1151 "Progress callback should be called at least twice, got {} calls",
1152 progress_calls.len());
1153
1154 assert_eq!(progress_calls[0].0, 0);
1156
1157 let last = progress_calls.last().unwrap();
1159 assert_eq!(last.0, last.1, "Final progress should be complete");
1160 }
1161
1162 #[test]
1163 fn test_run_bet_different_fractional_intensities() {
1164 let (data, nx, ny, nz) = make_sphere_volume(16, 6.0, 200.0);
1165 let grid = Grid::new(nx, ny, nz, 1.0, 1.0, 1.0);
1166
1167 let mask_small = run_bet(
1169 &data,
1170 &grid,
1171 &BetParams { fractional_intensity: 0.7, smoothness: 1.0, gradient_threshold: 0.0, iterations: 50, subdivisions: 1, voxel_scale: 1.0 },
1172 FslLeftRightOrder::Radiological, |_, _| {},
1173 );
1174 let mask_large = run_bet(
1175 &data,
1176 &grid,
1177 &BetParams { fractional_intensity: 0.3, smoothness: 1.0, gradient_threshold: 0.0, iterations: 50, subdivisions: 1, voxel_scale: 1.0 },
1178 FslLeftRightOrder::Radiological, |_, _| {},
1179 );
1180
1181 let count_small: usize = mask_small.iter().map(|&v| v as usize).sum();
1182 let count_large: usize = mask_large.iter().map(|&v| v as usize).sum();
1183
1184 assert!(count_small > 0);
1186 assert!(count_large > 0);
1187 }
1188
1189 #[test]
1190 fn test_run_bet_anisotropic_voxels() {
1191 let (data, nx, ny, nz) = make_sphere_volume(16, 6.0, 200.0);
1192 let grid = Grid::new(nx, ny, nz, 2.0, 2.0, 2.0);
1193
1194 let mask = run_bet(
1195 &data,
1196 &grid,
1197 &BetParams { fractional_intensity: 0.5, smoothness: 1.0, gradient_threshold: 0.0, iterations: 50, subdivisions: 1, voxel_scale: 1.0 },
1198 FslLeftRightOrder::Radiological, |_, _| {},
1199 );
1200
1201 assert_eq!(mask.len(), nx * ny * nz);
1202 assert!(mask.iter().all(|&v| v == 0 || v == 1));
1203 }
1204
1205 #[test]
1206 fn test_evolution_pass_basic() {
1207 let n = 16;
1209 let (data, nx, ny, nz) = make_sphere_volume(n, 6.0, 200.0);
1210 let voxel_size = [1.0, 1.0, 1.0];
1211 let bp = estimate_brain_parameters(&data, nx, ny, nz, &voxel_size);
1212
1213 let (unit_verts, faces) = create_icosphere(1);
1214 let n_vertices = unit_verts.len();
1215 let initial_radius_mm = bp.radius * 0.5;
1216
1217 let mut vertices: Vec<[f64; 3]> = unit_verts
1218 .iter()
1219 .map(|v| [
1220 v[0] * initial_radius_mm + bp.cog_mm[0],
1221 v[1] * initial_radius_mm + bp.cog_mm[1],
1222 v[2] * initial_radius_mm + bp.cog_mm[2],
1223 ])
1224 .collect();
1225
1226 let (neighbor_matrix, neighbor_counts) = build_neighbor_matrix(n_vertices, &faces, 6);
1227 let bt = 0.5_f64.powf(0.275);
1228
1229 let vertices_before = vertices.clone();
1230
1231 evolution_pass(
1232 &data, nx, ny, nz, &voxel_size, &bp,
1233 &mut vertices, &faces,
1234 &neighbor_matrix, &neighbor_counts,
1235 bt, 1.0, 0.0,
1236 10, 0, &mut None,
1239 );
1240
1241 let mut any_moved = false;
1243 for (before, after) in vertices_before.iter().zip(vertices.iter()) {
1244 let dist = ((after[0] - before[0]).powi(2) +
1245 (after[1] - before[1]).powi(2) +
1246 (after[2] - before[2]).powi(2)).sqrt();
1247 if dist > 1e-10 {
1248 any_moved = true;
1249 }
1250 assert!(after[0].is_finite() && after[1].is_finite() && after[2].is_finite());
1252 }
1253 assert!(any_moved, "Evolution should move at least some vertices");
1254 }
1255
1256 #[test]
1257 fn test_evolution_pass_recovery_pass() {
1258 let n = 16;
1260 let (data, nx, ny, nz) = make_sphere_volume(n, 6.0, 200.0);
1261 let voxel_size = [1.0, 1.0, 1.0];
1262 let bp = estimate_brain_parameters(&data, nx, ny, nz, &voxel_size);
1263
1264 let (unit_verts, faces) = create_icosphere(1);
1265 let n_vertices = unit_verts.len();
1266 let initial_radius_mm = bp.radius * 0.5;
1267
1268 let mut vertices: Vec<[f64; 3]> = unit_verts
1269 .iter()
1270 .map(|v| [
1271 v[0] * initial_radius_mm + bp.cog_mm[0],
1272 v[1] * initial_radius_mm + bp.cog_mm[1],
1273 v[2] * initial_radius_mm + bp.cog_mm[2],
1274 ])
1275 .collect();
1276
1277 let (neighbor_matrix, neighbor_counts) = build_neighbor_matrix(n_vertices, &faces, 6);
1278 let bt = 0.5_f64.powf(0.275);
1279
1280 evolution_pass(
1282 &data, nx, ny, nz, &voxel_size, &bp,
1283 &mut vertices, &faces,
1284 &neighbor_matrix, &neighbor_counts,
1285 bt, 1.0, 0.0,
1286 20, 1, &mut None,
1289 );
1290
1291 for v in &vertices {
1293 assert!(v[0].is_finite() && v[1].is_finite() && v[2].is_finite());
1294 }
1295 }
1296
1297 #[test]
1298 fn test_evolution_pass_with_gradient() {
1299 let n = 16;
1301 let (data, nx, ny, nz) = make_sphere_volume(n, 6.0, 200.0);
1302 let voxel_size = [1.0, 1.0, 1.0];
1303 let bp = estimate_brain_parameters(&data, nx, ny, nz, &voxel_size);
1304
1305 let (unit_verts, faces) = create_icosphere(1);
1306 let n_vertices = unit_verts.len();
1307 let initial_radius_mm = bp.radius * 0.5;
1308
1309 let mut vertices: Vec<[f64; 3]> = unit_verts
1310 .iter()
1311 .map(|v| [
1312 v[0] * initial_radius_mm + bp.cog_mm[0],
1313 v[1] * initial_radius_mm + bp.cog_mm[1],
1314 v[2] * initial_radius_mm + bp.cog_mm[2],
1315 ])
1316 .collect();
1317
1318 let (neighbor_matrix, neighbor_counts) = build_neighbor_matrix(n_vertices, &faces, 6);
1319 let bt = 0.5_f64.powf(0.275);
1320
1321 evolution_pass(
1322 &data, nx, ny, nz, &voxel_size, &bp,
1323 &mut vertices, &faces,
1324 &neighbor_matrix, &neighbor_counts,
1325 bt, 1.0, 0.5, 10, 0,
1327 &mut None,
1328 );
1329
1330 for v in &vertices {
1331 assert!(v[0].is_finite() && v[1].is_finite() && v[2].is_finite());
1332 }
1333 }
1334
1335 #[test]
1336 fn test_evolution_pass_with_progress_callback() {
1337 let n = 16;
1338 let (data, nx, ny, nz) = make_sphere_volume(n, 6.0, 200.0);
1339 let voxel_size = [1.0, 1.0, 1.0];
1340 let bp = estimate_brain_parameters(&data, nx, ny, nz, &voxel_size);
1341
1342 let (unit_verts, faces) = create_icosphere(1);
1343 let n_vertices = unit_verts.len();
1344 let initial_radius_mm = bp.radius * 0.5;
1345
1346 let mut vertices: Vec<[f64; 3]> = unit_verts
1347 .iter()
1348 .map(|v| [
1349 v[0] * initial_radius_mm + bp.cog_mm[0],
1350 v[1] * initial_radius_mm + bp.cog_mm[1],
1351 v[2] * initial_radius_mm + bp.cog_mm[2],
1352 ])
1353 .collect();
1354
1355 let (neighbor_matrix, neighbor_counts) = build_neighbor_matrix(n_vertices, &faces, 6);
1356 let bt = 0.5_f64.powf(0.275);
1357
1358 let mut calls = 0usize;
1359 let mut callback = |_iter: usize, _total: usize| {
1360 calls += 1;
1361 };
1362 let mut cb: Option<&mut dyn FnMut(usize, usize)> = Some(&mut callback);
1363
1364 evolution_pass(
1365 &data, nx, ny, nz, &voxel_size, &bp,
1366 &mut vertices, &faces,
1367 &neighbor_matrix, &neighbor_counts,
1368 bt, 1.0, 0.0,
1369 20, 0,
1370 &mut cb,
1371 );
1372
1373 assert!(calls > 0, "Progress callback should have been called");
1374 }
1375}