本课学习边缘检测的核心原理与实现。我们将从数学基础出发,通过代码实践真正理解每一个算法的运作机制。
边缘是图像中灰度值急剧变化的位置,对应一阶导数极值或二阶导数零交叉点。
import cv2, numpy as np
img = np.zeros((256,256), dtype=np.uint8)
cv2.rectangle(img, (40,40), (216,216), 200, -1)
cv2.circle(img, (128,128), 50, 100, -1)
noise = np.random.normal(0, 8, img.shape)
img_noisy = np.clip(img.astype(np.float64) + noise, 0, 255).astype(np.uint8)
sobel_x = cv2.Sobel(img_noisy, cv2.CV_64F, 1, 0, ksize=3)
sobel_y = cv2.Sobel(img_noisy, cv2.CV_64F, 0, 1, ksize=3)
magnitude = np.sqrt(sobel_x**2 + sobel_y**2)
print(f"梯度幅值均值: {magnitude.mean():.2f}")
print(f"边缘像素(>50)占比: {(magnitude > 50).sum()/magnitude.size:.2%}")
import cv2, numpy as np
img = np.zeros((256,256), dtype=np.uint8)
cv2.rectangle(img, (40,40), (216,216), 200, -1)
cv2.circle(img, (128,128), 50, 100, -1)
blurred = cv2.GaussianBlur(img, (3,3), 1.0)
sobel_x = cv2.Sobel(blurred, cv2.CV_64F, 1, 0, ksize=3)
sobel_y = cv2.Sobel(blurred, cv2.CV_64F, 0, 1, ksize=3)
magnitude = np.sqrt(sobel_x**2 + sobel_y**2)
magnitude_uint8 = np.clip(magnitude, 0, 255).astype(np.uint8)
approx_mag = np.abs(sobel_x) + np.abs(sobel_y)
print(f"Sobel X范围: [{sobel_x.min():.1f}, {sobel_x.max():.1f}]")
print(f"精确vs近似幅值差异: {np.mean(np.abs(magnitude-approx_mag)):.2f}")
cv2.imwrite('/var/www/ttl/cv/l03_sobel.png',
np.hstack([img, np.abs(sobel_x).astype(np.uint8), np.abs(sobel_y).astype(np.uint8), magnitude_uint8]))
Laplacian是二阶微分算子,对噪声更敏感但边缘定位更精确。
import cv2, numpy as np
img = np.zeros((256,256), dtype=np.uint8)
cv2.rectangle(img, (40,40), (216,216), 200, -1)
cv2.circle(img, (128,128), 50, 100, -1)
lap_direct = cv2.Laplacian(img, cv2.CV_64F, ksize=3)
blurred = cv2.GaussianBlur(img, (5,5), 1.0)
lap_log = cv2.Laplacian(blurred, cv2.CV_64F, ksize=3)
for sigma in [0.5, 1.0, 2.0]:
blur_s = cv2.GaussianBlur(img, (0,0), sigma)
lap_s = cv2.Laplacian(blur_s, cv2.CV_64F)
edge_pixels = (np.abs(lap_s) > 20).sum()
print(f"sigma={sigma}: 边缘像素数={edge_pixels}")
cv2.imwrite('/var/www/ttl/cv/l03_laplacian.png',
np.hstack([img, np.abs(lap_direct).astype(np.uint8), np.abs(lap_log).astype(np.uint8)]))
Canny是工业标准边缘检测算法,包含4步骤:
import cv2, numpy as np
img = np.zeros((256,256), dtype=np.uint8)
cv2.rectangle(img, (40,40), (216,216), 200, -1)
cv2.circle(img, (128,128), 50, 100, -1)
cv2.line(img, (40,40), (216,216), 150, 2)
for low, high in [(30,100), (50,150), (80,200), (100,250)]:
edges = cv2.Canny(img, low, high)
print(f"({low:3d}, {high:3d}): 边缘像素={np.count_nonzero(edges):5d}")
canny = cv2.Canny(img, 50, 150)
sobel_mag = np.sqrt(cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)**2 + cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)**2)
sobel_edge = (sobel_mag > 80).astype(np.uint8) * 255
cv2.imwrite('/var/www/ttl/cv/l03_canny.png', np.hstack([img, canny, sobel_edge]))
import cv2, numpy as np
img = np.zeros((256,256), dtype=np.uint8)
cv2.circle(img, (80,80), 40, 200, -1)
cv2.circle(img, (180,180), 20, 200, -1)
cv2.circle(img, (200,50), 10, 200, -1)
for sigma in [1.0, 2.0, 4.0]:
blurred = cv2.GaussianBlur(img, (0,0), sigma)
log = cv2.Laplacian(blurred, cv2.CV_64F) * sigma**2
print(f"sigma={sigma}: 归一化LoG峰值={np.abs(log).max():.1f}")
k = 1.6
for sigma in [1.0, 2.0, 4.0]:
g1 = cv2.GaussianBlur(img.astype(np.float64), (0,0), sigma)
g2 = cv2.GaussianBlur(img.astype(np.float64), (0,0), sigma*k)
dog = (g2 - g1) * sigma**2
print(f"DoG sigma={sigma}: peak={np.abs(dog).max():.1f}")
import cv2, numpy as np
img = np.zeros((256,256), dtype=np.uint8)
cv2.rectangle(img, (30,30), (226,226), 180, -1)
cv2.circle(img, (128,128), 60, 220, -1)
cv2.circle(img, (128,128), 30, 50, -1)
for sigma in [0.5, 1.0, 2.0, 4.0]:
blurred = cv2.GaussianBlur(img, (0,0), sigma)
edges = cv2.Canny(blurred, 50, 150)
print(f"sigma={sigma}: 边缘像素={np.count_nonzero(edges)}")
all_edges = np.zeros_like(img, dtype=np.float64)
for sigma in [0.5, 1.0, 2.0]:
blurred = cv2.GaussianBlur(img, (0,0), sigma)
edges = cv2.Canny(blurred, 50, 150).astype(np.float64)
all_edges += edges
fused = (all_edges > 0).astype(np.uint8) * 255
print(f"多尺度融合边缘像素: {np.count_nonzero(fused)}")
cv2.imwrite('/var/www/ttl/cv/l03_fused.png', fused)
import cv2, numpy as np
def edge_pipeline(image, method='canny'):
if method == 'canny':
processed = cv2.GaussianBlur(image, (3,3), 1.0)
otsu = cv2.threshold(processed, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[0]
edges = cv2.Canny(processed, otsu*0.4, otsu)
elif method == 'sobel':
sx = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=3)
sy = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=3)
mag = np.sqrt(sx**2 + sy**2)
edges = (mag > np.percentile(mag, 90)).astype(np.uint8) * 255
elif method == 'laplacian':
processed = cv2.GaussianBlur(image, (5,5), 1.5)
lap = cv2.Laplacian(processed, cv2.CV_64F, ksize=3)
edges = (np.abs(lap) > np.percentile(np.abs(lap), 92)).astype(np.uint8) * 255
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))
edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)
edges = cv2.morphologyEx(edges, cv2.MORPH_OPEN, kernel)
return edges
img = np.zeros((256,256), dtype=np.uint8)
cv2.rectangle(img, (30,30), (226,226), 180, -1)
cv2.circle(img, (128,128), 60, 220, -1)
cv2.circle(img, (128,128), 30, 50, -1)
for method in ['canny', 'sobel', 'laplacian']:
edge = edge_pipeline(img, method)
print(f"{method}: 边缘像素={np.count_nonzero(edge)}")
cv2.imwrite('/var/www/ttl/cv/l03_pipeline.png',
np.hstack([img, edge_pipeline(img,'canny'), edge_pipeline(img,'sobel'), edge_pipeline(img,'laplacian')]))
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 方法A | 简单高效 | 精度有限 | 快速原型 |
| 方法B | 精度高 | 计算量大 | 离线处理 |
| 方法C | 平衡精度与速度 | 参数调优复杂 | 实际应用 |
本课涉及的核心数学公式汇总,方便快速参考:
Canny边缘检测看似简单,但每一步都有工程细节值得深入理解:
NMS是Canny中最关键的一步,它将粗边缘细化到单像素宽:
import cv2, numpy as np
# 手动实现NMS理解原理
img = np.zeros((100,100), dtype=np.uint8)
cv2.rectangle(img, (20,20), (80,80), 200, -1)
# Step 1: 计算梯度
Ix = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
Iy = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
mag = np.sqrt(Ix**2 + Iy**2)
angle = np.arctan2(Iy, Ix) * 180 / np.pi
# Step 2: NMS
def nms(mag, angle):
h, w = mag.shape
result = np.zeros_like(mag)
for i in range(1, h-1):
for j in range(1, w-1):
a = angle[i, j]
# 量化到4个方向
if (0 <= a < 22.5) or (157.5 <= a <= 180) or (-22.5 <= a < 0) or (-180 <= a < -157.5):
n1, n2 = mag[i, j+1], mag[i, j-1]
elif 22.5 <= a < 67.5 or -157.5 <= a < -112.5:
n1, n2 = mag[i-1, j+1], mag[i+1, j-1]
elif 67.5 <= a < 112.5 or -112.5 <= a < -67.5:
n1, n2 = mag[i-1, j], mag[i+1, j]
else:
n1, n2 = mag[i-1, j-1], mag[i+1, j+1]
if mag[i, j] >= n1 and mag[i, j] >= n2:
result[i, j] = mag[i, j]
return result
nms_result = nms(mag, angle)
print(f"NMS前非零: {np.count_nonzero(mag > 50)}")
print(f"NMS后非零: {np.count_nonzero(nms_result > 50)}")
print(f"细化率: {1 - np.count_nonzero(nms_result > 50)/max(np.count_nonzero(mag > 50),1):.2%}")
Canny的第四步是双阈值+滞后跟踪,它解决了弱边缘的连接问题:
import cv2, numpy as np
# 自动Canny阈值
img = np.zeros((200,200), dtype=np.uint8)
cv2.rectangle(img, (40,40), (160,160), 200, -1)
cv2.circle(img, (100,100), 30, 100, -1)
noise = np.random.normal(0, 15, img.shape)
img = np.clip(img + noise, 0, 255).astype(np.uint8)
# Otsu自动阈值
otsu = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[0]
auto_canny = cv2.Canny(img, otsu * 0.5, otsu)
manual_canny = cv2.Canny(img, 50, 150)
auto_edges = np.count_nonzero(auto_canny)
manual_edges = np.count_nonzero(manual_canny)
print(f"自动阈值Canny: {auto_edges} 边缘像素")
print(f"手动阈值Canny: {manual_edges} 边缘像素")
cv2.imwrite('/var/www/ttl/cv/l03_auto_canny.png', np.hstack([auto_canny, manual_canny]))
你已经掌握了边缘检测的核心知识,继续前进!