Sobel gradients, Harris response, 3 x 3 non-maximum suppression and corner overlays, written with plain NumPy loops for clarity. Theory: Image Gradients and the Sobel Operator, Harris Corner Detector, Non-Maximum Suppression. filter_image and pad_input_wrt_filter are in Template Matching Snippets.
Sobel operator
def sobel_operator(image):
Mx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]) # responds to vertical edges
My = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]]) # responds to horizontal edges
Gx = filter_image(image, Mx)
Gy = filter_image(image, My)
return Gx, Gy, np.sqrt(Gx**2 + Gy**2) # magnitude
Gx, Gy, mag = sobel_operator(gray)
# display np.abs(Gx), np.abs(Gy), mag with cmap='gray'Smooth first for less noise: sobel_operator(filter_image(gray, generate_gaussian(2.0))).
Harris response
def harris_detector(image, window_size=5, alpha=0.04):
response = np.zeros(image.shape)
Ix, Iy, _ = sobel_operator(image)
pad = lambda a: pad_input_wrt_filter(a, window_size, window_size)
Ixx, Iyy, Ixy = pad(Ix * Ix), pad(Iy * Iy), pad(Ix * Iy) # products of first derivatives
window = np.ones((window_size, window_size)) # equal weights (box)
for y in range(image.shape[0]):
for x in range(image.shape[1]):
m1 = np.sum(Ixx[y:y+window_size, x:x+window_size] * window)
m2 = np.sum(Iyy[y:y+window_size, x:x+window_size] * window)
m3 = np.sum(Ixy[y:y+window_size, x:x+window_size] * window)
M = np.array([[m1, m3], [m3, m2]]) # structure tensor
response[y, x] = np.linalg.det(M) - alpha * np.trace(M) ** 2
return responseThe per-pixel loop is slow on large images. The same result comes from box-filtering the three product images once and computing m1 * m2 - m3**2 - alpha * (m1 + m2)**2 on whole arrays.
3 x 3 non-maximum suppression with a relative threshold
def nms(response, frac=0.1):
threshold = frac * response.max() # relative: the absolute scale varies with the image
out = np.zeros_like(response)
padded = pad_input_wrt_filter(response, 3, 3) # zero-padding
for y in range(response.shape[0]):
for x in range(response.shape[1]):
v = response[y, x]
if v > threshold and v >= padded[y:y+3, x:x+3].max(): # centre is the neighbourhood max
out[y, x] = v
return outOn a photograph of 800 x 531 pixels, frac = 0.01 gave about 2000 corners and frac = 0.1 gave 241. Using >= keeps ties, so a flat plateau of equal responses can yield several adjacent corners.
Corner overlay
ys, xs = np.where(nms_result > 0)
binary = np.zeros(nms_result.shape, np.uint8)
overlay = np.stack([gray] * 3, axis=-1) # gray image as RGB
for y, x in zip(ys, xs): # enlarge each corner to a 5 x 5 square
y0, y1 = max(0, y - 2), min(gray.shape[0], y + 3)
x0, x1 = max(0, x - 2), min(gray.shape[1], x + 3)
binary[y0:y1, x0:x1] = 255
overlay[y0:y1, x0:x1] = [255, 0, 0]With zero padding, expect extra “corners” along the image border; drop detections within a few pixels of the edge or switch to replicate padding (Image Borders and Padding).
Related: Code Snippets MOC, Computer Vision MOC