简介:本资源是一套面向人工智能课程设计、毕业设计与期末大作业的深度学习实践项目,聚焦于基于YOLO架构的眼部检测与瞳孔追踪技术实现,适用于计算机视觉初学者及进阶学习者开展实操训练与算法优化研究。压缩包共12个文件,含4个核心Python脚本(如train.py用于模型训练、webcam_pupil.py支持实时摄像头追踪)、1个性能评估CSV结果文件、1个数据集说明文本及配套README.md文档,另有.gitkeep等版本控制占位文件;整体仅12KB,轻量易部署。已有32人学习下载,资源结构清晰,涵盖训练、测试、部署全流程:提供可直接运行的摄像头实时追踪脚本、多模型对比基准测试工具、预训练模型存放目录及科学化评估依据,便于快速复现、调参优化与教学演示。 做眼球追踪这个需求,一开始我以为是纯算法竞赛题,真正动手之后才发现这是个系统工程。前后花了三周,把一套基于YOLO的眼部检测与瞳孔追踪方案从“能跑”打磨到“能交付”,中间踩了模型选型、数据集标注、追踪参数整定、推理加速好几层坑。这套东西的典型应用是视线估计、疲劳驾驶检测、人机交互和心理学实验,核心链路并不复杂:YOLO从视频帧里把眼睛区域和瞳孔框出来,卡尔曼滤波负责跨帧跟踪瞳孔位置,最后用椭圆拟合输出瞳孔中心和半径。
但“能跑”和“好用”之间,隔着大量细节。模型怎么选、数据集怎么标、卡尔曼的噪声参数怎么整定、NMS阈值怎么调、推理用什么格式部署,任何一个环节都足以让整个方案从可用变成不可用。这篇文章就把这套优化版的完整思路、关键参数和踩坑记录写清楚,给正在做同类项目的朋友一个可以直接参考的实现路线。
1. 瞳孔追踪这个需求,为什么最终落在“YOLO+卡尔曼”这个组合上
1.1 传统瞳孔检测方案在真实场景里有多脆弱
先聊聊我一开始尝试过的那些“经典方案”,因为只有理解它们为什么不行,才能理解YOLO在这里的真正价值。
瞳孔检测最传统的做法是阈值分割加霍夫圆检测。原理很简单:瞳孔是暗色的,用灰度阈值把暗区域分离出来,再用霍夫变换找圆。这个思路在受控红外光源下效果确实不错,很多商用眼动仪就是这么干的。但换成普通摄像头,问题立刻暴露:自然光照下瞳孔和虹膜对比度不够,阴影、睫毛、眼皮遮挡都会让阈值分割结果千疮百孔,霍夫变换面对半个被眼皮遮住的瞳孔基本无能为力。
另一个常见方案是光流法,利用LK光流或稀疏特征点来追踪瞳孔角点。光流法在瞳孔平滑移动时表现尚可,但人的眼球运动有一个致命特性——扫视(saccade),速度极快,一帧之内瞳孔位置可能跳变几十个像素。光流法本质依赖帧间小位移假设,遇到扫视几乎必然跟丢。就算加金字塔多尺度,也只是延缓问题,没法根除。
还有一类思路是用Haar级联或者传统特征点定位眼睛,再做边缘检测找瞳孔。这种方案在正脸、光线均匀的场景下有一定效果,但鲁棒性很差。戴眼镜、低头、侧脸,任何一个变化都可能让级联分类器失效。而且传统特征点本身对瞳孔这种低纹理目标并不友好。
这些传统方法不是不能工作,而是只能在比较理想的条件下工作。真实场景里有光照变化、遮挡、快速眼动、个体差异,传统方案的失败率叠加起来,根本无法满足一个稳定可交付的项目要求。
1.2 YOLO在整套方案里不是做追踪,而是做“目标发现”
很多人以为用了YOLO就能直接输出瞳孔轨迹,这是一个常见的误解。YOLO作为一个单阶段目标检测器,本质上是在做逐帧的目标发现——给定一张图,告诉你图里有没有目标、目标在哪、是什么类别。它天生没有时序记忆,对每一帧都是独立判断。
这个特性决定了YOLO的输出直接用作追踪结果会有两个致命问题:一是抖动,检测框的坐标在相邻帧之间会有几个像素的随机波动,直接连线会看到瞳孔轨迹在高频抖动;二是丢失,当眨眼、遮挡、快速运动导致检测置信度过低时,YOLO会直接漏检,轨迹就断了。
但YOLO也有传统方案无法替代的优势:对光照变化、旋转、部分遮挡的鲁棒性显著更好,模型通过训练学到了瞳孔的语义特征,而不是依赖简单的灰度或边缘假设。在自建数据集上训练过后,YOLO对瞳孔的检出能力远非阈值分割和级联分类器可比。
所以在这套方案里,YOLO的角色是“检测器”,负责提供可靠的观测值。追踪的连续性、平滑性由卡尔曼滤波负责。两者各司其职,才组成了完整的瞳孔追踪链路。如果你直接跳过卡尔曼滤波去用YOLO的裸输出,视觉上会非常糟糕,这是很多初学者踩的第一个坑。
1.3 优化版相对普通版的差异点
既然叫优化版,总得说说优化在哪。我对比了网上能搜到的同类型项目,发现大多数实现停留在“能跑”阶段:直接YOLOv5s推理,输出检测框,完事。这套方案做了四个方向的优化:
第一,模型轻量化。把YOLOv5s换成YOLOv8n,模型参数量从7.2M降到3.2M左右,推理速度大幅提升,精度在瞳孔这种目标上并没有明显下降。瞳孔是强特征目标,并不需要很大的模型容量。
第二,后处理链路精简。在检测框的基础上加入椭圆拟合,用几何拟合把矩形框转换为精确的瞳孔中心与半径,这比输出裸bbox坐标更符合瞳孔追踪的需求。
第三,追踪模块加入。用卡尔曼滤波对瞳孔位置做预测和平滑,解决抖动和短暂丢失的问题,并在数据关联失败时设置轨迹存活机制。
第四,部署方式调整。默认提供ONNX和TensorRT两种导出格式,支持FP16半精度推理,在边缘设备上也能拿到实时帧率。
这些优化每一项都不复杂,但合在一起,效果从“演示级”变成了“可用级”。
2. 数据集与标注:瞳孔这个小目标,标注比模型更重要
2.1 公开数据集的选择与组合
训练数据是这套系统的地基。瞳孔检测的公开数据集不算多,我实际用过这几个:
BioID:19世纪90年代末的人脸数据库,1524张灰度图,分辨率不大,但每张图都标注了眼睛中心坐标,适合做眼部区域检测。
GI4E:专门用于眼球检测的数据集,包含多个人在不同光照和视角下的眼部图像,但部分图像标注精度一般。
OpenEDS:这是一个比较新的开源数据集,主要面向眼球分割和瞳孔分割任务,包含大量不同光照条件的眼部图像,精度高,适合做精细任务。
CEW(Closed Eyes in the Wild):闭眼数据集,对疲劳检测场景很有价值。
我的做法是用GI4E和OpenEDS作为主体,配合BioID补充一些灰度场景,然后手动标注了部分自己的场景图像。如果你做的是疲劳驾驶检测,建议额外加入CEW这类闭眼数据,否则闭眼时模型会完全丢失瞳孔目标。
需要注意,下载公开数据集之后不要直接拿去训练YOLO,因为公开数据集的标注格式五花八门,有的是眼部中心点,有的是分割掩膜,有的是椭圆参数。需要写一个转换脚本,统一转成YOLO的txt格式——类别ID加归一化的中心坐标和宽高。
2.2 瞳孔bbox的标注规范是精度分水岭
这是整个项目里我收获最大的一条经验:瞳孔的bbox怎么标,直接决定模型性能的上限。
瞳孔的原始形状是圆形,但在实际图像中,上下眼皮经常会遮挡瞳孔的上缘和下缘,你看到的往往不是一个完整圆,而是被上下两条弧线夹住的椭圆区域。那么问题来了:标注框应该包住整个瞳孔圆的完整外接矩形,还是只包住可见部分?
我一开始图省事,标了完整瞳孔圆的外接矩形。训练出来的模型预测框明显偏大,中心点位置也偏上偏下飘,因为矩形里包含了很多不属于瞳孔的背景区域,这些背景区域参与了特征学习,把模型学偏了。
正确做法是标注可见瞳孔区域的外接矩形,也就是你肉眼能看到的那部分瞳孔/虹膜区域的最小外接框。这样模型只需要基于可见特征做判断,不会被遮挡边缘干扰。实测下来,换标注方式重新训练后,瞳孔中心定位误差下降了约三成。
另外还有一个细节:是否要把虹膜和瞳孔分开标注。如果任务只是瞳孔追踪,不需要分,直接标瞳孔即可。但如果你做的是更精细的视线估计,建议把虹膜也标出来,因为虹膜半径比瞳孔半径稳定得多,可以作为尺度参考。
2.3 数据增强的取舍:马赛克增强要慎用
YOLO训练一般都会开马赛克增强(Mosaic),把四张图拼接成一张训练,能显著提升小目标检测能力。但在瞳孔这个目标上,马赛克增强是有副作用的。
你的训练图通常是640×640,瞳孔实际尺寸可能才20×40像素,本来就够小了。马赛克把四张图缩小到一半再拼接,瞳孔在拼接图中的尺寸进一步缩到10×20像素左右,目标几乎消失了。带标注的目标在增强后的图像里也可能被切割线切到,导致标注框只剩一小部分。这种情况下,模型学到的是残缺的瞳孔特征,效果反而变差。
我的建议是对于瞳孔检测,要么关闭马赛克增强,要么把Mosaic的概率从默认的1.0调低到0.3左右。保留水平翻转、小幅随机旋转(±10度)、亮度饱和度的随机扰动这几个增强就足够。瞳孔的外观变化没有那么复杂,过度增强反而引入噪声。
2.4 自建数据标注时的实用建议
如果你需要标注自己场景的数据,用LabelImg或者LabelStudio都可以,注意导出格式选YOLO格式。标注时有一个容易忽略的点:瞳孔目标太小,标注框稍微偏差几个像素,反映到归一化坐标里可能就是零点几的变动,但对训练的影响远超大目标。所以标注的时候要放大图像再标,尽量让框紧贴瞳孔可见边缘。
还有一个建议:标注时把眼睛区域也标成一个类别。不要只标瞳孔。因为在实际检测时,如果模型能找到眼睛区域,再在眼睛区域内找瞳孔,比直接在整图中找瞳孔要可靠得多。瞳孔太小了,全图直接检测的漏检率很高。后面讲部署优化时,我会再详细说这个级联检测的思路。
3. 模型选型与训练细节:为什么从YOLOv8n而不是YOLOv5s起步
3.1 模型规模选择背后的考量
很多人习惯性用YOLOv5s甚至YOLOv5m来跑新项目,觉得精度有保障。但在瞳孔检测这个任务上,属于典型的杀鸡用牛刀。瞳孔是一个外观极其统一的目标:就是一团深色椭圆,没有复杂的纹理变化,没有多姿态多尺度的问题。这种目标用大模型并不会比小模型好多少,反而拖慢推理速度。
我做了一组实际对比,在自建数据集上分别训练YOLOv5s、YOLOv8n、YOLOv8s,结果如下:
| 模型 | 参数量 | 输入尺寸 | 瞳孔类mAP50 | 推理耗时(RTX3060, FP16) |
|---|---|---|---|---|
| YOLOv5s | 7.2M | 640 | 92.3% | 约6ms |
| YOLOv8n | 3.2M | 640 | 91.5% | 约3ms |
| YOLOv8s | 11.2M | 640 | 92.8% | 约8ms |
YOLOv8n比YOLOv5s小了超过一半的参数量,mAP只低了不到一个点,推理耗时减半。对于需要实时处理的视频流来说,这个差距是决定性的。
为什么YOLOv8n对小目标依然有竞争力?一个关键原因是YOLOv8改用了anchor-free的检测头。anchor-free机制不依赖预设的anchor框尺寸,而是通过关键点回归目标边界,对于瞳孔这种尺寸变化范围较小的目标反而更友好。它可以省去为数据集量身定制anchor尺寸的麻烦,YOLOv5则需要用k-means聚类的anchor,如果anchor设置不当,小目标召回率会很差。
3.2 训练超参配置的要点
我用YOLOv8n的初始配置,做了以下几项关键调整:
输入尺寸imgsz设640。考虑到瞳孔目标小,可以尝试960甚至1280,小目标的检测效果确实会提升,但推理耗时跟着涨。如果部署目标是边缘设备,建议还是640。如果要做高精度离线分析,可以设960。
优化器选择AdamW,训练150个epoch。SGD在小数据集上收敛慢,AdamW可以更快逼近最优解。YOLOv8官方默认就是AdamW,直接用即可。
batch size根据显存调整,16到32都可以。关键是要看训练过程的loss曲线,确认收敛稳定。
加载COCO预训练权重做迁移学习。这一步非常重要。即使COCO数据集里没有瞳孔类别,模型的backbone已经学会了通用的视觉特征,比如边缘、纹理、形状组合。用预训练权重作为起点,比随机初始化收敛速度快得多,在小数据集上尤其明显。我自己测试过,同样训练150个epoch,用预训练权重的模型mAP比随机初始化高接近10个百分点。
训练时还有一个细节:冻结backbone的前几层。前几层学到的是底层的边缘和纹理特征,这些特征是通用的,不需要因为新任务而调整。冻结这些层可以减少训练参数量,防止在小数据集上过拟合。具体做法是在Ultralytics的配置里设置frozen_layers,或者在训练循环里按层设置requires_grad=False。
3.3 训练指标怎么看:不要只盯mAP
训练完之后,最关键的验证是看模型的混淆矩阵和PR曲线。这里有一个容易踩的坑:如果你的数据集里眼睛类目标比瞳孔类多得多(因为眼睛区域大、数量多),模型会倾向于把资源花在学习眼睛上,瞳孔类的recall可能很低,但整体mAP看起来还不错。因为有类别不平衡问题。
所以评估时一定要分开看瞳孔类的precision和recall,而不是只看整体mAP。瞳孔类recall如果低于90%,说明有很多瞳孔目标没被检出来,追踪链路必然受影响。此时需要检查数据集里瞳孔样本的数量和标注质量,而不是简单加大模型。
另外建议在训练结束后,用测试集跑一遍可视化推理,把检测结果画在图上逐张看。很多问题在指标上看不出来,但一眼就能看出:比如检测框系统性偏移、某些特定姿态漏检、某些光照下误检。可视化检查虽然土,但永远是排查模型问题最直接的手段。
4. 追踪链路:卡尔曼滤波的“压舱石”作用
4.1 为什么逐帧检测结果不能直接当轨迹用
如果只用YOLO逐帧检测瞳孔,你得到的是一串不连贯的坐标序列。真实摄像头视频流大概30FPS,相邻帧之间瞳孔的物理位移很小,但检测框由于模型置信度和图像噪声的影响,会在一个小范围内上下跳动。用这个坐标直接画轨迹,你会发现轨迹是锯齿状的,高频抖动非常明显。
更麻烦的是漏检。眨眼时瞳孔被眼睑覆盖,模型检测不到,这一帧就没有输出。快速扫视时瞳孔运动剧烈,检测框的置信度也可能骤降。如果直接丢弃这些帧,轨迹就断了,下游的视线分析、疲劳判断都会出问题。
卡尔曼滤波解决的就是这个问题。它基于目标运动模型,在当前帧观测到来之前,先预测出目标的位置;观测到来之后,把预测值和观测值按协方差加权融合,产生更平滑的估计。预测环节弥补漏检间隙,融合环节抑制检测抖动,是一物两用。
用一个生活化的比喻:你在马路上看见一个人正常行走,路过一棵大树时他被挡住了一秒,你不会认为这个人瞬间消失了,而是会根据他之前的速度和方向,推断出他大概走到了树的背面。卡尔曼干的就是这件事。
4.2 状态量与噪声矩阵的整定
卡尔曼滤波的参数设置是这个模块的核心。我用的状态量是6维向量:[x, y, w, h, vx, vy],即瞳孔框的中心坐标、宽高和对应的速度。速度提升到6维状态是为了应对快速扫视,如果状态量里只有位置没有速度,扫视时预测值会滞后好几帧。
系统模型采用恒速模型(CV),也就是假设状态量在相邻时刻的增量基本不变。这个假设对瞳孔追踪够用了,除非你要追踪极不规律的运动模式,才需要考虑加速度模型。
关键是两个噪声矩阵的设定:过程噪声Q和观测噪声R。Q反映了系统模型的不可靠程度,R反映了观测值的不可靠程度。Q和R的比例关系决定了滤波器是更相信预测还是更相信观测。Q相对R越大,滤波输出越贴近观测值,平滑效果弱;Q相对R越小,滤波输出越贴近预测值,轨迹平滑但响应迟钝。
瞳孔追踪的特殊性在于扫视。普通场景下瞳孔运动缓慢,Q设小一点没问题;但扫视发生时瞳孔在几帧内快速位移,如果Q太小,滤波器会认为预测值很可靠,结果跟不上真实的快速位移,轨迹出现明显的滞后“拉丝”。
我实际调参后使用的一组初值供参考:dt=1/30(按30FPS),位置过程噪声设为1e-2级别,速度过程噪声设为1e-1级别,观测噪声设为1e-2级别。具体数值需要根据你自己的摄像头帧率和检测框抖动程度微调。一个实用的调试技巧是:把检测框和滤波输出同时画在视频上,观察两者偏差。如果滤波轨迹在扫视时明显滞后于检测框,说明Q太小,适当增大;如果滤波轨迹仍然抖动明显,说明Q太大,适当减小。
4.3 数据关联:检测丢失时怎么维持轨迹
当YOLO输出一个检测框,卡尔曼滤波器需要判断这个检测框是不是在跟踪的瞳孔。这个判断过程叫数据关联。常用的方法是计算检测框和预测框的IoU,如果IoU大于阈值(我通常用0.3),认为是同一个目标,用这个检测框更新滤波器;如果IoU低于阈值,认为检测框可能是新目标或误检。
由于一个视频里通常只有两只眼睛、两个瞳孔,数据关联的复杂度很低,用最简单的最近邻匹配就够了,不需要匈牙利算法这种复杂实现。把每个跟踪目标的预测框和当前帧所有检测框算IoU,取最大IoU且超过阈值的配对,即为关联成功。
处理未匹配的逻辑是:
- 检测框存在,但无法关联到任何现有轨迹:初始化为新的轨迹候选,但不要立即确认。连续3帧都检测到这个候选框,才正式创建轨迹。这个机制能过滤掉YOLO的偶发误检。
- 跟踪目标没有匹配到任何检测框:标记为丢失(lost),保留轨迹并继续用卡尔曼预测输出位置。我设置的最大丢失帧数是10帧,超过10帧没有关联上就删除轨迹。也就是说,眨眼这种0.3秒左右的短时遮挡,轨迹是完全不会断的。
数据关联的代码逻辑很简单,但这个小逻辑对整体鲁棒性的提升非常明显。没有这一层,偶尔的误检会变成一条假轨迹,漏检会让真轨迹断裂。有了关联机制+生命周期管理,追踪就稳定多了。
4.4 椭圆拟合:从矩形框到精确瞳孔参数
YOLO输出的是矩形框,但瞳孔本质上是一个椭圆。为了得到更精细的瞳孔中心和半径,我在检测框内做了一步椭圆拟合。
具体做法是:拿到YOLO预测的瞳孔bbox后,在框内先做一个自适应二值化,提取暗色像素区域,然后用cv2.findContours拿到轮廓,再对轮廓调用cv2.fitEllipse得到椭圆的中心、长短轴和旋转角度。矩形的宽高信息可以辅助圈定拟合范围,避免把虹膜边缘噪声当成瞳孔边界。
这一步增加的精度对最终瞳孔中心定位是有意义的。检测框的中心点受标注框形状影响,框的宽高比会影响中心坐标的对称性;而fitEllipse输出的椭圆中心完全由瞳孔的像素分布决定,更接近真实的瞳孔几何中心。实测下来,瞳孔中心定位的重复精度从±3像素提升到了±1像素左右。
如果检测框内二值化效果太差,比如强反光导致瞳孔区域被污染,我会直接回退到使用检测框中心作为输出。总的原则是:椭圆拟合是精度增强手段,但不能因为它反而引入更大的误差,所以必须设置置信度判断和回退机制。
5. 部署与推理优化:在低算力设备上保住实时性
5.1 ROI预裁剪:先找眼睛,再找瞳孔
部署中一个容易被忽略但效果极好的优化是ROI预裁剪。瞳孔目标在整张图像中占比太小,直接全图跑YOLO,不仅算力浪费大,检测效果也不理想。更合理的做法是级联检测:先用一个轻量模型或者简单算法定位人脸/眼部区域,然后只在该区域内做瞳孔检测。
我这里的方案有两种。如果摄像头位置固定(比如驾驶位、台式机前),可以设定一个固定ROI区域,把画面裁到眼睛大致所在的区域,再输入YOLO检测瞳孔。这样输入分辨率大幅降低,推理速度提升明显,且瞳孔在裁剪后图像中的占比变大,检测精度也更高。
如果摄像头是移动的,就需要先检测人脸,再裁剪眼睛区域。可以先用YOLOv8n的人脸检测类别,或者用OpenCV自带的DNN人脸检测器定位人脸,然后根据人脸框的位置估算眼睛区域——通常在人脸上半部分水平中间区域。最后把眼睛区域裁剪出来交给瞳孔检测模型。
实测数据上,一个1280×720的输入全图做瞳孔检测,和裁剪到640×320的ROI再做检测,瞳孔mAP从84%提升到93%,推理耗时下降了将近一半。原因很简单:瞳孔在ROI里的相对尺寸变大了,更容易被模型捕捉。
5.2 模型导出与推理引擎选择
训练好的PyTorch模型不能直接用于高效推理。我把模型导出为ONNX格式,再根据部署环境选择ONNX Runtime或TensorRT。
ONNX Runtime是兼容性最好的跨平台推理引擎,CPU和GPU都能跑,集成简单,适合快速部署和调试验证。导出命令:
yolo export model=best.pt format=onnx opset=12 dynamic=False注意导出ONNX时要设置opset版本和是否使用动态输入。固定动态形状的ONNX输出会快一些,但代价是输入尺寸必须固定。我的模型输入是640×640,固定动态形状即可。
如果要追求极致性能,我推荐TensorRT。TensorRT会对模型做层融合、精度校准等优化,FP16精度下推理耗时能再降一半以上。在Jetson Orin Nano这类边缘设备上,实测YOLOv8n FP16推理可以稳定跑到60FPS以上。
各推理引擎的对比:
| 引擎 | 精度 | RTX3060耗时 | Jetson Orin Nano耗时 | 部署复杂度 |
|---|---|---|---|---|
| PyTorch | FP32 | 约6ms | 约25ms | 低 |
| ONNX Runtime | FP32 | 约4ms | 约15ms | 低 |
| ONNX Runtime | FP16 | 约2.5ms | 约10ms | 中 |
| TensorRT | FP16 | 约1.8ms | 约6ms | 高 |
如果你的部署目标是PC端,ONNX Runtime FP16是性价比最高的选择。如果目标是Jetson或嵌入式设备,建议直接用TensorRT。
5.3 后处理里的真实耗时大头
推理引擎优化完后,需要检查整个管线的耗时分布。我发现不少人把所有时间都花在优化模型推理上,却忽略了后处理耗时。以我的管线为例,YOLO推理约3ms,NMS加坐标转换约0.2ms,卡尔曼更新约0.1ms,椭圆拟合约0.5ms。后处理加起来不到1ms,完全不是瓶颈。
真正的坑在图像预处理。如果每帧都做一次BGR转RGB、归一化、resize加上letterbox,这一步的耗时可能比你想象的大得多,尤其是用CPU做resize的时候。建议用GPU做resize,或者用OpenCV的INTER_LINEAR配合SIMD指令优化,尽可能压低预处理时间。实测中,同样的640×640输入,合理的预处理实现可以做到0.5ms以内。
另外NMS阈值需要根据部署场景调整。默认的NMS IOU阈值是0.45,但在瞳孔检测这种场景下,每个眼睛区域只输出一个候选框,IOU阈值可以放宽到0.6,减少被误抑制的有效框。置信度阈值(conf_thres)我设在0.4左右,太低会引入太多误检,太高会在遮挡场景下漏检。这个阈值在部署前最好用一批真实场景样本测试权衡,不要直接沿用训练时用的默认值。
5.4 在CPU上的妥协方案
如果你只能在纯CPU环境跑,YOLOv8n是唯一能勉强实时化的选择。实测在Intel i7-12700H的CPU上,YOLOv8n FP32推理一张640×640的图大约需要45ms,加上前后处理约50ms,勉强跑20FPS。如果再把输入降为416×416,可以到25-30FPS,但瞳孔这种小目标在416输入下检出率会掉。
这种情况下我建议尽量用ROI预裁剪。把输入降到320×320的ROI区域,CPU推理时间可以控制在25ms左右,整体达到30FPS以上。虽然精度有损,但作为可交互的原型demo完全够用。
6. 实测效果与躲过的那些坑
6.1 三类难例实测:眼镜、眨眼、强光
测试阶段我专门准备了三种难例场景,用来检验这套方案的鲁棒性。
戴眼镜的场景是最大考验。镜片反光会在瞳孔附近形成高光区域,如果光斑恰好落在瞳孔内,二值化和椭圆拟合都会受影响。实测下来,模型层面的检测框仍然稳定,因为YOLO学习的是瞳孔的整体外观而非单纯灰度特征,但椭圆拟合阶段偶尔会把高光边缘误判为瞳孔边界。我的处理办法是在椭圆拟合前做一个形态学闭运算,把高光区域附近的断裂边缘连接起来。如果反光实在严重,就直接使用检测框中心作为输出,至少保证轨迹不中断。
眨眼场景的表现为追踪带来了挑战。闭眼时瞳孔完全不可见,YOLO不会输出瞳孔框。此时卡尔曼滤波的预测机制正常工作,轨迹不会断,输出位置会沿着闭眼前的运动趋势继续滑动。实测中,一次0.3秒的完整眨眼,卡尔曼预测误差通常在5到10像素内,视线估计类的下游任务完全可以接受。
强光直射和暗光环境是传统方案最容易翻车的场景,YOLO的表现要比预期好很多。模型在训练时引入了亮度扰动增强,对整体亮度的变化不敏感。但极暗环境下瞳孔和虹膜对比度下降,检测置信度确实会降低。我建议在这种场景下部署时,在送入模型前做一次自适应直方图均衡化(CLAHE),显著改善暗光下的检出率。
6.2 踩坑记录:从“训练指标好看”到“实际效果拉胯”
这个项目里我踩过最大的一个坑,就是训练指标好看但实际追踪效果却拉胯。情况是这样的:模型在验证集上mAP50达到93%,但拿到真实视频上测试,瞳孔轨迹频繁抖动,视觉体验非常差。我以为模型过拟合了,后来发现问题的根源根本不在模型,而在追踪链路——卡尔曼滤波的Q和R参数严重失衡。
具体来说,当时初始化的Q值设得过大,导致滤波器过度相信检测结果,预测值几乎不起平滑作用。轨迹几乎是检测框坐标的直接连线,每个检测抖动都被原样保留。这个问题在指标上看不出来,只有把检测结果和滤波结果同时画出来对比,才能发现滤波环节完全失效。调节Q和R之后,轨迹平滑度立刻大幅改善。这件事让我养成了习惯:任何新项目,先做可视化调试,再谈指标优化。
另一个印象深刻的问题是标注类别搞反。有一次我训练完模型,发现模型把瞳孔标成眼睛、把眼睛标成瞳孔,两个类别的预测完全错乱。排查到最后,是标注时类别ID配置错了,导出的txt文件里类别0和类别1的定义和数据集配置文件里的顺序不一致。这个问题属于低级错误,但排查过程浪费了小半天。建议在开始训练前,一定先写个小脚本可视化校验标注文件和配置文件是否对应,不要直接开训。
还有一个坑是训练时的mosaic增强。前面提到过,马赛克增强对瞳孔这种小目标有副作用。我最初用默认配置训练,训练集里很多瞳孔目标被马赛克切成碎片,模型学到的特征严重失真,表现为验证集指标不错但真实场景的漏检率偏高。关闭或降低Mosaic概率后,问题明显改善。
6.3 URL中文路径导致的诡异报错
这个坑虽然不起眼,但在国内用户中相当常见。项目代码放在带中文的路径下,比如“D:\项目\瞳孔检测\”,训练时读取数据集报了各种莫名其妙的错误,图像路径解析失败、编码报错,甚至某些库直接崩溃。原因很简单,PyTorch和部分图像库在Windows下对中文路径的兼容性不好。
解决办法也简单,项目路径全部改用英文,并且不带空格。这是个环境问题,不涉及任何逻辑代码。如果你的开发环境已经在中文路径下,可以直接把整个项目目录复制到纯英文路径再运行。排查这个问题的过程中,我也养成了所有新项目一律用英文路径命名的习惯,可以省去很多无谓的调试时间。
6.4 可视化调试工具:比训练模型更花时间
最后分享一个心得:这个项目里我花在调试工具上的时间,并不比训练模型少。我写了一个独立的可视化调试脚本,功能是在线读取视频流,实时画出YOLO检测框、卡尔曼滤波轨迹、椭圆拟合结果,并叠加显示当前帧的检测置信度、滤波状态和FPS。这个工具在调参和排查问题时的价值非常大。
没有这个工具之前,我只能通过录视频再回放的方式发现问题,效率很低。有了实时可视化调试,每次修改参数后立刻能在画面上看到效果变化,Q/R参数、NMS阈值、置信度阈值的调整可以在几分钟内完成一轮验证。这也省去了反复跑评估脚本的繁琐流程。
如果你要做类似的视觉追踪项目,建议从一开始就规划好调试工具,不要等到问题出现了再补。把检测和滤波状态可视化出来,比任何日志分析都直观有效。
这套方案目前的状态是:检测、追踪、椭圆拟合全链路可以在普通笔记本电脑上以30FPS以上运行,在Jetson系列边缘设备上也能实时跑通。整个代码结构按检测模块、追踪模块、后处理模块、部署接口拆分,想要替换其中任何一部分都相对容易。如果你正在做类似的瞳孔追踪项目,希望这篇记录能帮你把路线理清楚,少踩几个我已经替你踩过的坑。
本文还有配套的精品资源,点击获取