From e839d205533c17dfa0b3e0306c2546a608c9f954 Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Sat, 14 Dec 2019 13:50:23 +0000
Subject: [PATCH 01/17] Start work on HOG extractor

---
 src/features/hog.rs | 160 ++++++++++++++++++++++++++++++++++++++++++++
 src/features/mod.rs |   1 +
 src/lib.rs          |   2 +
 3 files changed, 163 insertions(+)
 create mode 100644 src/features/hog.rs
 create mode 100644 src/features/mod.rs

diff --git a/src/features/hog.rs b/src/features/hog.rs
new file mode 100644
index 0000000..6b6c499
--- /dev/null
+++ b/src/features/hog.rs
@@ -0,0 +1,160 @@
+use crate::processing::conv::ConvolutionExt;
+use ndarray::{prelude::*, s, DataMut};
+use std::ops::Fn;
+
+#[derive(Debug, Clone)]
+pub struct Gradient {
+    magnitude: Array2<f64>,
+    angle: Array2<f64>,
+}
+
+pub enum GradientType {
+    /// Horizontal gradients only [-1, 0, 1]
+    Horizontal,
+    /// Vertical gradients only [-1, 0, 1]^T
+    Vertical,
+    /// Vertical and Horizontal gradients f(Image, Horizontal) + f(Image, Vertical)
+    Full,
+    /// Specify a single convolution kernel to apply to the image
+    Custom(Box<dyn Fn(ArrayView<f64, Ix3>) -> Gradient>),
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub enum HogType {
+    Rectangular,
+    Circular,
+}
+
+pub struct HistogramOfGradientsBuilder {
+    gradient: Option<GradientType>,
+    hog_type: Option<HogType>,
+    orientations: Option<usize>,
+    cell_width: Option<usize>,
+    block_width: Option<usize>,
+}
+
+pub struct HistogramOfGradientsExtractor {
+    gradient: GradientType,
+    hog_type: HogType,
+    orientations: usize,
+    cell_width: usize,
+    block_width: usize,
+}
+
+impl GradientType {
+    pub fn get_gradient<T>(&self, data: &ArrayBase<T, Ix3>) -> Gradient
+    where
+        T: DataMut<Elem = f64>,
+    {
+        match self {
+            Self::Horizontal => {
+                let w = vec![-1.0, 0.0, 1.0];
+                let hor: Array3<f64> = Array3::from_shape_vec((1, 3, 1), w).unwrap();
+                let mag = data
+                    .conv2d(hor.view())
+                    .unwrap()
+                    .slice(s![.., .., 0])
+                    .to_owned();
+                let dim = mag.dim();
+                Gradient {
+                    magnitude: mag,
+                    angle: Array::zeros(dim),
+                }
+            }
+            Self::Vertical => {
+                let w = vec![-1.0, 0.0, 1.0];
+                let ver: Array3<f64> = Array3::from_shape_vec((3, 1, 1), w).unwrap();
+                let mag = data
+                    .conv2d(ver.view())
+                    .unwrap()
+                    .slice(s![.., .., 0])
+                    .to_owned();
+                let dim = mag.dim();
+                Gradient {
+                    magnitude: mag,
+                    angle: Array::zeros(dim),
+                }
+            }
+            Self::Full => {
+                let w = vec![-1.0, 0.0, 1.0];
+                let hor: Array3<f64> = Array3::from_shape_vec((1, 3, 1), w.clone()).unwrap();
+                let ver: Array3<f64> = Array3::from_shape_vec((3, 1, 1), w).unwrap();
+                let mut magnitude = hor.slice(s![.., .., 0]).mapv(|x| x.powi(2))
+                    + ver.slice(s![.., .., 0]).mapv(|x| x.powi(2));
+                magnitude.mapv_inplace(|x| x.sqrt());
+
+                let mut angle = (ver / hor).slice(s![.., .., 0]).to_owned();
+                angle.mapv_inplace(|x| x.atan());
+                Gradient { magnitude, angle }
+            }
+            Self::Custom(f) => f(data.view()),
+        }
+    }
+}
+
+impl HistogramOfGradientsBuilder {
+    pub fn build(self) -> HistogramOfGradientsExtractor {
+        let gradient = match self.gradient {
+            Some(g) => g,
+            None => GradientType::Full,
+        };
+        let hog_type = match self.hog_type {
+            Some(h) => h,
+            None => HogType::Rectangular,
+        };
+        let orientations = match self.orientations {
+            Some(o) => o,
+            None => 9,
+        };
+        let cell_width = match self.cell_width {
+            Some(c) => c,
+            None => 8,
+        };
+        let block_width = match self.block_width {
+            Some(b) => b,
+            None => 2,
+        };
+
+        HistogramOfGradientsExtractor {
+            gradient,
+            hog_type,
+            orientations,
+            cell_width,
+            block_width,
+        }
+    }
+
+    pub fn gradient(mut self, g: GradientType) -> Self {
+        self.gradient = Some(g);
+        self
+    }
+
+    pub fn hog_type(mut self, h: HogType) -> Self {
+        self.hog_type = Some(h);
+        self
+    }
+
+    pub fn orientations(mut self, o: usize) -> Self {
+        self.orientations = Some(o);
+        self
+    }
+
+    pub fn cell_width(mut self, c: usize) -> Self {
+        self.cell_width = Some(c);
+        self
+    }
+
+    pub fn block_width(mut self, b: usize) -> Self {
+        self.block_width = Some(b);
+        self
+    }
+}
+
+impl HistogramOfGradientsExtractor {
+    pub fn get_features<T>(&self, image: &ArrayBase<T, Ix3>) -> Array2<f64>
+    where
+        T: DataMut<Elem = f64>,
+    {
+        unimplemented!()
+    }
+}
diff --git a/src/features/mod.rs b/src/features/mod.rs
new file mode 100644
index 0000000..6d064f9
--- /dev/null
+++ b/src/features/mod.rs
@@ -0,0 +1 @@
+pub mod hog;
diff --git a/src/lib.rs b/src/lib.rs
index 538eaa1..e42f339 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -30,6 +30,8 @@ pub mod core;
 /// Image enhancement intrinsics and algorithms
 #[cfg(feature = "enhancement")]
 pub mod enhancement;
+/// Feature extraction
+pub mod features;
 /// Image formats - encoding and decoding images from bytes for saving and
 /// loading
 #[cfg(feature = "format")]

From 8e750803bb309496c46ad81c25bc7e0682ab75f0 Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Sat, 14 Dec 2019 14:04:47 +0000
Subject: [PATCH 02/17] Add some doc comments rename things

Comments for what needs doing next
---
 src/features/hog.rs | 24 +++++++++++++++++++++---
 1 file changed, 21 insertions(+), 3 deletions(-)

diff --git a/src/features/hog.rs b/src/features/hog.rs
index 6b6c499..59c34f7 100644
--- a/src/features/hog.rs
+++ b/src/features/hog.rs
@@ -1,10 +1,13 @@
+use crate::core::*;
 use crate::processing::conv::ConvolutionExt;
 use ndarray::{prelude::*, s, DataMut};
 use std::ops::Fn;
 
 #[derive(Debug, Clone)]
 pub struct Gradient {
+    /// Magnitude of the gradient
     magnitude: Array2<f64>,
+    /// Angle of the gradient
     angle: Array2<f64>,
 }
 
@@ -21,15 +24,22 @@ pub enum GradientType {
 
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
 pub enum HogType {
+    /// Use a rectangular region to work out the histogram
     Rectangular,
-    Circular,
+    /// Use a radial region to work out the histogram
+    Radial,
 }
 
 pub struct HistogramOfGradientsBuilder {
+    /// Method used to calculate the image gradients
     gradient: Option<GradientType>,
+    /// Binning method for the histograms
     hog_type: Option<HogType>,
+    /// Number of orientations in the historgram
     orientations: Option<usize>,
+    /// Number of pixels in a cell
     cell_width: Option<usize>,
+    /// Width of an NxN block of cells
     block_width: Option<usize>,
 }
 
@@ -42,7 +52,8 @@ pub struct HistogramOfGradientsExtractor {
 }
 
 impl GradientType {
-    pub fn get_gradient<T>(&self, data: &ArrayBase<T, Ix3>) -> Gradient
+    /// Calculate the gradient for the image data
+    pub fn run<T>(&self, data: &ArrayBase<T, Ix3>) -> Gradient
     where
         T: DataMut<Elem = f64>,
     {
@@ -151,10 +162,17 @@ impl HistogramOfGradientsBuilder {
 }
 
 impl HistogramOfGradientsExtractor {
-    pub fn get_features<T>(&self, image: &ArrayBase<T, Ix3>) -> Array2<f64>
+    pub fn get_features<T>(&self, image: &ImageBase<T, Gray>) -> Array2<f64>
     where
         T: DataMut<Elem = f64>,
     {
+        let grad = self.gradient.run(&image.data);
+        // Binning
+
+        // descriptor blocks
+
+        // block normalisation
+
         unimplemented!()
     }
 }

From 41451997c9f6ba0dea42592f8000efe624609250 Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Wed, 25 Dec 2019 22:28:26 +0000
Subject: [PATCH 03/17] Add in frequency binning

---
 src/features/hog.rs | 35 +++++++++++++++++++++++++++++++++--
 1 file changed, 33 insertions(+), 2 deletions(-)

diff --git a/src/features/hog.rs b/src/features/hog.rs
index 59c34f7..643a019 100644
--- a/src/features/hog.rs
+++ b/src/features/hog.rs
@@ -1,6 +1,7 @@
 use crate::core::*;
 use crate::processing::conv::ConvolutionExt;
 use ndarray::{prelude::*, s, DataMut};
+use std::f64::consts::PI;
 use std::ops::Fn;
 
 #[derive(Debug, Clone)]
@@ -162,17 +163,47 @@ impl HistogramOfGradientsBuilder {
 }
 
 impl HistogramOfGradientsExtractor {
-    pub fn get_features<T>(&self, image: &ImageBase<T, Gray>) -> Array2<f64>
+    pub fn get_features<T>(&self, image: &ImageBase<T, Gray>) -> Array1<f64>
     where
         T: DataMut<Elem = f64>,
     {
+        // You can use number of cells and orientations to get feature vector length
+        let h_cells = image.cols() / self.cell_width;
+        let v_cells = image.rows() / self.cell_width;
+        let mut result = Array3::zeros((v_cells, h_cells, self.orientations));
+        let vec_length = h_cells * v_cells * self.orientations;
+
         let grad = self.gradient.run(&image.data);
+        let delta = (2.0 * PI) / (self.orientations as f64);
         // Binning
+        for r in 0..h_cells {
+            for c in 0..v_cells {
+                let r_start = r * self.cell_width;
+                let r_end = r_start + self.cell_width;
+                let c_start = c * self.cell_width;
+                let c_end = c_start + self.cell_width;
+                for (a, m) in grad
+                    .angle
+                    .slice(s![r_start..r_end, c_start..c_end])
+                    .iter()
+                    .zip(
+                        grad.magnitude
+                            .slice(s![r_start..r_end, c_start..c_end])
+                            .iter(),
+                    )
+                {
+                    let bucket = ((a + PI) / delta).floor() as usize;
+                    if let Some(v) = result.get_mut((r, c, bucket)) {
+                        *v += m;
+                    }
+                }
+            }
+        }
 
         // descriptor blocks
 
         // block normalisation
 
-        unimplemented!()
+        ArrayView1::from(result.as_slice().unwrap()).to_owned()
     }
 }

From 74d0b52d543bcd6e7955c68ce3d6b50c195705c7 Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Thu, 9 Jan 2020 22:23:08 +0000
Subject: [PATCH 04/17] Remove hog type

Only going to support rectangular for now
---
 src/features/hog.rs | 45 +++++++++++++--------------------------------
 1 file changed, 13 insertions(+), 32 deletions(-)

diff --git a/src/features/hog.rs b/src/features/hog.rs
index 643a019..b7f2c96 100644
--- a/src/features/hog.rs
+++ b/src/features/hog.rs
@@ -23,19 +23,9 @@ pub enum GradientType {
     Custom(Box<dyn Fn(ArrayView<f64, Ix3>) -> Gradient>),
 }
 
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
-pub enum HogType {
-    /// Use a rectangular region to work out the histogram
-    Rectangular,
-    /// Use a radial region to work out the histogram
-    Radial,
-}
-
 pub struct HistogramOfGradientsBuilder {
     /// Method used to calculate the image gradients
     gradient: Option<GradientType>,
-    /// Binning method for the histograms
-    hog_type: Option<HogType>,
     /// Number of orientations in the historgram
     orientations: Option<usize>,
     /// Number of pixels in a cell
@@ -46,7 +36,6 @@ pub struct HistogramOfGradientsBuilder {
 
 pub struct HistogramOfGradientsExtractor {
     gradient: GradientType,
-    hog_type: HogType,
     orientations: usize,
     cell_width: usize,
     block_width: usize,
@@ -110,10 +99,6 @@ impl HistogramOfGradientsBuilder {
             Some(g) => g,
             None => GradientType::Full,
         };
-        let hog_type = match self.hog_type {
-            Some(h) => h,
-            None => HogType::Rectangular,
-        };
         let orientations = match self.orientations {
             Some(o) => o,
             None => 9,
@@ -129,7 +114,6 @@ impl HistogramOfGradientsBuilder {
 
         HistogramOfGradientsExtractor {
             gradient,
-            hog_type,
             orientations,
             cell_width,
             block_width,
@@ -141,11 +125,6 @@ impl HistogramOfGradientsBuilder {
         self
     }
 
-    pub fn hog_type(mut self, h: HogType) -> Self {
-        self.hog_type = Some(h);
-        self
-    }
-
     pub fn orientations(mut self, o: usize) -> Self {
         self.orientations = Some(o);
         self
@@ -167,13 +146,20 @@ impl HistogramOfGradientsExtractor {
     where
         T: DataMut<Elem = f64>,
     {
-        // You can use number of cells and orientations to get feature vector length
-        let h_cells = image.cols() / self.cell_width;
-        let v_cells = image.rows() / self.cell_width;
+        let grad = self.gradient.run(&image.data);
+        let mut histograms = self.create_histograms(image.rows(), image.cols(), &grad);
+        // descriptor blocks
+
+        // block normalisation
+
+        ArrayView1::from(histograms.as_slice().unwrap()).to_owned()
+    }
+
+    fn create_histograms(&self, rows: usize, cols: usize, grad: &Gradient) -> Array3<f64> {
+        let h_cells = cols / self.cell_width;
+        let v_cells = rows / self.cell_width;
         let mut result = Array3::zeros((v_cells, h_cells, self.orientations));
-        let vec_length = h_cells * v_cells * self.orientations;
 
-        let grad = self.gradient.run(&image.data);
         let delta = (2.0 * PI) / (self.orientations as f64);
         // Binning
         for r in 0..h_cells {
@@ -199,11 +185,6 @@ impl HistogramOfGradientsExtractor {
                 }
             }
         }
-
-        // descriptor blocks
-
-        // block normalisation
-
-        ArrayView1::from(result.as_slice().unwrap()).to_owned()
+        result
     }
 }

From 364682364ce1c1f3134665214a4ef816b10c6711 Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Sun, 14 Jun 2020 21:57:22 +0100
Subject: [PATCH 05/17] Create feature vector

---
 src/features/hog.rs | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/src/features/hog.rs b/src/features/hog.rs
index b7f2c96..37c4fe8 100644
--- a/src/features/hog.rs
+++ b/src/features/hog.rs
@@ -147,12 +147,18 @@ impl HistogramOfGradientsExtractor {
         T: DataMut<Elem = f64>,
     {
         let grad = self.gradient.run(&image.data);
-        let mut histograms = self.create_histograms(image.rows(), image.cols(), &grad);
-        // descriptor blocks
-
-        // block normalisation
+        let histograms = self.create_histograms(image.rows(), image.cols(), &grad);
+        // descriptor blocks and normalisation
+        let mut feature_vector = Vec::new();
+        for window in histograms.windows((self.block_width, self.block_width, self.orientations)) {
+            // turn window into block and normalise
+            // L2 norm by default
+            let norm = window.iter().map(|x| x.powi(2)).sum::<f64>().sqrt();
+            let mut block = window.iter().map(|v| v / norm).collect::<Vec<f64>>();
+            feature_vector.append(&mut block);
+        }
 
-        ArrayView1::from(histograms.as_slice().unwrap()).to_owned()
+        feature_vector.into()
     }
 
     fn create_histograms(&self, rows: usize, cols: usize, grad: &Gradient) -> Array3<f64> {

From 5fdeaec6a0b926ebc321232d44d59ecd94617a49 Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Mon, 15 Jun 2020 23:01:33 +0100
Subject: [PATCH 06/17] Add a function to start creating HoGBuilder

---
 src/features/hog.rs | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/features/hog.rs b/src/features/hog.rs
index 37c4fe8..d8c56a1 100644
--- a/src/features/hog.rs
+++ b/src/features/hog.rs
@@ -23,6 +23,7 @@ pub enum GradientType {
     Custom(Box<dyn Fn(ArrayView<f64, Ix3>) -> Gradient>),
 }
 
+#[derive(Default)]
 pub struct HistogramOfGradientsBuilder {
     /// Method used to calculate the image gradients
     gradient: Option<GradientType>,
@@ -142,6 +143,11 @@ impl HistogramOfGradientsBuilder {
 }
 
 impl HistogramOfGradientsExtractor {
+    /// Start creating a hog extractor
+    pub fn create() -> HistogramOfGradientsBuilder {
+        Default::default()
+    }
+
     pub fn get_features<T>(&self, image: &ImageBase<T, Gray>) -> Array1<f64>
     where
         T: DataMut<Elem = f64>,

From 5e1c8b5ad6925cbcda8827191276f7da2872bf5c Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Tue, 16 Jun 2020 23:19:21 +0100
Subject: [PATCH 07/17] Get initial implementation finished.

So now:
* Gradients finished (and old gradient stuff got rid of)
* reserve descriptor vector (makes it easier)

TODO
* Signed angles true/false
* Visualisation of features
---
 src/features/hog.rs | 121 ++++++++++++++++----------------------------
 src/features/mod.rs |   2 +
 2 files changed, 45 insertions(+), 78 deletions(-)

diff --git a/src/features/hog.rs b/src/features/hog.rs
index d8c56a1..ab064ea 100644
--- a/src/features/hog.rs
+++ b/src/features/hog.rs
@@ -2,7 +2,6 @@ use crate::core::*;
 use crate::processing::conv::ConvolutionExt;
 use ndarray::{prelude::*, s, DataMut};
 use std::f64::consts::PI;
-use std::ops::Fn;
 
 #[derive(Debug, Clone)]
 pub struct Gradient {
@@ -12,21 +11,8 @@ pub struct Gradient {
     angle: Array2<f64>,
 }
 
-pub enum GradientType {
-    /// Horizontal gradients only [-1, 0, 1]
-    Horizontal,
-    /// Vertical gradients only [-1, 0, 1]^T
-    Vertical,
-    /// Vertical and Horizontal gradients f(Image, Horizontal) + f(Image, Vertical)
-    Full,
-    /// Specify a single convolution kernel to apply to the image
-    Custom(Box<dyn Fn(ArrayView<f64, Ix3>) -> Gradient>),
-}
-
 #[derive(Default)]
 pub struct HistogramOfGradientsBuilder {
-    /// Method used to calculate the image gradients
-    gradient: Option<GradientType>,
     /// Number of orientations in the historgram
     orientations: Option<usize>,
     /// Number of pixels in a cell
@@ -36,70 +22,40 @@ pub struct HistogramOfGradientsBuilder {
 }
 
 pub struct HistogramOfGradientsExtractor {
-    gradient: GradientType,
     orientations: usize,
     cell_width: usize,
     block_width: usize,
 }
 
-impl GradientType {
-    /// Calculate the gradient for the image data
-    pub fn run<T>(&self, data: &ArrayBase<T, Ix3>) -> Gradient
-    where
-        T: DataMut<Elem = f64>,
-    {
-        match self {
-            Self::Horizontal => {
-                let w = vec![-1.0, 0.0, 1.0];
-                let hor: Array3<f64> = Array3::from_shape_vec((1, 3, 1), w).unwrap();
-                let mag = data
-                    .conv2d(hor.view())
-                    .unwrap()
-                    .slice(s![.., .., 0])
-                    .to_owned();
-                let dim = mag.dim();
-                Gradient {
-                    magnitude: mag,
-                    angle: Array::zeros(dim),
-                }
-            }
-            Self::Vertical => {
-                let w = vec![-1.0, 0.0, 1.0];
-                let ver: Array3<f64> = Array3::from_shape_vec((3, 1, 1), w).unwrap();
-                let mag = data
-                    .conv2d(ver.view())
-                    .unwrap()
-                    .slice(s![.., .., 0])
-                    .to_owned();
-                let dim = mag.dim();
-                Gradient {
-                    magnitude: mag,
-                    angle: Array::zeros(dim),
-                }
-            }
-            Self::Full => {
-                let w = vec![-1.0, 0.0, 1.0];
-                let hor: Array3<f64> = Array3::from_shape_vec((1, 3, 1), w.clone()).unwrap();
-                let ver: Array3<f64> = Array3::from_shape_vec((3, 1, 1), w).unwrap();
-                let mut magnitude = hor.slice(s![.., .., 0]).mapv(|x| x.powi(2))
-                    + ver.slice(s![.., .., 0]).mapv(|x| x.powi(2));
-                magnitude.mapv_inplace(|x| x.sqrt());
-
-                let mut angle = (ver / hor).slice(s![.., .., 0]).to_owned();
-                angle.mapv_inplace(|x| x.atan());
-                Gradient { magnitude, angle }
-            }
-            Self::Custom(f) => f(data.view()),
-        }
-    }
+fn get_image_gradients<T>(data: &ArrayBase<T, Ix3>) -> Gradient
+where
+    T: DataMut<Elem = f64>,
+{
+    let w = vec![-1.0, 0.0, 1.0];
+    let hor: ArrayView3<f64> = ArrayView3::from_shape((1, 3, 1), &w).unwrap();
+    let mut h_mag = data
+        .conv2d(hor)
+        .unwrap()
+        .slice_mut(s![.., .., 0])
+        .to_owned();
+    h_mag.mapv_inplace(|x| x.powi(2));
+
+    let ver: ArrayView3<f64> = ArrayView3::from_shape((3, 1, 1), &w).unwrap();
+    let mut v_mag = data
+        .conv2d(ver)
+        .unwrap()
+        .slice_mut(s![.., .., 0])
+        .to_owned();
+    v_mag.mapv_inplace(|x| x.powi(2));
+
+    let magnitude = &h_mag + &v_mag;
+    let mut angle = (&v_mag / &h_mag).to_owned();
+    angle.mapv_inplace(|x| x.atan());
+    Gradient { magnitude, angle }
 }
 
 impl HistogramOfGradientsBuilder {
     pub fn build(self) -> HistogramOfGradientsExtractor {
-        let gradient = match self.gradient {
-            Some(g) => g,
-            None => GradientType::Full,
-        };
         let orientations = match self.orientations {
             Some(o) => o,
             None => 9,
@@ -114,18 +70,12 @@ impl HistogramOfGradientsBuilder {
         };
 
         HistogramOfGradientsExtractor {
-            gradient,
             orientations,
             cell_width,
             block_width,
         }
     }
 
-    pub fn gradient(mut self, g: GradientType) -> Self {
-        self.gradient = Some(g);
-        self
-    }
-
     pub fn orientations(mut self, o: usize) -> Self {
         self.orientations = Some(o);
         self
@@ -152,10 +102,12 @@ impl HistogramOfGradientsExtractor {
     where
         T: DataMut<Elem = f64>,
     {
-        let grad = self.gradient.run(&image.data);
+        let grad = get_image_gradients(&image.data);
         let histograms = self.create_histograms(image.rows(), image.cols(), &grad);
         // descriptor blocks and normalisation
         let mut feature_vector = Vec::new();
+        let vec_len = self.feature_len(self.cell_dim((image.rows(), image.cols())));
+        feature_vector.reserve(vec_len);
         for window in histograms.windows((self.block_width, self.block_width, self.orientations)) {
             // turn window into block and normalise
             // L2 norm by default
@@ -167,9 +119,22 @@ impl HistogramOfGradientsExtractor {
         feature_vector.into()
     }
 
+    fn block_descriptor_len(&self) -> usize {
+        self.cell_width.pow(2) * self.orientations
+    }
+
+    fn cell_dim(&self, dim: (usize, usize)) -> (usize, usize) {
+        (dim.0 / self.cell_width, dim.1 / self.cell_width)
+    }
+
+    fn feature_len(&self, cells: (usize, usize)) -> usize {
+        let blocks_wide = cells.1 - self.block_width;
+        let blocks_tall = cells.0 - self.block_width;
+        blocks_wide * blocks_tall * self.block_descriptor_len()
+    }
+
     fn create_histograms(&self, rows: usize, cols: usize, grad: &Gradient) -> Array3<f64> {
-        let h_cells = cols / self.cell_width;
-        let v_cells = rows / self.cell_width;
+        let (v_cells, h_cells) = self.cell_dim((rows, cols));
         let mut result = Array3::zeros((v_cells, h_cells, self.orientations));
 
         let delta = (2.0 * PI) / (self.orientations as f64);
diff --git a/src/features/mod.rs b/src/features/mod.rs
index 6d064f9..0f24de7 100644
--- a/src/features/mod.rs
+++ b/src/features/mod.rs
@@ -1 +1,3 @@
 pub mod hog;
+
+pub use hog::*;

From e9bf9d84e315609b5871fd0e46662e3215b98b7b Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Thu, 18 Jun 2020 23:57:50 +0100
Subject: [PATCH 08/17] Sort out angles

* Moved to sobel instead of simple gradient
* Fix potential divide by zero in sobel by adding epsilon
* Add Copy to padding when possible
* Add 2PI scaling for negative angles
---
 src/core/padding.rs     |  4 +--
 src/features/hog.rs     | 58 ++++++++++++++++-------------------------
 src/processing/sobel.rs |  4 +--
 3 files changed, 26 insertions(+), 40 deletions(-)

diff --git a/src/core/padding.rs b/src/core/padding.rs
index 2f6ecbd..1502929 100644
--- a/src/core/padding.rs
+++ b/src/core/padding.rs
@@ -39,13 +39,13 @@ where
 pub struct NoPadding;
 
 /// Pad the image with a constant value
-#[derive(Clone, Eq, PartialEq, Hash, Debug)]
+#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
 pub struct ConstantPadding<T>(T)
 where
     T: Copy;
 
 /// Pad the image with zeros. Uses ConstantPadding internally
-#[derive(Clone, Eq, PartialEq, Hash, Debug)]
+#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
 pub struct ZeroPadding;
 
 #[inline]
diff --git a/src/features/hog.rs b/src/features/hog.rs
index ab064ea..7fdd4d8 100644
--- a/src/features/hog.rs
+++ b/src/features/hog.rs
@@ -1,14 +1,15 @@
 use crate::core::*;
-use crate::processing::conv::ConvolutionExt;
+use crate::processing::SobelExt;
 use ndarray::{prelude::*, s, DataMut};
+use std::cmp::max;
 use std::f64::consts::PI;
 
 #[derive(Debug, Clone)]
 pub struct Gradient {
     /// Magnitude of the gradient
-    magnitude: Array2<f64>,
+    magnitude: Array3<f64>,
     /// Angle of the gradient
-    angle: Array2<f64>,
+    angle: Array3<f64>,
 }
 
 #[derive(Default)]
@@ -27,33 +28,6 @@ pub struct HistogramOfGradientsExtractor {
     block_width: usize,
 }
 
-fn get_image_gradients<T>(data: &ArrayBase<T, Ix3>) -> Gradient
-where
-    T: DataMut<Elem = f64>,
-{
-    let w = vec![-1.0, 0.0, 1.0];
-    let hor: ArrayView3<f64> = ArrayView3::from_shape((1, 3, 1), &w).unwrap();
-    let mut h_mag = data
-        .conv2d(hor)
-        .unwrap()
-        .slice_mut(s![.., .., 0])
-        .to_owned();
-    h_mag.mapv_inplace(|x| x.powi(2));
-
-    let ver: ArrayView3<f64> = ArrayView3::from_shape((3, 1, 1), &w).unwrap();
-    let mut v_mag = data
-        .conv2d(ver)
-        .unwrap()
-        .slice_mut(s![.., .., 0])
-        .to_owned();
-    v_mag.mapv_inplace(|x| x.powi(2));
-
-    let magnitude = &h_mag + &v_mag;
-    let mut angle = (&v_mag / &h_mag).to_owned();
-    angle.mapv_inplace(|x| x.atan());
-    Gradient { magnitude, angle }
-}
-
 impl HistogramOfGradientsBuilder {
     pub fn build(self) -> HistogramOfGradientsExtractor {
         let orientations = match self.orientations {
@@ -102,7 +76,11 @@ impl HistogramOfGradientsExtractor {
     where
         T: DataMut<Elem = f64>,
     {
-        let grad = get_image_gradients(&image.data);
+        let (magnitude, angle) = image.data.full_sobel().unwrap();
+        let grad = Gradient {
+            magnitude,
+            angle
+        };
         let histograms = self.create_histograms(image.rows(), image.cols(), &grad);
         // descriptor blocks and normalisation
         let mut feature_vector = Vec::new();
@@ -128,8 +106,10 @@ impl HistogramOfGradientsExtractor {
     }
 
     fn feature_len(&self, cells: (usize, usize)) -> usize {
-        let blocks_wide = cells.1 - self.block_width;
-        let blocks_tall = cells.0 - self.block_width;
+        let blocks_wide = cells.1 as isize - self.block_width as isize;
+        let blocks_tall = cells.0 as isize - self.block_width as isize;
+        let blocks_wide = max(1, blocks_wide) as usize;
+        let blocks_tall = max(1, blocks_tall) as usize;
         blocks_wide * blocks_tall * self.block_descriptor_len()
     }
 
@@ -138,6 +118,7 @@ impl HistogramOfGradientsExtractor {
         let mut result = Array3::zeros((v_cells, h_cells, self.orientations));
 
         let delta = (2.0 * PI) / (self.orientations as f64);
+        println!("Angle delta {}", delta * 180.0/PI);
         // Binning
         for r in 0..h_cells {
             for c in 0..v_cells {
@@ -147,15 +128,20 @@ impl HistogramOfGradientsExtractor {
                 let c_end = c_start + self.cell_width;
                 for (a, m) in grad
                     .angle
-                    .slice(s![r_start..r_end, c_start..c_end])
+                    .slice(s![r_start..r_end, c_start..c_end, 0])
                     .iter()
                     .zip(
                         grad.magnitude
-                            .slice(s![r_start..r_end, c_start..c_end])
+                            .slice(s![r_start..r_end, c_start..c_end, 0])
                             .iter(),
                     )
                 {
-                    let bucket = ((a + PI) / delta).floor() as usize;
+                    let a = if *a < 0.0 {
+                        a + 2.0 * PI
+                    } else {
+                        *a
+                    };
+                    let bucket = (a / delta).floor() as usize;
                     if let Some(v) = result.get_mut((r, c, bucket)) {
                         *v += m;
                     }
diff --git a/src/processing/sobel.rs b/src/processing/sobel.rs
index 3cdbc22..49b002c 100644
--- a/src/processing/sobel.rs
+++ b/src/processing/sobel.rs
@@ -74,8 +74,8 @@ where
     }
 
     fn full_sobel(&self) -> Result<(Self::Output, Self::Output), Error> {
-        let (h_deriv, v_deriv) = get_edge_images(self)?;
-
+        let (mut h_deriv, v_deriv) = get_edge_images(self)?;
+        h_deriv.mapv_inplace(|x| x + T::epsilon());
         let mut magnitude = h_deriv.mapv(|x| x.powi(2)) + v_deriv.mapv(|x| x.powi(2));
         magnitude.mapv_inplace(|x| x.sqrt());
 

From a3b374364a85b083271c0d999ca3dee38e326d08 Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Sat, 20 Jun 2020 12:59:56 +0100
Subject: [PATCH 09/17] Add visualise function

* Add rough "voting by bilinear interpolation" from duke university notes
* Add start of visualisation function (unverified)
* Remove magnitude limiting from sobel
* Pad by default in convolution
---
 src/features/hog.rs    | 110 +++++++++++++++++++++++++++++++++++------
 src/processing/conv.rs |   4 +-
 2 files changed, 96 insertions(+), 18 deletions(-)

diff --git a/src/features/hog.rs b/src/features/hog.rs
index 7fdd4d8..b92be89 100644
--- a/src/features/hog.rs
+++ b/src/features/hog.rs
@@ -1,7 +1,7 @@
 use crate::core::*;
 use crate::processing::SobelExt;
 use ndarray::{prelude::*, s, DataMut};
-use std::cmp::max;
+use std::cmp::{max, min};
 use std::f64::consts::PI;
 
 #[derive(Debug, Clone)]
@@ -22,6 +22,7 @@ pub struct HistogramOfGradientsBuilder {
     block_width: Option<usize>,
 }
 
+/// Extract the HoG descriptor from an image and optionally visualise it
 pub struct HistogramOfGradientsExtractor {
     orientations: usize,
     cell_width: usize,
@@ -66,21 +67,92 @@ impl HistogramOfGradientsBuilder {
     }
 }
 
+fn draw_line(start: (isize, isize), end: (isize, isize), image: &mut ArrayViewMut2<u8>) {
+    let dim = image.dim();
+    let dim = (dim.0 as isize, dim.1 as isize);
+    let deltax = (end.1 - start.1) as f64;
+    let deltay = (end.0 - start.0) as f64;
+    if deltax != 0.0 {
+        let deltaerr = (deltay / deltax).abs();
+        let mut error = 0.0;
+        let mut y = start.0;
+        for x in start.1..end.1 {
+            if x > 0 && y > 0 && x < dim.1 && y < dim.0 {
+                image[[y as usize, x as usize]] = 255;
+            }
+            error += deltaerr;
+            if error >= 0.5 {
+                y = y + deltay.signum() as isize;
+                error = error - 1.0;
+            }
+        }
+    } else {
+        for y in min(start.0, end.0)..max(start.0, end.0) {
+            if start.1 > 0 && y > 0 && start.1 < dim.1 && y < dim.0 {
+                image[[y as usize, start.1 as usize]] = 255;
+            }
+        }
+    }
+}
+
 impl HistogramOfGradientsExtractor {
     /// Start creating a hog extractor
     pub fn create() -> HistogramOfGradientsBuilder {
         Default::default()
     }
 
+    pub fn visualise_features(
+        &self,
+        image_dims: (usize, usize),
+        features: ArrayView1<f64>,
+    ) -> Image<u8, Gray> {
+        let mut result = Image::new(image_dims.0, image_dims.1);
+        let cells = self.cell_dim(image_dims);
+        let cell_len = self.block_descriptor_len();
+        let centre = (self.cell_width/2, self.cell_width/2);
+        let delta = self.angle_delta();
+        for row in 0..cells.0 {
+            for col in 0..cells.1 {
+                let start = row * cells.0 + col;
+                let mut vector = features
+                    .into_iter()
+                    .skip(start * cell_len)
+                    .take(self.orientations)
+                    .copied()
+                    .collect::<Vec<_>>();
+                if vector.is_empty() {
+                    println!("This shouldn't happen if it's working...");
+                    break;
+                }
+                let norm: f64 = vector.iter().sum::<f64>() + f64::EPSILON;
+                vector.iter_mut().for_each(|x| *x /= norm);
+
+                let mut cell = result.data.slice_mut(s![
+                    (row * self.cell_width)..((row + 1) * self.cell_width),
+                    (col * self.cell_width)..((col + 1) * self.cell_width),
+                    0
+                ]);
+                cell[centre] = 255; 
+                // From the features get circle coordinate x = r*sin(theta) y = r*cos(theta)
+                // theta is angle of the bin, r is normalised magnitude * cell_width
+                // Draw a white line from end coordinate to origin 
+                for a in 0..self.orientations {
+                    let a_f = a as f64;
+                    let x = centre.1 as isize + (vector[a] * (self.cell_width as f64) * (delta*a_f).sin()).round() as isize;
+                    let y = centre.0 as isize + (vector[a] * (self.cell_width as f64) * (delta*a_f).cos()).round() as isize;
+                    draw_line((centre.0 as isize, centre.1 as isize ), (y, x), &mut cell);
+                }
+            }
+        }
+        result
+    }
+
     pub fn get_features<T>(&self, image: &ImageBase<T, Gray>) -> Array1<f64>
     where
         T: DataMut<Elem = f64>,
     {
         let (magnitude, angle) = image.data.full_sobel().unwrap();
-        let grad = Gradient {
-            magnitude,
-            angle
-        };
+        let grad = Gradient { magnitude, angle };
         let histograms = self.create_histograms(image.rows(), image.cols(), &grad);
         // descriptor blocks and normalisation
         let mut feature_vector = Vec::new();
@@ -89,7 +161,7 @@ impl HistogramOfGradientsExtractor {
         for window in histograms.windows((self.block_width, self.block_width, self.orientations)) {
             // turn window into block and normalise
             // L2 norm by default
-            let norm = window.iter().map(|x| x.powi(2)).sum::<f64>().sqrt();
+            let norm = (window.iter().map(|x| x.powi(2)).sum::<f64>() + f64::EPSILON.powi(2)).sqrt();
             let mut block = window.iter().map(|v| v / norm).collect::<Vec<f64>>();
             feature_vector.append(&mut block);
         }
@@ -98,13 +170,17 @@ impl HistogramOfGradientsExtractor {
     }
 
     fn block_descriptor_len(&self) -> usize {
-        self.cell_width.pow(2) * self.orientations
+        self.block_width.pow(2) * self.orientations
     }
 
     fn cell_dim(&self, dim: (usize, usize)) -> (usize, usize) {
         (dim.0 / self.cell_width, dim.1 / self.cell_width)
     }
 
+    fn angle_delta(&self) -> f64 {
+        (2.0 * PI) / (self.orientations as f64)
+    }
+
     fn feature_len(&self, cells: (usize, usize)) -> usize {
         let blocks_wide = cells.1 as isize - self.block_width as isize;
         let blocks_tall = cells.0 as isize - self.block_width as isize;
@@ -117,8 +193,7 @@ impl HistogramOfGradientsExtractor {
         let (v_cells, h_cells) = self.cell_dim((rows, cols));
         let mut result = Array3::zeros((v_cells, h_cells, self.orientations));
 
-        let delta = (2.0 * PI) / (self.orientations as f64);
-        println!("Angle delta {}", delta * 180.0/PI);
+        let delta = self.angle_delta();
         // Binning
         for r in 0..h_cells {
             for c in 0..v_cells {
@@ -136,14 +211,17 @@ impl HistogramOfGradientsExtractor {
                             .iter(),
                     )
                 {
-                    let a = if *a < 0.0 {
-                        a + 2.0 * PI
-                    } else {
-                        *a
-                    };
-                    let bucket = (a / delta).floor() as usize;
+                    let a = if *a < 0.0 { a + 2.0 * PI } else { *a };
+                    let bucket = (a / delta - 0.5).floor() as usize % self.orientations;
+                    let centre = delta * (bucket as f64 + 0.5);
+                    let vote_2 = m * ((a - centre).abs()) / delta;
+                    let vote_1 = m - vote_2;
                     if let Some(v) = result.get_mut((r, c, bucket)) {
-                        *v += m;
+                        *v += vote_1;
+                    }
+                    let bucket_2 = (bucket + 1) % self.orientations;
+                    if let Some(v) = result.get_mut((r, c, bucket_2)) {
+                        *v += vote_2;
                     }
                 }
             }
diff --git a/src/processing/conv.rs b/src/processing/conv.rs
index fd34f71..e11f4b8 100644
--- a/src/processing/conv.rs
+++ b/src/processing/conv.rs
@@ -92,14 +92,14 @@ where
     type Output = Array<T, Ix3>;
 
     fn conv2d<B: Data<Elem = T>>(&self, kernel: ArrayBase<B, Ix3>) -> Result<Self::Output, Error> {
-        self.conv2d_with_padding(kernel, &NoPadding {})
+        self.conv2d_with_padding(kernel, &ZeroPadding {})
     }
 
     fn conv2d_inplace<B: Data<Elem = T>>(
         &mut self,
         kernel: ArrayBase<B, Ix3>,
     ) -> Result<(), Error> {
-        self.assign(&self.conv2d_with_padding(kernel, &NoPadding {})?);
+        self.assign(&self.conv2d_with_padding(kernel, &ZeroPadding {})?);
         Ok(())
     }
 

From 7c579d17cf68b375dc1381361025580deb65df50 Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Sat, 20 Jun 2020 13:30:14 +0100
Subject: [PATCH 10/17] Cargo fmt fix test

---
 src/features/hog.rs    | 19 ++++++++++++-------
 src/processing/conv.rs |  2 +-
 2 files changed, 13 insertions(+), 8 deletions(-)

diff --git a/src/features/hog.rs b/src/features/hog.rs
index b92be89..06857db 100644
--- a/src/features/hog.rs
+++ b/src/features/hog.rs
@@ -109,7 +109,7 @@ impl HistogramOfGradientsExtractor {
         let mut result = Image::new(image_dims.0, image_dims.1);
         let cells = self.cell_dim(image_dims);
         let cell_len = self.block_descriptor_len();
-        let centre = (self.cell_width/2, self.cell_width/2);
+        let centre = (self.cell_width / 2, self.cell_width / 2);
         let delta = self.angle_delta();
         for row in 0..cells.0 {
             for col in 0..cells.1 {
@@ -132,15 +132,19 @@ impl HistogramOfGradientsExtractor {
                     (col * self.cell_width)..((col + 1) * self.cell_width),
                     0
                 ]);
-                cell[centre] = 255; 
+                cell[centre] = 255;
                 // From the features get circle coordinate x = r*sin(theta) y = r*cos(theta)
                 // theta is angle of the bin, r is normalised magnitude * cell_width
-                // Draw a white line from end coordinate to origin 
+                // Draw a white line from end coordinate to origin
                 for a in 0..self.orientations {
                     let a_f = a as f64;
-                    let x = centre.1 as isize + (vector[a] * (self.cell_width as f64) * (delta*a_f).sin()).round() as isize;
-                    let y = centre.0 as isize + (vector[a] * (self.cell_width as f64) * (delta*a_f).cos()).round() as isize;
-                    draw_line((centre.0 as isize, centre.1 as isize ), (y, x), &mut cell);
+                    let x = centre.1 as isize
+                        + (vector[a] * (self.cell_width as f64) * (delta * a_f).sin()).round()
+                            as isize;
+                    let y = centre.0 as isize
+                        + (vector[a] * (self.cell_width as f64) * (delta * a_f).cos()).round()
+                            as isize;
+                    draw_line((centre.0 as isize, centre.1 as isize), (y, x), &mut cell);
                 }
             }
         }
@@ -161,7 +165,8 @@ impl HistogramOfGradientsExtractor {
         for window in histograms.windows((self.block_width, self.block_width, self.orientations)) {
             // turn window into block and normalise
             // L2 norm by default
-            let norm = (window.iter().map(|x| x.powi(2)).sum::<f64>() + f64::EPSILON.powi(2)).sqrt();
+            let norm =
+                (window.iter().map(|x| x.powi(2)).sum::<f64>() + f64::EPSILON.powi(2)).sqrt();
             let mut block = window.iter().map(|v| v / norm).collect::<Vec<f64>>();
             feature_vector.append(&mut block);
         }
diff --git a/src/processing/conv.rs b/src/processing/conv.rs
index e11f4b8..edf175f 100644
--- a/src/processing/conv.rs
+++ b/src/processing/conv.rs
@@ -296,7 +296,7 @@ mod tests {
         let input = Image::<u8, Gray>::from_shape_data(5, 5, input_pixels);
         let expected = Image::<u8, Gray>::from_shape_data(5, 5, output_pixels);
 
-        assert_eq!(Ok(expected), input.conv2d(kern.view()));
+        assert_eq!(Ok(expected), input.conv2d_with_padding(kern.view(), &NoPadding {}));
     }
 
     #[test]

From 4957bf5e92972128b5cff5e8fc58668f9a9a22bd Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Sat, 20 Jun 2020 23:13:41 +0100
Subject: [PATCH 11/17] Implement visualiser

Got all the blocks, got the vectors now just need the features to be all good so I can make sure it actually works
---
 src/features/hog.rs | 94 ++++++++++++++++++++++++++++-----------------
 1 file changed, 58 insertions(+), 36 deletions(-)

diff --git a/src/features/hog.rs b/src/features/hog.rs
index 06857db..cbf8cd3 100644
--- a/src/features/hog.rs
+++ b/src/features/hog.rs
@@ -67,7 +67,12 @@ impl HistogramOfGradientsBuilder {
     }
 }
 
-fn draw_line(start: (isize, isize), end: (isize, isize), image: &mut ArrayViewMut2<u8>) {
+fn draw_line(
+    start: (isize, isize),
+    end: (isize, isize),
+    colour: u8,
+    image: &mut ArrayViewMut2<u8>,
+) {
     let dim = image.dim();
     let dim = (dim.0 as isize, dim.1 as isize);
     let deltax = (end.1 - start.1) as f64;
@@ -89,7 +94,7 @@ fn draw_line(start: (isize, isize), end: (isize, isize), image: &mut ArrayViewMu
     } else {
         for y in min(start.0, end.0)..max(start.0, end.0) {
             if start.1 > 0 && y > 0 && start.1 < dim.1 && y < dim.0 {
-                image[[y as usize, start.1 as usize]] = 255;
+                image[[y as usize, start.1 as usize]] = colour;
             }
         }
     }
@@ -111,42 +116,59 @@ impl HistogramOfGradientsExtractor {
         let cell_len = self.block_descriptor_len();
         let centre = (self.cell_width / 2, self.cell_width / 2);
         let delta = self.angle_delta();
-        for row in 0..cells.0 {
-            for col in 0..cells.1 {
-                let start = row * cells.0 + col;
-                let mut vector = features
-                    .into_iter()
-                    .skip(start * cell_len)
-                    .take(self.orientations)
-                    .copied()
-                    .collect::<Vec<_>>();
-                if vector.is_empty() {
-                    println!("This shouldn't happen if it's working...");
-                    break;
-                }
-                let norm: f64 = vector.iter().sum::<f64>() + f64::EPSILON;
-                vector.iter_mut().for_each(|x| *x /= norm);
-
-                let mut cell = result.data.slice_mut(s![
-                    (row * self.cell_width)..((row + 1) * self.cell_width),
-                    (col * self.cell_width)..((col + 1) * self.cell_width),
-                    0
-                ]);
-                cell[centre] = 255;
-                // From the features get circle coordinate x = r*sin(theta) y = r*cos(theta)
-                // theta is angle of the bin, r is normalised magnitude * cell_width
-                // Draw a white line from end coordinate to origin
-                for a in 0..self.orientations {
-                    let a_f = a as f64;
-                    let x = centre.1 as isize
-                        + (vector[a] * (self.cell_width as f64) * (delta * a_f).sin()).round()
-                            as isize;
-                    let y = centre.0 as isize
-                        + (vector[a] * (self.cell_width as f64) * (delta * a_f).cos()).round()
-                            as isize;
-                    draw_line((centre.0 as isize, centre.1 as isize), (y, x), &mut cell);
+        let mut tl = (0, 0);
+        for chunk in features.exact_chunks(cell_len) {
+            if tl.1 > (cells.1 - self.block_width) {
+                tl.1 = 0;
+                tl.0 += 1;
+            }
+            let mut covered = 0;
+            for row in tl.0..(tl.0 + self.block_width) {
+                for col in tl.1..(tl.1 + self.block_width) {
+                    let mut cell = result.data.slice_mut(s![
+                        (row * self.cell_width)..((row + 1) * self.cell_width),
+                        (col * self.cell_width)..((col + 1) * self.cell_width),
+                        0
+                    ]);
+                    if cell[centre] == 255 {
+                        covered += self.orientations;
+                        continue;
+                    }
+
+                    let mut vector = chunk
+                        .iter()
+                        .skip(covered)
+                        .take(self.orientations)
+                        .copied()
+                        .collect::<Vec<_>>();
+                    covered += self.orientations;
+                    if vector.is_empty() {
+                        break;
+                    }
+                    let norm: f64 = vector.iter().sum::<f64>() + f64::EPSILON;
+                    vector.iter_mut().for_each(|x| *x /= norm);
+
+                    // From the features get circle coordinate x = r*sin(theta) y = r*cos(theta)
+                    // theta is angle of the bin, r is normalised magnitude * cell_width
+                    // Draw a white line from end coordinate to origin
+                    for a in 0..self.orientations {
+                        let a_f = a as f64;
+                        let x = centre.1 as isize
+                            + ((self.cell_width as f64) * (delta * a_f).sin()).round() as isize;
+                        let y = centre.0 as isize
+                            + ((self.cell_width as f64) * (delta * a_f).cos()).round() as isize;
+                        let colour = (vector[a] * 255.0).round() as u8;
+                        draw_line(
+                            (centre.0 as isize, centre.1 as isize),
+                            (y, x),
+                            colour,
+                            &mut cell,
+                        );
+                    }
+                    cell[centre] = 255;
                 }
             }
+            tl.1 += 1;
         }
         result
     }

From f498f1c93b429fba19408c8401cd4c45d37a982e Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Thu, 2 Jul 2020 22:07:19 +0100
Subject: [PATCH 12/17] Remove unnecessary epsilon

---
 src/processing/sobel.rs | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/processing/sobel.rs b/src/processing/sobel.rs
index 49b002c..8fff5ea 100644
--- a/src/processing/sobel.rs
+++ b/src/processing/sobel.rs
@@ -74,8 +74,7 @@ where
     }
 
     fn full_sobel(&self) -> Result<(Self::Output, Self::Output), Error> {
-        let (mut h_deriv, v_deriv) = get_edge_images(self)?;
-        h_deriv.mapv_inplace(|x| x + T::epsilon());
+        let (h_deriv, v_deriv) = get_edge_images(self)?;
         let mut magnitude = h_deriv.mapv(|x| x.powi(2)) + v_deriv.mapv(|x| x.powi(2));
         magnitude.mapv_inplace(|x| x.sqrt());
 

From 51ef40918f07580dd92abfbeccd1ba7738011e70 Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Sat, 21 Nov 2020 16:22:19 +0000
Subject: [PATCH 13/17] Minor changes

---
 CHANGELOG.md            | 1 +
 src/processing/sobel.rs | 2 ++
 2 files changed, 3 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30fdb26..ed5a836 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ## [Unreleased]
 ### Added
+- HoG feature extractor
 
 ### Changed
 - Fixed orientation of sobel filters
diff --git a/src/processing/sobel.rs b/src/processing/sobel.rs
index 8fff5ea..cdaaaa4 100644
--- a/src/processing/sobel.rs
+++ b/src/processing/sobel.rs
@@ -160,4 +160,6 @@ mod tests {
 
         assert_abs_diff_eq!(sobel.1.data.slice(s![1..10, 1..10, 0]), rot, epsilon = 1e-5);
     }
+    
+    
 }

From 73ddf3ff7941d649ea3e3544b485e8a5562642fd Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Sun, 18 Apr 2021 23:24:27 +0400
Subject: [PATCH 14/17] move features behind a feature

---
 Cargo.toml | 3 ++-
 src/lib.rs | 1 +
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/Cargo.toml b/Cargo.toml
index 03176f7..53ca064 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,8 +11,9 @@ categories = ["science", "science::robotics", "multimedia", "multimedia::images"
 edition = "2018"
 
 [features]
-default = ["enhancement", "format", "morphology", "processing", "transform"]
+default = ["enhancement", "features", "format", "morphology", "processing", "transform"]
 enhancement = []
+features = ["processing"]
 format = []
 morphology = []
 processing = []
diff --git a/src/lib.rs b/src/lib.rs
index e42f339..62d12a9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -31,6 +31,7 @@ pub mod core;
 #[cfg(feature = "enhancement")]
 pub mod enhancement;
 /// Feature extraction
+#[cfg(feature="features")]
 pub mod features;
 /// Image formats - encoding and decoding images from bytes for saving and
 /// loading

From 203dd6743d9e6c53131c97b7517c24f1bf64366b Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Mon, 19 Apr 2021 00:13:27 +0400
Subject: [PATCH 15/17] Move to github actions

---
 .github/workflows/ci.yml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 994460a..3ed4348 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,11 +26,11 @@ jobs:
           components: rustfmt
       - name: build
         run: |
-          cargo check --no-default-features
-          cargo check --all-features
+          cargo install cargo-all-features
+          cargo check-all-features
       - name: test
         run: |
-          cargo test 
+          cargo test --all-features
       - name: check formatting
         run: cargo fmt -- --check
       - name: code-coverage

From 4ef8c2152e6239486bf58acb88ee9d147b4bf3a1 Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Mon, 19 Apr 2021 08:52:52 +0400
Subject: [PATCH 16/17] Run fmt

---
 src/lib.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/lib.rs b/src/lib.rs
index 62d12a9..e741882 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -31,7 +31,7 @@ pub mod core;
 #[cfg(feature = "enhancement")]
 pub mod enhancement;
 /// Feature extraction
-#[cfg(feature="features")]
+#[cfg(feature = "features")]
 pub mod features;
 /// Image formats - encoding and decoding images from bytes for saving and
 /// loading

From 60e70241d6da78c241f4baf7fbb9b9cdb81ac2be Mon Sep 17 00:00:00 2001
From: xd009642 <danielmckenna93@gmail.com>
Date: Sun, 1 Aug 2021 09:36:19 +0100
Subject: [PATCH 17/17] Rename to Hog instead of HistogramOfGradients

---
 examples/hog.rs     | 40 ++++++++++++++++++++++++++++++++++++++++
 src/features/hog.rs | 14 +++++++-------
 2 files changed, 47 insertions(+), 7 deletions(-)
 create mode 100644 examples/hog.rs

diff --git a/examples/hog.rs b/examples/hog.rs
new file mode 100644
index 0000000..caa7a5e
--- /dev/null
+++ b/examples/hog.rs
@@ -0,0 +1,40 @@
+use ndarray_vision::core::*;
+use ndarray_vision::format::netpbm::*;
+use ndarray_vision::format::*;
+use ndarray_vision::features::*;
+use std::env::current_exe;
+use std::path::PathBuf;
+
+fn hog_features(img: &Image<f64, Gray>) -> Image<u8, Gray> {
+    let hog = HogExtractor::create()
+        .build();
+
+    let features = hog.get_features(img);
+    hog.visualise_features((img.rows(), img.cols()), features.view())
+}
+
+fn main() {
+    if let Ok(mut root) = current_exe() {
+        root.pop();
+        root.pop();
+        root.pop();
+        root.pop();
+        let mut lena = PathBuf::from(&root);
+        lena.push("images/lena.ppm");
+
+        let decoder = PpmDecoder::default();
+        let image: Image<u8, _> = decoder.decode_file(lena).expect("Couldn't open Lena.ppm");
+
+        let image: Image<f64, _> = image.into_type();
+        let image: Image<_, Gray> = image.into();
+
+        let hog = hog_features(&image);
+
+        let mut lena = PathBuf::from(&root);
+        lena.push("images/lena-hog.ppm");
+        let ppm = PpmEncoder::new_plaintext_encoder();
+        ppm.encode_file(&hog.into(), lena)
+            .expect("Unable to encode ppm");
+    }
+}
+
diff --git a/src/features/hog.rs b/src/features/hog.rs
index cbf8cd3..5150c0c 100644
--- a/src/features/hog.rs
+++ b/src/features/hog.rs
@@ -13,7 +13,7 @@ pub struct Gradient {
 }
 
 #[derive(Default)]
-pub struct HistogramOfGradientsBuilder {
+pub struct HogBuilder {
     /// Number of orientations in the historgram
     orientations: Option<usize>,
     /// Number of pixels in a cell
@@ -23,14 +23,14 @@ pub struct HistogramOfGradientsBuilder {
 }
 
 /// Extract the HoG descriptor from an image and optionally visualise it
-pub struct HistogramOfGradientsExtractor {
+pub struct HogExtractor {
     orientations: usize,
     cell_width: usize,
     block_width: usize,
 }
 
-impl HistogramOfGradientsBuilder {
-    pub fn build(self) -> HistogramOfGradientsExtractor {
+impl HogBuilder {
+    pub fn build(self) -> HogExtractor {
         let orientations = match self.orientations {
             Some(o) => o,
             None => 9,
@@ -44,7 +44,7 @@ impl HistogramOfGradientsBuilder {
             None => 2,
         };
 
-        HistogramOfGradientsExtractor {
+        HogExtractor {
             orientations,
             cell_width,
             block_width,
@@ -100,9 +100,9 @@ fn draw_line(
     }
 }
 
-impl HistogramOfGradientsExtractor {
+impl HogExtractor {
     /// Start creating a hog extractor
-    pub fn create() -> HistogramOfGradientsBuilder {
+    pub fn create() -> HogBuilder {
         Default::default()
     }
 
