use crate::error::{Result, VisionError};
use crate::object_detection::{compute_iou, nms, BoundingBox};
#[derive(Clone, Debug)]
pub struct IntegralImage {
data: Vec<f64>,
pub rows: usize,
pub cols: usize,
}
impl IntegralImage {
pub fn new(pixels: &[f64], rows: usize, cols: usize) -> Result<Self> {
if pixels.len() != rows * cols {
return Err(VisionError::InvalidInput(format!(
"IntegralImage::new: expected {} pixels, got {}",
rows * cols,
pixels.len()
)));
}
let stride = cols + 1;
let mut data = vec![0.0_f64; (rows + 1) * stride];
for r in 0..rows {
let mut row_sum = 0.0_f64;
for c in 0..cols {
row_sum += pixels[r * cols + c];
let above = data[r * stride + (c + 1)];
data[(r + 1) * stride + (c + 1)] = row_sum + above;
}
}
Ok(Self { data, rows, cols })
}
#[inline]
pub fn from_array(arr: &scirs2_core::ndarray::Array2<f64>) -> Result<Self> {
let rows = arr.nrows();
let cols = arr.ncols();
let pixels: Vec<f64> = arr.iter().copied().collect();
Self::new(&pixels, rows, cols)
}
#[inline]
pub fn rect_sum(&self, r1: usize, c1: usize, r2: usize, c2: usize) -> f64 {
if r1 >= r2 || c1 >= c2 {
return 0.0;
}
let r2 = r2.min(self.rows);
let c2 = c2.min(self.cols);
if r1 >= r2 || c1 >= c2 {
return 0.0;
}
let stride = self.cols + 1;
self.data[r2 * stride + c2] - self.data[r1 * stride + c2] - self.data[r2 * stride + c1]
+ self.data[r1 * stride + c1]
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct HaarRect {
pub row: usize,
pub col: usize,
pub height: usize,
pub width: usize,
pub weight: f64,
}
impl HaarRect {
pub fn new(row: usize, col: usize, height: usize, width: usize, weight: f64) -> Self {
Self {
row,
col,
height,
width,
weight,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum HaarFeatureType {
EdgeHorizontal,
EdgeVertical,
LineHorizontal,
LineVertical,
FourRectangle,
Custom,
}
#[derive(Clone, Debug)]
pub struct HaarFeature {
pub feature_type: HaarFeatureType,
pub rects: Vec<HaarRect>,
}
impl HaarFeature {
pub fn edge_horizontal(base_row: usize, base_col: usize, height: usize, width: usize) -> Self {
let half_h = height / 2;
Self {
feature_type: HaarFeatureType::EdgeHorizontal,
rects: vec![
HaarRect::new(base_row, base_col, half_h, width, 1.0),
HaarRect::new(base_row + half_h, base_col, height - half_h, width, -1.0),
],
}
}
pub fn edge_vertical(base_row: usize, base_col: usize, height: usize, width: usize) -> Self {
let half_w = width / 2;
Self {
feature_type: HaarFeatureType::EdgeVertical,
rects: vec![
HaarRect::new(base_row, base_col, height, half_w, 1.0),
HaarRect::new(base_row, base_col + half_w, height, width - half_w, -1.0),
],
}
}
pub fn line_horizontal(base_row: usize, base_col: usize, height: usize, width: usize) -> Self {
let third_h = height / 3;
Self {
feature_type: HaarFeatureType::LineHorizontal,
rects: vec![
HaarRect::new(base_row, base_col, third_h, width, -1.0),
HaarRect::new(base_row + third_h, base_col, third_h, width, 2.0),
HaarRect::new(
base_row + 2 * third_h,
base_col,
height - 2 * third_h,
width,
-1.0,
),
],
}
}
pub fn line_vertical(base_row: usize, base_col: usize, height: usize, width: usize) -> Self {
let third_w = width / 3;
Self {
feature_type: HaarFeatureType::LineVertical,
rects: vec![
HaarRect::new(base_row, base_col, height, third_w, -1.0),
HaarRect::new(base_row, base_col + third_w, height, third_w, 2.0),
HaarRect::new(
base_row,
base_col + 2 * third_w,
height,
width - 2 * third_w,
-1.0,
),
],
}
}
pub fn four_rectangle(base_row: usize, base_col: usize, height: usize, width: usize) -> Self {
let half_h = height / 2;
let half_w = width / 2;
Self {
feature_type: HaarFeatureType::FourRectangle,
rects: vec![
HaarRect::new(base_row, base_col, half_h, half_w, 1.0),
HaarRect::new(base_row, base_col + half_w, half_h, width - half_w, -1.0),
HaarRect::new(base_row + half_h, base_col, height - half_h, half_w, -1.0),
HaarRect::new(
base_row + half_h,
base_col + half_w,
height - half_h,
width - half_w,
1.0,
),
],
}
}
}
pub fn compute_haar_feature(
integral: &IntegralImage,
feature: &HaarFeature,
win_row: usize,
win_col: usize,
norm_factor: f64,
) -> f64 {
let mut response = 0.0_f64;
for rect in &feature.rects {
let r1 = win_row + rect.row;
let c1 = win_col + rect.col;
let r2 = r1 + rect.height;
let c2 = c1 + rect.width;
response += rect.weight * integral.rect_sum(r1, c1, r2, c2);
}
if norm_factor.abs() > 1e-12 {
response / norm_factor
} else {
response
}
}
#[derive(Clone, Debug)]
pub struct WeakClassifier {
pub feature: HaarFeature,
pub threshold: f64,
pub polarity: f64,
pub alpha: f64,
}
impl WeakClassifier {
pub fn new(feature: HaarFeature, threshold: f64, polarity: f64, alpha: f64) -> Self {
Self {
feature,
threshold,
polarity,
alpha,
}
}
pub fn evaluate(
&self,
integral: &IntegralImage,
win_row: usize,
win_col: usize,
norm_factor: f64,
) -> f64 {
let response = compute_haar_feature(integral, &self.feature, win_row, win_col, norm_factor);
if self.polarity * response >= self.polarity * self.threshold {
self.alpha
} else {
-self.alpha
}
}
}
#[derive(Clone, Debug)]
pub struct AdaBoostStage {
pub classifiers: Vec<WeakClassifier>,
pub stage_threshold: f64,
}
impl AdaBoostStage {
pub fn new(classifiers: Vec<WeakClassifier>, stage_threshold: f64) -> Self {
Self {
classifiers,
stage_threshold,
}
}
pub fn evaluate(
&self,
integral: &IntegralImage,
win_row: usize,
win_col: usize,
norm_factor: f64,
) -> bool {
let score: f64 = self
.classifiers
.iter()
.map(|clf| clf.evaluate(integral, win_row, win_col, norm_factor))
.sum();
score >= self.stage_threshold
}
}
#[derive(Clone, Debug)]
pub struct AdaBoostClassifier {
pub stages: Vec<AdaBoostStage>,
pub window_width: usize,
pub window_height: usize,
}
impl AdaBoostClassifier {
pub fn new(stages: Vec<AdaBoostStage>, window_width: usize, window_height: usize) -> Self {
Self {
stages,
window_width,
window_height,
}
}
pub fn default_24x24() -> Self {
let stage1 = AdaBoostStage::new(
vec![WeakClassifier::new(
HaarFeature::edge_vertical(4, 6, 8, 12),
0.0, 1.0, 0.5, )],
-0.5, );
let stage2 = AdaBoostStage::new(
vec![
WeakClassifier::new(HaarFeature::edge_horizontal(2, 4, 8, 16), 0.0, 1.0, 0.4),
WeakClassifier::new(HaarFeature::line_vertical(8, 4, 8, 16), 0.0, 1.0, 0.3),
],
-0.7,
);
let stage3 = AdaBoostStage::new(
vec![WeakClassifier::new(
HaarFeature::four_rectangle(12, 6, 8, 12),
0.0,
1.0,
0.6,
)],
-1.0,
);
Self::new(vec![stage1, stage2, stage3], 24, 24)
}
}
pub fn cascade_classify(
classifier: &AdaBoostClassifier,
integral: &IntegralImage,
win_row: usize,
win_col: usize,
norm_factor: f64,
) -> bool {
for stage in &classifier.stages {
if !stage.evaluate(integral, win_row, win_col, norm_factor) {
return false;
}
}
true
}
fn window_variance(
integral: &IntegralImage,
integral_sq: &IntegralImage,
win_row: usize,
win_col: usize,
win_h: usize,
win_w: usize,
) -> f64 {
let n = (win_h * win_w) as f64;
if n < 1.0 {
return 1.0;
}
let sum = integral.rect_sum(win_row, win_col, win_row + win_h, win_col + win_w);
let sum_sq = integral_sq.rect_sum(win_row, win_col, win_row + win_h, win_col + win_w);
let mean = sum / n;
let var = (sum_sq / n) - mean * mean;
var.max(0.0).sqrt().max(1e-6)
}
#[derive(Clone, Debug)]
pub struct FaceDetection {
pub bbox: BoundingBox,
pub stages_passed: usize,
}
pub fn detect_multiscale(
pixels: &[f64],
rows: usize,
cols: usize,
classifier: &AdaBoostClassifier,
min_scale: f64,
max_scale: f64,
scale_step: f64,
stride: usize,
nms_threshold: f64,
) -> Result<Vec<FaceDetection>> {
if pixels.len() != rows * cols {
return Err(VisionError::InvalidInput(format!(
"detect_multiscale: expected {} pixels, got {}",
rows * cols,
pixels.len()
)));
}
if min_scale < 1.0 {
return Err(VisionError::InvalidInput(
"detect_multiscale: min_scale must be ≥ 1.0".to_string(),
));
}
if max_scale < min_scale {
return Err(VisionError::InvalidInput(
"detect_multiscale: max_scale must be ≥ min_scale".to_string(),
));
}
if scale_step <= 1.0 {
return Err(VisionError::InvalidInput(
"detect_multiscale: scale_step must be > 1.0".to_string(),
));
}
if stride == 0 {
return Err(VisionError::InvalidInput(
"detect_multiscale: stride must be > 0".to_string(),
));
}
let integral = IntegralImage::new(pixels, rows, cols)?;
let pixels_sq: Vec<f64> = pixels.iter().map(|&p| p * p).collect();
let integral_sq = IntegralImage::new(&pixels_sq, rows, cols)?;
let base_w = classifier.window_width;
let base_h = classifier.window_height;
let mut raw_boxes: Vec<BoundingBox> = Vec::new();
let mut scale = min_scale;
while scale <= max_scale + 1e-9 {
let win_w = ((base_w as f64) * scale).round() as usize;
let win_h = ((base_h as f64) * scale).round() as usize;
if win_w > cols || win_h > rows || win_w == 0 || win_h == 0 {
scale *= scale_step;
continue;
}
let step = ((stride as f64) * scale).round().max(1.0) as usize;
let mut win_row = 0;
while win_row + win_h <= rows {
let mut win_col = 0;
while win_col + win_w <= cols {
let norm = window_variance(&integral, &integral_sq, win_row, win_col, win_h, win_w);
if cascade_classify(classifier, &integral, win_row, win_col, norm) {
raw_boxes.push(BoundingBox::new(
win_col as f64,
win_row as f64,
(win_col + win_w) as f64,
(win_row + win_h) as f64,
1.0,
0,
));
}
win_col += step;
}
win_row += step;
}
scale *= scale_step;
}
let kept = nms(&raw_boxes, nms_threshold);
Ok(kept
.into_iter()
.map(|bbox| FaceDetection {
stages_passed: classifier.stages.len(),
bbox,
})
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_face_pixels(rows: usize, cols: usize) -> Vec<f64> {
let mut p = vec![0.3_f64; rows * cols];
for r in 0..rows / 2 {
for c in 0..cols {
p[r * cols + c] = 0.8;
}
}
p
}
#[test]
fn test_integral_image_basic() {
let pixels = vec![1.0_f64, 2.0, 3.0, 4.0];
let ii = IntegralImage::new(&pixels, 2, 2).expect("should build");
assert!((ii.rect_sum(0, 0, 2, 2) - 10.0).abs() < 1e-10);
assert!((ii.rect_sum(0, 0, 1, 2) - 3.0).abs() < 1e-10);
assert!((ii.rect_sum(1, 1, 2, 2) - 4.0).abs() < 1e-10);
}
#[test]
fn test_integral_image_wrong_size() {
let pixels = vec![1.0_f64; 5];
assert!(IntegralImage::new(&pixels, 2, 3).is_err());
}
#[test]
fn test_haar_feature_edge_vertical() {
let mut pixels = vec![0.0_f64; 4 * 4];
for r in 0..4 {
for c in 0..2 {
pixels[r * 4 + c] = 1.0;
}
}
let ii = IntegralImage::new(&pixels, 4, 4).expect("should build");
let feat = HaarFeature::edge_vertical(0, 0, 4, 4);
let resp = compute_haar_feature(&ii, &feat, 0, 0, 1.0);
assert!(
resp > 0.0,
"expected positive response for bright-left image, got {resp}"
);
}
#[test]
fn test_weak_classifier_evaluate() {
let pixels = synthetic_face_pixels(24, 24);
let ii = IntegralImage::new(&pixels, 24, 24).expect("should build");
let clf = WeakClassifier::new(HaarFeature::edge_horizontal(0, 0, 24, 24), 0.0, 1.0, 0.5);
let score = clf.evaluate(&ii, 0, 0, 1.0);
assert_eq!(score, 0.5);
}
#[test]
fn test_cascade_classify_passes() {
let cascade = AdaBoostClassifier::default_24x24();
let pixels = synthetic_face_pixels(24, 24);
let ii = IntegralImage::new(&pixels, 24, 24).expect("should build");
let _ = cascade_classify(&cascade, &ii, 0, 0, 1.0);
}
#[test]
fn test_detect_multiscale_empty_on_uniform() {
let rows = 48;
let cols = 48;
let pixels = vec![0.5_f64; rows * cols];
let cascade = AdaBoostClassifier::default_24x24();
let detections = detect_multiscale(&pixels, rows, cols, &cascade, 1.0, 1.0, 1.25, 4, 0.4)
.expect("should succeed");
let _ = detections; }
#[test]
fn test_detect_multiscale_error_bad_input() {
let cascade = AdaBoostClassifier::default_24x24();
assert!(detect_multiscale(&[0.0; 10], 10, 10, &cascade, 1.0, 2.0, 1.25, 4, 0.4).is_err());
assert!(detect_multiscale(&[0.0; 100], 10, 10, &cascade, 0.5, 2.0, 1.25, 4, 0.4).is_err());
assert!(detect_multiscale(&[0.0; 100], 10, 10, &cascade, 1.0, 2.0, 1.0, 4, 0.4).is_err());
}
#[test]
fn test_four_rectangle_feature() {
let mut pixels = vec![0.0_f64; 8 * 8];
for r in 0..4 {
for c in 0..4 {
pixels[r * 8 + c] = 1.0;
}
}
for r in 4..8 {
for c in 4..8 {
pixels[r * 8 + c] = 1.0;
}
}
let ii = IntegralImage::new(&pixels, 8, 8).expect("should build");
let feat = HaarFeature::four_rectangle(0, 0, 8, 8);
let resp = compute_haar_feature(&ii, &feat, 0, 0, 1.0);
assert!(
resp > 0.0,
"expected positive diagonal response, got {resp}"
);
}
}