应用场景
Canny边缘检测广泛应用于计算机视觉和图像处理领域,主要用于从图像中准确提取目标轮廓和边缘信息。由于其具有较强的抗噪声能力和较高的边缘定位精度,因此常用于工业机器视觉检测、目标识别与跟踪、自动驾驶车道线检测、医学图像分析、视频监控以及FPGA实时图像处理等场景。在实际工程中,Canny通常作为后续特征提取、目标检测和图像分割算法的前端处理模块,为系统提供清晰、连续且单像素宽度的边缘信息。
和单纯的 Sobel 相比,Canny 的最大优势是能够在抑制噪声的同时获得更准确、更细且连续的边缘。Sobel 本质上只是计算图像梯度,得到的边缘通常较粗,容易受到噪声影响,并且会产生较多伪边缘;而 Canny 在 Sobel 梯度计算的基础上增加了**高斯滤波、非极大值抑制(NMS)、双阈值检测和边缘连接(Hysteresis)等步骤,能够将边缘压缩为单像素宽度,去除大量噪声响应,并保留连续的真实目标轮廓。因此在目标检测、图像分割、车道线识别和工业视觉检测等应用中,Canny 提取的边缘质量通常明显优于 Sobel。
实现过程
该图展示了 Canny边缘检测算法的完整处理流程。首先,对输入图像进行高斯滤波(Gaussian Filter),以去除噪声并平滑图像;随后利用 Sobel 算子计算图像在水平和垂直方向上的梯度(Gx、Gy),并进一步得到梯度幅值(Magnitude)和梯度方向(Direction)。接着,通过**非极大值抑制(NMS)仅保留沿梯度方向的局部最大值,将原本较宽的边缘细化为单像素宽度。之后,采用双阈值检测(Double Threshold)将边缘像素划分为强边缘、弱边缘和非边缘三类,最后通过边缘连接(Hysteresis)保留与强边缘相连的有效弱边缘,去除孤立噪声点,最终输出连续、清晰且定位精确的边缘图像。
FPGA实现过程
高斯滤波,sobel算子已在之前的章节实现。
Sobel 算子计算得到水平梯度 dx和垂直梯度 dy后,可以求出图像在每个像素处的梯度方向。为了降低运算复杂度,Canny 算法通常不会使用精确角度,而是将梯度方向量化为 0°、45°、90°、135° 四个主要方向。这样后续的非极大值抑制只需要沿这四个固定方向进行比较,无需进行复杂的三角函数运算。在 FPGA 实现中,通常利用 ∣dy∣/∣dx∣的大小关系和符号位判断方向,从而实现高效的硬件流水线处理。梯度方向的确定如下代码:
//0: 0 1:90 2:135 3:45
assign dir = (abs_dy < abs_dx*0.414)?0:(abs_dy > abs_dx*2.414)?1:(dx[10] ^ dy[10])?2:3;
经过 Sobel 计算后,边缘往往表现为较宽的梯度带。非极大值抑制的作用是将这些宽边缘细化为单像素边缘。具体做法是在梯度方向上选取当前像素两侧的邻点进行比较,如果当前像素的梯度幅值同时大于或等于两侧像素,则认为它是该方向上的局部最大值并予以保留;否则将其抑制为零。经过 NMS 处理后,图像中的边缘轮廓会明显变细,为后续阈值筛选提供更加准确的边缘定位结果。NMS 后的图像虽然已经得到细化边缘,但仍可能包含由噪声产生的伪边缘。
双阈值检测通过设置高阈值(High Threshold)和低阈值(Low Threshold)对边缘进行分类:大于高阈值的像素被标记为强边缘,小于低阈值的像素直接剔除,而介于两者之间的像素则被标记为弱边缘。这样既能保留真实边缘,又能避免因单一阈值过高导致边缘断裂。随后通过边缘连接(Hysteresis Tracking)判断弱边缘是否与强边缘连通,只有与强边缘相连的弱边缘才会被最终保留,从而获得连续且抗噪声能力更强的边缘检测结果。非极大值抑制和双阈值检测可以放到一起实现。
always @(posedge clk or negedge aresetn) begin
if(~aresetn)begin
r_result <= 0;
end else if(valid)begin
case(dir)
0:if(w11 >= w10 & w11 >= w12)begin
if(w11 < LOW_THRES)begin
r_result <= 0;
end else if(w11 > HIGH_THRES)begin
r_result <= 255;
end else begin
r_result <= 128;
end
end else begin
r_result <= 0;
end
1:if(w11 >= w01 & w11 >= w21)begin
if(w11 < LOW_THRES)begin
r_result <= 0;
end else if(w11 > HIGH_THRES)begin
r_result <= 255;
end else begin
r_result <= 128;
end
end else begin
r_result <= 0;
end
2:if(w11 >= w00 & w11 >= w22)begin
if(w11 < LOW_THRES)begin
r_result <= 0;
end else if(w11 > HIGH_THRES)begin
r_result <= 255;
end else begin
r_result <= 128;
end
end else begin
r_result <= 0;
end
3:if(w11 >= w02 & w11 >= w20)begin
if(w11 < LOW_THRES)begin
r_result <= 0;
end else if(w11 > HIGH_THRES)begin
r_result <= 255;
end else begin
r_result <= 128;
end
end else begin
r_result <= 0;
end
default:r_result <= 0;
endcase
end else begin
r_result <= 0;
end
end
边缘连接是 Canny 算法的最后一步,其目的是在抑制噪声的同时保证边缘的连续性。在双阈值检测后,图像中的像素被划分为强边缘、弱边缘和非边缘三类。边缘连接会以强边缘为起点,对其 8 邻域内的弱边缘进行搜索,如果弱边缘与强边缘直接或间接连通,则认为其属于真实边缘并予以保留;反之,孤立存在的弱边缘通常被视为噪声并被删除。通过这一过程,可以有效避免边缘断裂,提高边缘检测结果的完整性和抗噪声能力,最终得到连续、清晰且定位准确的边缘轮廓。
由于边缘连接本质上属于基于邻域连通性的迭代搜索过程,需要不断扩展和更新边缘集合,这类随机访问和递归式处理并不适合 FPGA 的流式流水线架构。为此,笔者将双阈值检测后的中间结果存储至 DDR 中,再利用 Zynq PS 端的 C 程序完成边缘连接与弱边缘跟踪,实现完整的 Canny 边缘检测流程。
仿真对比
仿真的框架如下图所示:

python的golden ref CANNY代码如下,为了和FPGA的处理过程最接近。
def golden_ref(img):
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gauss_img = cv2.GaussianBlur(
gray_img,
(3,3),
0,
borderType=cv2.BORDER_CONSTANT
)
dx = cv2.Sobel(
gauss_img,
cv2.CV_16S,
1,0,
ksize=3,
borderType=cv2.BORDER_CONSTANT
)
dy = cv2.Sobel(
gauss_img,
cv2.CV_16S,
0,1,
ksize=3,
borderType=cv2.BORDER_CONSTANT
)
abs_dx = np.abs(dx).astype(np.float32)
abs_dy = np.abs(dy).astype(np.float32)
mag = abs_dx + abs_dy
h,w = mag.shape
mag_pad = np.pad(mag, ((1,1), (1,1)), mode='constant', constant_values=0)
dx_pad = np.pad(dx, ((1,1), (1,1)), mode='constant', constant_values=0)
dy_pad = np.pad(dy, ((1,1), (1,1)), mode='constant', constant_values=0)
abs_dx_pad = np.abs(dx_pad).astype(np.float32)
abs_dy_pad = np.abs(dy_pad).astype(np.float32)
nms = np.zeros_like(mag, dtype=np.float32)
for y in range(h):
for x in range(w):
yp = y + 1
xp = x + 1
g = mag_pad[yp, xp]
if abs_dy_pad[yp, xp] < abs_dx_pad[yp, xp] * 0.414:
dir = 0
elif abs_dy_pad[yp, xp] > abs_dx_pad[yp, xp] * 2.414:
dir = 1
elif dx_pad[yp, xp] * dy_pad[yp, xp] < 0:
dir = 2
else:
dir = 3
if dir == 0:
g1 = mag_pad[yp, xp-1]
g2 = mag_pad[yp, xp+1]
elif dir == 1:
g1 = mag_pad[yp-1, xp]
g2 = mag_pad[yp+1, xp]
elif dir == 2:
g1 = mag_pad[yp-1, xp+1]
g2 = mag_pad[yp+1, xp-1]
else:
g1 = mag_pad[yp-1, xp-1]
g2 = mag_pad[yp+1, xp+1]
if g >= g1 and g >= g2:
nms[y, x] = g
# -------------------
# Double Threshold
# -------------------
high = 150
low = 50
edge = np.zeros_like(nms,dtype=np.uint8)
edge[nms >= high] = 255
weak_mask = (nms >= low) & (nms < high)
edge[weak_mask] = 128
edge = liantongyu(edge)
return edge
本设计基于 Verilog 搭建多级流水线架构完成 Canny 边缘检测全链路硬件实现,将 FPGA 实时处理输出的边缘图像与 OpenCV-Python cv2 标准 Canny 算法结果逐像素比对,二者边缘轮廓、强弱阈值筛选效果基本契合,但仔细观察还是可以发现两者的区别还是挺明显的,可能是每一步都会积累误差导致了最终的差异。
全部0条评论
快来发表一下你的评论吧 !