简介:面向眼睛睁开闭合检测任务的数据集包,为计算机视觉目标检测训练提供标准化数据基础,适用于疲劳驾驶预警、注意力分析等场景。数据集包含12884张人脸/眼部图片,对应VOC格式XML标注与YOLO格式TXT标签各12884份,标注类别为closed与opened两类,矩形框总数为13026个,图片清晰度整体较好且未做增强,可直接满足常规检测模型的输入需求。压缩包共2000个文件,其中xml标注文件1999个、txt说明文件1个,整体大小约272.73MB,目录结构区分图片、XML、TXT三个文件夹,便于按需调用与二次处理。目前已有249人学习浏览。数据集提供准确合理的标注,不附带训练权重,用户拿到后即可进行模型训练与调优,省去数据清洗和格式转换环节,适合需要高质量眼睛状态样本的算法工程师与研究者。 做疲劳驾驶检测那会儿,最让我头疼的其实不是模型结构,而是数据。眼睛睁开闭合识别这种任务,公开数据集本来就少,带干净标注的更少,能直接扔进YOLO训练的更是可遇不可求。当朋友发我一份“眼睛睁开闭合识别数据集 yolo+voc格式 12884张”的zip包时,我第一反应是:省下的不只是标注时间,还有几个周末。这篇文章我把从解压、格式核对、转换到训练的完整过程写一遍,给准备做眼睛状态检测的朋友一份实在的参考。
1. 12884张双格式数据集:眼睛状态检测从哪开始
1.1 眼睛开闭数据为什么这么难凑
先说一个反直觉的事实:通用目标检测数据集里,你找不到“眼睛闭合”这个类。COCO有人的各种部位标注,VOC有person,但都不会细到眼睑状态。疲劳驾驶检测、驾驶员注意力分析、长时间盯屏的用眼健康提醒,这些场景偏偏就依赖这样一个细粒度判断。
而且眼睛在整张画面里属于小目标,往往只有几十乘几十像素,类间差异又是眼睑那几毫米的位移,比普通目标检测难得多。自己从零标注的话,打开一张人脸图,框眼睛、区分睁开和闭合,一张图至少半分钟,一万张就是一百个小时的工作量,还不算审核和返工。所以拿到一份已经标注好、还提供双格式的数据集,解决的是项目启动期最大的瓶颈。
1.2 1.2万张的规模对训练意味着什么
12884张这个量级,放在二分类目标检测任务里是完全够用的。按常见的8:1:1划分,训练集约10300张,验证集和测试集各约1280张。对比一下,COCO虽然十几万张,但那是80个类;眼睛状态就两类,特征高度集中,10300张训练图配合数据增强,足以微调出一个工程可用的模型。
但要注意,12884是图片数量,不是标注框数量。一张人脸图里大概率有左右两只眼睛,有些图可能只标了一只,最终框数量通常大于12884。训练前统计一下实际框数,心里才有底。另外也要确认这1.2万张的多样性:是来自同一个视频序列抽帧,还是来自不同人、不同光照、不同姿态?数据来源越丰富,模型泛化能力越好。
1.3 解压后要先做一次资产盘点
拿到zip后我的习惯是先做资产盘点,而不是直接解压然后强行训练。常见的双格式目录长这样:
dataset/ ├── VOC/ │ ├── JPEGImages/ # 原始图片 │ ├── Annotations/ # XML标注文件 │ └── ImageSets/Main/ # train.txt val.txt test.txt └── YOLO/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/这一步看着基础,坑却不少。我曾遇到ImageSets里的train.txt文件指向的文件名和JPEGImages对不上,结果程序静默跳过缺失图片,白跑了一晚上。所以第一步写个脚本核对三件事:图片与标注文件名是否一一对应、每张图有没有空标注、两类样本各有多少框。文件名关联是训练框架的命根子,YOLO按basename找图片对应的txt,差一个字符都不会认。
提示:标注文件名与图片文件名必须完全一致,只是扩展名不同。压缩包里的目录结构在不同版本的数据集里可能不一样,优先以实际解压结果为准。
2. VOC与YOLO两套格式并存:坐标逻辑和转换避坑
2.1 两种格式标注同一张图,存储逻辑差在哪
VOC格式用XML描述一张图里的所有目标,关键字段是size里的width和height,以及object下的bndbox,记录xmin、ymin、xmax、ymax四个像素绝对值。YOLO格式则朴素得多,每个txt文件里一行一个目标:类别id、中心点x、中心点y、宽、高,后四个值全部归一化到0到1。
同一个框,两种格式只是同一信息的两种表达,但转换时最容易出错的是归一化基准和坐标顺序。VOC的坐标来自标注工具,YOLO需要的是归一化后的中心点表示。如果直接把xmin除以W当成cx,那模型学到的位置全是偏的。
| 属性 | VOC(XML) | YOLO(txt) |
|---|---|---|
| 文件后缀 | .xml | .txt |
| 坐标含义 | bndbox的xmin/ymin/xmax/ymax,像素绝对值 | class, cx, cy, w, h,归一化值 |
| 每图结构 | object节点,一个object一个框 | 一行一个框 |
| 类别记录 | name字符串 | 整数类别id |
| 坐标基准 | 原图分辨率 | 0到1的相对坐标 |
2.2 从XML到txt的换算公式与边界保护
从VOC到YOLO的核心公式不复杂:
- cx = ((xmin + xmax) / 2) / W
- cy = ((ymin + ymax) / 2) / H
- w = (xmax - xmin) / W
- h = (ymax - ymin) / H
真正麻烦的是边界条件。第一是坐标越界,手滑标注时xmax可能超过图片宽度,算出来w大于1,YOLO训练时会直接丢掉这个框。第二是无效框,w或h等于0、或只有一两个像素的框,绝大多数不是有效眼睛而是标注噪声。第三是类别映射,VOC里用字符串name,YOLO只认整数id,open和closed对应的id要固定下来,最好写进data.yaml。
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, output_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') W = int(size.find('width').text) H = int(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text # 类别映射,根据数据集的标注情况调整 if name == 'open': cls_id = 0 elif name == 'closed': cls_id = 1 else: continue box = obj.find('bndbox') xmin = float(box.find('xmin').text) ymin = float(box.find('ymin').text) xmax = float(box.find('xmax').text) ymax = float(box.find('ymax').text) xmin, xmax = sorted([xmin, xmax]) ymin, ymax = sorted([ymin, ymax]) dw = 1.0 / W dh = 1.0 / H cx = (xmin + xmax) / 2.0 * dw cy = (ymin + ymax) / 2.0 * dh w = (xmax - xmin) * dw h = (ymax - ymin) * dh # 过滤极小框和无效框 if w <= 0 or h <= 0: continue if w * W < 3 or h * H < 3: continue # 越界保护 cx = max(0.0, min(1.0, cx)) cy = max(0.0, min(1.0, cy)) w = max(0.0, min(1.0, w)) h = max(0.0, min(1.0, h)) lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") stem = os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(output_dir, stem + '.txt'), 'w') as f: f.write('\n'.join(lines))代码里的极小框过滤我特意保留了。眼睛目标虽然小,但小到几个像素的框基本是误标或脏数据,留着只会让模型学噪声。另外XML里的size字段偶尔会跟真实图片分辨率不一致,转换前抽查十几张图,用OpenCV或PIL读一下真实尺寸对比,能省掉后面排查的大把时间。
2.3 划分数据集时最容易漏的按人切分
双格式数据集的另一个隐性坑是数据划分。如果压缩包里已经给了train.txt和val.txt,先看一眼划分粒度。随机按图片划分会导致同一个人的多张连续帧被同时分进训练集和验证集,造成数据泄漏,验证指标虚高得离谱。
理想做法是按身份或视频序列划分。如果文件名前缀能区分来源,比如person001_0001.jpg这种,就以这个前缀为组别进行分组划分。很多公开数据集没有按这个做,所以别迷信压缩包自带的划分文件。我通常会把VOC的ImageSets读出来看一眼,如果发现划分粒度是单张图,就重新按组别划分一遍。
3. 把数据组织成YOLOv8想要的样子并完成训练
3.1 目录重组:一步到位还是踩坑重来
ultralytics训练YOLOv8或YOLO11时,默认按images和labels两个大目录找数据,子目录名要固定为train和val,测试集可以有但训练时不是必须。如果压缩包里的YOLO目录已经规范,直接建立软链接或拷贝即可;如果没有,从VOC的ImageSets/Main里读划分文件,逐张拷贝。
拷贝时别忘同名txt要跟着图片走。我写过不少脚本,最后发现最稳的写法是先遍历划分文件里的图片名,再同时拷贝图片和同名标注,而不是先拷全部图片再拷全部标注。目录结构一旦乱了,训练时会报大量“label not found”或“image not found”,排查起来非常费时间。
3.2 data.yaml写入与类别顺序确认
data.yaml是最容易写错又最不容易被发现的地方。
path: /path/to/face_eye train: images/train val: images/val test: images/test nc: 2 names: 0: open 1: closed注意names顺序必须和txt里的类别id一致。很多人在这里栽过:txt里0是open,data.yaml写成names: ['closed', 'open'],训练照样能跑,但模型学到的语义反了,推理时输出类别和预期完全颠倒。path建议用绝对路径,相对路径在不同系统下容易解读出歧义。改完data.yaml之后,我习惯先调用一下框架的数据检查命令,确认能正常读取图片数量和标注数量再开训。
3.3 模型选型与第一批超参数怎么定
模型选型方面,眼睛检测是实时性敏感任务,我一般先用yolov8n.pt跑通基线,确认数据没问题之后再换yolov8s.pt提升精度。如果只是想验证数据质量,选n模型就够;追求更高精度且算力允许,再上s模型。要知道眼睛开闭是二分类任务,模型容量需求不像80类检测那么高,盲目上l或x模型反而容易过拟合。
imgsz=640是平衡点。眼睛是小目标,理论上大输入有帮助,但推理速度会掉,做疲劳驾驶这种实时应用得不偿失。batch大小以显存为准,8G显卡的常见设置是n模型32、s模型16。epochs第一轮跑100,patience设20,让框架在val指标不再提升时自动停,避免死等。
yolo detect train data=data.yaml model=yolov8n.pt imgsz=640 batch=32 epochs=100 patience=203.4 训练过程中的监控指标与停止时机
训练后第一个看的不是mAP,而是val loss有没有像样的收敛。眼睛开闭任务在数据集干净的情况下,mAP50很容易到0.95以上,如果低于0.9,我建议先回去查标注,而不是继续堆epoch。
训练时打开results.png,重点看val/box_loss和val/cls_loss两条曲线。尾段小幅度震荡是正常的,一直不降就是有问题。如果loss曲线稳定但mAP偏低,考虑类别不平衡和标注噪声。还有一个容易被忽视的点:训练过程中最好保留最好的一次权重,也就是best.pt,不一定用last.pt,因为last很可能在过拟合边缘。
4. 眼睛样本特有的质量隐患:这些坑别等到训练后才查
4.1 微闭状态到底算睁眼还是闭眼
眼睛开闭数据集的标注标准,比坐标精确度更影响模型上限。最常见的分歧是微闭状态算哪一类。我的标准很简单:上眼睑和下眼睑有没有接触。没接触,哪怕只露出一条缝,也算open;完全贴合才算closed。
这套标准必须让所有标注人员统一执行。如果数据集内部标准不一,模型学到的特征会在半闭状态反复横跳,推理时你会看到闭眼置信度在两个类之间来回跳。我处理过一份数据,半闭图一会标open一会标closed,训练集精度很高,验证集一塌糊涂,最后定位到就是标注标准不一致。
4.2 眼镜、墨镜、刘海和红外画质
眼镜和墨镜是眼睛检测的宿敌。普通眼镜在强光下会有大面积反光,眼球纹理被高光盖掉,模型可能把反光区域当成眼睛边缘;墨镜直接盖住眼皮,根本没有闭眼特征,这种样本要么不标注,要么单独设一个unknown类。刘海遮挡也是同样逻辑,别硬归进open或closed。
另一个场景是红外相机。夜间驾驶监控常用红外图,特征分布和可见光差很多,如果这份数据集以可见光为主,直接拿去红外场景会明显掉点。遇到这种情况,需要补充对应的红外数据做微调,或者用红外相机重新采集,再使用之前说的半自动标注流程。
4.3 类别不平衡先统计再训练
类别不平衡在眼睛数据集里几乎必然存在。真实驾驶场景中睁眼是常态,闭眼是例外,所以标注框里open通常远多于closed。训练前统计类别数量,如果closed占比不到20%,先别急着调损失函数,优先做两类操作。
一个是对closed样本过采样,保证每个batch里都有足够闭眼框;另一个是在增强配置里适当提高亮度和对比度扰动,让模型更多地依赖眼睑形状而不是整图亮度。如果闭眼框实在太少,另一个可行方案是做水平翻转,把部分open样本在训练时翻到难侧,但要注意翻转后的位置信息由框架自动处理,不需要手动改txt坐标。
4.4 四步快速体检标注质量
标注质量快速体检,我按下面四步来:
- 写脚本统计每个框的宽高比。眼睛框宽高比通常在1.5到3之间,明显超出这个范围的大概率是错误框。
- 随机抽200张图,把GT框画出来肉眼过一遍,重点看框有没有歪、有没有框到眉毛、左右眼类别标反。
- 训练一个快速模型,在训练集上回看预测结果。置信度很高但GT没框的位置,往往是漏标。
- 抽查XML或txt里有没有坐标为0、面积为0或宽高逆序的脏数据。
注意:漏标对训练的影响比错标更隐蔽。一个漏标的闭眼框会让模型学到“这个位置没有目标”的负反馈,后期建议用半自动标注工具补漏。
5. 实战中把模型用起来的调优方向
5.1 单帧检测为什么会抖动
单帧模型部署到真实场景后,最常见的现象是误检和抖动。眨眼本身只有二三百毫秒,中间帧眼睑处于运动状态,模型可能一会儿判闭一会儿判睁。侧脸角度大时,眼睛框位置也会偏移,漏检率明显上升。
不要指望通过调NMS或置信度阈值解决所有问题。更稳的做法是把任务拆成两段:先做人脸检测和关键点对齐,按关键点坐标裁出左右眼部区域,再在眼部区域上跑open/closed判断。这样背景干扰大幅减少,模型容量可以不用太大,整体效果反而更稳。如果项目里有现成的人脸关键点模型,这个方案几乎是零成本。
5.2 用时序窗口和状态机稳住输出
如果你做的是疲劳驾驶这类持续监测应用,单帧输出根本没有意义。最终指标大概率是PERCLOS,也就是单位时间内眼睛闭合帧数的比例。这个指标自带时间窗口,正好适合在输出端做平滑。
我的做法是维护一个长度为30帧的滑动窗口,统计窗口内closed帧占比,超过阈值再触发告警。再配合一个简单状态机:连续3帧closed才进入闭眼状态,连续2帧open才恢复睁眼。这样眨眼过渡帧的抖动就被吃掉了,不需要在模型端硬压阈值。这套逻辑对算力几乎没开销,但体感上能过滤掉大部分误报。
5.3 小目标增强策略怎么调
关于数据增强,我踩过具体的坑。眼睛是小目标,随机尺度增强的scale参数不能设太大,否则很多眼睛框会被缩到几个像素,看着样本很多,实际学习效率很低。用ultralytics时我常用这套增强参数:
hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.4 degrees: 5 translate: 0.1 scale: 0.5 mosaic: 1.0mosaic增强对小目标有一定帮助,但在最后10个epoch会关闭,这是ultralytics的默认设置,不用手动干预。如果发现训练集精度高、验证集掉点严重,优先检查是不是增强过强,或者数据划分存在泄漏,而不是急着给模型加正则。
5.4 用半自动标注扩充自己的场景数据
扩充数据最省力的方式是视频抽帧加预标注。拿你自己的摄像头或场景监控拍一段,每隔3到5帧抽一帧,用已经训练好的模型做预标注,再把结果导入X-AnyLabeling这类工具人工修正。
抽帧间隔不能太密,连续帧几乎一样,增加的信息量很少;间隔太大又可能错过眨眼这类瞬时状态。人工修正时只需改框和改类别,比从零标注快一个量级。这样攒上两三千张场景数据,比盲目增加模型参数有用得多。扩充完之后,把新数据和原数据合并,重新按身份划分train和val,再继续微调一轮。
最后分享一个从这份数据里学到的体会:眼睛开闭检测的精度上限,很多时候不在模型而在于标注一致性。同样的12884张数据,如果open和closed的标准统一、划分干净,yolov8n这种小模型就能逼近工程可用;如果标准混乱、类别漏标,换再大的模型也白搭。拿到数据集后,先花半小时定好“闭合”的定义,再跑训练脚本,这是我踩过多次坑之后最想提醒你的一句话。
本文还有配套的精品资源,点击获取