眼睛开闭检测数据集实战:VOC转YOLO格式与YOLOv8训练全流程 简介面向眼睛睁开闭合检测任务的数据集包为计算机视觉目标检测训练提供标准化数据基础适用于疲劳驾驶预警、注意力分析等场景。数据集包含12884张人脸/眼部图片对应VOC格式XML标注与YOLO格式TXT标签各12884份标注类别为closed与opened两类矩形框总数为13026个图片清晰度整体较好且未做增强可直接满足常规检测模型的输入需求。压缩包共2000个文件其中xml标注文件1999个、txt说明文件1个整体大小约272.73MB目录结构区分图片、XML、TXT三个文件夹便于按需调用与二次处理。目前已有249人学习浏览。数据集提供准确合理的标注不附带训练权重用户拿到后即可进行模型训练与调优省去数据清洗和格式转换环节适合需要高质量眼睛状态样本的算法工程师与研究者。 做疲劳驾驶检测那会儿最让我头疼的其实不是模型结构而是数据。眼睛睁开闭合识别这种任务公开数据集本来就少带干净标注的更少能直接扔进YOLO训练的更是可遇不可求。当朋友发我一份“眼睛睁开闭合识别数据集 yolovoc格式 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那模型学到的位置全是偏的。属性VOCXMLYOLOtxt文件后缀.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) / Wcy ((ymin ymax) / 2) / Hw (xmax - xmin) / Wh (ymax - ymin) / H真正麻烦的是边界条件。第一是坐标越界手滑标注时xmax可能超过图片宽度算出来w大于1YOLO训练时会直接丢掉这个框。第二是无效框w或h等于0、或只有一两个像素的框绝大多数不是有效眼睛而是标注噪声。第三是类别映射VOC里用字符串nameYOLO只认整数idopen和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是opendata.yaml写成names: [closed, open]训练照样能跑但模型学到的语义反了推理时输出类别和预期完全颠倒。path建议用绝对路径相对路径在不同系统下容易解读出歧义。改完data.yaml之后我习惯先调用一下框架的数据检查命令确认能正常读取图片数量和标注数量再开训。3.3 模型选型与第一批超参数怎么定模型选型方面眼睛检测是实时性敏感任务我一般先用yolov8n.pt跑通基线确认数据没问题之后再换yolov8s.pt提升精度。如果只是想验证数据质量选n模型就够追求更高精度且算力允许再上s模型。要知道眼睛开闭是二分类任务模型容量需求不像80类检测那么高盲目上l或x模型反而容易过拟合。imgsz640是平衡点。眼睛是小目标理论上大输入有帮助但推理速度会掉做疲劳驾驶这种实时应用得不偿失。batch大小以显存为准8G显卡的常见设置是n模型32、s模型16。epochs第一轮跑100patience设20让框架在val指标不再提升时自动停避免死等。yolo detect train datadata.yaml modelyolov8n.pt imgsz640 batch32 epochs100 patience203.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这种小模型就能逼近工程可用如果标准混乱、类别漏标换再大的模型也白搭。拿到数据集后先花半小时定好“闭合”的定义再跑训练脚本这是我踩过多次坑之后最想提醒你的一句话。本文还有配套的精品资源点击获取