做工业视觉的人对钢材表面缺陷检测这个问题应该不陌生。热轧钢板在产线上高速通过,表面随时可能出现划痕、斑块、氧化皮、夹杂物这类缺陷,靠人工盯着屏幕看,时间一长就是视觉疲劳,漏检率根本控不住。我最近完整搭了一套基于深度学习的钢材表面缺陷检测系统,核心用YOLO系列模型——v5、v8、v11、v12四个版本都跑过对比,配套做了一版PyQt5交互界面,训练代码、数据集整理、模型部署全部串通。这篇文章就把这套系统的设计思路、技术选型逻辑、训练实操过程、界面开发细节和踩过的坑完整梳理一遍,给同样在做工业质检方向的朋友一个可直接参考的模板。
1. 项目背景与需求拆解
1.1 钢材表面缺陷检测的工业意义
钢材生产环节中,表面质量直接决定产品的等级和售价。热轧、冷轧、连铸过程中,因为工艺波动、设备磨损、原料杂质等因素,钢材表面容易出现划痕、裂纹、氧化皮、麻点、夹杂类缺陷。这些缺陷如果漏检,一块不合格的钢板流入下游客户手里,轻则退货赔偿,重则影响整车、建筑结构的可靠性。传统工厂的做法是靠质检员在强光下肉眼观察,或者抽样检查,但产线速度稍微提上来,人工根本跟不上。
我接触过的案例里,一条热轧产线一分钟要过几十米钢板,表面缺陷可能出现在视觉盲区,人工盯岗能保持20分钟的专注度就已经不错了。因此行业里对自动化视觉质检的需求非常强烈——不是"锦上添花",而是"必须上"。
1.2 传统机器视觉方案的局限
在深度学习普及之前,工厂里的表面缺陷检测大多用传统图像处理方式:通过阈值分割、边缘检测、纹理分析、形态学运算等手段来定位异常区域。这类方案在缺陷类型固定、背景干净、光照稳定的场景下勉强可用,一旦换钢种、换光照、板材表面有油污或者水渍,算法就需要重新调参,泛化能力很差。说白了,传统方案是在跟产线的各种"意外"对抗,每次参数调整都需要视觉工程师驻场调试,成本高、响应慢。
1.3 深度学习检测方案的优势
深度学习目标检测模型改的是一种思路:不再手动设计特征提取算子,而是让模型从大量标注样本中自动学习缺陷的视觉特征。以YOLO系列为代表的单阶段检测器,在速度和精度之间做到了很好的平衡。更重要的是,它把"检测"从分类任务升级为"定位+分类"任务,不仅告诉你有缺陷,还能给出缺陷在画面中的具体位置和类别,这对后续自动标记、分拣、追溯,价值巨大。
我最终在这套系统里选择YOLO系列作为检测核心,主要看中的就是它在工业场景的成熟度:训练生态完整、部署方案多、社区资料丰富,遇到问题基本都能找到解决方案。
1.4 系统功能需求梳理
在动手前,我先列了这份系统的功能需求清单:
- 支持加载预训练模型,对钢材表面缺陷图像进行实时检测;
- 能够切换不同的YOLO模型版本,对比检测效果;
- 支持图片检测、视频检测两种主要模式,最好能兼容RTSP摄像头输入;
- 检测结果可视化,在画面上直接绘制目标框和类别标签;
- 输出检测统计数据,比如每类缺陷的数量、单帧推理耗时;
- 界面能直观切换模型权重文件,便于测试不同训练阶段的模型。
这个需求清单在后面界面设计和代码实现过程中起了大作用,避免我做到一半发现功能缺了又要回去补。
2. 技术选型与整体方案设计
2.1 为什么锚定YOLO系列
提到工业缺陷检测,很多人第一反应是Faster R-CNN或者SSD这类经典模型。但我自己做下来体感是:在两阶段检测器上训练工业小目标缺陷,速度确实太慢。钢材表面缺陷多数是小尺寸目标,一张1280x1280的图像里可能只有几个几十像素的缺陷框,两阶段模型处理一张图耗时几百毫秒,放到产线上根本跑不起来。
YOLO系列主打的就是实时检测。从YOLOv5开始,模型推理速度就能达到几十到上百FPS,配合适当的图像缩放,完全能满足在线质检的帧率需求。另外YOLO生态里训练、验证、导出、量化的工具链非常完整,这是其他框架替代不了的。
2.2 四个版本的核心差异与选型对比
我用到的四个版本,各有各的特点:
| 模型版本 | 架构亮点 | 适用场景 | 实测体感 |
|---|---|---|---|
| YOLOv5 | anchor-based,CSPDarknet骨干 | 稳定性好,社区资源最丰富 | 部署最省心,老项目兼容性好 |
| YOLOv8 | anchor-free,C2f模块 | 官方生态完善,训练速度均衡 | 精度提升明显,改造成本低 |
| YOLO11 | 优化C3k2与SPPF,轻量化设计 | 关注推理性能的新版本 | 参数量控制好,小目标表现不错 |
| YOLOv12 | 引入区域注意力机制RAD | 探索注意力与CNN融合 | 理论新颖,需要更多数据验证 |
从实际项目角度,我不建议一上来就追最新版本。我的策略是:先用YOLOv8跑通整个流程,把它当作baseline,然后再用v5、v11、v12做对比实验,看哪个版本在自己数据集上的mAP更高、速度更快,再作为最终模型来部署。这样保证项目是稳健推进的,而不是被新版本带偏。
2.3 PyQt5界面层的取舍逻辑
界面这块,很多做算法的朋友会直接忽略,或者用Flask起一个Web服务就算完事。但工业现场不同——产线质检员需要的是一键打开、直观显示、稳定运行的桌面工具,Web页面在离线车间环境里部署麻烦,浏览器兼容性也容易出问题。
PyQt5的优势在于:跨平台、控件丰富、开发速度快。用QThread做后台推理线程,搭配QGraphicsView做图像显示,信号槽机制天然适合UI与推理之间的交互。而且通过pyinstaller可以打包成独立的exe程序,拷到没有Python环境的工控机上也能运行,这对项目交付来说非常关键。
2.4 系统整体架构与工作流程
这套系统的架构分三层:
- 数据层:整理好的标注数据集,按YOLO格式存放图像和txt标签文件;
- 模型层:YOLO系列训练得到的权重文件,推理时动态加载;
- 应用层:PyQt5界面负责交互,完成图像读取、模型推理、结果绘制和数据统计。
工作流程是:用户在界面上选择模型权重和输入源(图片/视频/摄像头),点击开始检测后,后台线程逐帧调用模型推理,把结果绘制在画面上,同时更新检测信息栏。这个流程整体不复杂,但每一步都有不少细节坑,后面逐个展开。
3. 数据集准备与预处理实战
3.1 数据集来源与组成
数据集是整个系统的基石。我用了一套公开的钢材表面缺陷数据集,业内很多人也在这套数据上做算法验证。它包含六类典型的钢材表面缺陷:氧化铁皮、斑块、开裂、点蚀、夹杂物、划痕,每一类有几百张样本,每张图像的分辨率在200x200左右。
这里我必须提醒一点:公开数据集的图片尺寸偏小,直接拿来做YOLO训练可以,但放到真实产线上未必够用。如果你手头有条件,尽量采集自己工厂产线上的高清图像,再配合公开数据集做预训练和迁移学习,这样模型泛化能力才会好。
3.2 标注工具与标签格式转换
YOLO系列要求每张图片对应一个txt文件,每行格式为:类别id 中心点x 中心点y 宽度w 高度h,坐标值全部归一化到0到1之间。我用标注工具先标记成VOC格式的xml文件,然后再写脚本转成YOLO的txt格式。
转换脚本的核心逻辑就是:
import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_path, class_names): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') w_img = int(size.find('width').text) h_img = int(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_names: continue cls_id = class_names.index(name) 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) dw = 1.0 / w_img dh = 1.0 / h_img x_center = (xmin + xmax) / 2.0 * dw y_center = (ymin + ymax) / 2.0 * dh w = (xmax - xmin) * dw h = (ymax - ymin) * dh lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(out_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines))转换完之后,务必抽查几张图的标注框和原图叠加,确认没有坐标错乱。这一步最容易出问题的是xml文件里某些图像的宽高信息和实际不一致,会导致标注框偏移。
3.3 数据增强:不是越多越好
训练时我开了mosaic增强,这是一种非常有效的策略,把四张图拼接成一张,增加检测器的鲁棒性。我用的增强配置是:
train_augmentation: mosaic: 1.0 mixup: 0.2 hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.4 fliplr: 0.5 scale: 0.5注意别把增强参数拉得太猛。我刚开始训练的时候把mosaic概率设成1.0的同时又把fliplr开到1.0,结果模型在验证集上表现震荡很大,原因就是增强太强导致训练分布和验证分布差距过大。后来降低到一半概率翻转,mosaic保留默认,mAP就稳定下来了。
3.4 数据集划分的坑
工业缺陷数据集通常存在严重的类别不平衡。划痕类样本多,点蚀、开裂类样本少。我做了两件事:
- 划分训练集和验证集时,尽量保证每个类别在两边都有足够的样本;
- 对数量少的类别做离线复制增强,把样本量抬到其他类别的60%以上,防止训练时模型对小类缺陷直接忽略。
划分脚本里加了一个类别数量统计函数,先统计每类的样本数,再做分层切分,而不是直接random.shuffle。小样本类别少一两张,验证结果就会明显失真。
4. 模型训练核心流程与参数调优
4.1 训练环境安装与验证
环境方面我的建议是:GPU显存够的话,直接装CUDA版PyTorch,不要用CPU版本顶。训练顺序是:
# 创建虚拟环境 conda create -n steel_yolo python=3.10 -y conda activate steel_yolo # 安装PyTorch(以CUDA 11.8为例) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装YOLO训练依赖 pip install ultralytics pyqt5 opencv-python这里有个容易被忽略的点:YOLO新版本对Python版本有要求,太老的Python可能装不上最新依赖。我常年用3.10,兼容性比较稳。装完之后用这样一段代码验证GPU是否可用:
import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果输出False,先检查驱动和CUDA版本是否匹配,别急着重装环境。
4.2 训练参数设置与解析
训练参数是决定模型效果的关键一环。我跑通整个流程用的参数如下:
model: yolov8m.pt data: steel_defect.yaml epochs: 150 imgsz: 640 batch: 16 patience: 20 optimizer: AdamW lr0: 0.001 close_mosaic: 10几个重要参数的实际意义:
- imgsz默认用640,如果缺陷很小可以考虑提高到960或1280,但显存占用和推理耗时都会增加;
- batch大小首先要看显卡显存,一般256M显存约能支撑1到2的batch,我用的设备是单卡16G,batch=16比较宽裕;
- close_mosaic表示最后10个epoch关闭mosaic增强,让模型在接近真实分布的数据上精修;
- patience是早停次数,连续20轮验证集指标不提升就自动停止,节省时间。
4.3 训练过程的监控信号
训练过程中我最关注三个曲线:train loss、val loss、mAP50和mAP50-95。
- train loss持续下降是正常的;
- val loss如果先降后升,说明过拟合了,需要加大数据增强或者提前早停;
- mAP50是IoU阈值0.5下的平均精度,工业场景比较关心这个指标;
- mAP50-95更严格,反映模型在不同IoU阈值下的综合能力,论文里常看这个。
有一次训练跑到第80轮,发现val loss开始反弹,但mAP还在小幅爬升,这时候其实可以停了,后面的提升基本是虚的,模型已经在记忆训练集的噪声。
4.4 模型评估与结果解读
训练结束后,我习惯先看混淆矩阵和PR曲线。混淆矩阵能直观看出哪些缺陷类别之间互相混淆——比如"氧化铁皮"和"斑块"在视觉上确实接近,容易分错。针对混淆比较严重的类别,策略是补充更多该类别的样本,或者调整类别权重。
我还导出每一类缺陷的mAP值,找出表现最差的类别重点分析。如果某个类别的AP特别低,大概率是样本量不足或标注框不够准确,这时回退到数据层面去解决,而不是盲目调模型。
4.5 模型导出与部署格式
训练完成后的权重是.pt格式,适合python环境直接推理。如果是部署到工控机,我通常导出成ONNX格式,方便在不同推理框架之间迁移;再进一步可以导出成TensorRT的engine格式,在N卡上推理速度最快。
我用到的导出命令:
yolo export model=best.pt format=onnx imgsz=640 yolo export model=best.pt format=engine imgsz=640 device=0特别注意:导出onnx时要把imgsz和训练时保持一致,否则后续推理会因输入尺寸不匹配而报错或大幅掉精度。
5. PyQt5界面设计与功能实现
5.1 界面分区与核心思路
PyQt5界面我设计成三个区域:上方是图像显示区,左下是模型控制和检测按钮区,右下是检测信息统计区。
整个界面用QMainWindow作为主窗体,图像显示用QLabel配合QPixmap实现。这里有一个细节:直接用QLabel显示高分辨率图像时,界面会卡,因为QLabel没有做图像缩放优化。我改成在QLabel上铺一个QGraphicsView,用QGraphicsPixmapItem承载图像,流畅度提升明显。
5.2 模型加载与初始化逻辑
模型加载放在界面的初始化函数中,尽量在后台线程完成,否则启动界面时会卡住好几秒。加载逻辑如下:
from ultralytics import YOLO class Detector: def __init__(self, weights_path): self.model = YOLO(weights_path) self.names = self.model.names self.conf_thres = 0.25 self.iou_thres = 0.45 def infer(self, frame): results = self.model(frame, conf=self.conf_thres, iou=self.iou_thres) return results推理结果是一个列表,每个元素对应一张图。从results[0].boxes中拿目标框坐标、置信度和类别id,再画出矩形框。
5.3 三种输入模式的处理
图片模式:通过QFileDialog打开本地图片,读取后直接推理并显示结果。
视频模式:用cv2.VideoCapture读取视频文件,逐帧推理并刷新图像显示。注意点:视频推理一定要放到QThread子线程里,不要在UI主线程中做循环,否则界面直接无响应。
摄像头模式:支持RTSP流地址。工业相机一般走RTSP协议,输入一个rtsp://开头的地址就能实时读取。关键是要控制读取帧率,摄像头30帧的流,推理不一定跑得动,我加了跳帧逻辑,每隔一帧处理一次。
下面是一个后台推理线程的简化代码:
import cv2 from PyQt5.QtCore import QThread, pyqtSignal class VideoThread(QThread): frame_ready = pyqtSignal(object) stats_updated = pyqtSignal(dict) def __init__(self, video_source, detector): super().__init__() self.video_source = video_source self.detector = detector self.running = True def run(self): cap = cv2.VideoCapture(self.video_source) frame_count = 0 while self.running and cap.isOpened(): ret, frame = cap.read() if not ret: break frame_count += 1 if frame_count % 2 != 0: continue results = self.detector.infer(frame) rendered = results[0].plot() self.frame_ready.emit(rendered) # 刷新统计信息 self.stats_updated.emit(self.collect_stats(results)) cap.release() def stop(self): self.running = False self.wait()5.4 检测结果显示与统计
检测结果可视化我直接用YOLO推理结果自带的plot()方法,它会自动在图像上绘制目标框、类别名和置信度,省去手动处理坐标的麻烦。如果你的需求更定制化,也可以手动遍历results[0].boxes然后自己画。
统计区域我放了一个表格控件,显示每个类别的检测数量、平均置信度、单帧推理耗时。用信号槽机制把子线程的统计数据传回主界面刷新表格,避免线程之间数据竞争。
5.5 界面打包发布与兼容问题
开发完成后需要用pyinstaller打包成exe。我踩过的坑是打包后打开就闪退,排查下来是多进程挂起问题——YOLO训练框架在导入时会尝试fork多进程。解决办法是打包时不加console,并关闭框架的多进程特性,或者在入口处加multiprocessing.freeze_support()。
pyinstaller配置里还建议把模型权重文件放在一个单独的models目录,打包时用--add-data参数包含进去,不要硬编码绝对路径。
6. 常见问题与排查技巧实录
6.1 训练阶段典型问题
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 训练loss不降 | 学习率过大或数据标注错误 | 降低lr0,检查标签文件是否越界 |
| 验证集mAP波动大 | 增强参数过猛或验证集数据太少 | 降低增强强度,重新切分层数据 |
| GPU显存不足 | batch太大或图像尺寸太大 | 减少batch,降低imgsz,开梯度累积 |
| 某些类别AP为0 | 该类样本太少 | 离线补样本,或调整类别权重 |
6.2 推理阶段典型问题
推理时不显示检测框,最常见的原因是置信度阈值设置过高。钢材表面缺陷本来对比度就不强,模型输出的置信度普遍偏低,我把默认阈值降到0.15到0.25之间,检测结果就出来了。
另外,视频检测时如果发现帧率极低,先看是不是没有用GPU推理。YOLO模型设备上如果用的是CPU,640*640的输入跑一次可能要300到500毫秒。排查方法是打印model.device,确认是cuda还是cpu。
6.3 PyQt5界面常见问题
界面卡死的绝大多数原因都是推理放在了主线程。解决方案就是把推理逻辑放进QThread子线程,主线程只负责接收信号刷新界面。
还有个容易忽略的问题:从子线程发信号传图像时,如果传的是cv2的numpy数组,跨线程传递要确保是拷贝而不是引用,我用frame.copy()手动拷贝一次,避免出现图像显示花屏或偶发崩溃。
6.4 性能优化实用建议
- 推理前把输入帧缩放到640x640再送模型,能显著降低耗时;
- 批量推理比单帧推理高效,视频检测可以攒几帧再一起推理;
- 用TensorRT导出engine格式后,推理耗时能再降30%以上;
- 如果产线需要实时性,考虑用一个专用线程做模型推理,另一个线程只负责图像采集和GUI更新,两个线程之间用队列解耦。
这些优化做完,整个系统的实时性会有质的提升。
7. 结语与个人经验
这套系统我前后跑了将近一个月,从数据集整理、模型对比训练到界面封装,每个环节都有值得复盘的地方。说句实在话,模型本身的选择不是最难的,难的是把数据整规则、把参数调稳定、把界面做得让现场工人愿意用。我个人的体会是:做工业视觉项目,先求稳定和可用,再追指标,因为产线环境永远比实验室复杂得多。
如果后续要扩展,可以考虑接入多路摄像头并行检测,或者把检测结果直接对接工厂的MES系统做质量追溯,这些都是这套架构上可以自然延伸的方向。最后再分享一个小技巧:训练结束后一定要把每个版本的权重文件、参数配置、数据集版本号放在一起归档,因为现场模型升级时,你往往需要回溯到某个旧版本去排查问题,这份记录能帮你省下大量时间。希望这篇分享对正在做同类项目的朋友有帮助。