1. 为什么最终选YOLO系列做空中手势识别
先说结论:手势控制无人机这件事,难的不是识别,而是"在高空、动态、算力受限"三重条件下还能稳定识别。我最早其实试过MediaPipe的Hand Landmark方案,单看手部关键点检测效果确实不错,但一放到无人机机载场景就暴露问题——它对手部完整度要求太高,飞起来之后桨叶震动、身体遮挡、光照突变都会导致关键点置信度断崖式下跌。后来我彻底转向YOLO系列做端到端的边界框识别,反而稳得多。
YOLO的优势在于它把"手在哪"和"手势是什么"合并成一次回归计算,不需要先检测手部区域再单独分类,推理一次就能同时拿到边界框和类别。这对机载平台是决定性的:树莓派或者Jetson系列的算力本来就有限,两步串联的pipeline到了空中每秒可能只能跑5到8帧,而端到端的单阶段检测器可以把帧率拉高到15甚至20帧。手势识别本质上是时序任务,单帧再准,帧率不够就会让飞控指令滞后到手感发飘。所以我最后确定的主线是:数据自采、自标注,再用不同版本的YOLO训练手势检测模型,整套系统走通从图像到飞控指令的闭环。
这套系统适合谁参考?一种是做智能飞行器课程设计或者毕业设计的人,另一种是想把计算机视觉模型真正部署到嵌入式设备上的开发者。注意,我讲的不是一个"跑通Demo就算完"的实验,而是包括数据采集、模型训练、推理优化、飞控通信、异常保护在内的完整链路。中间踩过的坑不少,每一个我都会拆开讲。
先说一个全篇最重要的定位:YOLOv5到YOLOv10不是简单的"版本越高越好",对无人机这个资源受限场景,版本的取舍会直接影响系统能不能飞起来。后面会用一个专门的表格把这几个版本的差异摊开讲。
2. YOLOv5到YOLOv10,几个版本对无人机场景的实际增益
我在这个项目里先后用了v5、v8、v10三个主力版本(v6、v7也短暂试过,这里不展开),目的不是为了追新,而是想搞清楚每个版本在"手势识别+机载部署"这个具体约束下,到底带来了什么可感知的变化。
2.1 变化最大的三个点:网络结构、无锚框、无NMS
从v5到v8,最核心的变化是主干网络里的C3结构被替换成了C2f结构。C3的梯度流相对固定,而C2f通过更多分支的跨层连接,在参数量增加不大的前提下提升了梯度流动性。我自己的实验结果非常典型:同样的2000张手势数据,v8s训练出来的mAP50比v5s高大概3到4个百分点,而推理时间只增加了2到3毫秒。在机载设备上,这个代价完全可接受。
v8的第二个关键改动是全面转为anchor-free(无锚框)检测。v5时代每个特征图位置要预设一堆不同尺寸的锚框,手部目标尺度变化又大,锚框参数需要反复调。到了v8直接预测中心点和宽高比,省掉了一大堆设计锚框的超参数。对新手最直观的好处是:换数据集的时候不用再想着"我的手在画面中平均多大",少了一个玄学调参环节。
v10最猛的一刀是去掉了NMS后处理。传统YOLO在推理结束之后要做非极大值抑制,把重叠的检测框合并掉。这一步虽然不大,但它是串行逻辑,在嵌入式端还要额外写优化。v10用双标签分配和一致匹配度量替代了NMS,把后处理时间直接砍到了接近零。我在Jetson Orin Nano上实测,端到端单帧延迟比v8s少了大概6到8毫秒。不要小看这七八毫秒,在25Hz的控制循环里,这已经是15%左右的余量了。
下面是几个版本在同样手势数据集下的实测对比,硬件统一用Jetson Orin Nano,TensorRT FP16精度,输入分辨率640x640:
| 模型 | 参数量 | mAP50 | 平均推理延迟 | 端侧可用性 |
|---|---|---|---|---|
| YOLOv5s | 7.2M | 88.6% | 16ms | 较好,但锚框调参费时 |
| YOLOv8s | 11.2M | 92.3% | 13ms | 推荐,精度/速度平衡 |
| YOLOv10s | 8.4M | 91.8% | 7ms | 最推荐,延迟最低 |
| YOLOv10n | 2.7M | 88.9% | 4ms | 适合超低功耗环境 |
2.2 对"手势识别"这个具体任务来说,版本的收益排序
如果把"模型识别性能"和"机载部署便利性"分开看,v5到v10的收益排序是这样的:
识别准确率收益最大的是v5到v8这一步,C2f加anchor-free的组合对手部这种非刚性目标很友好。手势和刚性物体不一样,同一个"五指张开"在不同角度下长宽比变化极大,锚框预设不合理就容易漏检。而v8之后的中心点回归对形状变化没那么敏感。
部署收益最大的是v8到v10这一步,NMS的去除直接简化了TensorRT的端侧移植。有一个细节很多人忽略:NMS在PyTorch里是一个函数调用,但导出的engine在DLA(深度学习加速器)上支持并不好,经常要退回GPU跑。v10完全绕开了这个问题,对于想做低成本部署的人这是一笔隐形的节省。
2.3 我的选型结论
如果你只能选一个版本起步,我建议直接上YOLOv8s,理由很简单:资料多、踩坑少、TensorRT部署示例最全。等整个系统验证跑通了,再切换到YOLOv10s去抠帧率。我自己的最终系统用的是v10s,但学习过程中最建议你先从v8s复现,因为网上能查到的部署资料密度完全不同。
有个反面教训提一下:我在中期试过一次YOLOv5s直接部署到树莓派4B,量化成INT8后延迟倒是低到10ms以内,但精度掉到75%左右,"五指张开"和"握拳"在低空逆光下频繁混淆。后来我复盘,问题不是量化本身,而是v5s模型容量小,INT8量化损失占比太高。如果非要用树莓派这类平台,建议优先选v8n或者v10n,而不是强行压缩v5s。
3. 手势控制系统的完整链路:从摄像头图像到飞控电机
模型本身只是系统里的一个环节。真实的手势控制无人机系统,图像从摄像头进来之后,要经过采集、推理、指令映射、通信、飞控执行五个阶段。任何一个环节的延迟失控,都会让整体控制变成"慢半拍"甚至完全不可用。
3.1 整条链路的硬件组成
我用的是模块化方案,而不是一体式无人机。这样做的目的是方便单独调试每个环节。
- 视觉端:Jetson Orin Nano 8GB,外加一个USB免驱工业摄像头,帧率上限90fps,实际跑30fps。
- 飞控端:Pixhawk 6C系列飞控,运行ArduPilot固件,通过串口接收来自Jetson的MAVLink指令。
- 通信链路:Jetson的USB转串口模块连接飞控的TELEM2口,波特率921600。
- 动力平台:一架350轴距的四旋翼,电机电调全套常规配置,不需要特殊改装。
这个链路里,Jetson和飞控之间用的是MAVLink而不是PWM信号直连,是个关键设计。PWM直连只能表达"滚转多少"这种原始量,而MAVLink可以通过SET_POSITION_TARGET_LOCAL_NED这类消息直接给飞控一个完整的速度或位置目标。飞控内部有EKF(扩展卡尔曼滤波)做状态估计,它需要的是一级的控制量,而不是电机级的PWM脉宽。用MAVLink能保留飞控的自主决策能力,比如悬停稳定、GPS保持、避障联动,手势系统只在更高层下发意图。
3.2 图像采集和预处理上的细节
摄像头输出的是MJPG格式,直接喂给YOLO之前要做两件事:解码和缩放。
解码性能很容易被忽略。我一开始用OpenCV的VideoCapture直接读,按照默认设置CPU解码,MJPG的CPU开销到了17%左右,看起来不高,但Jetson上CPU和GPU争抢资源会影响推理的稳定性。后来我把解码任务移到硬件编码器上,用V4L2的MJPEG硬件解码,CPU占用一下降到4%。这一项就让推理帧率从约13fps提到了16fps,完全是白捡的性能。
缩放我推荐直接等比例缩放加填充到640x640。如果不填充而是强制拉伸,手势的长宽比会畸变,"V字手势"可能在特定角度下被识别成"食指"——因为两个手指的像素距离被压扁了。
预处理流程参考如下:
- 从V4L2拿到MJPEG帧,送入硬件编码器解码为BGR。
- 计算输入帧宽高与640x640的比例,选择一个适中的缩放比。
- 生成一个640x640的灰色画布,把缩放后的图贴在左上角,其余区域填充灰色128。
- 将BGR转为RGB,并做归一化处理到0到1之间。
- 送入TensorRT推理。
这套固定填充的做法,比直接resize多消耗约1ms内存拷贝,但换来的是不变形,完全值。
3.3 手势协议设计与控制映射
手势协议是整个系统的灵魂。我定义了五个手势,每个手势只对应一个明确的飞控语义,避免歧义:
| 手势定义 | 视觉识别类别 | 飞控控制语义 |
|---|---|---|
| 五指张开 | palm | 解锁并起飞,悬停到1.2米高度 |
| 握拳 | fist | 原地降落并上锁 |
| 食指单指 | one | 前进,速度0.5m/s |
| 食指+中指V字 | peace | 后退,速度0.5m/s |
| 手势水平左右平移 | left / right | 向左或向右平移 |
这里有一个非常容易踩的坑:一旦定义了"握拳=降落",就必须把握拳变成"需要持续保持"的动作,还是"触发一次就执行"的动作。如果握拳只在单帧出现就执行降落,那么无人机飞行中机身震动导致手短暂出框、再回来时随便一个握拳动作都有可能触发降落。我的做法是:降落指令必须连续15帧(约0.5秒@30fps)都识别到握拳,才真正执行。其他方向指令则使用边沿触发,识别到一次就持续移动,直到下一个手势出现。这是为了匹配不同手势语义所需要的不同响应速度。
3.4 状态机:系统不只是"识别-执行"那么简单
如果系统只做"识别到起点就起飞",那空中风一吹,手稍微移出画面,无人机就会不知所措。所以我在Jetson上跑了一个简单的状态机:
- GROUND_IDLE:地面待命,只检测降落手势,其余手势一律忽略。
- TAKEOFF:收到起飞信号后执行,完成后进入AIR_HOVER。
- AIR_HOVER:空中悬停,持续检测所有手势,手部丢失超过2秒回到悬停。
- AIR_MOVING:根据手势移动,手部丢失超过0.5秒立即回到悬停。
- LANDING:收到降落信号后执行,直到落地。
状态机的意义在于,不是每一帧都要去响应手势,而是让手势成为状态迁移的条件。手临时出框不可怕,因为系统会自动回到悬停,而悬停是所有安全的锚点。我在试飞中遇到过一次GPS信号弱、悬停漂移的情况,靠这个状态机快速切回手动遥控,没有出事故。
4. 自建手势数据集:这是整个系统最容易翻车的环节
YOLO模型再强,没有贴合你场景的数据也是白搭。网上有公开的手势数据集,但大多是手机自拍或者固定摄像头拍的室内场景,空中俯视、逆光、运动模糊这些条件下效果很差。所以我从零自采了一套数据集,整个过程花了两周,但这部分投入的回报远超任何调参带来的精度提升。
4.1 数据采集的要点:不是拍得越多越好,而是要拍得"脏"
我在采集时的核心思路是把每一种"会让模型困惑"的情况都拍进来:
- 动态模糊:手持摄像头做快速左右摆动,模拟无人机运动时的手部拖影。
- 俯视角度:把摄像头固定在约1.5米高的支架上向下拍,模拟悬停高度的视角。
- 各种肤色和背景:找不同肤色的同学帮忙,背景包括草地、水泥地、室内地板、深色衣服。
- 半遮挡:手只露出一半、手指之间有遮挡,模拟手部被身体或机械臂局部挡住的极端情况。
每类手势的原始素材我录了约20分钟视频,抽帧后清洗掉虚焦和完全不可辨的帧,最终得到1200张有效图片。然后按8:1:1划分训练集、验证集、测试集。注意,按视频时间片段划分而不是按单帧随机划分,不然同一个视频片段里几乎一样的帧会同时出现在训练集和测试集,导致mAP虚高。
4.2 标注环节的几条经验
标注工具我用的是开源的labelImg,格式直接导成YOLO的txt格式。有一个非常容易忽略的问题:手势框的边界到底怎么定。我的建议是把整个前臂下缘到指尖全部框进去,也就是说框要包含一小段手臂,而不是把框死死卡在手掌边缘。理由是,YOLO的检测框内上下文(context)对分类有帮助,模型能通过手腕位置判断手的朝向,进而更好地区分"食指"和"V字"。
另外,凡是模糊边界帧,我都标注成"困难样本"单独放着。训练时用普通样本和困难样本以8:2的比例混合。这样可以防止模型只学会识别"清晰的手"而彻底丢掉对模糊帧的鲁棒性。实测下来,添加20%困难样本之后,测试集上的mAP50几乎没变化,但实际试飞中的稳定性提升非常明显,动态模糊下的漏检率从14%降到了6%左右。
4.3 数据增强要克制
YOLO自带马赛克(Mosaic)增强,对所有数据拼成一张大图训练。但有一个问题:马赛克增强会把四张图缩小之后拼在一起,导致原本就不大的手部目标变得更小。我一开始默认打开Mosaic增强,训练完发现"小目标"一栏的AP特别低,后来我把Mosaic的启用概率从1.0调到了0.3,并在增强管线里加入了随机旋转、亮度抖动、HSV扰动,去掉了随机擦除。为什么去掉擦除?因为随机擦除会把手指切掉,让"握拳"变成"三根手指"这种不存在于标签里的鬼样子,只会增加训练噪声。
如果非要给一组建议训练参数,我用的是640x640输入、初始学习率0.01、批量大小16、SGD优化器(带动量0.937)、余弦退火调度,总迭代300轮。多试几组你会发现,SGD在几百张数据的小数据集上往往比Adam稳定,Adam收敛太快,泛化性反而差一些。这个现象对新手来说可能有点反直觉,但多跑几次实验就知道了。
5. 实时推理的优化:延迟预算怎么算、怎么省
手势控制系统的实时性要先定预算,才知道哪里该优化。我给自己定的控制目标是:从手势发生到飞控收到速度指令,端到端不超过100ms。这个数字基于一个很朴素的评估——人在遥控器上打杆的反应时间一般是200到300ms,如果机器视觉系统比人的反应还慢,那这套系统就没有存在价值。
5.1 逐环节延迟拆解
我把整条链路拆成四段来测:
| 环节 | 实测耗时 | 说明 |
|---|---|---|
| 摄像头曝光+传输 | 约10ms | USB工业相机,曝光固定1/240防拖影 |
| 图像解码+预处理 | 约6ms | 硬件解码后,只需做缩放填充和归一化 |
| TensorRT推理 | 约7ms | YOLOv10s FP16,排除NMS |
| 状态机+MAVLink发送 | 约2ms | 状态转换和串口发送都在毫秒级 |
把四段加起来,大约25ms,离100ms的预算还有很大余量。但这只是平均数字,实际试飞时帧率波动会让延迟时不时飙到40~50ms。所以真正的优化目标不是把单帧压到极限,而是把"延迟抖动"压住。我做的核心优化是把整条链路从"同步串行"改成"双线程流水线"。
5.2 双线程流水线:采集和推理不再互相等待
简单来说,原来的代码是:读一帧 -> 推理一帧 -> 发送一次。这样如果某一次推理慢了20ms,就会让下一帧的读取也被推迟,整体效果是帧率被最慢的那一帧拖累。
改法是把采集和推理解耦:线程A只负责从摄像头拿帧,放进一个循环队列;线程B从队列里取最新的帧推理。队列长度固定为3,每次取时直接丢弃旧帧,保证永远推理最新帧,宁可跳帧也不处理过时画面。这个设计模仿了视频直播里的"丢帧保新"策略,不需要严格的帧同步,因为飞控本身有平滑滤波器,偶尔丢一帧对手感几乎没有影响。
这个改动让端到端延迟从平均约40ms降到了约25ms,而帧率还从17fps左右提升到了接近30fps。原因就是读帧和推理并行之后,两者之间互相等待的时间被消除了。
5.3 模型导出和TensorRT部署的要点
PyTorch模型不能直接上机载设备,需要先导出成ONNX,再构建TensorRT engine。有几个参数务必设置对:
- 动态shape改成静态shape,固定输入为1x3x640x640。TensorRT对动态shape的支持会引入额外显存预留和耗时,对无人机这种固定输入场景完全没必要。
- 输出层要确认是否包含检测框、置信度、类别三个分支。v8和v10的导出脚本输出往往是一个"1x84x8400"的矩阵,其中84是4+80类别,但我们的自定义数据集类别数只有5,所以改成1x9x8400。
- 如果使用v10,注意导出时要带上--nms标志。v10训练时不需要NMS,但在导出ONNX时可以附加一个NMS插件,这是为了兼容常见部署框架,不算走回头路。
我在第一次TensorRT部署时,卡在了一个论坛里翻遍都没找到答案的问题:engine构建完成之后,推理结果全部变成了一个类别的置信度0.999。后来查代码发现是导出ONNX时,模型的类别数参数没改,模型结构输出维度写的还是COCO的80类。panic排查了快一天,最后拿Python脚本把ONNX的节点输出维度拉出来看才定位。这个教训总结成一句:模型改类别后,先打印一遍ONNX输出shape,再进TensorRT。
6. 试飞验证与安全冗余:代码能跑通不等于飞机能飞好
系统在地面测试得再好,到了真实飞行环境还是会遇到一堆意外。这个部分我重点讲试飞中遇到的三个"反常识"问题和对应的安全设计。
6.1 第一个坑:手势在风和震动下会瞬间"变形"
第一次实飞,我把无人机悬停在1.2米,手动用手势让飞机前进。结果飞机刚启动前进,桨叶下洗气流就把我的手吹得抖了一下,模型在这几帧里输出从"食指"变成了"剪刀"。这直接导致前进状态被重置,飞机顿了一下。
我后面加了两个机制:一是对方向类手势做"连续3帧投票一致才切换",二是对方向移动设置死区——输出速度指令时做低通滤波,新的速度指令不会突然跳变,而是以每秒0.5m/s的上限斜率爬升。模型单帧误判还有可能,但几帧连续误判的概率就小得多,加上速度变化被限制,飞机手感一下子变得柔和可控。
6.2 第二个坑:光流和视觉手势争抢计算资源
我的飞控里默认开启了光流定点,而Jetson上的视觉手势识别也在跑,两者在某些光照条件下会互相干扰——不是计算资源上的,是信息上的。光流模块在弱纹理地面会漂移,导致飞机位置缓慢偏移,而手势识别又告诉飞机"继续悬停",结果飞机就慢慢飘到旁边去了。
这个问题的本质是:视觉手势系统和飞控各自依赖不同的定位传感器,它们的误差模型不一样,必须明确谁的定位优先级更高。我的方案是:手势控制只负责水平和垂直速度指令,不负责位置保持。位置保持完全交给飞控的EKF和光流。手势系统给出的速度目标一旦消失(手出框),立即回到位置保持模式。也就是说,手势是"推进器",飞控是"锚"。
6.3 安全冗余:强制接管优先于一切
不管手势系统多么完善,物理遥控器始终是最高优先级。我的设计里:
- 飞控遥控器切换到任意非自稳模式时,Jetson端的MAVLink速度指令会被飞控直接拒绝。
- Jetson持续向地面站发送心跳包,如果地面站连续1秒没有收到心跳包,会触发Failsafe——无人机原地悬停并等待遥控指令。
- 在任何时刻,遥控器油门杆拉到最低,无人机立即降落。
这里面有一个非常关键的实现细节:Jetson和飞控之间的串口如果松动或线路被桨叶打坏,Jetson自己并不知道。一定要在飞控端配置MAVLink的流量节流和看门狗。我在试飞中使用的是让Jetson每隔200ms发送一次心跳,飞控侧配置RC_FAILSAFE为"悬停等待",这样即使视觉系统完全失联,飞机也不会盲目返航或坠落,而是稳稳停在原地等遥控器介入。
6.4 试飞前的检查清单
最后分享一份我自己每次试飞都会过的检查清单,看似琐碎,但每一条都有血泪教训垫底:
- 电池电量至少3块,保证每次试飞前电压在满电状态,视觉推理耗电不容忽视。
- 摄像头镜头清洁,尤其室外尘土大的环境,一层灰可以让置信度掉5个百分点。
- 确认Jetson和飞控的串口波特率一致,有个细节是TELEM2默认波特率是57600,要在参数里显式改成921600。
- 试飞时系统日志level设为DEBUG,方便回来后复盘是视觉问题还是飞控问题。
- 第一次实飞永远在有保护网的小型场地进行,哪怕多跑两个来回。
7. 收尾:关于这套系统,我最后想再说的三件事
第一件事,别在还没有稳定悬停的基础上去调手势识别。我见过不少项目代码写得很漂亮,但飞机本身都飞不稳,最后所有问题都混在一起,排查起来非常痛苦。先把飞控调试到拨动摇杆机身指哪打哪,再把手势控制接进去。
第二件事,数据集比模型版本重要。YOLOv5跑得很好的老数据集,换到v10不一定更好;但数据质量差的话,换什么模型都一样烂。我自己最深刻的体会是,模型从v5升到v10带来的精度提升,远不如我在数据集里增加20%的困难样本带来的实飞稳定性提升。
第三件事,这个项目最大的扩展方向是结合语义理解。比如用户先做一个"圆圈"手势圈定目标区域,再用食指手势指引飞机到区域内巡检。话句话说,手势识别的下一步不是识别更多的手势,而是让同一个手势在不同的上下文里有不同的含义。这已经超出了简单的分类任务范围,需要引入时序模型甚至语言模型协同。我目前正在做的是把YOLO的输出接入一个轻量的LSTM序列模型,先把手势序列化理解起来,再往下走语义。等这版跑通,我再写一篇实操记录。