Computer Vision Chapter 11

Harris corner detector

The Harris detector scores each pixel by how much the image patch intensity would change under small shifts in any direction. High scores appear at corners (two strong edges meet); flat regions score low; straight edges score low along the edge direction. OpenCV exposes this as cornerHarris, with optional cornerSubPix refinement for sub-pixel accuracy.

Idea: structure tensor

Let Ix, Iy be image gradients. Over a window, form the second-moment matrix M from summed outer products of gradients. Its eigenvalues λ₁, λ₂ describe edge strength in two orthogonal directions: both large → corner; one large → edge; both small → flat. Harris uses the response R = det(M) − k·trace(M)² (with empirical k ≈ 0.04–0.06) to avoid explicit eigenvalue decomposition.

blockSize

Neighborhood size for summing gradients (odd integer). Larger → smoother response, fewer duplicate peaks.

ksize

Sobel aperture for computing Ix, Iy (e.g. 3).

k

Harris free parameter in the response; typical range 0.04–0.06. Too small → more edge responses.

cv2.cornerHarris

Input must be float32 grayscale. Output is a single-channel float response map; threshold and take local maxima to list corners.

import cv2
import numpy as np

gray = cv2.imread("checkerboard.png", cv2.IMREAD_GRAYSCALE)
gray_f = np.float32(gray)

block, ksz, k = 3, 3, 0.04
resp = cv2.cornerHarris(gray_f, block, ksz, k)

# Dilate to help local-max suppression in a simple way
resp_d = cv2.dilate(resp, None)
vis = np.zeros_like(gray)
vis[resp_d > 0.01 * resp_d.max()] = 255

Stricter threshold

thresh = 0.05 * resp.max()
mask = resp > thresh
# optional: keep only local maxima of `resp` on `mask` with further NMS

Sub-pixel refinement

cornerSubPix refines corner locations to sub-pixel accuracy using the local intensity pattern—useful for calibration, stitching, and metrology.

import cv2
import numpy as np

gray = cv2.imread("grid.jpg", cv2.IMREAD_GRAYSCALE)
gray_f = np.float32(gray)
resp = cv2.cornerHarris(gray_f, 3, 3, 0.04)
yxs = np.argwhere(resp > 0.01 * resp.max()).astype(np.float32)
# cornerSubPix expects shape (N, 1, 2) with (x, y) order
pts = np.zeros((len(yxs), 1, 2), dtype=np.float32)
pts[:, 0, 0] = yxs[:, 1]
pts[:, 0, 1] = yxs[:, 0]

criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 40, 0.001)
refined = cv2.cornerSubPix(gray, pts, (5, 5), (-1, -1), criteria)

Harris vs Shi-Tomasi

goodFeaturesToTrack implements the Shi-Tomasi criterion (minimum eigenvalue threshold)—often picks a cleaner set of points for tracking. Harris gives a dense response map you threshold yourself; Shi-Tomasi returns a capped list sorted by quality.

corners = cv2.goodFeaturesToTrack(
    gray, maxCorners=100, qualityLevel=0.01, minDistance=10,
    blockSize=3, useHarrisDetector=True, k=0.04)

corners_st = cv2.goodFeaturesToTrack(
    gray, maxCorners=100, qualityLevel=0.01, minDistance=10, blockSize=3)

Takeaways

  • Use float32 input; scale threshold relative to resp.max().
  • Increase blockSize to suppress duplicate corners on thick edges.
  • Use cornerSubPix when you need accurate coordinates, not just detection.

Quick FAQ

Raise the response threshold, increase blockSize, or pre-blur slightly. Alternatively switch to Shi-Tomasi with minDistance and a lower maxCorners.

No—same corner at different zooms moves in scale space. Use SIFT/ORB or a multi-scale Harris pyramid if you need scale robustness.