Skip to content

Commit 0677dea

Browse files
committed
perception: implement Histogram of Oriented Gradients (HOG) (#446)
AIMA Chapter 24 references the HOG descriptor (Dalal & Triggs, CVPR 2005) but gives no pseudocode. Added hog(): gradient magnitude + unsigned orientation, per-cell orientation histograms weighted by magnitude, L2 block normalization, concatenated into the feature vector. Docstring cites the source. Test added.
1 parent 6aff1e6 commit 0677dea

2 files changed

Lines changed: 52 additions & 0 deletions

File tree

‎aima/perception.py‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,48 @@ def gaussian_derivative_edge_detector(image):
6464
return edges
6565

6666

67+
def hog(image, cell_size=8, bins=9, block_size=2):
68+
"""Histogram of Oriented Gradients (HOG) feature descriptor [#446].
69+
70+
Implements Dalal & Triggs (2005) -- the descriptor referenced by AIMA Chapter
71+
24 (Perception), for which the book gives no pseudocode. The steps are:
72+
gradient magnitude and unsigned (0-180 deg) orientation; for each
73+
``cell_size`` x ``cell_size`` cell, a ``bins``-bin orientation histogram weighted
74+
by gradient magnitude; L2 normalization over each ``block_size`` x ``block_size``
75+
block of cells; the normalized blocks concatenated into one feature vector.
76+
77+
Source: N. Dalal & B. Triggs, "Histograms of Oriented Gradients for Human
78+
Detection", CVPR 2005.
79+
"""
80+
image = np.asarray(image, dtype=float)
81+
if image.ndim == 3: # collapse colour channels to grayscale
82+
image = image.mean(axis=2)
83+
84+
gy, gx = np.gradient(image)
85+
magnitude = np.hypot(gx, gy)
86+
orientation = np.rad2deg(np.arctan2(gy, gx)) % 180 # unsigned gradient angle
87+
88+
cells_y, cells_x = image.shape[0] // cell_size, image.shape[1] // cell_size
89+
bin_width = 180 / bins
90+
histograms = np.zeros((cells_y, cells_x, bins))
91+
for cy in range(cells_y):
92+
for cx in range(cells_x):
93+
rows = slice(cy * cell_size, (cy + 1) * cell_size)
94+
cols = slice(cx * cell_size, (cx + 1) * cell_size)
95+
cell_bins = (orientation[rows, cols] // bin_width).astype(int) % bins
96+
cell_mag = magnitude[rows, cols]
97+
for k in range(bins):
98+
histograms[cy, cx, k] = cell_mag[cell_bins == k].sum()
99+
100+
eps = 1e-5
101+
features = []
102+
for by in range(cells_y - block_size + 1):
103+
for bx in range(cells_x - block_size + 1):
104+
block = histograms[by:by + block_size, bx:bx + block_size].ravel()
105+
features.append(block / np.sqrt((block ** 2).sum() + eps ** 2))
106+
return np.concatenate(features) if features else histograms.ravel()
107+
108+
67109
def laplacian_edge_detector(image):
68110
"""Extract image edge with laplacian filter"""
69111
if not isinstance(image, np.ndarray):

‎tests/test_perception.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,15 @@ def test_ROIPoolingLayer():
8383
[1, 1, 1, 1, 1, 1, 50]]
8484

8585

86+
def test_hog():
87+
# a 16x16 image split by a vertical edge; HOG yields one L2-normalized 2x2 block
88+
image = np.zeros((16, 16))
89+
image[:, 8:] = 255.0
90+
features = hog(image, cell_size=8, bins=9, block_size=2)
91+
assert features.shape == (2 * 2 * 9,) # one block of 2x2 cells x 9 bins
92+
assert np.isfinite(features).all()
93+
assert abs(np.linalg.norm(features) - 1) < 1e-3 # the single block is L2-normalized
94+
95+
8696
if __name__ == '__main__':
8797
pytest.main()

0 commit comments

Comments
 (0)