基于FPGA的Canny边缘检测算法实现方案

描述

应用场景

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 算法结果逐像素比对,二者边缘轮廓、强弱阈值筛选效果基本契合,但仔细观察还是可以发现两者的区别还是挺明显的,可能是每一步都会积累误差导致了最终的差异。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分