#![allow(clippy::cast_possible_wrap)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::similar_names)]
#![allow(unsafe_code)]
use crate::Detection;
use crate::batch::{CandidateState, DetectionBatch, MAX_CANDIDATES, Point2f};
use crate::config::DetectorConfig;
use crate::edge_refinement::{ErfEdgeFitter, RefineConfig, SampleConfig};
use crate::image::ImageView;
use crate::segmentation::LabelResult;
use bumpalo::Bump;
use bumpalo::collections::Vec as BumpVec;
use multiversion::multiversion;
use crate::workspace::WORKSPACE_ARENA;
pub(crate) type CornerCovariances = [[f32; 4]; 4];
pub use crate::Point;
#[allow(dead_code)]
pub(crate) fn extract_quads_fast(
arena: &Bump,
img: &ImageView,
label_result: &LabelResult,
) -> Vec<Detection> {
extract_quads_with_config(arena, img, label_result, &DetectorConfig::default(), 1, img)
}
#[allow(clippy::too_many_lines)]
#[tracing::instrument(skip_all, name = "pipeline::quad_extraction")]
pub fn extract_quads_soa(
batch: &mut DetectionBatch,
img: &ImageView,
label_result: &LabelResult,
config: &DetectorConfig,
decimation: usize,
refinement_img: &ImageView,
debug_telemetry: bool,
) -> (usize, Option<Vec<[Point; 4]>>) {
use rayon::prelude::*;
let stats = &label_result.component_stats;
let detections: Vec<([Point; 4], [Point; 4], CornerCovariances)> = stats
.par_iter()
.enumerate()
.filter_map(|(label_idx, stat)| {
WORKSPACE_ARENA.with(|cell| {
let mut arena = cell.borrow_mut();
arena.reset();
let label = (label_idx + 1) as u32;
extract_single_quad(
&arena,
img,
label_result.labels,
label,
stat,
config,
decimation,
refinement_img,
)
})
})
.collect();
let n = detections.len().min(MAX_CANDIDATES);
let mut unrefined = if debug_telemetry {
Some(Vec::with_capacity(n))
} else {
None
};
for (i, (corners, unrefined_pts, covs)) in detections.into_iter().take(n).enumerate() {
for (j, corner) in corners.iter().enumerate() {
batch.corners[i][j] = Point2f {
x: corner.x as f32,
y: corner.y as f32,
};
}
if covs.is_empty() {
batch.corner_covariances[i].fill(0.0);
} else {
for (chunk, cov) in batch.corner_covariances[i]
.chunks_exact_mut(4)
.zip(covs.iter())
{
chunk.copy_from_slice(cov);
}
}
if let Some(ref mut u) = unrefined {
u.push(unrefined_pts);
}
batch.status_mask[i] = CandidateState::Active;
}
for i in n..MAX_CANDIDATES {
batch.status_mask[i] = CandidateState::Empty;
}
(n, unrefined)
}
#[inline]
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
fn extract_single_quad(
arena: &Bump,
img: &ImageView,
labels: &[u32],
label: u32,
stat: &crate::segmentation::ComponentStats,
config: &DetectorConfig,
decimation: usize,
refinement_img: &ImageView,
) -> Option<([Point; 4], [Point; 4], CornerCovariances)> {
let min_edge_len_sq = config.quad_min_edge_length * config.quad_min_edge_length;
let d = decimation as f64;
let bbox_w = u32::from(stat.max_x - stat.min_x) + 1;
let bbox_h = u32::from(stat.max_y - stat.min_y) + 1;
let bbox_area = bbox_w * bbox_h;
if bbox_area < config.quad_min_area || bbox_area > (img.width * img.height * 9 / 10) as u32 {
return None;
}
let aspect = bbox_w.max(bbox_h) as f32 / bbox_w.min(bbox_h).max(1) as f32;
if aspect > config.quad_max_aspect_ratio {
return None;
}
let fill = stat.pixel_count as f32 / bbox_area as f32;
if fill < config.quad_min_fill_ratio || fill > config.quad_max_fill_ratio {
return None;
}
if (config.quad_max_elongation > 0.0 || config.quad_min_density > 0.0)
&& let Some((elongation, density)) = crate::segmentation::compute_moment_shape(stat)
{
if config.quad_max_elongation > 0.0 && elongation > config.quad_max_elongation {
return None;
}
if config.quad_min_density > 0.0 && density < config.quad_min_density {
return None;
}
}
let (quad_pts_dec, gn_covs): ([Point; 4], CornerCovariances) = match config.quad_extraction_mode
{
crate::config::QuadExtractionMode::EdLines => {
let ed_cfg = crate::edlines::EdLinesConfig::from_detector_config(config);
crate::edlines::extract_quad_edlines(
arena,
img,
refinement_img,
labels,
label,
stat,
&ed_cfg,
)?
},
crate::config::QuadExtractionMode::ContourRdp => {
let sx = stat.first_pixel_x as usize;
let sy = stat.first_pixel_y as usize;
let contour = trace_boundary(arena, labels, img.width, img.height, sx, sy, label);
if contour.len() < 12 {
return None;
}
let simple_contour = chain_approximation(arena, &contour);
let perimeter = contour.len() as f64;
let epsilon = (perimeter * 0.02).max(1.0);
let simplified = douglas_peucker(arena, &simple_contour, epsilon);
if simplified.len() < 4 || simplified.len() > 11 {
return None;
}
let simpl_len = simplified.len();
let reduced = if simpl_len == 5 {
simplified
} else if simpl_len == 4 {
let mut closed = BumpVec::new_in(arena);
for p in &simplified {
closed.push(*p);
}
closed.push(simplified[0]);
closed
} else {
reduce_to_quad(arena, &simplified)
};
if reduced.len() != 5 {
return None;
}
let area = polygon_area(&reduced);
let compactness = (12.566 * area.abs()) / (perimeter * perimeter);
if area.abs() <= f64::from(config.quad_min_area) || compactness <= 0.1 {
return None;
}
if area > 0.0 {
(
[reduced[0], reduced[1], reduced[2], reduced[3]],
[[0.0; 4]; 4],
)
} else {
(
[reduced[0], reduced[3], reduced[2], reduced[1]],
[[0.0; 4]; 4],
)
}
},
};
let quad_pts = [
Point {
x: quad_pts_dec[0].x * d,
y: quad_pts_dec[0].y * d,
},
Point {
x: quad_pts_dec[1].x * d,
y: quad_pts_dec[1].y * d,
},
Point {
x: quad_pts_dec[2].x * d,
y: quad_pts_dec[2].y * d,
},
Point {
x: quad_pts_dec[3].x * d,
y: quad_pts_dec[3].y * d,
},
];
let quad_pts = if config.quad_extraction_mode == crate::config::QuadExtractionMode::EdLines {
quad_pts } else {
let center_x = (quad_pts[0].x + quad_pts[1].x + quad_pts[2].x + quad_pts[3].x) * 0.25;
let center_y = (quad_pts[0].y + quad_pts[1].y + quad_pts[2].y + quad_pts[3].y) * 0.25;
let mut ep = quad_pts;
for i in 0..4 {
ep[i].x += 0.5 * (quad_pts[i].x - center_x).signum();
ep[i].y += 0.5 * (quad_pts[i].y - center_y).signum();
}
ep
};
let mut ok = true;
for i in 0..4 {
let d2 = (quad_pts[i].x - quad_pts[(i + 1) % 4].x).powi(2)
+ (quad_pts[i].y - quad_pts[(i + 1) % 4].y).powi(2);
if d2 < min_edge_len_sq {
ok = false;
break;
}
}
if ok {
let (corners, out_covs) =
if config.refinement_mode == crate::config::CornerRefinementMode::None {
(quad_pts, gn_covs)
} else {
let use_erf = config.refinement_mode == crate::config::CornerRefinementMode::Erf;
(
[
refine_corner(
arena,
refinement_img,
quad_pts[0],
quad_pts[3],
quad_pts[1],
config.subpixel_refinement_sigma,
decimation,
use_erf,
),
refine_corner(
arena,
refinement_img,
quad_pts[1],
quad_pts[0],
quad_pts[2],
config.subpixel_refinement_sigma,
decimation,
use_erf,
),
refine_corner(
arena,
refinement_img,
quad_pts[2],
quad_pts[1],
quad_pts[3],
config.subpixel_refinement_sigma,
decimation,
use_erf,
),
refine_corner(
arena,
refinement_img,
quad_pts[3],
quad_pts[2],
quad_pts[0],
config.subpixel_refinement_sigma,
decimation,
use_erf,
),
],
[[0.0; 4]; 4],
)
};
let edge_score = calculate_edge_score(refinement_img, corners);
if edge_score > config.quad_min_edge_score {
return Some((corners, quad_pts, out_covs));
}
}
None
}
#[cfg(feature = "non_rectified")]
const MAX_UNDISTORT_RESIDUAL: f64 = 2e-4;
#[cfg(feature = "non_rectified")]
#[derive(Clone, Copy, Debug)]
struct ScaledIntrinsics {
fx: f64,
fy: f64,
cx: f64,
cy: f64,
}
#[cfg(feature = "non_rectified")]
impl ScaledIntrinsics {
#[inline]
fn from_intrinsics(intrinsics: &crate::pose::CameraIntrinsics, decimation: usize) -> Self {
let d = decimation as f64;
Self {
fx: intrinsics.fx / d,
fy: intrinsics.fy / d,
cx: (intrinsics.cx + 0.5) / d - 0.5,
cy: (intrinsics.cy + 0.5) / d - 0.5,
}
}
}
#[cfg(feature = "non_rectified")]
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(skip_all, name = "pipeline::quad_extraction_camera")]
pub fn extract_quads_soa_with_camera<C: crate::camera::CameraModel>(
batch: &mut DetectionBatch,
img: &ImageView,
label_result: &LabelResult,
config: &DetectorConfig,
decimation: usize,
refinement_img: &ImageView,
debug_telemetry: bool,
camera: &C,
intrinsics: &crate::pose::CameraIntrinsics,
) -> (usize, Option<Vec<[Point; 4]>>) {
use rayon::prelude::*;
let stats = &label_result.component_stats;
let scaled = ScaledIntrinsics::from_intrinsics(intrinsics, decimation);
let detections: Vec<([Point; 4], [Point; 4], CornerCovariances)> = stats
.par_iter()
.enumerate()
.filter_map(|(label_idx, stat)| {
WORKSPACE_ARENA.with(|cell| {
let mut arena = cell.borrow_mut();
arena.reset();
let label = (label_idx + 1) as u32;
extract_single_quad_with_camera(
&arena,
img,
label_result.labels,
label,
stat,
config,
decimation,
refinement_img,
camera,
scaled,
intrinsics,
)
})
})
.collect();
let n = detections.len().min(MAX_CANDIDATES);
let mut unrefined = if debug_telemetry {
Some(Vec::with_capacity(n))
} else {
None
};
for (i, (corners, unrefined_pts, covs)) in detections.into_iter().take(n).enumerate() {
for (j, corner) in corners.iter().enumerate() {
batch.corners[i][j] = Point2f {
x: corner.x as f32,
y: corner.y as f32,
};
}
if covs.is_empty() {
batch.corner_covariances[i].fill(0.0);
} else {
for (chunk, cov) in batch.corner_covariances[i]
.chunks_exact_mut(4)
.zip(covs.iter())
{
chunk.copy_from_slice(cov);
}
}
if let Some(ref mut u) = unrefined {
u.push(unrefined_pts);
}
batch.status_mask[i] = CandidateState::Active;
}
for i in n..MAX_CANDIDATES {
batch.status_mask[i] = CandidateState::Empty;
}
(n, unrefined)
}
#[cfg(feature = "non_rectified")]
#[inline]
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
fn extract_single_quad_with_camera<C: crate::camera::CameraModel>(
arena: &Bump,
img: &ImageView,
labels: &[u32],
label: u32,
stat: &crate::segmentation::ComponentStats,
config: &DetectorConfig,
decimation: usize,
refinement_img: &ImageView,
camera: &C,
scaled: ScaledIntrinsics,
intrinsics: &crate::pose::CameraIntrinsics,
) -> Option<([Point; 4], [Point; 4], CornerCovariances)> {
if config.quad_extraction_mode != crate::config::QuadExtractionMode::ContourRdp {
return None;
}
let min_edge_len_sq = config.quad_min_edge_length * config.quad_min_edge_length;
let d = decimation as f64;
let bbox_w = u32::from(stat.max_x - stat.min_x) + 1;
let bbox_h = u32::from(stat.max_y - stat.min_y) + 1;
let bbox_area = bbox_w * bbox_h;
if bbox_area < config.quad_min_area || bbox_area > (img.width * img.height * 9 / 10) as u32 {
return None;
}
let aspect = bbox_w.max(bbox_h) as f32 / bbox_w.min(bbox_h).max(1) as f32;
if aspect > config.quad_max_aspect_ratio {
return None;
}
let fill = stat.pixel_count as f32 / bbox_area as f32;
if fill < config.quad_min_fill_ratio || fill > config.quad_max_fill_ratio {
return None;
}
if (config.quad_max_elongation > 0.0 || config.quad_min_density > 0.0)
&& let Some((elongation, density)) = crate::segmentation::compute_moment_shape(stat)
{
if config.quad_max_elongation > 0.0 && elongation > config.quad_max_elongation {
return None;
}
if config.quad_min_density > 0.0 && density < config.quad_min_density {
return None;
}
}
let sx = stat.first_pixel_x as usize;
let sy = stat.first_pixel_y as usize;
let contour = trace_boundary(arena, labels, img.width, img.height, sx, sy, label);
if contour.len() < 12 {
return None;
}
let rectified = if C::IS_RECTIFIED {
contour
} else {
let mut rect = BumpVec::with_capacity_in(contour.len(), arena);
for p in &contour {
let xd = (p.x - scaled.cx) / scaled.fx;
let yd = (p.y - scaled.cy) / scaled.fy;
let [xn, yn] = camera.undistort(xd, yd);
let [xd_chk, yd_chk] = camera.distort(xn, yn);
let dx = xd_chk - xd;
let dy = yd_chk - yd;
if (dx * dx + dy * dy).sqrt() > MAX_UNDISTORT_RESIDUAL {
return None;
}
rect.push(Point {
x: xn * scaled.fx + scaled.cx,
y: yn * scaled.fy + scaled.cy,
});
}
rect
};
let simple_contour = chain_approximation(arena, &rectified);
let perimeter = rectified.len() as f64;
let epsilon = (perimeter * 0.02).max(1.0);
let simplified = douglas_peucker(arena, &simple_contour, epsilon);
if simplified.len() < 4 || simplified.len() > 11 {
return None;
}
let simpl_len = simplified.len();
let reduced = if simpl_len == 5 {
simplified
} else if simpl_len == 4 {
let mut closed = BumpVec::new_in(arena);
for p in &simplified {
closed.push(*p);
}
closed.push(simplified[0]);
closed
} else {
reduce_to_quad(arena, &simplified)
};
if reduced.len() != 5 {
return None;
}
let area = polygon_area(&reduced);
let compactness = (12.566 * area.abs()) / (perimeter * perimeter);
if area.abs() <= f64::from(config.quad_min_area) || compactness <= 0.1 {
return None;
}
let quad_rect: [Point; 4] = if area > 0.0 {
[reduced[0], reduced[1], reduced[2], reduced[3]]
} else {
[reduced[0], reduced[3], reduced[2], reduced[1]]
};
let quad_pts_dec: [Point; 4] = if C::IS_RECTIFIED {
quad_rect
} else {
let mut out = quad_rect;
for p in &mut out {
let xn = (p.x - scaled.cx) / scaled.fx;
let yn = (p.y - scaled.cy) / scaled.fy;
let [xd, yd] = camera.distort(xn, yn);
p.x = xd * scaled.fx + scaled.cx;
p.y = yd * scaled.fy + scaled.cy;
}
out
};
let quad_pts = quad_pts_dec.map(|p| Point {
x: p.x * d,
y: p.y * d,
});
let quad_pts = if C::IS_RECTIFIED {
let center_x = (quad_pts[0].x + quad_pts[1].x + quad_pts[2].x + quad_pts[3].x) * 0.25;
let center_y = (quad_pts[0].y + quad_pts[1].y + quad_pts[2].y + quad_pts[3].y) * 0.25;
let mut ep = quad_pts;
for i in 0..4 {
ep[i].x += 0.5 * (quad_pts[i].x - center_x).signum();
ep[i].y += 0.5 * (quad_pts[i].y - center_y).signum();
}
ep
} else {
quad_pts
};
for i in 0..4 {
let d2 = (quad_pts[i].x - quad_pts[(i + 1) % 4].x).powi(2)
+ (quad_pts[i].y - quad_pts[(i + 1) % 4].y).powi(2);
if d2 < min_edge_len_sq {
return None;
}
}
let quad_rect_full = quad_rect.map(|p| Point {
x: p.x * d,
y: p.y * d,
});
let (corners, out_covs) = if config.refinement_mode == crate::config::CornerRefinementMode::None
{
(quad_pts, [[0.0_f32; 4]; 4])
} else if C::IS_RECTIFIED {
let use_erf = config.refinement_mode == crate::config::CornerRefinementMode::Erf;
(
[
refine_corner(
arena,
refinement_img,
quad_pts[0],
quad_pts[3],
quad_pts[1],
config.subpixel_refinement_sigma,
decimation,
use_erf,
),
refine_corner(
arena,
refinement_img,
quad_pts[1],
quad_pts[0],
quad_pts[2],
config.subpixel_refinement_sigma,
decimation,
use_erf,
),
refine_corner(
arena,
refinement_img,
quad_pts[2],
quad_pts[1],
quad_pts[3],
config.subpixel_refinement_sigma,
decimation,
use_erf,
),
refine_corner(
arena,
refinement_img,
quad_pts[3],
quad_pts[2],
quad_pts[0],
config.subpixel_refinement_sigma,
decimation,
use_erf,
),
],
[[0.0_f32; 4]; 4],
)
} else {
(
[
refine_corner_with_camera(
refinement_img,
quad_rect_full[0],
quad_rect_full[3],
quad_rect_full[1],
quad_pts[0],
decimation,
intrinsics,
camera,
),
refine_corner_with_camera(
refinement_img,
quad_rect_full[1],
quad_rect_full[0],
quad_rect_full[2],
quad_pts[1],
decimation,
intrinsics,
camera,
),
refine_corner_with_camera(
refinement_img,
quad_rect_full[2],
quad_rect_full[1],
quad_rect_full[3],
quad_pts[2],
decimation,
intrinsics,
camera,
),
refine_corner_with_camera(
refinement_img,
quad_rect_full[3],
quad_rect_full[2],
quad_rect_full[0],
quad_pts[3],
decimation,
intrinsics,
camera,
),
],
[[0.0_f32; 4]; 4],
)
};
let edge_score = if C::IS_RECTIFIED {
calculate_edge_score(refinement_img, corners)
} else {
calculate_edge_score_curved(refinement_img, &quad_rect, camera, scaled, decimation)
};
if edge_score <= config.quad_min_edge_score {
return None;
}
Some((corners, quad_pts, out_covs))
}
#[allow(clippy::too_many_lines)]
pub fn extract_quads_with_config(
_arena: &Bump,
img: &ImageView,
label_result: &LabelResult,
config: &DetectorConfig,
decimation: usize,
refinement_img: &ImageView,
) -> Vec<Detection> {
use rayon::prelude::*;
let stats = &label_result.component_stats;
let d = decimation as f64;
stats
.par_iter()
.enumerate()
.filter_map(|(label_idx, stat)| {
WORKSPACE_ARENA.with(|cell| {
let mut arena = cell.borrow_mut();
arena.reset();
let label = (label_idx + 1) as u32;
let quad_result = extract_single_quad(
&arena,
img,
label_result.labels,
label,
stat,
config,
decimation,
refinement_img,
);
let (corners, _unrefined, _covs) = quad_result?;
let area = polygon_area(&corners);
Some(Detection {
id: label,
center: [
(corners[0].x + corners[1].x + corners[2].x + corners[3].x) / 4.0,
(corners[0].y + corners[1].y + corners[2].y + corners[3].y) / 4.0,
],
corners: [
[corners[0].x, corners[0].y],
[corners[1].x, corners[1].y],
[corners[2].x, corners[2].y],
[corners[3].x, corners[3].y],
],
hamming: 0,
rotation: 0,
decision_margin: area * d * d, bits: 0,
pose: None,
pose_covariance: None,
})
})
})
.collect()
}
#[allow(dead_code)]
pub(crate) fn extract_quads(arena: &Bump, img: &ImageView, labels: &[u32]) -> Vec<Detection> {
let mut detections = Vec::new();
let num_labels = (labels.len() / 32) + 1;
let processed_labels = arena.alloc_slice_fill_copy(num_labels, 0u32);
let width = img.width;
let height = img.height;
for y in 1..height - 1 {
let row_off = y * width;
let prev_row_off = (y - 1) * width;
for x in 1..width - 1 {
let idx = row_off + x;
let label = labels[idx];
if label == 0 {
continue;
}
if labels[idx - 1] == label || labels[prev_row_off + x] == label {
continue;
}
let bit_idx = (label as usize) / 32;
let bit_mask = 1 << (label % 32);
if processed_labels[bit_idx] & bit_mask != 0 {
continue;
}
processed_labels[bit_idx] |= bit_mask;
let contour = trace_boundary(arena, labels, width, height, x, y, label);
if contour.len() >= 12 {
let simplified = douglas_peucker(arena, &contour, 4.0);
if simplified.len() == 5 {
let area = polygon_area(&simplified);
let perimeter = contour.len() as f64;
let compactness = (12.566 * area) / (perimeter * perimeter);
if area > 400.0 && compactness > 0.5 {
let mut ok = true;
for i in 0..4 {
let d2 = (simplified[i].x - simplified[i + 1].x).powi(2)
+ (simplified[i].y - simplified[i + 1].y).powi(2);
if d2 < 100.0 {
ok = false;
break;
}
}
if ok {
detections.push(Detection {
id: label,
center: polygon_center(&simplified),
corners: [
[simplified[0].x, simplified[0].y],
[simplified[1].x, simplified[1].y],
[simplified[2].x, simplified[2].y],
[simplified[3].x, simplified[3].y],
],
hamming: 0,
rotation: 0,
decision_margin: area,
bits: 0,
pose: None,
pose_covariance: None,
});
}
}
}
}
}
}
detections
}
#[multiversion(targets(
"x86_64+avx2+bmi1+bmi2+popcnt+lzcnt",
"x86_64+avx512f+avx512bw+avx512dq+avx512vl",
"aarch64+neon"
))]
fn find_max_distance_optimized(points: &[Point], start: usize, end: usize) -> (f64, usize) {
let a = points[start];
let b = points[end];
let dx = b.x - a.x;
let dy = b.y - a.y;
let mag_sq = dx * dx + dy * dy;
if mag_sq < 1e-18 {
let mut dmax = 0.0;
let mut index = start;
for (i, p) in points.iter().enumerate().take(end).skip(start + 1) {
let d = ((p.x - a.x).powi(2) + (p.y - a.y).powi(2)).sqrt();
if d > dmax {
dmax = d;
index = i;
}
}
return (dmax, index);
}
let mut dmax = 0.0;
let mut index = start;
let mut i = start + 1;
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
if let Some(_dispatch) = multiversion::target::x86_64::avx2::get() {
unsafe {
use std::arch::x86_64::*;
let v_dx = _mm256_set1_pd(dx);
let v_dy = _mm256_set1_pd(dy);
let v_ax = _mm256_set1_pd(a.x);
let v_ay = _mm256_set1_pd(a.y);
let v_bx = _mm256_set1_pd(b.x);
let v_by = _mm256_set1_pd(b.y);
let mut v_dmax = _mm256_setzero_pd();
let mut v_indices = _mm256_setzero_pd();
while i + 4 <= end {
let p_ptr = points.as_ptr().add(i) as *const f64;
let raw0 = _mm256_loadu_pd(p_ptr);
let raw1 = _mm256_loadu_pd(p_ptr.add(4));
let x01y01 = _mm256_shuffle_pd(raw0, raw0, 0b0000); let x0x1 = _mm256_set_pd(
points[i + 3].x,
points[i + 2].x,
points[i + 1].x,
points[i].x,
);
let y0y1 = _mm256_set_pd(
points[i + 3].y,
points[i + 2].y,
points[i + 1].y,
points[i].y,
);
let term1 = _mm256_mul_pd(v_dy, x0x1);
let term2 = _mm256_mul_pd(v_dx, y0y1);
let term3 = _mm256_set1_pd(b.x * a.y - b.y * a.x);
let dist_v = _mm256_sub_pd(term1, term2);
let dist_v = _mm256_add_pd(dist_v, term3);
let mask = _mm256_set1_pd(-0.0);
let dist_v = _mm256_andnot_pd(mask, dist_v);
let cmp = _mm256_cmp_pd(dist_v, v_dmax, _CMP_GT_OQ);
if _mm256_movemask_pd(cmp) != 0 {
let dists: [f64; 4] = std::mem::transmute(dist_v);
for (j, &d) in dists.iter().enumerate() {
if d > dmax {
dmax = d;
index = i + j;
}
}
v_dmax = _mm256_set1_pd(dmax);
}
i += 4;
}
}
}
while i < end {
let d = perpendicular_distance(points[i], a, b);
if d > dmax {
dmax = d;
index = i;
}
i += 1;
}
(dmax, index)
}
pub(crate) fn douglas_peucker<'a>(
arena: &'a Bump,
points: &[Point],
epsilon: f64,
) -> BumpVec<'a, Point> {
if points.len() < 3 {
let mut v = BumpVec::new_in(arena);
v.extend_from_slice(points);
return v;
}
let n = points.len();
let mut keep = BumpVec::from_iter_in((0..n).map(|_| false), arena);
keep[0] = true;
keep[n - 1] = true;
let mut stack = BumpVec::new_in(arena);
stack.push((0, n - 1));
while let Some((start, end)) = stack.pop() {
if end - start < 2 {
continue;
}
let (dmax, index) = find_max_distance_optimized(points, start, end);
if dmax > epsilon {
keep[index] = true;
stack.push((start, index));
stack.push((index, end));
}
}
let mut simplified = BumpVec::new_in(arena);
for (i, &k) in keep.iter().enumerate() {
if k {
simplified.push(points[i]);
}
}
simplified
}
fn perpendicular_distance(p: Point, a: Point, b: Point) -> f64 {
let dx = b.x - a.x;
let dy = b.y - a.y;
let mag = (dx * dx + dy * dy).sqrt();
if mag < 1e-9 {
return ((p.x - a.x).powi(2) + (p.y - a.y).powi(2)).sqrt();
}
((dy * p.x - dx * p.y + b.x * a.y - b.y * a.x).abs()) / mag
}
fn polygon_area(points: &[Point]) -> f64 {
let mut area = 0.0;
for i in 0..points.len() - 1 {
area += (points[i].x * points[i + 1].y) - (points[i + 1].x * points[i].y);
}
area * 0.5
}
#[allow(dead_code)]
fn polygon_center(points: &[Point]) -> [f64; 2] {
let mut cx = 0.0;
let mut cy = 0.0;
let n = points.len() - 1;
for p in points.iter().take(n) {
cx += p.x;
cy += p.y;
}
[cx / n as f64, cy / n as f64]
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub(crate) fn refine_corner(
arena: &Bump,
img: &ImageView,
p: Point,
p_prev: Point,
p_next: Point,
sigma: f64,
decimation: usize,
use_erf: bool,
) -> Point {
let line1 = if use_erf {
refine_edge_erf(arena, img, p_prev, p, sigma, decimation)
.or_else(|| fit_edge_line(img, p_prev, p, decimation))
} else {
fit_edge_line(img, p_prev, p, decimation)
};
let line2 = if use_erf {
refine_edge_erf(arena, img, p, p_next, sigma, decimation)
.or_else(|| fit_edge_line(img, p, p_next, decimation))
} else {
fit_edge_line(img, p, p_next, decimation)
};
if let (Some(l1), Some(l2)) = (line1, line2) {
let det = l1.0 * l2.1 - l2.0 * l1.1;
if det.abs() > 1e-6 {
let x = (l1.1 * l2.2 - l2.1 * l1.2) / det;
let y = (l2.0 * l1.2 - l1.0 * l2.2) / det;
let dist_sq = (x - p.x).powi(2) + (y - p.y).powi(2);
let max_dist = if decimation > 1 {
(decimation as f64) + 2.0
} else {
2.0
};
if dist_sq < max_dist * max_dist {
return Point { x, y };
}
}
}
p
}
fn fit_edge_line(
img: &ImageView,
p1: Point,
p2: Point,
decimation: usize,
) -> Option<(f64, f64, f64)> {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let len = (dx * dx + dy * dy).sqrt();
if len < 4.0 {
return None;
}
let nx = dy / len;
let ny = -dx / len;
let mut sum_d = 0.0;
let mut count = 0;
let n_samples = (len as usize).clamp(5, 15);
let r = if decimation > 1 {
(decimation as i32) + 1
} else {
3
};
for i in 1..=n_samples {
let t = i as f64 / (n_samples + 1) as f64;
let px = p1.x + dx * t;
let py = p1.y + dy * t;
let mut best_px = px;
let mut best_py = py;
let mut best_mag = 0.0;
for step in -r..=r {
let sx = px + nx * f64::from(step);
let sy = py + ny * f64::from(step);
let g = img.sample_gradient_bilinear(sx, sy);
let mag = g[0] * g[0] + g[1] * g[1];
if mag > best_mag {
best_mag = mag;
best_px = sx;
best_py = sy;
}
}
if best_mag > 10.0 {
let mut mags = [0.0f64; 3];
for (j, offset) in [-1.0, 0.0, 1.0].iter().enumerate() {
let sx = best_px + nx * offset;
let sy = best_py + ny * offset;
let g = img.sample_gradient_bilinear(sx, sy);
mags[j] = g[0] * g[0] + g[1] * g[1];
}
let num = mags[2] - mags[0];
let den = 2.0 * (mags[0] + mags[2] - 2.0 * mags[1]);
let sub_offset = if den.abs() > 1e-6 {
(-num / den).clamp(-0.5, 0.5)
} else {
0.0
};
let refined_x = best_px + nx * sub_offset;
let refined_y = best_py + ny * sub_offset;
sum_d += -(nx * refined_x + ny * refined_y);
count += 1;
}
}
if count < 3 {
return None;
}
Some((nx, ny, sum_d / f64::from(count)))
}
fn refine_edge_erf(
arena: &Bump,
img: &ImageView,
p1: Point,
p2: Point,
sigma: f64,
decimation: usize,
) -> Option<(f64, f64, f64)> {
let mut fitter = ErfEdgeFitter::new(img, [p1.x, p1.y], [p2.x, p2.y], true)?;
let sample_cfg = SampleConfig::for_quad(fitter.edge_len(), decimation);
let refine_cfg = RefineConfig::quad_style(sigma);
fitter.fit(arena, &sample_cfg, &refine_cfg);
Some(fitter.line_params())
}
#[cfg(feature = "non_rectified")]
#[must_use]
#[allow(clippy::too_many_arguments)]
fn refine_corner_with_camera<C: crate::camera::CameraModel>(
img: &ImageView,
p_rect: Point,
p_prev_rect: Point,
p_next_rect: Point,
p_px: Point,
decimation: usize,
intrinsics: &crate::pose::CameraIntrinsics,
camera: &C,
) -> Point {
let line1 = fit_edge_line_curved(img, p_prev_rect, p_rect, decimation, intrinsics, camera);
let line2 = fit_edge_line_curved(img, p_rect, p_next_rect, decimation, intrinsics, camera);
if let (Some(l1), Some(l2)) = (line1, line2) {
let det = l1.0 * l2.1 - l2.0 * l1.1;
if det.abs() > 1e-6 {
let xr = (l1.1 * l2.2 - l2.1 * l1.2) / det;
let yr = (l2.0 * l1.2 - l1.0 * l2.2) / det;
let xn = (xr - intrinsics.cx) / intrinsics.fx;
let yn = (yr - intrinsics.cy) / intrinsics.fy;
let [xd, yd] = camera.distort(xn, yn);
let x = xd * intrinsics.fx + intrinsics.cx;
let y = yd * intrinsics.fy + intrinsics.cy;
let dist_sq = (x - p_px.x).powi(2) + (y - p_px.y).powi(2);
let max_dist = if decimation > 1 {
(decimation as f64) + 2.0
} else {
2.0
};
if dist_sq < max_dist * max_dist {
return Point { x, y };
}
}
}
p_px
}
#[cfg(feature = "non_rectified")]
fn fit_edge_line_curved<C: crate::camera::CameraModel>(
img: &ImageView,
p1_rect: Point,
p2_rect: Point,
decimation: usize,
intrinsics: &crate::pose::CameraIntrinsics,
camera: &C,
) -> Option<(f64, f64, f64)> {
let dx_r = p2_rect.x - p1_rect.x;
let dy_r = p2_rect.y - p1_rect.y;
let len_r = (dx_r * dx_r + dy_r * dy_r).sqrt();
if len_r < 4.0 {
return None;
}
let n_samples = (len_r as usize).clamp(5, 15);
let r = if decimation > 1 {
(decimation as i32) + 1
} else {
3
};
let fx = intrinsics.fx;
let fy = intrinsics.fy;
let cx = intrinsics.cx;
let cy = intrinsics.cy;
let fx_over_fy = fx / fy;
let fy_over_fx = fy / fx;
let mut moments = crate::gwlf::MomentAccumulator::new();
for i in 1..=n_samples {
let t = i as f64 / (n_samples + 1) as f64;
let rx = p1_rect.x + dx_r * t;
let ry = p1_rect.y + dy_r * t;
let xn = (rx - cx) / fx;
let yn = (ry - cy) / fy;
let [xd, yd] = camera.distort(xn, yn);
let px = xd * fx + cx;
let py = yd * fy + cy;
let j = camera.distort_jacobian(xn, yn);
let t_px_x = j[0][0] * dx_r + j[0][1] * dy_r * fx_over_fy;
let t_px_y = j[1][0] * dx_r * fy_over_fx + j[1][1] * dy_r;
let t_len = (t_px_x * t_px_x + t_px_y * t_px_y).sqrt();
if t_len < 1e-6 {
continue;
}
let nx = t_px_y / t_len;
let ny = -t_px_x / t_len;
let window = (2 * r + 1) as usize;
let mut mag_buf = [0.0f64; 16];
let mag_slice = &mut mag_buf[..window];
let mut best_idx: usize = 0;
let mut best_mag = 0.0;
for step in -r..=r {
let idx = (step + r) as usize;
let sx = px + nx * f64::from(step);
let sy = py + ny * f64::from(step);
let g = img.sample_gradient_bilinear(sx, sy);
let mag = g[0] * g[0] + g[1] * g[1];
mag_slice[idx] = mag;
if mag > best_mag {
best_mag = mag;
best_idx = idx;
}
}
if best_mag <= 10.0 {
continue;
}
let step_best = best_idx as i32 - r;
let best_px = px + nx * f64::from(step_best);
let best_py = py + ny * f64::from(step_best);
let m_center = best_mag;
let m_minus = if best_idx > 0 {
mag_slice[best_idx - 1]
} else {
let g = img.sample_gradient_bilinear(best_px - nx, best_py - ny);
g[0] * g[0] + g[1] * g[1]
};
let m_plus = if best_idx + 1 < window {
mag_slice[best_idx + 1]
} else {
let g = img.sample_gradient_bilinear(best_px + nx, best_py + ny);
g[0] * g[0] + g[1] * g[1]
};
let num = m_plus - m_minus;
let den = 2.0 * (m_minus + m_plus - 2.0 * m_center);
let sub_offset = if den.abs() > 1e-6 {
(-num / den).clamp(-0.5, 0.5)
} else {
0.0
};
let refined_px = best_px + nx * sub_offset;
let refined_py = best_py + ny * sub_offset;
let [xn_r, yn_r] = camera.undistort((refined_px - cx) / fx, (refined_py - cy) / fy);
moments.add(xn_r * fx + cx, yn_r * fy + cy, 1.0);
}
if moments.sum_w < 3.0 {
return None;
}
let centroid = moments.centroid()?;
let cov = moments.covariance()?;
let eig = crate::gwlf::solve_2x2_symmetric(cov[(0, 0)], cov[(0, 1)], cov[(1, 1)]);
let n_vec = eig.v_min;
let c = -(n_vec.x * centroid.x + n_vec.y * centroid.y);
Some((n_vec.x, n_vec.y, c))
}
fn reduce_to_quad<'a>(arena: &'a Bump, poly: &[Point]) -> BumpVec<'a, Point> {
if poly.len() <= 5 {
return BumpVec::from_iter_in(poly.iter().copied(), arena);
}
let mut current = BumpVec::from_iter_in(poly.iter().copied(), arena);
current.pop();
while current.len() > 4 {
let n = current.len();
let mut min_area = f64::MAX;
let mut min_idx = 0;
for i in 0..n {
let p_prev = current[(i + n - 1) % n];
let p_curr = current[i];
let p_next = current[(i + 1) % n];
let area = (p_prev.x * (p_curr.y - p_next.y)
+ p_curr.x * (p_next.y - p_prev.y)
+ p_next.x * (p_prev.y - p_curr.y))
.abs()
* 0.5;
if area < min_area {
min_area = area;
min_idx = i;
}
}
current.remove(min_idx);
}
if !current.is_empty() {
let first = current[0];
current.push(first);
}
current
}
#[cfg(feature = "non_rectified")]
fn calculate_edge_score_curved<C: crate::camera::CameraModel>(
img: &ImageView,
rect_corners: &[Point; 4],
camera: &C,
scaled: ScaledIntrinsics,
decimation: usize,
) -> f64 {
let d = decimation as f64;
let mut min_score = f64::MAX;
for i in 0..4 {
let p1 = rect_corners[i];
let p2 = rect_corners[(i + 1) % 4];
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let len = (dx * dx + dy * dy).sqrt();
if len < 4.0 {
return 0.0;
}
let n_samples = (len as usize).clamp(3, 10);
let mut edge_mag_sum = 0.0;
for k in 1..=n_samples {
let t = k as f64 / (n_samples + 1) as f64;
let rx = p1.x + dx * t;
let ry = p1.y + dy * t;
let xn = (rx - scaled.cx) / scaled.fx;
let yn = (ry - scaled.cy) / scaled.fy;
let [xd, yd] = camera.distort(xn, yn);
let px = (xd * scaled.fx + scaled.cx) * d;
let py = (yd * scaled.fy + scaled.cy) * d;
let g = img.sample_gradient_bilinear(px, py);
edge_mag_sum += (g[0] * g[0] + g[1] * g[1]).sqrt();
}
let avg_mag = edge_mag_sum / n_samples as f64;
if avg_mag < min_score {
min_score = avg_mag;
}
}
min_score
}
fn calculate_edge_score(img: &ImageView, corners: [Point; 4]) -> f64 {
let mut min_score = f64::MAX;
for i in 0..4 {
let p1 = corners[i];
let p2 = corners[(i + 1) % 4];
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let len = (dx * dx + dy * dy).sqrt();
if len < 4.0 {
return 0.0;
}
let n_samples = (len as usize).clamp(3, 10); let mut edge_mag_sum = 0.0;
for k in 1..=n_samples {
let t = k as f64 / (n_samples + 1) as f64;
let x = p1.x + dx * t;
let y = p1.y + dy * t;
let g = img.sample_gradient_bilinear(x, y);
edge_mag_sum += (g[0] * g[0] + g[1] * g[1]).sqrt();
}
let avg_mag = edge_mag_sum / n_samples as f64;
if avg_mag < min_score {
min_score = avg_mag;
}
}
min_score
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::float_cmp, clippy::unwrap_used)]
mod tests {
use super::*;
use bumpalo::Bump;
use proptest::prelude::*;
#[test]
fn test_edge_score_rejection() {
let width = 20;
let height = 20;
let stride = 20;
let mut data = vec![128u8; width * height];
for y in 6..14 {
for x in 6..14 {
data[y * width + x] = 138;
}
}
let img = ImageView::new(&data, width, height, stride).unwrap();
let corners = [
Point { x: 6.0, y: 6.0 },
Point { x: 14.0, y: 6.0 },
Point { x: 14.0, y: 14.0 },
Point { x: 6.0, y: 14.0 },
];
let score = calculate_edge_score(&img, corners);
assert!(score < 10.0, "Score {score} should be < 10.0");
for y in 6..14 {
for x in 6..14 {
data[y * width + x] = 200;
}
}
for y in 0..height {
for x in 0..width {
if !(6..14).contains(&x) || !(6..14).contains(&y) {
data[y * width + x] = 50;
}
}
}
let img = ImageView::new(&data, width, height, stride).unwrap();
let score = calculate_edge_score(&img, corners);
assert!(score > 40.0, "Score {score} should be > 40.0");
}
proptest! {
#[test]
fn prop_douglas_peucker_invariants(
points in prop::collection::vec((0.0..1000.0, 0.0..1000.0), 3..100),
epsilon in 0.1..10.0f64
) {
let arena = Bump::new();
let contour: Vec<Point> = points.iter().map(|&(x, y)| Point { x, y }).collect();
let simplified = douglas_peucker(&arena, &contour, epsilon);
for p in &simplified {
assert!(contour.iter().any(|&op| (op.x - p.x).abs() < 1e-9 && (op.y - p.y).abs() < 1e-9));
}
assert_eq!(simplified[0].x, contour[0].x);
assert_eq!(simplified[0].y, contour[0].y);
assert_eq!(simplified.last().unwrap().x, contour.last().unwrap().x);
assert_eq!(simplified.last().unwrap().y, contour.last().unwrap().y);
assert!(simplified.len() <= contour.len());
for i in 1..simplified.len() {
let a = simplified[i-1];
let b = simplified[i];
let mut start_idx = None;
let mut end_idx = None;
for (j, op) in contour.iter().enumerate() {
if (op.x - a.x).abs() < 1e-9 && (op.y - a.y).abs() < 1e-9 {
start_idx = Some(j);
}
if (op.x - b.x).abs() < 1e-9 && (op.y - b.y).abs() < 1e-9 {
end_idx = Some(j);
}
}
if let (Some(s), Some(e)) = (start_idx, end_idx) {
for op in contour.iter().take(e + 1).skip(s) {
let d = perpendicular_distance(*op, a, b);
assert!(d <= epsilon + 1e-7, "Distance {d} > epsilon {epsilon} at point");
}
}
}
}
}
use crate::config::TagFamily;
use crate::segmentation::label_components_with_stats;
use crate::simd::math::erf_approx;
use crate::test_utils::{
TestImageParams, compute_corner_error, generate_test_image_with_params,
};
use crate::threshold::ThresholdEngine;
fn run_quad_extraction(tag_size: usize, canvas_size: usize) -> (Vec<Detection>, [[f64; 2]; 4]) {
let params = TestImageParams {
family: TagFamily::AprilTag36h11,
id: 0,
tag_size,
canvas_size,
..Default::default()
};
let (data, corners) = generate_test_image_with_params(¶ms);
let img = ImageView::new(&data, canvas_size, canvas_size, canvas_size).unwrap();
let arena = Bump::new();
let engine = ThresholdEngine::new();
let stats = engine.compute_tile_stats(&arena, &img);
let mut binary = vec![0u8; canvas_size * canvas_size];
engine.apply_threshold(&arena, &img, &stats, &mut binary);
let label_result =
label_components_with_stats(&arena, &binary, canvas_size, canvas_size, true);
let detections = extract_quads_fast(&arena, &img, &label_result);
(detections, corners)
}
#[test]
fn test_quad_extraction_at_varying_sizes() {
let canvas_size = 640;
let tag_sizes = [32, 48, 64, 100, 150, 200, 300];
for tag_size in tag_sizes {
let (detections, _corners) = run_quad_extraction(tag_size, canvas_size);
let detected = !detections.is_empty();
if tag_size >= 48 {
assert!(detected, "Tag size {tag_size}: No quad detected");
}
if detected {
println!(
"Tag size {:>3}px: {} quads, center=[{:.1},{:.1}]",
tag_size,
detections.len(),
detections[0].center[0],
detections[0].center[1]
);
} else {
println!("Tag size {tag_size:>3}px: No quad detected");
}
}
}
#[test]
fn test_quad_corner_accuracy() {
let canvas_size = 640;
let tag_sizes = [100, 150, 200, 300];
for tag_size in tag_sizes {
let (detections, gt_corners) = run_quad_extraction(tag_size, canvas_size);
assert!(!detections.is_empty(), "Tag size {tag_size}: No detection");
let det_corners = detections[0].corners;
let error = compute_corner_error(&det_corners, >_corners);
let max_error = 5.0;
assert!(
error < max_error,
"Tag size {tag_size}: Corner error {error:.2}px exceeds max"
);
println!("Tag size {tag_size:>3}px: Corner error = {error:.2}px");
}
}
#[test]
fn test_quad_center_accuracy() {
let canvas_size = 640;
let tag_size = 150;
let (detections, gt_corners) = run_quad_extraction(tag_size, canvas_size);
assert!(!detections.is_empty(), "No detection");
let expected_cx =
(gt_corners[0][0] + gt_corners[1][0] + gt_corners[2][0] + gt_corners[3][0]) / 4.0;
let expected_cy =
(gt_corners[0][1] + gt_corners[1][1] + gt_corners[2][1] + gt_corners[3][1]) / 4.0;
let det_center = detections[0].center;
let dx = det_center[0] - expected_cx;
let dy = det_center[1] - expected_cy;
let center_error = (dx * dx + dy * dy).sqrt();
assert!(
center_error < 2.0,
"Center error {center_error:.2}px exceeds 2px"
);
println!(
"Quad center: detected=[{:.1},{:.1}], expected=[{:.1},{:.1}], error={:.2}px",
det_center[0], det_center[1], expected_cx, expected_cy, center_error
);
}
#[test]
fn test_quad_extraction_with_decimation() {
let canvas_size = 640;
let tag_size = 160;
let decimation = 2;
let params = TestImageParams {
family: TagFamily::AprilTag36h11,
id: 0,
tag_size,
canvas_size,
..Default::default()
};
let (data, gt_corners) = generate_test_image_with_params(¶ms);
let img = ImageView::new(&data, canvas_size, canvas_size, canvas_size).unwrap();
let new_w = canvas_size / decimation;
let new_h = canvas_size / decimation;
let mut decimated_data = vec![0u8; new_w * new_h];
let decimated_img = img
.decimate_to(decimation, &mut decimated_data)
.expect("decimation failed");
let arena = Bump::new();
let engine = ThresholdEngine::new();
let stats = engine.compute_tile_stats(&arena, &decimated_img);
let mut binary = vec![0u8; new_w * new_h];
engine.apply_threshold(&arena, &decimated_img, &stats, &mut binary);
let label_result = label_components_with_stats(&arena, &binary, new_w, new_h, true);
let config = DetectorConfig {
decimation,
..Default::default()
};
let detections = extract_quads_with_config(
&arena,
&decimated_img,
&label_result,
&config,
decimation,
&img,
);
assert!(!detections.is_empty(), "No quad detected with decimation");
let det_corners = detections[0].corners;
let error = compute_corner_error(&det_corners, >_corners);
assert!(
error < 2.0,
"Corner error with decimation: {error:.2}px exceeds 2px"
);
println!("Decimated (d={decimation}) corner error: {error:.4}px");
}
fn generate_vertical_edge_image(
width: usize,
height: usize,
edge_x: f64,
sigma: f64,
dark: u8,
light: u8,
) -> Vec<u8> {
let mut data = vec![0u8; width * height];
let a = f64::from(dark);
let b = f64::from(light);
let s_sqrt2 = sigma * std::f64::consts::SQRT_2;
for y in 0..height {
for x in 0..width {
let px = x as f64 + 0.5;
let intensity =
f64::midpoint(a, b) + (b - a) / 2.0 * erf_approx((px - edge_x) / s_sqrt2);
data[y * width + x] = intensity.clamp(0.0, 255.0) as u8;
}
}
data
}
fn generate_corner_image(
width: usize,
height: usize,
corner_x: f64,
corner_y: f64,
sigma: f64,
) -> Vec<u8> {
let mut data = vec![0u8; width * height];
let s_sqrt2 = sigma * std::f64::consts::SQRT_2;
for y in 0..height {
for x in 0..width {
let px = x as f64 + 0.5;
let py = y as f64 + 0.5;
let dist_v = px - corner_x;
let dist_h = py - corner_y;
let signed_dist = if px < corner_x && py < corner_y {
-dist_v.abs().min(dist_h.abs())
} else if px >= corner_x && py >= corner_y {
dist_v.min(dist_h).max(0.0)
} else {
if px < corner_x {
dist_h } else {
dist_v }
};
let intensity = 127.5 + 127.5 * erf_approx(signed_dist / s_sqrt2);
data[y * width + x] = intensity.clamp(0.0, 255.0) as u8;
}
}
data
}
#[test]
fn test_refine_corner_subpixel_accuracy() {
let arena = Bump::new();
let width = 60;
let height = 60;
let sigma = 0.6;
let test_cases = [
(30.4, 30.4), (25.7, 25.7), (35.23, 35.23), (28.0, 28.0), (32.5, 32.5), ];
for (true_x, true_y) in test_cases {
let data = generate_corner_image(width, height, true_x, true_y, sigma);
let img = ImageView::new(&data, width, height, width).unwrap();
let init_p = Point {
x: true_x.round(),
y: true_y.round(),
};
let p_prev = Point {
x: true_x.round(),
y: true_y.round() - 10.0,
};
let p_next = Point {
x: true_x.round() - 10.0,
y: true_y.round(),
};
let refined = refine_corner(&arena, &img, init_p, p_prev, p_next, sigma, 1, true);
let error_x = (refined.x - true_x).abs();
let error_y = (refined.y - true_y).abs();
let error_total = (error_x * error_x + error_y * error_y).sqrt();
println!(
"Corner ({:.2}, {:.2}): refined=({:.4}, {:.4}), error=({:.4}, {:.4}), total={:.4}px",
true_x, true_y, refined.x, refined.y, error_x, error_y, error_total
);
assert!(
error_total < 0.15,
"Corner ({true_x}, {true_y}): error {error_total:.4}px exceeds 0.15px threshold"
);
}
}
#[test]
fn test_refine_corner_vertical_edge() {
let arena = Bump::new();
let width = 40;
let height = 40;
let sigma = 0.6;
let true_edge_x = 20.4;
let data = generate_vertical_edge_image(width, height, true_edge_x, sigma, 0, 255);
let img = ImageView::new(&data, width, height, width).unwrap();
let corner_y = 20.0;
let init_p = Point {
x: true_edge_x.round(),
y: corner_y,
};
let p_prev = Point {
x: true_edge_x.round(),
y: corner_y - 10.0,
};
let p_next = Point {
x: true_edge_x.round() - 10.0,
y: corner_y,
};
let refined = refine_corner(&arena, &img, init_p, p_prev, p_next, sigma, 1, true);
let error_x = (refined.x - true_edge_x).abs();
println!(
"Vertical edge x={:.2}: refined.x={:.4}, error={:.4}px",
true_edge_x, refined.x, error_x
);
assert!(
error_x < 0.1,
"Vertical edge x={true_edge_x}: error {error_x:.4}px exceeds 0.1px threshold"
);
}
}
#[multiversion(targets(
"x86_64+avx2+bmi1+bmi2+popcnt+lzcnt",
"x86_64+avx512f+avx512bw+avx512dq+avx512vl",
"aarch64+neon"
))]
fn trace_boundary<'a>(
arena: &'a Bump,
labels: &[u32],
width: usize,
height: usize,
start_x: usize,
start_y: usize,
target_label: u32,
) -> BumpVec<'a, Point> {
let mut points = BumpVec::new_in(arena);
let w = width as isize;
let offsets: [isize; 8] = [
-w, -w + 1, 1, w + 1, w, w - 1, -1, -w - 1, ];
let dx: [isize; 8] = [0, 1, 1, 1, 0, -1, -1, -1];
let dy: [isize; 8] = [-1, -1, 0, 1, 1, 1, 0, -1];
let mut curr_x = start_x as isize;
let mut curr_y = start_y as isize;
let mut curr_idx = start_y * width + start_x;
let mut walk_dir = 2usize;
for _ in 0..10000 {
points.push(Point {
x: curr_x as f64 + 0.5,
y: curr_y as f64 + 0.5,
});
let mut found = false;
let search_start = (walk_dir + 6) % 8;
for i in 0..8 {
let dir = (search_start + i) % 8;
let nx = curr_x + dx[dir];
let ny = curr_y + dy[dir];
if (nx as usize) < width && (ny as usize) < height {
let nidx = (curr_idx as isize + offsets[dir]) as usize;
if labels[nidx] == target_label {
curr_x = nx;
curr_y = ny;
curr_idx = nidx;
walk_dir = dir;
found = true;
break;
}
}
}
if !found || (curr_x == start_x as isize && curr_y == start_y as isize) {
break;
}
}
points
}
pub(crate) fn chain_approximation<'a>(arena: &'a Bump, points: &[Point]) -> BumpVec<'a, Point> {
if points.len() < 3 {
let mut v = BumpVec::new_in(arena);
v.extend_from_slice(points);
return v;
}
let mut result = BumpVec::new_in(arena);
result.push(points[0]);
for i in 1..points.len() - 1 {
let p_prev = points[i - 1];
let p_curr = points[i];
let p_next = points[i + 1];
let dx1 = p_curr.x - p_prev.x;
let dy1 = p_curr.y - p_prev.y;
let dx2 = p_next.x - p_curr.x;
let dy2 = p_next.y - p_curr.y;
if (dx1 * dy2 - dx2 * dy1).abs() > 1e-6 {
result.push(p_curr);
}
}
result.push(*points.last().unwrap_or(&points[0]));
result
}