use std::cmp::Ordering;
use std::collections::BinaryHeap;
use ndarray::{Array2, ArrayView2};
use super::annulus::extract_annuli;
use super::config::{Connectivity, GrowthConfig, LabelInput};
use super::result::{GrowError, GrowthResult, StopReason};
use super::stop::StopState;
#[derive(Debug, Clone, Copy)]
struct HeapItem {
priority: f64,
row: usize,
col: usize,
}
impl Ord for HeapItem {
fn cmp(&self, other: &Self) -> Ordering {
self.priority
.total_cmp(&other.priority)
.then(self.row.cmp(&other.row))
.then(self.col.cmp(&other.col))
}
}
impl PartialOrd for HeapItem {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for HeapItem {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for HeapItem {}
fn count_mask_neighbors(
row: usize,
col: usize,
mask: &Array2<bool>,
connectivity: Connectivity,
) -> usize {
let rows = mask.shape()[0];
let cols = mask.shape()[1];
let mut support = 0;
for &(d_row, d_col) in connectivity.offsets() {
let neighbor_row_signed = row as isize + d_row;
let neighbor_col_signed = col as isize + d_col;
if neighbor_row_signed < 0 || neighbor_col_signed < 0 {
continue;
}
let neighbor_row = neighbor_row_signed as usize;
let neighbor_col = neighbor_col_signed as usize;
if neighbor_row >= rows || neighbor_col >= cols {
continue;
}
if mask[(neighbor_row, neighbor_col)] {
support += 1;
}
}
support
}
fn count_cardinal_mask_neighbors(row: usize, col: usize, mask: &Array2<bool>) -> usize {
let rows = mask.shape()[0];
let cols = mask.shape()[1];
let mut support = 0;
for &(d_row, d_col) in Connectivity::Four.offsets() {
let neighbor_row = row as isize + d_row;
let neighbor_col = col as isize + d_col;
if neighbor_row < 0 || neighbor_col < 0 {
continue;
}
let neighbor_row = neighbor_row as usize;
let neighbor_col = neighbor_col as usize;
if neighbor_row >= rows || neighbor_col >= cols {
continue;
}
if mask[(neighbor_row, neighbor_col)] {
support += 1;
}
}
support
}
fn push_cardinal_unvisited(
row: usize,
col: usize,
mask: &Array2<bool>,
stack: &mut Vec<(usize, usize)>,
) {
let rows = mask.shape()[0];
let cols = mask.shape()[1];
for &(d_row, d_col) in Connectivity::Four.offsets() {
let next_row = row as isize + d_row;
let next_col = col as isize + d_col;
if next_row < 0 || next_col < 0 {
continue;
}
let next_row = next_row as usize;
let next_col = next_col as usize;
if next_row >= rows || next_col >= cols {
continue;
}
if !mask[(next_row, next_col)] {
stack.push((next_row, next_col));
}
}
}
fn push_unvisited_neighbors(
row: usize,
col: usize,
detection: ArrayView2<f64>,
mask: &Array2<bool>,
label: Option<&LabelInput>,
config: &GrowthConfig,
heap: &mut BinaryHeap<HeapItem>,
) {
let rows = mask.shape()[0];
let cols = mask.shape()[1];
let connectivity = config.connectivity;
let max_neighbors = connectivity.max_neighbors() as f64;
for &(d_row, d_col) in connectivity.offsets() {
let next_row_signed = row as isize + d_row;
let next_col_signed = col as isize + d_col;
if next_row_signed < 0 || next_col_signed < 0 {
continue;
}
let next_row = next_row_signed as usize;
let next_col = next_col_signed as usize;
if next_row >= rows || next_col >= cols {
continue;
}
if mask[(next_row, next_col)] {
continue;
}
if let Some(label) = label
&& !label.allowed.contains(&label.map[(next_row, next_col)])
{
continue;
}
let detection_value = detection[(next_row, next_col)];
if !detection_value.is_finite() {
continue;
}
let support = count_mask_neighbors(next_row, next_col, mask, connectivity);
let support_fraction = support as f64 / max_neighbors;
let priority = detection_value + config.shape_weight * support_fraction;
heap.push(HeapItem {
priority,
row: next_row,
col: next_col,
});
}
}
#[allow(clippy::too_many_arguments)]
fn fill_enclosed_cascade(
started_from: &[(usize, usize)],
detection: ArrayView2<f64>,
mask: &mut Array2<bool>,
label: Option<&LabelInput>,
config: &GrowthConfig,
heap: &mut BinaryHeap<HeapItem>,
n_iterations: &mut usize,
touches_edge: &mut bool,
) {
let Some(threshold) = config.fill_min_cardinal_support else {
return;
};
let rows = mask.shape()[0];
let cols = mask.shape()[1];
let mut stack: Vec<(usize, usize)> = Vec::new();
for &(row, col) in started_from {
push_cardinal_unvisited(row, col, mask, &mut stack);
}
while let Some((row, col)) = stack.pop() {
if mask[(row, col)] {
continue;
}
if let Some(label) = label
&& !label.allowed.contains(&label.map[(row, col)])
{
continue;
}
if count_cardinal_mask_neighbors(row, col, mask) < threshold {
continue;
}
mask[(row, col)] = true;
*n_iterations += 1;
if row == 0 || row + 1 == rows || col == 0 || col + 1 == cols {
*touches_edge = true;
}
push_unvisited_neighbors(row, col, detection, mask, label, config, heap);
push_cardinal_unvisited(row, col, mask, &mut stack);
}
}
pub fn grow_mask(
detection: ArrayView2<f64>,
data: ArrayView2<f64>,
err: Option<ArrayView2<f64>>,
label: Option<LabelInput>,
seed_pixels: &[(usize, usize)],
config: &GrowthConfig,
) -> Result<GrowthResult, GrowError> {
let rows = data.shape()[0];
let cols = data.shape()[1];
let shape = (rows, cols);
if config.check_interval == 0 {
return Err(GrowError::CheckIntervalZero);
}
if config.stop.snr.is_none() && config.stop.gradient.is_none() {
return Err(GrowError::NoStopCriterion);
}
let max_neighbors = config.connectivity.max_neighbors();
if config.min_neighbor_support > max_neighbors {
return Err(GrowError::MinNeighborSupportTooLarge {
min_neighbor_support: config.min_neighbor_support,
max_neighbors,
});
}
if let Some(gradient) = config.stop.gradient {
let (lo, hi) = (gradient.lo_percentile, gradient.hi_percentile);
if !(lo >= 0.0 && lo < hi && hi <= 100.0) {
return Err(GrowError::GradientPercentileInvalid { lo, hi });
}
}
if let Some(threshold) = config.fill_min_cardinal_support
&& !(3..=4).contains(&threshold)
{
return Err(GrowError::FillSupportInvalid { value: threshold });
}
match (err.as_ref(), config.stop.snr) {
(Some(_), None) => return Err(GrowError::ErrWithoutSnrStop),
(None, Some(_)) => return Err(GrowError::SnrStopWithoutErr),
_ => {}
}
let detection_shape = (detection.shape()[0], detection.shape()[1]);
if detection_shape != shape {
return Err(GrowError::DetectionShapeMismatch {
detection_shape,
data_shape: shape,
});
}
if let Some(err_view) = err.as_ref() {
let err_shape = (err_view.shape()[0], err_view.shape()[1]);
if err_shape != shape {
return Err(GrowError::ErrShapeMismatch {
err_shape,
data_shape: shape,
});
}
}
if let Some(label) = label.as_ref() {
let label_shape = (label.map.shape()[0], label.map.shape()[1]);
if label_shape != shape {
return Err(GrowError::LabelShapeMismatch {
label_shape,
data_shape: shape,
});
}
if label.allowed.is_empty() {
return Err(GrowError::LabelAllowedEmpty);
}
}
for &seed in seed_pixels {
if seed.0 >= rows || seed.1 >= cols {
return Err(GrowError::SeedOutOfBounds { seed, shape });
}
if let Some(label) = label.as_ref() {
let label_at_seed = label.map[(seed.0, seed.1)];
if !label.allowed.contains(&label_at_seed) {
return Err(GrowError::SeedOnDisallowedLabel {
seed,
label: label_at_seed,
});
}
}
}
let mut mask = Array2::<bool>::from_elem(shape, false);
let mut heap: BinaryHeap<HeapItem> = BinaryHeap::new();
let mut touches_edge = false;
let on_edge = |row: usize, col: usize| -> bool {
row == 0 || row + 1 == rows || col == 0 || col + 1 == cols
};
for &(row, col) in seed_pixels {
if mask[(row, col)] {
continue;
}
mask[(row, col)] = true;
if on_edge(row, col) {
touches_edge = true;
}
push_unvisited_neighbors(
row,
col,
detection,
&mask,
label.as_ref(),
config,
&mut heap,
);
}
let mut n_iterations: usize = 0;
let mut stop_state = StopState::new();
fill_enclosed_cascade(
seed_pixels,
detection,
&mut mask,
label.as_ref(),
config,
&mut heap,
&mut n_iterations,
&mut touches_edge,
);
loop {
if touches_edge {
return Ok(GrowthResult {
mask,
stop_reason: StopReason::Filled,
n_iterations,
});
}
let Some(item) = heap.pop() else {
return Ok(GrowthResult {
mask,
stop_reason: StopReason::Filled,
n_iterations,
});
};
let (row, col) = (item.row, item.col);
if mask[(row, col)] {
continue;
}
if config.min_neighbor_support > 1
&& n_iterations >= config.min_pixels_before_shape_gate
&& count_mask_neighbors(row, col, &mask, config.connectivity)
< config.min_neighbor_support
{
continue;
}
mask[(row, col)] = true;
n_iterations += 1;
if on_edge(row, col) {
touches_edge = true;
}
push_unvisited_neighbors(
row,
col,
detection,
&mask,
label.as_ref(),
config,
&mut heap,
);
fill_enclosed_cascade(
&[(row, col)],
detection,
&mut mask,
label.as_ref(),
config,
&mut heap,
&mut n_iterations,
&mut touches_edge,
);
if n_iterations >= config.min_pixels_before_stop_check
&& n_iterations.is_multiple_of(config.check_interval)
{
let annuli = extract_annuli(
mask.view(),
label.as_ref(),
config.connectivity,
config.annulus_thickness,
);
if let Some(reason) = stop_state.evaluate(&annuli, data, err, &config.stop) {
return Ok(GrowthResult {
mask,
stop_reason: reason,
n_iterations,
});
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::aperture::region_growing::config::{
Connectivity, GradientStop, SnrStop, StopCriterion,
};
use ndarray::Array2;
fn trivial_config() -> GrowthConfig {
GrowthConfig {
connectivity: Connectivity::Eight,
stop: StopCriterion {
snr: Some(SnrStop {
threshold: 0.5,
hysteresis: usize::MAX,
}),
gradient: None,
},
shape_weight: 0.0,
min_neighbor_support: 1,
min_pixels_before_shape_gate: 0,
fill_min_cardinal_support: None,
min_pixels_before_stop_check: 0,
check_interval: 1,
annulus_thickness: 1,
}
}
fn ones_err(shape: (usize, usize)) -> Array2<f64> {
Array2::<f64>::from_elem(shape, 1.0)
}
#[test]
fn flat_field_grows_until_edge_touch() {
let data = Array2::<f64>::from_elem((5, 5), 1.0);
let err = ones_err((5, 5));
let seeds = [(2, 2)];
let result = grow_mask(
data.view(),
data.view(),
Some(err.view()),
None,
&seeds,
&trivial_config(),
)
.expect("flat-field growth must succeed");
assert_eq!(result.stop_reason, StopReason::Filled);
assert!(result.n_iterations >= 1);
assert!(result.mask[(2, 2)], "seed must be preserved");
let true_count = result.mask.iter().filter(|&&v| v).count();
assert_eq!(true_count, 1 + result.n_iterations);
let touched_edge = (0..5).any(|i| {
result.mask[(0, i)] || result.mask[(4, i)] || result.mask[(i, 0)] || result.mask[(i, 4)]
});
assert!(touched_edge, "Filled requires the mask to have hit an edge");
}
#[test]
fn seed_out_of_bounds_errors() {
let data = Array2::<f64>::zeros((3, 3));
let err_array = ones_err((3, 3));
let seeds = [(3, 0)];
let err = grow_mask(
data.view(),
data.view(),
Some(err_array.view()),
None,
&seeds,
&trivial_config(),
)
.unwrap_err();
assert_eq!(
err,
GrowError::SeedOutOfBounds {
seed: (3, 0),
shape: (3, 3),
}
);
}
#[test]
fn label_gate_prevents_growth_into_disallowed_region() {
let rows = 7;
let cols = 7;
let mut data = Array2::<f64>::from_elem((rows, cols), 0.1);
let blob_a = (1, 2);
let blob_b = (5, 5);
for &(blob_row, blob_col) in &[blob_a, blob_b] {
for d_row in -1..=1_isize {
for d_col in -1..=1_isize {
let row = (blob_row as isize + d_row) as usize;
let col = (blob_col as isize + d_col) as usize;
data[(row, col)] = 10.0;
}
}
}
let mut label_map = Array2::<i32>::zeros((rows, cols));
for d_row in -1..=1_isize {
for d_col in -1..=1_isize {
label_map[(
(blob_a.0 as isize + d_row) as usize,
(blob_a.1 as isize + d_col) as usize,
)] = 1;
label_map[(
(blob_b.0 as isize + d_row) as usize,
(blob_b.1 as isize + d_col) as usize,
)] = 2;
}
}
let label = LabelInput {
map: label_map.view(),
allowed: vec![0, 1],
};
let err = ones_err((rows, cols));
let seeds = [blob_a];
let result = grow_mask(
data.view(),
data.view(),
Some(err.view()),
Some(label),
&seeds,
&trivial_config(),
)
.expect("label-gated growth must succeed");
for row in 0..rows {
for col in 0..cols {
if result.mask[(row, col)] {
assert_ne!(
label_map[(row, col)],
2,
"mask leaked into disallowed label at ({row}, {col})",
);
}
}
}
assert!(result.mask[blob_a]);
assert!(result.n_iterations >= 1);
}
#[test]
fn seed_on_disallowed_label_errors() {
let data = Array2::<f64>::zeros((3, 3));
let err_array = ones_err((3, 3));
let label_map = Array2::<i32>::zeros((3, 3));
let label = LabelInput {
map: label_map.view(),
allowed: vec![1],
};
let seeds = [(1, 1)];
let err = grow_mask(
data.view(),
data.view(),
Some(err_array.view()),
Some(label),
&seeds,
&trivial_config(),
)
.unwrap_err();
assert_eq!(
err,
GrowError::SeedOnDisallowedLabel {
seed: (1, 1),
label: 0,
}
);
}
#[test]
fn snr_stop_fires_on_gaussian_with_per_pixel_err() {
let n = 21;
let center = 10;
let sigma = 2.0_f64;
let amplitude = 100.0_f64;
let mut data = Array2::<f64>::zeros((n, n));
for row in 0..n {
for col in 0..n {
let d_row = row as f64 - center as f64;
let d_col = col as f64 - center as f64;
data[(row, col)] =
amplitude * (-(d_row * d_row + d_col * d_col) / (2.0 * sigma * sigma)).exp();
}
}
let err = ones_err((n, n));
let config = GrowthConfig {
connectivity: Connectivity::Eight,
stop: StopCriterion {
snr: Some(SnrStop {
threshold: 2.0,
hysteresis: 3,
}),
gradient: None,
},
shape_weight: 0.0,
min_neighbor_support: 1,
min_pixels_before_shape_gate: 0,
fill_min_cardinal_support: None,
min_pixels_before_stop_check: 5,
check_interval: 1,
annulus_thickness: 1,
};
let result = grow_mask(
data.view(),
data.view(),
Some(err.view()),
None,
&[(center, center)],
&config,
)
.expect("growth must succeed");
assert_eq!(result.stop_reason, StopReason::SnrBelow);
assert!(result.mask[(center, center)], "seed must be in mask");
let touched_edge = (0..n).any(|i| {
result.mask[(0, i)]
|| result.mask[(n - 1, i)]
|| result.mask[(i, 0)]
|| result.mask[(i, n - 1)]
});
assert!(
!touched_edge,
"SnrBelow must fire before the mask reaches the edge"
);
}
#[test]
fn gradient_stop_prevents_crossing_into_neighbour_blob() {
let rows = 31;
let cols = 31;
let sigma = 2.0_f64;
let amplitude = 100.0_f64;
let blob_a = (15, 11);
let blob_b = (15, 21);
let mut data = Array2::<f64>::zeros((rows, cols));
for &(blob_row, blob_col) in &[blob_a, blob_b] {
for row in 0..rows {
for col in 0..cols {
let d_row = row as f64 - blob_row as f64;
let d_col = col as f64 - blob_col as f64;
data[(row, col)] += amplitude
* (-(d_row * d_row + d_col * d_col) / (2.0 * sigma * sigma)).exp();
}
}
}
let config = GrowthConfig {
connectivity: Connectivity::Eight,
stop: StopCriterion {
snr: None,
gradient: Some(GradientStop {
ratio_threshold: 1.0,
hysteresis: 2,
lo_percentile: 75.0,
hi_percentile: 99.0,
}),
},
shape_weight: 0.0,
min_neighbor_support: 1,
min_pixels_before_shape_gate: 0,
fill_min_cardinal_support: None,
min_pixels_before_stop_check: 5,
check_interval: 1,
annulus_thickness: 2,
};
let result = grow_mask(data.view(), data.view(), None, None, &[blob_a], &config)
.expect("growth must succeed");
assert_eq!(result.stop_reason, StopReason::GradientFlip);
assert!(result.mask[blob_a], "seed (blob A centre) must be in mask");
assert!(
!result.mask[blob_b],
"blob B centre must NOT be reached — gradient must stop the crossing",
);
let touched_edge = (0..rows)
.any(|row| result.mask[(row, 0)] || result.mask[(row, cols - 1)])
|| (0..cols).any(|col| result.mask[(0, col)] || result.mask[(rows - 1, col)]);
assert!(
!touched_edge,
"GradientFlip must fire before the mask reaches the edge",
);
}
#[test]
fn detection_drives_growth_not_data() {
let n = 11;
let seed = (5, 5);
let mut detection = Array2::<f64>::from_elem((n, n), 1.0);
for col in seed.1..n {
detection[(seed.0, col)] = 100.0; }
let mut data = Array2::<f64>::from_elem((n, n), 1.0);
for row in seed.0..n {
data[(row, seed.1)] = 100.0; }
let err = ones_err((n, n));
let result = grow_mask(
detection.view(),
data.view(),
Some(err.view()),
None,
&[seed],
&trivial_config(),
)
.expect("growth must succeed");
assert!(
result.mask[(5, 10)],
"growth must follow the detection ridge to the right edge"
);
assert!(
!result.mask[(10, 5)],
"growth must not follow the data ridge — data drives stops, not the heap"
);
}
#[test]
fn shape_weight_prefers_compact_growth_over_bright_tendril() {
let n = 11;
let seed = (5, 5);
let off_ridge = (6, 6);
let mut data = Array2::<f64>::from_elem((n, n), 1.0);
for col in seed.1..n {
data[(seed.0, col)] = 100.0;
}
let err = ones_err((n, n));
let mut config = trivial_config();
config.shape_weight = 0.0;
let thin = grow_mask(
data.view(),
data.view(),
Some(err.view()),
None,
&[seed],
&config,
)
.expect("growth must succeed");
config.shape_weight = 1000.0;
let fat = grow_mask(
data.view(),
data.view(),
Some(err.view()),
None,
&[seed],
&config,
)
.expect("growth must succeed");
assert!(thin.mask[(5, 10)], "ridge growth must reach the right edge");
assert!(
!thin.mask[off_ridge],
"without shape_weight, growth must not leave the ridge"
);
assert!(
fat.mask[off_ridge],
"shape_weight must pull growth off the ridge into the flank"
);
assert!(
fat.n_iterations > thin.n_iterations,
"compact growth must admit more pixels before terminating \
({} vs {})",
fat.n_iterations,
thin.n_iterations,
);
}
#[test]
fn min_neighbor_support_floor_blocks_one_pixel_tendril() {
let n = 7;
let arm_root = (3, 5);
let arm_tip = (3, 6);
let mut image = Array2::<f64>::from_elem((n, n), f64::NAN);
for row in 2..=4 {
for col in 2..=4 {
image[(row, col)] = 10.0;
}
}
image[arm_root] = 9.0;
image[arm_tip] = 9.0;
let err = ones_err((n, n));
let mut config = trivial_config();
config.min_neighbor_support = 1;
let unguarded = grow_mask(
image.view(),
image.view(),
Some(err.view()),
None,
&[(3, 3)],
&config,
)
.expect("growth must succeed");
assert!(
unguarded.mask[arm_tip],
"with the floor disabled the tendril tip must be admitted"
);
config.min_neighbor_support = 2;
config.min_pixels_before_shape_gate = 8;
let guarded = grow_mask(
image.view(),
image.view(),
Some(err.view()),
None,
&[(3, 3)],
&config,
)
.expect("growth must succeed");
assert!(
guarded.mask[arm_root],
"the arm root has support 3 and must still be admitted"
);
assert!(
!guarded.mask[arm_tip],
"the min_neighbor_support floor must reject the one-pixel tendril tip"
);
}
#[test]
fn detection_shape_mismatch_errors() {
let data = Array2::<f64>::zeros((3, 3));
let detection = Array2::<f64>::zeros((2, 2));
let err_array = ones_err((3, 3));
let err = grow_mask(
detection.view(),
data.view(),
Some(err_array.view()),
None,
&[(1, 1)],
&trivial_config(),
)
.unwrap_err();
assert_eq!(
err,
GrowError::DetectionShapeMismatch {
detection_shape: (2, 2),
data_shape: (3, 3),
}
);
}
#[test]
fn fill_closes_enclosed_pixel_regardless_of_flux() {
let mut data = Array2::<f64>::from_elem((5, 5), f64::NAN);
for row in 1..=3 {
for col in 1..=3 {
data[(row, col)] = 10.0;
}
}
data[(2, 2)] = f64::NAN; let err = ones_err((5, 5));
let seeds = [(1, 1)];
let mut config = trivial_config();
config.fill_min_cardinal_support = None;
let unfilled = grow_mask(
data.view(),
data.view(),
Some(err.view()),
None,
&seeds,
&config,
)
.expect("growth must succeed");
assert!(
!unfilled.mask[(2, 2)],
"without the fill the dead centre must stay a hole"
);
config.fill_min_cardinal_support = Some(3);
let filled = grow_mask(
data.view(),
data.view(),
Some(err.view()),
None,
&seeds,
&config,
)
.expect("growth must succeed");
assert!(
filled.mask[(2, 2)],
"the cardinal fill must close the enclosed dead centre"
);
for &(row, col) in &[
(1, 1),
(1, 2),
(1, 3),
(2, 1),
(2, 3),
(3, 1),
(3, 2),
(3, 3),
] {
assert!(filled.mask[(row, col)] && unfilled.mask[(row, col)]);
}
}
#[test]
fn fill_support_out_of_range_errors() {
let data = Array2::<f64>::zeros((3, 3));
let err_array = ones_err((3, 3));
let mut config = trivial_config();
config.fill_min_cardinal_support = Some(2); let err = grow_mask(
data.view(),
data.view(),
Some(err_array.view()),
None,
&[(1, 1)],
&config,
)
.unwrap_err();
assert_eq!(err, GrowError::FillSupportInvalid { value: 2 });
}
#[test]
fn gradient_percentile_invalid_errors() {
let data = Array2::<f64>::zeros((3, 3));
let config = GrowthConfig {
connectivity: Connectivity::Eight,
stop: StopCriterion {
snr: None,
gradient: Some(GradientStop {
ratio_threshold: 1.0,
hysteresis: 2,
lo_percentile: 99.0,
hi_percentile: 75.0, }),
},
shape_weight: 0.0,
min_neighbor_support: 1,
min_pixels_before_shape_gate: 0,
fill_min_cardinal_support: None,
min_pixels_before_stop_check: 5,
check_interval: 1,
annulus_thickness: 2,
};
let err = grow_mask(data.view(), data.view(), None, None, &[(1, 1)], &config).unwrap_err();
assert_eq!(
err,
GrowError::GradientPercentileInvalid { lo: 99.0, hi: 75.0 }
);
}
#[test]
fn min_neighbor_support_too_large_errors() {
let data = Array2::<f64>::zeros((3, 3));
let err_array = ones_err((3, 3));
let mut config = trivial_config();
config.connectivity = Connectivity::Four; config.min_neighbor_support = 5;
let err = grow_mask(
data.view(),
data.view(),
Some(err_array.view()),
None,
&[(1, 1)],
&config,
)
.unwrap_err();
assert_eq!(
err,
GrowError::MinNeighborSupportTooLarge {
min_neighbor_support: 5,
max_neighbors: 4,
}
);
}
}