ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLO钢材表面缺陷检测实战:从数据集解析到训练调优

YOLO钢材表面缺陷检测实战:从数据集解析到训练调优 简介面向目标检测与钢材表面缺陷识别场景这份数据集由资深算法工程师整理包含1800张已标注图像覆盖crazing、inclusion、patches、pitted_surface、rolled-in_scale、scratches六类典型缺陷适用于计算机、电子信息工程、数学等专业学生的课程设计、期末大作业与毕业设计。压缩包共3608个文件核心为1800个jpg原图与对应的1803个txt标注文件另附YOLO训练用的Python脚本和yaml配置方便直接修改参数、快速启动训练整体体积约25.95MB。图片与标注一一对应代码注释明细、编程思路清晰能帮助用户节省数据预处理时间快速掌握基于YOLO的缺陷检测流程。目前该数据集已有1591人学习浏览适合需要开展表面缺陷检测实验或完成相关项目实战的学习者直接使用。1. YOLO目标检测与钢材表面缺陷1800张已标注图像能做什么拿到这个zip包时我的第一反应不是“又有数据集了”而是“终于有一个不用自己画框的数据集了”。钢材表面缺陷检测在产线上是典型的视觉定位分类任务六类缺陷crazing、inclusion、patches、pitted_surface、rolled-in_scale、scratches覆盖了热轧钢板最常见的表面异常。1800张已标注图像对于YOLO目标检测来说不大不小比COCO小得多但比学术上的玩具数据集又大恰好适合做课程设计、毕业设计或者快速验证检测算法的训练流程。zip里除了jpg图片还有train.cache和val.cache这两个文件是YOLO训练前对图片和标签校验生成的缓存说明作者已经把train/val划分好且跑通过一次训练流程。接下来的内容我会从数据解析、标注格式转换、训练参数设置到缺陷漏检调优完整拆一遍这个数据集的用法。2. 六类缺陷样本特征与标注文件解析2.1 缺陷命名规则与视觉先验文件名以crazing_50.jpg、inclusion_270.jpg这类前缀区分数字只是序号。六类缺陷在钢材表面的表现差异很大缺陷类型视觉特征常见位置crazing网状裂纹细密龟裂状纹理呈网络状分布轧辊异常导致表面应力集中区inclusion夹杂物点状或短条状深色颗粒形状不规则钢板表层或皮下常成群出现patches斑块块状灰暗区域边界模糊水渍或冷却不均造成的表面区域pitted_surface麻点密集的黑色小凹坑边缘较锐利氧化皮剥落后留下的痕迹rolled-in_scale压入氧化皮片状或条状覆盖物反光与基底不一致轧制工序中氧化皮压入表面scratches划伤细长连续线状方向单一与轧制方向平行的机械划痕这种先验很重要因为后续做数据增强时crazing和pitted_surface都是密集小目标对mosaic的随机裁剪缩放非常敏感scratches则是极端长条形如果imgsz小于640很容易在缩放到默认尺寸时丢失横跨整图的细线。我一般会在做训练前先按类别抽几张图看一眼确认标注框是不是贴合纹理边界而不是包络一个大矩形。2.2 train.cache与val.cache的正确读法cache文件是YOLO工具链无论是v5还是v8在第一次扫描数据后保存的校验缓存。如果直接删掉训练时会重新生成如果里面记录的图片路径失效训练会直接报File not found。很多初学者上来就报错其实是cache文件里的路径和当前目录不匹配。可以用下面的代码读出cache内容import pickle from pathlib import Path def load_cache(cache_path): with open(cache_path, rb) as f: data pickle.load(f) return data cache load_cache(train.cache) print(type(cache)) # dict print(cache.keys()) # dict_keys([labels, file_hash, results, msgs, version]) # labels: {image_path: (num_boxes, (x1,y1,x2,y2,...))} # results: 1 表示该图片解析正常 for img_path, img_info in cache[labels].items(): n_boxes img_info[0] if isinstance(img_info, tuple) else len(img_info) print(f{img_path}: {n_boxes} boxes)这段代码用pickle加载YOLO缓存文件。cache里的labels字段存储了每张图片的标注框数量results为1表示图片和标签匹配无误。第一次读这个文件时我建议重点看两点一是路径前缀如果里面的路径是/home/user/...而你解压路径不同就要人工修正或删除cache让它重新生成二是每个图片的框数如果出现大量0框的图片说明标注文件漏了需要检查标签目录。另外一个小细节cache文件里的file_hash是对图片内容做的哈希校验如果zip包在传输过程中损坏或者你用某些国产解压软件强行解压导致图片字节变化训练时会提示corrupt cache并自动重建。此时留意一下报错时卡的图片把它单独拎出来看是不是真的打不开。2.3 标注文件格式txt还是xmlzip文件名只写了“已标注文件”没有说明具体格式。钢材表面缺陷常见数据集如NEU-DET提供的是VOC XML而很多平台打包时为了YOLO默认配套会转成txt。区分方式很简单看与图片同名的.txt文件是否存在或看labels目录结构。这里给出通用的检查脚本import os img_dir images/train lab_dir labels/train missing 0 for img in os.listdir(img_dir): if not img.endswith(.jpg): continue base os.path.splitext(img)[0] txt os.path.join(lab_dir, base .txt) xml os.path.join(lab_dir, base .xml) if os.path.exists(txt): with open(txt) as f: line f.readline().strip() print(f{base}: txt, first line {line}) elif os.path.exists(xml): import xml.etree.ElementTree as ET tree ET.parse(xml) first_obj tree.getroot().find(object) print(f{base}: xml, object name {first_obj.findtext(name) if first_obj is not None else none}) else: missing 1 print(fmissing labels: {missing})这段脚本的作用是遍历训练图片检查每个图片对应的标签是txt还是xml并打印首行内容。如果是YOLO txt首行是class_id x_c y_c w h其中x_c、y_c是归一化到0-1的浮点数如果是VOC xml首行object包含name和bndbox。根据输出就能决定后续是否需要做格式转换。如果缺失标签的图片数量超过0.1%我建议先补标注再训练否则这些图片会被YOLO当成背景负样本反而干扰定位。2.4 标注坐标合法性校验即使格式正确标注坐标也常常存在越界、宽高为0、类别ID超出范围等问题。这些错误在训练时不会立即崩但会污染损失函数曲线。可以写一个简单的过滤脚本import os def check_label(txt_path, img_w, img_h): bad 0 with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: bad 1 continue cls_id, xc, yc, w, h [float(p) for p in parts] if cls_id 6 or cls_id 0: bad 1 if w 0 or h 0 or xc 0 or xc 1 or yc 0 or yc 1: bad 1 x1 (xc - w / 2) * img_w y1 (yc - h / 2) * img_h x2 (xc w / 2) * img_w y2 (yc h / 2) * img_h if x1 0 or y1 0 or x2 img_w or y2 img_h: bad 1 return bad参数说明img_w和img_h是图片实际宽高YOLO格式的xc、yc、w、h都是归一化值。但很多标注工具导出时会把坐标计算成小数可能因为像素值除以宽高时取整误差导致w或h为负。越界检测这里用了反算像素坐标的方式如果框边缘跑到图片外说明标注工具没有执行裁剪。对于钢材缺陷我的建议是如果异常框占比小于0.5%直接丢弃因为缺陷本身密集多一个少一个不敏感如果大于1%说明标注工具导出流程有问题需要回到原始XML重新转换。3. 将已标注数据转换为YOLO可训练格式目录重构与类别映射3.1 标准YOLO数据目录结构不管原始标注是txt还是xml最终都要组织成YOLO要求的目录结构steel_defect/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/我们把zip解压后所有图片先复制到images/train然后按比例切分。这里要注意YOLO的cache文件记录的路径是相对于data.yaml里指定的path如果目录结构变了cache会自动重生成。所以先搭目录再训练是安全的。有很多人喜欢把图片和标注放在同一个目录里YOLOv5/v8也能跑但后续做pipeline分流时会很痛苦建议一步到位。3.2 从VOC XML到YOLO txt的转换脚本如果检查发现是VOC XML或者混合格式可以用下面的脚本统一转换。这个脚本假设所有xml文件在annotations/目录下import os import xml.etree.ElementTree as ET from glob import glob class_map [crazing, inclusion, patches, pitted_surface, rolled-in_scale, scratches] def xml_to_yolo(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() img_w int(root.findtext(size/width)) img_h int(root.findtext(size/height)) lines [] for obj in root.findall(object): name obj.findtext(name) if name not in class_map: continue cls_id class_map.index(name) bndbox obj.find(bndbox) x1 float(bndbox.findtext(xmin)) y1 float(bndbox.findtext(ymin)) x2 float(bndbox.findtext(xmax)) y2 float(bndbox.findtext(ymax)) xc (x1 x2) / 2 / img_w yc (y1 y2) / 2 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}) out_path os.path.join(out_dir, os.path.basename(xml_path).replace(.xml, .txt)) with open(out_path, w) as f: f.write(\n.join(lines)) os.makedirs(labels/train, exist_okTrue) for xml_file in glob(annotations/*.xml): xml_to_yolo(xml_file, labels/train)逻辑说明先读取xml里的图像宽高然后对每个object计算bounding box的归一化中心坐标和宽高。注意类别必须与后文data.yaml里的names顺序一致否则训练出来的类别ID会错位。如果你的标注里出现了name未在class_map中的值脚本会直接跳过这样可以过滤掉不感兴趣的类。参数说明img_w和img_h也可以从xml以外获得但xml里通常都有。如果xml没有size节点可以读对应图片用PIL获取但那样速度慢而且需要额外IO。对于1800张图像这个脚本几秒就能跑完。3.3 数据划分与类别均衡性统计数据划分不能直接使用随机shuffle因为钢材缺陷类别之间数量可能不平衡。我一般用分层抽样确保train和val里每类缺陷的比例大致一致。下面是一个简单的实现import random from collections import defaultdict from glob import glob import shutil random.seed(42) img_paths glob(images/train/*.jpg) defect_to_imgs defaultdict(list) for img_path in img_paths: base os.path.basename(img_path).split(_)[0] # 用前缀做粗分类 defect_to_imgs[base].append(img_path) val_ratio 0.2 val_imgs [] for defect, items in defect_to_imgs.items(): random.shuffle(items) val_count int(len(items) * val_ratio) val_imgs.extend(items[:val_count]) # 移动文件到images/val for img in items[:val_count]: shutil.move(img, images/val/) # 对应移动标签 txt img.replace(/images/, /labels/).replace(.jpg, .txt) shutil.move(txt, labels/val/) total_train len(glob(images/train/*.jpg)) total_val len(glob(images/val/*.jpg)) print(ftrain: {total_train}, val: {total_val})这个脚本用文件名前缀当类别标签做分层抽样虽然不够精确因为prefix并不100%等于标注类别但对于保证每类在验证集中不缺失已经足够。如果你需要更精确的分层应该先读一遍标签文件统计每类的图片数量再按图片打标签。注意shutil.move会直接移动文件建议在副本上做保留zip解压后的原始目录作为备份方便后续重新切分。划分完成后最好统计一下每个类别的真实框数量而不是图片数量。因为patches类每张图可能只有1-2个框而crazing每张可能有几十个框。统计可以用下面命令for f in labels/train/*.txt; do cut -d -f1 $f; done | sort | uniq -c | sort -nr这条bash命令把训练集标签每一行的第一个字段类别ID提取出来排序计数。输出格式是“数量 类别ID”比如5341 0。如果发现某个类别的框数特别少比如rolled-in_scale只有几百框在训练时就要考虑类别权重或copy-paste增强。复制几份这种少样本图像到训练集里是错误做法正确做法是使用repeat_augment或instance_segment增强。3.4 data.yaml配置与绝对路径的坑所有细节准备好后创建data.yamlpath: D:/datasets/steel_defect train: images/train val: images/val names: 0: crazing 1: inclusion 2: patches 3: pitted_surface 4: rolled-in_scale 5: scratches注意path必须是绝对路径用于YOLOv8。如果使用YOLOv5需要把train和val写成带完整路径的形式。这里最容易出错的是Windows下的反斜杠YAML解析时会把D:\datasets当成转义字符所以统一用正斜杠。另外如果项目文件夹在移动硬盘或网络盘上训练时路径前缀可能变化建议把data.yaml放在项目根目录并确保每次训练前用相对路径重新生成。4. YOLOv8/v5训练配置与损失曲线解读4.1 模型规模选型n/s/m怎么选1800张图每张图可能多个缺陷综合算下来训练样本量不算大。我用YOLOv8在类似规模的数据集上跑过建议优先选yolov8s.pt而不是n或m。原因如下表模型参数量mAP50-95 (COCO)训练时间(单卡3060)适用场景YOLOv8n3.2M37.3约0.5h快速验证pipelineYOLOv8s11.2M44.9约1.2h平衡精度与速度首选YOLOv8m25.9M50.2约2.3h更大参数量但易过拟合这里的时间是估算值实际取决于batchsize和imgsz。对于钢材表面缺陷六类缺陷的纹理特征非常细微特别是crazing网络需要从小感受野中捕获细节。YOLOv8n在小目标上往往漏检严重而YOLOv8m在这个数据量下容易过拟合除非开启更强的dropout或增大增强强度。所以我一般先用s模型跑一个100 epoch的baseline看PR曲线再决定是否换模型。4.2 训练命令与超参数说明假设已经按第三章配好data.yaml训练命令如下yolo detect train \ datasteel.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ mosaic1.0 \ mixup0.2 \ fliplr0.5 \ scale0.5 \ patience15 \ device0参数说明imgsz640训练输入尺寸。如果raw图像分辨率高于640这个值会触发letterbox缩放。钢材缺陷不是极端小目标640足够想提升细微缺陷如scratches的召回可以改成768或896但显存占用会平方上升。我建议先跑640的baseline再在best.pt上用768验证一遍mAP取分数高者。mosaic1.0启用mosaic增强把所有图片拼成四宫格。mosaic对密集缺陷有奇效但要注意如果标注框太小mosaic后框可能缩到1像素反而变成噪声。如果loss曲线开始震荡可以把mosaic降到0.5。mixup0.2混合两张图适合数据量偏少的场景。但mixup会让框的边界变得模糊钢材缺陷本身边缘不清晰这个值不宜过大。0.2是我的惯例如果你看到loss在中期反弹先关mixup再试。lr00.01初始学习率。YOLOv8官方默认是0.01但小数据集建议降到0.005避免前几个epoch梯度震荡。可以先用0.01跑10个epoch如果train loss一开始就冲高立即停掉改成0.005。patience15早停如果15个epoch验证集mAP没有提升就停止。我通常设置30但这个数据集小15已经能防止过拟合。训练过程中项目会自动生成runs/detect/train目录里面有weights/best.pt和weights/last.pt。我的习惯是每个epoch存一次checkpointsave_period10方便中途恢复。4.3 损失函数曲线与收敛判断YOLOv8的损失由三部分组成box_lossCIoU损失、cls_lossBCE分类损失、dfl_lossDistribution Focal Loss用于回归框的分布估计。在训练日志中会看到这些指标也可以用tensorboard查看tensorboard --logdir runs/detect/train正常的收敛过程前20个epochbox_loss从0.15快速下降到0.05cls_loss从1.0下降到0.1之后下降变缓val/box_loss和train/box_loss的差距如果慢慢拉大说明开始过拟合。对于钢材缺陷我特别关注val/cls_loss因为六类缺陷中rolled-in_scale和patches外观相似容易在分类边界上混淆。如果cls_loss停滞在0.2以上建议去检查下一个epoch的PR曲线。看PR曲线的命令yolo detect val \ datasteel.yaml \ modelruns/detect/train/weights/best.pt输出的results.png里有每个类别的PR曲线。重点关注P和R都低于0.7的类别这类缺陷要么标注质量差要么特征与另一个类别太接近。如果scratches的召回率低通常是因为长条形目标在mosaic增强时被切断可以在训练时把mosaic改成0.8并增大scale或者把imgsz调整到与原始图像宽高比接近。我还习惯跑一次yolo detect val后生成confusion_matrix.png看哪两个类别互相误检然后用类别权重给难分类的类加大loss。4.4 类别不均衡的损失权重调整如果统计发现rolled-in-scale只有几百个框而有几千个框可以通过loss权重拉高小类的梯度贡献。在YOLOv8中可以在data.yaml中加入weight字段或者用augment参数copy_paste0.3来复制少样本实例。更直接的做法是修改损失权重# 在训练前通过ultralytics接口自定义 from ultralytics import YOLO model YOLO(yolov8s.yaml) model.model.loss ... # 自定义loss加权不过大部分时候不需要走到这一步我一般先跑baseline如果少样本类mAP小于0.5再设计一个二次训练冻结backbone用过采样后的少样本数据finetune head。5. 针对小缺陷漏检的调优技巧切片推理与重叠边框后处理训练完的模型仍然可能在密集小缺陷上漏检。这时候可以在推理阶段做SAHI切片推理把每张图像裁剪成512x512的小patch带重叠区域单独检测最后合并结果。对于crazing和pitted_surface这类目标密度极高的场景切片推理往往能比整图推理提升5-10个点mAP而且不需要重新训练。安装并运行pip install sahi yolov8 sahi predict \ --model_type yolov8 \ --model_path runs/detect/train/weights/best.pt \ --model_config steel.yaml \ --source images/val \ --slice_size 512 \ --overlap_ratio 0.2 \ --image_size 640 \ --project runs/sahi参数说明--slice_size是切片边长overlap_ratio是相邻切片的重叠比例。重叠是为了避免缺陷正好被切片边界切开0.2的比值在计算量和召回率之间比较均衡。--image_size是每次输入到模型的尺寸一般和训练时一致但切片推理时每个patch的内容相对简单可以适当调大比如用768。切片推理的核心难点是合并阶段的重复框。如果某一个缺陷跨了两个patch它会被检测两次甚至三次需要使用NMS后处理合并。SAHI默认会做NMS但要注意--postprocess_type的选择默认是GREEDYNMM对密集小目标的效果优于传统NMS。如果合并后发现检测框出现细小偏移可以把overlap_ratio提到0.3代价是推理时间增加约40%。另外还有一个不那么明显的参数--slice_height和--slice_width。在钢材表面缺陷这种长条形图像比如1920x108上方形切片会带来大量无效计算。我一般会先看一眼图片平均宽高比如果宽度远大于高度就设置slice_height512、slice_width768让切片贴合目标分布。最后提醒一件事SAHI merge后的坐标需要映射回原图尺度如果你保存了切片坐标但直接用整图坐标会出现框错位的诡异问题。检查方法很简单在runs/sahi里找切回整图的可视化结果看scratches这类长条形目标是不是完整覆盖。如果出现断成几段就把重叠比调大或者对长条目标做形态学合并而不是单纯加大NMS阈值。本文还有配套的精品资源点击获取
返回列表