简介:钢材表面缺陷数据集NEU-DET是面向计算机视觉与机器学习研究者的专业资源,旨在支持钢材表面缺陷自动检测算法的训练与评估,适用于工业质检场景中的缺陷识别与定位任务。压缩包内共两千个文件,以标注文件、图像文件和标签文件为主体,另附格式转换脚本与配置文件,整体大小约二十七兆字节,可方便切换至主流目标检测框架进行训练。数据集图像源自真实钢材表面,涵盖夹杂、斑块、裂纹、划痕等多种典型缺陷类别,每张图像均带有精确的位置与类别标注,为监督学习提供完备样本基础,同时也有助于检验模型在复杂背景下的鲁棒性。目前已有四千九百五十二人浏览学习,获取后可快速得到整理好的标注数据集、格式转换工具及示例配置,省去大量数据预处理时间,使研究者能够专注于模型结构设计、参数调优与效果验证,加速工业智能质检项目的算法落地。1. NEU-DET:钢材表面缺陷检测绕不开的 1800 张图,先看清它到底值不值NEU-DET 是钢材表面缺陷检测方向被引用最多的公开数据集之一,某高校团队对热轧带钢表面缺陷做了分类与检测两套标注,检测版包含 1800 张灰度图像、6 类缺陷、每类 300 张。它解决的问题很具体:你没有产线数据时,需要一个能快速验证检测算法、能横向对比框架差距的基准;你在写工业质检方案前,也需要搞清楚“小样本加强形态差异”的数据到底会把模型带向哪里。它适合三类人:工业视觉算法工程师、做目标检测起步课题的研究生、想验证自己训练流程是否正确的从业者。反直觉的一点是:这个数据集分辨率只有 200×200,看起来不像能喂饱深度模型,但在合理配置下 mAP50 完全可以跑到 0.7 以上;而翻车的人几乎都栽在同一个地方——直接把通用检测框架的默认参数套在这么小的图上。2. 数据集构成与标签格式:六类缺陷、VOC 标注,动手前先对齐边界框写法2.1 六类缺陷怎么区分、哪个最难NEU-DET 的六类缺陷在视觉上差异很大,先看一眼它们的共性和差异,比直接开训更重要。常见缩写下,六类分别是 Crazing(Cr,裂纹/龟裂)、Inclusion(In,夹杂物)、Patches(Pa,斑块)、Pitted Surface(PS,麻点)、Rolled-in Scale(RS,氧化铁皮)、Scratches(Sc,划痕)。每类 300 张,但“每类一样多”不等于“每类一样好学”,因为标注框形态差别极大。类别中文通称视觉特征标注框形态特点Crazing裂纹/龟裂表面网状或放射状细线,灰度低、对比弱框极窄长,往往贯穿整图Inclusion夹杂物片状或条形暗块,边缘模糊框中等,矩形为主Patches斑块大面积灰度不均区域,边界很不规则框大而碎,背景占比高Pitted Surface麻点密集小凹坑,点状分布一个框内可能叠着多个小目标Rolled-in Scale氧化铁皮条带状暗块,沿轧制方向框窄长,方向相对固定Scratches划痕细长亮线或暗线,方向随机长宽比极端,形态多变这张表里最值得关注的是 Cr 和 Sc:它们的长宽比可能到 10:1 甚至更高,默认目标检测 head 里的 anchor 几乎覆盖不到。Pa 和 RS 的问题则是框内背景太多,模型容易把“灰度不均的区域”误学成“整块背景”。PS 的麻烦在于一个框里塞了多个小点,NMS 时容易互相抑制。所以后面做 anchor 调整和增强策略时,不是拍脑袋,是根据这张表来定。2.2 标签文件逐行拆解:XML 里每个字段在检测时做什么用NEU-DET 的检测标注采用 PASCAL VOC 风格,也就是每张图对应一个 XML 文件。解压镜像后你通常能看到 Images 和 Annotations 两个目录,XML 文件名和图片名一致。下面是一份示意标注内容,字段结构与真实样本一致:<annotation> <filename>IMG_0167.jpg</filename> <size> <width>200</width> <height>200</height> <depth>1</depth> </size> <object> <name>Scratches</name> <bndbox> <xmin>18</xmin> <ymin>26</ymin> <xmax>186</xmax> <ymax>196</ymax> </bndbox> </object> </annotation>这里几个字段在训练时各有用途。filename用于和图片对应;size里的 width 和 height 是做坐标归一化的分母,很多转换脚本写错就在这一步;depth是 1,表示灰度图,如果你的训练框架默认读三通道,通常会复制成三通道,这不影响训练但会浪费一点显存。object可以出现多次,表示一张图有多个缺陷框。name是类别字符串,bndbox是左上角和右下角坐标,单位是像素。需要留一个心眼:有些镜像里 XML 文件名和 JPG 文件名大小写不一致,有些样本存在 xmin 等于 xmax 的退化框。前者会导致转换后标签文件缺失,后者会导致宽高为 0,训练时被当成非法目标。我的习惯是拿到数据先做一次字段普查,用脚本把所有 XML 解析一遍,确认几张关键样本没问题再往下走。2.3 统计缺陷分布:用脚本算出框面积占比与长宽比,再决定 anchor 策略不要凭感觉判断哪类缺陷“难”,用数据说话。下面这个脚本会遍历全部 XML,输出每个类别的框数量、平均框面积占比和平均宽高比,这些数值直接决定你后面要不要改 anchor、要不要上多尺度训练。import os import xml.etree.ElementTree as ET ann_dir = "path/to/Annotations" # 改成你的 XML 目录 cnt = {} boxes = {} for fn in os.listdir(ann_dir): if not fn.endswith(".xml"): continue root = ET.parse(os.path.join(ann_dir, fn)).getroot() size = root.find("size") W, H = int(size.find("width").text), int(size.find("height").text) for obj in root.iter("object"): name = obj.find("name").text b = obj.find("bndbox") x1, y1 = int(b.find("xmin").text), int(b.find("ymin").text) x2, y2 = int(b.find("xmax").text), int(b.find("ymax").text) w, h = max(1, x2 - x1), max(1, y2 - y1) area_ratio = (w / W) * (h / H) # 框面积占整图面积比例 wh_ratio = (w / W) / (h / H) # 宽高比 cnt[name] = cnt.get(name, 0) + 1 boxes.setdefault(name, []).append((area_ratio, wh_ratio)) for name in cnt: area_list = [x[0] for x in boxes[name]] ratio_list = [x[1] for x in boxes[name]] print(f"{name}: count={cnt[name]}, " f"mean_area={sum(area_list)/len(area_list):.3f}, " f"mean_wh_ratio={sum(ratio_list)/len(ratio_list):.2f}")脚本逻辑很简单,但输出很值钱。mean_area告诉你缺陷在整张图里占多大比例,比如 Pa 可能到 0.3 以上,Cr 可能只有 0.02 到 0.05;mean_wh_ratio告诉你的目标有多“扁”,数值越大越接近细线。把这两列打印出来后,anchor 怎么改就不是猜的了:如果 Cr 和 Sc 的平均宽高比超过 8,默认的 COCO anchor 几乎不会和它们匹配上,你需要手工聚类 anchor 或者调整匹配阈值。这个脚本在任何带 VOC 标注的检测数据上都通用,建议直接收进自己的工具箱。3. 跑通 YOLOv5 的最小闭环:目录划分、格式转换与训练参数3.1 先把 1800 张图按类别分层拆成训练集和验证集拿到数据后的第一个实操动作是划分数据集。常见做法是按 8:2 分成训练集和验证集,但 NEU-DET 的每类只有 300 张,如果完全随机切,某个类别在验证集里可能只剩十几张,mAP 波动会非常大。正确做法是按类别分层采样,让每类缺陷在训练集和验证集中都保持 8:2。cd NEU-DET mkdir -p images/train images/val labels/train labels/val然后运行下面的分层划分脚本。注意脚本里固定了随机种子,这是可复现实验的前提。import os import random import xml.etree.ElementTree as ET random.seed(42) # 固定种子,保证每次划分结果一致 src_img = "Images" # 原始图片目录 src_ann = "Annotations" # XML 目录 img_dir = "images" val_ratio = 0.2 def get_first_class(ann_path): """取一张图标注里出现的第一个类别,用于分层""" root = ET.parse(ann_path).getroot() for obj in root.iter("object"): return obj.find("name").text return None all_files = [f[:-4] for f in os.listdir(src_img) if f.endswith(".jpg")] # 注意:有的镜像图片是 .png 或 .bmp,按实际后缀改这里 class_groups = {} for name in all_files: cls = get_first_class(os.path.join(src_ann, name + ".xml")) if cls is None: continue class_groups.setdefault(cls, []).append(name) train_files, val_files = [], [] for cls, names in class_groups.items(): random.shuffle(names) n_val = int(len(names) * val_ratio) val_files.extend(names[:n_val]) train_files.extend(names[n_val:]) random.shuffle(train_files) random.shuffle(val_files) for name in train_files: os.rename(os.path.join(src_img, name + ".jpg"), os.path.join(img_dir, "train", name + ".jpg")) for name in val_files: os.rename(os.path.join(src_img, name + ".jpg"), os.path.join(img_dir, "val", name + ".jpg")) print(f"train={len(train_files)}, val={len(val_files)}")这段脚本用get_first_class读取每张图的第一个类别做分组,原因是 NEU-DET 的单张图片基本对应单一缺陷类别。如果你后续要做多标签混合,验证集也要保持各类别比例一致,那就得改成按所有类别统计。脚本里的os.rename是移动文件,建议第一次跑之前先复制而不是移动,免得路径写错把原始数据搞乱。3.2 把 VOC 格式转成 YOLO 格式:转换脚本与坐标归一化细节YOLO 系框架不读 VOC XML,它要求每个图片对应一个 txt,每行是类别索引 中心点x 中心点y 宽 高,四个坐标全部归一化到 0 到 1。这个转换是整个流程里最容易出错的环节,坐标格式错了,训练出来的模型会“看起来能跑但框全偏”。import os import xml.etree.ElementTree as ET # 类别顺序必须和后续 neu.yaml 里的 names 完全一致 CLASSES = ["Crazing", "Inclusion", "Patches", "Pitted Surface", "Rolled-in Scale", "Scratches"] def voc_to_yolo(ann_path, out_path): root = ET.parse(ann_path).getroot() size = root.find("size") W, H = int(size.find("width").text), int(size.find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in CLASSES: continue cls_id = CLASSES.index(name) b = obj.find("bndbox") x1, y1 = int(b.find("xmin").text), int(b.find("ymin").text) x2, y2 = int(b.find("xmax").text), int(b.find("ymax").text) # 关键点:中心点和宽高全部除以图片宽高,得到 0~1 之间的数 cx = (x1 + x2) / 2 / W cy = (y1 + y2) / 2 / H w = abs(x2 - x1) / W h = abs(y2 - y1) / H # 过滤掉宽度或高度为 0 的退化框 if w <= 0 or h <= 0: print(f"skip bad box in {ann_path}") continue lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines)) ann_dir = "Annotations" out_dir = "labels" os.makedirs(out_dir, exist_ok=True) for fn in os.listdir(ann_dir): if not fn.endswith(".xml"): continue out_txt = os.path.join(out_dir, fn.replace(".xml", ".txt")) voc_to_yolo(os.path.join(ann_dir, fn), out_txt)转换脚本里两个地方需要重点核对。第一,CLASSES的顺序决定了类别索引,它必须和训练配置里的names顺序保持一致,顺序一旦错位,模型训练时会把所有类别标签错位学习;第二,末尾的w <= 0 or h <= 0过滤不能省,NEU-DET 镜像里偶尔会出现 xmin 等于 xmax 的坏标注,不过滤的话训练日志里会报一堆 warning,甚至影响 loss 计算。转换完必须做可视化抽查。不要直接开训,先随机挑 10 张图把 txt 里的框画回原图看一眼:import cv2, glob for txt in glob.glob("labels/train/*.txt")[:10]: name = txt.split("/")[-1][:-4] img = cv2.imread(f"images/train/{name}.jpg") H, W = img.shape[:2] with open(txt) as f: for line in f: c, cx, cy, w, h = map(float, line.split()) x1 = int((cx - w / 2) * W) y1 = int((cy - h / 2) * H) x2 = int((cx + w / 2) * W) y2 = int((cy + h / 2) * H) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(f"check_{name}.jpg", img)框和缺陷轮廓贴合,说明转换正确;框整体偏移、明显放大或缩小,基本就是归一化或坐标换算写错了。这一步的“肉眼验证”比任何日志都管用,也是我后来在所有数据集上都保留的习惯。提示:YOLO 标签的第一列是整数类别索引,后四列全部是 0~1 的小数。手工检查时可以做一个断言:np.loadtxt(txt)[:, 1:].min() >= 0。3.3 最小训练命令与参数取舍:为什么是 640、16、300数据和标签都就位后,训练命令很简单。以某个常见框架为例,最小闭环是这样的:python train.py \ --data neu.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 300 \ --freeze 10 \ --seed 20240608配套的neu.yaml长这样:path: ./NEU-DET train: images/train val: images/val nc: 6 names: ['Crazing', 'Inclusion', 'Patches', 'Pitted Surface', 'Rolled-in Scale', 'Scratches']几个参数的选择理由值得展开。--img 640不是随便给的:NEU-DET 原图只有 200×200,直接训会损失大量细节,放大到 640 相当于把裂纹、划痕这种细线结构在像素上拉长,模型才有机会学到它们的连续性;显存不够就降到 416,但别低于 416。--batch 16对 1800 张图来说足够让 BN 统计量稳定,单卡 8G 也能跑。--epochs 300是因为小数据集收敛慢,尤其是 Cr、Sc 这类难学样本,150 轮之前主要是在学通用特征,后 150 轮才是精细调框。参数建议值理由img640原图 200×200 信息太稀,放大后细线缺陷可学习batch161800 张图,batch 太小 BN 抖动明显epochs300小数据集需要更长训练周期,mAP 后期仍在缓慢上升freeze10前 10 轮冻结 backbone,防止迁移初期梯度震荡seed固定值保证数据划分、增强和初始化可复现训练过程里重点看两个指标:mAP50 和 mAP50:95。NEU-DET 在 YOLOv5s 这个量级下,mAP50 跑到 0.75 到 0.85 之间是比较常见的水平,mAP50:95 通常只有 mAP50 的一半左右。如果你的 mAP50 在 0.5 以下,大概率不是框架问题,而是数据划分、标签转换或参数设置里的某一步出了问题,往下看排查章节。4. 数据搭配陷阱:类别不均衡、小图增强边界与验证集不可复现4.1 每类 300 张并不平衡:框形态差异带来的“隐性不均衡”NEU-DET 从数据量上看每类都是 300 张,非常“均衡”,但实际训练时会发现某些类别的 loss 降不下去。问题不在数量,在框形态。用前面统计脚本的输出再往深想一层:Cr 和 Sc 的框长宽比极端,一个框面积可能只占整图的 1% 到 3%,它们在特征图上的响应本身就弱;Pa 和 RS 的框大,但边界模糊,模型很难确定“缺陷到哪里结束”;PS 的框内包含多个小目标,一个框算一个正样本的匹配方式会让模型对框内位置不敏感。这种结构性不均衡带来的直接后果是:默认 anchor 策略下,Cr 类的正样本数量远少于 Pa 类。模型学会了大目标的回归,却很少有机会修正细长框,最后表现就是 Cr 和 Sc 的 mAP 长期压在 0.3 以下。处理方案有两个方向:一是调整 anchor,针对窄长框增加高宽比大的候选;二是调整输入分辨率,把 640 改成 1280 跑一遍对比,往往能直接看到 Cr 类 mAP 回升。很多人觉得放大输入是“作弊”,但在工业缺陷检测场景里,输入分辨率本身就是可以调的超参数。4.2 增强策略的边界:mosaic 在这份数据上的副作用与替换方案通用检测框架默认开的 mosaic 增强,在 NEU-DET 这种小图上是有副作用的。mosaic 会把 4 张图拼接成一张再随机裁剪缩放,本来 200×200 的图被裁切后,缺陷尺寸被进一步缩小。如果原图里一个裂纹框只有 10 个像素宽,mosaic 之后再缩一半,这个目标在特征图上可能只剩 1 到 2 个像素,正样本匹配直接失败。常见做法是:训练前 250 轮开 mosaic 增强模型的泛化能力,最后 50 轮把 mosaic 关掉,让模型在接近真实尺度的输入上收敛。# hyp.yaml 片段 mosaic: 1.0 # 前 250 轮保持,最后 50 轮降为 0 copy_paste: 0.2 # 小目标增强更温和,适合这种细线缺陷 rotate: 10 # 不要超过 20,热轧缺陷方向有物理约束 shear: 5 hsv_h: 0.01 hsv_s: 0.4rotate和shear要克制。钢材表面的裂纹、划痕方向受轧制工艺影响,有物理约束;你要是把图旋转 45 度,等于人为制造了一批现实里不存在的缺陷形态,模型会把特征空间学歪,推理时遇到正常形态反而漏检。copy_paste是把一个样本里的目标复制到另一张图上,它对小目标更友好,比 mosaic 适合这份数据。注意:任何增强参数改完后,都要可视化几张增强结果确认缺陷没被扭曲得不像样。增强是数据生成的“黑匣子”,肉眼过一遍比看曲线有用。4.3 固定划分、分层采样:让实验能复现的三点做法小数据集的另一个坑是“验证集不可复现”。有人训练时每次都在代码里随机划分数据集,结果同一份代码、同一个参数,三次实验的 mAP 差了 5 个点,还以为是模型随机性,实际上只是某类缺陷在验证集里的数量变了。我在这类数据上的习惯是三条。第一,划分脚本单独跑一次,把划分结果存成固定的train.txt和val.txt,后续所有实验只读这两个文件,不再重新划分。第二,所有涉及随机的地方(划分、训练增强、anchor 聚类)统一固定种子。第三,划分时按类别分层,确保每类缺陷在验证集里都有足够样本,而不是让验证集被某类大目标主导。# 读取固定划分,不重新生成 with open("train.txt") as f: train_files = [x.strip() for x in f if x.strip()] with open("val.txt") as f: val_files = [x.strip() for x in f if x.strip()]这套做法看起来笨,但能节省大量排错时间。否则你在对比“加了这个模块有没有效果”时,根本分不清收益来自代码改动还是数据划分的小波动。数据划分本身成了变量,实验就失去了意义。5. NEU-DET 实战常见问题排查:五条踩坑记录与解决路径5.1 细长缺陷类 mAP 上不去:Cr 和 Sc 一直压在 0.3 以下现象:训练 300 轮后,In、Pa、PS、RS 的 mAP 都在 0.7 以上,唯独 Cr 和 Sc 只有 0.3 左右,验证集上大量漏检。原因:这两类缺陷框长宽比极端,默认 anchor 的宽高比通常覆盖 0.5 到 2 的范围,对 10:1 以上的框完全匹配不上。匹配不上就没有正样本,没有正样本模型就学不到这类缺陷。解决:先把 anchor 改成“窄长优先”。常见做法是关掉框架的自动 anchor 计算,用自己的聚类脚本重新生成锚框,突出高宽比大于 5 的候选;同时可以把anchor_t阈值放宽,让更多窄长框进入正样本匹配范围。我当时把输入从 640 提到 1280 + 聚类 anchor 一起做,Cr 的 mAP 从 0.3 直接提到 0.6,效果非常明显。改完 anchor 后一定要重新验证框匹配结果,不能只看训练 loss。5.2 推理时预测框整体偏移:训练正常但框明显“错位”现象:训练日志里 loss 正常下降,验证集 mAP 也还行,但把模型部署到单张图上推理时,预测框比实际缺陷的位置整体向左上或右下偏移半个身位。原因:我用过的最隐蔽的原因是标签转换时坐标归一化基数写错。有些脚本把 xmin、xmax 除以了宽高比而不是图片本身的宽高,或者用了整数除法,导致所有框坐标被系统性缩放。解决:先画框回原图,确认偏移方向是否一致。如果所有框都朝一个方向偏移,问题基本在归一化;如果偏移随机,问题可能出在数据增强流程没同步更新坐标。验证命令很简单:python - <<'EOF' import numpy as np, glob for txt in glob.glob("labels/train/*.txt")[:20]: arr = np.loadtxt(txt) if arr.ndim == 1: arr = arr.reshape(1, -1) print(txt, "min=%.3f max=%.3f" % (arr[:, 1:].min(), arr[:, 1:].max())) EOF正常情况后四列的 min 应该大于等于 0、max 小于等于 1。一旦发现 max 超过 1.5 甚至几十,直接查转换脚本里的分母。这类问题越早发现成本越低,训练几小时后再发现基本只能重来。5.3 开着 mosaic 训练卡在一个偏低的上限:mAP 停在 0.5 上不去现象:训练到 200 轮左右 mAP50 稳定在 0.5,再用实验性的参数调整也拉不上去,看起来像模型容量到顶。原因:NEU-DET 原图 200×200,mosaic 会把图缩小到原来的一半甚至四分之一,细小缺陷在增强后的图片上直接“消失”。模型在训练时见过大量无效正样本,收敛上限被拉低。解决:最后 50 到 80 轮关闭 mosaic,只保留 copy_paste、轻微旋转和尺度抖动,让模型在接近真实输入分布的尺寸上微调。如果你的框架支持写死增强参数,分两阶段训练即可:前 250 轮用默认增强,后 50 轮把mosaic: 0重新跑一遍。这样改动后 mAP 普遍能回升 5 到 10 个点,属于小数据集上性价比很高的操作。5.4 加载预训练权重后前期 loss 剧烈震荡:前 20 轮 loss 乱跳现象:用了 COCO 预训练权重,结果前 20 轮 box_loss 和 cls_loss 大幅度震荡,验证集 mAP 甚至掉到 0,到 40 轮才缓慢恢复。原因:COCO 数据以中大目标为主,预训练特征和 NEU-DET 的细长小目标尺度差异太大。加载权重后,最后一层检测头的参数还没适应新数据,前几轮梯度噪声很大,BN 统计量也在重新估计。解决:前 10 到 15 轮冻结 backbone,只训练检测头,等 loss 稳定后再解冻全部参数。命令里加--freeze 10即可;如果震荡依然严重,把初始学习率从 0.01 降到 0.003,同时把 warmup 轮数加长。冻结阶段结束后 loss 会出现一次小跳变,那是解冻的正常反应,不用慌。5.5 换了一个方向就漏检:增强角度太大,模型把特征学歪了现象:验证集 mAP 正常,但对着现场拍回来的热轧带钢图推理,同一个缺陷换了个方向就漏检,尤其是划痕。原因:训练时增强参数里rotate设成了 30 甚至 45。NEU-DET 的缺陷方向有物理约束,划痕大多沿轧制方向分布;大幅旋转产生的训练样本在现实里不存在,模型花了不少参数去拟合这些“假样本”,对真实方向的敏感度反而下降。解决:把旋转限制在 ±10 度以内,关闭或弱化透视畸变,增加 hsv 亮度扰动来模拟光照变化。热轧带钢本身表面反光差异大,亮度扰动比角度扰动更符合真实场景。改完增强后重新训练,推理时对方向变化的鲁棒性会明显改善。这个问题的本质是:增强的目的是扩充分布,而不是创造现实里不存在的分布。6. 把 1800 张图用出更大数据量的效果:三点进阶实践6.1 图像分块让细小缺陷从“看不见”变“看得见”NeU-DET 的 Cr 和 Sc 在 200×200 原图上可能只有几个像素宽,即使放大到 640 输入,仍不足以让模型捕捉完整纹理。我常用的做法是分块推理:把输入图切成 2×2 或 4×4 的小块,每块独立检测后再按原坐标合并,最后做一次 NMS。def split_infer(img, model, tile=320, overlap=32): h, w = img.shape[:2] boxes, step = [], tile - overlap for y in range(0, h, step): for x in range(0, w, step): crop = img[y:y+tile, x:x+tile] det = model(crop)[0] for x1, y1, x2, y2, conf, cls in det: boxes.append([x1 + x, y1 + y, x2 + x, y2 + y, conf, cls]) return boxes # 合并后按置信度排序再做 NMS分块后细线缺陷在每一块里占据的比例变大,检测器更容易命中。代价是推理时间翻倍,但对工业离线抽检场景通常可接受。配合前面说的 anchor 调整,Cr 和 Sc 的漏检能再降一截。6.2 伪标签自训练:把没标注的带钢图变成训练集NEU-DET 本身只有 1800 张标注图,但实际产线或公开渠道能拿到大量未标注的热轧带钢表面图。一个可靠做法是用训练好的模型给这些无标注图做推理,置信度超过 0.85 的框整理成伪标签,加入训练集再训一轮。这份数据里值得特别小心的是类别平衡:伪标签天然偏向模型已经学好的类,所以加样本时要按类别抽样,甚至只挑 Cr、Sc 这种弱类来补,效果更直接。6.3 测试时增强:不训练也能换回一点召回率如果你已经训完一版模型,还想榨出一点点性能,测试时增强是最快的路径。做法是推理时将输入图做水平翻转和 0.8、1.0、1.25 三种尺度缩放,分别推理后把结果映射回原图坐标,再做 NMS 融合。水平翻转对 Sc 这种方向随机的缺陷提升最明显,多尺度则对尺度变化有鲁棒性。代价是推理耗时增加 3 到 5 倍,但有些场景下 mAP 能涨 1 到 2 个点。我自己的收尾习惯是:训练之前永远先做两步可视化——第一步画原始标注框,第二步画增强后的样本。这两步做完了,后面训练其实很少翻车。这不算什么高超技巧,但很多“玄学掉点”的深夜排查,追根溯源都发生在没看这两张图的时候。希望帮到你。本文还有配套的精品资源,点击获取
简介:钢材表面缺陷数据集NEU-DET是面向计算机视觉与机器学习研究者的专业资源,旨在支持钢材表面缺陷自动检测算法的训练与评估,适用于工业质检场景中的缺陷识别与定位任务。压缩包内共两千个文件,以标注文件、图像文件和标签文件为主体,另附格式转换脚本与配置文件,整体大小约二十七兆字节,可方便切换至主流目标检测框架进行训练。数据集图像源自真实钢材表面,涵盖夹杂、斑块、裂纹、划痕等多种典型缺陷类别,每张图像均带有精确的位置与类别标注,为监督学习提供完备样本基础,同时也有助于检验模型在复杂背景下的鲁棒性。目前已有四千九百五十二人浏览学习,获取后可快速得到整理好的标注数据集、格式转换工具及示例配置,省去大量数据预处理时间,使研究者能够专注于模型结构设计、参数调优与效果验证,加速工业智能质检项目的算法落地。1. NEU-DET:钢材表面缺陷检测绕不开的 1800 张图,先看清它到底值不值NEU-DET 是钢材表面缺陷检测方向被引用最多的公开数据集之一,某高校团队对热轧带钢表面缺陷做了分类与检测两套标注,检测版包含 1800 张灰度图像、6 类缺陷、每类 300 张。它解决的问题很具体:你没有产线数据时,需要一个能快速验证检测算法、能横向对比框架差距的基准;你在写工业质检方案前,也需要搞清楚“小样本加强形态差异”的数据到底会把模型带向哪里。它适合三类人:工业视觉算法工程师、做目标检测起步课题的研究生、想验证自己训练流程是否正确的从业者。反直觉的一点是:这个数据集分辨率只有 200×200,看起来不像能喂饱深度模型,但在合理配置下 mAP50 完全可以跑到 0.7 以上;而翻车的人几乎都栽在同一个地方——直接把通用检测框架的默认参数套在这么小的图上。2. 数据集构成与标签格式:六类缺陷、VOC 标注,动手前先对齐边界框写法2.1 六类缺陷怎么区分、哪个最难NEU-DET 的六类缺陷在视觉上差异很大,先看一眼它们的共性和差异,比直接开训更重要。常见缩写下,六类分别是 Crazing(Cr,裂纹/龟裂)、Inclusion(In,夹杂物)、Patches(Pa,斑块)、Pitted Surface(PS,麻点)、Rolled-in Scale(RS,氧化铁皮)、Scratches(Sc,划痕)。每类 300 张,但“每类一样多”不等于“每类一样好学”,因为标注框形态差别极大。类别中文通称视觉特征标注框形态特点Crazing裂纹/龟裂表面网状或放射状细线,灰度低、对比弱框极窄长,往往贯穿整图Inclusion夹杂物片状或条形暗块,边缘模糊框中等,矩形为主Patches斑块大面积灰度不均区域,边界很不规则框大而碎,背景占比高Pitted Surface麻点密集小凹坑,点状分布一个框内可能叠着多个小目标Rolled-in Scale氧化铁皮条带状暗块,沿轧制方向框窄长,方向相对固定Scratches划痕细长亮线或暗线,方向随机长宽比极端,形态多变这张表里最值得关注的是 Cr 和 Sc:它们的长宽比可能到 10:1 甚至更高,默认目标检测 head 里的 anchor 几乎覆盖不到。Pa 和 RS 的问题则是框内背景太多,模型容易把“灰度不均的区域”误学成“整块背景”。PS 的麻烦在于一个框里塞了多个小点,NMS 时容易互相抑制。所以后面做 anchor 调整和增强策略时,不是拍脑袋,是根据这张表来定。2.2 标签文件逐行拆解:XML 里每个字段在检测时做什么用NEU-DET 的检测标注采用 PASCAL VOC 风格,也就是每张图对应一个 XML 文件。解压镜像后你通常能看到 Images 和 Annotations 两个目录,XML 文件名和图片名一致。下面是一份示意标注内容,字段结构与真实样本一致:<annotation> <filename>IMG_0167.jpg</filename> <size> <width>200</width> <height>200</height> <depth>1</depth> </size> <object> <name>Scratches</name> <bndbox> <xmin>18</xmin> <ymin>26</ymin> <xmax>186</xmax> <ymax>196</ymax> </bndbox> </object> </annotation>这里几个字段在训练时各有用途。filename用于和图片对应;size里的 width 和 height 是做坐标归一化的分母,很多转换脚本写错就在这一步;depth是 1,表示灰度图,如果你的训练框架默认读三通道,通常会复制成三通道,这不影响训练但会浪费一点显存。object可以出现多次,表示一张图有多个缺陷框。name是类别字符串,bndbox是左上角和右下角坐标,单位是像素。需要留一个心眼:有些镜像里 XML 文件名和 JPG 文件名大小写不一致,有些样本存在 xmin 等于 xmax 的退化框。前者会导致转换后标签文件缺失,后者会导致宽高为 0,训练时被当成非法目标。我的习惯是拿到数据先做一次字段普查,用脚本把所有 XML 解析一遍,确认几张关键样本没问题再往下走。2.3 统计缺陷分布:用脚本算出框面积占比与长宽比,再决定 anchor 策略不要凭感觉判断哪类缺陷“难”,用数据说话。下面这个脚本会遍历全部 XML,输出每个类别的框数量、平均框面积占比和平均宽高比,这些数值直接决定你后面要不要改 anchor、要不要上多尺度训练。import os import xml.etree.ElementTree as ET ann_dir = "path/to/Annotations" # 改成你的 XML 目录 cnt = {} boxes = {} for fn in os.listdir(ann_dir): if not fn.endswith(".xml"): continue root = ET.parse(os.path.join(ann_dir, fn)).getroot() size = root.find("size") W, H = int(size.find("width").text), int(size.find("height").text) for obj in root.iter("object"): name = obj.find("name").text b = obj.find("bndbox") x1, y1 = int(b.find("xmin").text), int(b.find("ymin").text) x2, y2 = int(b.find("xmax").text), int(b.find("ymax").text) w, h = max(1, x2 - x1), max(1, y2 - y1) area_ratio = (w / W) * (h / H) # 框面积占整图面积比例 wh_ratio = (w / W) / (h / H) # 宽高比 cnt[name] = cnt.get(name, 0) + 1 boxes.setdefault(name, []).append((area_ratio, wh_ratio)) for name in cnt: area_list = [x[0] for x in boxes[name]] ratio_list = [x[1] for x in boxes[name]] print(f"{name}: count={cnt[name]}, " f"mean_area={sum(area_list)/len(area_list):.3f}, " f"mean_wh_ratio={sum(ratio_list)/len(ratio_list):.2f}")脚本逻辑很简单,但输出很值钱。mean_area告诉你缺陷在整张图里占多大比例,比如 Pa 可能到 0.3 以上,Cr 可能只有 0.02 到 0.05;mean_wh_ratio告诉你的目标有多“扁”,数值越大越接近细线。把这两列打印出来后,anchor 怎么改就不是猜的了:如果 Cr 和 Sc 的平均宽高比超过 8,默认的 COCO anchor 几乎不会和它们匹配上,你需要手工聚类 anchor 或者调整匹配阈值。这个脚本在任何带 VOC 标注的检测数据上都通用,建议直接收进自己的工具箱。3. 跑通 YOLOv5 的最小闭环:目录划分、格式转换与训练参数3.1 先把 1800 张图按类别分层拆成训练集和验证集拿到数据后的第一个实操动作是划分数据集。常见做法是按 8:2 分成训练集和验证集,但 NEU-DET 的每类只有 300 张,如果完全随机切,某个类别在验证集里可能只剩十几张,mAP 波动会非常大。正确做法是按类别分层采样,让每类缺陷在训练集和验证集中都保持 8:2。cd NEU-DET mkdir -p images/train images/val labels/train labels/val然后运行下面的分层划分脚本。注意脚本里固定了随机种子,这是可复现实验的前提。import os import random import xml.etree.ElementTree as ET random.seed(42) # 固定种子,保证每次划分结果一致 src_img = "Images" # 原始图片目录 src_ann = "Annotations" # XML 目录 img_dir = "images" val_ratio = 0.2 def get_first_class(ann_path): """取一张图标注里出现的第一个类别,用于分层""" root = ET.parse(ann_path).getroot() for obj in root.iter("object"): return obj.find("name").text return None all_files = [f[:-4] for f in os.listdir(src_img) if f.endswith(".jpg")] # 注意:有的镜像图片是 .png 或 .bmp,按实际后缀改这里 class_groups = {} for name in all_files: cls = get_first_class(os.path.join(src_ann, name + ".xml")) if cls is None: continue class_groups.setdefault(cls, []).append(name) train_files, val_files = [], [] for cls, names in class_groups.items(): random.shuffle(names) n_val = int(len(names) * val_ratio) val_files.extend(names[:n_val]) train_files.extend(names[n_val:]) random.shuffle(train_files) random.shuffle(val_files) for name in train_files: os.rename(os.path.join(src_img, name + ".jpg"), os.path.join(img_dir, "train", name + ".jpg")) for name in val_files: os.rename(os.path.join(src_img, name + ".jpg"), os.path.join(img_dir, "val", name + ".jpg")) print(f"train={len(train_files)}, val={len(val_files)}")这段脚本用get_first_class读取每张图的第一个类别做分组,原因是 NEU-DET 的单张图片基本对应单一缺陷类别。如果你后续要做多标签混合,验证集也要保持各类别比例一致,那就得改成按所有类别统计。脚本里的os.rename是移动文件,建议第一次跑之前先复制而不是移动,免得路径写错把原始数据搞乱。3.2 把 VOC 格式转成 YOLO 格式:转换脚本与坐标归一化细节YOLO 系框架不读 VOC XML,它要求每个图片对应一个 txt,每行是类别索引 中心点x 中心点y 宽 高,四个坐标全部归一化到 0 到 1。这个转换是整个流程里最容易出错的环节,坐标格式错了,训练出来的模型会“看起来能跑但框全偏”。import os import xml.etree.ElementTree as ET # 类别顺序必须和后续 neu.yaml 里的 names 完全一致 CLASSES = ["Crazing", "Inclusion", "Patches", "Pitted Surface", "Rolled-in Scale", "Scratches"] def voc_to_yolo(ann_path, out_path): root = ET.parse(ann_path).getroot() size = root.find("size") W, H = int(size.find("width").text), int(size.find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in CLASSES: continue cls_id = CLASSES.index(name) b = obj.find("bndbox") x1, y1 = int(b.find("xmin").text), int(b.find("ymin").text) x2, y2 = int(b.find("xmax").text), int(b.find("ymax").text) # 关键点:中心点和宽高全部除以图片宽高,得到 0~1 之间的数 cx = (x1 + x2) / 2 / W cy = (y1 + y2) / 2 / H w = abs(x2 - x1) / W h = abs(y2 - y1) / H # 过滤掉宽度或高度为 0 的退化框 if w <= 0 or h <= 0: print(f"skip bad box in {ann_path}") continue lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines)) ann_dir = "Annotations" out_dir = "labels" os.makedirs(out_dir, exist_ok=True) for fn in os.listdir(ann_dir): if not fn.endswith(".xml"): continue out_txt = os.path.join(out_dir, fn.replace(".xml", ".txt")) voc_to_yolo(os.path.join(ann_dir, fn), out_txt)转换脚本里两个地方需要重点核对。第一,CLASSES的顺序决定了类别索引,它必须和训练配置里的names顺序保持一致,顺序一旦错位,模型训练时会把所有类别标签错位学习;第二,末尾的w <= 0 or h <= 0过滤不能省,NEU-DET 镜像里偶尔会出现 xmin 等于 xmax 的坏标注,不过滤的话训练日志里会报一堆 warning,甚至影响 loss 计算。转换完必须做可视化抽查。不要直接开训,先随机挑 10 张图把 txt 里的框画回原图看一眼:import cv2, glob for txt in glob.glob("labels/train/*.txt")[:10]: name = txt.split("/")[-1][:-4] img = cv2.imread(f"images/train/{name}.jpg") H, W = img.shape[:2] with open(txt) as f: for line in f: c, cx, cy, w, h = map(float, line.split()) x1 = int((cx - w / 2) * W) y1 = int((cy - h / 2) * H) x2 = int((cx + w / 2) * W) y2 = int((cy + h / 2) * H) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(f"check_{name}.jpg", img)框和缺陷轮廓贴合,说明转换正确;框整体偏移、明显放大或缩小,基本就是归一化或坐标换算写错了。这一步的“肉眼验证”比任何日志都管用,也是我后来在所有数据集上都保留的习惯。提示:YOLO 标签的第一列是整数类别索引,后四列全部是 0~1 的小数。手工检查时可以做一个断言:np.loadtxt(txt)[:, 1:].min() >= 0。3.3 最小训练命令与参数取舍:为什么是 640、16、300数据和标签都就位后,训练命令很简单。以某个常见框架为例,最小闭环是这样的:python train.py \ --data neu.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 300 \ --freeze 10 \ --seed 20240608配套的neu.yaml长这样:path: ./NEU-DET train: images/train val: images/val nc: 6 names: ['Crazing', 'Inclusion', 'Patches', 'Pitted Surface', 'Rolled-in Scale', 'Scratches']几个参数的选择理由值得展开。--img 640不是随便给的:NEU-DET 原图只有 200×200,直接训会损失大量细节,放大到 640 相当于把裂纹、划痕这种细线结构在像素上拉长,模型才有机会学到它们的连续性;显存不够就降到 416,但别低于 416。--batch 16对 1800 张图来说足够让 BN 统计量稳定,单卡 8G 也能跑。--epochs 300是因为小数据集收敛慢,尤其是 Cr、Sc 这类难学样本,150 轮之前主要是在学通用特征,后 150 轮才是精细调框。参数建议值理由img640原图 200×200 信息太稀,放大后细线缺陷可学习batch161800 张图,batch 太小 BN 抖动明显epochs300小数据集需要更长训练周期,mAP 后期仍在缓慢上升freeze10前 10 轮冻结 backbone,防止迁移初期梯度震荡seed固定值保证数据划分、增强和初始化可复现训练过程里重点看两个指标:mAP50 和 mAP50:95。NEU-DET 在 YOLOv5s 这个量级下,mAP50 跑到 0.75 到 0.85 之间是比较常见的水平,mAP50:95 通常只有 mAP50 的一半左右。如果你的 mAP50 在 0.5 以下,大概率不是框架问题,而是数据划分、标签转换或参数设置里的某一步出了问题,往下看排查章节。4. 数据搭配陷阱:类别不均衡、小图增强边界与验证集不可复现4.1 每类 300 张并不平衡:框形态差异带来的“隐性不均衡”NEU-DET 从数据量上看每类都是 300 张,非常“均衡”,但实际训练时会发现某些类别的 loss 降不下去。问题不在数量,在框形态。用前面统计脚本的输出再往深想一层:Cr 和 Sc 的框长宽比极端,一个框面积可能只占整图的 1% 到 3%,它们在特征图上的响应本身就弱;Pa 和 RS 的框大,但边界模糊,模型很难确定“缺陷到哪里结束”;PS 的框内包含多个小目标,一个框算一个正样本的匹配方式会让模型对框内位置不敏感。这种结构性不均衡带来的直接后果是:默认 anchor 策略下,Cr 类的正样本数量远少于 Pa 类。模型学会了大目标的回归,却很少有机会修正细长框,最后表现就是 Cr 和 Sc 的 mAP 长期压在 0.3 以下。处理方案有两个方向:一是调整 anchor,针对窄长框增加高宽比大的候选;二是调整输入分辨率,把 640 改成 1280 跑一遍对比,往往能直接看到 Cr 类 mAP 回升。很多人觉得放大输入是“作弊”,但在工业缺陷检测场景里,输入分辨率本身就是可以调的超参数。4.2 增强策略的边界:mosaic 在这份数据上的副作用与替换方案通用检测框架默认开的 mosaic 增强,在 NEU-DET 这种小图上是有副作用的。mosaic 会把 4 张图拼接成一张再随机裁剪缩放,本来 200×200 的图被裁切后,缺陷尺寸被进一步缩小。如果原图里一个裂纹框只有 10 个像素宽,mosaic 之后再缩一半,这个目标在特征图上可能只剩 1 到 2 个像素,正样本匹配直接失败。常见做法是:训练前 250 轮开 mosaic 增强模型的泛化能力,最后 50 轮把 mosaic 关掉,让模型在接近真实尺度的输入上收敛。# hyp.yaml 片段 mosaic: 1.0 # 前 250 轮保持,最后 50 轮降为 0 copy_paste: 0.2 # 小目标增强更温和,适合这种细线缺陷 rotate: 10 # 不要超过 20,热轧缺陷方向有物理约束 shear: 5 hsv_h: 0.01 hsv_s: 0.4rotate和shear要克制。钢材表面的裂纹、划痕方向受轧制工艺影响,有物理约束;你要是把图旋转 45 度,等于人为制造了一批现实里不存在的缺陷形态,模型会把特征空间学歪,推理时遇到正常形态反而漏检。copy_paste是把一个样本里的目标复制到另一张图上,它对小目标更友好,比 mosaic 适合这份数据。注意:任何增强参数改完后,都要可视化几张增强结果确认缺陷没被扭曲得不像样。增强是数据生成的“黑匣子”,肉眼过一遍比看曲线有用。4.3 固定划分、分层采样:让实验能复现的三点做法小数据集的另一个坑是“验证集不可复现”。有人训练时每次都在代码里随机划分数据集,结果同一份代码、同一个参数,三次实验的 mAP 差了 5 个点,还以为是模型随机性,实际上只是某类缺陷在验证集里的数量变了。我在这类数据上的习惯是三条。第一,划分脚本单独跑一次,把划分结果存成固定的train.txt和val.txt,后续所有实验只读这两个文件,不再重新划分。第二,所有涉及随机的地方(划分、训练增强、anchor 聚类)统一固定种子。第三,划分时按类别分层,确保每类缺陷在验证集里都有足够样本,而不是让验证集被某类大目标主导。# 读取固定划分,不重新生成 with open("train.txt") as f: train_files = [x.strip() for x in f if x.strip()] with open("val.txt") as f: val_files = [x.strip() for x in f if x.strip()]这套做法看起来笨,但能节省大量排错时间。否则你在对比“加了这个模块有没有效果”时,根本分不清收益来自代码改动还是数据划分的小波动。数据划分本身成了变量,实验就失去了意义。5. NEU-DET 实战常见问题排查:五条踩坑记录与解决路径5.1 细长缺陷类 mAP 上不去:Cr 和 Sc 一直压在 0.3 以下现象:训练 300 轮后,In、Pa、PS、RS 的 mAP 都在 0.7 以上,唯独 Cr 和 Sc 只有 0.3 左右,验证集上大量漏检。原因:这两类缺陷框长宽比极端,默认 anchor 的宽高比通常覆盖 0.5 到 2 的范围,对 10:1 以上的框完全匹配不上。匹配不上就没有正样本,没有正样本模型就学不到这类缺陷。解决:先把 anchor 改成“窄长优先”。常见做法是关掉框架的自动 anchor 计算,用自己的聚类脚本重新生成锚框,突出高宽比大于 5 的候选;同时可以把anchor_t阈值放宽,让更多窄长框进入正样本匹配范围。我当时把输入从 640 提到 1280 + 聚类 anchor 一起做,Cr 的 mAP 从 0.3 直接提到 0.6,效果非常明显。改完 anchor 后一定要重新验证框匹配结果,不能只看训练 loss。5.2 推理时预测框整体偏移:训练正常但框明显“错位”现象:训练日志里 loss 正常下降,验证集 mAP 也还行,但把模型部署到单张图上推理时,预测框比实际缺陷的位置整体向左上或右下偏移半个身位。原因:我用过的最隐蔽的原因是标签转换时坐标归一化基数写错。有些脚本把 xmin、xmax 除以了宽高比而不是图片本身的宽高,或者用了整数除法,导致所有框坐标被系统性缩放。解决:先画框回原图,确认偏移方向是否一致。如果所有框都朝一个方向偏移,问题基本在归一化;如果偏移随机,问题可能出在数据增强流程没同步更新坐标。验证命令很简单:python - <<'EOF' import numpy as np, glob for txt in glob.glob("labels/train/*.txt")[:20]: arr = np.loadtxt(txt) if arr.ndim == 1: arr = arr.reshape(1, -1) print(txt, "min=%.3f max=%.3f" % (arr[:, 1:].min(), arr[:, 1:].max())) EOF正常情况后四列的 min 应该大于等于 0、max 小于等于 1。一旦发现 max 超过 1.5 甚至几十,直接查转换脚本里的分母。这类问题越早发现成本越低,训练几小时后再发现基本只能重来。5.3 开着 mosaic 训练卡在一个偏低的上限:mAP 停在 0.5 上不去现象:训练到 200 轮左右 mAP50 稳定在 0.5,再用实验性的参数调整也拉不上去,看起来像模型容量到顶。原因:NEU-DET 原图 200×200,mosaic 会把图缩小到原来的一半甚至四分之一,细小缺陷在增强后的图片上直接“消失”。模型在训练时见过大量无效正样本,收敛上限被拉低。解决:最后 50 到 80 轮关闭 mosaic,只保留 copy_paste、轻微旋转和尺度抖动,让模型在接近真实输入分布的尺寸上微调。如果你的框架支持写死增强参数,分两阶段训练即可:前 250 轮用默认增强,后 50 轮把mosaic: 0重新跑一遍。这样改动后 mAP 普遍能回升 5 到 10 个点,属于小数据集上性价比很高的操作。5.4 加载预训练权重后前期 loss 剧烈震荡:前 20 轮 loss 乱跳现象:用了 COCO 预训练权重,结果前 20 轮 box_loss 和 cls_loss 大幅度震荡,验证集 mAP 甚至掉到 0,到 40 轮才缓慢恢复。原因:COCO 数据以中大目标为主,预训练特征和 NEU-DET 的细长小目标尺度差异太大。加载权重后,最后一层检测头的参数还没适应新数据,前几轮梯度噪声很大,BN 统计量也在重新估计。解决:前 10 到 15 轮冻结 backbone,只训练检测头,等 loss 稳定后再解冻全部参数。命令里加--freeze 10即可;如果震荡依然严重,把初始学习率从 0.01 降到 0.003,同时把 warmup 轮数加长。冻结阶段结束后 loss 会出现一次小跳变,那是解冻的正常反应,不用慌。5.5 换了一个方向就漏检:增强角度太大,模型把特征学歪了现象:验证集 mAP 正常,但对着现场拍回来的热轧带钢图推理,同一个缺陷换了个方向就漏检,尤其是划痕。原因:训练时增强参数里rotate设成了 30 甚至 45。NEU-DET 的缺陷方向有物理约束,划痕大多沿轧制方向分布;大幅旋转产生的训练样本在现实里不存在,模型花了不少参数去拟合这些“假样本”,对真实方向的敏感度反而下降。解决:把旋转限制在 ±10 度以内,关闭或弱化透视畸变,增加 hsv 亮度扰动来模拟光照变化。热轧带钢本身表面反光差异大,亮度扰动比角度扰动更符合真实场景。改完增强后重新训练,推理时对方向变化的鲁棒性会明显改善。这个问题的本质是:增强的目的是扩充分布,而不是创造现实里不存在的分布。6. 把 1800 张图用出更大数据量的效果:三点进阶实践6.1 图像分块让细小缺陷从“看不见”变“看得见”NeU-DET 的 Cr 和 Sc 在 200×200 原图上可能只有几个像素宽,即使放大到 640 输入,仍不足以让模型捕捉完整纹理。我常用的做法是分块推理:把输入图切成 2×2 或 4×4 的小块,每块独立检测后再按原坐标合并,最后做一次 NMS。def split_infer(img, model, tile=320, overlap=32): h, w = img.shape[:2] boxes, step = [], tile - overlap for y in range(0, h, step): for x in range(0, w, step): crop = img[y:y+tile, x:x+tile] det = model(crop)[0] for x1, y1, x2, y2, conf, cls in det: boxes.append([x1 + x, y1 + y, x2 + x, y2 + y, conf, cls]) return boxes # 合并后按置信度排序再做 NMS分块后细线缺陷在每一块里占据的比例变大,检测器更容易命中。代价是推理时间翻倍,但对工业离线抽检场景通常可接受。配合前面说的 anchor 调整,Cr 和 Sc 的漏检能再降一截。6.2 伪标签自训练:把没标注的带钢图变成训练集NEU-DET 本身只有 1800 张标注图,但实际产线或公开渠道能拿到大量未标注的热轧带钢表面图。一个可靠做法是用训练好的模型给这些无标注图做推理,置信度超过 0.85 的框整理成伪标签,加入训练集再训一轮。这份数据里值得特别小心的是类别平衡:伪标签天然偏向模型已经学好的类,所以加样本时要按类别抽样,甚至只挑 Cr、Sc 这种弱类来补,效果更直接。6.3 测试时增强:不训练也能换回一点召回率如果你已经训完一版模型,还想榨出一点点性能,测试时增强是最快的路径。做法是推理时将输入图做水平翻转和 0.8、1.0、1.25 三种尺度缩放,分别推理后把结果映射回原图坐标,再做 NMS 融合。水平翻转对 Sc 这种方向随机的缺陷提升最明显,多尺度则对尺度变化有鲁棒性。代价是推理耗时增加 3 到 5 倍,但有些场景下 mAP 能涨 1 到 2 个点。我自己的收尾习惯是:训练之前永远先做两步可视化——第一步画原始标注框,第二步画增强后的样本。这两步做完了,后面训练其实很少翻车。这不算什么高超技巧,但很多“玄学掉点”的深夜排查,追根溯源都发生在没看这两张图的时候。希望帮到你。本文还有配套的精品资源,点击获取