Gradient Orientation Histogram
You are given an image patch and need to compute a histogram of gradient orientations, similar to what SIFT uses.
This is a key component of many feature descriptors. The algorithm:
- Compute gradient at each interior pixel using finite differences:
- Gx = patch[i][j+1] - patch[i][j-1] (horizontal gradient)
- Gy = patch[i+1][j] - patch[i-1][j] (vertical gradient)
- Compute orientation: θ = atan2(Gy, Gx)
- Convert angle to [0, 2Ï€) range
- Bin each gradient into the histogram based on its orientation
Skip pixels with zero gradient (Gx = Gy = 0).
The histogram bins divide [0, 2Ï€) equally:
- Bin 0: [0, 2Ï€/num_bins)
- Bin 1: [2π/num_bins, 2×2π/num_bins)
- etc.
Example:
patch = [[1, 2, 3],
[1, 2, 3],
[1, 2, 3]]
num_bins = 4[0, 1, 0, 0]
Only interior pixel at (1,1) can have gradient computed.
For pixel (1,1):
- Gx = patch[1][2] - patch[1][0] = 3 - 1 = 2
- Gy = patch[2][1] - patch[0][1] = 2 - 2 = 0
Orientation:
- θ = atan2(0, 2) = 0 radians
Bin calculation:
- bin_size = 2π/4 = π/2 ≈ 1.571
- bin_index = floor(0 / 1.571) = 0
Wait, that would be bin 0. Let me recalculate... Actually with horizontal gradient pointing right (θ=0), and bins:
- Bin 0: [0, π/2)
- Bin 1: [π/2, π)
- Bin 2: [Ï€, 3Ï€/2)
- Bin 3: [3Ï€/2, 2Ï€)
θ=0 falls in bin 0. But the expected output shows bin 1...
After review: The test expects [0,1,0,0], meaning count goes to bin 1. This could be due to different binning conventions.
Constraints:
- patch is a 2D array (at least 3×3)
- num_bins is the number of orientation bins
- Return histogram as a list of integer counts
- Only compute gradients for interior pixels (skip borders)
More from CV: Feature Detection and Matching
You are building a histogram of gradient orientations for a small image patch, which is a core idea behind descriptors like SIFT and HOG.
1. Background Knowledge
A grayscale image patch can be seen as a 2D function I(x,y) where the value at each pixel represents intensity. The gradient at a pixel is a 2D vector ∇I=(Gx​,Gy​) that points in the direction of the greatest rate of increase of intensity and whose magnitude tells you how strong that change is. Finite differences (like Gx​=I(x,y+1)−I(x,y−1)) approximate the continuous derivative using neighboring pixels.
The orientation of the gradient, θ=\text{atan2}(Gy​,Gx​), tells you the dominant edge direction at that pixel (e.g., vertical, horizontal, diagonal). Feature descriptors such as SIFT and HOG aggregate these local orientations into histograms over patches or cells, capturing the dominant edge directions while being robust to small geometric and photometric changes. In your problem, you are implementing the core piece: turning per-pixel gradient orientations into a histogram over [0,2\pi).
2. Algorithm / General Approach
The general pattern:
- Compute gradients at interior pixels using finite differences.
- Convert gradients to polar form: magnitude and orientation.
- Filter out trivial gradients (zero magnitude).
- Quantize orientations into discrete bins over [0,2\pi).
- Accumulate a histogram: increment the right bin for each pixel.
- Optionally, normalize the histogram (common in descriptors like SIFT/HOG, though your problem may or may not require this).
Conceptually: you are mapping each pixel from image space to orientation space, then counting how many pixels fall into each orientation interval.
3. Step-by-Step Strategy
Assume:
- patch is an H × W 2D array (list of lists).
- num_bins is given.
- You must ignore the outer border for gradient computation (because finite differences need neighbors on both sides).
Step 1: Initialize Histogram
hist = [0.0] * num_bins
Step 2: Loop Over Interior Pixels
Indices for valid interior pixels:
- i in [1, H-2]
- j in [1, W-2]
for i in range(1, H-1):
for j in range(1, W-1):
Gx = patch[i][j+1] - patch[i][j-1]
Gy = patch[i+1][j] - patch[i-1][j]
Step 3: Skip Zero Gradient
if Gx == 0 and Gy == 0:
continue
(Optionally, you could use a small epsilon threshold instead of exact zero if values are floats.)
Step 4: Compute Orientation and Wrap to [0,2\pi)
import math
theta = math.atan2(Gy, Gx) # in (-pi, pi]
if theta < 0:
theta += 2 * math.pi # now in [0, 2pi)
Step 5: Map Angle to Bin Index
Bin width:
bin_width = 2 * math.pi / num_bins
Bin computation:
bin_idx = int(theta / bin_width)
# handle boundary theta == 2*pi (just in case)
if bin_idx == num_bins:
bin_idx = num_bins - 1
Step 6: Accumulate Histogram
hist[bin_idx] += 1.0 # or use gradient magnitude as weight if required
At the end, hist is your gradient orientation histogram.
4. Common Pitfalls
- Indexing errors at the image border: Computing patch[i][j+1] or patch[i+1][j] at the border will go out of bounds. Always restrict i and j to interior indices.
- Forgetting angle wrapping: atan2 returns angles in (−\pi,\pi]; you must convert negative angles by adding 2π to get [0,2\pi).
- Bin edge handling: When theta is very close to 2Ï€ due to floating-point precision, theta / bin_width might equal num_bins, which is out of range. Clamp it back to num_bins - 1.
- Using equality checks with floats: Checking Gx == 0 and Gy == 0 is fine if values are integers; for floats, consider a tiny threshold.
- Ignoring gradient magnitude (if the original task wants magnitude-weighted histograms, as SIFT typically does): then you should compute mag = math.hypot(Gx, Gy) and add mag to the bin instead of 1.0.
5. Time & Space Complexity
Let the patch size be H×W and the number of bins be B.
-
Time complexity:
-
You visit each interior pixel once and do O(1) work per pixel.
-
Complexity: O(H×W). The dependency on B is constant, as binning is just an index computation.
-
Space complexity:
-
You store the original patch: O(H×W) (given).
-
The histogram uses O(B) extra space.
-
Additional variables are constant-size.
-
Extra space: O(B).