十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA实现实时人脸检测:从摄像头采集到HDMI显示的完整流水线设计

FPGA实现实时人脸检测:从摄像头采集到HDMI显示的完整流水线设计 简介面向FPGA图像处理学习者的完整工程代码以咸鱼FPGA开发板为载体实现人脸检测中的肤色提取环节。资源基于YCbCr颜色空间采用人工阈值法将肤色与非肤色区域分离通过二值化图像完成目标分割适合正在学习图像处理算法硬件落地、Verilog编程或完整FPGA项目流程的开发者参考。压缩包内含2076个文件整体约75.39MB以cdb、hdb、tdf、v等FPGA工程与源码文件为主同时包含txt说明、rpt报告、jpg图片及sof配置文件等覆盖从RTL设计、综合布局布线到仿真验证的常见文件类型便于对照工程结构理解各阶段产物。已有1413人学习下载。通过该资源可获取一整套可直接查看的肤色检测FPGA实现包括顶层模块、逻辑综合脚本和硬件配置有助于快速掌握颜色空间转换、阈值比较器设计以及二值图像输出的具体写码思路。 闲鱼上收了一块二手的FPGA板子加起来花了三个周末把基于FPGA的人脸检测从零写到了能跑通。这篇博文先说结论整条链路是摄像头采集图像经过灰度化、中值滤波、肤色分割和候选框筛选最后通过HDMI把结果输出到显示器。代码我已经整理好放在项目仓库里下面先把整体设计思路拆开讲再把几个核心模块的Verilog代码逐段解释。这套实现完全是为了学习研究几十块钱的摄像头模块加一块三百块左右的二手板子就能搭起来。有人可能会问树莓派加OpenCV半小时就能出结果何必在FPGA上折腾我在这篇文章里想说明的恰恰是FPGA把人脸检测变成一条可感知的数据流水线像素像工件一样在流水线上移动每一级都做一件具体的事局部性很强、并行性很高这种过程对理解图像处理底层非常有帮助。如果你手上只有一块入门级板子又想做点看起来复杂但实际可控的项目这篇文章应该正合适。1. 项目定位与整体架构1.1 为什么在FPGA上做人脸检测很多初学者接触FPGA都是从呼吸灯、数码管、串口回环开始的玩过一轮之后很容易陷入“不知道下一步做什么”的迷茫。人脸检测恰好是一个非常合适的进阶项目它的算法链路足够长能覆盖从图像采集到输出显示的完整流程但又没有复杂到要靠大规模神经网络才能撑起来。选FPGA而不是CPU/GPU主要看中的是实时视频流处理的天然流水线特性。CPU处理一帧图像是把数据从内存搬来搬去计算完再搬回去FPGA不一样像素数据从摄像头进来后可以像水一样顺着模块间搭建的管道流动每个时钟周期都有一批像素在不同阶段被处理。换句话说在CPU上做检测是“一批一批来”在FPGA上是“一拍一拍过”帧率很容易做到稳定。另外还有一个很实际的原因二手板子价格便宜坏了不心疼。我用的这块板子带OV5640摄像头接口、SDRAM、HDMI输出功能上正好覆盖需求。做这个项目最核心的一条经验是先确认板卡的外设接口有现成例程否则自己从零搓摄像头时序和SDRAM控制器光这两个部分就够写一个月的代码。1.2 整体数据流设计我的方案整体分成五个阶段摄像头采集OV5640输出RGB565格式分辨率设为640x480帧率60fps的配置下带宽压力适中。色彩空间转换把RGB565拆成R、G、B三分量计算灰度值Y同时也保留RGB用于后续肤色判断。图像预处理用3x3中值滤波去除摄像头噪声这一步对肤色的稳定性有很大帮助。人脸检测基于肤色模型进行像素级二值化然后通过行列投影统计锁定人脸候选区域再用宽高比做最后过滤。显示叠加把判定结果用矩形框叠加到原始图像上通过HDMI输出。数据流里最容易忽略的是缓冲设计。摄像头来的像素时钟通常是24MHz或48MHz而SDRAM和HDMI分别在不同时钟域像素数据必须要经过异步FIFO跨时钟域。我一开始图省事直接把像素塞到同一时钟域里结果图像经常出现横向撕裂这个坑后面在调试章节会专门讲。2. 核心Verilog模块详解2.1 顶层模块与端口规划顶层模块的职责是例化所有子模块、连接时钟和复位、定义外部接口。一个规范的顶层模块应该做到“一眼看去就知道这个板卡有哪些外设”这也是项目可维护性的第一道保障。下面是我这个项目的顶层端口定义module face_detection_top( input wire clk_100m, input wire rst_n, // 摄像头接口 input wire cam_pclk, input wire cam_vsync, input wire cam_href, input wire [7:0] cam_data, output wire cam_scl, inout wire cam_sda, output wire cam_rst_n, output wire cam_pwdn, // SDRAM 接口 output wire sdram_clk, output wire sdram_cke, output wire sdram_cs_n, output wire sdram_we_n, output wire sdram_cas_n, output wire sdram_ras_n, output wire [1:0] sdram_bank, output wire [12:0] sdram_addr, inout wire [15:0] sdram_dq, // HDMI 输出 output wire hdmi_clk, output wire hdmi_de, output wire hdmi_vsync, output wire hdmi_hsync, output wire [23:0] hdmi_rgb );端口规划有几个需要注意的点。摄像头数据是8位并行的但OV5640输出的是RGB565实际操作时是把高字节和低字节分开拼接起来的所以我在顶层专门加了一个cam_din[15:0]的内部信号做字节拼装。SDRAM的数据总线是16位双向端口不能直接连到任意寄存器上必须通过三态门控制这一点初学者特别容易翻车。HDMI我直接采用了RGB888输出省去了转换成TMDS的过程因为我的板子带有HDMI发送芯片只需要给出标准的DE、VSYNC、HSYNC和像素数据即可。顶层之下我分了六个子模块摄像头配置模块I2C、像素采集模块、预处理模块、检测模块、SDRAM读写模块、显示输出模块。各模块之间用握手信号valid和ready连接没有用全局使能信号去控制这样后续加流水线级数或者换分辨率时不需要动整条链路。2.2 预处理灰度化与3x3中值滤波灰度化用的是最经典的加权公式Y0.299R0.587G0.114B。FPGA里做浮点运算是很浪费的所以我用整数移位和加法近似module rgb2gray( input wire clk, input wire rst_n, input wire [7:0] r, input wire [7:0] g, input wire [7:0] b, output reg [7:0] gray ); // Y (76*R 150*G 29*B) 8 reg [15:0] y_r; reg [15:0] y_g; reg [15:0] y_b; reg [15:0] y_sum; always (posedge clk or negedge rst_n) begin if (!rst_n) begin y_r 16d0; y_g 16d0; y_b 16d0; y_sum 16d0; gray 8d0; end else begin y_r r * 16d76; y_g g * 16d150; y_b b * 16d29; y_sum y_r y_g y_b; gray y_sum[15:8]; end end endmodule这里我故意用了三级流水线每一级只做一次乘法和一次加法。为什么不直接用组合逻辑一步算完因为摄像头像素时钟较高时组合逻辑路径过长会导致时序不收敛内部乘法器再好也架不住路径上串太多东西。拆成流水线后虽然结果会晚两个时钟周期出来但对于视频流来说这点延迟完全不影响。中值滤波我用了3x3窗口。实现思路是用两个LineBuffer缓存前两行数据第三行数据实时进来组成三行三列的矩阵。这里最麻烦的不是排序而是行缓存的管理。很多初学者容易把LineBuffer写成FIFO但实际上用移位寄存器数组更合适因为我们需要同时取到第0、1、2列的数据。如果用了FIFO就得多做几次读操作时序上会很紧张。排序部分常规做法是32个比较器组成排序网络但对3x3取中值有个更简洁的办法分别对三行做冒泡排序取中值得到三个行中值再对这三个值排序取中值。这个近似结果精度够用资源少很多。排序过程我用的是纯组合逻辑因为是9个8位数的比较路径还不算太长。2.3 人脸检测逻辑肤色分割与候选框筛选人脸检测的核心我选了肤色模型这是性价比最高的算法。YCrCb颜色空间下肤色在Cr约77到127、Cb约133到173的椭圆区域内聚集用两个比较器就能完成像素级分类。RGB直接转YCrCb要算矩阵乘法我更推荐用一个查表法先算出色度分量Cr R - YCb B - Y然后在这个差值空间里做阈值判断。module skin_detect( input wire clk, input wire rst_n, input wire [7:0] r, input wire [7:0] g, input wire [7:0] b, output reg skin ); wire [7:0] y (r*8d76 g*8d150 b*8d29) 8; wire signed [8:0] cr $signed({1b0, r}) - $signed({1b0, y}); wire signed [8:0] cb $signed({1b0, b}) - $signed({1b0, y}); always (posedge clk or negedge rst_n) begin if (!rst_n) skin 1b0; else if ((cr 8d127) (cr 8d167) (cb 8d113) (cb 8d161)) skin 1b1; else skin 1b0; end endmodule阈值范围是OpenCV官方肤色模型在YCbCr空间的常用值但实际用的时候必须结合你的摄像头做微调因为不同摄像头白平衡差异很大。我板子上的OV5640明显偏红导致大量背景像素被误判成皮肤后来我把Cr下限从127调高到138才缓解。像素级二值化只是第一步得到的是像雪花一样散落的皮肤点。要想变成人脸框还需要做连通区域分析。全片级的连通区域标记在FPGA上非常消耗存储所以我的策略是行列投影统计每行和每列中皮肤像素的数量当某行和某列同时超过阈值时认为这个交叠区域是候选框。这个办法很像直方图法优点是只需要行求和寄存器、列求和寄存器各一套资源开销极低。候选框确定后我会用几个简单规则过滤宽高比在0.6到1.8之间、面积大于某个像素数、区域内肤色密度大于40%。这些参数都是运行时通过拨码开关或者串口命令去调的我强烈建议你也这么做因为写死在代码里调试一次就得重新综合一次效率太低。实测下来在光线合适的室内检测率能到七八成误检大概每几帧一两次对学习项目来说已经完全够用。3. 仿真调试与性能优化3.1 用Testbench做像素级仿真很多人拿到FPGA板子习惯直接上板调但我这个项目奉劝先仿真再上板。原因很简单人脸检测链路太长了任何一个中间模块出错最后显示出来的图像都是乱的你根本不知道问题出在哪个环节。仿真时可以在每个模块后面加监视断言像素值不对当场就能抓出来。我的Testbench做法是为每个模块单独写一个最小激励然后在顶层再做一个集成测试。举个例子灰度化模块的仿真就非常简单timescale 1ns / 1ps module tb_rgb2gray; reg clk 0; reg rst_n 0; reg [7:0] r 8d255; reg [7:0] g 8d0; reg [7:0] b 8d0; wire [7:0] gray; rgb2gray uut( .clk(clk), .rst_n(rst_n), .r(r), .g(g), .b(b), .gray(gray) ); always #5 clk ~clk; initial begin #20 rst_n 1b1; #20 r 8d100; g 8d150; b 8d200; #20 r 8d10; g 8d20; b 8d30; #100 $finish; end endmodule测试彩色纯红色时预期灰度值应该是255*0.299约等于76误差在1以内。如果仿真出来的结果偏差太大那就是查找表或者移位近似的地方写错了。这类问题如果直接上板显示画面整个发绿或者发紫你甚至不会想到是灰度化的问题。集成测试我更推荐用真实的图像数据转成十六进制文件喂给仿真。做法是把一张BMP图片用Python脚本读出来生成一行行像素值Testbench读这个文件模拟摄像头输出最后再输出成文件。跑完后用Python把结果图像重新拼出来肉眼一看就知道链路哪里断了。这个方法相当于在纯逻辑环境下做了一次“软硬件协同验证”。3.2 资源占用与帧率瓶颈做完实现之后我重点看了资源和时序报告。总体资源如下表模块占用Slice寄存器占用Slice LUT说明摄像头采集约400约600时序逻辑为主灰度化与中值滤波约1200约18003x3窗口缓存是大头肤色分割约150约200比较器和减法器行列投影检测约600约900行/列求和寄存器SDRAM控制器约900约1100突发读写状态机HDMI输出约200约300像素时序生成整个工程大约用掉了板子上一半的LUT和六成寄存器资源余量还算健康。真正卡脖子的不是逻辑资源而是SDRAM带宽。我设的分辨率是640x48060fpsRGB565像素一帧约614KB一秒钟要读写36MB左右的数据再加上刷新开销SDRAM带宽占用率很容易到80%以上。如果后续想升级到更高分辨率就得改用DDR3或者对图像先做缩放否则带宽必然不够。帧率方面检测逻辑本身是逐像素流式的几乎不影响帧率瓶颈都在存储和显示。实测开检测和关检测帧率没有差别都能稳定在60fps。这也正是FPGA方案最值得骄傲的地方算法增加的延迟只有固定几个时钟周期不像CPU方案处理一帧可能要用掉几十毫秒。4. 常见问题与避坑指南4.1 显示与内存相关的坑我把调试过程中遇到的现象、原因和最终解决办法整理成了表格这些情况几乎每个做过FPGA图像项目的人都会碰到现象原因分析解决办法图像横向撕裂、上下半屏错位跨时钟域没有做好像素写入和读取时钟不同步引入异步FIFO写指针和读指针分别用各自的时钟域计数花屏且颜色随机跳变SDRAM初始化时序不对或者读写切换没有预留tRP/tRCD时间规范状态机加至少2拍的空闲间隔图像有规律性竖条纹SDRAM突发长度和行大小不匹配将SDRAM突发长度设为8每次写入16像素对齐HDMI无输出但有背光像素时钟或者DE时序不对用示波器量像素时钟检查水平同步参数是否匹配显示器EDID图像偏色严重摄像头白平衡和AWB配置没有生效确认I2C寄存器写成功对比寄存器回读值最让我印象深刻的是第一个问题。一开始我用的是单时钟FIFO总觉得摄像头像素时钟和显示像素时钟都对上了结果发现两个时钟虽然标称频率一样但来源不同相位差累积到一定量就会导致FIFO溢出或者读空。后来改成双时钟FIFO并且在写端做余量检测当剩余空间不足四分之一时暂停写入才彻底解决。4.2 检测效果相关的调试心得如果检测效果不理想不要去怀疑算法不好用先怀疑输入图像质量。中值滤波虽然能滤掉脉冲噪声但对低照度下的噪点处理能力有限。我做的第一版在宿舍日光灯下测试对面墙上有块浅色污渍肤色分割直接把它当成脸框出来了。后来我在白平衡校准上做了文章又加了一步最大类间方差二值化的变体但最终帮助最大的还是把行投影阈值从固定值改成了自适应值——用最近N行的均值乘以系数作为当前行的判定阈值。还有一个非常容易踩的坑不要把肤色阈值调得太“完美”。当阈值刚好在当前场景下检测完美时往往意味着在其他场景下彻底失灵。我的建议是刻意在偏暗和偏亮的环境下各拍一组样张用代码统计皮肤像素的Cr/Cb分布两个环境分布的交集才是合适范围而不是人工盯着屏幕调出来的“感觉值”。模型无法区分人脸和肤色相近的物体这是一个绕不开的局限。我在视频流里加了简单的帧间约束如果上一帧在此区域检测到人脸下一帧只有当肤色密度下降超过一半时才判定为消失否则认为当前帧检测到的人脸和上一帧是同一个目标。这个办法有些“作弊”但用来做演示效果极好基本看不到检测框闪烁。5. 后续扩展与个人体会这个项目做完之后我最大的收获不是“我能用FPGA检测人脸了”而是理解了流水线思维对硬件设计的重要性。软件工程师写代码总是在围绕数据结构和算法转硬件工程师想的却是每一拍数据应该走到哪个模块、哪个寄存器应该保存什么状态。两者思维模式差异非常大但恰恰是这种差异让FPGA方案在视频处理领域保持不可替代的地位。如果后续要继续扩展我会考虑两条路。一是把肤色模型替换成简单的Haar特征检测器FPGA上可以用多个滑动窗口并行的方式实现资源消耗可控但检测准确率会明显上一个台阶。二是换成带DSP的器件做小型卷积网络推理比如把二值化神经网络前两层放到PL端后续放在ARM端既能充分利用FPGA并行性又不用写过于复杂的互联逻辑。这条路我还在探索等有新进展了再写一篇新的博客分享。最后再给一个实用小技巧调试帧率问题时不要把检测开关直接打到0或1而是把检测结果用一个计数器统计出来通过串口dump到PC上。这样做的好处是你不用盯着显示器就能准确知道每个场景下检测了多少帧、误检了多少次数据驱动调整远比肉眼判断可靠。调试图像算法的本质就是不断用数据验证直觉FPGA给了你这种即时反馈的能力别浪费它。本文还有配套的精品资源点击获取
返回列表