1use crate::utils::morphology::{erode_mask, fill_holes};
20use crate::Grid;
21#[cfg(feature = "parallel")]
22use rayon::prelude::*;
23
24#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
26#[derive(Clone, Debug, PartialEq)]
27pub struct SignalErosionParams {
28 pub threshold: f64,
31 pub depth_cap: usize,
35 pub global_erosions: usize,
38 pub bias_sigma: f64,
41 pub min_component: usize,
44}
45
46impl Default for SignalErosionParams {
47 fn default() -> Self {
48 Self { threshold: 0.80, depth_cap: 5, global_erosions: 1, bias_sigma: 12.0, min_component: 1000 }
49 }
50}
51
52pub fn signal_gated_erosion(
57 mask: &[u8],
58 magnitude: &[f64],
59 grid: &Grid,
60 params: &SignalErosionParams,
61) -> Vec<u8> {
62 let (nx, ny, nz) = grid.dims;
63 let n = nx * ny * nz;
64 assert_eq!(mask.len(), n, "mask length does not match grid");
65 assert_eq!(magnitude.len(), n, "magnitude length does not match grid");
66 let inside: Vec<bool> = mask.iter().map(|&m| m != 0).collect();
67 if !inside.iter().any(|&b| b) {
68 return mask.to_vec();
69 }
70
71 let sig = gaussian_filter(magnitude, grid.dims, 1.0);
74 let masked: Vec<f64> = sig.iter().zip(&inside).map(|(&s, &m)| if m { s } else { 0.0 }).collect();
75 let num = gaussian_filter(&masked, grid.dims, params.bias_sigma);
76 let mask_f: Vec<f64> = inside.iter().map(|&m| m as u8 as f64).collect();
77 let den = gaussian_filter(&mask_f, grid.dims, params.bias_sigma);
78 let rel: Vec<f64> = (0..n).map(|i| sig[i] / (num[i] / den[i].max(1e-6)).max(1e-6)).collect();
79
80 let mut in_mask: Vec<f64> = (0..n).filter(|&i| inside[i]).map(|i| rel[i]).collect();
81 let gate = params.threshold * median(&mut in_mask);
82
83 let depth = (params.depth_cap > 0).then(|| distance_transform_padded(&inside, grid.dims));
86 let mut current = if params.global_erosions > 0 {
87 erode_mask(mask, grid, params.global_erosions)
88 } else {
89 mask.iter().map(|&m| (m != 0) as u8).collect()
90 };
91 let candidates: Vec<usize> = (0..n)
92 .filter(|&i| current[i] != 0 && rel[i] < gate)
93 .filter(|&i| depth.as_ref().is_none_or(|d| d[i] <= params.depth_cap as f64))
94 .collect();
95
96 let passes = (params.depth_cap + 2).max(40);
100 let nxy = nx * ny;
101 for _ in 0..passes {
102 let drop: Vec<usize> = candidates
103 .iter()
104 .copied()
105 .filter(|&i| current[i] != 0)
106 .filter(|&i| {
107 let (x, y, z) = (i % nx, (i / nx) % ny, i / nxy);
108 x == 0 || x == nx - 1 || y == 0 || y == ny - 1 || z == 0 || z == nz - 1
109 || current[i - 1] == 0 || current[i + 1] == 0
110 || current[i - nx] == 0 || current[i + nx] == 0
111 || current[i - nxy] == 0 || current[i + nxy] == 0
112 })
113 .collect();
114 if drop.is_empty() {
115 break;
116 }
117 for i in drop {
118 current[i] = 0;
119 }
120 }
121
122 let filled = fill_holes(¤t, grid, n);
125 keep_large_components(&filled, grid.dims, params.min_component)
126}
127
128fn median(v: &mut [f64]) -> f64 {
130 let len = v.len();
131 let mid = len / 2;
132 let (_, &mut hi, _) = v.select_nth_unstable_by(mid, |a, b| a.total_cmp(b));
133 if len % 2 == 1 {
134 hi
135 } else {
136 let lo = v[..mid].iter().copied().fold(f64::NEG_INFINITY, f64::max);
137 0.5 * (lo + hi)
138 }
139}
140
141#[inline]
144fn reflect(i: isize, n: usize) -> usize {
145 let n = n as isize;
146 let m = i.rem_euclid(2 * n);
147 (if m >= n { 2 * n - 1 - m } else { m }) as usize
148}
149
150pub(crate) fn gaussian_filter(data: &[f64], dims: (usize, usize, usize), sigma: f64) -> Vec<f64> {
153 gaussian_filter_anisotropic(data, dims, [sigma; 3])
154}
155
156fn gaussian_weights(sigma: f64) -> Vec<f64> {
158 let radius = (4.0 * sigma + 0.5) as isize;
159 let mut w: Vec<f64> =
160 (-radius..=radius).map(|x| (-0.5 * (x * x) as f64 / (sigma * sigma)).exp()).collect();
161 let s: f64 = w.iter().sum();
162 w.iter_mut().for_each(|v| *v /= s);
163 w
164}
165
166pub(crate) fn gaussian_filter_anisotropic(
169 data: &[f64],
170 dims: (usize, usize, usize),
171 sigma: [f64; 3],
172) -> Vec<f64> {
173 if sigma.iter().all(|&s| s <= 0.0) {
174 return data.to_vec();
175 }
176 let (nx, ny, nz) = dims;
177 let nxy = nx * ny;
178
179 let a = if sigma[0] > 0.0 {
181 let w = gaussian_weights(sigma[0]);
182 let radius = (w.len() / 2) as isize;
183 let mut a = vec![0.0; data.len()];
184 crate::maybe_par_chunks_mut!(a, nx).enumerate().for_each(|(r, row)| {
185 let src = &data[r * nx..(r + 1) * nx];
186 for (x, out) in row.iter_mut().enumerate() {
187 *out = w
188 .iter()
189 .enumerate()
190 .map(|(t, &wt)| wt * src[reflect(x as isize + t as isize - radius, nx)])
191 .sum();
192 }
193 });
194 a
195 } else {
196 data.to_vec()
197 };
198 let b = if sigma[1] > 0.0 {
200 let w = gaussian_weights(sigma[1]);
201 let radius = (w.len() / 2) as isize;
202 let mut b = vec![0.0; data.len()];
203 crate::maybe_par_chunks_mut!(b, nxy).enumerate().for_each(|(z, slab)| {
204 let src = &a[z * nxy..(z + 1) * nxy];
205 for y in 0..ny {
206 let out = &mut slab[y * nx..(y + 1) * nx];
207 for (t, &wt) in w.iter().enumerate() {
208 let sy = reflect(y as isize + t as isize - radius, ny);
209 for (o, &v) in out.iter_mut().zip(&src[sy * nx..(sy + 1) * nx]) {
210 *o += wt * v;
211 }
212 }
213 }
214 });
215 b
216 } else {
217 a
218 };
219 if sigma[2] <= 0.0 {
221 return b;
222 }
223 let w = gaussian_weights(sigma[2]);
224 let radius = (w.len() / 2) as isize;
225 let mut c = vec![0.0; data.len()];
226 crate::maybe_par_chunks_mut!(c, nxy).enumerate().for_each(|(z, slab)| {
227 for (t, &wt) in w.iter().enumerate() {
228 let sz = reflect(z as isize + t as isize - radius, nz);
229 for (o, &v) in slab.iter_mut().zip(&b[sz * nxy..(sz + 1) * nxy]) {
230 *o += wt * v;
231 }
232 }
233 });
234 c
235}
236
237pub(crate) fn distance_transform_padded(inside: &[bool], dims: (usize, usize, usize)) -> Vec<f64> {
241 let (nx, ny, nz) = dims;
242 let (px, py, pz) = (nx + 2, ny + 2, nz + 2);
243 const FAR: f64 = 1e20;
244 let mut f = vec![0.0; px * py * pz];
245 for z in 0..nz {
246 for y in 0..ny {
247 for x in 0..nx {
248 if inside[x + nx * (y + ny * z)] {
249 f[(x + 1) + px * ((y + 1) + py * (z + 1))] = FAR;
250 }
251 }
252 }
253 }
254 let (mut line, mut out) = (Vec::new(), Vec::new());
256 let mut pass = |f: &mut [f64], base: usize, stride: usize, len: usize| {
257 line.clear();
258 line.extend((0..len).map(|i| f[base + i * stride]));
259 edt_1d(&line, &mut out);
260 for (i, &v) in out.iter().enumerate() {
261 f[base + i * stride] = v;
262 }
263 };
264 for z in 0..pz {
265 for y in 0..py {
266 pass(&mut f, px * (y + py * z), 1, px);
267 }
268 }
269 for z in 0..pz {
270 for x in 0..px {
271 pass(&mut f, x + px * py * z, px, py);
272 }
273 }
274 for y in 0..py {
275 for x in 0..px {
276 pass(&mut f, x + px * y, px * py, pz);
277 }
278 }
279 let mut d = vec![0.0; nx * ny * nz];
280 for z in 0..nz {
281 for y in 0..ny {
282 for x in 0..nx {
283 let i = x + nx * (y + ny * z);
284 if inside[i] {
285 d[i] = f[(x + 1) + px * ((y + 1) + py * (z + 1))].sqrt();
286 }
287 }
288 }
289 }
290 d
291}
292
293fn edt_1d(f: &[f64], d: &mut Vec<f64>) {
295 let n = f.len();
296 let mut v = vec![0usize; n];
297 let mut z = vec![0.0f64; n + 1];
298 let mut k = 0usize;
299 z[0] = f64::NEG_INFINITY;
300 z[1] = f64::INFINITY;
301 let inter = |q: usize, p: usize| {
302 ((f[q] + (q * q) as f64) - (f[p] + (p * p) as f64)) / (2.0 * q as f64 - 2.0 * p as f64)
303 };
304 for q in 1..n {
305 let mut s = inter(q, v[k]);
306 while s <= z[k] {
307 k -= 1;
308 s = inter(q, v[k]);
309 }
310 k += 1;
311 v[k] = q;
312 z[k] = s;
313 z[k + 1] = f64::INFINITY;
314 }
315 d.clear();
316 d.resize(n, 0.0);
317 k = 0;
318 for (q, dq) in d.iter_mut().enumerate() {
319 while z[k + 1] < q as f64 {
320 k += 1;
321 }
322 let dx = q as f64 - v[k] as f64;
323 *dq = dx * dx + f[v[k]];
324 }
325}
326
327fn keep_large_components(mask: &[u8], dims: (usize, usize, usize), min_size: usize) -> Vec<u8> {
329 let (label, sizes) = crate::utils::connected::label_components(
330 mask,
331 dims,
332 crate::utils::connected::Connectivity::Face,
333 );
334 let largest = sizes.iter().copied().max().unwrap_or(0);
335 let keep = min_size.min(largest);
336 label.iter().map(|&l| (l != 0 && sizes[l as usize] >= keep) as u8).collect()
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 fn grid(n: (usize, usize, usize)) -> Grid {
344 Grid::new(n.0, n.1, n.2, 1.0, 1.0, 1.0)
345 }
346
347 fn idx(x: usize, y: usize, z: usize, n: (usize, usize, usize)) -> usize {
348 x + n.0 * (y + n.1 * z)
349 }
350
351 #[test]
352 fn edt_matches_brute_force() {
353 let dims = (9, 7, 6);
354 let inside: Vec<bool> = (0..dims.0 * dims.1 * dims.2)
356 .map(|i| {
357 let (x, y, z) = (i % dims.0, (i / dims.0) % dims.1, i / (dims.0 * dims.1));
358 (x as f64 - 2.0).powi(2) / 9.0 + (y as f64 - 3.0).powi(2) / 6.0 + (z as f64 - 2.5).powi(2) / 5.0 < 1.6
359 && (x + y + z) % 7 != 3
360 })
361 .collect();
362 let d = distance_transform_padded(&inside, dims);
363 let (px, py, pz) = (dims.0 as isize + 2, dims.1 as isize + 2, dims.2 as isize + 2);
364 let is_in = |x: isize, y: isize, z: isize| {
365 x >= 1 && y >= 1 && z >= 1 && x <= dims.0 as isize && y <= dims.1 as isize && z <= dims.2 as isize
366 && inside[idx((x - 1) as usize, (y - 1) as usize, (z - 1) as usize, dims)]
367 };
368 for z in 0..dims.2 {
369 for y in 0..dims.1 {
370 for x in 0..dims.0 {
371 let i = idx(x, y, z, dims);
372 let mut best = f64::INFINITY;
373 if inside[i] {
374 for qz in 0..pz {
375 for qy in 0..py {
376 for qx in 0..px {
377 if !is_in(qx, qy, qz) {
378 let dd = ((qx - x as isize - 1).pow(2) + (qy - y as isize - 1).pow(2)
379 + (qz - z as isize - 1).pow(2)) as f64;
380 best = best.min(dd);
381 }
382 }
383 }
384 }
385 assert!((d[i] - best.sqrt()).abs() < 1e-9, "voxel {:?}: {} vs {}", (x, y, z), d[i], best.sqrt());
386 } else {
387 assert_eq!(d[i], 0.0);
388 }
389 }
390 }
391 }
392 }
393
394 #[test]
395 fn gaussian_matches_direct_reflect_convolution() {
396 let dims = (7, 5, 4);
397 let data: Vec<f64> = (0..140).map(|i| ((i * 37) % 11) as f64 - 3.0).collect();
398 let sigma = 1.5;
400 let got = gaussian_filter(&data, dims, sigma);
401 let r = (4.0 * sigma + 0.5) as isize;
402 let w: Vec<f64> = (-r..=r).map(|t| (-0.5 * (t * t) as f64 / (sigma * sigma)).exp()).collect();
403 let s: f64 = w.iter().sum();
404 for z in 0..dims.2 {
405 for y in 0..dims.1 {
406 for x in 0..dims.0 {
407 let mut acc = 0.0;
408 for (a, wa) in w.iter().enumerate() {
409 for (b, wb) in w.iter().enumerate() {
410 for (c, wc) in w.iter().enumerate() {
411 let sx = reflect(x as isize + a as isize - r, dims.0);
412 let sy = reflect(y as isize + b as isize - r, dims.1);
413 let sz = reflect(z as isize + c as isize - r, dims.2);
414 acc += wa * wb * wc * data[idx(sx, sy, sz, dims)];
415 }
416 }
417 }
418 let want = acc / (s * s * s);
419 assert!((got[idx(x, y, z, dims)] - want).abs() < 1e-10);
420 }
421 }
422 }
423 }
424
425 #[test]
426 fn reflect_is_half_sample_symmetric() {
427 let n = 4;
428 let got: Vec<usize> = (-5..9).map(|i| reflect(i, n)).collect();
429 assert_eq!(got, vec![3, 3, 2, 1, 0, 0, 1, 2, 3, 3, 2, 1, 0, 0]);
430 }
431
432 #[test]
433 fn median_matches_numpy() {
434 assert_eq!(median(&mut [3.0, 1.0, 2.0]), 2.0);
435 assert_eq!(median(&mut [4.0, 1.0, 3.0, 2.0]), 2.5);
436 }
437
438 #[test]
442 fn peels_boundary_dropout_but_not_interior_dark_structures() {
443 let dims = (40, 40, 40);
444 let g = grid(dims);
445 let c = 19.5;
446 let n = dims.0 * dims.1 * dims.2;
447 let mut mask = vec![0u8; n];
448 let mut mag = vec![100.0; n];
450 for z in 0..dims.2 {
451 for y in 0..dims.1 {
452 for x in 0..dims.0 {
453 let (dx, dy, dz) = (x as f64 - c, y as f64 - c, z as f64 - c);
454 let r = (dx * dx + dy * dy + dz * dz).sqrt();
455 let i = idx(x, y, z, dims);
456 if r < 16.0 {
457 mask[i] = 1;
458 if dx < -6.0 && dy.abs() < 5.0 && dz.abs() < 5.0 {
459 mag[i] = 2.0; }
461 if r < 3.0 {
462 mag[i] = 2.0; }
464 }
465 }
466 }
467 }
468 let params = SignalErosionParams { global_erosions: 0, min_component: 10, ..Default::default() };
469 let out = signal_gated_erosion(&mask, &mag, &g, ¶ms);
470 let depth = distance_transform_padded(&mask.iter().map(|&m| m != 0).collect::<Vec<_>>(), dims);
471
472 assert_eq!(out[idx(20, 20, 20, dims)], 1);
474 assert_eq!(out[idx(35, 20, 20, dims)], 1);
476 assert_eq!(out[idx(5, 20, 20, dims)], 0);
478 for i in 0..n {
480 if mask[i] == 1 && out[i] == 0 {
481 assert!(depth[i] <= params.depth_cap as f64 + 1e-9, "removed voxel at depth {}", depth[i]);
482 }
483 }
484 assert_eq!(fill_holes(&out, &g, n), out);
486 }
487
488 #[test]
489 fn uniform_signal_only_applies_global_erosion() {
490 let dims = (20, 20, 20);
491 let g = grid(dims);
492 let mask: Vec<u8> = (0..8000)
493 .map(|i| {
494 let (x, y, z) = (i % 20, (i / 20) % 20, i / 400);
495 (3..17).contains(&x) && (3..17).contains(&y) && (3..17).contains(&z)
496 } as u8)
497 .collect();
498 let mag = vec![50.0; 8000];
499 let params = SignalErosionParams { min_component: 1, ..Default::default() };
500 let out = signal_gated_erosion(&mask, &mag, &g, ¶ms);
501 assert_eq!(out, erode_mask(&mask, &g, 1));
502 }
503}