Features & Representation
Histogram of Oriented Gradients
A dense descriptor that summarizes an image window by histograms of gradient orientations in small cells, contrast-normalized over overlapping blocks; how it is computed, why its parameters are what they are, and how its variants and implementations differ.
intermediate
The histogram of oriented gradients (HOG) is a feature descriptor that represents an image window by the distribution of its gradient orientations. The window is divided into small cells, each cell accumulates a histogram of gradient orientations weighted by gradient magnitude, and the histograms are contrast-normalized over larger, overlapping blocks of cells. The concatenated block vectors form one long, fixed-length vector that a linear classifier can score. Dalal and Triggs introduced it in 2005 for detecting upright people, and HOG-based detectors, deformable part models among them, remained the reference for window-based object detection until convolutional networks overtook them. The name is also written “histograms of oriented gradients”, and the vector is often called the HOG descriptor or HOG features.
HOG shares its main ingredient with SIFT: a grid of orientation histograms. It uses the ingredient differently. SIFT describes a sparse set of keypoints, each in its own rotated and scaled frame; HOG describes a whole window densely, at a single scale and without rotating anything, so that the position of each histogram within the window is part of the code.
Problem
A window classifier for a class such as “standing person” needs a representation in which all people look alike and differ from the background. Raw pixels fail at both: clothing, lighting and background change every intensity, and a person who moves a limb by a few pixels changes many of them. What stays constant is the silhouette: a head-and-shoulders outline, a vertical torso, two roughly vertical legs. The silhouette shows up as strong gradients at fairly predictable places in the window, but their exact positions vary from person to person, and their strength and sign depend on the contrast between the person and whatever is behind them.
Keypoint descriptors do not solve this. Dalal and Triggs argued that keypoint detectors do not reliably fire on human body structures, so a representation built on keypoints misses the object before the descriptor is computed. HOG avoids keypoint detection altogether: it describes every cell of the window, and leaves it to the classifier to learn which cells matter.
Inputs and Outputs
The input is a window of pixels, grayscale or colour. Dalal and Triggs’s person window is , with about 16 pixels of margin around the person on all four sides. Intensities may be in or ; normalization makes the descriptor almost independent of the scale.
The output is a vector of fixed length . With Dalal and Triggs’s default parameters, pixel cells, -cell blocks at a stride of 8 pixels, and 9 orientation bins, a window gives 105 blocks of 36 values each, so .
Detectors often compute HOG once for a whole image rather than once per window: a feature map holding a histogram (or normalized block vector) for every cell, from which the vector of any window aligned to the cell grid is read off. The deformable part model of Felzenszwalb et al. works on such a map directly.
Intuition
Three ideas make the descriptor.
Gradients, not intensities. A gradient records where intensity changes and in which direction. It is unaffected by adding a constant to the image, and the outline of a person produces gradients whether the person is darker or lighter than the background.
Pool position coarsely, orientation finely. Within a cell of pixels, HOG records only how much gradient energy points in each direction, not where in the cell it lies. A leg that moves by a few pixels stays in the same cell, or spreads its votes over neighbouring cells, and the descriptor changes little. Orientation, by contrast, is binned finely, because the direction of an edge (vertical leg, sloping shoulder, horizontal foot) is what separates a person from clutter. Dalal and Triggs summed up their findings as fine orientation sampling and relatively coarse spatial sampling.
Normalize locally, and more than once. The gradient strength of a contour depends on illumination and on how much the person stands out from the local background, which varies over the window. HOG therefore normalizes each block of cells separately, so that a weak contour in a dim region carries as much weight as a strong one in a bright region. Blocks overlap, so each cell is normalized several times, relative to each of its neighbourhoods, and each normalized copy enters the descriptor. Dalal and Triggs found that this redundancy improved detection. One way to see why: a cell between a strong edge on one side and a flat region on the other is described both relative to the edge and relative to the flat region.
Algorithm
The steps below follow Dalal and Triggs’s default detector. Each is discussed further under Parameters.
- Optional global normalization. Compress the intensity range, for example with a square root of each colour channel (gamma compression). The default detector used none.
- Gradient computation. Filter the image with the centred mask horizontally and its transpose vertically, with no smoothing beforehand. For colour images, compute gradients in each channel and keep, at each pixel, the one with the largest magnitude.
- Orientation binning. Divide the window into cells of pixels. Each pixel votes for the orientation bin of its gradient with a weight equal to the gradient magnitude. Orientations are unsigned (0° to 180°) and split into 9 bins of 20°. Votes are interpolated linearly between the two nearest orientation bins and bilinearly between the four nearest cells, and pixels are first weighted by a Gaussian window centred on their block, with equal to half the block width.
- Block normalization. Group the cells into blocks of cells ( pixels), with a stride of one cell (8 pixels), so that blocks overlap by half and each interior cell belongs to four blocks. Concatenate each block’s four histograms into a 36-dimensional vector and normalize it, by default with L2-Hys (below).
- Concatenation. Join the normalized vectors of all blocks in the window into the descriptor.
In Dalal and Triggs’s detector the descriptor is scored by a linear support vector machine trained on positive windows and on negatives that include hard examples mined with a first version of the detector. The window is scanned over all positions and scales of an image, as described in sliding window detection, and overlapping detections are merged by non-maximum suppression. Dalal and Triggs collected the INRIA person dataset to train and test it.
Mathematical Formulation
Gradients and orientation
For a grayscale image (or the selected colour channel), with along columns and down rows,
The signed variant keeps . Dropping the conventional factor from the central difference scales every magnitude by the same amount and is undone by normalization.
Interpolated voting
With bins over the range ( or ), bin is centred at . The continuous bin coordinate of a pixel is ; with , the pixel gives the fraction of its vote to bin and to bin , both taken modulo because orientation wraps around. Positions are treated the same way: a pixel at has cell coordinates for cell size , and splits its vote bilinearly among the four cells whose centres surround it. The histogram of cell is then
where and are the orientation and spatial interpolation weights of pixel , and is the Gaussian block window (which makes a cell’s histogram depend on the block it is computed for). Interpolation removes the jumps that hard binning causes when an edge rotates across a bin boundary or slides across a cell boundary; Dalal and Triggs used it to reduce aliasing.
Block normalization
Let be the concatenated histograms of one block and a small constant. Dalal and Triggs compared four schemes:
L2-Hys applies L2, clips every component at 0.2 and applies L2 again, the clipping Lowe used for SIFT; scikit-image’s documentation expands “Hys” as hysteresis. The clip stops a single very strong edge from dominating the block, so the relative pattern of the weaker gradients survives. Dalal and Triggs found L2, L2-Hys and L1-sqrt about equally good, plain L1 worse, and no normalization much worse. They also note that is needed because blocks in empty regions have , and that results were insensitive to its value over a large range.
L1-sqrt has a probabilistic reading that Dalal and Triggs pointed out: if and are L1-normalized histograms, the square-rooted vectors have unit length and their dot product is the Bhattacharyya coefficient between the two distributions, so for
Euclidean distances between L1-sqrt vectors are therefore proportional to the Hellinger distance between the histograms; RootSIFT, described in the SIFT article, applies the same transformation to SIFT.
Descriptor length
For a window of pixels, cells of pixels, blocks of cells moved by a stride of pixels, and bins,
which for , , and a window gives . With , every cell not on the window border appears in blocks.
What the normalization removes
The following follows from the definitions. An affine intensity change with leaves unchanged and multiplies every , every histogram and every block vector by ; each normalization scheme then returns the same vector, up to the effect of . With , a contrast inversion, every gradient flips direction, moves by , and the unsigned descriptor is unchanged while the signed one is not. A gain that varies across the window is removed only where it is roughly constant over a block; this is the reason for normalizing blocks rather than the whole window. Gamma compression is not covered by these arguments, since it changes the ratios between gradients.
Parameters
| Parameter | Dalal–Triggs default | Effect and reason |
|---|---|---|
| Gradient filter | , no smoothing | Smoothing or larger masks (Sobel, cubic-corrected) hurt; fine edges carry the information |
| Colour | Per-pixel channel with the largest gradient | Grayscale was slightly worse; gamma had a modest effect |
| Orientation range | Unsigned, 0°–180° | For people, clothing and backgrounds make contrast sign uninformative |
| Orientation bins | 9 (20° each) | Gains up to about 9 bins, little beyond |
| Cell size | pixels | 6–8 pixel cells worked best, about the width of a limb in their images |
| Block size | cells ( pixels) | and blocks best; larger blocks adapt less to local contrast, loses spatial information |
| Block stride | 8 pixels (one cell) | Overlap, so each cell is normalized four times |
| Block window | Gaussian, pixels | Down-weights pixels near the block edge, as SIFT does |
| Normalization | L2-Hys, clip at 0.2 | L2, L2-Hys and L1-sqrt similar; L1 worse; none far worse |
| Window | pixels | About 16 pixels of context around the person; less margin hurt |
The defaults were chosen by experiment for upright people at the scale of the INRIA images, and they are not universal. Dalal and Triggs noted that signed gradients help for some other classes, such as cars and motorbikes. Their observation that the best cells were about a limb wide suggests, as a rule of thumb rather than a measured result, choosing cells about as wide as the structures that distinguish the class at the window’s scale. The table states only the direction of each finding; the measured effects are reported in the paper.
Complexity
Computing the gradients and the cell histograms is linear in the number of pixels: a few operations per pixel for the gradient, and up to eight interpolated votes per pixel (two bins times four cells), or one per pixel with hard voting. Normalizing one block costs , and the number of blocks is about for an image of pixels, so a whole image’s block vectors cost .
Scoring a window with a linear classifier costs multiply-adds, 3,780 for the default window. Because windows placed on the block grid share their blocks, a detector computes the block vectors once per pyramid level and reuses them; the linear score of a window is then a sum of per-block dot products, which is a correlation of the block-vector map with the classifier’s weights. OpenCV’s implementation caches block histograms for this reason, and the deformable part model evaluates its filters as correlations over a HOG feature map. The cost of the scan itself, which grows with the number of positions and scales, is analysed in sliding window detection. Dalal and Triggs reported that their linear detector processed a scale-space image, about 4,000 windows, in under a second.
Implementation
The example implements HOG in NumPy and checks it against skimage.feature.hog from scikit-image 0.25.2. Images are synthetic: a stick figure about 92 pixels tall, standing in a window over clutter made of smooth random texture and random bars, with intensities in . The NumPy version supports hard voting (one bin, one cell, as scikit-image does) and trilinear soft voting; it omits the Gaussian block window, so a cell’s histogram is the same in every block.
import numpy as np
from scipy import ndimage
import skimage
from skimage.feature import hog as skimage_hog
H, W = 128, 64 # detection window: 128 rows x 64 columns, intensities in [0, 1]
def draw_segments(img, segments, width, value):
"""Paint anti-aliased thick line segments (r0, c0, r1, c1) with one intensity."""
rr, cc = np.mgrid[0:img.shape[0], 0:img.shape[1]].astype(float)
for r0, c0, r1, c1 in segments:
d = np.array([r1 - r0, c1 - c0])
t = np.clip(((rr - r0) * d[0] + (cc - c0) * d[1]) / max(d @ d, 1e-9), 0, 1)
dist = np.hypot(rr - r0 - t * d[0], cc - c0 - t * d[1])
cover = np.clip(width / 2 - dist + 0.5, 0, 1)
img[:] = img * (1 - cover) + value * cover
return img
def clutter(rng):
"""Smooth random texture plus a few random bars of random intensity."""
img = 0.5 + 1.5 * ndimage.gaussian_filter(rng.standard_normal((H, W)), 6)
for _ in range(6):
r0, c0 = rng.uniform(0, H), rng.uniform(0, W)
a, length = rng.uniform(0, np.pi), rng.uniform(10, 40)
draw_segments(img, [(r0, c0, r0 + length * np.sin(a), c0 + length * np.cos(a))],
rng.uniform(2, 5), rng.uniform(0.2, 0.9))
return np.clip(img, 0, 1)
def person(img, value=0.1, legs=None, shift=(0, 0), pose_seed=7):
"""Upright stick figure about 92 px tall, leaving ~16 px of margin in the window."""
j = np.random.default_rng(pose_seed).normal(0, 3, size=8) # limb jitter (pose)
dr, dc = shift
body = [(36, 32, 70, 32), (40, 32, 62 + j[0], 20 + j[1]), (40, 32, 62 + j[2], 44 + j[3])]
lower = [(70, 32, 110 + j[4], 24 + j[5]), (70, 32, 110 + j[6], 40 + j[7])]
move = lambda segs: [(a + dr, b + dc, c + dr, d + dc) for a, b, c, d in segs]
draw_segments(img, move(body), 6, value)
draw_segments(img, move([(28, 32, 28, 32)]), 14, value) # head
draw_segments(img, move(lower), 6, value if legs is None else legs)
return img
# ---------------------------------------------------------------- HOG in NumPy
def cell_histograms(img, cell=8, bins=9, signed=False, soft=True):
"""Magnitude-weighted orientation histograms on a grid of cell x cell pixels."""
gr, gc = np.zeros_like(img), np.zeros_like(img)
gr[1:-1, :] = img[2:, :] - img[:-2, :] # centred [-1, 0, 1] mask, no smoothing
gc[:, 1:-1] = img[:, 2:] - img[:, :-2]
span = 360.0 if signed else 180.0 # signed or unsigned orientation
nr, nc = img.shape[0] // cell, img.shape[1] // cell
mag = np.hypot(gr, gc)[:nr * cell, :nc * cell]
ang = (np.degrees(np.arctan2(gr, gc)) % span)[:nr * cell, :nc * cell]
rows, cols = np.mgrid[0:nr * cell, 0:nc * cell]
hist = np.zeros((nr, nc, bins))
if not soft: # hard voting: one bin, one cell (what scikit-image does)
b = np.minimum((ang / (span / bins)).astype(int), bins - 1)
np.add.at(hist, (rows // cell, cols // cell, b), mag)
return hist / cell**2
# Soft voting: linear between the two nearest bin centres and bilinear
# between the four nearest cell centres (trilinear interpolation).
u = ang / (span / bins) - 0.5
y, x = (rows + 0.5) / cell - 0.5, (cols + 0.5) / cell - 0.5
b0, r0, c0 = np.floor(u).astype(int), np.floor(y).astype(int), np.floor(x).astype(int)
for db in (0, 1):
wb = (u - b0) if db else 1 - (u - b0)
for dr in (0, 1):
wr = (y - r0) if dr else 1 - (y - r0)
for dc in (0, 1):
wc = (x - c0) if dc else 1 - (x - c0)
r, c = r0 + dr, c0 + dc
ok = (r >= 0) & (r < nr) & (c >= 0) & (c < nc)
np.add.at(hist, (r[ok], c[ok], (b0[ok] + db) % bins), (mag * wb * wr * wc)[ok])
return hist / cell**2
def normalize(v, method, eps=1e-5):
if method == "none":
return v
if method == "L1":
return v / (np.abs(v).sum() + eps)
if method == "L1-sqrt":
return np.sqrt(v / (np.abs(v).sum() + eps))
v = v / np.sqrt((v**2).sum() + eps**2) # L2
if method == "L2-Hys": # clip at 0.2 and renormalize
v = np.minimum(v, 0.2)
v = v / np.sqrt((v**2).sum() + eps**2)
return v
def hog(img, cell=8, block=2, bins=9, norm="L2-Hys", signed=False, soft=True):
h = cell_histograms(img, cell, bins, signed, soft)
if norm == "global": # a single L2 normalization of the whole window
return normalize(h.ravel(), "L2")
nr, nc, _ = h.shape # overlapping blocks of block x block cells, stride one cell
return np.concatenate([normalize(h[r:r + block, c:c + block].ravel(), norm)
for r in range(nr - block + 1) for c in range(nc - block + 1)])
window = person(clutter(np.random.default_rng(0)))
print("scikit-image", skimage.__version__)
for norm in ("L1", "L1-sqrt", "L2", "L2-Hys"):
ours = hog(window, norm=norm, soft=False)
ref = skimage_hog(window, orientations=9, pixels_per_cell=(8, 8),
cells_per_block=(2, 2), block_norm=norm)
print(f"{norm:8s} length {ours.size}, max |NumPy - skimage| = {np.abs(ours - ref).max():.1e}")
print("skimage defaults (3 x 3-cell blocks):", skimage_hog(window).size, "values")
Output:
scikit-image 0.25.2
L1 length 3780, max |NumPy - skimage| = 3.4e-08
L1-sqrt length 3780, max |NumPy - skimage| = 4.2e-08
L2 length 3780, max |NumPy - skimage| = 1.0e-07
L2-Hys length 3780, max |NumPy - skimage| = 4.4e-08
skimage defaults (3 x 3-cell blocks): 6804 values
With hard voting, the NumPy descriptor matches scikit-image’s for all four normalization schemes, element by element and in the same order. The remaining differences, around , come from scikit-image’s compiled histogram routine, which sums each cell’s magnitudes in single precision. Both give the 3,780 values of the formula above. scikit-image’s own defaults use -cell blocks and give 6,804 values for the same window, a reminder that two “HOG features” are comparable only when their parameters are.
The second block continues from the first. It asks what each design choice buys: for five scenes (different clutter and poses), it changes the person’s window in five ways and measures how far the descriptor moves. Each distance is divided by the median distance from the original window to 40 windows of clutter with no person, so a value well below 1 means the changed window is still much closer to the original than background is. The changes are a global affine contrast change, legs drawn barely darker than the background while the rest of the figure stays dark, a contrast inversion, and shifts of the figure by 2 and 4 pixels. The printed numbers are the smallest and largest ratios over the five scenes.
variants = { # descriptor settings; all use 9 unsigned bins unless stated
"no normalization": dict(norm="none"),
"global L2": dict(norm="global"),
"block L1": dict(norm="L1"),
"block L2-Hys": dict(norm="L2-Hys"),
"L2-Hys, hard votes": dict(norm="L2-Hys", soft=False),
"L2-Hys, signed 18": dict(norm="L2-Hys", signed=True, bins=18),
"L2-Hys, 4 px cells": dict(norm="L2-Hys", cell=4),
"L2-Hys, 16 px cells": dict(norm="L2-Hys", cell=16),
}
names = ["contrast", "dim legs", "inverted", "shift 2", "shift 4"]
ratios = {v: [] for v in variants}
for scene in range(5): # five backgrounds and poses
rng = np.random.default_rng(scene)
background = clutter(rng)
make = lambda **kw: person(background.copy(), pose_seed=scene, **kw)
window = make()
changed = [0.3 * window + 0.35, # global affine contrast change
make(legs=0.42), # legs barely darker than the background
1 - window, # contrast inversion
make(shift=(0, 2)), make(shift=(0, 4))] # figure moved right
others = [clutter(rng) for _ in range(40)] # windows with no person
for v, kw in variants.items():
ref = hog(window, **kw)
scale = np.median([np.linalg.norm(hog(o, **kw) - ref) for o in others])
ratios[v].append([np.linalg.norm(hog(c, **kw) - ref) / scale for c in changed])
print(f"{'descriptor':20s}" + "".join(f"{n:>12s}" for n in names))
for v, r in ratios.items():
r = np.array(r)
print(f"{v:20s}" + "".join(f"{lo:.2f}-{hi:.2f}".rjust(12) for lo, hi in zip(r.min(0), r.max(0))))
Output:
descriptor contrast dim legs inverted shift 2 shift 4
no normalization 0.69-0.71 0.41-0.55 0.00-0.00 0.24-0.26 0.45-0.50
global L2 0.00-0.00 0.32-0.43 0.00-0.00 0.19-0.20 0.34-0.39
block L1 0.00-0.00 0.18-0.28 0.00-0.00 0.18-0.20 0.32-0.37
block L2-Hys 0.00-0.00 0.17-0.28 0.00-0.00 0.18-0.21 0.33-0.38
L2-Hys, hard votes 0.00-0.00 0.16-0.31 0.00-0.00 0.28-0.35 0.44-0.52
L2-Hys, signed 18 0.00-0.00 0.16-0.29 0.91-1.02 0.20-0.22 0.36-0.39
L2-Hys, 4 px cells 0.00-0.00 0.12-0.22 0.00-0.00 0.26-0.28 0.42-0.46
L2-Hys, 16 px cells 0.00-0.00 0.26-0.33 0.00-0.00 0.11-0.14 0.21-0.25
Without normalization, reducing the window’s contrast to 30% moves the descriptor most of the way to the clutter, because every gradient shrinks by the same factor; any normalization, global or per block, removes the change exactly, as the formulation predicts. The dim legs are a change of local contrast, and here the two kinds of normalization part ways: a single normalization of the whole window lets the strong torso edges dominate and the weak legs nearly vanish, while block normalization rescales the leg blocks on their own and moves the whole range of distances down. It does not remove the change, because within each leg block the background texture keeps its strength while the leg edges weaken, so their proportions change.
Contrast inversion leaves every unsigned descriptor exactly unchanged and moves the signed one as far as an unrelated clutter window, which is the property Dalal and Triggs relied on when they chose unsigned gradients for people in clothing of any colour. Soft voting is what makes small shifts cheap: with hard votes a 2-pixel shift already costs nearly as much as a 4-pixel shift with interpolation, because edges that cross a cell boundary jump from one histogram to the next. The last two rows show the cell-size trade-off in miniature (blocks stay cells, so they grow with the cells). Large cells tolerate shifts best but do worst on the dim legs, presumably because each block then mixes the legs with more of the torso and background; small cells keep the legs separate but are the most sensitive to position of the three cell sizes. Which balance is right depends on the class and the scale, and the optimum Dalal and Triggs measured for people is in the Parameters table.
scikit-image
skimage.feature.hog in version 0.25.2 implements the dense descriptor with these properties, read from its source. Gradients are unnormalized central differences, set to zero on the image border. Orientations are always unsigned: the function has no option for signed gradients. Each pixel votes with its full magnitude into one orientation bin and one cell, without interpolation or a Gaussian window, and each cell’s histogram is divided by the cell’s area. Blocks always move by one cell, and pixels beyond the last whole cell are ignored. For colour images (channel_axis set), each pixel uses the channel with the largest gradient magnitude, as Dalal and Triggs did, and transform_sqrt=True applies square-root gamma compression. block_norm accepts L1, L1-sqrt, L2 and L2-Hys, with as in the example. The default block is cells and the default normalization is L2-Hys. The docstring’s Notes section, however, lists L1 as one of the function’s departures from the paper, which no longer matches the default.
OpenCV
OpenCV’s cv::HOGDescriptor (version 4.14.0, from its header and source) is a detector as well as a descriptor. Its default constructor uses Dalal and Triggs’s geometry: a window, blocks with an stride, cells and 9 unsigned bins, L2-Hys with a clip of 0.2, and gamma correction (a square-root lookup table) switched on. The constructor that takes explicit parameters defaults to gamma correction off, so the two ways of building the “same” descriptor differ. Input must be 8-bit, grayscale or three-channel; gradients use the mask with the strongest colour channel, votes are interpolated trilinearly with a Gaussian block window whose default is pixels, half the value in the paper, and the first L2 step divides by plus 0.1 times the block vector’s length (3.6 for 36 values), not by . A source comment states that the vectors are compatible with INRIA’s Object Detection and Localization Toolkit.
getDefaultPeopleDetector() returns 3,781 coefficients, the 3,780 weights of a linear classifier for the window plus its bias, and getDaimlerPeopleDetector() returns 1,981 for a window; the documentation describes both as trained for people detection without naming the training data. detectMultiScale builds the pyramid with a default scale step of 1.05 and at most 64 levels, scans with a stride of one block step unless told otherwise, and merges overlapping detections by grouping similar rectangles (or by mean-shift) rather than by greedy non-maximum suppression. As of October 2026, in OpenCV 5.0.0 (released in June 2026) the class is no longer in the main repository’s objdetect module; it moved to the xobjdetect module of opencv_contrib, where the parameter constructor gives every argument a default and turns gamma correction on whether or not parameters are given.
Properties and Behavior
What the classifier looks at. Dalal and Triggs examined the weights of their trained linear SVM and found that the detector relied mainly on silhouette contours, especially the head, shoulders and feet, and that the most useful blocks were centred on the background just outside the contour; gradients inside the person, especially vertical ones, tended to count against a detection. The descriptor therefore works as a template of where outline edges of each orientation should and should not be.
Tolerance by construction. Translations much smaller than a cell change the descriptor little, as the example shows, and Dalal and Triggs make the same point for rotations much smaller than an orientation bin; interpolation keeps the change smooth. Larger changes, such as a person seen from the side when the classifier was trained on frontal views, or a bent pose, change the descriptor as much as any other content would.
A single scale and a single orientation. HOG has no scale or rotation invariance of its own. The window must contain the object at roughly the trained size and upright, and detectors obtain scale invariance by scanning an image pyramid.
Dependence on parameters. Because the descriptor’s length and the meaning of each component depend on cell size, block size, stride, bins and normalization, a classifier trained on one implementation’s vectors cannot be used with another’s unless every parameter, the component order and the gradient and normalization details agree.
Limitations
- Rigid layout. A single HOG template assumes the object’s parts appear at roughly fixed positions in the window. Articulated or strongly deformed objects need several templates or a model with movable parts, the motivation for deformable part models.
- Occlusion and truncation. Every block contributes to a linear score, so blocks covered by another object or cut off by the image border add misleading evidence.
- Clutter with the right shape. Long vertical structures produce person-like gradients; Dalal and Triggs read the negative weights on vertical gradients inside the person (Properties and Behavior) as the classifier’s way of suppressing such false positives.
- Fixed aspect ratio and scale. One window shape covers one view; small objects below the window size have to be found by upsampling the image, where gradients are blurred.
- Hand-designed. HOG encodes gradient orientation and layout only. Colour, texture statistics and higher-level parts are absent unless added as further channels. Learned convolutional features replaced it once enough labelled data and computation were available: the R-CNN paper reported large gains on PASCAL VOC over the HOG-based deformable part model.
Variants
Circular blocks (C-HOG). Dalal and Triggs also tested blocks divided into log-polar sectors, reminiscent of shape contexts but with an orientation histogram in each sector. With as few as two radial bins (a centre and a surround) and four angular sectors, they performed about as well as rectangular blocks (R-HOG). They also reported that adding second-derivative “bar” responses (R2-HOG) helped, at twice the length.
Felzenszwalb et al.’s HOG. The deformable part model’s journal paper reorganized HOG as a feature map with one vector per cell. Each pixel votes into a single orientation bin (rounded rather than interpolated) and bilinearly into the four surrounding cells. Each cell’s 9-bin unsigned histogram is normalized by the gradient energy of each of the four -cell blocks that contain it and truncated at 0.2, giving 36 values per cell; the authors note small differences from Dalal and Triggs’s features that did not significantly affect their system. A principal component analysis of these vectors showed that 11 dimensions captured essentially all of the information, and that the top eigenvectors were nearly constant along the orientation or the normalization axis. The authors therefore replaced the projection with sums: 9 sums over the four normalizations, one per orientation, and 4 sums over the orientations, one per normalization, a 13-dimensional feature that performed as well as the 36-dimensional one. Their final feature adds contrast-sensitive information: 18 signed and 9 unsigned orientation channels, each summed over the four normalizations, plus the 4 energy features, for 31 dimensions per cell, which they found improved performance for most PASCAL classes. The tracker below is one later system built on this version.
Cascades and integral histograms. Zhu, Avidan, Yeh and Cheng (2006) combined HOG with the boosted rejection cascade of face detection. They computed one integral image per orientation bin, so the histogram of any rectangle costs four lookups per bin, and dropped the Gaussian window and trilinear interpolation, which do not fit that scheme, along with L2 normalization, which they replaced by L1. Blocks of variable size (from pixels up to the whole window) and aspect ratio gave 5,031 candidate blocks, from which AdaBoost selected those used in each stage. In their timings on a image, their implementation of Dalal and Triggs’s detector took 500 ms for about 800 windows and 7 s for 12,800, against 26 ms and 106 ms for the L1 cascade.
Motion. Applying the same cell-and-block machinery to optical flow gives histograms of flow (HOF) and motion boundary histograms (MBH), which the dense trajectories article describes as used for action recognition.
Tracking. Correlation-filter trackers can use HOG cells as multichannel features. Henriques et al. (2015) ran their kernelized correlation filter (KCF) on Felzenszwalb’s 31-channel variant with 4-pixel cells. On a benchmark of 50 videos, the HOG version reached a mean precision of 73.2% at a 20-pixel threshold, against 56.0% on raw pixels, at 172 frames per second, slightly faster than the raw-pixel version because the cells shrink the arrays to be transformed (Table 1 of the arXiv version of the journal paper). The object tracking article places correlation filters among other trackers.
A target for self-supervised learning. Masked Feature Prediction (MaskFeat; Wei et al., 2022) pretrains a transformer to predict the HOG features of masked image regions instead of their pixels, with HOG computed on 8-pixel cells with 9 unsigned bins, separately for each colour channel, and each cell’s histogram L2-normalized. In their ImageNet-1K comparison of targets (ViT-B pre-trained for 300 epochs), HOG gave 83.6% top-1 after 100 epochs of fine-tuning against 82.5% for pixel colours, and their ablation found that dropping the contrast normalization lowered accuracy from 83.6% to 82.2%, with L1 in between at 82.8%. Local contrast normalization mattered for this use too. Masked autoencoders and self-supervised learning cover the wider family.
Related
- Sliding Window Detection
Detecting objects by running a fixed-size window classifier at every position and scale of an image, how the stride, the pyramid's scale step and the window shape set what is found and what it costs, and how the idea survives inside modern detectors.
- SIFT
The Scale-Invariant Feature Transform, which detects blob-like keypoints at extrema of a difference-of-Gaussians scale space and describes each with a 128-dimensional histogram of gradient orientations that is invariant to scale and rotation.
- Object Detection
Finding every object of a set of categories in an image and returning, for each one, a bounding box, a class label and a confidence score.
- Non-Maximum Suppression
The post-processing step that removes duplicate detections by repeatedly keeping the highest-scoring box and discarding the boxes that overlap it beyond an IoU threshold, with its soft, distance-aware and learned variants.
- Support Vector Machine
The classifier that separates two classes of images or image features by the hyperplane with the widest margin, its soft-margin and hinge-loss forms, the dual problem and kernels such as the RBF, histogram intersection and χ² kernels, its solvers and library defaults, and its use with HOG features, deformable part models, bags of visual words and CNN features.
- Histograms of Oriented Gradients for Human Detection
Dalal and Triggs's CVPR 2005 paper, which showed that dense grids of locally normalized gradient-orientation histograms with a linear SVM detect people far better than earlier feature sets, measured each design choice, and introduced the INRIA person dataset.
References
- Dalal, N. & Triggs, B. (2005). Histograms of Oriented Gradients for Human Detection. IEEE Computer Society Conference on Computer Vision and Pattern Recognition (CVPR), 1, 886–893.
- Felzenszwalb, P. F., Girshick, R. B., McAllester, D. & Ramanan, D. (2010). Object Detection with Discriminatively Trained Part-Based Models. IEEE Transactions on Pattern Analysis and Machine Intelligence, 32(9), 1627–1645.
- Zhu, Q., Avidan, S., Yeh, M.-C. & Cheng, K.-T. (2006). Fast Human Detection Using a Cascade of Histograms of Oriented Gradients. Mitsubishi Electric Research Laboratories, Technical Report TR2006-068 (presented at CVPR 2006).
- Henriques, J. F., Caseiro, R., Martins, P. & Batista, J. (2015). High-Speed Tracking with Kernelized Correlation Filters. IEEE Transactions on Pattern Analysis and Machine Intelligence, 37(3), 583–596.
- Wei, C., Fan, H., Xie, S., Wu, C.-Y., Yuille, A. & Feichtenhofer, C. (2022). Masked Feature Prediction for Self-Supervised Visual Pre-Training. IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR), 14648–14658.
- scikit-image developers (2025). skimage.feature.hog, skimage/feature/_hog.py, scikit-image v0.25.2 source code.
- scikit-image developers (2025). skimage/feature/_hoghistogram.pyx, scikit-image v0.25.2 source code.
- OpenCV (2026). HOGDescriptor declaration and documentation, modules/objdetect/include/opencv2/objdetect.hpp, OpenCV 4.14.0 source code.
- OpenCV (2026). HOGDescriptor implementation, modules/objdetect/src/hog.cpp, OpenCV 4.14.0 source code.
- OpenCV (2026). xobjdetect module, opencv_contrib 5.0.0 source code.