use crate::error::VisionError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HOGNorm {
L2,
L2Hys,
L1,
L1Sqrt,
}
#[derive(Debug, Clone)]
pub struct HOGConfig {
pub cell_size: usize,
pub block_size: usize,
pub n_bins: usize,
pub unsigned: bool,
pub normalize: HOGNorm,
}
impl Default for HOGConfig {
fn default() -> Self {
Self {
cell_size: 8,
block_size: 2,
n_bins: 9,
unsigned: true,
normalize: HOGNorm::L2Hys,
}
}
}
#[derive(Debug, Clone)]
pub struct HOGDescriptor {
pub cells: Vec<Vec<Vec<f32>>>,
pub config: HOGConfig,
pub n_cell_rows: usize,
pub n_cell_cols: usize,
}
#[inline]
fn sobel_at(image: &[Vec<f32>], r: usize, c: usize, rows: usize, cols: usize) -> (f32, f32) {
let get = |dr: i32, dc: i32| -> f32 {
let rr = (r as i32 + dr).clamp(0, rows as i32 - 1) as usize;
let cc = (c as i32 + dc).clamp(0, cols as i32 - 1) as usize;
image[rr][cc]
};
let gx =
-get(-1, -1) + get(-1, 1) - 2.0 * get(0, -1) + 2.0 * get(0, 1) - get(1, -1) + get(1, 1);
let gy =
-get(-1, -1) - 2.0 * get(-1, 0) - get(-1, 1) + get(1, -1) + 2.0 * get(1, 0) + get(1, 1);
(gx, gy)
}
fn normalize_block(v: &mut [f32], scheme: HOGNorm) {
const EPS: f32 = 1e-5;
match scheme {
HOGNorm::L2 => {
let norm = v.iter().map(|x| x * x).sum::<f32>().sqrt() + EPS;
v.iter_mut().for_each(|x| *x /= norm);
}
HOGNorm::L2Hys => {
let norm = v.iter().map(|x| x * x).sum::<f32>().sqrt() + EPS;
v.iter_mut().for_each(|x| {
*x = (*x / norm).min(0.2);
});
let norm2 = v.iter().map(|x| x * x).sum::<f32>().sqrt() + EPS;
v.iter_mut().for_each(|x| *x /= norm2);
}
HOGNorm::L1 => {
let norm = v.iter().map(|x| x.abs()).sum::<f32>() + EPS;
v.iter_mut().for_each(|x| *x /= norm);
}
HOGNorm::L1Sqrt => {
let norm = v.iter().map(|x| x.abs()).sum::<f32>() + EPS;
v.iter_mut().for_each(|x| *x = (*x / norm).sqrt());
}
}
}
pub fn compute_hog(image: &[Vec<f32>], config: &HOGConfig) -> Result<HOGDescriptor, VisionError> {
let rows = image.len();
let cols = image.first().map_or(0, |r| r.len());
if rows < config.cell_size || cols < config.cell_size {
return Err(VisionError::InvalidInput(
"Image too small for HOG cell size".into(),
));
}
let n_cell_rows = rows / config.cell_size;
let n_cell_cols = cols / config.cell_size;
let n_bins = config.n_bins;
let angle_range = if config.unsigned {
180.0_f32
} else {
360.0_f32
};
let mut grad_mag = vec![vec![0.0_f32; cols]; rows];
let mut grad_ang = vec![vec![0.0_f32; cols]; rows];
for r in 0..rows {
for c in 0..cols {
let (gx, gy) = sobel_at(image, r, c, rows, cols);
grad_mag[r][c] = (gx * gx + gy * gy).sqrt();
let ang = gy.atan2(gx).to_degrees();
let ang = if config.unsigned {
let a = ang % 180.0;
if a < 0.0 {
a + 180.0
} else {
a
}
} else {
let a = ang % 360.0;
if a < 0.0 {
a + 360.0
} else {
a
}
};
grad_ang[r][c] = ang;
}
}
let mut cells: Vec<Vec<Vec<f32>>> = vec![vec![vec![0.0; n_bins]; n_cell_cols]; n_cell_rows];
#[allow(clippy::needless_range_loop)]
for cr in 0..n_cell_rows {
for cc in 0..n_cell_cols {
let r0 = cr * config.cell_size;
let c0 = cc * config.cell_size;
let hist = &mut cells[cr][cc];
for r in r0..r0 + config.cell_size {
for c in c0..c0 + config.cell_size {
let mag = grad_mag[r][c];
let ang = grad_ang[r][c];
let bin_f = ang / angle_range * n_bins as f32;
let bin_lo = bin_f.floor() as usize % n_bins;
let bin_hi = (bin_lo + 1) % n_bins;
let frac = bin_f - bin_f.floor();
hist[bin_lo] += mag * (1.0 - frac);
hist[bin_hi] += mag * frac;
}
}
}
}
Ok(HOGDescriptor {
cells,
config: config.clone(),
n_cell_rows,
n_cell_cols,
})
}
pub fn hog_feature_vector(image: &[Vec<f32>], config: &HOGConfig) -> Result<Vec<f32>, VisionError> {
let desc = compute_hog(image, config)?;
let block = config.block_size;
let n_bins = config.n_bins;
let block_dim = block * block * n_bins;
let n_block_rows = desc.n_cell_rows.saturating_sub(block - 1);
let n_block_cols = desc.n_cell_cols.saturating_sub(block - 1);
let mut feature = Vec::with_capacity(n_block_rows * n_block_cols * block_dim);
for br in 0..n_block_rows {
for bc in 0..n_block_cols {
let mut block_vec: Vec<f32> = Vec::with_capacity(block_dim);
for dr in 0..block {
for dc in 0..block {
block_vec.extend_from_slice(&desc.cells[br + dr][bc + dc]);
}
}
normalize_block(&mut block_vec, config.normalize);
feature.extend_from_slice(&block_vec);
}
}
Ok(feature)
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_image(rows: usize, cols: usize) -> Vec<Vec<f32>> {
(0..rows)
.map(|r| (0..cols).map(|c| ((r + c) % 256) as f32 / 255.0).collect())
.collect()
}
#[test]
fn test_hog_cell_count() {
let img = synthetic_image(64, 128);
let cfg = HOGConfig {
cell_size: 8,
block_size: 2,
n_bins: 9,
..Default::default()
};
let desc = compute_hog(&img, &cfg).expect("compute_hog should succeed on valid image");
assert_eq!(desc.n_cell_rows, 8);
assert_eq!(desc.n_cell_cols, 16);
assert_eq!(desc.cells[0][0].len(), 9);
}
#[test]
fn test_hog_feature_vector_length() {
let img = synthetic_image(64, 128);
let cfg = HOGConfig::default();
let fv = hog_feature_vector(&img, &cfg)
.expect("hog_feature_vector should succeed on valid image");
assert_eq!(fv.len(), 7 * 15 * 36);
}
#[test]
fn test_hog_too_small() {
let img = synthetic_image(4, 4);
let cfg = HOGConfig::default(); assert!(compute_hog(&img, &cfg).is_err());
}
#[test]
fn test_hog_l1_normalisation() {
let img = synthetic_image(32, 32);
let cfg = HOGConfig {
cell_size: 8,
block_size: 2,
n_bins: 9,
normalize: HOGNorm::L1,
unsigned: true,
};
let fv = hog_feature_vector(&img, &cfg)
.expect("hog_feature_vector should succeed on valid image");
assert!(fv.iter().all(|&v| v >= 0.0));
}
#[test]
fn test_hog_l1sqrt_normalisation() {
let img = synthetic_image(32, 32);
let cfg = HOGConfig {
normalize: HOGNorm::L1Sqrt,
..Default::default()
};
let fv = hog_feature_vector(&img, &cfg)
.expect("hog_feature_vector should succeed on valid image");
assert!(fv.iter().all(|&v| v >= 0.0));
}
}