ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

钢铁表面缺陷检测图像数据集:1800张带标签图像实战指南

钢铁表面缺陷检测图像数据集:1800张带标签图像实战指南

简介:这份钢铁表面缺陷检测图像数据集面向从事机器视觉与工业质检研究的开发者、学生及算法工程师,用于训练和评估裂纹、锈蚀、凹坑、划痕等表面缺陷的识别模型,可作为监督学习与深度学习实验的样本来源。压缩包为zip格式,共约2000个文件,主体是1800张jpg缺陷图像,并配有1400余个xml标注文件与1400余个txt说明文件,另有少量data、names等配置类文件,整体约26.81MB,体量轻便、便于快速下载与本地部署。目前已有2453人学习下载,具备一定的使用参考基础。图像与标签一一对应,读者可直接据此划分训练集与验证集,开展CNN等模型的训练、超参数调优与精度、召回率、F1分数评估,也能用于数据扩增和预处理流程的对比实验,为钢铁产线自动化缺陷检测方案的搭建提供现成数据支撑。

1. 钢铁表面缺陷检测图像数据集:1800 张带标签图像到底能撑起什么

产线上跑视觉检测的同行大概都遇到过这种局面:算法框架搭好了,推理脚本也跑通了,卡在最后一步——手里没有一批带标注的钢铁表面图像。钢铁表面缺陷检测图像数据集(1800 张图像,含标签)这个标题,说的就是这类小而精的工业质检数据资产:图像来自热轧或冷轧带钢表面,缺陷类型通常覆盖划痕、麻点、夹杂、氧化皮压入、边裂等几类,标签以检测框或分割掩码的形式给出。1800 张不算大,但对一个想快速验证缺陷检测方案、跑通从标注格式转换到模型训练再到指标评估全链路的团队来说,这个量级恰好够用——既不至于训练不动,也不至于一眼过拟合。适合谁?适合做工业视觉落地的算法工程师、想复现缺陷检测 baseline 的学生、以及需要给产线质检做可行性验证的技术负责人。下面按“先看清数据、再跑通流程、最后避开坑”的顺序拆开讲。

2. 先摸清 1800 张钢铁缺陷图像的底细:类别分布与标注格式

拿到一批工业图像,最忌讳上来就train.py一把梭。钢铁表面缺陷有个特点:类间极不均衡,划痕可能几百张,边裂可能只有几十张;而且同一张图上可能同时出现多种缺陷。不先把这些摸清楚,后面训练出来的模型大概率是“只会认划痕”的偏科生。这一章的目标是让你在动手训练前,用几段脚本把数据的类别分布、标注格式、图像尺寸、缺陷面积占比全部量化出来。

2.1 为什么钢铁缺陷数据必须先做分布统计

钢铁表面的缺陷形态和自然图像里的目标检测任务差别很大。自然图像里一只猫占画面比例通常不小,而钢铁表面的麻点可能只有十几个像素,边裂则是细长条。如果直接套用 COCO 预训练模型的默认 anchor 尺寸,小目标召回率会非常难看。更现实的问题是:1800 张图像如果按 8:1:1 划分,验证集只有 180 张,某些稀有类别在验证集里可能一张都没有,这时候算出来的 mAP 是失真的。

我一般会先做三件事:统计每类缺陷的实例数、统计每张图的缺陷数量分布、统计缺陷框的宽高比分布。前两个决定你要不要做重采样或类别加权,第三个决定 anchor 怎么设或者要不要改用无锚框的检测头。这三项统计用一段 Python 就能跑完,不需要任何深度学习框架。

import os import json from collections import Counter, defaultdict # 假设标签是 COCO 格式的 annotations.json with open("annotations.json", "r", encoding="utf-8") as f: coco = json.load(f) # 类别 id -> 名称 cat_map = {c["id"]: c["name"] for c in coco["categories"]} inst_counter = Counter() img_defect_count = defaultdict(int) aspect_ratios = [] for ann in coco["annotations"]: cid = ann["category_id"] inst_counter[cat_map[cid]] += 1 img_defect_count[ann["image_id"]] += 1 x, y, w, h = ann["bbox"] if h > 0: aspect_ratios.append(w / h) print("各类别实例数:", dict(inst_counter)) print("单图缺陷数分布:", Counter(img_defect_count.values())) print("宽高比 min/max:", min(aspect_ratios), max(aspect_ratios))

这段脚本的关键在三个输出。inst_counter直接告诉你哪几类样本少得可怜,少于总实例数 5% 的类别就要考虑过采样或者 focal loss。img_defect_count的分布如果大量集中在 1,说明多数图是单缺陷,模型不容易学到多缺陷共存的特征,训练时可以适当做 mosaic 增强。aspect_ratios的极值如果超过 10 或者低于 0.1,说明存在极端细长目标,默认 anchor 的 1:1、1:2、2:1 三组比例覆盖不住,需要补 1:4、4:1 这类比例。

提示:统计脚本跑完先别急着删,把结果存成 CSV,后面调参时回头对照,能省很多“为什么这轮 mAP 掉了”的排查时间。

2.2 标注格式转换:从 COCO 到 YOLO 的四个边界坑

钢铁缺陷数据集的标签常见两种:COCO 的 JSON 和 Pascal VOC 的 XML。如果你打算用 YOLO 系列做 baseline,就得转成每张图一个 txt、每行class cx cy w h的归一化格式。这个转换看着简单,实际有四个坑会让你的训练直接翻车。

第一个坑是坐标越界。钢铁图像在标注时,标注员可能把框拖到了图像边缘外一两个像素,转成归一化坐标后出现负数或大于 1 的值,YOLO 读取时会直接跳过这个框,等于悄悄丢样本。第二个坑是宽高为 0 的退化框,通常来自标注工具的误操作。第三个坑是类别 id 从 1 开始还是从 0 开始,COCO 习惯从 1 开始,YOLO 要求从 0 开始,差一位就是全部标签错位。第四个坑是图像文件名和标签文件名的对应关系,有些数据集图像叫img_001.jpg,标签却叫001.txt,直接按同名匹配会漏掉一半。

import json import os with open("annotations.json", "r", encoding="utf-8") as f: coco = json.load(f) # COCO 类别 id 可能不连续,重映射成 0 起始 cat_ids = sorted({c["id"] for c in coco["categories"]}) id2idx = {cid: i for i, cid in enumerate(cat_ids)} img_info = {img["id"]: img for img in coco["images"]} os.makedirs("labels", exist_ok=True) skipped = 0 for ann in coco["annotations"]: img = img_info[ann["image_id"]] W, H = img["width"], img["height"] x, y, w, h = ann["bbox"] # 边界裁剪:把框限制在图像内 x1, y1 = max(0, x), max(0, y) x2, y2 = min(W, x + w), min(H, y + h) w, h = x2 - x1, y2 - y1 if w <= 1 or h <= 1: # 退化框直接丢弃并计数 skipped += 1 continue cx, cy = (x1 + w / 2) / W, (y1 + h / 2) / H nw, nh = w / W, h / H cls = id2idx[ann["category_id"]] stem = os.path.splitext(img["file_name"])[0] with open(f"labels/{stem}.txt", "a", encoding="utf-8") as out: out.write(f"{cls} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n") print("丢弃退化框数量:", skipped)

转换逻辑里有几个参数值得说清楚。max(0, x)和min(W, x+w)这一对裁剪是必须的,宁可把框裁小也不能让它越界,因为越界框在 YOLO 的 dataloader 里会被静默过滤,你看到的 loss 曲线正常,但实际参与训练的样本少了。w <= 1 or h <= 1这个阈值不是随便定的,钢铁缺陷里小于 1 像素的框没有检测意义,留着只会污染训练。归一化保留 6 位小数是为了避免累积误差,尤其是小目标框,小数点后少两位可能就差出好几个像素。最后用"a"追加模式写文件,是因为一张图可能有多个缺陷框,每次追加一行,天然支持多标签。

转换完一定要做一次反向校验:随机抽 20 张图,把 txt 里的框画回原图上看位置对不对。这一步花五分钟,能避免训练两天后发现标签全错位的血泪教训。

2.3 图像尺寸与缺陷面积占比:决定输入分辨率的关键数字

钢铁缺陷检测的输入分辨率不能拍脑袋定 640。缺陷面积占比这个指标直接决定你该用多大的输入。如果麻点类缺陷平均只占图像的 0.1%,在 640 输入下经过 backbone 的 32 倍下采样,特征图上就剩不到一个像素,神仙也检不出来。

import json with open("annotations.json", "r", encoding="utf-8") as f: coco = json.load(f) img_info = {img["id"]: img for img in coco["images"]} ratios = [] for ann in coco["annotations"]: img = img_info[ann["image_id"]] area = ann["bbox"][2] * ann["bbox"][3] ratios.append(area / (img["width"] * img["height"])) ratios.sort() n = len(ratios) print("面积占比 P10/P50/P90:", ratios[int(n*0.1)], ratios[int(n*0.5)], ratios[int(n*0.9)])

如果 P10 低于 0.001,我的建议是把输入提到 1024 甚至 1280,或者改用带高分辨率特征的检测头(比如在 stride 8 的特征图上做小目标分支)。如果 P50 在 0.05 以上,640 完全够用,再大只是浪费显存。这个数字还影响数据增强策略:小目标占比高的数据集,随机缩放的下限不能设太低,否则增强完目标直接消失。

3. 用 1800 张图跑通缺陷检测 baseline:训练配置与增强策略

数据摸清楚了,接下来是把它喂给模型。1800 张图做检测训练,属于“小数据”范畴,直接从头训大概率欠拟合或过拟合二选一。这一章讲清楚三件事:为什么选迁移学习而不是从头训、钢铁缺陷场景下哪些增强有用哪些有害、以及一套能复现的训练配置。

3.1 小样本钢铁缺陷检测为什么必须走迁移学习

1800 张图,按类别分下来每类可能就两三百个实例。检测模型 backbone 动辄几千万参数,从头训需要的数据量是万级起步。迁移学习的逻辑是:backbone 在自然图像上学到的边缘、纹理、角点特征,在钢铁表面同样存在——划痕是边缘,麻点是局部纹理突变,氧化皮是区域纹理。所以加载在通用数据上预训练的权重,只微调检测头加部分 backbone 层,收敛速度和最终精度都会好很多。

具体怎么冻结有讲究。我一般分三段:backbone 的前 2/3 冻结,后 1/3 用较小学习率(base_lr 的 0.1 倍)微调,检测头用正常学习率。原因是浅层特征太通用,钢铁数据量不足以撼动它们,强行训反而破坏;深层特征和任务相关,必须放开。如果显存紧张,也可以只训检测头,但 mAP 通常会低 3 到 5 个点,属于用精度换资源的取舍。

# 以常见检测框架的配置思路示意,非特定框架 API model = build_detector(num_classes=len(cat_ids), pretrained=True) # 分段设置学习率:backbone 浅层冻结,深层小学习率,head 正常 for name, param in model.backbone.named_parameters(): if "stage1" in name or "stage2" in name: param.requires_grad = False else: param.requires_grad = True optimizer = build_optimizer([ {"params": model.backbone.parameters(), "lr": 1e-4}, # 深层微调 {"params": model.head.parameters(), "lr": 1e-3}, # 检测头 ])

这里1e-4和1e-3的十倍关系不是玄学,是经验值:深层特征已经比较接近任务,学习率大了容易把预训练权重冲垮;检测头是随机初始化的,需要正常学习率快速收敛。如果你的框架不支持分组学习率,退而求其次用全局1e-4加 cosine 衰减也能跑,只是前期收敛慢一些。

3.2 钢铁缺陷场景下哪些数据增强真有用

数据增强在工业质检里是个双刃剑。自然图像那套随机裁剪、大角度旋转、颜色抖动,搬到钢铁表面可能直接制造出“不存在的缺陷”或者“抹掉真实缺陷”。我按有用程度排个序。

第一梯队是水平翻转和垂直翻转。钢铁表面的划痕、边裂没有方向语义,翻转后仍然是合理的缺陷形态,等于把数据量翻倍,几乎零风险。第二梯队是亮度对比度微调,幅度控制在 ±15% 以内,模拟产线光照波动,这个很实用,因为实际部署时打光不可能和训练集完全一致。第三梯队是小角度旋转,±10 度以内,模拟相机安装偏差,超过这个角度缺陷形态就开始失真。

要慎用的是随机裁剪和 mosaic。随机裁剪可能把边裂这种贴边缺陷裁掉一半,标签框跟着变形,模型学到的是残缺特征。Mosaic 把四张图拼一起,对小目标检测有好处,但钢铁缺陷里如果缺陷本身就在图像边缘,拼接后边界处的缺陷会被切断,产生大量噪声标签。我的做法是 mosaic 只在训练前 80% 的 epoch 开,最后 20% 关掉,让模型在干净数据上收尾。

# 增强配置示意:翻转必开,亮度小幅,旋转小角度,mosaic 后期关闭 train_pipeline = [ dict(type="RandomFlip", direction="horizontal", prob=0.5), dict(type="RandomFlip", direction="vertical", prob=0.5), dict(type="RandomBrightnessContrast", brightness_limit=0.15, contrast_limit=0.15, prob=0.5), dict(type="RandomRotate", max_angle=10, prob=0.3), dict(type="Mosaic", prob=0.8, close_epoch_ratio=0.8), # 后 20% 关闭 ]

close_epoch_ratio=0.8这个参数的意思是训练进度到 80% 后自动关掉 mosaic,不同框架写法不同,核心思想是“前期靠增强提泛化,后期靠干净数据收精度”。prob值也别全设 1.0,翻转 0.5 是为了保留一半原始朝向的样本,避免模型对翻转产生依赖。

3.3 一套可复现的训练超参配置

把上面这些拼起来,给一套我实际用过、在 1800 张量级上比较稳的配置。输入 1024×1024(依据 2.3 的面积占比统计调整),batch size 8(单卡 24G 显存下的安全值),初始学习率 1e-3 配 cosine 衰减,warmup 500 iter,总 epoch 100,优化器用 SGD momentum 0.9。类别不均衡用类别加权交叉熵,权重按实例数的倒数归一化。

参数取值说明
输入分辨率1024×1024小目标占比高时上调
batch size824G 显存安全值
初始学习率1e-3检测头;backbone 深层 1e-4
学习率策略cosine + warmup 500 iter小数据防早期震荡
总 epoch100配合 mosaic 后 20% 关闭
类别权重实例数倒数归一化缓解长尾
NMS IoU 阈值0.5钢铁缺陷重叠少,可略低

训练过程中重点盯两个信号:一是分类 loss 和回归 loss 的比例,如果分类 loss 远大于回归 loss,说明类别不均衡严重,加权系数要调;二是验证集上小目标类别的 AP,如果它一直不涨而大目标涨得快,回头检查输入分辨率和 anchor 配置。100 个 epoch 在单卡上大概几小时,跑完先看整体 mAP,再看每类 AP,偏科严重的类别单独分析。

4. 钢铁缺陷检测的避坑清单:五条踩过的真实教训

这一章不讲新东西,专门讲翻车。下面五条都是我在类似量级的钢铁缺陷数据上真实遇到过的,每条按“现象、原因、解决”写,你对照自己的训练日志看,大概率能提前避开。

4.1 训练 loss 正常但验证 mAP 极低

现象:训练集 loss 稳定下降,验证集 mAP 卡在 0.1 左右不动,模型在验证图上几乎框不出东西。原因八成是标签坐标系搞错了。钢铁数据集的标注有时用左上角原点,有时用左下角原点(某些标注工具导出习惯),转换脚本没做 y 轴翻转,训练时模型学的是上下颠倒的框,验证时自然全错。解决:抽一张图把标签框画上去,肉眼确认框是否套在缺陷上,这一步比看任何指标都直接。

4.2 稀有类别 AP 始终为 0

现象:划痕类 AP 能到 0.6,边裂类一直是 0,验证集里明明有边裂样本。原因通常是验证集里该类实例太少,按 8:1:1 随机划分时边裂全被分进了训练集。解决:改用分层划分,保证每个类别在训练、验证、测试集里都有最低数量(比如每类验证集至少 10 个实例),稀有类别可以适当提高验证集比例。这个改动看着小,但对指标可信度影响极大。

4.3 模型在产线新图上大面积误检

现象:验证集指标不错,换一批新拍的钢铁图像,背景里的氧化皮纹理被大量误检成麻点。原因是训练集和测试集的拍摄条件不一致,训练集可能来自单一产线、单一光照,模型把某种背景纹理当成了缺陷的判别特征。解决:训练时加强亮度对比度增强,同时在负样本里显式加入“无缺陷但纹理复杂”的背景图,让模型学会区分纹理和缺陷。如果条件允许,新数据先做一轮小样本微调再上线。

4.4 转换后的标签文件数量对不上图像数量

现象:图像 1800 张,转出来的 txt 只有 1200 个,训练时大量样本没有标签被当成背景。原因是文件名匹配逻辑写死了后缀,图像是.jpg而标签生成时用了.png的 stem,或者图像有子目录而标签全平铺在一层。解决:转换脚本里统一用os.path.splitext取 stem,并且转换后做一次集合比对,打印出“有图无标签”和“有标签无图”的清单,数量不为零就不往下走。

4.5 增大输入分辨率后显存爆了但精度没涨

现象:按小目标统计把输入从 640 提到 1280,显存直接 OOM,降 batch size 勉强跑起来,mAP 只涨了不到 1 个点。原因是单纯提分辨率,小目标在特征图上的绝对像素确实多了,但如果 anchor 尺寸没跟着调,匹配上的正样本反而变少。解决:提分辨率的同时重算 anchor,用 2.1 里统计的宽高比分布做 k-means 聚类生成新 anchor;或者干脆换无锚框检测头,省掉这层调参。另外提分辨率前先确认小目标类别的 AP 是不是真的低,如果低的是大目标类别,提分辨率纯属浪费。

5. 从 1800 张到可交付模型:验证方法与一个提点技巧

数据跑通、坑也避了,最后一章讲怎么判断这个模型到底能不能交付,以及一个在钢铁缺陷场景下实测有效的提点技巧。工业质检和学术刷榜最大的区别是:mAP 高不代表能用,漏检率和误检率在产线语境下比 mAP 更有说服力。

5.1 用漏检率和误检率替代 mAP 做交付判断

mAP 是 IoU 阈值下的平均精度,它把定位质量和分类质量混在一起。产线关心的是两件事:真缺陷有没有漏掉(漏检率),好产品有没有被误判(误检率)。这两个指标要单独算,而且阈值要按业务定。

# 按置信度阈值统计漏检与误检,需先跑推理拿到预测结果 def eval_miss_false(preds, gts, conf_thr=0.3, iou_thr=0.5): miss, false_pos, total_gt = 0, 0, 0 for img_id, gt_boxes in gts.items(): total_gt += len(gt_boxes) pred_boxes = [p for p in preds.get(img_id, []) if p["score"] >= conf_thr] matched = set() for pb in pred_boxes: hit = False for i, gb in enumerate(gt_boxes): if i in matched: continue if iou(pb["box"], gb) >= iou_thr: matched.add(i); hit = True; break if not hit: false_pos += 1 # 预测了但没有对应真值 miss += len(gt_boxes) - len(matched) return miss / total_gt, false_pos / max(1, len(preds))

conf_thr是业务阈值,产线通常宁可误检不可漏检,所以这个值会设得偏低(0.2 到 0.3),代价是误检率上升,需要后处理或人工复检兜底。iou_thr在钢铁缺陷里可以设 0.5,因为缺陷框通常比较规整,不像自然图像里遮挡严重。算出来的漏检率如果超过 5%,这个模型就不能直接上线,得回去补数据或者调阈值。

5.2 一个提点技巧:难例挖掘加针对性补标

1800 张的量级,模型能力上限很大程度被数据卡住。与其反复调参榨那 1 个点,不如做一轮难例挖掘。具体做法:用当前模型在验证集和一批未标注的新图上跑推理,把漏检的、误检置信度高的图挑出来,人工复核后补标或修正标签,再混进训练集重训。这一轮下来,通常能带来 3 到 5 个点的实际提升,比换 backbone 划算得多。

我一般会按“漏检优先、误检次之”的顺序挑图,每次补 100 到 200 张,重训一轮看指标,有效就继续,两轮没动静就说明数据分布已经覆盖住了,该考虑模型结构了。这个循环里最容易被忽略的是修正错误标签——有些“漏检”其实是标注漏了,模型检出来是对的,这种样本补上标签比任何增强都值钱。

说个我自己的习惯:每次重训前把上一版的漏检清单存一份,新版本跑完对比,看漏的是不是同一批图。如果是,说明这类缺陷模型根本没学会,得从数据侧找原因;如果不是,说明模型在泛化,继续补数据就行。这个对比习惯帮我省过好几次盲目调参的时间。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表