1use crate::utils::frangi::{FrangiParams, frangi_filter_3d};
14
15#[derive(Clone, Debug)]
17pub struct VasculatureParams {
18 pub sphere_radius: i32,
20 pub frangi_scale_range: [f64; 2],
22 pub frangi_scale_ratio: f64,
24 pub frangi_c: f64,
26}
27
28impl Default for VasculatureParams {
29 fn default() -> Self {
30 Self {
31 sphere_radius: 8,
32 frangi_scale_range: [0.5, 6.0],
34 frangi_scale_ratio: 0.5,
35 frangi_c: 500.0,
36 }
37 }
38}
39
40pub fn generate_vasculature_mask(
51 magnitude: &[f64],
52 mask: &[u8],
53 nx: usize, ny: usize, nz: usize,
54 params: &VasculatureParams,
55) -> Vec<f64> {
56 generate_vasculature_mask_with_progress(magnitude, mask, nx, ny, nz, params, |_, _| {})
57}
58
59pub fn generate_vasculature_mask_with_progress<F>(
61 magnitude: &[f64],
62 mask: &[u8],
63 nx: usize, ny: usize, nz: usize,
64 params: &VasculatureParams,
65 progress_callback: F,
66) -> Vec<f64>
67where
68 F: Fn(usize, usize),
69{
70 progress_callback(0, 4);
73
74 let bottom_hat = morphological_bottom_hat(magnitude, nx, ny, nz, params.sphere_radius);
75
76 progress_callback(1, 4);
78
79 let masked_bottom_hat: Vec<f64> = bottom_hat.iter()
80 .zip(mask.iter())
81 .map(|(&v, &m)| if m != 0 { v } else { 0.0 })
82 .collect();
83
84 progress_callback(2, 4);
86
87 let frangi_params = FrangiParams {
88 scale_range: params.frangi_scale_range,
89 scale_ratio: params.frangi_scale_ratio,
90 alpha: 0.5,
91 beta: 0.5,
92 c: params.frangi_c,
93 black_white: false, };
95
96 let frangi_result = frangi_filter_3d(&masked_bottom_hat, nx, ny, nz, &frangi_params);
97 let enhanced = frangi_result.vesselness;
98
99 progress_callback(3, 4);
106
107 let max_enhanced = enhanced.iter()
108 .zip(mask.iter())
109 .filter(|(_, &m)| m != 0)
110 .map(|(&v, _)| v)
111 .fold(0.0f64, f64::max);
112
113 let noise_floor = max_enhanced * 2e-3;
114
115 let vasc_only: Vec<f64> = enhanced.iter()
117 .zip(mask.iter())
118 .map(|(&v, &m)| {
119 if m == 0 { 1.0 } else if v > noise_floor { 0.0 } else { 1.0 } })
123 .collect();
124
125 progress_callback(4, 4);
126
127 vasc_only
128}
129
130struct SphereKernel {
132 linear_offsets: Vec<isize>,
134 coord_offsets: Vec<(i32, i32, i32)>,
136 radius: usize,
138}
139
140impl SphereKernel {
141 fn new(radius: i32, nx: usize, ny: usize) -> Self {
143 let mut linear_offsets = Vec::new();
144 let mut coord_offsets = Vec::new();
145 let r2 = (radius * radius) as f64;
146 let stride_y = nx as isize;
147 let stride_z = (nx * ny) as isize;
148
149 for dk in -radius..=radius {
150 for dj in -radius..=radius {
151 for di in -radius..=radius {
152 let dist2 = (di * di + dj * dj + dk * dk) as f64;
153 if dist2 <= r2 {
154 let linear = di as isize + dj as isize * stride_y + dk as isize * stride_z;
156 linear_offsets.push(linear);
157 coord_offsets.push((di, dj, dk));
158 }
159 }
160 }
161 }
162
163 Self {
164 linear_offsets,
165 coord_offsets,
166 radius: radius as usize,
167 }
168 }
169}
170
171fn morphological_bottom_hat(
177 data: &[f64],
178 nx: usize, ny: usize, nz: usize,
179 radius: i32,
180) -> Vec<f64> {
181 let kernel = SphereKernel::new(radius, nx, ny);
183
184 let dilated = dilate_grayscale_optimized(data, nx, ny, nz, &kernel);
186 let closed = erode_grayscale_optimized(&dilated, nx, ny, nz, &kernel);
187
188 closed.iter()
190 .zip(data.iter())
191 .map(|(&c, &d)| (c - d).max(0.0))
192 .collect()
193}
194
195fn dilate_grayscale_optimized(
198 data: &[f64],
199 nx: usize, ny: usize, nz: usize,
200 kernel: &SphereKernel,
201) -> Vec<f64> {
202 let n_total = nx * ny * nz;
203 let mut result = vec![0.0f64; n_total];
204 let r = kernel.radius;
205 let stride_z = nx * ny;
206
207 if nx > 2 * r && ny > 2 * r && nz > 2 * r {
209 for k in r..(nz - r) {
210 let k_offset = k * stride_z;
211 for j in r..(ny - r) {
212 let jk_offset = j * nx + k_offset;
213 for i in r..(nx - r) {
214 let idx = i + jk_offset;
215 let mut max_val = f64::NEG_INFINITY;
216
217 for &offset in &kernel.linear_offsets {
219 let neighbor_idx = (idx as isize + offset) as usize;
220 let val = unsafe { *data.get_unchecked(neighbor_idx) };
222 if val > max_val {
223 max_val = val;
224 }
225 }
226
227 unsafe { *result.get_unchecked_mut(idx) = max_val };
228 }
229 }
230 }
231 }
232
233 let nx_i32 = nx as i32;
235 let ny_i32 = ny as i32;
236 let nz_i32 = nz as i32;
237
238 for k in 0..nz {
239 let k_offset = k * stride_z;
240 let k_is_boundary = k < r || k >= nz - r;
241
242 for j in 0..ny {
243 let jk_offset = j * nx + k_offset;
244 let j_is_boundary = j < r || j >= ny - r;
245
246 for i in 0..nx {
247 let i_is_boundary = i < r || i >= nx - r;
248
249 if !k_is_boundary && !j_is_boundary && !i_is_boundary {
251 continue;
252 }
253
254 let idx = i + jk_offset;
255 let mut max_val = f64::NEG_INFINITY;
256
257 for &(di, dj, dk) in &kernel.coord_offsets {
259 let ni = i as i32 + di;
260 let nj = j as i32 + dj;
261 let nk = k as i32 + dk;
262
263 if ni >= 0 && ni < nx_i32 &&
264 nj >= 0 && nj < ny_i32 &&
265 nk >= 0 && nk < nz_i32 {
266 let neighbor_idx = ni as usize + nj as usize * nx + nk as usize * stride_z;
267 max_val = max_val.max(data[neighbor_idx]);
268 }
269 }
270
271 result[idx] = if max_val.is_finite() { max_val } else { data[idx] };
272 }
273 }
274 }
275
276 result
277}
278
279fn erode_grayscale_optimized(
281 data: &[f64],
282 nx: usize, ny: usize, nz: usize,
283 kernel: &SphereKernel,
284) -> Vec<f64> {
285 let n_total = nx * ny * nz;
286 let mut result = vec![0.0f64; n_total];
287 let r = kernel.radius;
288 let stride_z = nx * ny;
289
290 if nx > 2 * r && ny > 2 * r && nz > 2 * r {
292 for k in r..(nz - r) {
293 let k_offset = k * stride_z;
294 for j in r..(ny - r) {
295 let jk_offset = j * nx + k_offset;
296 for i in r..(nx - r) {
297 let idx = i + jk_offset;
298 let mut min_val = f64::INFINITY;
299
300 for &offset in &kernel.linear_offsets {
302 let neighbor_idx = (idx as isize + offset) as usize;
303 let val = unsafe { *data.get_unchecked(neighbor_idx) };
304 if val < min_val {
305 min_val = val;
306 }
307 }
308
309 unsafe { *result.get_unchecked_mut(idx) = min_val };
310 }
311 }
312 }
313 }
314
315 let nx_i32 = nx as i32;
317 let ny_i32 = ny as i32;
318 let nz_i32 = nz as i32;
319
320 for k in 0..nz {
321 let k_offset = k * stride_z;
322 let k_is_boundary = k < r || k >= nz - r;
323
324 for j in 0..ny {
325 let jk_offset = j * nx + k_offset;
326 let j_is_boundary = j < r || j >= ny - r;
327
328 for i in 0..nx {
329 let i_is_boundary = i < r || i >= nx - r;
330
331 if !k_is_boundary && !j_is_boundary && !i_is_boundary {
333 continue;
334 }
335
336 let idx = i + jk_offset;
337 let mut min_val = f64::INFINITY;
338
339 for &(di, dj, dk) in &kernel.coord_offsets {
340 let ni = i as i32 + di;
341 let nj = j as i32 + dj;
342 let nk = k as i32 + dk;
343
344 if ni >= 0 && ni < nx_i32 &&
345 nj >= 0 && nj < ny_i32 &&
346 nk >= 0 && nk < nz_i32 {
347 let neighbor_idx = ni as usize + nj as usize * nx + nk as usize * stride_z;
348 min_val = min_val.min(data[neighbor_idx]);
349 }
350 }
351
352 result[idx] = if min_val.is_finite() { min_val } else { data[idx] };
353 }
354 }
355 }
356
357 result
358}
359
360
361pub fn generate_vasculature_mask_default(
363 magnitude: &[f64],
364 mask: &[u8],
365 nx: usize, ny: usize, nz: usize,
366) -> Vec<f64> {
367 generate_vasculature_mask(magnitude, mask, nx, ny, nz, &VasculatureParams::default())
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 #[test]
375 fn test_bottom_hat_basic() {
376 let data = vec![1.0f64; 27];
378 let result = morphological_bottom_hat(&data, 3, 3, 3, 1);
379
380 for &v in &result {
381 assert!(v.abs() < 0.01, "Bottom-hat of constant should be ~0");
382 }
383 }
384
385 #[test]
386 fn test_vasculature_mask_basic() {
387 let magnitude = vec![1.0f64; 1000];
389 let mask = vec![1u8; 1000];
390
391 let params = VasculatureParams {
392 sphere_radius: 2,
393 frangi_scale_range: [1.0, 2.0],
394 frangi_scale_ratio: 1.0,
395 frangi_c: 100.0,
396 };
397
398 let result = generate_vasculature_mask(&magnitude, &mask, 10, 10, 10, ¶ms);
399 assert_eq!(result.len(), 1000);
400 }
401}