☰
FPGA手势识别源码解析:DVP采集、肤色分割与Verilog实现
2026/10/14 3:54:56 网站建设 项目流程

简介:面向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 = 16'sd77 * r + 16'sd150 * g + 16'sd29 * b + 16'sd4096; wire signed [15:0] cb_tmp = -16'sd43 * r - 16'sd85 * g + 16'sd128 * b + 16'sd65536; wire signed [15:0] cr_tmp = 16'sd128 * r - 16'sd107 * g - 16'sd21 * b + 16'sd65536; assign y = y_tmp[15:8]; assign cb = cb_tmp[15:8]; assign cr = cr_tmp[15:8];

逻辑说明:三个通道的系数都扩大了256倍,Y的常量偏置4096对应标准里的16再乘256,Cb和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 <= 1'b0; 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_flag
morph_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 = 3'd0, // 等待帧起始 CALC = 3'd1, // 帧有效期间累计特征 EVAL = 3'd2, // 帧结束后计算面积、宽高比 OUTPUT = 3'd3; // 锁存结果并输出手势编号 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_50m,cam_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 == 1'b0) 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显示通路和肤色检测通路没有共用同一份像素缓存。常见实现是预处理结果直接送VGA,VGA行场扫描时序和摄像头写入时序不一致,导致画面撕裂。另一个次要原因是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图像处理的一个可靠起点,原理、代码、调试路径都齐了,剩下的就是亲手改几个参数、烧几版硬件,把别人趟过的路再走一遍。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询