Skip to main content

iris/hog/
mod.rs

1use crate::error::{IrisError, Result};
2use crate::image::Image;
3use burn::tensor::{Tensor, TensorData, backend::Backend};
4
5/// Histogram of Oriented Gradients descriptor.
6///
7/// Computes a feature descriptor based on gradient orientation histograms
8/// localized to fine-spatial regions (cells), grouped into blocks for contrast normalization.
9pub struct HogDescriptor {
10    cell_size: usize,
11    block_size: usize,
12    nbins: usize,
13}
14
15impl HogDescriptor {
16    /// Creates a new HOG descriptor.
17    ///
18    /// - `cell_size`: Size of each cell in pixels (e.g., 8).
19    /// - `block_size`: Number of cells per block (e.g., 2).
20    /// - `nbins`: Number of orientation bins (e.g., 9).
21    pub fn new(cell_size: usize, block_size: usize, nbins: usize) -> Self {
22        Self {
23            cell_size,
24            block_size,
25            nbins,
26        }
27    }
28
29    /// Computes the HOG descriptor for the given image.
30    ///
31    /// The image is converted to grayscale, gradient magnitude and direction are computed,
32    /// orientation histograms are built per cell, and blocks are L2-normalized.
33    /// Returns a 1D tensor containing the concatenated block descriptors.
34    pub fn compute<B: Backend>(&self, image: &Image<B>) -> Result<Tensor<B, 1>> {
35        let gray = image.grayscale()?;
36        let dims = gray.tensor.dims();
37        let h = dims[1];
38        let w = dims[2];
39
40        if h < self.cell_size || w < self.cell_size {
41            return Err(IrisError::InvalidParameter(format!(
42                "Image {}x{} too small for cell_size {}",
43                w, h, self.cell_size
44            )));
45        }
46
47        let device = gray.tensor.device();
48        let tensor_data = gray.tensor.clone().into_data();
49        let flat: Vec<f32> = tensor_data.iter::<f32>().collect();
50
51        // --- Compute gradient magnitude and direction ---
52        let mut magnitude = vec![0.0f32; h * w];
53        let mut direction = vec![0.0f32; h * w];
54
55        for y in 0..h {
56            for x in 0..w {
57                // Horizontal gradient (Sobel-like)
58                let gx = if x == 0 {
59                    flat[y * w + 1] - flat[y * w]
60                } else if x == w - 1 {
61                    flat[y * w + w - 1] - flat[y * w + w - 2]
62                } else {
63                    flat[y * w + x + 1] - flat[y * w + x - 1]
64                };
65
66                // Vertical gradient
67                let gy = if y == 0 {
68                    flat[w + x] - flat[x]
69                } else if y == h - 1 {
70                    flat[(h - 1) * w + x] - flat[(h - 2) * w + x]
71                } else {
72                    flat[(y + 1) * w + x] - flat[(y - 1) * w + x]
73                };
74
75                magnitude[y * w + x] = (gx * gx + gy * gy).sqrt();
76                direction[y * w + x] = gy.atan2(gx); // range [-pi, pi]
77            }
78        }
79
80        // --- Build orientation histograms per cell ---
81        let n_cells_y = h / self.cell_size;
82        let n_cells_x = w / self.cell_size;
83        let bin_width = std::f32::consts::PI / self.nbins as f32;
84
85        // cell_hists[cell_y][cell_x][bin]
86        let mut cell_hists = vec![vec![vec![0.0f32; self.nbins]; n_cells_x]; n_cells_y];
87
88        for cy in 0..n_cells_y {
89            for cx in 0..n_cells_x {
90                let y_start = cy * self.cell_size;
91                let x_start = cx * self.cell_size;
92
93                for dy in 0..self.cell_size {
94                    for dx in 0..self.cell_size {
95                        let y = y_start + dy;
96                        let x = x_start + dx;
97                        let mag = magnitude[y * w + x];
98                        let ang = direction[y * w + x]; // [-pi, pi]
99
100                        // Map angle to [0, pi] (unsigned gradients)
101                        let angle = if ang < 0.0 {
102                            ang + std::f32::consts::PI
103                        } else {
104                            ang
105                        };
106
107                        let bin_f = angle / bin_width;
108                        let bin0 = (bin_f as usize) % self.nbins;
109                        let bin1 = (bin0 + 1) % self.nbins;
110                        let frac = bin_f - bin0 as f32;
111
112                        cell_hists[cy][cx][bin0] += mag * (1.0 - frac);
113                        cell_hists[cy][cx][bin1] += mag * frac;
114                    }
115                }
116            }
117        }
118
119        // --- Normalize blocks and concatenate ---
120        let n_blocks_y = n_cells_y.saturating_sub(self.block_size - 1);
121        let n_blocks_x = n_cells_x.saturating_sub(self.block_size - 1);
122        let block_desc_len = self.block_size * self.block_size * self.nbins;
123        let total_len = n_blocks_y * n_blocks_x * block_desc_len;
124
125        let mut descriptor = vec![0.0f32; total_len];
126
127        for by in 0..n_blocks_y {
128            for bx in 0..n_blocks_x {
129                let mut block_vec: Vec<f32> = Vec::with_capacity(block_desc_len);
130                for dy in 0..self.block_size {
131                    for dx in 0..self.block_size {
132                        let hist = &cell_hists[by + dy][bx + dx];
133                        block_vec.extend_from_slice(hist);
134                    }
135                }
136
137                // L2 normalize the block
138                let eps = 1e-6f32;
139                let norm: f32 = block_vec.iter().map(|v| v * v).sum::<f32>().sqrt() + eps;
140                for v in &mut block_vec {
141                    *v /= norm;
142                }
143
144                let block_idx = (by * n_blocks_x + bx) * block_desc_len;
145                descriptor[block_idx..block_idx + block_desc_len].copy_from_slice(&block_vec);
146            }
147        }
148
149        let data = TensorData::new(descriptor, [total_len]);
150        let tensor = Tensor::<B, 1>::from_data(data, &device);
151        Ok(tensor)
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::test_helpers::{TestBackend, test_device};
159
160    #[test]
161    fn test_hog_descriptor_computation() {
162        let device = test_device();
163        // 32x32 grayscale image
164        let flat_data = vec![0.5f32; 32 * 32];
165        let tensor =
166            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [1, 32, 32]), &device);
167        let img = Image::new(tensor);
168
169        let hog = HogDescriptor::new(8, 2, 9);
170        let descriptor = hog.compute(&img).unwrap();
171
172        // n_cells = 32/8 = 4, n_blocks = (4-2+1)^2 = 9
173        // block_desc_len = 2*2*9 = 36
174        // total = 9 * 36 = 324
175        assert_eq!(descriptor.dims(), [324]);
176    }
177
178    #[test]
179    fn test_hog_too_small_image() {
180        let device = test_device();
181        let flat_data = vec![0.5f32; 4 * 4];
182        let tensor =
183            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [1, 4, 4]), &device);
184        let img = Image::new(tensor);
185
186        let hog = HogDescriptor::new(8, 2, 9);
187        assert!(hog.compute(&img).is_err());
188    }
189}