屏幕缺陷检测数据集:VOC+YOLO双格式与YOLOv8训练实战
1. 项目背景与需求拆解做工业视觉这行的朋友应该都有体会算法模型本身的复杂度往往不是最大的门槛真正让人头疼的是数据。尤其是屏幕缺陷检测这种场景产线上玻璃面板、显示模组、触控屏表面的瑕疵种类多、形态细微、对比度低想靠公开数据集练出能用的模型基本不现实。所以我这次整理的这套工业质检屏幕缺陷检测数据集是围绕真实产线质检需求设计的一套小规模、双格式、可直接开训的数据集共616张图像、4个缺陷类别同时提供VOC格式的XML标注和YOLO格式的TXT标注拿过来就能按YOLO系模型的标准流程跑也可以作为VOC格式数据做其他目标检测框架的验证。先说一个很多人容易忽略的点屏幕缺陷检测本质上是一个小目标、低对比度、类别内差异极大的细粒度检测任务。缺陷可能只有几十甚至十几个像素反光、指纹、脏污还容易和背景混在一起。工业界做这个事通常不会一上来就给算法提检测所有缺陷这种不切实际的目标而是先聚焦产线售后和最终检工位反馈最集中的几类缺陷。1.1 屏幕缺陷检测在工业质检里的位置屏幕类产品从玻璃盖板到贴合模组、再到整机装配每个环节都可能引入缺陷。常见的有亮点、暗点这类点状缺陷也有划伤、脏污这类条状或片状缺陷。它们在外观上表现差异极大但对光学成像系统来说无非就是灰度异常、纹理异常和局部区域突变。深度学习目标检测在这里的价值是把人眼盯着流水线看这种极其枯燥且容易疲劳的质检动作替换成自动化的ROI抓取和缺陷分类。616张数据放在深度学习里说实话不算多但放在工业质检场景下它是合理的。工业缺陷样本的获取成本高尤其是真正的缺陷件很多是产线攒了几个星期甚至几个月才积累下来的。616张、4类别平均每个类别150张左右这个量级很适合做三件事第一验证YOLOv8这类模型在该任务上的可行性第二做迁移学习和数据增强的实验基座第三给现场工程师快速搭一个能跑的检测demo后续再用产线数据迭代扩充。1.2 4个类别怎么定样本量怎么设计我这次按屏幕质检最常见的四个方向组织标签亮点bright spot、暗点dark spot、划伤scratch、脏污contamination。这四个类别基本覆盖了屏幕外观检测中80%以上的返修原因而且它们在图像特征上差异清晰亮点是高灰度小面积区域暗点是低灰度小面积区域划伤是细长条状的低灰度或者异色纹理脏污则是不规则片状的灰度弥散区域。每个类别约150张对于四类检测任务来说类别分布相对均衡。实际使用中你完全可以根据现场情况调整比如把亮点和暗点合并成点状缺陷或者把划伤按方向再细分成线性划伤和弧形划伤。我的建议是初始版本类别宁粗勿细先把能稳定区分的大类做对再逐步细分。因为标注一致性在小样本项目中极为关键类别定义越主观标注噪声越大模型收敛越差。2. 数据集的整体设计与格式选型为什么同时给VOC和YOLO两种格式这是我在交付数据集时的一个习惯尽可能让使用者不卡在格式转换上。目标检测领域的标注格式五花八门但VOC和YOLO是最高频的两种。VOC格式适合做传统的两阶段检测器Faster R-CNN、SSD等训练也方便用LabelImg等工具二次编辑YOLO格式则是YOLOv5、YOLOv8等系列模型的直接输入格式。两种格式本质上是同一批标注信息的不同表达区别只在于存储方式和坐标体系。2.1 VOC格式的核心结构与目录规范VOC格式PASCAL VOC的标注信息存放在XML文件中每个XML文件对应一张图片文件名和图片名保持一致。一个标准的VOC XML标注包含文件名、路径、图像尺寸以及一个或多个object节点。每个object节点里有name类别名、pose、truncated、difficult以及bndbox左上角和右下角坐标。下面是一个典型的XML片段结构annotation folderJPEGImages/folder filenamescreen_defect_001.jpg/filename path/data/screen_defect_001.jpg/path source databaseUnknown/database /source size width1280/width height720/height depth3/depth /size segmented0/segmented object namescratch/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin312/xmin ymin245/ymin xmax589/xmax ymax276/ymax /bndbox /object /annotation这套数据结构里bndbox是绝对像素坐标从0到图像宽高。大家用LabelImg标注后保存的XML天然就是VOC格式所以拿到这种数据集后最顺手的操作方式就是直接打开XML看标注内容或者用LabelImg打开图片和对应的XML做检查和微调。2.2 YOLO格式的标注规则与归一化细节YOLO格式通常是一个TXT文件对应一张图片文件每一行代表一个目标行内共5列依次是类别id从0开始、归一化后的中心点x坐标、中心点y坐标、归一化后的宽度w、高度h。归一化的意思就是把真实像素坐标除以图像宽高让所有数值落在0到1之间。例如上面那个scratch目标如果图片宽1280、高720那么对应YOLO格式的结果计算如下x_center (312 589) / 2 / 1280 0.35195 y_center (245 276) / 2 / 720 0.36181 width (589 - 312) / 1280 0.21641 height (276 - 245) / 720 0.04306对应TXT文件里的一行就是2 0.35195 0.36181 0.21641 0.04306。这里面有一个特别容易踩的坑YOLO的类别id必须从0开始并且与你在yaml配置文件里定义的类别顺序严格对应。如果你的数据集是VOC格式转过来的原XML里name是字符串比如scratch那就必须建立一个字符串到整数id的映射字典比如{bright_spot: 0, dark_spot: 1, scratch: 2, contamination: 3}。转的时候如果顺序对不上模型训练时就会出现类别错乱的诡异问题——检测框位置全对但类别标签全错。2.3 为什么同时提供两种格式我在实际项目里遇到过好几次这样的情况拿YOLO训练到一半发现某个类别的缺陷定义需要调整手动改几百个TXT文件效率极低此时如果有VOC格式的XML用LabelImg直接打开改改完了用脚本批量转回YOLO整个过程不到十分钟。反过来也一样如果你想在detectron2这类工具上做实验VOC格式是更通用的选择。所以这套数据集两种格式都给等于把灵活性交给使用者避免因为格式绑死而影响后续的标注迭代。3. 从VOC到YOLO格式转换与目录组织实操这套数据集里VOC和YOLO两个版本是同一个标注信息的两份产物。如果你想在拿到数据后自己再处理一遍、或者以后自己标数据做转换这一步完全可以复制。3.1 转换思路与目录组织核心转换逻辑就是把XML中的bndbox坐标做一次归一化换算同时把类别名称映射成数字id。组织目录的时候我习惯把数据集按标准结构摆放方便后续喂给训练脚本screen_defect_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── VOC/ │ ├── JPEGImages/ │ ├── Annotations/ │ └── ImageSets/ │ └── Main/ │ ├── train.txt │ ├── val.txt │ └── test.txt └── screen_defect.yamlimages和labels按train/val/test分开是YOLOv8最直观的读取方式VOC目录则是标准的PASCAL VOC布局JPEGImages放图片Annotations放XMLImageSets/Main下放划分文件的txt列表。划分比例我建议按8:1:1来分train约493张、val约61张、test约62张。验证集和测试集虽然不大但对这个数据量来说足够初步评估模型了。3.2 转换脚本实现与参数说明下面给出一个可以直接运行的VOC转YOLO脚本我加了详细注释。这个脚本假定你的VOC XML都在Annotations目录下图片在JPEGImages目录下划分列表在ImageSets/Main/train.txt和val.txt里。import os import xml.etree.ElementTree as ET # 类别映射表顺序就是YOLO的class id CLASS_MAPPING { bright_spot: 0, dark_spot: 1, scratch: 2, contamination: 3, } def convert_annotation(xml_path, output_path, image_width, image_height): 将单个VOC XML转换为YOLO TXT标注文件 tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASS_MAPPING: print(f警告: 跳过未知类别 {name} 在文件 {xml_path}) continue class_id CLASS_MAPPING[name] bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 归一化并限制在0-1范围内防止越界 x_center ((xmin xmax) / 2) / image_width y_center ((ymin ymax) / 2) / image_height w (xmax - xmin) / image_width h (ymax - ymin) / image_height # 越界保护 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(output_path, w) as f: f.write(\n.join(lines)) def batch_convert(image_dir, annotation_dir, output_dir, id_list_file): 批量转换train或val的划分列表 os.makedirs(output_dir, exist_okTrue) with open(id_list_file, r) as f: image_ids [line.strip() for line in f if line.strip()] for img_id in image_ids: xml_path os.path.join(annotation_dir, f{img_id}.xml) if not os.path.exists(xml_path): print(f跳过缺失标注: {img_id}) continue # 读取图片尺寸需要PIL支持 from PIL import Image img_path os.path.join(image_dir, f{img_id}.jpg) with Image.open(img_path) as img: width, height img.size output_path os.path.join(output_dir, f{img_id}.txt) convert_annotation(xml_path, output_path, width, height) # 使用示例 batch_convert( image_dirVOC/JPEGImages, annotation_dirVOC/Annotations, output_dirlabels/train, id_list_fileVOC/ImageSets/Main/train.txt, )这个脚本有两个设计细节值得注意。第一我是用图片的实际像素尺寸来归一化不是用XML里的size节点因为有可能发生图片被压缩或预处理导致实际尺寸与XML记录不一致的情况直接读图最保险。第二归一化后做了0到1的截断处理防止标注框略微超出图像边界时产生负数宽度或越界数值这类数据异常在训练时会导致YOLO的loss直接变成nan。3.3 类别映射与文件名对齐转换过程中最容易被带偏的就是类别顺序。你拿到这套数据集的时候建议先看一遍labels/train下的任意一个TXT文件再对照打开对应的VOC XML确认一下映射关系。特别是如果你后续要合并自己的数据原数据的类别顺序和这套数据不一致必须把类别id重新映射到统一的标准上。我的做法是维护一份单独的classes.json文件把类别名的顺序固定下来任何脚本都从这份文件读映射关系而不是硬编码在代码里。这样数据集被别人接手的时候不至于还得翻代码才知道哪个id是哪个缺陷。文件名对齐也容易出问题。YOLO训练时默认找与图片同名的TXT文件如果你的图片是screen_defect_001.jpg那么标注文件必须是screen_defect_001.txt。后缀名无所谓但主文件名必须完全一致。我见过有人因为图片从.bmp转成.jpg后文件名变了导致几百张图全部没有对应标签训练时loss正常但map为0。检查这个问题的办法很直接看YOLO训练日志中每张图是否有标签文件被加载或者自己在数据集目录里跑一个脚本对比一下两边的文件名集合。4. 基于该数据集训练YOLOv8完整落地流程数据准备好之后训练环节反而是最省事的。我用YOLOv8举例因为它对数据集格式的要求最标准而且ultralytics这个库把数据加载、增强、训练、验证整个流程封装得足够简单。当然这套数据集的格式对YOLOv5、YOLOX、RT-DETR同样适用只是配置文件写法略有差异。4.1 数据集yaml配置写法YOLOv8通过一个yaml文件描述数据集路径、类别数量和类别名称。我提供的screen_defect.yaml内容如下# 屏幕缺陷检测数据集配置 path: /data/screen_defect_dataset # 数据集根目录 train: images/train # 训练集图片目录 val: images/val # 验证集图片目录 test: images/test # 测试集图片目录可选 nc: 4 names: 0: bright_spot 1: dark_spot 2: scratch 3: contamination这里有一个值得注意的细节path字段指向数据集根目录后train和val写相对路径即可ultralytics会拼接成绝对路径。如果你的项目里不同机器路径不同可以用环境变量或者相对当前工作目录来写避免每换一台机器就要改yaml。另外nc必须和names的数量一致这里面如果写错了训练时会出现类别数量不匹配的报错或者训练能跑但map结果完全不可信。4.2 训练命令与关键参数选择在终端里进入ultralytics环境后一条命令就可以开训yolo detect train \ --model yolov8n.pt \ --data screen_defect.yaml \ --epochs 200 \ --imgsz 640 \ --batch 16 \ --device 0 \ --patience 30 \ --project runs/screen_defect \ --name baseline_v1一些参数的选择理由我给各位说明一下。模型选择yolov8n的预训练权重是因为这个数据集只有616张属于典型的小样本。n模型参数量最小迁移学习的起点是COCO预训练权重在小数据集上更容易收敛且不容易过拟合。如果你追求极端精度可以用yolov8s试试s后面就不建议在这个数据量下硬上了容易提前过拟合。epochs设成200配合patience早停参数30。就是说连续30个epoch验证集mAP没有提升就自动停止这样训练不一定真的跑满200轮但给了模型足够的收敛时间。工业缺陷检测的收敛速度往往比自然图像快因为背景相对单一。imgsz 640是一个精度和速度都比较平衡的输入尺寸如果你的缺陷目标偏小可以试imgsz 1280但显存占用会明显增加。AMD RX 580这类显卡显存通常8GB跑yolov8n imgsz 640 batch 16有点紧张建议把batch降到8甚至4NVIDIA 30系以上显卡就从容很多。4.3 训练结果验证与模型导出训练结束后在runs/screen_defect/baseline_v1/目录下会生成weights/best.pt和last.pt。best.pt是验证集上mAP最高的权重实际部署用这个。验证一下模型效果yolo detect val \ --model runs/screen_defect/baseline_v1/weights/best.pt \ --data screen_defect.yaml \ --split val然后看输出的mAP50和mAP50-95指标。对于这类小样本工业数据集我通常更关注mAP50——因为缺陷检测在实际质检里对框的精确度要求往往没有对是否检出、检出后类别是否准确要求高。mAP50如果能到0.85以上这个数据集作为起步版本就已经很有说服力了。接着导出推理需要的格式yolo export \ --model runs/screen_defect/baseline_v1/weights/best.pt \ --format onnx \ --opset 12导出ONNX是为后面用TensorRT或者OpenVINO部署做准备的。到这里从数据到模型的闭环就通了。5. 小样本屏幕缺陷任务的常见问题与避坑经验最后这部分是我做工业视觉项目时踩过的坑汇总针对这套数据集的实际情况按问题出现的频率和影响程度排序。5.1 小样本与类别不平衡616张图4类平均每类一百多张这在小样本目标检测里属于勉强能跑的规模。最常见的问题是训练集上loss降得很低验证集上mAP却不高典型过拟合信号。应对办法有几个维度一是数据增强开满特别是mosaic、copy-paste、旋转、平移、亮度扰动这些对屏幕缺陷这种目标形态变化不大的场景几何增强比颜色增强更有效二是冻结backbone前几层做迁移学习YOLOv8默认会加载预训练权重你可以先冻结backbone训50轮再解冻全模型微调三是引入额外的负样本——如果你手头有大量没有缺陷的屏幕图像可以在训练时混入一小部分纯负样本图YOLO本身能处理图片中没有目标的情况对应TXT文件为空文件这样做能显著降低误检率。5.2 边界框标注不准确与文件名错位屏幕缺陷的边缘往往模糊不同标注员对缺陷范围的界定可能差出两三个像素。两三个像素对分类任务无所谓但对回归框的loss影响不小。我的做法是标注后统一做一次框审查把每个框在图上画出来轮流盯一遍重点看框是否完整包住缺陷有没有把正常纹理框进去。这个环节虽然费时间但能明显提升mAP尤其对划伤这类长条目标。至于文件名错位前面已经给过检查脚本的思路这里不赘述只提醒一句数据集分发后第一件事就是核对图片数量、标签数量、文件名交集三者的数量相等才继续往下走。5.3 推理阶段的误检与后处理训练时模型见过的基本都是产线固定工位、固定光源下的图像换一个光源角度推理结果可能明显变差。屏幕缺陷对反光极其敏感同一个划痕在某个打光角度下清晰可见在另一个角度下完全隐没。所以工业部署时推理端最好和训练数据保持相同的光学条件这比换模型网络更管用。如果现场条件无法完全一致可以准备一个小型的badcase回归集——把现场新拍的异常图不断追加到验证集里每次模型更新后都重新跑一遍防止过拟合到旧数据分布。另外推理阶段有个小技巧YOLO输出的置信度阈值conf_thres在屏幕缺陷场景下要调低一点比如0.15到0.25因为缺陷小目标对比度低时模型的置信度普遍偏低。代价是误检变多但你可以通过后处理来兜底——比如根据缺陷面积过滤掉过小的框、根据框的长宽比过滤掉明显不像缺陷的目标。这套逻辑看着简单实际工程里多数误检都能靠它压下去。5.4 扩展建议数据增强、迁移学习与持续迭代最后聊聊这套数据集的扩展路径。616张只是起点工业现场的数据积累是一个持续过程。最实用的扩展方式是从产线捡出更多带缺陷的图片自动做一次粗标注用现有模型预测然后人工修正这样标注效率能提升一大截。其次是做缺陷合成屏幕的坏点、亮点这类规则形状缺陷可以通过图像处理的方式合成到干净屏幕上。合成数据虽然和真实数据有domain gap但在小样本前提下作为补充是划算的。迁移学习这块还有个细节COCO预训练权重对屏幕缺陷这类人造纹理场景的帮助有限因为COCO里几乎没有类似目标。如果你们公司有之前积累的其他工业检测数据集哪怕类别完全不同用它做预训练再迁到屏幕缺陷任务效果往往比直接从COCO迁移更好。这个经验我在多个工业项目里验证过值得一试。数据集的维护同样重要。我习惯在每个版本发布时做三件事记录类别定义的变化、记录划分文件用了哪个随机种子、记录模型基线的精度。这样后面任何一次改动都能定位性能变化是数据引起的还是模型引起的。616张的库虽然小但有了这套规范后续扩到一千张、五千张的时候整个流程都不会乱。屏幕缺陷检测这个方向数据的获取难度永远大于模型调参的难度。一套结构规整、格式统一的数据集能让后面的工作少走很多弯路。这套VOCYOLO双格式的数据集无论是作为工业视觉入门的学习材料还是作为实际项目的起步版本都很合适。真正上手跑一遍再根据自己的产线情况扩展数据、调整类别你会发现这个领域的门槛并没有想象中那么高。