Introduction

In 2005 Navneet Dalal and Bill Triggs asked a deceptively simple question: what single number best describes the local shape of an image patch? Their answer — count the orientations of edges inside tiny grid cells — produced the Histogram of Oriented Gradients (HOG) descriptor, and with it the first reliable pedestrian detector that worked in the wild.

The idea starts with gradients. At each pixel the image brightness changes slightly as you move left-to-right or top-to-bottom. Those two numbers — the horizontal and vertical change — define an edge direction (an angle from 0°0° to 180°180°) and a magnitude (how strong the edge is). HOG ignores the exact pixel values and instead bins the angles, weighted by magnitude, inside each small cell of the image.

The resulting histogram — typically 9 bins covering 0°0° to 180°180° in 20°20° steps — is a compact fingerprint of the local shape. Stack the histograms of all cells across a detection window, normalize them in overlapping blocks to handle lighting changes, and you have a vector of a few thousand numbers that is stable, fast to compute, and remarkably effective at telling humans from backgrounds.

Try It: Gradient Cells

Draw on the canvas below — any stroke, letter, or shape. The grid overlaid on the canvas is the HOG cell grid. Inside each cell the demo computes the gradient at every pixel, bins the angle into one of 9 orientation buckets, and draws a small oriented line proportional to the total weight in that bucket.

<!-- HOG Descriptor demo — draw on canvas, see gradient cell arrows -->
<p class="hint">Draw anything on the canvas. Each grid cell shows oriented lines (HOG arrows) weighted by the gradient magnitude inside it — stronger edges produce longer arrows.</p>
<div class="controls">
  <label for="brushSize">Brush size:</label>
  <input type="range" id="brushSize" min="4" max="24" value="10" title="Adjust brush size">
  <button id="clearBtn" type="button">Clear</button>
  <button id="showGrad" type="button">Hide HOG</button>
</div>
<div class="canvas-wrap">
  <canvas id="drawCanvas" width="288" height="288" aria-label="Drawing canvas — draw here to create edges"></canvas>
  <canvas id="hogCanvas"  width="288" height="288" aria-label="HOG overlay — shows gradient orientation arrows per cell"></canvas>
</div>
<p class="status" id="status">Draw on the canvas to see HOG cells fill with gradient arrows.</p>
/* Layout for the HOG descriptor demo */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; }
.hint { font-size: .9rem; color: #444; margin-bottom: .6rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; }
.controls label { font-size: .85rem; color: #555; white-space: nowrap; }
.controls input[type=range] { width: 80px; cursor: pointer; }
button { font: 600 13px system-ui; padding: .4rem .8rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button#showGrad { background: #fff; color: #1d3557; }
button#showGrad.active { background: #1d3557; color: #fff; }
.canvas-wrap { position: relative; display: inline-block; border: 1px solid #cdd9e3; border-radius: 6px; overflow: hidden; }
canvas { display: block; }
#hogCanvas { position: absolute; top: 0; left: 0; pointer-events: none; }
.status { font-size: .9rem; color: #555; margin-top: .5rem; min-height: 1.3em; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// HOG parameters
const CELL = 32;        // pixels per cell (cell size)
const BINS = 9;         // number of unsigned orientation bins (0–180 degrees)
const BIN_ANGLE = 180 / BINS; // degrees per bin

const drawCanvas = document.getElementById('drawCanvas');
const hogCanvas  = document.getElementById('hogCanvas');
const dCtx = drawCanvas.getContext('2d');
const hCtx = hogCanvas.getContext('2d');
const W = drawCanvas.width;
const H = drawCanvas.height;
const COLS = W / CELL;
const ROWS = H / CELL;

// Fill canvas with a light background
dCtx.fillStyle = '#f5f7fa';
dCtx.fillRect(0, 0, W, H);

let drawing = false;
let showHog = true;

function getBrush() { return parseInt(document.getElementById('brushSize').value, 10); }

// Drawing event handlers — track mouse and touch
function getPos(e) {
  const r = drawCanvas.getBoundingClientRect();
  const src = e.touches ? e.touches[0] : e;
  return { x: src.clientX - r.left, y: src.clientY - r.top };
}
function startDraw(e) { drawing = true; const p = getPos(e); dCtx.beginPath(); dCtx.moveTo(p.x, p.y); e.preventDefault(); }
function moveDraw(e) {
  if (!drawing) return;
  const p = getPos(e);
  dCtx.lineWidth = getBrush();
  dCtx.lineCap = 'round';
  dCtx.strokeStyle = '#1d2a38';
  dCtx.lineTo(p.x, p.y);
  dCtx.stroke();
  dCtx.beginPath();
  dCtx.moveTo(p.x, p.y);
  if (showHog) computeHog();
  e.preventDefault();
}
function endDraw() { drawing = false; if (showHog) computeHog(); }

drawCanvas.addEventListener('mousedown', startDraw);
drawCanvas.addEventListener('mousemove', moveDraw);
drawCanvas.addEventListener('mouseup',   endDraw);
drawCanvas.addEventListener('mouseleave',endDraw);
drawCanvas.addEventListener('touchstart', startDraw, { passive: false });
drawCanvas.addEventListener('touchmove',  moveDraw,  { passive: false });
drawCanvas.addEventListener('touchend',   endDraw);

// Clear button resets canvas and overlay
document.getElementById('clearBtn').addEventListener('click', () => {
  dCtx.fillStyle = '#f5f7fa';
  dCtx.fillRect(0, 0, W, H);
  hCtx.clearRect(0, 0, W, H);
  document.getElementById('status').textContent = 'Draw on the canvas to see HOG cells fill with gradient arrows.';
});

// Toggle the HOG overlay on or off
const showBtn = document.getElementById('showGrad');
showBtn.addEventListener('click', () => {
  showHog = !showHog;
  showBtn.classList.toggle('active', showHog);
  showBtn.textContent = showHog ? 'Hide HOG' : 'Show HOG';
  if (showHog) computeHog(); else hCtx.clearRect(0, 0, W, H);
});

// Compute and draw HOG overlay for the entire canvas
function computeHog() {
  const img = dCtx.getImageData(0, 0, W, H).data;
  // Convert an (x,y) pixel to grayscale brightness
  function gray(x, y) {
    const i = (y * W + x) * 4;
    return 0.299 * img[i] + 0.587 * img[i+1] + 0.114 * img[i+2];
  }
  // Compute gradient magnitude and unsigned orientation at (x,y)
  function grad(x, y) {
    const gx = (x > 0 && x < W-1) ? gray(x+1, y) - gray(x-1, y) : 0;
    const gy = (y > 0 && y < H-1) ? gray(x, y+1) - gray(x, y-1) : 0;
    const mag = Math.sqrt(gx*gx + gy*gy);
    let ang = (Math.atan2(Math.abs(gy), gx) * 180 / Math.PI) % 180;
    if (ang < 0) ang += 180;
    return { mag, ang };
  }

  hCtx.clearRect(0, 0, W, H);
  let totalMag = 0;
  let activeCells = 0;

  // Loop over each HOG cell in the grid
  for (let row = 0; row < ROWS; row++) {
    for (let col = 0; col < COLS; col++) {
      const hist = new Float32Array(BINS);
      for (let dy = 0; dy < CELL; dy++) {
        for (let dx = 0; dx < CELL; dx++) {
          const px = col * CELL + dx;
          const py = row * CELL + dy;
          const { mag, ang } = grad(px, py);
          // Accumulate gradient magnitude into the nearest orientation bin
          const bin = Math.floor(ang / BIN_ANGLE) % BINS;
          hist[bin] += mag;
        }
      }
      // Find the peak bin to normalize arrow lengths
      const maxH = Math.max(...hist);
      totalMag += maxH;
      if (maxH < 8) continue;
      activeCells++;

      const cx = col * CELL + CELL / 2;
      const cy = row * CELL + CELL / 2;
      const scale = (CELL * 0.45) / maxH;

      // Draw one oriented line per bin, scaled by its weight
      for (let b = 0; b < BINS; b++) {
        if (hist[b] < 2) continue;
        const angle = (b * BIN_ANGLE + BIN_ANGLE / 2) * Math.PI / 180;
        const len = hist[b] * scale;
        const dx = Math.cos(angle) * len;
        const dy = Math.sin(angle) * len;
        const alpha = Math.min(1, hist[b] / maxH);
        hCtx.strokeStyle = `rgba(220,50,50,${alpha.toFixed(2)})`;
        hCtx.lineWidth = 1.8;
        hCtx.lineCap = 'round';
        hCtx.beginPath();
        hCtx.moveTo(cx - dx, cy - dy);
        hCtx.lineTo(cx + dx, cy + dy);
        hCtx.stroke();
      }
    }
  }

  // Draw the cell grid overlay in light blue
  hCtx.strokeStyle = 'rgba(100,130,160,0.25)';
  hCtx.lineWidth = 0.8;
  for (let r = 0; r <= ROWS; r++) { hCtx.beginPath(); hCtx.moveTo(0, r*CELL); hCtx.lineTo(W, r*CELL); hCtx.stroke(); }
  for (let c = 0; c <= COLS; c++) { hCtx.beginPath(); hCtx.moveTo(c*CELL, 0); hCtx.lineTo(c*CELL, H); hCtx.stroke(); }

  const statusEl = document.getElementById('status');
  if (activeCells === 0) {
    statusEl.textContent = 'Draw on the canvas to see HOG cells fill with gradient arrows.';
  } else {
    statusEl.textContent = i18nFmt('%n% active cells with gradient signal detected.', { n: activeCells });
  }
}

// Initial HOG pass on blank canvas (no arrows visible until drawing)
computeHog();
Result
Console

Notice how straight lines produce a single strong arrow aligned with the edge, while curves fill multiple buckets. Blank regions produce no arrows at all. That is exactly what HOG sends to a classifier: not pixels, but a compact summary of which directions have edges and how strongly.

The Real Complexity

HOG is a solved feature-engineering problem — a deliberate design rather than an open question. Understanding its tradeoffs explains both why it worked so well and why it was eventually replaced.

  • Computation. For a detection window of W×HW \times H pixels, divided into C×CC \times C pixel cells and B×BB \times B cell blocks, the descriptor length is roughly WC⋅HC⋅B2⋅k\frac{W}{C} \cdot \frac{H}{C} \cdot B^2 \cdot k where k=9k = 9 is the number of orientation bins. A typical 64×12864 \times 128 window with 8×88 \times 8 cells and 2×22 \times 2 block normalization yields a 3780-dimensional vector — computed in milliseconds with integral images.
  • Why it works. Gradients respond to shape boundaries regardless of color or slow lighting variation. Normalizing overlapping blocks makes the descriptor robust to local contrast changes. The orientation bins are unsigned (0°0°–180°180°), so a left-to-right edge and a right-to-left edge fall in the same bin — capturing shape without being confused by contrast polarity.
  • Status: solved (2005). Dalal and Triggs combined HOG with a linear Support Vector Machine and won the PASCAL VOC pedestrian challenge by a large margin. The design choices — cell size, block overlap, ℓ2\ell_2-norm block normalization — were validated exhaustively and have not been significantly bettered for hand-crafted descriptors.
  • Limits. HOG cannot handle large viewpoint changes or occlusions, and the sliding-window detection pipeline scales poorly with image size. Deep convolutional networks learn features similar to HOG in their first layers but then build richer representations that surpass it on every benchmark.

HOG sits at the intersection of signal processing and machine learning: a closed-form descriptor that encodes enough shape information to be paired with a simple classifier and still beat the state of the art for nearly a decade.

Where It Matters

Oriented gradient histograms appear wherever local shape must be described efficiently:

  • Pedestrian and vehicle detection: HOG + SVM was the engine of early automotive-safety cameras and surveillance systems before deep learning took over.
  • Action recognition: stacking HOG across video frames gives a descriptor (HOF/MBH) that captures how limbs move even at low resolution.
  • Document layout analysis: text regions, tables, and figures produce distinctive orientation patterns that HOG-based classifiers can locate.
  • Medical imaging: bone contours and organ boundaries have strong gradient signals; HOG features feed classical classifiers in low-data settings where a neural network would overfit.
  • Texture description: the SIFT keypoint descriptor — the backbone of feature matching in panorama stitching and augmented reality — is essentially a HOG computed around salient points.

Even now that deep networks dominate, HOG remains the canonical example of hand-crafted feature engineering: a case where carefully chosen math outperformed raw pixels for a decade and still illuminates why the first layers of a CNN look the way they do.

Conclusion

The HOG descriptor is a beautiful example of insight over brute force: instead of memorizing pixel intensities, it counts how many edges point in each direction, cell by cell, and assembles those counts into a shape fingerprint.

That fingerprint was good enough to solve pedestrian detection in 2005, to inspire SIFT and its descendants, and to reveal — once neural networks arrived — that the first thing a deep model learns is a bank of gradient detectors strikingly similar to HOG cells.

The algorithm is solved, the math is clean, and the lesson endures: understand what varies (pixels, lighting, color) and what stays stable (edge orientations), measure only the stable part, and a simple classifier will do the rest.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/hog-descriptor/Content licensed under CC BY-NC 4.0.