1#![cfg_attr(not(feature = "onnx"), allow(dead_code))]
84
85use crate::geometry::AxisCode;
86use crate::grid::Grid;
87#[cfg(feature = "onnx")]
88use crate::models::onnx::{OnnxError, OnnxModel, Tensor};
89use crate::segment::labels::SynthSegVersion;
90use crate::utils::connected::{largest_component, Connectivity};
91use crate::utils::resample::resize;
92use crate::utils::morphology::signal_erosion::gaussian_filter_anisotropic;
93#[cfg(feature = "parallel")]
94use rayon::prelude::*;
95
96const DIVISOR: usize = 32;
98const LATTICE: usize = 16;
102const MIN_PAD: usize = 128;
104const TARGET_RES: f64 = 1.0;
106const HEAD_THRESHOLD: f32 = 0.1;
109
110#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
112#[derive(Clone, Copy, Debug, PartialEq)]
113pub enum SynthSegInference {
114 FullVolume,
119 SlidingWindow {
141 patch: usize,
143 overlap: usize,
145 },
146}
147
148impl SynthSegInference {
149 pub const LOW_MEMORY: Self = Self::SlidingWindow { patch: 96, overlap: 48 };
160}
161
162#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
164#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
165pub enum PosteriorPrecision {
166 #[default]
168 F32,
169 F16,
183}
184
185#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
187#[derive(Clone, Debug, PartialEq)]
188pub struct SynthSegParams {
189 pub version: SynthSegVersion,
192 pub crop: Option<usize>,
197 pub flip_averaging: bool,
200 pub topology_cleanup: bool,
203 pub sigma_smoothing: f64,
206 pub inference: SynthSegInference,
208 pub head_crop: Option<usize>,
218 pub posteriors: PosteriorPrecision,
220}
221
222impl Default for SynthSegParams {
223 fn default() -> Self {
224 Self {
225 version: SynthSegVersion::default(),
226 crop: None,
227 flip_averaging: true,
228 topology_cleanup: true,
229 sigma_smoothing: 0.5,
230 inference: SynthSegInference::FullVolume,
231 head_crop: None,
232 posteriors: PosteriorPrecision::F32,
233 }
234 }
235}
236
237impl SynthSegParams {
238 pub fn fast() -> Self {
241 Self { flip_averaging: false, topology_cleanup: false, ..Self::default() }
242 }
243
244 pub fn low_memory() -> Self {
263 Self {
264 inference: SynthSegInference::LOW_MEMORY,
265 head_crop: Some(16),
266 posteriors: PosteriorPrecision::F16,
267 ..Self::default()
268 }
269 }
270}
271
272pub struct SynthSegResult {
274 pub labels: Vec<i32>,
276 pub volumes: Vec<f64>,
280}
281
282#[cfg(feature = "onnx")]
290pub fn synthseg(
291 magnitude: &[f64],
292 grid: &Grid,
293 affine: &[f64; 16],
294 model_onnx: &[u8],
295 params: &SynthSegParams,
296 mut progress: impl FnMut(usize, usize),
297) -> Result<SynthSegResult, OnnxError> {
298 let (nx, ny, nz) = grid.dims;
299 assert_eq!(magnitude.len(), nx * ny * nz, "magnitude length must match grid");
300 if params.sigma_smoothing < 0.0 {
301 return Err(OnnxError::Shape("sigma_smoothing must not be negative".into()));
302 }
303 let n_labels = params.version.n_labels();
304
305 let window = match params.inference {
306 SynthSegInference::FullVolume => None,
307 SynthSegInference::SlidingWindow { patch, overlap } => {
308 if patch < 64 {
309 return Err(OnnxError::Shape(format!("the SynthSeg patch must be at least 64 voxels, not {patch}")));
310 }
311 let patch = round_up(patch, LATTICE);
312 if overlap >= patch {
313 return Err(OnnxError::Shape(format!(
314 "the SynthSeg window overlap must be less than the patch ({patch}), not {overlap}"
315 )));
316 }
317 Some((patch, overlap))
318 }
319 };
320
321 let pre = preprocess(magnitude, grid, affine, params);
322 let model = OnnxModel::load(model_onnx)?;
323 let flip = params.flip_averaging.then(|| params.version.labels().flip);
324 match params.posteriors {
325 PosteriorPrecision::F32 => infer::<f32>(&model, &pre, n_labels, window, flip, params, &mut progress),
326 PosteriorPrecision::F16 => infer::<half::f16>(&model, &pre, n_labels, window, flip, params, &mut progress),
327 }
328}
329
330#[cfg(feature = "onnx")]
332fn infer<T: Posterior>(
333 model: &OnnxModel,
334 pre: &Preprocessed,
335 n_labels: usize,
336 window: Option<(usize, usize)>,
337 flip: Option<&[usize]>,
338 params: &SynthSegParams,
339 progress: &mut impl FnMut(usize, usize),
340) -> Result<SynthSegResult, OnnxError> {
341 if let Some((patch, overlap)) = window.filter(|&(patch, _)| pre.dims.iter().any(|&d| d > patch)) {
343 let mut post: Vec<T> = run_windows(model, &pre.data, pre.dims, n_labels, patch, overlap, flip, progress)?;
344 if params.sigma_smoothing > 0.0 {
345 blur_posteriors(&mut post, pre.dims, n_labels, params.sigma_smoothing);
346 }
347 return Ok(postprocess(post, pre, params));
348 }
349
350 let passes = 1 + flip.is_some() as usize;
351 let post = run_pass(model, &pre.data, pre.dims, n_labels, params)?;
352 progress(1, passes);
353 let mut post = T::store(post);
356 if let Some(table) = flip {
357 let flipped = flip_axis0(&pre.data, pre.dims, 1);
358 let post_f = run_pass(model, &flipped, pre.dims, n_labels, params)?;
359 drop(flipped);
360 progress(2, passes);
361 merge_flipped(&mut post, &post_f, pre.dims, n_labels, table);
362 }
363 Ok(postprocess(post, pre, params))
364}
365
366trait Posterior: Copy + Default + Send + Sync + 'static {
369 fn get(self) -> f32;
370 fn put(v: f32) -> Self;
371 fn store(v: Vec<f32>) -> Vec<Self>;
373 fn load(src: &[Self], dst: &mut [f32]) {
375 dst.iter_mut().zip(src).for_each(|(d, s)| *d = s.get());
376 }
377 fn save(src: &[f32], dst: &mut [Self]) {
379 dst.iter_mut().zip(src).for_each(|(d, &s)| *d = Self::put(s));
380 }
381}
382
383impl Posterior for f32 {
384 #[inline]
385 fn get(self) -> f32 {
386 self
387 }
388 #[inline]
389 fn put(v: f32) -> Self {
390 v
391 }
392 fn store(v: Vec<f32>) -> Vec<Self> {
393 v
394 }
395 fn load(src: &[Self], dst: &mut [f32]) {
396 dst.copy_from_slice(src);
397 }
398 fn save(src: &[f32], dst: &mut [Self]) {
399 dst.copy_from_slice(src);
400 }
401}
402
403#[cfg(feature = "onnx")]
404impl Posterior for half::f16 {
405 #[inline]
406 fn get(self) -> f32 {
407 self.to_f32()
408 }
409 #[inline]
410 fn put(v: f32) -> Self {
411 half::f16::from_f32(v)
412 }
413 fn store(v: Vec<f32>) -> Vec<Self> {
414 let mut out = vec![half::f16::ZERO; v.len()];
415 Self::save(&v, &mut out);
416 out
417 }
418 fn load(src: &[Self], dst: &mut [f32]) {
422 use half::slice::HalfFloatSliceExt;
423 src.convert_to_f32_slice(dst);
424 }
425 fn save(src: &[f32], dst: &mut [Self]) {
426 use half::slice::HalfFloatSliceExt;
427 dst.convert_from_f32_slice(src);
428 }
429}
430
431#[cfg(feature = "onnx")]
435fn merge_flipped<T: Posterior>(post: &mut [T], flipped: &[f32], dims: [usize; 3], n_labels: usize, table: &[usize]) {
436 let plane = dims[1] * dims[2] * n_labels;
437 crate::maybe_par_chunks_mut!(post, plane).enumerate().for_each(|(o0, out)| {
438 let src = &flipped[(dims[0] - 1 - o0) * plane..(dims[0] - o0) * plane];
439 for (o, f) in out.chunks_exact_mut(n_labels).zip(src.chunks_exact(n_labels)) {
440 for c in 0..n_labels {
441 o[c] = T::put(0.5 * (o[c].get() + f[table[c]]));
442 }
443 }
444 });
445}
446
447struct Preprocessed {
449 data: Vec<f32>,
452 dims: [usize; 3],
453 aligned_dims: [usize; 3],
455 origin: [isize; 3],
458 valid: [(usize, usize); 3],
461 perm: [usize; 3],
463 flip: [bool; 3],
465 source_dims: [usize; 3],
467 res: f64,
469 out_dims: (usize, usize, usize),
471 resampled: bool,
474}
475
476fn ras_alignment(affine: &[f64; 16]) -> ([usize; 3], [bool; 3]) {
483 crate::geometry::alignment_to(affine, [AxisCode::R, AxisCode::A, AxisCode::S])
484}
485
486fn round_up(n: usize, m: usize) -> usize {
489 if n.is_multiple_of(m) { n } else { (n / m + 1) * m }
490}
491
492pub fn synthseg_network_grid(grid: &Grid, params: &SynthSegParams) -> [usize; 3] {
499 let vs = [grid.vsx(), grid.vsy(), grid.vsz()];
500 let dims = [grid.nx(), grid.ny(), grid.nz()];
501 let resample = vs.iter().any(|&v| (v - TARGET_RES).abs() > 0.05);
502 let min_pad = params.crop.map(|c| round_up(c, DIVISOR)).unwrap_or(MIN_PAD);
503 std::array::from_fn(|i| {
504 let n = if resample { ((dims[i] as f64) * vs[i] / TARGET_RES).ceil().max(1.0) as usize } else { dims[i] };
505 let n = params.crop.map_or(n, |c| round_up(c, DIVISOR).min(n));
506 round_up(n, DIVISOR).max(round_up(min_pad, DIVISOR))
507 })
508}
509
510fn percentile(sorted_scratch: &mut [f32], q: f64) -> f64 {
512 let n = sorted_scratch.len();
513 if n == 0 {
514 return 0.0;
515 }
516 let pos = q / 100.0 * (n - 1) as f64;
517 let lo = pos.floor() as usize;
518 let frac = pos - lo as f64;
519 let a = *sorted_scratch.select_nth_unstable_by(lo, |x, y| x.total_cmp(y)).1 as f64;
520 if frac == 0.0 || lo + 1 >= n {
521 return a;
522 }
523 let b = sorted_scratch[lo + 1..].iter().copied().fold(f32::INFINITY, f32::min) as f64;
526 a + frac * (b - a)
527}
528
529fn preprocess(
530 magnitude: &[f64],
531 grid: &Grid,
532 affine: &[f64; 16],
533 params: &SynthSegParams,
534) -> Preprocessed {
535 let (perm, flip) = ras_alignment(affine);
536 let out_dims = grid.dims;
537
538 let vs = [grid.vsx(), grid.vsy(), grid.vsz()];
540 let needs_resample = vs.iter().any(|&v| (v - TARGET_RES).abs() > 0.05);
541 let (source, source_dims_cm) = if needs_resample {
542 resample_to_iso(magnitude, grid)
543 } else {
544 (magnitude.to_vec(), [grid.nx(), grid.ny(), grid.nz()])
545 };
546
547 let aligned_dims = [source_dims_cm[perm[0]], source_dims_cm[perm[1]], source_dims_cm[perm[2]]];
549 let mut data = align_to_ras(&source, source_dims_cm, aligned_dims, perm, flip);
550
551 let mut dims = aligned_dims;
553 let crop = match params.crop {
554 Some(c) => {
555 let want = round_up(c, DIVISOR);
556 let mut bounds = [(0, 0); 3];
557 for i in 0..3 {
558 let keep = want.min(dims[i]);
559 let start = (dims[i] - keep) / 2;
560 bounds[i] = (start, start + keep);
561 }
562 data = crop_c_order(&data, dims, bounds, 1);
563 dims = [bounds[0].1 - bounds[0].0, bounds[1].1 - bounds[1].0, bounds[2].1 - bounds[2].0];
564 bounds
565 }
566 None => [(0, dims[0]), (0, dims[1]), (0, dims[2])],
567 };
568
569 let mut scratch = data.clone();
571 let lo = percentile(&mut scratch, 0.5);
572 let mut scratch = data.clone();
573 let hi = percentile(&mut scratch, 99.5);
574 if hi > lo {
575 let scale = 1.0 / (hi - lo);
576 for v in data.iter_mut() {
577 *v = (((*v as f64).clamp(lo, hi) - lo) * scale) as f32;
578 }
579 } else {
580 data.iter_mut().for_each(|v| *v = 0.0);
581 }
582
583 let min_pad = params.crop.map(|c| round_up(c, DIVISOR)).unwrap_or(MIN_PAD);
585 let padded: [usize; 3] =
586 std::array::from_fn(|i| round_up(dims[i], DIVISOR).max(round_up(min_pad, DIVISOR)));
587 let mut pad = [(0, 0); 3];
588 for i in 0..3 {
589 let start = (padded[i] - dims[i]) / 2;
590 pad[i] = (start, start + dims[i]);
591 }
592 let mut data = pad_c_order(&data, dims, padded, pad, 1);
593 let mut net_dims = padded;
594 let mut origin: [isize; 3] = std::array::from_fn(|i| crop[i].0 as isize - pad[i].0 as isize);
595 let mut valid = pad;
596
597 if let Some(bounds) = params.head_crop.and_then(|margin| head_box(&data, padded, margin)) {
600 data = crop_c_order(&data, padded, bounds, 1);
601 net_dims = std::array::from_fn(|i| bounds[i].1 - bounds[i].0);
602 for i in 0..3 {
603 origin[i] += bounds[i].0 as isize;
604 let start = pad[i].0.max(bounds[i].0);
605 let end = pad[i].1.min(bounds[i].1).max(start);
606 valid[i] = (start - bounds[i].0, end - bounds[i].0);
607 }
608 }
609
610 Preprocessed {
611 data,
612 dims: net_dims,
613 aligned_dims,
614 origin,
615 valid,
616 perm,
617 flip,
618 source_dims: source_dims_cm,
619 res: TARGET_RES,
620 out_dims,
621 resampled: needs_resample,
622 }
623}
624
625fn head_box(data: &[f32], dims: [usize; 3], margin: usize) -> Option<[(usize, usize); 3]> {
630 let above: Vec<u8> = data.iter().map(|&v| (v > HEAD_THRESHOLD) as u8).collect();
631 let head = largest_component(&above, (dims[2], dims[1], dims[0]), Connectivity::Face);
634 drop(above);
635 let mut lo = dims;
636 let mut hi = [0usize; 3];
637 for (l, _) in head.iter().enumerate().filter(|(_, &h)| h != 0) {
638 let at = [l / (dims[1] * dims[2]), (l / dims[2]) % dims[1], l % dims[2]];
639 for i in 0..3 {
640 lo[i] = lo[i].min(at[i]);
641 hi[i] = hi[i].max(at[i] + 1);
642 }
643 }
644 if hi.contains(&0) {
645 return None;
646 }
647 let bounds: [(usize, usize); 3] = std::array::from_fn(|i| {
648 let start = lo[i].saturating_sub(margin) / LATTICE * LATTICE;
649 let end = round_up((hi[i] + margin).min(dims[i]), LATTICE).min(dims[i]);
650 (start, end)
651 });
652 (bounds != [(0, dims[0]), (0, dims[1]), (0, dims[2])]).then_some(bounds)
653}
654
655fn resample_to_iso(magnitude: &[f64], grid: &Grid) -> (Vec<f64>, [usize; 3]) {
658 let vs = [grid.vsx(), grid.vsy(), grid.vsz()];
659 let dims = [grid.nx(), grid.ny(), grid.nz()];
660 let factor: [f64; 3] = std::array::from_fn(|i| vs[i] / TARGET_RES);
661 let sigma: [f64; 3] =
662 std::array::from_fn(|i| if factor[i] > 1.0 { 0.0 } else { 0.25 / factor[i] });
663 let blurred = gaussian_filter_anisotropic(magnitude, grid.dims, sigma);
664 let new_dims: [usize; 3] =
665 std::array::from_fn(|i| ((dims[i] as f64) * factor[i]).ceil().max(1.0) as usize);
666 let out = resize(
668 &blurred,
669 [dims[2], dims[1], dims[0]],
670 [new_dims[2], new_dims[1], new_dims[0]],
671 1,
672 );
673 (out, new_dims)
674}
675
676fn align_to_ras(
678 source: &[f64],
679 source_dims: [usize; 3],
680 dims: [usize; 3],
681 perm: [usize; 3],
682 flip: [bool; 3],
683) -> Vec<f32> {
684 let (nx, ny) = (source_dims[0], source_dims[1]);
685 let nxy = nx * ny;
686 let strides = [1usize, nx, nxy];
687 let mut out = vec![0f32; dims[0] * dims[1] * dims[2]];
688 let mut base = 0usize;
690 let mut step = [0isize; 3];
691 for i in 0..3 {
692 let s = strides[perm[i]] as isize;
693 if flip[i] {
694 base += (dims[i] - 1) * strides[perm[i]];
695 step[i] = -s;
696 } else {
697 step[i] = s;
698 }
699 }
700 crate::maybe_par_chunks_mut!(out, dims[1] * dims[2]).enumerate().for_each(|(o0, plane)| {
701 let row0 = base as isize + step[0] * o0 as isize;
702 for o1 in 0..dims[1] {
703 let row = row0 + step[1] * o1 as isize;
704 let dst = &mut plane[o1 * dims[2]..(o1 + 1) * dims[2]];
705 for (o2, d) in dst.iter_mut().enumerate() {
706 *d = source[(row + step[2] * o2 as isize) as usize] as f32;
707 }
708 }
709 });
710 out
711}
712
713fn unalign_from_ras<T: Copy>(
715 aligned: &[T],
716 dims: [usize; 3],
717 out_dims: [usize; 3],
718 perm: [usize; 3],
719 flip: [bool; 3],
720 out: &mut [T],
721) {
722 let (nx, ny) = (out_dims[0], out_dims[1]);
723 let nxy = nx * ny;
724 let strides = [1usize, nx, nxy];
725 let mut base = 0usize;
726 let mut step = [0isize; 3];
727 for i in 0..3 {
728 let s = strides[perm[i]] as isize;
729 if flip[i] {
730 base += (dims[i] - 1) * strides[perm[i]];
731 step[i] = -s;
732 } else {
733 step[i] = s;
734 }
735 }
736 for o0 in 0..dims[0] {
737 let row0 = base as isize + step[0] * o0 as isize;
738 for o1 in 0..dims[1] {
739 let row = row0 + step[1] * o1 as isize;
740 let src = &aligned[(o0 * dims[1] + o1) * dims[2]..(o0 * dims[1] + o1 + 1) * dims[2]];
741 for (o2, &v) in src.iter().enumerate() {
742 out[(row + step[2] * o2 as isize) as usize] = v;
743 }
744 }
745 }
746}
747
748fn crop_c_order<T: Copy + Default>(
750 data: &[T],
751 dims: [usize; 3],
752 bounds: [(usize, usize); 3],
753 n_ch: usize,
754) -> Vec<T> {
755 let out_dims: [usize; 3] = std::array::from_fn(|i| bounds[i].1 - bounds[i].0);
756 let mut out = vec![T::default(); out_dims.iter().product::<usize>() * n_ch];
757 for o0 in 0..out_dims[0] {
758 for o1 in 0..out_dims[1] {
759 let src = ((o0 + bounds[0].0) * dims[1] + o1 + bounds[1].0) * dims[2] + bounds[2].0;
760 let dst = (o0 * out_dims[1] + o1) * out_dims[2];
761 out[dst * n_ch..(dst + out_dims[2]) * n_ch]
762 .copy_from_slice(&data[src * n_ch..(src + out_dims[2]) * n_ch]);
763 }
764 }
765 out
766}
767
768fn pad_c_order<T: Copy + Default>(
770 data: &[T],
771 dims: [usize; 3],
772 out_dims: [usize; 3],
773 at: [(usize, usize); 3],
774 n_ch: usize,
775) -> Vec<T> {
776 let mut out = vec![T::default(); out_dims.iter().product::<usize>() * n_ch];
777 for o0 in 0..dims[0] {
778 for o1 in 0..dims[1] {
779 let src = (o0 * dims[1] + o1) * dims[2];
780 let dst = ((o0 + at[0].0) * out_dims[1] + o1 + at[1].0) * out_dims[2] + at[2].0;
781 out[dst * n_ch..(dst + dims[2]) * n_ch]
782 .copy_from_slice(&data[src * n_ch..(src + dims[2]) * n_ch]);
783 }
784 }
785 out
786}
787
788fn flip_axis0(data: &[f32], dims: [usize; 3], n_ch: usize) -> Vec<f32> {
790 let plane = dims[1] * dims[2] * n_ch;
791 let mut out = vec![0f32; data.len()];
792 for o0 in 0..dims[0] {
793 let src = (dims[0] - 1 - o0) * plane;
794 out[o0 * plane..(o0 + 1) * plane].copy_from_slice(&data[src..src + plane]);
795 }
796 out
797}
798
799#[cfg(feature = "onnx")]
802fn run_pass(
803 model: &OnnxModel,
804 data: &[f32],
805 dims: [usize; 3],
806 n_labels: usize,
807 params: &SynthSegParams,
808) -> Result<Vec<f32>, OnnxError> {
809 let input = Tensor::new(vec![1, dims[0], dims[1], dims[2], 1], data.to_vec());
810 let out = model.run_single(&input)?;
811 let expect = [1, dims[0], dims[1], dims[2], n_labels];
812 if out.shape != expect {
813 return Err(OnnxError::Shape(format!(
814 "SynthSeg returned {:?}, expected {:?} — do the weights match params.version?",
815 out.shape, expect
816 )));
817 }
818 let mut post = out.data;
819 if params.sigma_smoothing > 0.0 {
820 blur_posteriors(&mut post, dims, n_labels, params.sigma_smoothing);
821 }
822 Ok(post)
823}
824
825fn lattice_windows(size: usize, p: usize, overlap: usize) -> Vec<usize> {
830 debug_assert!(size.is_multiple_of(LATTICE) && p.is_multiple_of(LATTICE), "{size} {p}");
831 if size <= p {
832 return vec![0];
833 }
834 let span = (size - p) / LATTICE;
835 let max_step = (p.saturating_sub(overlap) / LATTICE).max(1);
836 let steps = span.div_ceil(max_step);
837 (0..=steps).map(|i| i * span / steps * LATTICE).collect()
838}
839
840#[cfg(feature = "onnx")]
852#[allow(clippy::too_many_arguments)]
853fn run_windows<T: Posterior>(
854 model: &OnnxModel,
855 data: &[f32],
856 dims: [usize; 3],
857 n_labels: usize,
858 patch: usize,
859 overlap: usize,
860 flip: Option<&[usize]>,
861 progress: &mut impl FnMut(usize, usize),
862) -> Result<Vec<T>, OnnxError> {
863 use crate::bet::hdbet::gaussian_importance;
864 let p: [usize; 3] = std::array::from_fn(|a| patch.min(dims[a]));
865 let starts: [Vec<usize>; 3] = std::array::from_fn(|a| lattice_windows(dims[a], p[a], overlap));
866 let mut origins = Vec::new();
867 for &s0 in &starts[0] {
868 for &s1 in &starts[1] {
869 for &s2 in &starts[2] {
870 origins.push([s0, s1, s2]);
871 }
872 }
873 }
874 let gauss = gaussian_importance(p);
875 let shape = [1, p[0], p[1], p[2], 1];
876 let plan = model.plan_for(&[&shape])?;
877 let passes = 1 + flip.is_some() as usize;
878 let total = origins.len() * passes;
879 let mut done = 0;
880
881 let rows = |o: [usize; 3], f: &mut dyn FnMut(usize, usize)| {
883 for z in 0..p[0] {
884 for y in 0..p[1] {
885 f(((o[0] + z) * dims[1] + o[1] + y) * dims[2] + o[2], (z * p[1] + y) * p[2]);
886 }
887 }
888 };
889
890 let n: usize = dims.iter().product();
892 let mut inv_weight = vec![0f32; n];
893 for &o in &origins {
894 rows(o, &mut |v, w| {
895 for k in 0..p[2] {
896 inv_weight[v + k] += gauss[w + k];
897 }
898 });
899 }
900 inv_weight.iter_mut().for_each(|w| *w = if *w > 0.0 { 1.0 / *w } else { 0.0 });
901
902 let mut post = vec![T::default(); n * n_labels];
903 let mut input = vec![0f32; p.iter().product()];
904 let mut row = vec![0f32; p[2] * n_labels];
905 let run = |x: Vec<f32>| -> Result<Vec<f32>, OnnxError> {
906 let out = plan.run_single(&Tensor::new(shape.to_vec(), x))?;
907 let expect = [1, p[0], p[1], p[2], n_labels];
908 if out.shape != expect {
909 return Err(OnnxError::Shape(format!(
910 "SynthSeg returned {:?}, expected {:?} — do the weights match params.version?",
911 out.shape, expect
912 )));
913 }
914 Ok(out.data)
915 };
916 for &o in &origins {
917 rows(o, &mut |v, w| input[w..w + p[2]].copy_from_slice(&data[v..v + p[2]]));
918 let mut pred = run(input.clone())?;
919 done += 1;
920 progress(done, total);
921 if let Some(table) = flip {
922 let back = flip_axis0(&run(flip_axis0(&input, p, 1))?, p, n_labels);
923 done += 1;
924 progress(done, total);
925 for (out, chunk) in pred.chunks_exact_mut(n_labels).zip(back.chunks_exact(n_labels)) {
926 for c in 0..n_labels {
927 out[c] = 0.5 * (out[c] + chunk[table[c]]);
928 }
929 }
930 }
931 rows(o, &mut |v, w| {
933 let dst = &mut post[v * n_labels..(v + p[2]) * n_labels];
934 T::load(dst, &mut row);
935 for k in 0..p[2] {
936 let g = gauss[w + k] * inv_weight[v + k];
937 let (a, b) = (k * n_labels, (w + k) * n_labels);
938 for c in 0..n_labels {
939 row[a + c] += g * pred[b + c];
940 }
941 }
942 T::save(&row, dst);
943 });
944 }
945 Ok(post)
946}
947
948fn blur_posteriors<T: Posterior>(post: &mut [T], dims: [usize; 3], n_ch: usize, sigma: f64) {
954 let width = ((2.5 * sigma).ceil() as usize / 2) * 2 + 1;
955 if width <= 1 {
956 return;
957 }
958 let radius = width / 2;
959 let mut w: Vec<f32> = (0..width)
960 .map(|i| {
961 let x = i as f64 - (width - 1) as f64 / 2.0;
962 (-(x * x) / (2.0 * sigma * sigma)).exp() as f32
963 })
964 .collect();
965 let s: f32 = w.iter().sum();
966 w.iter_mut().for_each(|v| *v /= s);
967
968 blur_along(post, 1, dims[0], dims[1] * dims[2] * n_ch, &w, radius);
970 blur_along(post, dims[0], dims[1], dims[2] * n_ch, &w, radius);
971 blur_along(post, dims[0] * dims[1], dims[2], n_ch, &w, radius);
972}
973
974fn blur_along<T: Posterior>(data: &mut [T], outer: usize, n: usize, inner: usize, w: &[f32], radius: usize) {
976 if n <= 1 {
977 return;
978 }
979 let stride = n * inner;
980 let taps = 2 * radius + 1;
981 crate::maybe_par_chunks_mut!(data, stride).take(outer).for_each(|line| {
982 let mut history = vec![0f32; taps * inner];
985 let mut acc = vec![0f32; inner];
986 for i in 0..n + radius {
987 if i < n {
988 let slot = (i % taps) * inner;
989 T::load(&line[i * inner..(i + 1) * inner], &mut history[slot..slot + inner]);
990 }
991 if i < radius {
992 continue;
993 }
994 let o = i - radius;
996 acc.fill(0.0);
997 for (t, &wt) in w.iter().enumerate() {
998 let src = o as isize + t as isize - radius as isize;
999 if src < 0 || src as usize >= n {
1000 continue; }
1002 let slot = (src as usize % taps) * inner;
1003 for (a, &h) in acc.iter_mut().zip(&history[slot..slot + inner]) {
1004 *a += wt * h;
1005 }
1006 }
1007 T::save(&acc, &mut line[o * inner..(o + 1) * inner]);
1008 }
1009 });
1010}
1011
1012#[cfg(feature = "onnx")]
1015fn postprocess<T: Posterior>(mut post: Vec<T>, pre: &Preprocessed, params: &SynthSegParams) -> SynthSegResult {
1016 let labels = params.version.labels();
1017 let n_labels = labels.ids.len();
1018 let dims = pre.dims;
1019 let n_vox = dims.iter().product::<usize>();
1020 let cc_dims = (dims[2], dims[1], dims[0]);
1023
1024 let mut fg = vec![0u8; n_vox];
1026 for (v, chunk) in fg.iter_mut().zip(post.chunks_exact(n_labels)) {
1027 *v = (chunk[1..].iter().map(|p| p.get()).sum::<f32>() > 0.25) as u8;
1028 }
1029 let fg = largest_component(&fg, cc_dims, Connectivity::Face);
1030 for (keep, chunk) in fg.iter().zip(post.chunks_exact_mut(n_labels)) {
1031 if *keep == 0 {
1032 chunk[1..].fill(T::default());
1033 }
1034 }
1035 drop(fg);
1036
1037 if params.topology_cleanup {
1038 let max_class = labels.topology.iter().copied().max().unwrap_or(0);
1039 for class in 1..=max_class {
1040 let members: Vec<usize> =
1041 (0..n_labels).filter(|&c| labels.topology[c] == class).collect();
1042 if members.is_empty() {
1043 continue;
1044 }
1045 let mut mask = vec![0u8; n_vox];
1046 for (v, chunk) in mask.iter_mut().zip(post.chunks_exact(n_labels)) {
1047 *v = members.iter().any(|&c| chunk[c].get() > 0.25) as u8;
1048 }
1049 let mask = largest_component(&mask, cc_dims, Connectivity::Face);
1050 for (keep, chunk) in mask.iter().zip(post.chunks_exact_mut(n_labels)) {
1051 if *keep == 0 {
1052 for &c in &members {
1053 chunk[c] = T::default();
1054 }
1055 }
1056 }
1057 }
1058 } else {
1059 for chunk in post.chunks_exact_mut(n_labels) {
1061 for c in chunk.iter_mut().skip(1) {
1062 if c.get() <= 0.2 {
1063 *c = T::default();
1064 }
1065 }
1066 }
1067 }
1068
1069 let valid = pre.valid;
1072 let valid_dims: [usize; 3] = std::array::from_fn(|i| valid[i].1 - valid[i].0);
1073 let mut volumes = vec![0f64; n_labels];
1074 let mut hard = Vec::with_capacity(valid_dims.iter().product::<usize>());
1075 let mut p = vec![0f32; n_labels];
1076 for o0 in valid[0].0..valid[0].1 {
1077 for o1 in valid[1].0..valid[1].1 {
1078 let row = ((o0 * dims[1] + o1) * dims[2] + valid[2].0) * n_labels;
1079 for chunk in post[row..row + valid_dims[2] * n_labels].chunks_exact(n_labels) {
1080 p.iter_mut().zip(chunk).for_each(|(p, c)| *p = c.get());
1081 let total: f32 = p.iter().sum();
1082 if total > 0.0 {
1083 p.iter_mut().for_each(|v| *v /= total);
1084 }
1085 let mut best = 0usize;
1086 for c in 1..n_labels {
1087 if p[c] > p[best] {
1088 best = c;
1089 }
1090 }
1091 hard.push(labels.ids[best]);
1092 for c in 1..n_labels {
1095 volumes[c] += p[c] as f64;
1096 }
1097 }
1098 }
1099 }
1100 drop(post);
1101 let voxel_volume = pre.res * pre.res * pre.res;
1102 volumes.iter_mut().for_each(|v| *v *= voxel_volume);
1103
1104 let at: [(usize, usize); 3] = std::array::from_fn(|i| {
1107 let start = (valid[i].0 as isize + pre.origin[i]) as usize;
1108 (start, start + valid_dims[i])
1109 });
1110 let aligned = if valid_dims == pre.aligned_dims {
1111 hard
1112 } else {
1113 let mut out = vec![labels.ids[0]; pre.aligned_dims.iter().product()];
1114 for o0 in 0..valid_dims[0] {
1115 for o1 in 0..valid_dims[1] {
1116 let src = (o0 * valid_dims[1] + o1) * valid_dims[2];
1117 let dst = ((o0 + at[0].0) * pre.aligned_dims[1] + o1 + at[1].0) * pre.aligned_dims[2] + at[2].0;
1118 out[dst..dst + valid_dims[2]].copy_from_slice(&hard[src..src + valid_dims[2]]);
1119 }
1120 }
1121 out
1122 };
1123
1124 let mut source = vec![labels.ids[0]; pre.source_dims.iter().product::<usize>()];
1125 unalign_from_ras(&aligned, pre.aligned_dims, pre.source_dims, pre.perm, pre.flip, &mut source);
1126
1127 let out_dims = [pre.out_dims.0, pre.out_dims.1, pre.out_dims.2];
1128 let labels_out = if pre.resampled {
1129 resample_labels_nearest(&source, pre.source_dims, out_dims)
1130 } else {
1131 source
1132 };
1133
1134 SynthSegResult { labels: labels_out, volumes }
1135}
1136
1137fn resample_labels_nearest(src: &[i32], dims: [usize; 3], out_dims: [usize; 3]) -> Vec<i32> {
1139 let mut out = vec![0i32; out_dims.iter().product()];
1140 let scale: [f64; 3] = std::array::from_fn(|i| dims[i] as f64 / out_dims[i] as f64);
1141 for z in 0..out_dims[2] {
1142 let sz = (((z as f64 + 0.5) * scale[2] - 0.5).round().max(0.0) as usize).min(dims[2] - 1);
1143 for y in 0..out_dims[1] {
1144 let sy =
1145 (((y as f64 + 0.5) * scale[1] - 0.5).round().max(0.0) as usize).min(dims[1] - 1);
1146 for x in 0..out_dims[0] {
1147 let sx = (((x as f64 + 0.5) * scale[0] - 0.5).round().max(0.0) as usize)
1148 .min(dims[0] - 1);
1149 out[x + out_dims[0] * (y + out_dims[1] * z)] =
1150 src[sx + dims[0] * (sy + dims[1] * sz)];
1151 }
1152 }
1153 }
1154 out
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159 use super::*;
1160
1161 const LAS: [f64; 16] = [
1163 -1.0, 0.0, 0.0, 91.0, 0.0, 1.0, 0.0, -113.5, 0.0, 0.0, 1.0, -39.7, 0.0, 0.0, 0.0, 1.0,
1164 ];
1165
1166 #[test]
1167 fn las_affine_flips_only_the_left_right_axis() {
1168 let (perm, flip) = ras_alignment(&LAS);
1169 assert_eq!(perm, [0, 1, 2]);
1170 assert_eq!(flip, [true, false, false]);
1171 }
1172
1173 #[test]
1174 fn identity_affine_is_already_ras() {
1175 let mut aff = [0.0; 16];
1176 aff[0] = 1.0;
1177 aff[5] = 1.0;
1178 aff[10] = 1.0;
1179 aff[15] = 1.0;
1180 assert_eq!(ras_alignment(&aff), ([0, 1, 2], [false; 3]));
1181 }
1182
1183 #[test]
1184 fn permuted_affine_is_detected() {
1185 let mut aff = [0.0; 16];
1187 aff[8] = 1.0; aff[1] = -1.0; aff[6] = -1.0; aff[15] = 1.0;
1191 let (perm, flip) = ras_alignment(&aff);
1192 assert_eq!(perm, [1, 2, 0], "R, A, S should come from axes 1, 2, 0");
1193 assert_eq!(flip, [true, true, false]);
1194 }
1195
1196 #[test]
1197 fn alignment_round_trips() {
1198 let dims = [3usize, 4, 5];
1199 let n = dims.iter().product::<usize>();
1200 let src: Vec<f64> = (0..n).map(|i| i as f64).collect();
1201 let (perm, flip) = ras_alignment(&LAS);
1202 let aligned_dims = [dims[perm[0]], dims[perm[1]], dims[perm[2]]];
1203 let aligned = align_to_ras(&src, dims, aligned_dims, perm, flip);
1204 let mut back = vec![0f32; n];
1205 unalign_from_ras(&aligned, aligned_dims, dims, perm, flip, &mut back);
1206 let expected: Vec<f32> = src.iter().map(|&v| v as f32).collect();
1207 assert_eq!(back, expected);
1208 }
1209
1210 #[test]
1211 fn alignment_round_trips_under_permutation() {
1212 let dims = [3usize, 4, 5];
1213 let n = dims.iter().product::<usize>();
1214 let src: Vec<f64> = (0..n).map(|i| i as f64).collect();
1215 let (perm, flip) = ([2usize, 0, 1], [true, false, true]);
1216 let aligned_dims = [dims[perm[0]], dims[perm[1]], dims[perm[2]]];
1217 let aligned = align_to_ras(&src, dims, aligned_dims, perm, flip);
1218 assert_eq!(aligned.len(), n);
1219 let mut back = vec![0f32; n];
1220 unalign_from_ras(&aligned, aligned_dims, dims, perm, flip, &mut back);
1221 let expected: Vec<f32> = src.iter().map(|&v| v as f32).collect();
1222 assert_eq!(back, expected);
1223 }
1224
1225 #[test]
1226 fn percentile_matches_linear_interpolation() {
1227 let make = || (0..101).map(|i| i as f32).collect::<Vec<_>>();
1228 assert!((percentile(&mut make(), 0.0) - 0.0).abs() < 1e-6);
1229 assert!((percentile(&mut make(), 100.0) - 100.0).abs() < 1e-6);
1230 assert!((percentile(&mut make(), 50.0) - 50.0).abs() < 1e-6);
1231 assert!((percentile(&mut make(), 0.5) - 0.5).abs() < 1e-6);
1233 assert!((percentile(&mut make(), 99.5) - 99.5).abs() < 1e-6);
1234 }
1235
1236 #[test]
1237 fn round_up_hits_multiples_of_the_divisor() {
1238 assert_eq!(round_up(128, DIVISOR), 128);
1239 assert_eq!(round_up(129, DIVISOR), 160);
1240 assert_eq!(round_up(184, DIVISOR), 192);
1241 assert_eq!(round_up(256, DIVISOR), 256);
1242 }
1243
1244 #[test]
1245 fn crop_and_pad_are_inverses() {
1246 let dims = [4usize, 5, 6];
1247 let n = dims.iter().product::<usize>();
1248 let src: Vec<f32> = (0..n).map(|i| i as f32).collect();
1249 let bounds = [(1, 3), (1, 4), (2, 5)];
1250 let cropped = crop_c_order(&src, dims, bounds, 1);
1251 let inner: [usize; 3] = std::array::from_fn(|i| bounds[i].1 - bounds[i].0);
1252 assert_eq!(cropped.len(), inner.iter().product::<usize>());
1253 let back = pad_c_order(&cropped, inner, dims, bounds, 1);
1254 for o0 in bounds[0].0..bounds[0].1 {
1255 for o1 in bounds[1].0..bounds[1].1 {
1256 for o2 in bounds[2].0..bounds[2].1 {
1257 let i = (o0 * dims[1] + o1) * dims[2] + o2;
1258 assert_eq!(back[i], src[i]);
1259 }
1260 }
1261 }
1262 }
1263
1264 #[test]
1265 fn blur_preserves_a_constant_interior() {
1266 let dims = [8usize, 8, 8];
1268 let n_ch = 2;
1269 let mut post = vec![1f32; dims.iter().product::<usize>() * n_ch];
1270 blur_posteriors(&mut post, dims, n_ch, 0.5);
1271 let at = |a: usize, b: usize, c: usize, ch: usize| ((a * dims[1] + b) * dims[2] + c) * n_ch + ch;
1272 assert!((post[at(4, 4, 4, 0)] - 1.0).abs() < 1e-5);
1273 assert!((post[at(4, 4, 4, 1)] - 1.0).abs() < 1e-5);
1274 assert!(post[at(0, 0, 0, 0)] < 0.95);
1276 }
1277
1278 #[test]
1279 fn blur_keeps_channels_independent() {
1280 let dims = [6usize, 6, 6];
1281 let n_ch = 3;
1282 let mut post = vec![0f32; dims.iter().product::<usize>() * n_ch];
1283 let at = |a: usize, b: usize, c: usize, ch: usize| ((a * dims[1] + b) * dims[2] + c) * n_ch + ch;
1284 post[at(3, 3, 3, 1)] = 1.0;
1285 blur_posteriors(&mut post, dims, n_ch, 0.5);
1286 assert!(post[at(3, 3, 3, 1)] > 0.0, "channel 1 should carry the blurred impulse");
1287 for a in 0..dims[0] {
1288 for b in 0..dims[1] {
1289 for c in 0..dims[2] {
1290 assert_eq!(post[at(a, b, c, 0)], 0.0, "channel 0 leaked");
1291 assert_eq!(post[at(a, b, c, 2)], 0.0, "channel 2 leaked");
1292 }
1293 }
1294 }
1295 }
1296
1297 #[cfg(feature = "onnx")]
1298 #[test]
1299 fn a_window_under_64_or_an_overlap_of_the_whole_patch_is_refused() {
1300 let grid = Grid::new(4, 4, 4, 1.0, 1.0, 1.0);
1301 let mag = vec![1.0; 64];
1302 let aff = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0];
1303 for (patch, overlap) in [(63, 0), (32, 0), (128, 128), (100, 112)] {
1304 let params = SynthSegParams { inference: SynthSegInference::SlidingWindow { patch, overlap }, ..SynthSegParams::default() };
1305 let err = synthseg(&mag, &grid, &aff, &[], ¶ms, |_, _| {}).err().expect("refused");
1306 assert!(matches!(err, OnnxError::Shape(_)), "{patch} {overlap}: {err:?}");
1307 }
1308 }
1309
1310 #[test]
1311 fn low_memory_is_the_defaults_with_windows_a_head_crop_and_half_precision() {
1312 let p = SynthSegParams::low_memory();
1313 assert_eq!(p.inference, SynthSegInference::LOW_MEMORY);
1314 assert_eq!(p.posteriors, PosteriorPrecision::F16);
1315 assert!(p.head_crop.is_some());
1316 let whole = SynthSegParams { inference: SynthSegInference::FullVolume, head_crop: None, posteriors: PosteriorPrecision::F32, ..p };
1317 assert_eq!(whole, SynthSegParams::default());
1318 }
1319
1320 #[test]
1321 fn windows_sit_on_the_pooling_lattice_and_keep_the_overlap() {
1322 for (size, p, overlap) in [(256, 96, 48), (256, 96, 64), (192, 96, 48), (256, 128, 48), (256, 128, 64), (176, 128, 32), (240, 112, 0)] {
1323 let w = lattice_windows(size, p, overlap);
1324 assert_eq!(w[0], 0);
1325 assert_eq!(*w.last().unwrap(), size - p, "{size} {p} {overlap}: {w:?}");
1326 assert!(w.iter().all(|o| o % LATTICE == 0), "{w:?}");
1327 for pair in w.windows(2) {
1328 assert!(pair[1] > pair[0] && p - (pair[1] - pair[0]) >= overlap, "{size} {p} {overlap}: {w:?}");
1329 }
1330 let max_step = (p - overlap) / LATTICE * LATTICE;
1332 assert!(w.len() == 2 || (size - p) > (w.len() - 2) * max_step, "{size} {p} {overlap}: {w:?}");
1333 }
1334 assert_eq!(lattice_windows(256, 96, 48), vec![0, 32, 80, 112, 160]);
1335 assert_eq!(lattice_windows(192, 96, 48), vec![0, 48, 96]);
1336 assert_eq!(lattice_windows(256, 128, 48), vec![0, 64, 128]);
1337 assert_eq!(lattice_windows(96, 96, 48), vec![0]);
1338 assert_eq!(lattice_windows(64, 96, 48), vec![0]);
1339 }
1340
1341 #[test]
1342 fn the_head_box_is_the_largest_bright_region_grown_and_on_the_lattice() {
1343 let dims = [64usize, 96, 128];
1344 let mut data = vec![0f32; dims.iter().product()];
1345 let at = |a: usize, b: usize, c: usize| (a * dims[1] + b) * dims[2] + c;
1346 for a in 20..40 {
1348 for b in 30..61 {
1349 for c in 40..90 {
1350 data[at(a, b, c)] = 0.5;
1351 }
1352 }
1353 }
1354 data[at(2, 2, 2)] = 1.0;
1355 let bounds = head_box(&data, dims, 4).expect("a box");
1356 assert_eq!(bounds, [(16, 48), (16, 80), (32, 96)]);
1357 assert_eq!(head_box(&data, dims, 40), None);
1359 assert_eq!(head_box(&vec![0f32; data.len()], dims, 4), None);
1360 }
1361
1362 #[test]
1363 fn blur_at_half_precision_follows_single_precision() {
1364 let dims = [6usize, 7, 8];
1365 let n_ch = 3;
1366 let n = dims.iter().product::<usize>() * n_ch;
1367 let src: Vec<f32> = (0..n).map(|i| ((i * 7919) % 101) as f32 / 101.0).collect();
1368 let mut single = src.clone();
1369 blur_posteriors(&mut single, dims, n_ch, 0.5);
1370 #[cfg(feature = "onnx")]
1371 {
1372 let mut half: Vec<half::f16> = <half::f16 as Posterior>::store(src);
1373 blur_posteriors(&mut half, dims, n_ch, 0.5);
1374 for (a, b) in single.iter().zip(&half) {
1375 assert!((a - b.to_f32()).abs() < 2e-3, "{a} vs {b}");
1376 }
1377 }
1378 }
1379
1380 #[cfg(feature = "onnx")]
1381 #[test]
1382 fn merging_the_flipped_pass_in_place_matches_mirroring_it_first() {
1383 let dims = [5usize, 3, 2];
1384 let n_ch = 3;
1385 let table = [0usize, 2, 1];
1386 let n = dims.iter().product::<usize>() * n_ch;
1387 let post: Vec<f32> = (0..n).map(|i| i as f32 * 0.25).collect();
1388 let flipped: Vec<f32> = (0..n).map(|i| (n - i) as f32 * 0.5).collect();
1389 let mut expected = post.clone();
1390 let mirrored = flip_axis0(&flipped, dims, n_ch);
1391 for (out, chunk) in expected.chunks_exact_mut(n_ch).zip(mirrored.chunks_exact(n_ch)) {
1392 for c in 0..n_ch {
1393 out[c] = 0.5 * (out[c] + chunk[table[c]]);
1394 }
1395 }
1396 let mut got = post;
1397 merge_flipped(&mut got, &flipped, dims, n_ch, &table);
1398 assert_eq!(got, expected);
1399 }
1400
1401 #[test]
1402 fn flip_axis0_is_an_involution() {
1403 let dims = [3usize, 2, 2];
1404 let n_ch = 2;
1405 let n = dims.iter().product::<usize>() * n_ch;
1406 let src: Vec<f32> = (0..n).map(|i| i as f32).collect();
1407 assert_eq!(flip_axis0(&flip_axis0(&src, dims, n_ch), dims, n_ch), src);
1408 }
1409}