资讯详情

资讯详情

FPGA手势识别源码解析:DVP采集、肤色分割与Verilog实现

简介面向FPGA与图像处理开发者的一套原创手势识别工程采用Verilog语言实现提供静态手势识别例如OK、V字、动态手势识别挥手、摇手指和跟踪手势轨迹三种工作模式覆盖从摄像头图像采集、预处理、特征提取到匹配决策的完整数字逻辑处理链路。整套系统为原创设计经过验证可实际投入使用压缩包为rar格式共523个文件、约22.12MB核心代码以Verilog源码为主同时包含VHDL文件、Xilinx ISE工程文件、UCF约束、Tcl/do仿真脚本、NGC网表以及PDF说明文档可直接加载工程进行仿真与上板调试。目前已有4229人学习下载。配套说明文档系统梳理了整体架构、关键模块的Verilog实现逻辑、FPGA配置与硬件调试流程、使用步骤和扩展优化方向能帮助开发者深入理解FPGA并行图像处理、手势识别算法落地以及人机交互系统设计适合研究者、工程师与竞赛团队作为学习、改造或二次开发的基础。1. 手势识别在FPGA上到底能不能落地这套源码给了个能跑的答案手势识别在FPGA上算不上一件轻松的事摄像头时序、像素缓存、二值化、状态判断全部挤在同一个工程里网上能找到的参考代码又多半是残缺的仿真壳子一综合就报错。这份资源是作者自己写的原创源码加说明文档不是网上拼出来的旧工程直接覆盖了从DVP摄像头采集、RGB转YCbCr肤色分割、形态学滤波到手势判定的完整链路配套testbench和说明文档目标平台是入门级FPGA开发板。它适合两类人做毕设或竞赛需要现成参考的以及想搞懂FPGA图像处理管线、但不想从零把坑全踩一遍的从业者。拿到代码先别急着上板把说明文档里的工程结构和信号定义读一遍后面会少走很多弯路。2. 先吃透FPGA手势识别的完整链路从像素流到手势判定手势识别在FPGA上和PC上最大的区别在于数据流。PC上是“抓一帧图然后跑算法”FPGA上是“像素一个一个进来边进边算”。这意味着整个算法必须拆成流水线采集、颜色转换、分割、特征统计、状态输出每一级都在同一个像素时钟下推进。理解了这条链路再打开源码就会发现每个模块的位置都顺理成章。2.1 从摄像头到帧缓存DVP接口与RGB888的硬件处理摄像头输出接口有两种常见形态一种是MIPI CSI一种是DVP并行接口。这套代码包选的是DVP也就是PCLK、VSYNC、HREF、DATA[7:0]这四组信号。DVP的优势在于时序直观FPGA直接采样数据线就行不需要做差分串行解串非常适合入门级开发板和教学场景。代价是带宽有限所以分辨率一般控制在VGA左右数据位宽8位每个像素时钟传一个字节。采集模块里最关键的是处理好行场同步。VSYNC拉高表示一帧开始HREF拉高表示一行有效数据PCLK的上升沿采样像素。如果这三个信号没对齐后面的肤色检测全是错位的。常见做法是把行计数和帧计数做成两个计数器HREF有效期间累计列号VSYNC有效期间刷新帧号。帧缓存用双口RAM即可一帧写入的同时上一帧可以被识别模块读出避免画面撕裂。颜色空间转换是手势识别绕不开的第一步。摄像头直接给的是RGB888但RGB三个分量高度相关肤色在不同光照下R、G、B会一起漂移阈值很难固定。转到YCbCr之后亮度Y和色度Cb、Cr分离肤色在CbCr平面上的聚类相对稳定。RGB到YCbCr的硬件实现不用浮点用定点乘加再右移8位就够了代码包里的rgb2ycbcr.v就是按这个思路写的// RGB888 to YCbCr, BT.601 定点近似系数整体放大约256倍 wire signed [15:0] y_tmp 16sd77 * r 16sd150 * g 16sd29 * b 16sd4096; wire signed [15:0] cb_tmp -16sd43 * r - 16sd85 * g 16sd128 * b 16sd65536; wire signed [15:0] cr_tmp 16sd128 * r - 16sd107 * g - 16sd21 * b 16sd65536; assign y y_tmp[15:8]; assign cb cb_tmp[15:8]; assign cr cr_tmp[15:8];逻辑说明三个通道的系数都扩大了256倍Y的常量偏置4096对应标准里的16再乘256Cb和Cr的偏置65536对应128乘256。截取高8位等于做了一次右移8位等价于除以256取整。这样做的好处是综合工具只会生成乘法器和加法器不会去推断浮点IP核。参数说明这套系数按BT.601标准取近似值实际肤色聚类效果对系数精度不敏感关键是后续阈值要按同一套公式的偏置来定例如用了偏置128的CbCr阈值也要按128居中对称否则肤色区域整体偏移后面怎么调参数都压不住。2.2 肤色分割与形态学滤波阈值怎么定、噪声怎么去肤色分割听起来是最简单的环节就是拿Cb、Cr和阈值比一比但在硬件里真正影响结果的是阈值本身的可靠性和二值化之后噪声的规模。代码包里的默认阈值是Cb在77到127之间、Cr在133到173之间这是基于大量室内光照样本统计出来的经验区间。注意这个区间是按CbCr带128偏置算的如果前面转换模块改成了无偏置版本阈值要整体减128两边对不上就会导致整帧图像没有任何肤色像素。二值化输出很简单就是一个高电平表示“这个像素是肤色”。但在实际画面里背景中的木色桌面、黄色书皮、暖色灯光都会落进这个区间形成大片椒盐噪声。所以在肤色分割和特征统计之间代码包里垫了一个3乘3的形态学滤波模块先腐蚀再膨胀。腐蚀要求窗口内9个像素全是肤色才保留中心点用来打掉孤立的亮点膨胀是窗口内只要有1个肤色像素就置1用来把断裂的手部区域重新连起来。3x3窗口在FPGA上用两个行缓存加九个寄存器实现资源开销很小。// morph_filter.v 中的腐蚀核心逻辑窗口为 3x3 wire window_all p11 p12 p13 p21 p22 p23 p31 p32 p33; reg eroded_pixel; always (posedge clk) begin if (fsync) eroded_pixel 1b0; else eroded_pixel window_all; end逻辑说明p11到p33是当前像素邻域的9个二值化结果window_all要求全部为1。因为是流水线结构每个时钟周期窗口滑动一个像素腐蚀结果天然延迟两拍输出但不影响整条链路的吞吐。膨胀模块和它对称只是把与逻辑换成或逻辑。参数说明腐蚀和膨胀的窗口大小直接影响手部边缘。窗口太小指纹和手毛这类高频细节会产生大量残留噪声窗口太大手指之间的缝隙会被填上导致后续特征统计把五根手指识别成一块。代码包里默认3x3室内环境下不用动室外背光场景可以考虑改5x5但要同步把特征统计的最小面积阈值调大否则手掌会被完全腐蚀掉。2.3 特征统计与手势判定面积、宽高比、帧间位移怎么用二值化之后真正的“识别”就开始了。这个代码包没有上卷积神经网络FPGA上也跑不动它用的是传统图像特征肤色区域的面积、包围盒宽高比、质心位置以及相邻帧之间的位移。这些特征的计算全部是累加和比较非常适合硬件并行。特征统计模块会在每一帧的有效像素期间做两件事一是累加肤色像素总数得到面积area二是记录肤色像素出现的最小行号、最大行号、最小列号、最大列号从而得到包围盒。帧结束之后用这四个边界值算出宽度和高度再用宽高比判断手型的大类。例如五指张开时宽高比接近1并拢手掌时宽高比明显大于1。挥手则靠质心横坐标的帧间变化量来判断连续两帧的质心x坐标差超过一个阈值就认为发生了一次水平挥动。// gesture_core.v 判定逻辑节选 if ((area AREA_MIN) (area AREA_MAX) (width_reg * 10 height_reg * 8) (width_reg * 10 height_reg * 12)) begin gesture_result GESTURE_OPEN_PALM; end else if (dx_abs MOVE_X_THRESHOLD) begin gesture_result GESTURE_WAVE; end else begin gesture_result GESTURE_NONE; end逻辑说明第一段判断覆盖“张开手掌”的条件面积在合理区间内宽高比在0.8到1.2之间。第二段判断覆盖“挥动”无需关心手型只看质心位移。因为这两个条件可能有重叠代码里用if-else if做了优先级排序优先识别静态手型再识别动态挥动。参数说明AREA_MIN和AREA_MAX必须按实际分辨率调整。VGA分辨率下室内伸开手掌肤色面积通常在几千到一万像素左右但这和镜头距离、手掌大小强相关。拿到源码后先打印一帧面积统计值再反推阈值不要直接用默认值。MOVE_X_THRESHOLD同理设置过小会把手掌轻微颤抖都识别成挥手设置过大则挥动半幅画面都不触发。3. 源码包结构拆解文件地图、模块接口与主状态机说明文档的价值在这个包里体现得很明显。没有文档的Verilog工程接手时只能靠信号名猜功能有了文档模块边界和接口约定都是定死的。下面按代码包的实际组织顺序说建议对照着源码看。3.1 代码包文件地图每个文件负责什么代码包解压之后是一套完整的工程目录含源码、约束、测试文件和说明文档。我当时整理下来的文件职责大致如下表文件名类型职责gesture_top.v顶层模块例化所有子模块分配时钟、复位、引脚cam_capture.v采集模块处理DVP时序输出行帧同步信号和RGB888像素流rgb2ycbcr.v预处理模块把RGB888转换成YCbCr输出Y/Cb/Cr三路skin_detect.v分割模块按阈值输出肤色二值信号skin_flagmorph_filter.v滤波模块3x3腐蚀膨胀输出干净的二值图gesture_core.v识别模块统计面积、宽高比、质心位移输出手势结果tb_gesture.v仿真测试文件产生时钟、复位和模拟像素激励doc/说明文档文档引脚表、时序说明、模块接口定义、调试记录这个文件划分有一个明显的好处就是每个模块都能独立仿真。我一般会按依赖顺序从下往上单独测先测rgb2ycbcr再测skin_detect最后把gesture_core单独灌入手工构造的统计值确认状态机响应正确再整链路联调。3.2 顶层模块接口三组信号线各管什么顶层模块基本没有逻辑就是例化。但引脚分配必须一次对错了上板就是黑匣子。代码包把接口分成了三组系统接口、摄像头接口、显示与指示接口。gesture_top u_top( .clk_50m (clk_50m), // 开发板自带 50MHz 时钟 .rst_n (rst_n), // 异步复位低有效 .cam_pclk (cam_pclk), // 摄像头像素时钟输入 .cam_vsync (cam_vsync), // 帧同步信号 .cam_href (cam_href), // 行有效信号 .cam_data (cam_data), // 8bit 像素数据 .vga_r (vga_r), // VGA 红色通道 .vga_g (vga_g), // VGA 绿色通道 .vga_b (vga_b), // VGA 蓝色通道 .led_gesture (led_gesture) // 手势结果指示按位输出手势编号 );逻辑说明第三个摄像头接口全部来自DVP排线cam_pclk是独立时钟输入不在50MHz系统时钟域里。在设计里cam_pclk负责采集与预处理50MHz负责识别与输出两个时钟域之间通过帧同步信号打拍握手。真正的跨时钟域数据只有skin_flag一条线所以握手逻辑很简单没有用异步FIFO。参数说明cam_data的位宽必须和实际摄像头输出匹配常见的是RGB565各路各接几位、或者YUV422顺序输出。如果摄像头输出RGB565而工程按RGB888接颜色分量会错位肤色检测出来的区域形状完全不合逻辑。拿到代码包后第一件事就是去查摄像头手册和引脚约束文件里对cam_data的定义。3.3 识别主状态机四个状态完成一轮判定gesture_core.v里是整包逻辑最集中的地方一个四状态状态机在帧同步信号驱动下循环运转。状态转移的节奏和摄像头帧率是一致的VGA分辨率30帧每秒时每帧大约33毫秒状态机要在这一帧时间内完成所有统计量的收集和判断。localparam IDLE 3d0, // 等待帧起始 CALC 3d1, // 帧有效期间累计特征 EVAL 3d2, // 帧结束后计算面积、宽高比 OUTPUT 3d3; // 锁存结果并输出手势编号 always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else case (state) IDLE: if (fsync_pulse) state CALC; CALC: if (fsync_done) state EVAL; EVAL: state OUTPUT; OUTPUT: state IDLE; default: state IDLE; endcase end逻辑说明fsync_pulse是帧起始脉冲由gesture_top根据VSYNC边沿产生fsync_done是帧结束脉冲。CALC状态贯穿整个有效像素区特征累加逻辑在这个状态下使能EVAL状态只有两三个周期做除法和比较OUTPUT状态把结果寄存器更新一次然后回到IDLE等下一帧。四个状态各司其职避免在累加过程中改判结果造成数据竞争。参数说明OUTPUT状态停留在几个时钟周期内VGA帧消隐期足够完成锁存。如果将来换成了高清分辨率、帧率提高消隐期变短EVAL里的除法器最好改成查找表或移位减法否则一个周期算不完就会被下一帧的fsync_pulse打断。4. 复现这份代码包的完整动作仿真、综合与上板验证资源能不能跑关键看三条仿真波形对不对、综合约束认不认、上板现象稳不稳。下面按我复现时走的顺序来每一步都列了可操作命令和判断标准。4.1 第一步跑通testbench先看波形再谈上板代码包里带了tb_gesture.v这份testbench用行为级方式模拟了摄像头输出。它不跑真实图片而是生成一个简化的手势图案让肤色像素集中在一个矩形区域用来验证肤色分割和特征统计是否按预期响应。先跑仿真有一个好处上板前的错都是逻辑错好查上板后的错往往混合了引脚时序问题难查。vlib work vlog -sv tb_gesture.v gesture_top.v cam_capture.v rgb2ycbcr.v \ skin_detect.v morph_filter.v gesture_core.v vsim -c tb_gesture -do run -all; quit逻辑说明第一行创建仿真库第二行把所有源文件编译进库第三行以命令行模式运行仿真跑完退出。注意文件编译顺序顶层模块放在第二个子模块按依赖关系依次放在后面编译顺序倒挂会报找不到模块的错。参数说明如果你的仿真环境是图形界面可以在工程里手动逐项添加文件最终效果一样。跑完之后打开波形重点抓skin_flag、area_reg、gesture_result三个信号。skin_flag应该出现在输入像素进入肤色区间之后一拍area_reg在每帧结束前等于模拟图案的肤色像素总数gesture_result在EVAL之后更新。三处都符合预期逻辑链路就通了。4.2 第二步引脚约束与时钟域约束一并对齐仿真通过只说明逻辑正确上板能不能工作要看约束。代码包的说明文档给出了引脚表但不同开发板的引脚定义不同必须逐一对照原理图修改。最容易出问题的是cam_pclk它既是输入时钟又驱动了大半条图像流水线在约束文件里要单独指定时钟周期。# 约束示例所有输入引脚映射到这里 # set_property PACKAGE_PIN P17 [get_ports clk_50m] # set_property PACKAGE_PIN R12 [get_ports cam_pclk] # set_property PACKAGE_PIN T12 [get_ports cam_vsync] # set_property PACKAGE_PIN T13 [get_ports cam_href] # 摄像头数据线 Data[7:0] 依次映射位序不可颠倒逻辑说明上述命令是综合工具约束的标准写法前面是注释示意实际使用时要替换成自己开发板的引脚编号。数据线位序颠倒是个隐蔽坑因为综合不报错上板后肤色区域会是斜向条纹。建议映射完后逐位核对datasheet里的引脚顺序。参数说明时钟约束不要只约束clk_50mcam_pclk同样要约束否则综合工具会把它当普通数据信号处理布局布线后的路径时序可能完全随机。如果摄像头规格是24MHz就在约束里写明周期约41.6纳秒。pclk约束缺掉是后续上板花屏和结果抖动的第一嫌疑。4.3 第三步上板调试三板斧逐级确认链路上板不离三样工具LED、内部测试图案、逻辑分析仪或示波器。代码包里已经预留了LED口调试时把gesture_result的低三位接到LED每一帧刷新一次。这样摄像头一拿到手挥手时LED变化是否正确一眼就能看出来不需要接显示器。三板斧的顺序是固定的。第一板斧不接摄像头直接把内部测试图案灌进预处理模块确认分割链路能出二值图。第二板斧接上摄像头让肤色检测结果直通VGA显示观察人体皮肤在屏幕上的黑白轮廓是否干净这一步能同时验证引脚映射和时序。第三板斧确认VGA显示正常后再把状态机输出接到LED。// 调试模式选择0 内部测试图案1 摄像头实时输入 reg debug_mode; always (posedge clk) begin if (debug_mode 1b0) pixel_data test_pattern_data; // 测试图案 else pixel_data camera_data; // 摄像头数据 end逻辑说明这是调试模式的实现思路一个寄存器切换数据源。test_pattern_data由计数器生成构造出一块符合肤色阈值范围的色块。切到内部图案时如果肤色分割和状态机都正常LED应该稳定输出OPEN_PALM不闪烁。参数说明debug_mode可以绑到一个拨码开关上方便上板时来回切换。这个模式在正式综合前必须拨回摄像头输入否则会取到测试图案产品表现就是“插上摄像头却不认人”。代码包里是把这个模式开关做成了宏定义改宏比改引脚省事也更不容易漏。5. 避坑指南FPGA手势识别最容易翻车的四个场景这类图像工程有一个共同特点就是仿真全绿、上板全黑问题基本集中在光照、抖动、时序和显示四个方向。下面四条是我自己复现时踩过的也都对应说明文档里反复强调的注意点。5.1 现象室内正常台灯一开肤色检测大面积失效原因肤色阈值是固定的灯光色温一变肤色在CbCr平面的落点整体偏移可能直接移出设定的77到127和133到173区间。白炽灯偏暖荧光灯偏冷偏移方向不同参数没法两头兼顾。解决把阈值改成可调宏放在固定头文件里上板调试时对着光源边调边综合。更强的做法是在场景固定后做一次白平衡校准把摄像头前放一张白纸统计一帧的CbCr均值把阈值区间整体平移这个偏移量。代码包说明文档里给了手动校准的表格模板把几种光源下的偏移值记下来之后直接查表替换。5.2 现象LED显示的结果连续跳变同一手势一秒内切换多次原因没有帧间确认机制。每一帧独立判断手部轻微移动就会让面积或宽高比越过阈值边界结果就在两个手势之间来回翻。这种情况不是状态机写错了而是判定条件缺少时间维度。解决在OUTPUT状态加帧确认计数只有连续N帧同一个手势结果才更新输出寄存器。我把N设为3效果立竿见影。代价是多等两帧再输出识别延迟增加约66毫秒但换来的是结果稳定。这个参数在说明文档里标成了用户可调项场景对延迟敏感就设2对稳定敏感就设5。5.3 现象VGA输出花屏或者肤色轮廓边缘参差不齐原因多半是VGA显示通路和肤色检测通路没有共用同一份像素缓存。常见实现是预处理结果直接送VGAVGA行场扫描时序和摄像头写入时序不一致导致画面撕裂。另一个次要原因是RGB和YCbCr来回转换时截断了低位颜色过渡区出现条带。解决确认信号是否来自同一个RAM读端口如果来自两条通路要么加一行行缓冲对齐要么在VGA消隐期才切换数据源。还有一个血泪教训是检查VGA的时钟相位PLL输出相差90度的一路专门给VGA打拍能消除大部分显示边缘毛刺。5.4 现象综合报告时序违例计算路径集中在肤色检测模块原因skin_detect.v里用了一长串if-else判断CbCr范围综合后组合逻辑链太长像素时钟稍高就满足不了时序。VGA分辨率30帧大约25MHz通常还能忍但一旦换成60帧或更高分辨率这条路径必然先崩。解决把组合判断拆成两级流水。第一级比较Cb是否在区间内、Cr是否在区间内各出一个单比特标志第二级把两个标志做与运算得到skin_flag。牺牲一个时钟周期的延迟换掉整条违例路径。代码包里给出的写法是直接在skin_detect里分了两拍打不占用额外寄存器上板也不用改约束。6. 调出“手感”离线回灌、宏参数表和状态指示三个让调试更快的小技巧代码能跑通只是第一步真正决定一个手势识别工程好不好用的是调试效率。我在复现这套代码时养成了三个习惯讲出来供参考。第一个习惯是离线回灌。FPGA图像调试最烦的就是“现场复现不了”摄像头对着的场景一变算法表现就变。我后来把代码包的testbench扩展了一下让它从文本文件按行读入预先导出的灰度像素数据把现场视频帧离线灌进整个流水线。这样算法逻辑的修改可以在桌面上反复验证不需要一直占着一块板子对着光源折腾。代码包里的tb本来就是读完文件再驱动像素流改造成本很低。第二个习惯是宏参数表。包里的阈值、确认帧数、面积上下限一开始散落在各个模块里我把它们全部集中到一个gesture_params.vh文件每个参数配一行注释说明“这个值在什么光照条件下校准过”。调试时只改这一个文件重新综合一次就能对比不同参数下的表现而不是在好几个文件里翻找魔数。从那以后我每次接手Verilog图像工程都会强制自己把阈值集中成宏表省掉的综合时间非常可观。第三个习惯是状态可视化。手势结果只有三个LED很难判断状态机飞到哪个状态、卡在哪个环节。我在gesture_core里加了两个调试输出引脚一个输出当前状态编码一个输出帧计数低四位用逻辑分析仪或者示波器抓这两个信号一眼就能确认状态机是否按IDLE、CALC、EVAL、OUTPUT正常循环。曾经有一次挥手不识别波形显示状态机卡在CALC回不到EVAL原因是fsync_done脉冲宽度只有一个周期被其它信号竞争掉了。加了状态引脚后这类问题十分钟就能定位。这套源码和说明文档可以作为接触FPGA图像处理的一个可靠起点原理、代码、调试路径都齐了剩下的就是亲手改几个参数、烧几版硬件把别人趟过的路再走一遍。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →