ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FPGA实现Sobel边缘检测与VGA显示:硬件流水线实战

FPGA实现Sobel边缘检测与VGA显示:硬件流水线实战

简介:这是一份完整的FPGA Sobel边缘检测与VGA显示工程,面向数字逻辑设计、Verilog编程及数字图像处理学习者,覆盖从图像采集、梯度卷积计算到VGA时序输出的完整硬件链路,适合课内实验、竞赛训练或毕业设计进阶。压缩包共514个文件、6.45MB,以Quartus工程为主体:tdf与v源码负责逻辑实现,cdb/hdb/qsf/qpf为工程数据库与配置,mif用于初始化数据,sof为可下载比特流,辅以rpt/pin等编译报告与约束,便于直接打开工程复现;目录中还包含多份.bak备份与调试记录,可追踪设计迭代。已有1249人学习下载。内含vga_module、sobel2、RGB_YCbCr_Gray、data_trans等关键模块,分别对应颜色空间转换、Sobel梯度计算与VGA显示控制,可对照理解算法到RTL的落地过程;这些源码便于二次开发或模块复用,整体工程层次清晰、复用性强,是实践FPGA图像加速处理的高价值参考。

1. FPGA实现Sobel边缘检测:从“纯算法”到“硬件流水线”的一步到位

在PC上跑一个Sobel边缘检测只要一行Python,但放进FPGA里要面对的是行缓存、卷积窗口和VGA时序同步这三个看似不相关的问题。这套FPGA+Verilog实现Sobel图像边缘检测并VGA显示的资源,把灰度转换、3x3卷积、阈值化、VGA控制器全部串成一条实时流水线,对着摄像头或ROM里的测试图就能看到屏幕上的边缘线。适合正在做嵌入式视觉毕设、FPGA竞赛和图像处理入门阶段的开发者。它解决的是“算法怎么落到硬件上”而不是“算法是什么”的问题,下面我会从原理到代码逐一拆开。

2. Sobel算子的硬件化:卷积核、行缓存与梯度数据的时钟对齐

Sobel算子本身不复杂,但硬件实现里如何把3x3窗口数据按时钟对齐,才是整个工程的命门。这一章先从卷积核的数学形式讲清楚为什么选Sobel,再讲行缓存如何节省BRAM,最后聊梯度幅值近似与阈值比较在硬件里怎么落地。

2.1 卷积核选型:为什么Sobel比一阶差分更抗噪,又比Canny更省资源

Sobel算子做的是水平和垂直两个方向的近似一阶导数。它有两个3x3卷积核,对8位灰度图做离散卷积。卷积核取值如下:

方向3x3卷积核响应特征
Gx[ -1 0 1; -2 0 2; -1 0 1 ]检测竖直方向的灰度突变,也就是水平梯度
Gy[ -1 -2 -1; 0 0 0; 1 2 1 ]检测水平方向的灰度突变,也就是垂直梯度

假设灰度函数是I(x,y),Gx在(x,y)处的值近似于∂I/∂x,Gy近似于∂I/∂y。注意这里的x对应VGA扫描中的列方向,y对应行方向,很多人把Gx误以为是检测水平边缘,实际上它是对竖直边缘最敏感。调试时如果发现横线断裂,而竖线完好,通常就是Gx和Gy的方向定义反了。

在FPGA里实现Sobel最大的好处是卷积核系数全是整数,-1、0、1、2都可以用移位和加法替代乘法,不需要DSP核。相比一阶差分算子[-1,1],Sobel在中心行和中心列给了2倍权重,对随机噪声有更好的抑制作用;相比Canny,Sobel不需要高斯平滑、双阈值滞后和连通性分析,逻辑量少一个数量级,所以实时图形处理领域里Sobel是性价比最高的边缘检测起点。

硬件里做卷积其实就是延迟累加。对3x3窗口,Gx = (p13 - p11) + 2*(p23 - p21) + (p33 - p31),Gy = (p31 - p11) + 2*(p32 - p12) + (p33 - p13)。每个像素只要9次读取、几次加法和移位,设计成流水线后一个时钟周期就能出一个梯度值。640x480@60Hz大约每秒要处理1840万像素,对现代FPGA来说属于低负载。

2.2 行缓存Line Buffer:两条行存储撑起3x3窗口

逐行扫描是图像传感器和VGA的共同特点。要实现3x3窗口,必须同时拿到当前行、上一行、上上行相同x坐标的像素。常见做法是用行缓存(Line Buffer)配合移位寄存器,把串行像素流变成三行并行数据。

我一般用两个行缓存串联:像素流先进入第一个FIFO,输出端再接到第二个FIFO的输入端,这样第二个FIFO的输出就是延迟了两行的数据。当前像素、延迟一行像素、延迟两行像素同时到达,再各用两个寄存器打拍,就能拼出3x3窗口。另一种实现是用寄存器堆加写指针,代码直观但占用LUT资源较多,适合行宽小于800的场合;行宽很大时建议用RAM原语或同步FIFO,否则综合后资源会爆掉。

行缓存的资源消耗大约是2COLSWIDTH bits。以640x480灰度图为例,26408=10240bit,相比完整帧缓冲的6404808=2457600bit,节省了200多倍。这也是为什么FPGA实时视频处理几乎都采用行缓存而不是帧缓冲。很多第一次做视频处理的人会顺手例化两个整帧RAM,结果BRAM占用率直接冲上90%,后面布线布局必然失败。

有一个重要细节往往被忽略:行缓存必须在消隐区清零或复位。如果不用行有效信号de来控制写入,而让din一直移位,那么行消隐和场消隐期间进来的垃圾数据会被当作有效像素,导致VGA画面右侧出现杂边,或者换场时残留上一场的行数据。所以我习惯让din只在de_in为高时写入,de_in变低时写指针归零。

2.3 梯度幅值近似与阈值化:硬件不玩浮点数

Gx和Gy的结果是有符号数,8位像素的卷积结果范围大约在-1020到+1020之间。计算幅值有三种方案:平方和开方最准确,但需要CORDIC或浮点IP核;绝对值和|Gx|+|Gy|误差约15%,逻辑极少;绝对值最大值max(|Gx|,|Gy|)误差更大,会削弱斜向边缘。这套工程默认用绝对值和,因为大多数边缘检测应用对15%的误差不敏感,而省下的LUT和寄存器可以用来做后续的形态学滤波。

硬件里实现绝对值不需要查表:对补码表示的有符号数,判断符号位,为1就取反加1,为0就保持原值。两个绝对值相加用一个周期就能完成。如果后续想做更精确的幅值,可以用乘法器累加Gx^2和Gy^2,再用一个近似开方公式,但注意乘法器路径会变长,时钟频率要打个折扣。

阈值化是最后一步:当幅值大于等于阈值T,输出2'b1,否则输出2'b0。T可以做成8位寄存器,由拨码开关或串口写入,调试时不用重新综合。固定阈值的问题在于换一张光照不同的图,弱边缘会全丢或者噪声全放出来,所以我习惯把T默认设在32,同时预留外部调节总线,后面第6章会讲具体做法。

到这里原理层已经通了,接下来的问题是:算出来的边缘二值图怎么和VGA扫描线精确对齐。

3. 系统架构与VGA显示参数:让图像数据流匹配像素时钟

Sobel边缘检测算出的二值图要送到VGA显示,得先解决两件事:数据和时序。数据流是一行一行进来的,VGA也是一行一行扫的,但两者的行同步、场同步必须严格对齐,否则画面漂移、闪屏甚至无信号。

3.1 顶层模块划分:图像源、Sobel核心、VGA控制器三大块

一套典型的FPGA工程,我一般会拆成以下模块:

  • 图像源模块:从摄像头、ROM或片上RAM中读出一帧灰度图,按像素时钟输出8位数据
  • 灰度转换模块:若输入是RGB565,则先转成Y亮度,再进Sobel核心
  • Sobel核心模块:内含行缓存、3x3窗口、梯度计算、阈值化
  • VGA时序控制模块:产生行/场同步信号,并在有效像素区间输出二值图像
  • 缓存/显示模块:Sobel输出写入双口RAM,VGA读端从同地址读取,实现写读解耦

模块间接口信号建议统一为:clk_pix、rst_n、din_valid、din[7:0]、dout_valid、dout[7:0]。尤其要保留din_valid,它告诉下游“这个像素有效”。如果省略valid信号,直接用数据变化判断,在ROM输入时勉强能跑,换成摄像头后会在消隐期把无效数据当真,画面必然花。

图像源和VGA必须使用同一个像素时钟,或者做跨时钟域同步。常见做法是统一使用VGA像素时钟驱动整个数据通路,摄像头数据先经过异步FIFO跨时钟域,再进Sobel流水线。这样可以避免亚稳态,显示端也不会出现撕裂。

3.2 数据通路:从8位灰度到1位边缘线的三级流水线

数据流大方向是:din[7:0] → 灰度缓存 → 行缓存 → 3x3窗口 → 计算Gx/Gy → 幅值近似 → 比较阈值 → dout[0](二值边缘)。

关键是要保证每个阶段都有相同的流水线延迟,否则本来对齐的像素会错位。以3x3窗口为例,第一列数据要等第三行出现才能形成窗口,所以从像素输入到输出有效结果有若干周期延迟。VGA控制器必须在延迟补偿后再输出de,否则画面最左边会串掉一行或一列。

延迟对齐是数据通路里最玄学的部分。Sobel窗口从输入到输出需要大约3到4个时钟周期,VGA读显存也有读延迟。为了对得准,我通常在Sobel输出端拉出一个FIFO,写入时标记像素的x和y坐标,读出时用x、y直接去比较VGA当前坐标,不相等就输出黑色。这种按坐标匹配的方式比固定延迟数更稳健,代价是显存深度增加,但调试起来省心得多。

如果只是做演示,更简单的办法是把de和像素一起打拍延迟,固定延迟N拍,用N个字深度的移位链。缺点是当分辨率改变时N也要重新推算。我建议至少要留一个让仿真观察窗口内部信号的测试口,比如把p11、p33引出来,方便用逻辑分析仪核对。

3.3 VGA时序参数:640x480@60Hz的计数器边界

VGA行场同步其实是一套严格的计数器规则。以最常见的640x480@60Hz为例,像素时钟约为25.175MHz,一行总共800个像素时钟,其中96个是行同步脉冲,48个后沿,640个有效像素,16个前沿;一场总共525行,其中2行场同步脉冲,33行后沿,480行有效,10行前沿。

给出一个Verilog参数块:

// VGA 640x480@60Hz 时序参数 localparam H_SYNC = 96; // 行同步脉冲宽度 localparam H_BACK = 48; // 行后沿 localparam H_ACTIVE = 640; // 有效像素 localparam H_FRONT = 16; // 行前沿 localparam H_TOTAL = H_SYNC + H_BACK + H_ACTIVE + H_FRONT; // 800 localparam V_SYNC = 2; // 场同步脉冲宽度 localparam V_BACK = 33; // 场后沿 localparam V_ACTIVE = 480; // 有效行数 localparam V_FRONT = 10; // 场前沿 localparam V_TOTAL = V_SYNC + V_BACK + V_ACTIVE + V_FRONT; // 525

逻辑说明:h_count是行计数器,从0计到799,当h_count小于H_SYNC时hsync拉低,形成同步脉冲;有效区间是h_count位于[H_SYNC+H_BACK, H_SYNC+H_BACK+H_ACTIVE-1]之间。场同步类似。参数一旦改成1024x768,这些数值必须全部更换,像素时钟也要从25.175MHz变成65MHz,否则显示器会回报“超出范围”。

实际工程中如果板卡没有25.175MHz晶振,可以用锁相环从50MHz或100MHz主时钟分频得到。PLL输出稳定后要等locked信号拉高再释放FPGA复位,这一步不做,上电后偶发花屏基本是必然的。

3.4 工程参数设计表:分辨率、像素时钟、阈值默认值

参数推荐值说明
图像分辨率640x480与VGA显示一致
像素时钟25.175 MHz可用25MHz近似
灰度位宽8 bit256级灰
输入图像格式RGB565或灰度预处理统一为灰度
阈值T初始值32根据测试图调节
Sobel幅值位宽11 bit绝对值求和前
VGA输出色彩12bit RGB可用1bit二值拉高/拉低
Line Buffer深度640与H_ACTIVE相同

使用25MHz近似25.175MHz时,行频变成31.25kHz,场频约59.52Hz,大多数LCD显示器都能接受,但VGA CRT可能看到轻微横滚,这时候就要用PLL生成精确时钟。

4. 核心代码实战:行缓存、Sobel卷积与VGA控制器怎么连才不“翻车”

理论讲通了,下面直接上可仿真的Verilog代码。我不按完整工程贴,只贴三个最容易写错的模块:行缓存、Sobel卷积、VGA控制器,并且说明参数怎么改,以及仿真时该盯哪些波形。

4.1 行缓存模块:用寄存器堆搭出三行对齐

下面是一个用寄存器堆实现的行延迟链,注意这种写法适合行宽小于800的场合;行宽较大时建议换成RAM原语或FIFO。

module line_buffer #( parameter WIDTH = 8, parameter COLS = 640 )( input wire clk, input wire rst_n, input wire de_in, input wire [WIDTH-1:0] din, output reg [WIDTH-1:0] row0, output reg [WIDTH-1:0] row1, output reg [WIDTH-1:0] row2 ); // 两行数据暂存区 reg [WIDTH-1:0] line0 [0:COLS-1]; reg [WIDTH-1:0] line1 [0:COLS-1]; // 行写入指针 reg [9:0] wr_ptr; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin wr_ptr <= 1'b0; end else if (de_in) begin line0[wr_ptr] <= din; line1[wr_ptr] <= line0[wr_ptr]; row0 <= line0[wr_ptr]; // 上一行像素 row1 <= line1[wr_ptr]; // 上上行像素 row2 <= din; // 当前行像素 wr_ptr <= (wr_ptr == COLS-1) ? 1'b0 : wr_ptr + 1; end end endmodule

逻辑说明:din是当前像素,line0存当前行,line1存上一行。当前像素写入line0时,line0中被替换的旧值被搬到line1,因此row2是当前像素,row0是上一行像素,row1是上上行像素。白纸黑字的“三行并行”在硬件里就是靠这种写入前搬家的方式实现的。

参数说明:WIDTH是灰度位宽,COLS是一行有效像素数。COLS必须和H_ACTIVE严格相等,如果改为800x480分辨率,COLS要改成800,wr_ptr位宽至少10位。de_in必须在有效像素期间为1,消隐期间为0,否则消隐数据会混进窗口。

4.2 Sobel卷积模块:补码转换与绝对值累加

拿到3x3矩阵后,下面的代码在一个周期内算出Gx、Gy和幅值,再与阈值比较。

module sobel_core #( parameter WIDTH = 8, parameter THRESHOLD = 32 )( input wire clk, input wire rst_n, input wire [WIDTH-1:0] p11, p12, p13, input wire [WIDTH-1:0] p21, p22, p23, input wire [WIDTH-1:0] p31, p32, p33, output reg edge_out ); wire signed [10:0] gx = (p13 - p11) + ((p23 - p21) << 1) + (p33 - p31); wire signed [10:0] gy = (p31 - p11) + ((p32 - p12) << 1) + (p33 - p13); wire [10:0] abs_gx = gx[10] ? (~gx + 1) : gx; wire [10:0] abs_gy = gy[10] ? (~gy + 1) : gy; wire [10:0] mag = abs_gx + abs_gy; always @(posedge clk or negedge rst_n) begin if (!rst_n) edge_out <= 1'b0; else edge_out <= (mag >= THRESHOLD) ? 1'b1 : 1'b0; end endmodule

逻辑说明:p11~p33从行缓存模块的3x3窗口接来。gx对应右边减左边,gy对应下边减上边,中心行和中心列乘以2。8位无符号数相减的结果范围从-255到255,所以差值的中间结果需要扩到有符号数;绝对值通过符号位判断,为负就取反加1,mag是绝对值和。

参数说明:THRESHOLD默认32,可以改成输入端口接到外部寄存器,这样下板后能用按键实时调阈值。edge_out在下一拍输出,整个Sobel核心天然带一级流水线延迟。如果组合逻辑路径太长导致时序紧张,可以把gx和gy的计算拆成两级,先把差值算完打一拍,再算累加,这样时钟能跑更高,但输出延迟会多一拍。

4.3 VGA控制器:计数器生成同步信号和有效区间

VGA控制器本身不参与Sobel计算,但它的de信号决定了图像何时被显示,时序不准整个画面都废。

module vga_controller #( parameter H_ACTIVE = 640, parameter H_SYNC = 96, parameter H_BACK = 48, parameter H_FRONT = 16, parameter V_ACTIVE = 480, parameter V_SYNC = 2, parameter V_BACK = 33, parameter V_FRONT = 10 )( input wire clk, input wire rst_n, output reg hsync, output reg vsync, output wire de, output wire [9:0] vga_x, output wire [9:0] vga_y ); localparam H_TOTAL = H_SYNC + H_BACK + H_ACTIVE + H_FRONT; localparam V_TOTAL = V_SYNC + V_BACK + V_ACTIVE + V_FRONT; reg [9:0] h_count; reg [9:0] v_count; assign de = (h_count >= H_SYNC + H_BACK) && (h_count < H_SYNC + H_BACK + H_ACTIVE) && (v_count >= V_SYNC + V_BACK) && (v_count < V_SYNC + V_BACK + V_ACTIVE); assign vga_x = de ? (h_count - H_SYNC - H_BACK) : 1'b0; assign vga_y = de ? (v_count - V_SYNC - V_BACK) : 1'b0; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin h_count <= 1'b0; v_count <= 1'b0; end else begin if (h_count == H_TOTAL - 1) begin h_count <= 1'b0; v_count <= (v_count == V_TOTAL - 1) ? 1'b0 : v_count + 1; end else begin h_count <= h_count + 1; end end end always @(posedge clk or negedge rst_n) begin if (!rst_n) hsync <= 1'b0; else hsync <= ~(h_count < H_SYNC); end always @(posedge clk or negedge rst_n) begin if (!rst_n) vsync <= 1'b0; else vsync <= ~(v_count < V_SYNC); end endmodule

重点说明de信号:h_count和v_count都落在有效区间内de才为1。vga_x和vga_y是相对于有效区域左上角的坐标,正好用来读显存地址。同步信号hsync和vsync的极性是低电平有效,计数器小于脉冲宽度时输出低。参数H_ACTIVE这些必须查标准VGA表,不能自己拍脑袋。

4.4 仿真验证:Testbench里怎么喂数据、看哪里

仿真时我习惯写一段任务,把一张原始图像的灰度值按行输入,然后观察Sobel输出。关键波形包括:

  • de_in是否只在有效区拉高,消隐区保持低
  • 行缓存内的row0/row1/row2是否在同一时刻对应三行同列像素
  • edge_out输出与输入像素之间是否有恒定周期延迟
  • VGA的hsync、vsync周期是否符合800和525

常见坑是Testbench里把数据喂得太快,导致de_in和din错位。正确做法是de_in为1时才发送一个像素,de_in为0时保持din不变。可以先用10x10像素的小图像跑通,再换成640x480,不然仿真波形里信号名都找不全。

仿真通过只能说明逻辑正确,上板能不能跑还得看时序。如果你把Sobel输出直接接到VGA数据线上而不经过缓存,大概率画面左边是一条渐变色条,因为Sobel的流水线延迟没有补偿。正确做法是先把Sobel输出写入RAM,VGA端读RAM时按vga_x和vga_y取地址,这样读写完全解耦,显示端延迟固定为RAM读延迟,不会再受Sobel内部流水线影响。

5. 避坑指南:边缘检测+VGA显示常见的五个翻车现场

不管仿真多么完美,上板总会有意外。这一章列出我在调试Sobel+VGA过程中遇到的五个高频问题,按“现象→原因→解决”记录。

5.1 现象:VGA画面出现明显斜纹或滚动条

原因:绝大多数是行场同步信号极性错误,或者像素时钟不是标准频率。VGA协议中hsync、vsync的极性和前后沿参数随分辨率不同而变化;很多显示器能自动检测极性,但CRT和部分工业屏不行。

解决:先用一个纯色测试图案替代Sobel输出,把VGA时序模块单独拉出来验证。确认H_TOTAL和V_TOTAL数值正确后,再接入Sobel数据流。也可以把hsync、vsync极性通过寄存器配置,先用逻辑分析仪抓波形,确认同步脉冲宽度是96个时钟和2个行周期。如果用的是PLL生成的时钟,还要确认PLL locked信号拉高后是否等100us再释放复位,否则会偶发不同步。

5.2 现象:图像最左边有竖条,或者左边一列像素重复

原因:Sobel核心从数据输入到输出有固定延迟,但VGA有效区间没有做延迟补偿,导致读显存时的地址比写入地址超前或滞后,看起来像图像左移了一截。

解决:在顶层例化时,Sobel输出通过FIFO做一次地址对齐。常见做法是写RAM时把写地址延迟2到3拍,或者让VGA端坐标比写地址提前相应周期。调试时可以输入特殊图案,比如第一行前8列为白色、其余为黑色,观察花边的具体位置,就能算出延迟周期数。我通常会直接比较写地址和读地址,二者偏差时做动态对齐,这样换分辨率也不用改延迟数。

5.3 现象:边缘检测结果断断续续,强边缘却出现双线

原因:阈值设得太高、Gx和Gy方向用反,或者幅值近似用了绝对值最大值导致弱边缘被滤掉。双线和断线经常同时出现,因为阈值高会让边缘宽窄不一致。

解决:把阈值寄存器做成可调,先用最保守的8到16,再逐步加大到32、64。同时检查代码里gx和gy的方向定义:用一张有明显横竖线条的测试图,横线只由Gy响应,竖线只由Gx响应。如果反了,交换p11和p13的位置即可。双线往往是阈值过低而且没有做非极大抑制,这时可以让edge_out只输出幅值局部最大的像素,但简单的做法是把阈值提高,先保证线条连续性。

5.4 现象:BRAM利用率超过90%,布局布线失败

原因:很多人实现时直接为整帧图像开了两个RAM缓冲,一帧存原始图、一帧存边缘图。640x480x8bit每帧约245万个bit,两个帧缓冲直接干爆入门级FPGA的BRAM。

解决:改成行缓存加双FIFO的轻量架构,只在Sobel输出端加一个小的双口RAM做显示缓冲,深度一般256像素即可,开销瞬间下来。如果非要缓存整帧,建议使用SDRAM或DDR,但那样就要处理刷新控制器和仲裁逻辑,复杂度会陡增。入门项目建议别碰帧缓冲,用行缓存才是正路。

5.5 现象:仿真能看到边缘,上板后却完全白屏或黑屏

原因:上板后最常见的隐性坑是复位时序。很多板卡上电后外部复位信号和时钟不同步,FPGA内部寄存器进入亚稳态。还有一个原因是综合工具优化掉了没有输出负载的模块,比如顶层某些内部信号未连接到引脚。

解决:复位用“异步复位、同步释放”的标准电路;仿真模型里加一个上电后200ms的低电平复位脉冲。上板前把所有综合后的警告过一遍,确认没有“unconnected”警告。若还是白屏,用逻辑分析仪抓hsync、vsync和de,只要这三个信号正常,剩下的就是数据通路问题。还有一种情况是Sobel输出极性与VGA显示要求反了,边缘线是白色,背景是黑色,如果VGA RGB信号拉高表示白,edge_out为1时应该送白色,为0时送黑色,要是接反就会看到黑底黑线。

避坑整理完之后,工程基本能达到“仿真正确、上板能跑”的状态。最后聊一个能让调试效率翻倍的进阶技巧。

6. 进阶玩法:把阈值变成可调的旋钮,顺便看梯度方向

基础版本的Sobel阈值是固定的,一旦换场景,边缘效果就得重新综合。我习惯在顶层加一个8位寄存器作为阈值T,用按键或串口写入,实时调节。具体做法是:把sobel_core里的THRESHOLD从parameter改成input wire,每次按键沿到来时T加4或减4,然后通过数码管或串口回显当前值。这样拿到一张新图时,不用重新布线就能快速找到最佳阈值。

另一个值得加的功能是梯度方向输出。Sobel在算出Gx和Gy时,方向就是atan2(Gy, Gx)。FPGA里不用真的算arctan,可以把象限拆成4个或8个区域,例如把梯度方向量化到0°、45°、90°、135°,然后配合边缘二值图,就能去掉某些特定角度的噪声。实现上只需比较Gx和Gy的绝对值和正负号,逻辑开销极小,但做视觉定位时很实用。

我曾经做过一个丝印缺陷检测项目,只用Sobel加固定阈值,误检率居高不下;后来把梯度方向量化加进去,只保留走线方向的边缘,误检率降了一半。从那以后我每次搭建Sobel+VGA工程都会先确认三件事:行缓存是否按de对齐、阈值能不能在运行时调节、有没有保留梯度方向信号。这三个点看起来小,但直接影响调试效率和最终效果。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进