
简介面向咸鱼FPGA平台的人脸检测学习需求代码实现了从肤色模型建立到二值图像输出的完整流程。设计采用YCbCr颜色空间进行肤色识别通过人工阈值法将肤色区域与非肤色区域分离最终生成二值图像适用于需要入门FPGA图像处理或在硬件上快速验证肤色检测算法的开发者。全套资源共2076个文件压缩包约75.39MB以cdb/hdb等Quartus工程数据库文件、tdf/v硬件描述源码为主同时包含sof配置文件、mif初始化数据、rpt报告、pin引脚分配等便于完整复现工程并对照学习布局布线、资源占用与时序结果。已吸引1413人学习浏览说明该案例具备一定参考价值。代码包内除了主干逻辑还保留了大量工程中间文件与少量txt说明、mp4演示可辅助理解工程结构作为人脸检测前端的肤色提取模块能直接迁移到相关图像处理项目中。1. 项目先想清楚FPGA做人脸检测靠不靠谱先说结论用一块咸鱼淘来的FPGA开发板做实时人脸检测完全可行但前提是选对算法路线。我第一次接触这个项目的时候第一反应是上神经网络——YOLO、SSD这些目标检测模型在PC上跑得飞起放FPGA里岂不是降维打击实际查了一圈资源占用后老实了。入门级板卡的逻辑单元撑死几万到十几万LUT跑流水线卷积还行搞完整的深度检测网络不现实除非你有UltraScale级别的资源但那已经不是咱们这种学习项目的讨论范畴了。所以这个项目我最终定的方案是肤色检测 几何特征验证。思路很简单人脸的肤色在YCbCr色彩空间里有非常集中的聚类范围只要把摄像头采集的RGB图像转到YCbCr空间做肤色像素判定再用形态学滤波把噪声消掉最后框出连通域就能实时输出人脸检测框。整个流程完全适合FPGA的流水线结构一拍一个像素延迟只有几行真正做到了实时。这个项目适合谁三类人一是刚学完Verilog语法、想找个综合性项目练手的FPGA入门者二是玩图像处理但一直在PC上用OpenCV、想体验一下“像素级流水线”思维的软件工程师三是纯粹想把手里的咸鱼板子派上用场的人。整套代码下来你至少能摸清楚摄像头接口、I2C配置、图像缩放、色彩空间转换、二值化、形态学滤波、帧缓存这一整条视频处理链路比单纯点个LED灯有价值太多了。1.1 为什么选肤色检测而不是神经网络这可能是整个项目最关键的一个决策。我见过不少人一上来就想在FPGA里跑人脸检测网络查了一堆论文下载了各种量化工具最后卡在资源不够或者时序收敛不了热情直接熄灭。实际算一笔账一个简化的MobileNet-SSD光卷积层的乘加运算就需要数百万次即便用8bit量化也要消耗大量DSP48和BRAM还要处理BatchNorm折叠、激活函数查找表、后处理NMS这些麻烦事。对于一块几万LUT的入门级板卡这基本属于不可能完成的任务。就算勉强塞进去帧率也感人完全失去了“实时”的意义。而肤色检测的算法逻辑用Verilog描述出来大概只要几百行代码。核心计算就是一次颜色空间转换加几次比较器再加上一些行缓存做形态学处理。这部分逻辑在FPGA上跑起来几乎是白送的资源占用2560x1440分辨率都能轻松跑到60帧以上。代价就是检测精度不如深度学习方法但对于学习目的来说能在低端硬件上跑通一整条视觉流水线学到的东西远比“调个现成IP核”要多。我还要强调一点FPGA学习的核心不是算法本身有多先进而是硬件思维。处理一张图像PC上的思路是for循环遍历每个像素FPGA的思路是每个流水线阶段同时处理不同的像素点。这个思维转换只有在这种逐像素流式处理的项目里才能真正体会到。1.2 咸鱼板卡的资源底牌既然是咸鱼板子第一步得搞清楚手里的牌。我用的是一块常见的Xilinx Artix-7系列板卡具体型号就不说了大家拿到手里的可能各不相同。但不管哪块板子你在开始写代码之前必须做到心里有数逻辑单元数量决定你的并行度和流水分级能力。Artix-7入门款大概有1.5万到2万个逻辑单元跑肤色检测完全够用。BRAM大小决定你能缓存多少行图像数据。一般开发板都有几百KB的BRAM而行缓存方案只用到几十KB。DSP数量颜色空间转换涉及乘加运算但用移位加法实现定点计算后DSP几乎用不到这反而让项目在低端板卡上更有普适性。摄像头接口大部分板卡配套OV5640或者OV7725走DVP并口引脚数量大约10-12个速率在100MHz以内对布局布线要求不高。显示接口HDMI或者VGA后者在咸鱼板卡上更常见。我建议你在写第一行代码之前把板卡的引脚约束文件找出来看清楚摄像头时钟、行同步、场同步、像素时钟对应的FPGA引脚编号。这步不做后面综合出来的工程十有八九没法跑因为约束缺失是新手最常见的失败原因之一。2. 流水线设计从摄像头到显示器的一整条链路FPGA图像处理项目的本质就是搭一条数据流水线。摄像头源源不断地吐出像素流FPGA在像素流上做各种运算最后把结果送到显示器显示。理解这条链路的每个环节比背下任何一段代码都重要。2.1 图像采集与预处理摄像头那边OV5640默认输出的是RGB565格式也就是每个像素用16bit表示RGB各占5-6位。但这里有个坑OV5640上电后默认输出模式不是我们想要的分辨率可能是640x480也可能是别的什么需要通过SCCB接口兼容I2C协议写入寄存器配置才能让它按设定的分辨率和输出格式工作。SCCB初始化配置是人脸检测系统第一个容易卡住的环节。OV5640的寄存器手册有一百多页但常用的配置项其实就几个输出分辨率、像素时钟分频、数据格式、镜像方向。网上有很多现成的初始化寄存器序列可以直接抄来用但有一点要提醒不同型号的板卡晶振频率可能不一样如果你的板子用的是24MHz晶振而初始化代码是按24MHz写的就没问题但如果板子配的是27MHz晶振要仔细查看寄存器0x3035、0x3036这些时钟配置参数。预处理这一块我用了一个简单的做法在摄像头输出的数据有效信号de有效时把RGB565拆成R、G、B三个分量各自补齐到8bit然后直接送入后续的颜色空间转换模块。这里不需要额外的FIFO做缓冲因为整条链路是逐像素同步流式处理的只需要处理好行同步和场同步信号的对齐关系。2.2 肤色检测与二值化预处理完的RGB像素第一个正经算法模块就是色彩空间转换。为什么非要用YCbCr而不是直接在RGB空间做肤色判断原因我在后面的原理部分会详细展开这里先记住结论在YCbCr空间里肤色像素的Cb和Cr分量聚集在一个相对稳定的椭圆区域里而RGB空间里肤色容易跟背景混淆。转换完成之后肤色检测变成了一组非常简单的比较操作Cb分量落在某个下限和上限之间Cr分量落在某个下限和上限之间满足条件则输出1否则输出0这样每一帧图像经过这个模块后就变成了一幅二值图像白点是肤色像素黑点是背景。把这幅二值图像直接输出到显示器上你会看到类似红外面板的效果——人脸是白色的一团头发和背景基本是黑色的。这一步完成整个人脸检测系统就已经完成了60%的工作。但二值图像往往有大量椒盐噪声可能是环境光变化引起的也可能是摄像头传感器本身的底噪。这就需要在二值化之后接一个滤波模块。2.3 形态学滤波与人脸框生成形态学滤波我用的是开运算也就是先腐蚀再膨胀。腐蚀操作会消除孤立的噪点膨胀操作会把主体区域恢复回来。两个操作组合起来既能去噪又不至于把整个人脸区域抹掉。实现方式需要仔细说。腐蚀和膨胀都是基于邻域窗口的运算最简单的实现是3x3窗口。但FPGA不像PC那样可以随意访问整幅图像的内存你需要用行缓存来构造窗口。具体做法是用FIFO缓存前两行数据当前行数据直接输入这样任意时刻你都能拿到当前像素所在3x3邻域的全部9个像素。我用的行缓存方案是实例化了两个FIFO IP核深度设为图像一行像素的数量位宽8bit。像素时钟驱动下每来一个新像素FIFO链就往前移一格三个数据输出分别对应上一行对应位置的像素、上一行的下一个像素、当前行像素。这样每个时钟周期都能输出一个完整的3x3窗口流水线完全不间断。这是FPGA图像处理最经典的技巧吃透了它后面学中值滤波、高斯滤波、Sobel边缘检测都是同一个套路。腐蚀和膨胀的实现各有各的诀窍。腐蚀是“所有邻居都为1才输出1”也就是对3x3窗口做与运算膨胀是“任何一个邻居为1就输出1”也就是对3x3窗口做或运算。用Verilog写就是两个always块加一个与门或或门看似简单但时序上有个细节如果直接组合逻辑输出路径延迟可能太长导致时序不收敛我建议在滤波模块的输出端加一级寄存器打拍能有效改善时序。滤波完成之后二值图像已经比较干净了这时候需要一个模块来统计肤色像素的分布定位人脸框。最简单的实现方式是对每一行做游程编码RLE记录连续肤色像素段的起始坐标和长度然后再用一些跨行合并的策略把属于同一个连通域的行段归并到一起。这个算法在PC上写很简单但用Verilog写要稍微动点脑筋因为需要维护多个状态变量。我当时的设计思路是维护一个候选区域数组每个候选区域包含四个参数最小行号、最大行号、最小列号、最大列号。每来一个肤色像素就尝试合并到已存在的候选区域中如果实在合并不了就新建一个区域。这个思路对单个人脸的情况完全没有问题多人的情况稍微复杂一点但对于学习项目来说已经够了。最后把框选区域叠加到原始RGB图像上输出到HDMI或VGA显示器。叠加的原理也简单如果当前像素坐标落在人脸检测框的边框区域内就把像素强制设为绿色或者红色否则输出原始颜色。3. 核心代码实现与讲解这一节进入正题我把代码里最核心的几个模块拿出来拆开讲。工程里文件比较多但主路径上的核心模块就四个SCCB配置模块、RGB转YCbCr模块、肤色检测模块、框选叠加模块。3.1 RGB转YCbCr用移位代替乘法的定点技巧先说色彩空间转换。之前我在PC上用OpenCV做人脸检测时肤色检测这一步基本就是调用cv2.cvtColor然后做inRange根本没有想过底层是怎么算的。写Verilog可没有现成库给你调必须自己把公式写出来。YCbCr转换的完整公式是Y 0.299R 0.587G 0.114B Cb -0.1687R - 0.3313G 0.5B 128 Cr 0.5R - 0.4187G - 0.0813B 128FPGA不擅长浮点计算所以要把浮点系数转成定点。我的做法是向左移位放大到8位整数然后计算完再右移回来。具体地把0.299近似为77/2560.300781250.587近似为150/2560.58593750.114近似为29/2560.11328125三个系数加起来是256/256误差很小不会对肤色判断造成实质影响。对应的Verilog实现// RGB565输入8bit各分量 wire [7:0] R rgb_data[15:11] 3; wire [7:0] G rgb_data[10:5] 2; wire [7:0] B rgb_data[4:0] 3; // 定点计算Y (77*R 150*G 29*B) 8 wire [15:0] y_sum 77*R 150*G 29*B; wire [7:0] Y y_sum[15:8]; // Cb (-43*R - 85*G 128*B) 8 128 wire [15:0] cb_sum 128*B - 43*R - 85*G; wire [7:0] Cb cb_sum[15:8] 8d128; // Cr (128*R - 107*G - 21*B) 8 128 wire [15:0] cr_sum 128*R - 107*G - 21*B; wire [7:0] Cr cr_sum[15:8] 8d128;注意Cb和Cr的计算里可能出现负的中间结果所以在做减法时要保证位宽足够我用16bit中间变量同时在正负号上做了处理避免Verilog里无符号数相减的经典问题。这里有个经验之谈在FPGA里做带符号运算最稳妥的方式是定义有符号数类型而不是靠补码硬算。3.2 肤色判定与行缓存处理转完YCbCr之后肤色检测模块就是一组阈值比较。我用的经典经验阈值范围是分量下限上限Cb77127Cr133173这个范围是学术界通过大量人脸样本统计出来的在多数室内环境下适应性很好。但不同摄像头拍出来的颜色会有偏差所以我把阈值做成了可调参数定义了几个parameter实测下来效果更好。肤色判定代码核心就几句话wire skin_flag (Cb 8d77) (Cb 8d127) (Cr 8d133) (Cr 8d173);这里必须提一个容易踩的坑YCbCr空间里的肤色聚类区域并不是一个正矩形而是一个倾斜的椭圆。用矩形框做近似会引入一些误检尤其是背景中有偏黄或者偏红的物体时容易被误判为肤色。解决方法是加一个额外的限制条件排除掉亮度太低的像素。因为人脸皮肤在正常光照下Y值一般不低于80。这样能过滤掉一部分深色背景的干扰。二值化输出的肤色掩膜在送到形态学滤波之前我先做了一次中值滤波。虽然中值滤波要9个像素的排序逻辑稍费一些资源但对消除椒盐噪声的效果比单纯的腐蚀要好而且代码也不复杂。中值滤波的经典实现方式是3x3窗口内对9个像素按行、列、对角线做三段排序具体做法网上资料很多这里不赘述。3.3 框选叠加与输出显示框选叠加是整个流水线的最后一段。这里需要知道当前扫描像素的坐标而坐标信息是从行场同步信号里恢复的。// 行计数器h_sync有效时清零像素有效时累加 always (posedge pclk) begin if (h_sync) h_cnt 0; else if (de) h_cnt h_cnt 1; end // 场计数器v_sync有效时清零每行结束时累加 always (posedge pclk) begin if (v_sync) v_cnt 0; else if (h_sync) v_cnt v_cnt 1; end有了坐标框选就简单了。检测模块计算出人脸区域的左上角坐标和宽高后只要当前像素坐标落在四条边框线上就把输出像素强制置为绿色24h00FF00否则保持原始图像数据。边框宽度的逻辑可以让几根像素列同时变色效果更明显。关于输出的时序有个地方特别容易出错从检测模块算出的坐标是相对于有效图像区域的而显示输出也有自己的有效区。如果摄像头分辨率、显示分辨率、存储分辨率三者不一致坐标对齐要格外小心。我的做法是整个工程只用一套分辨率处理比如摄像头输出640x480显示也输出640x480的VGA时序这样坐标对所有模块都是统一的省去了大量的坐标换算工作。如果你买的板卡是HDMI输出那需要额外做一层分辨率适配建议用现成的HDMI IP核自己写时序编码器会很痛苦。4. 仿真、上板与排错代码写完了千万别直接综合跑板子。先仿真是必须做的而且仿真要做得细不是跑一个功能完事要把每一级流水线的中间结果都拉出来看一遍。这一节分享我实际调试中积累的经验。4.1 仿真环境的搭建思路我的仿真顶层模块里例化了被测模块同时写了一个模拟摄像头时序的testbench。这个testbench做的事很简单就是按时钟产生de、hsync、vsync信号然后循环喂入一幅预先设计好的测试图案。测试图案的选择上有讲究。我第一次直接用了真实摄像头采集的图片转成hex文件喂给仿真结果波形密密麻麻根本看不出问题在哪里。后来我学聪明了做了一幅只有几个色块的简单图案上面一半是接近肤色的纯色块比如R180G130B100下面一半是蓝色背景R20G50B200。这样的图案经过颜色转换和肤色检测后输出应该是一个清晰的矩形区域。用这个简单的测试图案我很快定位到了坐标计算偏移的问题。仿真时有一个必须重视的点fifo需要预留足够的初始延迟。行缓存FIFO在前几个像素时钟周期里是没有有效数据的如果不处理这个初始延迟图像的前几行会错位。我的处理方式是在模块内部用一个状态机在第一个有效行到来之前拉低输出有效信号等到FIFO填满两行之后才放行数据。这样虽然浪费了几行像素的时间但换来的是后续逻辑的稳定性。4.2 上板调试ILA探针与肉眼调优仿真通过只能说明逻辑正确上了板子还会遇到各种“活见鬼”的问题。这时候就要请出ILA集成逻辑分析仪也就是Vivado里内置的逻辑探针。ILA的使用方法很简单综合之后在网表视图里选择要观察的信号设置触发条件把比特流下载到板子上然后通过JTAG实时采集波形。我主要用ILA观察了几个关键信号SCCB配置是否写完、摄像头像素时钟是否稳定、肤色检测输出的行同步是否切齐。实测中我遇到的最大问题是输出图像的错位。具体现象是肤色检测区域在垂直方向上有规律的错位大约偏移了3到4行。排查到最后问题出在形态学滤波模块的行缓存上——因为我用了3x3窗口窗口中心相对于输入数据天然有1行延迟形态学滤波又加了1级流水线延迟再加上中值滤波的延迟总的行偏移累积到了3行以上。解决方法是给行同步和场同步信号打相同的节拍延迟让同步信号和数据始终对齐。这个坑在图像处理管线里非常典型只要用到行缓存或者窗口滤波就一定要记得把同步信号一起打拍。显示端还有一个常见问题VGA输出的画面有轻微的行抖动。这个通常是电源纹波或者时钟抖动造成的跟代码关系不大但可以通过在输出端加一级寄存器来降低毛刺的影响。另外如果板载晶振在摄像头和FPGA之间没有同源可能出现偶发的像素错位这时候可以在时序约束文件里把摄像头的像素时钟设为false path避免Vivado强行做跨时钟域约束产生一堆警告。5. 咸鱼板卡实操避坑手册最后集中写一写我在这个项目里从翻车到翻盘的过程。咸鱼板子不像官方开发板资料经常残缺不全很多问题需要自己摸索下面这些是我实测总结的经验希望你能少走几步弯路。5.1 拿到手先做的事第一件事永远都是确认板卡型号和核心芯片型号。丝印上可能写得很模糊拍照上网搜是最快的或者直接打开Silicon Labs的识别工具看芯片ID。确认芯片之后去官网下载对应的数据手册和封装引脚图对照板卡原理图把关键信号找出来。第二件事是检查晶振频率。很多咸鱼板卡为了降低成本用了非标晶振比如24MHz、27MHz、50MHz都有这会直接影响PLL配置和串口波特率。如果你发现摄像头能出图像但SCCB配置失败优先怀疑晶振频率不对。第三件事是检查电源。FPGA开发板一般需要多路电源最常见的问题是核心电压不对导致程序无法加载。我买过一块板子上电后配置灯常亮但FPGA不工作检查后发现板卡上的AMS1117稳压芯片输出实际只有1.0V而FPGA核心电压需要1.2V换了个稳压芯片就好了。5.2 工程编译与约束文件的坑咸鱼板卡最缺的资料就是XDC约束文件。有些卖家甚至不提供原理图只能靠万用表飞线测量。我建议你拿到板卡之后自己花半小时对照原理图把关键引脚整理成一张表格信号名、FPGA引脚号、电平标准、用途。这样后续写约束的时候就不会出错了。关于电平标准早期我犯过一个错误OV5640的IO口供电是1.8V还是3.3V不同板子设计不同。如果约束文件里写成LVCMOS33但实际摄像头供电是1.8V轻则图像花屏重则烧坏摄像头。做约束的时候一定要确认摄像头的VDDIO电压再选择电平标准。还有一点很多人忽视Xilinx FPGA的引脚分布和Altera完全不同同一份代码换到另一块板子可能要重新做引脚分配。所以整个工程在设计时就应该把摄像头数据引脚做成模块参数不要硬编码成具体引脚号这样移植起来会省很多事。5.3 常见问题排查对照表现象可能原因排查方法摄像头不出图像SCCB配置失败ILA抓取SCCB时序确认ACK信号画面全黑但有时序行场同步方向反了检查摄像头寄存器配置镜像方向肤色区域碎片化阈值不合适或者噪声太大调整Cb/Cr阈值加强中值滤波检测框抖动坐标边界未做滤波对框坐标做一级滑动平均VGA输出色彩偏绿数据位宽对齐错误检查RGB565到RGB888的拼接逻辑整幅图像颜色偏移白平衡未初始化配置OV5640的AWB寄存器5.4 关于性能优化与后续扩展的一点心得肤色检测方案在室内单一背景下效果不错但搬到复杂环境里误检率会明显上升。如果你想让这个项目更有挑战性可以沿着两个方向扩展。第一个方向是检测算法升级。在肤色检测后面接一个人眼或者嘴部验证模块用简单的模板匹配或Haar特征验证候选区域能有效去除背景中肤色类物体造成的误检。Haar特征在FPGA上的实现并不复杂本质上就是一组加法和比较运算网上有现成的参考设计。第二个方向是帧率与分辨率的提升。目前的流水线在720P分辨率下跑60帧绰绰有余如果你的板卡资源足够可以考虑把摄像头输出提到1080P这时要注意DDR3或DDR4存储器的接入把帧数据缓存到DDR里方便做更复杂的后处理。最后分享一个心得这个项目真正的价值不是“在FPGA上实现了人脸检测”这个结果而是通过亲手搭建整条图像流水线理解了视频数据在硬件上是怎样流动的。从SCCB配置到像素时钟从行缓存到同步信号对齐每一个环节都充满了硬件工程师独有的思维模式。走完一遍这个流程你再回去看OpenCV那些函数视角是完全不一样的。本文还有配套的精品资源点击获取