use super::{
accepted_peak_refine, check_pixel_len, elongation_from_cov, peak_sharpness, runs,
sort_and_truncate_by_mass, BackgroundGrid, CentroidExtractionResult,
};
use crate::centroid::Centroid;
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct FastCentroidConfig {
pub sigma_threshold: f32,
pub bg_grid: u32,
pub min_pixels: usize,
pub max_centroids: Option<usize>,
pub max_sharpness: Option<f32>,
pub saturation_level: Option<f32>,
pub max_pixels: usize,
pub max_elongation: Option<f32>,
pub border_margin: u32,
}
impl Default for FastCentroidConfig {
fn default() -> Self {
Self {
sigma_threshold: 5.0,
bg_grid: 64,
min_pixels: 2,
max_centroids: None,
max_sharpness: Some(0.9),
saturation_level: None,
max_pixels: 10000,
max_elongation: None,
border_margin: 0,
}
}
}
pub fn extract_centroids_fast(
pixels: &[f32],
width: u32,
height: u32,
config: &FastCentroidConfig,
) -> Result<CentroidExtractionResult> {
let w = width as usize;
let h = height as usize;
check_pixel_len(pixels.len(), width, height)?;
if !(config.sigma_threshold.is_finite() && config.sigma_threshold > 0.0) {
return Err(Error::InvalidInput(format!(
"sigma_threshold must be finite and positive, got {}",
config.sigma_threshold
)));
}
if config.bg_grid == 0 {
return Err(Error::InvalidInput("bg_grid must be >= 1".into()));
}
if w < 2 || h < 2 {
return Err(Error::InvalidInput("image must be at least 2x2".into()));
}
let block = config.bg_grid as usize;
let (bg, sigma) = BackgroundGrid::build(pixels, w, h, block, (block / 8).max(1));
let nx = w.div_ceil(block);
let k = config.sigma_threshold;
let mut grid_row = vec![0.0_f32; nx];
let mut grid_row_y = usize::MAX;
let regions = runs::sweep_runs(w, h, |r, c| {
if r != grid_row_y {
bg.blend_row(bg.row_params(r), &mut grid_row);
grid_row_y = r;
}
let p = pixels[r * w + c];
p.is_finite() && p > bg.lerp_in_row(&grid_row, c) + k * sigma
});
let (offsets, order) = regions.group_by_region();
let cx = (width - 1) as f32 / 2.0;
let cy = (height - 1) as f32 / 2.0;
let mut centroids: Vec<Centroid> = Vec::new();
let num_blobs_raw = regions.n_regions;
'region: for kreg in 0..regions.n_regions {
let region_runs = &order[offsets[kreg] as usize..offsets[kreg + 1] as usize];
let npix: usize = region_runs
.iter()
.map(|&i| regions.runs[i as usize].len())
.sum();
if npix < config.min_pixels || npix > config.max_pixels {
continue;
}
if config.border_margin > 0 {
let m = config.border_margin as usize;
let mut min_row = usize::MAX;
let mut max_row = 0usize;
let mut min_col = usize::MAX;
let mut max_col = 0usize;
for &i in region_runs {
let run = regions.runs[i as usize];
min_row = min_row.min(run.row as usize);
max_row = max_row.max(run.row as usize);
min_col = min_col.min(run.c0 as usize);
max_col = max_col.max(run.c1 as usize);
}
if min_row < m || min_col < m || max_row >= h - m || max_col >= w - m {
continue;
}
}
let mut sum_w = 0.0_f64;
let mut sum_wx = 0.0_f64;
let mut sum_wy = 0.0_f64;
let mut sum_wxx = 0.0_f64;
let mut sum_wyy = 0.0_f64;
let mut sum_wxy = 0.0_f64;
let mut peak_val = f32::NEG_INFINITY;
let mut peak_x = 0usize;
let mut peak_y = 0usize;
for &i in region_runs {
let run = regions.runs[i as usize];
let r = run.row as usize;
let rp = bg.row_params(r);
let row_off = r * w;
for c in run.c0 as usize..=run.c1 as usize {
let p = pixels[row_off + c];
let weight = (p - bg.value_at(c, rp)).max(0.0) as f64;
let (cf, rf) = (c as f64, r as f64);
sum_w += weight;
sum_wx += weight * cf;
sum_wy += weight * rf;
sum_wxx += weight * cf * cf;
sum_wyy += weight * rf * rf;
sum_wxy += weight * cf * rf;
if p > peak_val {
peak_val = p;
peak_x = c;
peak_y = r;
}
}
}
if sum_w <= 0.0 {
continue;
}
let mut fx = sum_wx / sum_w;
let mut fy = sum_wy / sum_w;
let cxx = sum_wxx / sum_w - fx * fx;
let cyy = sum_wyy / sum_w - fy * fy;
let cxy = sum_wxy / sum_w - fx * fy;
if let Some(max_elong) = config.max_elongation {
if elongation_from_cov(cxx, cyy, cxy) > max_elong {
continue 'region;
}
}
let (pc, pr) = (peak_x, peak_y);
let peak_bg = bg.value_at(pc, bg.row_params(pr)) as f64;
let v = |dy: isize, dx: isize| -> f64 {
let rr = (pr as isize + dy) as usize;
let cc = (pc as isize + dx) as usize;
pixels[rr * w + cc] as f64 - peak_bg
};
if let Some(max_sharp) = config.max_sharpness {
if let Some(s) = peak_sharpness((pc, pr), (w, h), v) {
if s > max_sharp as f64 {
continue;
}
}
}
let saturated = config.saturation_level.is_some_and(|s| peak_val >= s);
if !saturated {
if let Some((qx, qy)) = accepted_peak_refine(npix, (pc, pr), (w, h), (fx, fy), v) {
fx = qx;
fy = qy;
}
}
centroids.push(Centroid {
x: fx as f32 - cx,
y: fy as f32 - cy,
mass: Some(sum_w as f32),
cov: Some(crate::Matrix2::new([
[cxx as f32, cxy as f32],
[cxy as f32, cyy as f32],
])),
});
}
sort_and_truncate_by_mass(&mut centroids, config.max_centroids);
let bg_mean = bg.level();
Ok(CentroidExtractionResult {
centroids,
image_width: width,
image_height: height,
background_mean: bg_mean,
background_sigma: sigma,
threshold: bg_mean + k * sigma,
num_blobs_raw,
})
}