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
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/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/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/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
new file mode 100644
index 0000000..5150c0c
--- /dev/null
+++ b/src/features/hog.rs
@@ -0,0 +1,258 @@
+use crate::core::*;
+use crate::processing::SobelExt;
+use ndarray::{prelude::*, s, DataMut};
+use std::cmp::{max, min};
+use std::f64::consts::PI;
+
+#[derive(Debug, Clone)]
+pub struct Gradient {
+    /// Magnitude of the gradient
+    magnitude: Array3<f64>,
+    /// Angle of the gradient
+    angle: Array3<f64>,
+}
+
+#[derive(Default)]
+pub struct HogBuilder {
+    /// 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>,
+}
+
+/// Extract the HoG descriptor from an image and optionally visualise it
+pub struct HogExtractor {
+    orientations: usize,
+    cell_width: usize,
+    block_width: usize,
+}
+
+impl HogBuilder {
+    pub fn build(self) -> HogExtractor {
+        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,
+        };
+
+        HogExtractor {
+            orientations,
+            cell_width,
+            block_width,
+        }
+    }
+
+    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
+    }
+}
+
+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;
+    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]] = colour;
+            }
+        }
+    }
+}
+
+impl HogExtractor {
+    /// Start creating a hog extractor
+    pub fn create() -> HogBuilder {
+        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();
+        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
+    }
+
+    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 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
+            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);
+        }
+
+        feature_vector.into()
+    }
+
+    fn block_descriptor_len(&self) -> usize {
+        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;
+        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()
+    }
+
+    fn create_histograms(&self, rows: usize, cols: usize, grad: &Gradient) -> Array3<f64> {
+        let (v_cells, h_cells) = self.cell_dim((rows, cols));
+        let mut result = Array3::zeros((v_cells, h_cells, self.orientations));
+
+        let delta = self.angle_delta();
+        // 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, 0])
+                    .iter()
+                    .zip(
+                        grad.magnitude
+                            .slice(s![r_start..r_end, c_start..c_end, 0])
+                            .iter(),
+                    )
+                {
+                    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 += vote_1;
+                    }
+                    let bucket_2 = (bucket + 1) % self.orientations;
+                    if let Some(v) = result.get_mut((r, c, bucket_2)) {
+                        *v += vote_2;
+                    }
+                }
+            }
+        }
+        result
+    }
+}
diff --git a/src/features/mod.rs b/src/features/mod.rs
new file mode 100644
index 0000000..0f24de7
--- /dev/null
+++ b/src/features/mod.rs
@@ -0,0 +1,3 @@
+pub mod hog;
+
+pub use hog::*;
diff --git a/src/lib.rs b/src/lib.rs
index 538eaa1..e741882 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -30,6 +30,9 @@ pub mod core;
 /// Image enhancement intrinsics and algorithms
 #[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
 #[cfg(feature = "format")]
diff --git a/src/processing/conv.rs b/src/processing/conv.rs
index fd34f71..edf175f 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(())
     }
 
@@ -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]
diff --git a/src/processing/sobel.rs b/src/processing/sobel.rs
index 3cdbc22..cdaaaa4 100644
--- a/src/processing/sobel.rs
+++ b/src/processing/sobel.rs
@@ -75,7 +75,6 @@ where
 
     fn full_sobel(&self) -> Result<(Self::Output, Self::Output), Error> {
         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());
 
@@ -161,4 +160,6 @@ mod tests {
 
         assert_abs_diff_eq!(sobel.1.data.slice(s![1..10, 1..10, 0]), rot, epsilon = 1e-5);
     }
+    
+    
 }
