简介:本资源是一套开箱即用的YOLOv5钢材表面缺陷检测完整方案,面向工业视觉初学者、自动化质检工程师及高校科研人员,解决钢铁制造中常见缺陷(如裂纹、划痕、氧化斑等)的快速识别与定位问题。压缩包共180个文件,总计141.66MB,涵盖34个核心Python脚本(含训练/推理/QT界面逻辑)、29个配置yaml文件(数据集路径、类别定义、模型超参)、32张标注样本JPG图及配套XML/TXT双格式标签、5个预训练.pt权重文件、4个PyQt设计.ui界面文件,以及PR曲线、loss变化图、results.csv评估结果等训练成果。已有1750人学习下载,资源附带可直接运行的PyQt图形界面,支持图片批量检测、视频逐帧分析及实时摄像头调用,并提供Dockerfile与环境部署说明,便于在边缘设备或产线工控机上快速部署验证。 每次看到有人把YOLOv5目标检测和Qt界面放在一起做毕业设计或工程项目,我其实挺有感触的。这类项目在工业视觉里非常典型,因为一套完整的检测系统,如果没有图形界面,模型权重训得再好,到产线或者实验室验收时,也会显得缺乏"最后一公里"的交付感。我之前接触过的钢材表面缺陷检测基本上就是这个套路:用YOLOv5训练出一套能识别划痕、麻点、氧化皮等缺陷的权重,再套上一个基于Qt开发的界面,让检测过程不再只是黑乎乎的终端输出。整个过程确实有人觉得容易,有人觉得坑特别多。这篇打算把我实际做过的思路和踩坑记录整理出来,重点聊数据集准备、权重训练、Qt界面封装这三个大块,以及它们之间衔接的细节。希望能给正在做YOLOv5钢材缺陷检测,或者正卡在Qt部署环节的朋友一点参考。
1. 钢材缺陷检测项目全貌:为什么偏偏是YOLOv5配Qt
1.1 项目能做什么,适用场景在哪
钢材表面缺陷检测,通俗点说就是通过工业相机拍摄热轧钢带或钢板表面,用深度学习模型对图像中的缺陷目标进行定位和分类,再通过软件的界面把检测结果实时展示出来。这个项目最常见的交付形态,就是一套可以点开运行的桌面程序:左边是图像显示区域,右边是检测结果的列表和统计信息,下方有"打开图片""开始检测""相机采集""保存结果"之类的按钮。
我实际做过的版本主要是针对钢铁厂平整分检线、卷材表面质检这类场景做的原型验证。钢带在运动过程中,表面会出现不同的瑕疵,比如:
- 轧制划痕:平行于轧制方向,细长条状,颜色深浅不一。
- 麻点:密集分布的小点状凹陷,通常呈现成群出现。
- 氧化铁皮压入:呈现不规则的块状,颜色比周边暗,多伴随在钢材冷却过程中压入。
- 夹杂:深浅不一的点状或块状颗粒。
- 裂纹:形状不规则的折线状,危害等级较高。
这套方案能做的事情,就是把原始图像输入给模型,模型输出每个缺陷框的类别、坐标和置信度,然后程序把这些结果画在图像上,同时生成统计报告。适合的场景不止是钢企质检站,实验室做材料表面评估、科研人员做算法对比这类场景也用得上。从岗位技能讲,它覆盖了环境配置、模型训练、代码工程化、界面开发这几个维度,所以会成为很多工程类课题和简历项目的首选。
1.2 为什么选YOLOv5而不是YOLOv8或Faster R-CNN
这几年新出的检测模型非常多,YOLOv8、YOLOv9甚至YOLOv11都已经很成熟了。但在我做的钢材缺陷检测这个具体需求里,仍然倾向于继续用YOLOv5,原因不是追求最先进,而是要平衡几个实际条件。
第一,格式转换和权重兼容性问题。YOLOv5的PyTorch权重转为ONNX再用OpenCV的DNN模块或者ONNXRuntime来推理,社区里已经有非常成熟的方案,几乎不会有坑。而YOLOv8虽然推理代码也不复杂,但其输出的格式和YOLOv5在某些版本上有细微区别,处理Anchor或DFL解码时要额外写不少代码。对于Qt界面集成来说,稳定大于花哨。
第二,钢材缺陷数据集的标签风格与YOLOv5契合度很高。钢材缺陷数据集里的目标相对紧凑,缺陷的长宽比变化很大(比如划痕可能是狭长矩形),YOLOv5的Anchor机制可以通过聚类适配这些形状。很多公开的钢材缺陷数据集(比如NEU-DET)早期版本附带的示例配置就直接对标YOLOv5,用起来顺理成章。
第三,硬件资源与推理实时性。很多项目最终要跑在工控机上,这类机器往往只有CPU或者一张入门级显卡。YOLOv5s可以在i5处理器的CPU上跑到接近实时(每秒十几帧,取决于图像分辨率),这在演示和验证阶段已经非常够用。YOLOv8在相同条件下推理速度略慢一点,并非不可接受,但调试资料相对少。既然目标是交付一套能用的界面,不是刷排行榜,YOLOv5就是那个"不太容易出错"的选择。
注意:这里并非说YOLOv8不好。如果你对最新机制有兴趣,YOLOv8完全没问题。但如果你想快速做出一套带Qt界面的可演示检测系统,YOLOv5的生态成熟度是最高的。
2. 数据集准备与整理:钢材缺陷样本的底子决定模型上限
2.1 使用公开数据集还是自制数据
钢材表面缺陷检测最常用的公开数据集是NEU-DET(东北大学表面缺陷数据库)。它包含1800张热轧带钢表面的灰度图像,每张尺寸是200x200像素,涵盖六类缺陷:Rolled-in Scale(氧化皮压入)、Patches(麻点)、Crazing(裂纹)、Pitted Surface(麻点/凹坑)、Inclusion(夹杂)、Scratches(划痕)。
这里有个容易踩坑的点:NEU-DET的标签数据并不都是标准的YOLO格式。早期版本给的是XML文件(Pascal VOC格式),需要自己转换成YOLOv5需要的txt标注(每行:类别id x_center y_center width height,所有坐标均归一化到[0,1])。而网上下载的某些转换后版本,可能因为转换脚本问题出现坐标错位。所以我建议不要直接拿着网上的成品txt训练,最好自己走一遍格式转换和可视化确认。
2.2 标签格式转换与数据集划分实操
以NEU-DET为例,我一般这样处理:
- 确认原始图像和XML文件路径。
- 写一个脚本解析XML中的
object节点,提取name和bndbox坐标(xmin, ymin, xmax, ymax)。 - 将xmin、ymin、xmax、ymax转换为YOLOv5格式:
import xml.etree.ElementTree as ET import os def convert_voc_to_yolo(xml_path, out_txt_path, class_names): tree = ET.parse(xml_path) root = tree.getroot() img_width = int(root.find('size/width').text) img_height = int(root.find('size/height').text) lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in class_names: continue class_id = class_names.index(name) bbox = obj.find('bndbox') xmin = int(bbox.find('xmin').text) ymin = int(bbox.find('ymin').text) xmax = int(bbox.find('xmax').text) ymax = int(bbox.find('ymax').text) x_center = (xmin + xmax) / 2 / img_width y_center = (ymin + ymax) / 2 / img_height w = (xmax - xmin) / img_width h = (ymax - ymin) / img_height lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(out_txt_path, 'w') as f: f.write('\n'.join(lines))- 将整个数据集按
train:val:test = 8:1:1划分。划分时建议用脚本按缺陷类别做分层抽样,而不是简单随机切分,否则可能出现某个类别在验证集中数量极少的情况。
数据集划分需要注意:钢材缺陷图像往往一个样本包含多个目标,而且目标分布不平衡。像**裂纹(Crazing)和夹杂(Inclusion)**这类缺陷出现频率较低,如果随机划分,训练时这些类别的loss会一直偏高。我在做的时候会提前统计每个类别在训练集和验证集中的数量,确保比例大致一致。
2.3 数据增强的注意点:灰度图别乱转RGB
NEU-DET图像原本是灰度图,但很多代码习惯用三通道读图。YOLOv5在训练时会对图像做归一化,但不会因为输入是单通道就自动复制成三通道。我的做法是:在读取时统一转成三通道RGB,每个通道数值相同即可。这种操作不会丢失信息,还会避免后续模型输入维度不对的报错。
关于增强策略,YOLOv5自带HSV扰动、随机翻转、Mosaic增强等。对钢材表面缺陷来说,以下几点需要特别注意:
- 不要用太过激的旋转增强。钢材缺陷中划痕的方向是有物理意义的,尤其平行于轧制方向的划痕是主要缺陷形态。如果旋转90度或大角度旋转,可能会让模型学到错误的语义。
- Mosaic增强对钢材缺陷非常有用。200x200的小图本身分辨率低,Mosaic可以把四张图拼在一起变相扩大感受野和背景多样性,但训练时要注意mosaic概率不要设得太高,比如0.5~0.7之间比较合适,否则模型对真实图像的形态分布适应会变差。
- blur增强可以适度加。工业相机在产线上运动拍摄时会有运动模糊,简单的高斯模糊或均值模糊模拟这种场景,能提升泛化能力。
3. 模型训练与权重生成:YOLOv5训练钢材缺陷的核心步骤
3.1 环境搭建与目录组织
YOLOv5训练环境建议直接用官方仓库,我一般用:
- Python 3.8 ~ 3.10
- PyTorch 1.8 ~ 2.1,带CUDA(如果机器有NVIDIA显卡)
- opencv-python、numpy、matplotlib、pandas、seaborn、tensorboard 等依赖
安装训练依赖很简单:
git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt数据集目录结构按YOLOv5约定来组织,这也是最容易出错的地方。YOLOv5在训练时需要数据配置yaml文件,指向图片和标签的路径。
我常用的钢材缺陷数据目录结构如下:
steel_defect_dataset/ ├── images/ │ ├── train/ │ │ ├── 1.jpg │ │ └── ... │ └── val/ │ ├── 2.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── 1.txt │ │ └── ... │ └── val/ │ ├── 2.txt │ └── ... └── steel_defect.yamlsteel_defect.yaml的内容:
train: ./steel_defect_dataset/images/train val: ./steel_defect_dataset/images/val nc: 6 names: ['crazing', 'inclusion', 'patches', 'pitted_surface', 'rolled_in_scale', 'scratches']这里有个小经验:训练时建议把标签和图片放在同一根目录下分属两个文件夹,而不是放在datasets目录里让YOLOv5自动创建。这样排查路径问题更直观。如果标签与图片不一致(比如某张图没有对应txt),YOLOv5训练时会自动忽略空标签,但很多缺陷样本其实是有标签的,如果忽略了会极大影响训练效果。我通常会在训练前用脚本跑一遍全量检查,确保每张图都有对应的txt文件,且txt内容不为空。
3.2 选择预训练权重还是从头训练
YOLOv5官方提供了五个预训练模型:yolov5n、yolov5s、yolov5m、yolov5l、yolov5x。它们的参数量和推理速度依次增加,精度一般也依次略好。
对于钢材缺陷检测,我一般按照以下逻辑选择:
| 模型 | 参数量 | 推理速度 | 适用场景 |
|---|---|---|---|
| yolov5s | 较小 | 快 | 大多数原型验证和工控机部署 |
| yolov5m | 中等 | 中 | 需要提点且算力不紧张时 |
| yolov5l/x | 大 | 较慢 | 追求极致精度、离线检测、GPU算力充足 |
我实际测试过,NEU-DET这种小目标密集的数据集,用yolov5s预训练权重微调,mAP能达到0.75以上(视划分方式有波动);用yolov5m可以达到0.80左右。再往上提升有限,但推理时间成倍增加。如果是做界面演示,yolov5s已经足够,部署到CPU上也能流畅运行。
选择预训练权重时有一个细节:YOLOv5官方仓库里下载的yolov5s.pt是基于COCO数据集80类训练的。它的特征提取层(Backbone)通用性强,可以直接用来微调钢材缺陷类别。训练时关键词是--weights yolov5s.pt。如果不加预训练权重,而是用--weights '',模型从头训练,收敛会慢很多,且最终精度普遍偏低。
3.3 训练参数调整与过程监控
训练命令参考如下:
python train.py --img 640 --batch 16 --epochs 150 --data steel_defect.yaml --weights yolov5s.pt --device 0 --project ./runs/train_steel --name exp_steel几个参数说明:
--img 640:虽然原图是200x200,但YOLOv5会resize到640x640训练。这个操作有点反直觉,因为把低分辨率图放大到640,其实并没有增加信息量,只是把像素插值放大了。如果想要速度更快,可以试试--img 320。实测下来,NEU-DET在640输入下AP会高一点点,但模型对微小缺陷的定位能力提升有限,因为原图分辨率本身就低。如果你的图像源是产线相机,分辨率可能达到1000~2000像素以上,这种情况下建议让--img尽量接近原图分辨率(比如1280),否则小目标缺陷很难被检测出来。--batch 16:如果显存不够,可以调小到8或4。YOLOv5对batch size比较敏感,太小的batch会导致BN层的统计量不稳定,loss波动明显。如果不改其他参数,只把batch调小,可以考虑同时开启--label-smoothing 0.05来稳定训练。--epochs 150:NEU-DET图片少,一般100~150轮足够。如果加了很多数据增强,可以适当增加轮数。--device 0:指定显卡,CPU则用--device cpu,但训练巨慢不建议。
训练过程中最重要的事情是盯loss曲线和验证集mAP曲线。YOLOv5默认输出results.png和TensorBoard日志。一种常见的失败模式是:训练轮数一多,验证集mAP在某个epoch之后下降或震荡,而训练集loss还在下降。这说明模型过拟合——本来NEU-DET数据就只有1800张图,过拟合是常态。解决途径包括:
- 调低epochs,设置
--patience 30让模型在验证集停止提升时早停。 - 增加数据增强的权重,尤其是随机翻转和HSV扰动,让模型不容易记住训练图像的细节。
- 换成更小的模型,比如从yolov5s换成yolov5n。
训练完成后,在runs/train_steel/exp_steel/weights/下会有best.pt和last.pt。尽量选择best.pt作为最终部署权重。
3.4 权重转换:从PyTorch到ONNX再到Qt可加载的格式
在Qt界面中加载YOLOv5模型,有几种主流方式:
- 直接加载PyTorch权重(.pt):需要界面环境中安装PyTorch,并且通过
torch.load加载模型。这样方便,但会让程序体积变大,且部署环境必须装齐依赖。 - 转为ONNX,再用ONNXRuntime推理:推荐。ONNX模型体积小,跨平台好,CPU加速效果也不错,且不需要再依赖PyTorch就可以加载。
- 转为OpenCV DNN支持的格式(.onnx或.pb):通过OpenCV的
dnn.readNetFromONNX读取ONNX文件,完全脱离深度学习框架。但YOLOv5的输出层有很多后处理逻辑,写到OpenCV里代码会稍微繁琐一些。
我自己的方案是导出ONNX,并在Qt中用ONNXRuntime做InferenceSession。这样界面程序只依赖opencv和onnxruntime,启动快,部署也干净。
导出命令:
python export.py --weights runs/train_steel/exp_steel/weights/best.pt --img 640 --include onnx这里有个长期以来的坑:YOLOv5的export.py在导出ONNX时,输出层是三维的(形状类似于1x25200x85),需要自己解码。25200是三个尺度特征图(80x80、40x40、20x20)的anchor总数量,85是5个框属性(x,y,w,h,confidence)加80个类别概率。钢材缺陷只有6类时,这个维度会变成1x25200x11。我们可以直接用模型输出shape来得到类别数,或者干脆在导出后把原始shape用脚本打出来确认。
ONNX导出成功后,用Python快速验证一下:
import cv2 import onnxruntime as ort import numpy as np session = ort.InferenceSession('best.onnx', providers=['CPUExecutionProvider']) input_name = session.get_inputs()[0].name input_shape = session.get_inputs()[0].shape print(f"input: {input_name}, shape: {input_shape}") output_names = [o.name for o in session.get_outputs()] print(f"outputs: {output_names}")确认无误后再开始写Qt界面的推理代码。
4. Qt界面开发:把检测权重变成一个能操作的程序
4.1 界面功能设计与整体布局
Qt界面不一定要做得多炫,但功能要讲清楚。以钢材缺陷检测为例,我设计的界面方案是这样的:
- 左侧区域:图像显示控件(QLabel或自定义QWidget),用于显示原始图像和检测后的结果图像。
- 右侧区域:
- 类别信息表格(QTableView或QTableWidget),列出每帧图像中检测到的缺陷类别、坐标、置信度。
- 统计标签,比如当前检测到多少缺陷、平均置信度是多少。
- "打开图片""开始检测""相机采集""保存结果""退出"等按钮。
- 状态栏:显示当前模型加载状态、推理耗时、FPS等信息。
界面布局推荐用QHBoxLayout和QVBoxLayout组合,控制在500-1000行代码量内。如果工程实际有大图显示需求,建议重写一个QImageWidget,在paintEvent中绘制图像和检测框,而不是直接用QLabel加pixmap缩放,因为QLabel在实时刷新时容易闪烁。
4.2 Qt中加载ONNX模型与推理
在Qt里使用ONNX模型,核心依赖是onnxruntime C++库,或者用Python版Qt(PySide2/PyQt5)直接调用onnxruntime的Python接口。
用Python写Qt界面是最快的开发路径:
import sys import cv2 import numpy as np import onnxruntime as ort from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget, QFileDialog, QTableWidget, QTableWidgetItem, QSplitter from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import Qt, QTimer class SteelDefectWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("YOLOv5钢材缺陷检测系统") self.session = ort.InferenceSession("best.onnx", providers=['CPUExecutionProvider']) self.input_name = self.session.get_inputs()[0].name # 加载类别名 self.class_names = ['crazing', 'inclusion', 'patches', 'pitted_surface', 'rolled_in_scale', 'scratches'] # ... 后续初始化UI这里有几个容易错的地方需要提醒:
- ONNX模型输入尺寸是固定值,比如640x640。在推理前,需要将任意尺寸的输入图像按比例缩放并填充到640x640,而不是直接resize成640x640,否则目标比例会失真,检测精度明显下降。
- 图像归一化:YOLOv5导出ONNX时,输入需要除以255,并且要求通道顺序为RGB。如果在Qt里用OpenCV读图(默认BGR),要先做
cv2.cvtColor(img, cv2.COLOR_BGR2RGB)再归一化。 - 建议把resize时的缩放比例记录下来,推理后把检测框坐标映射回原始图像尺寸时要用。
推理核心代码示例:
def detect(self, frame_bgr): h, w = frame_bgr.shape[:2] letterbox_img, ratio, (dw, dh) = self.letterbox(frame_bgr, 640) img_rgb = cv2.cvtColor(letterbox_img, cv2.COLOR_BGR2RGB) img_norm = img_rgb.astype(np.float32) / 255.0 blob = np.transpose(img_norm, (2, 0, 1))[None] outputs = self.session.run(None, {self.input_name: blob}) preds = self.postprocess(outputs[0], ratio, dw, dh, (w, h)) return preds后处理中要实现的NMS(非极大值抑制)代码相对固定,包括:置信度阈值过滤、每个框坐标还原、类别置信度最高的类作为最终类别、用cv2.dnn.NMSBoxes抑制重复框。这部分建议直接参考YOLOv5官方utils/general.py里的non_max_suppression函数逻辑重新实现一版,注意输入输出是numpy数组而不是torch张量。
4.3 线程卡顿问题:别把推理放在UI主线程
新手做Qt界面最容易犯的错就是:点击"开始检测"按钮后,直接在主线程里写一个while True循环去推理,结果界面卡死,无法拖动窗口,也无法点击"停止"按钮。这是Qt事件循环被阻塞导致的。
正确处理方法是使用QThread或QTimer:
- 方案一(适合实时视频/相机流):新建一个
QThread子类,重写run()方法,在其中循环读帧、推理、通过信号finished_frame(QImage)发送结果回主线程更新界面。 - 方案二(适合单张图片/离线检测):用
QThreadPool和QRunnable做一次性任务,点击按钮时开启任务,结束后通过信号通知。
我最常用的是QThread方案,因为钢材缺陷检测项目在展示时常需要接摄像头或者录好的视频文件。简单示意:
class DetectThread(QThread): change_pixmap = pyqtSignal(QImage) update_table = pyqtSignal(list) def __init__(self): super().__init__() self.running = True def run(self): cap = cv2.VideoCapture(0) # 或视频文件路径 while self.running: ret, frame = cap.read() if not ret: break detections = self.detect(frame) result_frame = self.draw_detections(frame, detections) rgb_image = cv2.cvtColor(result_frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape bytes_per_line = ch * w qimage = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) self.change_pixmap.emit(qimage) self.update_table.emit(detections) cap.release()写线程时需要注意:不要在子线程里直接操作UI控件,必须使用信号槽机制更新界面。这是Qt的铁律,违反它会随机崩溃或出现诡异刷新问题。
4.4 检测结果的可视化与表格输出
检测结果的绘制我一般用OpenCV完成,在原生图像上画矩形框、类别名和置信度。颜色根据类别分配:划痕类用红色,裂纹类用蓝色等。这里有一个小技巧:如果钢材图片是灰度图,画框后最好加一层伪彩色映射或把背景稍微提亮,否则红绿蓝的检测框在灰底图上区别不大。
表格输出方面,用QTableWidget设置行列:
self.table.setColumnCount(5) self.table.setHorizontalHeaderLabels(["类别", "x_min", "y_min", "x_max", "y_max", "置信度"])注意上面的列数其实写的是5,但表头有6个标签,写代码时务必保持一致。这类小问题目测根本没法发现,只有跑起来报错才意识到。我在实际项目里遇到过一次,当时在初始化时写反了,结果表格一直显示异常,检查了半天才发现是列数定义不对。
如果想把检测结果导出,建议保存为CSV或者JSON,方便后面做报表统计。Qt里可以用QFileDialog.getSaveFileName让用户选路径,再把表格内容逐行写入文件。
4.5 界面运行环境与打包部署
开发完成后,如果要交付给别人用,建议用PyInstaller打包成exe(Windows)或可执行文件(Linux)。打包命令大致如下:
pyinstaller -w -F main.py --hidden-import=onnxruntime有几个打包经验:
- 如果直接用
--hidden-import=onnxruntime还不够,可能还需要指定--collect-all onnxruntime,因为onnxruntime里的动态链接库和proto文件比较多。 - 图像文件、模型文件、UI文件等要放在相对路径下。打包后程序的工作目录可能和开发时不同,建议在代码里用
sys.path[0]或Path(__file__).parent定位资源路径。否则会报找不到best.onnx。 - 如果界面用到了中文字体,打包后在某些精简系统上可能显示乱码。可以设定一个使用QSS样式表包含常用字体,或直接指定微软雅黑。
5. 实际运行中的性能观察与典型问题排查
5.1 推理耗时对比:CPU和GPU的差距
我用yolov5s的ONNX模型,在640x640输入下测过一次推理耗时:
- Intel i5-10400(CPU),ONNXRuntime:约120~180ms/帧,大约每秒6-8帧。
- NVIDIA GTX 1660 Super(GPU),ONNXRuntime:约15~25ms/帧,每秒40-60帧。
如果想在CPU上把速度提上去,可以尝试量化。ONNX模型可以转成int8量化模型,速度能提升不少,但精度会有损失。钢材缺陷检测里,置信度比较低的微弱缺陷在量化后更容易漏检,所以建议先测清楚再决定是否量化。
另一个常见优化是输入尺寸减半。比如从640x640降到416x416甚至320x320,推理时间能明显下降,但小目标(比如麻点群中的单点缺陷)可能会丢失。如果界面主要处理的是产线上200x200的原始小图,其实320x320输入是很好的折中选择,因为原图分辨率在那里,放大到640并不会增加细节。
5.2 缺陷漏检率偏高的原因分析
在钢材表面缺陷检测的实战中,漏检率往往是用户最关心的指标。我遇到过的漏检场景主要有三种:
- 低对比度缺陷:像很浅的划痕,肉眼都看不清,模型更难。这种情况建议在预处理阶段加入对比度增强(比如CLAHE),提高缺陷与背景的灰度差异。
- 遮挡或重叠目标:多个缺陷靠得很近时,NMS会抑制掉部分框。可以通过降低NMS IoU阈值(比如从0.5降到0.3)来保留更多候选框。
- 训练集样本不足:NEU-DET每类只有300张图,个别类别形态变化有限。如果项目要求更高,建议自己补充产线采集的多样本,这样效果提升会很明显。
5.3 Qt界面运行时的卡顿与内存泄漏
在Qt界面中,推理线程持续运行,如果每帧都创建大量临时对象(如cv::Mat副本、numpy数组),可能导致内存占用不断上升。一个惨痛教训是:早期版本的绘图代码在每帧里np.copy原始图像后再画框,结果运行十几分钟后内存涨了几百MB。后来改成在原始帧上直接绘制,只有在发信号时才拷贝一份,内存就稳定了。
如果界面刷新闪烁,可以试试双缓冲机制。Qt的QWidget默认启用了双缓冲,但如果你自定义了paintEvent且绘制耗时较高,建议用QPixmap作为缓冲,先绘制到pixmap,再整个贴到控件上。
还有一点与摄像头相关:如果相机的帧率很高(比如60fps),而推理速度只有10fps,就需要在读取线程里跳帧而不是让队列无限制堆积。否则界面延迟会越来越大,实时性变成"回放系统"。常见的做法是每读3帧取1帧送入推理,队列中只保留最新一帧。
6. 从检测到可视化:完整代码骨架的浓缩版
为了让大家对整体结构有个直观感受,这里我整理一个浓缩版的代码骨架,涵盖界面初始化和推理流程。
# main.py from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QFileDialog, QTableWidget, QVBoxLayout, QHBoxLayout, QWidget, QSplitter, QTableWidgetItem from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import QTimer import sys import cv2 import numpy as np import onnxruntime as ort class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("钢材表面缺陷检测系统") self.setGeometry(100, 100, 1200, 700) self.session = ort.InferenceSession("best.onnx", providers=['CPUExecutionProvider']) self.class_names = ['crazing', 'inclusion', 'patches', 'pitted_surface', 'rolled_in_scale', 'scratches'] self.conf_thres = 0.25 self.iou_thres = 0.45 self.init_ui() self.timer = QTimer(self) self.timer.timeout.connect(self.update_frame) self.cap = None def init_ui(self): # 左:图像显示 self.image_label = QLabel("请打开图片或启动相机") self.image_label.setMinimumSize(640, 640) self.image_label.setAlignment(Qt.AlignCenter) # 右:按钮和表格 btn_open = QPushButton("打开图片") btn_open.clicked.connect(self.open_image) btn_camera = QPushButton("启动相机") btn_camera.clicked.connect(self.start_camera) btn_stop = QPushButton("停止") btn_stop.clicked.connect(self.stop_camera) self.table = QTableWidget() self.table.setColumnCount(6) self.table.setHorizontalHeaderLabels(["类别", "x_min", "y_min", "x_max", "y_max", "置信度"]) self.table.horizontalHeader().setStretchLastSection(True) layout_right = QVBoxLayout() layout_right.addWidget(btn_open) layout_right.addWidget(btn_camera) layout_right.addWidget(btn_stop) layout_right.addWidget(self.table) widget_right = QWidget() widget_right.setLayout(layout_right) splitter = QSplitter() splitter.addWidget(self.image_label) splitter.addWidget(widget_right) splitter.setStretchFactor(0, 4) splitter.setStretchFactor(1, 1) self.setCentralWidget(splitter) def open_image(self): path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "Images (*.png *.jpg *.bmp)") if not path: return frame = cv2.imread(path) detections = self.detect(frame) result = self.draw_detections(frame, detections) self.show_frame(result) self.update_table(detections) def detect(self, frame): # letterbox + 推理 + 后处理,省略具体实现 pass def draw_detections(self, frame, detections): # 绘制矩形框和类别 pass def show_frame(self, result_bgr): rgb_image = cv2.cvtColor(result_bgr, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape bytes_per_line = ch * w qimage = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) self.image_label.setPixmap(QPixmap.fromImage(qimage)) def update_table(self, detections): self.table.setRowCount(0) for det in detections: row = self.table.rowCount() self.table.insertRow(row) class_name, x1, y1, x2, y2, conf = det self.table.setItem(row, 0, QTableWidgetItem(class_name)) self.table.setItem(row, 1, QTableWidgetItem(str(int(x1)))) self.table.setItem(row, 2, QTableWidgetItem(str(int(y1)))) self.table.setItem(row, 3, QTableWidgetItem(str(int(x2)))) self.table.setItem(row, 4, QTableWidgetItem(str(int(y2)))) self.table.setItem(row, 5, QTableWidgetItem(f"{conf:.3f}")) def start_camera(self): self.cap = cv2.VideoCapture(0) self.timer.start(30) def stop_camera(self): if self.timer.isActive(): self.timer.stop() if self.cap: self.cap.release() def update_frame(self): ret, frame = self.cap.read() if not ret: return detections = self.detect(frame) result = self.draw_detections(frame, detections) self.show_frame(result) self.update_table(detections) if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_())这个骨架不是完整可运行的代码,detect和draw_detections需要自己补全。但把界面和推理的逻辑边界理清楚之后,填充细节就很顺手了。
关于detect方法,我再补充一个关键细节:letterbox实现时,填充颜色用灰色(114,114,114),这是YOLOv5的标准做法。填充在右下角还是居中,取决于实现,但推理后坐标映射时,偏移量要跟填充方式对应。我建议统一采用居中填充(左右各pad一半,上下各pad一半),这样映射时不容易出错。
7. 项目实际落地的一点体会
整个YOLOv5钢材缺陷检测加Qt界面的项目,做下来最大的感受是:模型训练不是最难的,边边角角的工程化问题才是最消耗时间的。
比如,数据集的类别名称要保证在训练yaml、标签txt、界面代码三处完全一致。很多人在训练时用的是缩写或数字,到界面里要映射成中文名,结果发现某一处写错了,显示出的类别总是对不上。我的习惯是从一开始就在一个Python字典里维护类别ID到中文名的映射,不论是训练标注、评估结果还是界面表格,都复用这一份映射,避免到处硬编码。
再比如,模型推理时的输入尺寸和letterbox参数,导出ONNX时、Python推理时、Qt推理时三处都要保持一致。如果训练时用的--img 640,导出时用的默认值也是640,但对界面里的resize尺寸不小心写成了416,那么实际输入的是416x416,而ONNX模型的输入层是640x640,运行时要么报维度不匹配,要么自动resize到640但性能受影响。排查这种问题非常耗时,所以我的建议是在所有配置节点显式写明输入尺寸。
如果是做毕业设计或项目演示,建议把界面做得稍微完整一点:比如在检测结果图上显示FPS、模型名称、当前处理的图片路径,甚至可以加一个简单的历史截图功能。这些细节虽然不加技术分,但能让演示效果更饱满。我自己在答辩演示时,就是靠着在界面右下角添加"累计检测缺陷数"、"按类别统计柱状图"这些功能,让评委觉得项目更完整。
如果你下一步想在这个项目基础上扩展,比较合理的路径是:在Qt界面中接入相机的实时拉流(比如GigE工业相机或RTSP流),然后把检测结果上传到MQTT或者本地数据库,形成检测数据看板。这类功能在工业现场往往比模型精度更受关注。当然,这些都建立在基础检测链路已经跑通的基础上——先把模型权重和Qt界面这套链路彻底吃透,后面的扩展就是水到渠成的事。
本文还有配套的精品资源,点击获取