1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct CropBox {
40 pub origin: (isize, isize, isize),
43 pub dims: (usize, usize, usize),
45 pub full_dims: (usize, usize, usize),
47}
48
49impl CropBox {
50 pub fn full(full_dims: (usize, usize, usize)) -> Self {
52 Self { origin: (0, 0, 0), dims: full_dims, full_dims }
53 }
54
55 pub fn pads(&self) -> bool {
57 let (ox, oy, oz) = self.origin;
58 let (dx, dy, dz) = self.dims;
59 let (fx, fy, fz) = self.full_dims;
60 ox < 0 || oy < 0 || oz < 0
61 || ox + dx as isize > fx as isize
62 || oy + dy as isize > fy as isize
63 || oz + dz as isize > fz as isize
64 }
65
66 pub fn is_full(&self) -> bool {
68 self.origin == (0, 0, 0) && self.dims == self.full_dims
69 }
70
71 pub fn voxels(&self) -> usize {
72 self.dims.0 * self.dims.1 * self.dims.2
73 }
74
75 pub fn full_voxels(&self) -> usize {
76 self.full_dims.0 * self.full_dims.1 * self.full_dims.2
77 }
78
79 pub fn reduction(&self) -> f64 {
83 if self.voxels() == 0 { 1.0 } else { self.full_voxels() as f64 / self.voxels() as f64 }
84 }
85
86 pub fn crop_affine(&self, affine: &[f64; 16]) -> [f64; 16] {
88 let (ox, oy, oz) = (self.origin.0 as f64, self.origin.1 as f64, self.origin.2 as f64);
89 let mut out = *affine;
90 for row in 0..3 {
91 out[4 * row + 3] = affine[4 * row + 3]
92 + affine[4 * row] * ox
93 + affine[4 * row + 1] * oy
94 + affine[4 * row + 2] * oz;
95 }
96 out
97 }
98}
99
100pub fn next_fft_friendly_size(n: usize) -> usize {
104 if n <= 1 {
105 return n.max(1);
106 }
107 let smooth = |mut m: usize| {
108 for p in [2usize, 3, 5, 7] {
109 while m.is_multiple_of(p) {
110 m /= p;
111 }
112 }
113 m == 1
114 };
115 let mut candidate = n;
116 while !smooth(candidate) {
117 candidate += 1;
118 }
119 candidate
120}
121
122pub fn margin_voxels(margin_mm: f64, voxel_size: (f64, f64, f64)) -> (usize, usize, usize) {
126 let per_axis = |mm: f64, vs: f64| -> usize {
127 if mm <= 0.0 || vs <= 0.0 {
128 0
129 } else {
130 ((mm / vs).ceil() as usize).max(1)
131 }
132 };
133 (
134 per_axis(margin_mm, voxel_size.0),
135 per_axis(margin_mm, voxel_size.1),
136 per_axis(margin_mm, voxel_size.2),
137 )
138}
139
140pub fn crop_box_for_mask(
146 mask: &[u8],
147 full_dims: (usize, usize, usize),
148 voxel_size: (f64, f64, f64),
149 margin_mm: f64,
150) -> CropBox {
151 let (nx, ny, nz) = full_dims;
152 if mask.len() != nx * ny * nz {
153 return fft_pad_box(full_dims);
154 }
155
156 let (mut lo, mut hi) = ([usize::MAX; 3], [0usize; 3]);
157 let mut any = false;
158 for z in 0..nz {
159 for y in 0..ny {
160 for x in 0..nx {
161 if mask[x + y * nx + z * nx * ny] > 0 {
162 any = true;
163 for (d, v) in [x, y, z].iter().enumerate() {
164 lo[d] = lo[d].min(*v);
165 hi[d] = hi[d].max(*v);
166 }
167 }
168 }
169 }
170 }
171 if !any {
172 return fft_pad_box(full_dims);
173 }
174
175 let margin = margin_voxels(margin_mm, voxel_size);
176 let margin = [margin.0, margin.1, margin.2];
177 let full = [nx as isize, ny as isize, nz as isize];
178 let (mut origin, mut dims) = ([0isize; 3], [0usize; 3]);
179
180 for d in 0..3 {
181 let start = (lo[d] as isize - margin[d] as isize).max(0);
183 let end = ((hi[d] + margin[d] + 1) as isize).min(full[d]);
184 let wanted = next_fft_friendly_size((end - start) as usize) as isize;
185 let extra = wanted - (end - start);
189 let mut s0 = start - extra / 2;
190 if wanted <= full[d] {
192 s0 = s0.clamp(0, full[d] - wanted);
193 }
194 origin[d] = s0;
195 dims[d] = wanted as usize;
196 }
197
198 CropBox {
199 origin: (origin[0], origin[1], origin[2]),
200 dims: (dims[0], dims[1], dims[2]),
201 full_dims,
202 }
203}
204
205pub fn fft_pad_box(full_dims: (usize, usize, usize)) -> CropBox {
212 let full = [full_dims.0, full_dims.1, full_dims.2];
213 let (mut origin, mut dims) = ([0isize; 3], [0usize; 3]);
214 for d in 0..3 {
215 let wanted = next_fft_friendly_size(full[d]);
216 origin[d] = -(((wanted - full[d]) / 2) as isize);
218 dims[d] = wanted;
219 }
220 CropBox {
221 origin: (origin[0], origin[1], origin[2]),
222 dims: (dims[0], dims[1], dims[2]),
223 full_dims,
224 }
225}
226
227pub fn crop_volume<T: Copy + Default>(data: &[T], b: &CropBox) -> Vec<T> {
232 crop_volume_with(data, b, T::default())
233}
234
235pub fn crop_volume_with<T: Copy>(data: &[T], b: &CropBox, fill: T) -> Vec<T> {
240 let (nx, ny, nz) = b.full_dims;
241 assert_eq!(data.len(), b.full_voxels(), "crop_volume: data does not match the full grid");
242 if b.is_full() {
243 return data.to_vec();
244 }
245 let (cx, cy, cz) = b.dims;
246 let (ox, oy, oz) = b.origin;
247 let mut out = vec![fill; b.voxels()];
248 for z in 0..cz {
249 let sz = oz + z as isize;
250 if sz < 0 || sz >= nz as isize {
251 continue;
252 }
253 for y in 0..cy {
254 let sy = oy + y as isize;
255 if sy < 0 || sy >= ny as isize {
256 continue;
257 }
258 let x0 = (-ox).max(0);
260 let x1 = (nx as isize - ox).min(cx as isize);
261 if x1 <= x0 {
262 continue;
263 }
264 let src = (ox + x0) as usize + sy as usize * nx + sz as usize * nx * ny;
265 let dst = x0 as usize + y * cx + z * cx * cy;
266 let len = (x1 - x0) as usize;
267 out[dst..dst + len].copy_from_slice(&data[src..src + len]);
268 }
269 }
270 out
271}
272
273pub fn uncrop_volume<T: Copy>(data: &[T], b: &CropBox, fill: T) -> Vec<T> {
279 assert_eq!(data.len(), b.voxels(), "uncrop_volume: data does not match the crop box");
280 if b.is_full() {
281 return data.to_vec();
282 }
283 let (nx, ny, nz) = b.full_dims;
284 let (cx, cy, cz) = b.dims;
285 let (ox, oy, oz) = b.origin;
286 let mut out = vec![fill; b.full_voxels()];
287 for z in 0..cz {
288 let dz = oz + z as isize;
289 if dz < 0 || dz >= nz as isize {
290 continue; }
292 for y in 0..cy {
293 let dy = oy + y as isize;
294 if dy < 0 || dy >= ny as isize {
295 continue;
296 }
297 let x0 = (-ox).max(0);
298 let x1 = (nx as isize - ox).min(cx as isize);
299 if x1 <= x0 {
300 continue;
301 }
302 let src = x0 as usize + y * cx + z * cx * cy;
303 let dst = (ox + x0) as usize + dy as usize * nx + dz as usize * nx * ny;
304 let len = (x1 - x0) as usize;
305 out[dst..dst + len].copy_from_slice(&data[src..src + len]);
306 }
307 }
308 out
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 fn box_mask(full: (usize, usize, usize), lo: (usize, usize, usize), hi: (usize, usize, usize)) -> Vec<u8> {
316 let (nx, ny, nz) = full;
317 let mut m = vec![0u8; nx * ny * nz];
318 for z in lo.2..=hi.2 {
319 for y in lo.1..=hi.1 {
320 for x in lo.0..=hi.0 {
321 m[x + y * nx + z * nx * ny] = 1;
322 }
323 }
324 }
325 m
326 }
327
328 #[test]
329 fn fft_friendly_sizes() {
330 assert_eq!(next_fft_friendly_size(339), 343); assert_eq!(next_fft_friendly_size(340), 343);
332 assert_eq!(next_fft_friendly_size(256), 256);
333 assert_eq!(next_fft_friendly_size(188), 189); assert_eq!(next_fft_friendly_size(1), 1);
335 for n in 1..2000 {
336 let s = next_fft_friendly_size(n);
337 assert!(s >= n);
338 let mut m = s;
339 for p in [2, 3, 5, 7] {
340 while m % p == 0 {
341 m /= p;
342 }
343 }
344 assert_eq!(m, 1, "{s} (from {n}) is not 7-smooth");
345 }
346 }
347
348 #[test]
349 fn margin_is_geometric_not_voxel_count() {
350 assert_eq!(margin_voxels(8.0, (0.8, 0.8, 3.0)), (10, 10, 3));
352 assert_eq!(margin_voxels(0.0, (1.0, 1.0, 1.0)), (0, 0, 0));
353 }
354
355 #[test]
357 fn empty_or_unusable_mask_falls_back_to_padding() {
358 let full = (16, 16, 8); for mask in [vec![0u8; 16 * 16 * 8], vec![1u8; 16 * 16 * 8], vec![1u8; 10]] {
360 let b = crop_box_for_mask(&mask, full, (1.0, 1.0, 1.0), 4.0);
361 assert!(b.is_full(), "{:?}+{:?}", b.origin, b.dims);
362 }
363 let awkward = (17, 16, 8);
365 let b = crop_box_for_mask(&vec![0u8; 17 * 16 * 8], awkward, (1.0, 1.0, 1.0), 4.0);
366 assert!(b.pads() && b.dims.0 == next_fft_friendly_size(17));
367 }
368
369 #[test]
370 fn box_contains_the_mask_with_its_margin() {
371 let full = (64, 64, 32);
372 let mask = box_mask(full, (20, 24, 10), (40, 44, 20));
373 let b = crop_box_for_mask(&mask, full, (1.0, 1.0, 1.0), 3.0);
374 assert!(!b.is_full(), "should have cropped something");
375 let lo = [b.origin.0, b.origin.1, b.origin.2];
376 let hi = [
377 b.origin.0 + b.dims.0 as isize,
378 b.origin.1 + b.dims.1 as isize,
379 b.origin.2 + b.dims.2 as isize,
380 ];
381 for (d, (mlo, mhi)) in [(20, 40), (24, 44), (10, 20)].iter().enumerate() {
383 assert!(lo[d] <= mlo - 3, "axis {d}: box starts at {}, mask-3 at {}", lo[d], mlo - 3);
384 assert!(hi[d] >= mhi + 1 + 3, "axis {d}: box ends at {}, mask+3 at {}", hi[d], mhi + 4);
385 }
386 assert!(!b.pads(), "box {:?}+{:?} should stay inside {:?}", b.origin, b.dims, full);
388 for d in [b.dims.0, b.dims.1, b.dims.2] {
389 assert_eq!(next_fft_friendly_size(d), d, "{d} should already be FFT-friendly");
390 }
391 assert!(b.reduction() > 1.0);
392 }
393
394 #[test]
396 fn ukb_sized_volume_shrinks_substantially() {
397 let full = (256, 288, 48);
398 let mask = box_mask(full, (42, 36, 2), (213, 251, 45));
400 let b = crop_box_for_mask(&mask, full, (0.8, 0.8, 3.0), 8.0);
401 assert!(b.reduction() > 1.2, "expected a worthwhile reduction, got {:.2}x", b.reduction());
402 assert!(b.voxels() < b.full_voxels());
403 }
404
405 #[test]
411 fn fft_pad_box_grows_every_awkward_axis() {
412 let b = fft_pad_box((272, 339, 77));
413 assert_eq!(b.dims, (280, 343, 80), "each axis should reach the next 7-smooth size");
414 assert!(b.pads());
415 assert_eq!(b.origin, (-4, -2, -1));
417 assert!(b.reduction() < 1.0, "{}", b.reduction());
419 let overhead = b.voxels() as f64 / b.full_voxels() as f64 - 1.0;
420 assert!(overhead < 0.10, "padding overhead {overhead:.3} should stay under 10%");
421 }
422
423 #[test]
424 fn fft_pad_box_is_a_no_op_on_a_friendly_grid() {
425 let b = fft_pad_box((256, 288, 48));
426 assert!(b.is_full(), "already 7-smooth: {:?}+{:?}", b.origin, b.dims);
427 assert!(!b.pads());
428 }
429
430 #[test]
432 fn pad_then_unpad_is_lossless() {
433 let full = (17, 13, 5); let n = full.0 * full.1 * full.2;
435 let data: Vec<f64> = (0..n).map(|i| (i as f64) * 0.5 + 1.0).collect();
436 let b = fft_pad_box(full);
437 assert!(b.pads() && b.voxels() > n);
438
439 let padded = crop_volume(&data, &b);
440 assert_eq!(padded.len(), b.voxels());
441 assert_eq!(padded.iter().filter(|v| **v != 0.0).count(), n, "all data present, nothing extra");
443
444 let back = uncrop_volume(&padded, &b, f64::NAN);
445 assert_eq!(back.len(), n);
446 for (i, (got, want)) in back.iter().zip(data.iter()).enumerate() {
447 assert_eq!(got, want, "voxel {i} changed across a pad round trip");
448 }
449 }
450
451 #[test]
452 fn pad_fill_value_is_respected() {
453 let full = (11, 4, 2);
454 let b = fft_pad_box(full);
455 assert!(b.pads());
456 let padded = crop_volume_with(&vec![7.0f64; full.0 * full.1 * full.2], &b, -1.0);
457 assert!(padded.iter().any(|v| *v == -1.0), "padding should use the fill value");
458 assert_eq!(padded.iter().filter(|v| **v == 7.0).count(), full.0 * full.1 * full.2);
459 }
460
461 #[test]
464 fn a_box_can_crop_and_pad_at_the_same_time() {
465 let full = (64, 17, 16);
466 let mask = box_mask(full, (20, 2, 4), (40, 14, 11));
467 let b = crop_box_for_mask(&mask, full, (1.0, 1.0, 1.0), 2.0);
468 assert!(b.dims.0 < full.0, "x should crop, got {}", b.dims.0);
470 assert!(b.pads(), "y should pad: {:?}+{:?}", b.origin, b.dims);
471 for d in [b.dims.0, b.dims.1, b.dims.2] {
472 assert_eq!(next_fft_friendly_size(d), d, "{d} not FFT-friendly");
473 }
474 let n = full.0 * full.1 * full.2;
476 let data: Vec<f64> = (0..n).map(|i| i as f64).collect();
477 let back = uncrop_volume(&crop_volume(&data, &b), &b, -1.0);
478 let mut restored = 0usize;
480 for (i, v) in back.iter().enumerate() {
481 if *v != -1.0 {
482 assert_eq!(*v, data[i], "voxel {i} altered by the round trip");
483 restored += 1;
484 }
485 }
486 assert!(restored > 0 && restored < n, "expected a partial cover, got {restored} of {n}");
487 }
488
489 #[test]
490 fn crop_then_uncrop_round_trips_inside_the_box() {
491 let full = (12, 10, 6);
492 let (nx, ny, _) = full;
493 let data: Vec<f64> = (0..12 * 10 * 6).map(|i| i as f64).collect();
494 let b = CropBox { origin: (2, 3, 1), dims: (6, 4, 3), full_dims: full };
495 assert!(!b.pads());
496
497 let cropped = crop_volume(&data, &b);
498 assert_eq!(cropped.len(), b.voxels());
499 for z in 0..b.dims.2 {
501 for y in 0..b.dims.1 {
502 for x in 0..b.dims.0 {
503 let got = cropped[x + y * b.dims.0 + z * b.dims.0 * b.dims.1];
504 let want = data[(x + 2) + (y + 3) * nx + (z + 1) * nx * ny];
505 assert_eq!(got, want, "at ({x},{y},{z})");
506 }
507 }
508 }
509
510 let restored = uncrop_volume(&cropped, &b, 0.0);
511 assert_eq!(restored.len(), data.len());
512 for z in 0..full.2 {
513 for y in 0..full.1 {
514 for x in 0..full.0 {
515 let i = x + y * nx + z * nx * ny;
516 let inside = (2..8).contains(&x) && (3..7).contains(&y) && (1..4).contains(&z);
517 assert_eq!(restored[i], if inside { data[i] } else { 0.0 }, "at ({x},{y},{z})");
518 }
519 }
520 }
521 }
522
523 #[test]
524 fn full_box_is_a_no_op() {
525 let full = (5, 4, 3);
526 let data: Vec<f64> = (0..60).map(|i| i as f64).collect();
527 let b = CropBox::full(full);
528 assert_eq!(crop_volume(&data, &b), data);
529 assert_eq!(uncrop_volume(&data, &b, 0.0), data);
530 assert_eq!(b.reduction(), 1.0);
531 }
532
533 #[test]
534 fn masks_crop_and_uncrop_too() {
535 let full = (8, 8, 4);
536 let mask = box_mask(full, (2, 2, 1), (5, 5, 2));
537 let b = CropBox { origin: (1, 1, 0), dims: (6, 6, 4), full_dims: full };
538 let c = crop_volume(&mask, &b);
539 assert_eq!(c.iter().filter(|v| **v == 1).count(), mask.iter().filter(|v| **v == 1).count());
540 let back = uncrop_volume(&c, &b, 0u8);
541 assert_eq!(back, mask);
542 }
543
544 #[test]
545 fn crop_affine_shifts_the_origin_only() {
546 let affine = [
548 2.0, 0.0, 0.0, -10.0,
549 0.0, 2.0, 0.0, -20.0,
550 0.0, 0.0, 2.0, -30.0,
551 0.0, 0.0, 0.0, 1.0,
552 ];
553 let b = CropBox { origin: (3, 4, 5), dims: (4, 4, 4), full_dims: (16, 16, 16) };
554 let out = b.crop_affine(&affine);
555 for i in [0, 1, 2, 4, 5, 6, 8, 9, 10] {
557 assert_eq!(out[i], affine[i]);
558 }
559 assert_eq!((out[3], out[7], out[11]), (-4.0, -12.0, -20.0));
561 }
562}