skip to content

In OpenCV, how do cv2.cornerHarris and cv2.goodFeaturesToTrack differ?

level: middleimportance: should knowfreq 50%

answer

  1. one returns a map, one a list
  2. structure tensor eigenvalues underneath
  3. min-eigenvalue versus det minus k trace squared
  4. qualityLevel is relative to the best corner
  5. neither produces descriptors

basics

~10 s

cv2.cornerHarris returns a float32 response map the same size as the input image, which you must threshold yourself. cv2.goodFeaturesToTrack scores corners with the Shi-Tomasi minimum-eigenvalue rule and returns an already-filtered array of corner coordinates.

solid answer

~40 s

Both score how corner-like each pixel is from the local gradient structure matrix, but they hand back different things. `cv2.cornerHarris(src, blockSize, ksize, k)` takes a single-channel 8-bit or float32 image and returns a **float32 response image of the same shape** — no peaks are picked for you, so the next line is typically `resp > 0.01 * resp.max()`. The `k` argument (usually 0.04–0.06) is the Harris response constant. `cv2.goodFeaturesToTrack(image, maxCorners, qualityLevel, minDistance)` uses the Shi-Tomasi minimum-eigenvalue score by default (`useHarrisDetector=False`) and returns an `(N, 1, 2)` float32 array of corner points after non-maximum suppression, a **relative** `qualityLevel` cutoff, a `minDistance` spacing rule and the `maxCorners` cap. Neither produces a descriptor: you get locations only, so you cannot match these points across images without a separate descriptor step. `cv2.cornerSubPix` refines either result to sub-pixel accuracy.

code

python · 11 lines
python
import cv2
import numpy as np

img = np.zeros((200, 200), np.uint8)
cv2.rectangle(img, (60, 60), (140, 140), 255, -1)

resp = cv2.cornerHarris(np.float32(img), 2, 3, 0.04)
print(resp.shape, resp.dtype)          # (200, 200) float32 - a response map

pts = cv2.goodFeaturesToTrack(img, maxCorners=10, qualityLevel=0.01, minDistance=10)
print(pts.shape, pts.dtype)            # (N, 1, 2) float32 - actual corner points

go deeper

for a junior

Be able to say that cornerHarris gives a per-pixel response image you threshold, while goodFeaturesToTrack gives you the corner coordinates directly, and that both need a grayscale input.

for a middle

Explain the structure tensor and the two scoring rules — det minus k times trace squared versus min eigenvalue — and walk through what qualityLevel, minDistance and maxCorners each filter out.

for a senior

Show judgment about tuning on real imagery: blockSize versus feature size, blurring noisy input, sub-pixel refinement for calibration, and knowing that a corner detector alone cannot solve correspondence across viewpoints.

for a principal

Own the decision of whether a corner detector belongs in the pipeline at all: it is cheap and dense but scale-sensitive and descriptor-free, so it fits frame-to-frame or calibration work, not wide-baseline matching.

## What both functions measure A corner is a point where intensity changes strongly in **two** directions — an edge changes in one, a flat patch in none. Both detectors formalise that with the same object: over a small window around each pixel, build the 2x2 structure tensor (second-moment matrix) from the image gradients Ix and Iy. Its eigenvalues lambda1 and lambda2 say how strongly intensity varies along the two principal directions. Two large eigenvalues means a corner; one large and one small means an edge; two small means flat. The two functions differ in how they collapse those eigenvalues into one number, and — much more importantly in practice — in what they return. ## cv2.cornerHarris: a raw response map Signature: `cv2.cornerHarris(src, blockSize, ksize, k[, dst[, borderType]])`. - `src` must be single-channel, 8-bit or float32. Passing a three-channel BGR image is a common first error; convert with `cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)` first. - `blockSize` is the neighbourhood size used to accumulate the structure tensor. - `ksize` is the Sobel aperture used to compute the gradients. - `k` is the Harris detector free parameter in the response R = det(M) - k * trace(M)^2, conventionally 0.04 to 0.06. The return value is a **float32 image the same size as the input**, holding R per pixel. It is not a list of corners. R is large and positive at corners, large and negative at edges, and near zero on flat regions. You pick corners yourself, almost always with a threshold relative to the maximum response, because R has no absolute scale — it depends on image contrast, gradient magnitude and `blockSize`. A fixed absolute threshold that works on one image will find nothing on the next. Because you get the whole map, you also inherit the responsibility for non-maximum suppression: a strong corner produces a blob of high responses, not a single pixel. `cv2.dilate` followed by an equality comparison, or `cv2.cornerSubPix`, is the usual cleanup. ## cv2.goodFeaturesToTrack: a finished corner list Signature: `cv2.goodFeaturesToTrack(image, maxCorners, qualityLevel, minDistance[, mask[, blockSize[, useHarrisDetector[, k]]]])`. By default it uses the Shi-Tomasi score R = min(lambda1, lambda2), which the original authors argued is a better-behaved corner measure than the Harris determinant/trace combination, and which removes the arbitrary `k`. Setting `useHarrisDetector=True` switches it back to the Harris response with the given `k`, so the scoring rule and the packaging are independent choices. What it adds beyond scoring is the filtering pipeline you would otherwise write by hand: - **qualityLevel** is *relative*: corners scoring below `qualityLevel * best_score` are discarded. A value of 0.01 keeps anything at least 1% as strong as the best corner in this image, which is why the same value transfers across images of different contrast. - **minDistance** enforces a minimum Euclidean spacing in pixels between accepted corners, spreading points out instead of letting them cluster on one high-contrast structure. - **maxCorners** caps the count, keeping the strongest. - **mask** restricts the search to a region of interest. The return is an `(N, 1, 2)` float32 array of x,y coordinates — or `None` when nothing passes the filters, which is worth guarding. ## What neither gives you Neither returns a descriptor, and neither returns `cv2.KeyPoint` objects. A corner location alone cannot be matched to a corner in another image: you have no vector describing the surrounding patch. That is why detectors such as SIFT, ORB and AKAZE exist — they detect *and* describe. Harris and Shi-Tomasi corners are used where correspondence comes from elsewhere: seeding a sparse tracker across consecutive frames where motion is small, or chessboard-style calibration where the geometry is known. Both are also **not scale-invariant**. The structure tensor is computed at one fixed window size, so a corner detected at one zoom level may not be detected after a 2x resize. If your images vary in scale, a pyramid-based detector is the right tool. ## Practical notes - Convert to grayscale explicitly; do not rely on a colour image being handled. - `cv2.cornerHarris` wants float32 for best results; `np.float32(gray)` is the usual conversion. - `cv2.cornerSubPix(image, corners, winSize, zeroZone, criteria)` refines integer corner coordinates to sub-pixel precision and matters a great deal for calibration accuracy. - Tuning is mostly `blockSize` versus the size of the structures you care about: too large and fine corners smear away, too small and noise scores as corners. Blur the image first if it is noisy.

  • Why is qualityLevel expressed as a fraction rather than an absolute score?
    Because the corner score has no absolute scale — it grows with image contrast, gradient magnitude and blockSize. A fixed absolute cutoff tuned on a bright, high-contrast frame silently returns zero corners on a dim one. Making the threshold relative to the strongest corner in the current image makes one setting transfer across images.
  • When would you follow either detector with cv2.cornerSubPix?
    Whenever sub-pixel accuracy affects downstream geometry: camera calibration, homography or pose estimation. Both detectors quantise corners to pixel centres, and that half-pixel bias propagates into reprojection error. cornerSubPix iterates on the local gradient field to refine each corner, taking winSize, zeroZone and a termination criteria tuple.
  • Are these corners usable for matching between two photographs taken from different distances?
    Not reliably. Both operate at a single fixed window scale, so a corner found at one zoom may vanish at another, and neither emits a descriptor to match with. For scale change across images you want a pyramid-based detect-and-describe method such as SIFT, ORB or AKAZE.

cornerHarris hands you the raw heat map and lets you decide where the hot spots are; goodFeaturesToTrack reads the same heat map and hands you a shortlist of pinned locations, already spaced out and ranked.

saying these in an interview costs you the question

  • Thinking cornerHarris returns a list of corner points
  • Passing a three-channel BGR image straight to cornerHarris
  • Using a fixed absolute threshold on the Harris response
  • Believing Harris corners are scale-invariant
  • Assuming goodFeaturesToTrack also returns descriptors

context