1use crate::utils::gibbs_mrtrix::{gibbs_unring_2d, gibbs_unring_3d, MrtrixUnringParams};
87use num_complex::Complex64;
88use rustfft::{Fft, FftPlanner};
89use std::sync::Arc;
90
91#[cfg(feature = "parallel")]
92use rayon::prelude::*;
93
94const N_SHIFTS: usize = 45;
95const PI: f64 = std::f64::consts::PI;
96
97fn tv_min_line(row: &[f64], n: usize, n_points: usize, out: &mut [f64], diff: &mut [f64]) {
105 for j in 0..n - 1 {
106 diff[j] = (row[j] - row[j + 1]).abs();
107 }
108 diff[n - 1] = (row[n - 1] - row[0]).abs();
109 for i in 0..n {
110 let mut ptv = 0.0;
111 let mut ntv = 0.0;
112 if i >= n_points && i + n_points <= n {
113 for o in 0..n_points {
115 ptv += diff[i + o];
116 ntv += diff[i - 1 - o];
117 }
118 } else {
119 for o in 0..n_points {
120 ptv += diff[(i + o) % n];
121 ntv += diff[(i + n - 1 - o) % n];
122 }
123 }
124 out[i] = ptv.min(ntv);
125 }
126}
127
128#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
131pub enum GibbsMode {
132 #[default]
136 Bautista3d,
137 Mrtrix2d {
141 slice_axis: usize,
143 },
144 Dipy2d {
149 slice_axis: usize,
151 },
152}
153
154
155const N_POINTS: usize = 3;
157
158fn linspace(start: f64, stop: f64, num: usize) -> Vec<f64> {
160 if num == 1 {
161 return vec![start];
162 }
163 let step = (stop - start) / (num - 1) as f64;
164 let mut y: Vec<f64> = (0..num).map(|i| i as f64 * step + start).collect();
165 y[num - 1] = stop;
166 y
167}
168
169struct LineCtx {
173 n: usize,
174 ssamp: Vec<f64>,
175 phases: Vec<Complex64>,
177 fft: Arc<dyn Fft<f64>>,
178 ifft: Arc<dyn Fft<f64>>,
179}
180
181impl LineCtx {
182 fn new(n: usize, planner: &mut FftPlanner<f64>) -> Self {
183 let ssamp = linspace(0.02, 0.9, N_SHIFTS);
184 let inv_2pi = 1.0 / (2.0 * PI);
187 let b = n as f64 * -inv_2pi;
188 let val_im = -1.0 / b;
189 let mut phases = vec![Complex64::new(0.0, 0.0); ssamp.len() * n];
190 for (s_i, &s) in ssamp.iter().enumerate() {
191 for m in 0..n {
192 let mi = if m < n.div_ceil(2) { m as f64 } else { m as f64 - n as f64 };
193 let theta = (mi * val_im) * s;
194 phases[s_i * n + m] = Complex64::new(theta.cos(), theta.sin());
195 }
196 }
197 LineCtx { n, ssamp, phases, fft: planner.plan_fft_forward(n), ifft: planner.plan_fft_inverse(n) }
198 }
199}
200
201struct LineBufs {
203 c: Vec<Complex64>,
204 buf: Vec<Complex64>,
205 tvp: Vec<f64>,
206 tvn: Vec<f64>,
207 isp: Vec<f64>,
208 isn: Vec<f64>,
209 sp: Vec<f64>,
210 sn: Vec<f64>,
211 img: Vec<f64>,
212 tvs: Vec<f64>,
213 diff: Vec<f64>,
214 scratch_f: Vec<Complex64>,
215 scratch_i: Vec<Complex64>,
216}
217
218impl LineBufs {
219 fn new(ctx: &LineCtx) -> Self {
220 let (n, z) = (ctx.n, Complex64::new(0.0, 0.0));
221 LineBufs {
222 c: vec![z; n],
223 buf: vec![z; n],
224 tvp: vec![0.0; n],
225 tvn: vec![0.0; n],
226 isp: vec![0.0; n],
227 isn: vec![0.0; n],
228 sp: vec![0.0; n],
229 sn: vec![0.0; n],
230 img: vec![0.0; n],
231 tvs: vec![0.0; n],
232 diff: vec![0.0; n],
233 scratch_f: vec![z; ctx.fft.get_inplace_scratch_len()],
234 scratch_i: vec![z; ctx.ifft.get_inplace_scratch_len()],
235 }
236 }
237}
238
239fn unring_line(row: &mut [f64], b: &mut LineBufs, ctx: &LineCtx) {
243 let n = ctx.n;
244 let LineBufs { c, buf, tvp, tvn, isp, isn, sp, sn, img, tvs, diff, scratch_f, scratch_i } = b;
245 for m in 0..n {
246 c[m] = Complex64::new(row[m], 0.0);
247 }
248 ctx.fft.process_with_scratch(c, scratch_f);
249 tv_min_line(row, n, N_POINTS, tvp, diff);
250 tvn.copy_from_slice(tvp);
251 isp.copy_from_slice(row);
252 isn.copy_from_slice(row);
253 sp.iter_mut().for_each(|x| *x = 0.0);
254 sn.iter_mut().for_each(|x| *x = 0.0);
255 let inv_n = 1.0 / n as f64;
256 for (s_i, &s) in ctx.ssamp.iter().enumerate() {
257 let ph = &ctx.phases[s_i * n..s_i * n + n];
258 for (positive, (best_tv, best_img, best_s)) in [(true, (&mut *tvp, &mut *isp, &mut *sp)), (false, (&mut *tvn, &mut *isn, &mut *sn))] {
259 for m in 0..n {
260 buf[m] = if positive { c[m] * ph[m] } else { c[m] * ph[m].conj() };
261 }
262 ctx.ifft.process_with_scratch(buf, scratch_i);
263 for m in 0..n {
264 img[m] = (buf[m] * inv_n).norm();
265 }
266 tv_min_line(img, n, N_POINTS, tvs, diff);
267 for i in 0..n {
268 if best_tv[i] > tvs[i] {
269 best_img[i] = img[i];
270 best_s[i] = s;
271 best_tv[i] = tvs[i];
272 }
273 }
274 }
275 }
276 for i in 0..n {
277 let d = sp[i] + sn[i];
278 if d != 0.0 {
279 row[i] = (isp[i] - isn[i]) / d * sn[i] + isn[i];
280 }
281 }
282}
283
284#[cfg(test)]
285fn fftfreq(n: usize) -> Vec<f64> {
286 (0..n)
287 .map(|m| {
288 if m < n.div_ceil(2) {
289 m as f64 / n as f64
290 } else {
291 (m as f64 - n as f64) / n as f64
292 }
293 })
294 .collect()
295}
296
297#[cfg(test)]
300fn unring_axis(vol: &[f64], (nx, ny, nz): (usize, usize, usize), axis: usize) -> Vec<f64> {
301 let n = [nx, ny, nz][axis];
302 if n < 2 {
303 return vol.to_vec();
304 }
305 let mut planner = FftPlanner::<f64>::new();
306 let ctx = LineCtx::new(n, &mut planner);
307 let (stride, lines): (usize, Vec<usize>) = match axis {
308 0 => (1, (0..ny * nz).map(|l| l * nx).collect()),
309 1 => (nx, (0..nz).flat_map(|z| (0..nx).map(move |x| z * nx * ny + x)).collect()),
310 _ => (nx * ny, (0..ny).flat_map(|y| (0..nx).map(move |x| y * nx + x)).collect()),
311 };
312 let mut mat = vec![0.0_f64; lines.len() * n];
313 for (l, &start) in lines.iter().enumerate() {
314 for (i, d) in mat[l * n..l * n + n].iter_mut().enumerate() {
315 *d = vol[start + i * stride];
316 }
317 }
318 #[cfg(feature = "parallel")]
319 mat.par_chunks_mut(n).for_each_init(|| LineBufs::new(&ctx), |b, row| unring_line(row, b, &ctx));
320 #[cfg(not(feature = "parallel"))]
321 {
322 let mut b = LineBufs::new(&ctx);
323 for row in mat.chunks_mut(n) {
324 unring_line(row, &mut b, &ctx);
325 }
326 }
327 let mut out = vol.to_vec();
328 for (l, &start) in lines.iter().enumerate() {
329 for (i, &s) in mat[l * n..l * n + n].iter().enumerate() {
330 out[start + i * stride] = s;
331 }
332 }
333 out
334}
335
336fn dipy_weights(n0: usize, n1: usize) -> (Vec<f64>, Vec<f64>) {
339 let mut g0 = vec![0.0_f64; n0 * n1];
340 let mut g1 = vec![0.0_f64; n0 * n1];
341 let k0 = linspace(-PI, PI, n0);
342 let k1 = linspace(-PI, PI, n1);
343 for i in 1..n0.saturating_sub(1) {
344 for j in 1..n1.saturating_sub(1) {
345 let c0 = 1.0 + k0[i].cos();
346 let c1 = 1.0 + k1[j].cos();
347 g1[i * n1 + j] = c0 / (c0 + c1);
348 g0[i * n1 + j] = c1 / (c0 + c1);
349 }
350 }
351 if n0 >= 2 && n1 >= 1 {
352 for i in 1..n0 - 1 {
353 g1[i * n1] = 1.0;
354 g1[i * n1 + n1 - 1] = 1.0;
355 }
356 }
357 if n1 >= 2 && n0 >= 1 {
358 for j in 1..n1 - 1 {
359 g0[j] = 1.0;
360 g0[(n0 - 1) * n1 + j] = 1.0;
361 }
362 }
363 for &(i, j) in &[(0, 0), (n0 - 1, n1 - 1), (0, n1 - 1), (n0 - 1, 0)] {
364 g1[i * n1 + j] = 0.5;
365 g0[i * n1 + j] = 0.5;
366 }
367 (g0, g1)
368}
369
370struct SliceCtx {
372 n0: usize,
373 n1: usize,
374 line0: LineCtx,
375 line1: LineCtx,
376 g0: Vec<f64>,
377 g1: Vec<f64>,
378}
379
380fn fft2_inplace(a: &mut [Complex64], n0: usize, n1: usize, along1: &Arc<dyn Fft<f64>>, along0: &Arc<dyn Fft<f64>>) {
382 for row in a.chunks_mut(n1) {
383 along1.process(row);
384 }
385 let mut col = vec![Complex64::new(0.0, 0.0); n0];
386 for j in 0..n1 {
387 for i in 0..n0 {
388 col[i] = a[i * n1 + j];
389 }
390 along0.process(&mut col);
391 for i in 0..n0 {
392 a[i * n1 + j] = col[i];
393 }
394 }
395}
396
397fn unring_slice(img: &[f64], ctx: &SliceCtx) -> Vec<f64> {
400 let (n0, n1) = (ctx.n0, ctx.n1);
401 let mut c1 = img.to_vec();
403 let mut b1 = LineBufs::new(&ctx.line1);
404 for row in c1.chunks_mut(n1) {
405 unring_line(row, &mut b1, &ctx.line1);
406 }
407 let mut c0 = img.to_vec();
409 let mut b0 = LineBufs::new(&ctx.line0);
410 let mut col = vec![0.0_f64; n0];
411 for j in 0..n1 {
412 for i in 0..n0 {
413 col[i] = c0[i * n1 + j];
414 }
415 unring_line(&mut col, &mut b0, &ctx.line0);
416 for i in 0..n0 {
417 c0[i * n1 + j] = col[i];
418 }
419 }
420 let mut s1: Vec<Complex64> = c1.iter().map(|&v| Complex64::new(v, 0.0)).collect();
421 let mut s0: Vec<Complex64> = c0.iter().map(|&v| Complex64::new(v, 0.0)).collect();
422 fft2_inplace(&mut s1, n0, n1, &ctx.line1.fft, &ctx.line0.fft);
423 fft2_inplace(&mut s0, n0, n1, &ctx.line1.fft, &ctx.line0.fft);
424 let (h0, h1) = (n0 / 2, n1 / 2);
426 let mut sum = vec![Complex64::new(0.0, 0.0); n0 * n1];
427 for i in 0..n0 {
428 let si = (i + n0 - h0) % n0;
429 for j in 0..n1 {
430 let sj = (j + n1 - h1) % n1;
431 let w = i * n1 + j;
432 sum[w] = s1[si * n1 + sj] * ctx.g1[w] + s0[si * n1 + sj] * ctx.g0[w];
433 }
434 }
435 fft2_inplace(&mut sum, n0, n1, &ctx.line1.ifft, &ctx.line0.ifft);
436 let inv = 1.0 / (n0 * n1) as f64;
437 sum.iter().map(|z| (z * inv).norm()).collect()
438}
439
440fn unring_slices(vol: &[f64], (nx, ny, nz): (usize, usize, usize), slice_axis: usize) -> Vec<f64> {
442 let (ns, n0, n1) = match slice_axis {
444 0 => (nx, ny, nz),
445 1 => (ny, nx, nz),
446 _ => (nz, nx, ny),
447 };
448 let index = |s: usize, i: usize, j: usize| match slice_axis {
449 0 => s + i * nx + j * nx * ny,
450 1 => i + s * nx + j * nx * ny,
451 _ => i + j * nx + s * nx * ny,
452 };
453 let mut planner = FftPlanner::<f64>::new();
454 let (g0, g1) = dipy_weights(n0, n1);
455 let ctx = SliceCtx { n0, n1, line0: LineCtx::new(n0, &mut planner), line1: LineCtx::new(n1, &mut planner), g0, g1 };
456 let slice_out = |s: usize| -> Vec<f64> {
457 let mut img = vec![0.0_f64; n0 * n1];
458 for i in 0..n0 {
459 for j in 0..n1 {
460 img[i * n1 + j] = vol[index(s, i, j)];
461 }
462 }
463 unring_slice(&img, &ctx)
464 };
465 #[cfg(feature = "parallel")]
466 let slices: Vec<Vec<f64>> = (0..ns).into_par_iter().map(slice_out).collect();
467 #[cfg(not(feature = "parallel"))]
468 let slices: Vec<Vec<f64>> = (0..ns).map(slice_out).collect();
469 let mut out = vec![0.0_f64; vol.len()];
470 for (s, img) in slices.iter().enumerate() {
471 for i in 0..n0 {
472 for j in 0..n1 {
473 out[index(s, i, j)] = img[i * n1 + j];
474 }
475 }
476 }
477 out
478}
479
480pub fn gibbs_unring_volume(vol: &[f64], dims: (usize, usize, usize), mode: GibbsMode) -> Vec<f64> {
489 let (nx, ny, nz) = dims;
490 assert_eq!(vol.len(), nx * ny * nz, "vol length must be nx*ny*nz");
491 match mode {
492 GibbsMode::Bautista3d => gibbs_unring_3d(vol, dims, MrtrixUnringParams::default()),
493 GibbsMode::Mrtrix2d { slice_axis } => gibbs_unring_2d(vol, dims, slice_axis, MrtrixUnringParams::default()),
494 GibbsMode::Dipy2d { slice_axis } => {
495 assert!(slice_axis < 3, "slice_axis must be 0, 1 or 2");
496 unring_slices(vol, dims, slice_axis)
497 }
498 }
499}
500
501pub fn gibbs_unring(data: &[f64], dims: (usize, usize, usize), n_vols: usize, mode: GibbsMode) -> Vec<f64> {
514 let (nx, ny, nz) = dims;
515 let n_vox = nx * ny * nz;
516 assert_eq!(data.len(), n_vox * n_vols, "data length must be n_voxels * n_vols");
517 let mut out = data.to_vec();
518 let mut vol = vec![0.0_f64; n_vox];
519 for e in 0..n_vols {
520 for v in 0..n_vox {
521 vol[v] = data[v * n_vols + e];
522 }
523 let un = gibbs_unring_volume(&vol, dims, mode);
524 for v in 0..n_vox {
525 out[v * n_vols + e] = un[v];
526 }
527 }
528 out
529}
530
531pub fn gibbs_unring_masked(
552 data: &[f64],
553 dims: (usize, usize, usize),
554 n_vols: usize,
555 mask: &[u8],
556 margin: usize,
557 mode: GibbsMode,
558) -> Vec<f64> {
559 let (nx, ny, nz) = dims;
560 let n_vox = nx * ny * nz;
561 assert_eq!(data.len(), n_vox * n_vols, "data length must be n_voxels * n_vols");
562 assert_eq!(mask.len(), n_vox, "mask length must be n_voxels");
563
564 let (mut x0, mut y0, mut z0) = (nx, ny, nz);
566 let (mut x1, mut y1, mut z1) = (0usize, 0usize, 0usize);
567 let mut any = false;
568 for z in 0..nz {
569 for y in 0..ny {
570 for x in 0..nx {
571 if mask[x + y * nx + z * nx * ny] != 0 {
572 any = true;
573 x0 = x0.min(x);
574 y0 = y0.min(y);
575 z0 = z0.min(z);
576 x1 = x1.max(x);
577 y1 = y1.max(y);
578 z1 = z1.max(z);
579 }
580 }
581 }
582 }
583 if !any {
584 return data.to_vec();
585 }
586 x0 = x0.saturating_sub(margin);
588 y0 = y0.saturating_sub(margin);
589 z0 = z0.saturating_sub(margin);
590 x1 = (x1 + margin + 1).min(nx);
591 y1 = (y1 + margin + 1).min(ny);
592 z1 = (z1 + margin + 1).min(nz);
593 let (cx, cy, cz) = (x1 - x0, y1 - y0, z1 - z0);
594
595 if cx * cy * cz >= n_vox * 9 / 10 {
597 return gibbs_unring(data, dims, n_vols, mode);
598 }
599
600 let n_crop = cx * cy * cz;
601 let mut out = data.to_vec();
602 let mut sub = vec![0.0_f64; n_crop];
603 for e in 0..n_vols {
604 for zz in 0..cz {
606 for yy in 0..cy {
607 for xx in 0..cx {
608 let full = (x0 + xx) + (y0 + yy) * nx + (z0 + zz) * nx * ny;
609 sub[xx + yy * cx + zz * cx * cy] = data[full * n_vols + e];
610 }
611 }
612 }
613 let un = gibbs_unring_volume(&sub, (cx, cy, cz), mode);
614 for zz in 0..cz {
616 for yy in 0..cy {
617 for xx in 0..cx {
618 let full = (x0 + xx) + (y0 + yy) * nx + (z0 + zz) * nx * ny;
619 out[full * n_vols + e] = un[xx + yy * cx + zz * cx * cy];
620 }
621 }
622 }
623 }
624 out
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630 use crate::fft::{fft3d_real, ifft3d_real};
631
632 #[test]
633 fn test_unring_reduces_edge_overshoot() {
634 let (nx, ny, nz) = (64usize, 3usize, 3usize);
637 let mut clean = vec![0.0_f64; nx];
638 for x in 0..nx {
639 clean[x] = if x >= nx / 2 { 1.0 } else { 0.0 };
640 }
641 let mut planner = FftPlanner::<f64>::new();
643 let fft = planner.plan_fft_forward(nx);
644 let ifft = planner.plan_fft_inverse(nx);
645 let mut c: Vec<Complex64> = clean.iter().map(|&v| Complex64::new(v, 0.0)).collect();
646 fft.process(&mut c);
647 let keep = nx / 4;
648 for m in 0..nx {
649 let fm = if m < nx / 2 { m } else { nx - m };
650 if fm > keep {
651 c[m] = Complex64::new(0.0, 0.0);
652 }
653 }
654 ifft.process(&mut c);
655 let rung: Vec<f64> = c.iter().map(|z| z.re / nx as f64).collect();
656
657 let mut vol = vec![0.0_f64; nx * ny * nz];
659 for z in 0..nz {
660 for y in 0..ny {
661 for x in 0..nx {
662 vol[x + y * nx + z * nx * ny] = rung[x];
663 }
664 }
665 }
666 let un = unring_axis(&vol, (nx, ny, nz), 0);
670 let line: Vec<f64> = (0..nx).map(|x| un[x + nx + nx * ny]).collect();
672
673 let dipy_ref = [
676 0.06875, 0.25484, 0.67489, 1.06882, 1.02528, 0.96117, 0.98865, 1.02746, 1.00689,
677 0.97820, 0.97820,
678 ];
679 for (k, &r) in dipy_ref.iter().enumerate() {
680 let got = line[30 + k];
681 assert!(
682 (got - r).abs() < 5e-3,
683 "index {}: Rust {:.5} vs DIPY {:.5}",
684 30 + k,
685 got,
686 r
687 );
688 }
689 let tv = |a: &[f64]| a.windows(2).map(|w| (w[1] - w[0]).abs()).sum::<f64>();
691 let tv_rung: f64 = rung.windows(2).map(|w| (w[1] - w[0]).abs()).sum();
692 assert!(tv(&line) < 0.85 * tv_rung, "unring should cut TV: {:.3} -> {:.3}", tv_rung, tv(&line));
693 }
694
695 #[test]
698 fn test_unring_volume_is_never_negative() {
699 let (nx, ny, nz) = (24usize, 24usize, 24usize);
700 let n = nx * ny * nz;
701 let mut block = vec![0.0_f64; n];
702 for z in 8..16 {
703 for y in 8..16 {
704 for x in 8..16 {
705 block[x + y * nx + z * nx * ny] = 1.0;
706 }
707 }
708 }
709 let mut spec = fft3d_real(&block, nx, ny, nz);
711 let f = fftfreq(nx);
712 for z in 0..nz {
713 for y in 0..ny {
714 for x in 0..nx {
715 if f[x].abs() > 0.2 || f[y].abs() > 0.2 || f[z].abs() > 0.2 {
716 spec[x + y * nx + z * nx * ny] = Complex64::new(0.0, 0.0);
717 }
718 }
719 }
720 }
721 let rung = ifft3d_real(&spec, nx, ny, nz);
722 assert!(rung.iter().any(|&v| v < -1e-3), "the test input should undershoot");
723 let un = gibbs_unring_volume(&rung, (nx, ny, nz), GibbsMode::default());
724 assert!(un.iter().all(|&v| v >= 0.0), "unrung magnitude must not be negative");
725 assert!(un.contains(&0.0));
726 let c = 12 + 12 * nx + 12 * nx * ny;
727 assert!(un[c] > 0.5, "block interior kept: {}", un[c]);
728 for mode in [
729 GibbsMode::Bautista3d,
730 GibbsMode::Mrtrix2d { slice_axis: 2 },
731 GibbsMode::Mrtrix2d { slice_axis: 0 },
732 GibbsMode::Dipy2d { slice_axis: 2 },
733 GibbsMode::Dipy2d { slice_axis: 0 },
734 ] {
735 let multi = gibbs_unring(&rung, (nx, ny, nz), 1, mode);
736 assert!(multi.iter().all(|&v| v >= 0.0), "{mode:?}");
737 assert!(multi[c] > 0.5, "{mode:?}: block interior kept: {}", multi[c]);
738 }
739 }
740
741 #[test]
745 #[ignore]
746 fn bench_unring_volume() {
747 let (nx, ny, nz) = (128usize, 128usize, 128usize);
748 let mut vol = vec![0.0_f64; nx * ny * nz];
749 let mut lcg: u64 = 42;
750 for z in 0..nz {
751 for y in 0..ny {
752 for x in 0..nx {
753 let dx = x as f64 - nx as f64 / 2.0;
754 let dy = y as f64 - ny as f64 / 2.0;
755 let dz = z as f64 - nz as f64 / 2.0;
756 let r = (dx * dx + dy * dy + dz * dz).sqrt();
757 let base = if r < nx as f64 / 3.0 { 1.0 } else { 0.1 };
758 lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
759 let noise = (lcg >> 33) as f64 / (1u64 << 31) as f64 * 0.02;
760 vol[x + y * nx + z * nx * ny] = base + noise;
761 }
762 }
763 }
764 let t = std::time::Instant::now();
765 let out = gibbs_unring_volume(&vol, (nx, ny, nz), GibbsMode::default());
766 let dt = t.elapsed();
767 let sum: f64 = out.iter().sum();
768 let sq: f64 = out.iter().map(|v| v * v).sum();
769 let mx = out.iter().cloned().fold(f64::MIN, f64::max);
770 println!(
771 "gibbs_unring_volume 128^3: {:?} sum {:.6} sumsq {:.6} max {:.6}",
772 dt, sum, sq, mx
773 );
774 }
775
776 #[test]
777 fn test_fftfreq_matches_numpy() {
778 let f = fftfreq(4);
780 assert!((f[0] - 0.0).abs() < 1e-12);
781 assert!((f[1] - 0.25).abs() < 1e-12);
782 assert!((f[2] + 0.5).abs() < 1e-12);
783 assert!((f[3] + 0.25).abs() < 1e-12);
784 }
785}