1use crate::definitions::{Clamp, Image};
5use crate::gradients::{horizontal_sobel, vertical_sobel};
6use crate::math::l2_norm;
7use image::{GenericImage, GrayImage, ImageBuffer, Luma};
8use num::Zero;
9use std::f32;
10
11#[derive(Debug, Copy, Clone, PartialEq, Eq)]
13pub struct HogOptions {
14 pub orientations: usize,
16 pub signed: bool,
18 pub cell_side: usize,
20 pub block_side: usize,
22 pub block_stride: usize, }
25
26impl HogOptions {
27 pub fn new(
29 orientations: usize,
30 signed: bool,
31 cell_side: usize,
32 block_side: usize,
33 block_stride: usize,
34 ) -> HogOptions {
35 HogOptions {
36 orientations,
37 signed,
38 cell_side,
39 block_side,
40 block_stride,
41 }
42 }
43}
44
45#[derive(Debug, Copy, Clone, PartialEq, Eq)]
49pub struct HogSpec {
50 options: HogOptions,
52 cells_wide: usize,
54 cells_high: usize,
56 blocks_wide: usize,
58 blocks_high: usize,
60}
61
62impl HogSpec {
63 pub fn from_options(width: u32, height: u32, options: HogOptions) -> Result<HogSpec, String> {
65 let (cells_wide, cells_high) =
66 Self::checked_cell_dimensions(width as usize, height as usize, options)?;
67 let (blocks_wide, blocks_high) =
68 Self::checked_block_dimensions(cells_wide, cells_high, options)?;
69 Ok(HogSpec {
70 options,
71 cells_wide,
72 cells_high,
73 blocks_wide,
74 blocks_high,
75 })
76 }
77
78 fn invalid_options_message(errors: &[String]) -> String {
79 format!("Invalid HoG options: {0}", errors.join(", "))
80 }
81
82 fn checked_cell_dimensions(
84 width: usize,
85 height: usize,
86 options: HogOptions,
87 ) -> Result<(usize, usize), String> {
88 let mut errors: Vec<String> = vec![];
89 if width % options.cell_side != 0 {
90 errors.push(format!(
91 "cell side {} does not evenly divide width {}",
92 options.cell_side, width
93 ));
94 }
95 if height % options.cell_side != 0 {
96 errors.push(format!(
97 "cell side {} does not evenly divide height {}",
98 options.cell_side, height
99 ));
100 }
101 if !errors.is_empty() {
102 return Err(Self::invalid_options_message(&errors));
103 }
104 Ok((width / options.cell_side, height / options.cell_side))
105 }
106
107 fn checked_block_dimensions(
110 cells_wide: usize,
111 cells_high: usize,
112 options: HogOptions,
113 ) -> Result<(usize, usize), String> {
114 let mut errors: Vec<String> = vec![];
115 if (cells_wide - options.block_side) % options.block_stride != 0 {
116 errors.push(format!(
117 "block stride {} does not evenly divide (cells wide {} - block side {})",
118 options.block_stride, cells_wide, options.block_side
119 ));
120 }
121 if (cells_high - options.block_side) % options.block_stride != 0 {
122 errors.push(format!(
123 "block stride {} does not evenly divide (cells high {} - block side {})",
124 options.block_stride, cells_high, options.block_side
125 ));
126 }
127 if !errors.is_empty() {
128 return Err(Self::invalid_options_message(&errors));
129 }
130 Ok((
131 num_blocks(cells_wide, options.block_side, options.block_stride),
132 num_blocks(cells_high, options.block_side, options.block_stride),
133 ))
134 }
135
136 pub fn descriptor_length(&self) -> usize {
138 self.blocks_wide * self.blocks_high * self.block_descriptor_length()
139 }
140
141 fn block_descriptor_length(&self) -> usize {
143 self.options.orientations * self.options.block_side.pow(2)
144 }
145
146 fn cell_grid_lengths(&self) -> [usize; 3] {
150 [self.options.orientations, self.cells_wide, self.cells_high]
151 }
152
153 fn block_grid_lengths(&self) -> [usize; 3] {
157 [
158 self.block_descriptor_length(),
159 self.blocks_wide,
160 self.blocks_high,
161 ]
162 }
163
164 fn block_internal_lengths(&self) -> [usize; 3] {
168 [
169 self.options.orientations,
170 self.options.block_side,
171 self.options.block_side,
172 ]
173 }
174
175 fn cell_area(&self) -> usize {
177 self.options.cell_side * self.options.cell_side
178 }
179}
180
181fn num_blocks(num_cells: usize, block_side: usize, block_stride: usize) -> usize {
185 (num_cells + block_stride - block_side) / block_stride
186}
187
188pub fn hog(image: &GrayImage, options: HogOptions) -> Result<Vec<f32>, String> {
192 match HogSpec::from_options(image.width(), image.height(), options) {
193 Err(e) => Err(e),
194 Ok(spec) => {
195 let mut grid: Array3d<f32> = cell_histograms(image, spec);
196 let grid_view = grid.view_mut();
197 let descriptor = hog_descriptor_from_hist_grid(grid_view, spec);
198 Ok(descriptor)
199 }
200 }
201}
202
203fn hog_descriptor_from_hist_grid(grid: View3d<'_, f32>, spec: HogSpec) -> Vec<f32> {
206 let mut descriptor = Array3d::new(spec.block_grid_lengths());
207 {
208 let mut block_view = descriptor.view_mut();
209
210 for by in 0..spec.blocks_high {
211 for bx in 0..spec.blocks_wide {
212 let block_data = block_view.inner_slice_mut(bx, by);
213 let mut block = View3d::from_raw(block_data, spec.block_internal_lengths());
214
215 for iy in 0..spec.options.block_side {
216 let cy = by * spec.options.block_stride + iy;
217 for ix in 0..spec.options.block_side {
218 let cx = bx * spec.options.block_stride + ix;
219 let slice = block.inner_slice_mut(ix, iy);
220 let hist = grid.inner_slice(cx, cy);
221 copy(hist, slice);
222 }
223 }
224 }
225 }
226
227 for by in 0..spec.blocks_high {
228 for bx in 0..spec.blocks_wide {
229 let norm = block_norm(&block_view, bx, by);
230 if norm > 0f32 {
231 let block_mut = block_view.inner_slice_mut(bx, by);
232 for i in 0..block_mut.len() {
233 block_mut[i] /= norm;
234 }
235 }
236 }
237 }
238 }
239
240 descriptor.data
241}
242
243fn block_norm(view: &View3d<'_, f32>, bx: usize, by: usize) -> f32 {
245 let block_data = view.inner_slice(bx, by);
246 l2_norm(block_data)
247}
248
249fn copy<T: Copy>(from: &[T], to: &mut [T]) {
250 to.clone_from_slice(&from[..to.len()]);
251}
252
253pub fn cell_histograms(image: &GrayImage, spec: HogSpec) -> Array3d<f32> {
256 let (width, height) = image.dimensions();
257 let mut grid = Array3d::new(spec.cell_grid_lengths());
258 let cell_area = spec.cell_area() as f32;
259 let cell_side = spec.options.cell_side as f32;
260 let horizontal = horizontal_sobel(image);
261 let vertical = vertical_sobel(image);
262 let interval = orientation_bin_width(spec.options);
263 let range = direction_range(spec.options);
264
265 for y in 0..height {
266 let mut grid_view = grid.view_mut();
267 let y_inter = Interpolation::from_position(y as f32 / cell_side);
268
269 for x in 0..width {
270 let x_inter = Interpolation::from_position(x as f32 / cell_side);
271
272 let h = horizontal.get_pixel(x, y)[0] as f32;
273 let v = vertical.get_pixel(x, y)[0] as f32;
274 let m = (h.powi(2) + v.powi(2)).sqrt();
275
276 let mut d = v.atan2(h);
277 if d < 0f32 {
278 d += range;
279 }
280 if !spec.options.signed && d >= f32::consts::PI {
281 d -= f32::consts::PI;
282 }
283
284 let o_inter =
285 Interpolation::from_position_wrapping(d / interval, spec.options.orientations);
286
287 for iy in 0..2usize {
288 let py = y_inter.indices[iy];
289
290 for ix in 0..2usize {
291 let px = x_inter.indices[ix];
292
293 for io in 0..2usize {
294 let po = o_inter.indices[io];
295 if contains_outer(&grid_view, px, py) {
296 let wy = y_inter.weights[iy];
297 let wx = x_inter.weights[ix];
298 let wo = o_inter.weights[io];
299 let up = wy * wx * wo * m / cell_area;
300 let current = *grid_view.at_mut([po, px, py]);
301 *grid_view.at_mut([po, px, py]) = current + up;
302 }
303 }
304 }
305 }
306 }
307 }
308
309 grid
310}
311
312fn contains_outer<T>(view: &View3d<'_, T>, u: usize, v: usize) -> bool {
314 u < view.lengths[1] && v < view.lengths[2]
315}
316
317fn orientation_bin_width(options: HogOptions) -> f32 {
319 direction_range(options) / (options.orientations as f32)
320}
321
322fn direction_range(options: HogOptions) -> f32 {
324 if options.signed {
325 2f32 * f32::consts::PI
326 } else {
327 f32::consts::PI
328 }
329}
330
331#[derive(Debug, Copy, Clone, PartialEq)]
333struct Interpolation {
334 indices: [usize; 2],
335 weights: [f32; 2],
336}
337
338impl Interpolation {
339 fn new(indices: [usize; 2], weights: [f32; 2]) -> Interpolation {
341 Interpolation { indices, weights }
342 }
343
344 fn from_position(pos: f32) -> Interpolation {
346 let fraction = pos - pos.floor();
347 Self::new(
348 [pos as usize, pos as usize + 1],
349 [1f32 - fraction, fraction],
350 )
351 }
352
353 fn from_position_wrapping(pos: f32, length: usize) -> Interpolation {
356 let mut right = (pos as usize) + 1;
357 if right >= length {
358 right = 0;
359 }
360 let fraction = pos - pos.floor();
361 Self::new([pos as usize, right], [1f32 - fraction, fraction])
362 }
363}
364
365pub fn render_hist_grid(star_side: u32, grid: &View3d<'_, f32>, signed: bool) -> Image<Luma<u8>> {
372 let width = grid.lengths[1] as u32 * star_side;
373 let height = grid.lengths[2] as u32 * star_side;
374 let mut out = ImageBuffer::new(width, height);
375
376 for y in 0..grid.lengths[2] {
377 let y_window = y as u32 * star_side;
378 for x in 0..grid.lengths[1] {
379 let x_window = x as u32 * star_side;
380 let mut window = out.sub_image(x_window, y_window, star_side, star_side);
381 let hist = grid.inner_slice(x, y);
382 draw_star_mut(window.inner_mut(), hist, signed);
383 }
384 }
385
386 out
387}
388
389fn draw_ray_mut<I>(image: &mut I, theta: f32, color: I::Pixel)
393where
394 I: GenericImage,
395{
396 use crate::drawing::draw_line_segment_mut;
397 use std::cmp;
398
399 let (width, height) = image.dimensions();
400 let scale = cmp::max(width, height) as f32 / 2f32;
401 let start_x = (width / 2) as f32;
402 let start_y = (height / 2) as f32;
403 let start = (start_x, start_y);
404 let x_step = -scale * theta.sin();
405 let y_step = scale * theta.cos();
406 let end = (start_x + x_step, start_y + y_step);
407
408 draw_line_segment_mut(image, start, end, color);
409}
410
411fn draw_star_mut<I>(image: &mut I, hist: &[f32], signed: bool)
415where
416 I: GenericImage<Pixel = Luma<u8>>,
417{
418 let orientations = hist.len() as f32;
419 for bucket in 0..hist.len() {
420 if signed {
421 let dir = (2f32 * f32::consts::PI * bucket as f32) / orientations;
422 let intensity = Clamp::clamp(hist[bucket]);
423 draw_ray_mut(image, dir, Luma([intensity]));
424 } else {
425 let dir = (f32::consts::PI * bucket as f32) / orientations;
426 let intensity = Clamp::clamp(hist[bucket]);
427 draw_ray_mut(image, dir, Luma([intensity]));
428 draw_ray_mut(image, dir + f32::consts::PI, Luma([intensity]));
429 }
430 }
431}
432
433pub struct Array3d<T> {
435 data: Vec<T>,
437 lengths: [usize; 3],
439}
440
441pub struct View3d<'a, T> {
443 data: &'a mut [T],
445 lengths: [usize; 3],
447}
448
449impl<T: Zero + Clone> Array3d<T> {
450 fn new(lengths: [usize; 3]) -> Array3d<T> {
452 let data = vec![Zero::zero(); data_length(lengths)];
453 Array3d { data, lengths }
454 }
455
456 pub fn view_mut(&mut self) -> View3d<'_, T> {
458 View3d::from_raw(&mut self.data, self.lengths)
459 }
460}
461
462impl<'a, T> View3d<'a, T> {
463 fn from_raw(data: &'a mut [T], lengths: [usize; 3]) -> View3d<'a, T> {
465 View3d { data, lengths }
466 }
467
468 fn at_mut(&mut self, indices: [usize; 3]) -> &mut T {
470 &mut self.data[self.offset(indices)]
471 }
472
473 fn inner_slice(&self, x1: usize, x2: usize) -> &[T] {
476 let offset = self.offset([0, x1, x2]);
477 &self.data[offset..offset + self.lengths[0]]
478 }
479
480 fn inner_slice_mut(&mut self, x1: usize, x2: usize) -> &mut [T] {
483 let offset = self.offset([0, x1, x2]);
484 &mut self.data[offset..offset + self.lengths[0]]
485 }
486
487 fn offset(&self, indices: [usize; 3]) -> usize {
488 indices[2] * self.lengths[1] * self.lengths[0] + indices[1] * self.lengths[0] + indices[0]
489 }
490}
491
492fn data_length(lengths: [usize; 3]) -> usize {
494 lengths[0] * lengths[1] * lengths[2]
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500 use crate::utils::gray_bench_image;
501 use ::test;
502
503 #[test]
504 fn test_num_blocks() {
505 assert_eq!(num_blocks(5, 3, 2), 2);
509 assert_eq!(num_blocks(5, 5, 2), 1);
512 assert_eq!(num_blocks(4, 2, 2), 2);
516 assert_eq!(num_blocks(3, 1, 1), 3);
521 }
522
523 #[test]
524 fn test_hog_spec_valid_options() {
525 assert_eq!(
526 HogSpec::from_options(40, 40, HogOptions::new(8, true, 5, 2, 1))
527 .unwrap()
528 .descriptor_length(),
529 1568
530 );
531 assert_eq!(
532 HogSpec::from_options(40, 40, HogOptions::new(9, true, 4, 2, 1))
533 .unwrap()
534 .descriptor_length(),
535 2916
536 );
537 assert_eq!(
538 HogSpec::from_options(40, 40, HogOptions::new(8, true, 4, 2, 1))
539 .unwrap()
540 .descriptor_length(),
541 2592
542 );
543 }
544
545 #[test]
546 fn test_hog_spec_invalid_options() {
547 let opts = HogOptions {
548 orientations: 8,
549 signed: true,
550 cell_side: 3,
551 block_side: 4,
552 block_stride: 2,
553 };
554 let expected = "Invalid HoG options: block stride 2 does not evenly divide (cells wide 7 - block side 4), \
555 block stride 2 does not evenly divide (cells high 7 - block side 4)";
556 assert_eq!(
557 HogSpec::from_options(21, 21, opts),
558 Err(expected.to_owned())
559 );
560 }
561
562 #[test]
563 fn test_interpolation_from_position() {
564 assert_eq!(
565 Interpolation::from_position(10f32),
566 Interpolation::new([10, 11], [1f32, 0f32])
567 );
568 assert_eq!(
569 Interpolation::from_position(10.25f32),
570 Interpolation::new([10, 11], [0.75f32, 0.25f32])
571 );
572 }
573
574 #[test]
575 fn test_interpolation_from_position_wrapping() {
576 assert_eq!(
577 Interpolation::from_position_wrapping(10f32, 11),
578 Interpolation::new([10, 0], [1f32, 0f32])
579 );
580 assert_eq!(
581 Interpolation::from_position_wrapping(10.25f32, 11),
582 Interpolation::new([10, 0], [0.75f32, 0.25f32])
583 );
584 assert_eq!(
585 Interpolation::from_position_wrapping(10f32, 12),
586 Interpolation::new([10, 11], [1f32, 0f32])
587 );
588 assert_eq!(
589 Interpolation::from_position_wrapping(10.25f32, 12),
590 Interpolation::new([10, 11], [0.75f32, 0.25f32])
591 );
592 }
593
594 #[test]
595 fn test_hog_descriptor_from_hist_grid() {
596 let opts = HogOptions {
602 orientations: 2,
603 signed: true,
604 cell_side: 5,
605 block_side: 2,
606 block_stride: 1,
607 };
608
609 let spec = HogSpec::from_options(15, 10, opts).unwrap();
610
611 let mut grid = Array3d::<f32>::new([2, 3, 2]);
612 let mut view = grid.view_mut();
613
614 {
615 let tl = view.inner_slice_mut(0, 0);
616 copy(&[1f32, 3f32, 2f32], tl);
617 }
618 {
619 let tm = view.inner_slice_mut(1, 0);
620 copy(&[2f32, 3f32, 5f32], tm);
621 }
622 {
623 let tr = view.inner_slice_mut(2, 0);
624 copy(&[0f32, 1f32, 0f32], tr);
625 }
626 {
627 let bl = view.inner_slice_mut(0, 1);
628 copy(&[5f32, 0f32, 7f32], bl);
629 }
630 {
631 let bm = view.inner_slice_mut(1, 1);
632 copy(&[3f32, 7f32, 9f32], bm);
633 }
634 {
635 let br = view.inner_slice_mut(2, 1);
636 copy(&[6f32, 1f32, 4f32], br);
637 }
638
639 let descriptor = hog_descriptor_from_hist_grid(view, spec);
640 assert_eq!(descriptor.len(), 16);
641
642 let counts = [1, 3, 2, 3, 5, 0, 3, 7, 2, 3, 0, 1, 3, 7, 6, 1];
643 let mut expected = [0f32; 16];
644
645 let left_norm = 106f32.sqrt();
646 let right_norm = 109f32.sqrt();
647
648 for i in 0..8 {
649 expected[i] = counts[i] as f32 / left_norm;
650 }
651 for i in 8..16 {
652 expected[i] = counts[i] as f32 / right_norm;
653 }
654
655 assert_eq!(descriptor, expected);
656 }
657
658 #[test]
659 fn test_direction_interpolation_within_bounds() {
660 let image = gray_image!(
661 2, 1, 0;
662 2, 1, 0;
663 2, 1, 0);
664
665 let opts_signed = HogOptions {
666 orientations: 8,
667 signed: true,
668 cell_side: 3,
669 block_side: 1,
670 block_stride: 1,
671 };
672
673 let desc_signed = hog(&image, opts_signed);
674 test::black_box(desc_signed.unwrap());
675
676 let opts_unsigned = HogOptions {
677 orientations: 8,
678 signed: false,
679 cell_side: 3,
680 block_side: 1,
681 block_stride: 1,
682 };
683
684 let desc_unsigned = hog(&image, opts_unsigned);
685 test::black_box(desc_unsigned.unwrap());
686 }
687
688 #[bench]
689 fn bench_hog(b: &mut test::Bencher) {
690 let image = gray_bench_image(88, 88);
691 let opts = HogOptions {
692 orientations: 8,
693 signed: true,
694 cell_side: 8,
695 block_side: 3,
696 block_stride: 2,
697 };
698 b.iter(|| {
699 let desc = hog(&image, opts);
700 test::black_box(desc.unwrap());
701 });
702 }
703}