做工业视觉这些年,被问得最多的需求就是“能不能帮我把质检这块用深度学习做起来”。钢材表面缺陷检测就是这个场景里非常典型的一个:人工目检容易疲劳,传统机器视觉又扛不住复杂纹理,而用YOLOv11这类目标检测模型,可以直接把“带缺陷的图片”训练成“能输出缺陷位置和类别”的检测系统。这篇文章把你需要的东西都拆开讲一遍——从数据集的YOLO标注处理,到YOLOv11的训练调参,再到带登录注册的UI界面打包,算是把一整个Python项目的完整链路都过一遍,适合正在做毕设、竞赛,或者想快速验证工业检测方案的开发者参考。
1. 检测目标拆解:钢材表面六类缺陷到底长什么样
1.1 为什么传统视觉方案在钢材质检上行不通
先别急着聊模型,我得先把检测对象说清楚。钢材表面缺陷检测,本质上是“在灰度纹理背景里找到异常区域”。早期做法是走传统图像处理路线:阈值分割、边缘检测、灰度共生矩阵提取纹理特征,再套一个分类器。这套方案在实验室干净样品上跑得挺好,一到产线就露馅——光照变化、氧化皮残留、水渍、油污,都会让阈值和特征失效,维护成本极高。
深度学习方案的思路完全不同:模型从标注数据里自己学“缺陷长什么样”,不需要手工设计特征,对光照和复杂纹理的鲁棒性也明显更好。这也是为什么现在工业视觉项目里,目标检测模型几乎成了默认首选。钢材这类表面纹理复杂的对象,恰好是深度学习最擅长处理的场景之一。
1.2 六类缺陷的形态特征与检测难点
我这次用的是业界很常见的一套钢材表面缺陷公开数据集,共1800余张灰度图像,涵盖6类缺陷,每类300张。虽然单看数量不算大,但缺陷形态差异明显,训练起来很有代表性。六类缺陷分别是:
| 类别 | 典型形态 | 检测难点 |
|---|---|---|
| crazing(裂纹) | 细小网状裂纹,像龟裂 | 纹理细、对比度低,容易被当成背景纹理 |
| inclusion(夹杂) | 颗粒状暗色异物嵌入表面 | 大小差异大,小尺寸夹杂容易漏检 |
| patches(斑块) | 大块不规则灰度区域 | 边界模糊,和正常氧化区域不易区分 |
| pitted_surface(麻坑) | 密集点状凹陷 | 密度高、目标小,容易粘连 |
| rolled-in_scale(氧化皮) | 片状压入的氧化物 | 形态多变,和斑块在视觉上有重叠 |
| scratches(划痕) | 细长条状连续痕迹 | 长宽比极端,常规锚框策略不友好 |
一开始我对这套数据掉以轻心,觉得六分类小数据集,跑个模型还不简单,结果第一轮训练mAP直接教我做人。后来复盘发现,问题根本不在模型,而在数据准备和对缺陷形态的理解上。比如“裂纹”这一类,很多标注框里的目标只有几个像素宽,模型如果默认用大卷积核的感受野去扫,很容易把细纹理当成背景。这类经验你后面训练的时候大概率也会遇到。
1.3 整体系统组成:从训练到交付的完整链路
这个项目最终交付的不只是模型权重,而是一套能直接打开用的桌面系统,整体分两条线:
- 训练端:处理YOLO格式数据集 → 配置YOLOv11模型 → 训练得到best.pt → 导出ONNX或直接保留PyTorch权重。
- 应用端:Python编写登录注册界面 → 验证用户身份 → 进入检测主界面 → 加载模型 → 对上传图片推理 → 绘制检测框并展示结果。
训练端解决“模型性能”,应用端解决“系统可用性”。两者缺一不可。很多初学者拿到best.pt之后不知道下一步怎么接UI,或者吭哧吭哧写完界面却不知道模型怎么塞进去。我下面会按照这个链路逐段拆开,把每一步的实操记录和坑都写出来,尽量让你能照着复现。
2. 数据集工程:标注、划分与增强的实操记录
2.1 原始图像怎么处理成YOLO格式
YOLO系列训练用的标注格式非常固定:一张图片对应一个同名txt文件,文件里每一行代表一个目标框,格式为:
class_id x_center y_center width height注意,这里的坐标全部是归一化到0~1之间的相对值,分子是实际像素坐标除以图片宽高。比如一张640x480的图,某个框左上角在(160, 120),宽320,高240,对应的YOLO行就是:
2 0.5 0.5 0.5 0.5中间的class_id从0开始编号。在这个钢材数据集里,六类缺陷的顺序我固定成了:0-crazing、1-inclusion、2-patches、3-pitted_surface、4-rolled_in_scale、5-scratches。这个顺序一旦定下来,训练和UI显示都必须一致,不然会出现类别错位的奇葩问题。
如果你手里的数据是VOC格式的XML或者COCO格式的JSON,可以用ultralytics仓库提供的格式转换脚本,也可以自己写个几十行的Python转换函数。我的经验是:转换脚本一定要加“可视化校验”这一步,也就是把框画回原图上人工抽查。坐标格式错了模型照样能训练,但收敛非常慢,而且坏得无声无息,这是数据工程里最坑的一类bug。
2.2 训练集/验证集划分与目录组织
YOLOv11通过一个data.yaml文件来告诉训练脚本数据集在哪、类别有哪些。我习惯这样组织目录:
steel_data/ ├── data.yaml ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/data.yaml的内容大概长这样:
path: ./steel_data train: images/train val: images/val nc: 6 names: ["crazing", "inclusion", "patches", "pitted_surface", "rolled_in_scale", "scratches"]划分比例我用的是8:2,从每类缺陷中均匀抽20%作为验证集。这里有个很容易犯的错:直接random.shuffle所有图片,导致某一类缺陷在验证集里数量过少,评估结果波动巨大。按类别分层抽样,保证验证集里每个类别都有足够的样本,评估才有参考价值。
2.3 数据增强策略与在线增强的关系
钢材缺陷检测跟一般物体检测有个不同点:缺陷区域往往占整图比例小,而且形态高度依赖纹理细节。数据集只有1800多张图,不做增强很难训出稳定的模型。
YOLOv11自带的在线增强已经很成熟,训练时默认开启Mosaic和MixUp等策略。Mosaic会把四张图拼在一起喂进模型,相当于变相增大了batch内的目标数量和场景多样性,对小目标训练有明显帮助。我在训练时开启了Mosaic 1.0,同时在最后的epoch会逐渐关闭Mosaic,让模型适应真实的单图分布。
除了在线增强,离线增强我也做了一部分——对图像做小角度的旋转、平移和缩放,但这里得提醒一句:钢材缺陷的方向性很强,比如划痕就是细长条,盲目做90度或180度旋转会把“划痕”变成“横向划痕”之外的畸形样本。我最终只用了正负10度以内的微旋转,避免破坏缺陷本身的形态语义。
2.4 一个影响收敛的细节:灰度图三通道化
这套钢材数据集原始图像是单通道灰度图,而YOLOv11的输入层默认是3通道。ultralytics框架会自动把单通道图复制成3通道,但你最好还是心里有数:模型学习的是“灰度纹理差异”,而不是“颜色差异”。这其实是好事——钢材产线用黑白工业相机居多,灰度图训练的模型部署到黑白相机场景下不需要做颜色适配。
另外一个容易被忽略的点是:训练和推理时的图片尺寸要对齐。我全程使用imgsz=640,如果训练用640,推理却用1280,模型对目标尺度的感知会偏移,mAP直接打折扣。类似这种“数据不一致”的问题,在工程里往往比模型结构更影响结果。
3. YOLOv11选型与训练调参:把模型压到工业可用的状态
3.1 为什么在众多模型里最终选了YOLOv11
YOLO系列我已经用了好几代,从v5到v8再到v11。这次选YOLOv11,核心理由有三个:
- 结构上的C3k2和C2PSA模块,在保持推理速度的同时进一步提升了特征提取能力,尤其对中等尺寸目标更友好。
- Anchor-Free解耦检测头,让正负样本分配更合理,对“细长条划痕”这类极端宽高比的目标,比老版本Anchor机制更好调。
- ultralytics生态相当成熟,训练、验证、导出一条龙,适合快速落地。
当然,YOLOv11不是万能的。如果你追求极致精度、不差推理时延,换成更大体量的模型或两阶段检测器也会有收益。但在“精度、速度、部署成本”三角权衡下,YOLOv11对钢材缺陷这个场景是综合分最高的选择。模型型号我选的是yolo11m,中等版本,参数量和显存占用都适中,既能装在普通显卡上训练,之后部署到边缘设备也不至于跑不动。
3.2 训练环境与关键超参数配置
训练环境我按这套组合来配,兼顾稳定性和性能:
- Python 3.10
- PyTorch 2.x(CPU/GPU版本根据自己机器选)
- ultralytics(版本号我锁了一个当时稳定的版本,避免大版本API变动)
- 显卡是入门级独立显卡,显存8GB左右
训练脚本其实非常简单:
from ultralytics import YOLO model = YOLO("yolo11m.pt") model.train( data="steel_data/data.yaml", epochs=200, imgsz=640, batch=16, optimizer="AdamW", lr0=0.01, lrf=0.01, patience=30, cache=True, )说几个关键参数为什么这么设:
- batch=16:8GB显存在imgsz=640下,yolo11m大概只能跑到这个值。batch过小BN层统计不稳定,过大显存爆掉,16属于起步安全区。
- optimizer=AdamW:小数据集上AdamW收敛比SGD省心,后面几轮想要压榨精度可以切SGD,但我实测差距不大。
- patience=30:连续30轮验证集指标不提升就早停,防止无效训练把时间耗干。
- lrf=0.01:学习率从0.01线性衰减到万分之一,避免后期震荡。
训练过程输出里重点关注三个指标:box_loss、cls_loss、dfl_loss。如果box_loss持续下降但dfl_loss在中间就平台,很多情况是细长目标的边界回归不够,可以在后续增大一些数据增强的scale参数。
3.3 训练过程的观察点:loss曲线、mAP变化与过拟合判断
200个epoch对1800张图来说偏多,所以我依赖早停机制,实际在120轮左右就停了。观察训练过程时我做了这样几件事:
- 每20轮把best.pt在验证集上跑一遍,可视化输出,直接看框的贴合度。裂纹和划痕这类细长缺陷,就算mAP数字在涨,框贴合度也可能很差,必须肉眼确认。
- 看验证集的mAP50和mAP50-95差距。两者的gap大,说明模型在“容易判定的缺陷”上自信,但对难样本(边界模糊、小目标)还是很挣扎。我训完的mAP50在85%到90%区间,mAP50-95在60%出头,对这个小数据集来说算正常水平。
关于过拟合判断:只看训练loss下降说明不了问题,要看验证集loss是否跟着下降。如果验证loss在中间回升,而训练loss还在降,就是明确的过拟合信号。这时候优先调低数据增强强度,或者减小模型体量,而不是盲目加epoch。
3.4 常见训练坑与应对(类别不均衡、小目标漏检、样本重叠)
训练过程中实际踩到三个比较典型的坑。
第一个是类别不均衡。这套数据每类300张还算均衡,但如果你自己建的数据集,某类缺陷特别少,模型会明显偏科。最简单的应对是欠采样或过采样,也可以在loss里给少数类提高权重。ultralytics本身没直接暴露这个参数,我一般直接做离线重采样,效果最可控。
第二个是小目标漏检。麻坑和细小夹杂,尺寸小、数量多,模型容易整片漏掉。我的应对是把imgsz从640提到800试试,在显存允许的情况下,这个调整对小目标改善非常直观。不过要注意推理时也用800,否则训练推理不一致,白折腾。
第三个是密集目标的NMS误抑制。有些缺陷一片区域内密密麻麻,预测框重叠严重,NMS阈值设太高就会把真实目标压掉。我用的是默认iou=0.7,如果密集场景多,可以适当降低到0.5~0.6,让重叠的框保留更多候选,再做二次筛选。
训练完成后,从runs/train/exp目录下拿到weights/best.pt,这才是后面要部署的模型文件。不要用last.pt,除非你明确知道best.pt已经过拟合而last还在正常训练。
4. 模型导出与推理封装:让权重文件变成能稳定调用的服务
4.1 权重选择:best.pt还是last.pt
训练结束后weights目录下会同时出现best.pt和last.pt。best.pt是验证集性能最优的权重,last.pt是最后一轮的权重。绝大多数情况下部署用best.pt,因为它承受住了验证集的检验。有些教程说last.pt泛化更好,我的实测是:在小数据集上best.pt照样比last.pt靠谱,别在这个问题上纠结。
拿到best.pt之后,可以直接用ultralytics的YOLO类加载推理,但这种用法在工程上有个问题——每次推理都要重新实例化模型,浪费初始化的时间。更合理的做法是封装一个单例检测器,初始化一次,反复调用。
4.2 ONNX导出的参数细节与推理加速
PyTorch权重在CPU推理时速度一般,且部署环境不一定装得了PyTorch,所以我把模型导出为ONNX,推理时用ONNXRuntime,速度和兼容性都会更好。导出命令一行搞定:
from ultralytics import YOLO model = YOLO("runs/train/exp/weights/best.pt") model.export(format="onnx", opset=12, imgsz=640, dynamic=False)导出后目录里会多出best.onnx。这里解释几个参数:
- opset=12:老一点的环境也兼容,ONNXRuntime对opset12支持稳定,我实测不报错。
- dynamic=False:固定输入尺寸为640x640,换来的是更高的算子融合优化。如果你一定要支持任意尺寸输入,才考虑dynamic=True,但会牺牲一部分性能。
- 导出后建议用onnxruntime-gpu跑推理,CPU和GPU的推理耗时差距能到几倍。我的实测数据是:一张640x640的钢材图,CPU约200毫秒,入门级独立显卡约20毫秒,这个量级足以支撑离线质检和低速在线抽检。
用ONNXRuntime加载模型也很直接,但要注意模型输出的解析。YOLOv11的ONNX输出是一个形状为(1, 84, 8400)的张量(6个类别时,前4个元素是框坐标,后80个是类别得分,8400是不同尺度特征图铺平后的候选框数量)。你需要自己解析坐标,再做NMS。
4.3 推理后处理:NMS与坐标还原
解析逻辑其实不复杂:把输出的第1维拆成“坐标部分”和“类别部分”,坐标前两位是中心点x、y,后两位是w、h,注意这些值在ONNX里往往是640尺度下的绝对值,要除以640归一化或者直接按比例映射回原图尺寸。
接着对每个类别做一次阈值过滤和NMS。我常用的是IoU阈值0.45、置信度阈值0.25的组合,钢材缺陷虚检比较多的话,可以把置信度拉到0.35。下面是核心代码片段:
import numpy as np import cv2 def post_process(output, conf_thres=0.25, iou_thres=0.45, img_shape=(640, 640)): preds = output[0] # (84, 8400) preds = np.transpose(preds, (1, 0)) # (8400, 84) boxes = preds[:, :4] class_scores = preds[:, 4:] class_ids = np.argmax(class_scores, axis=1) scores = np.max(class_scores, axis=1) valid = scores > conf_thres boxes, class_ids, scores = boxes[valid], class_ids[valid], scores[valid] # 还原到输入尺寸 boxes[:, 0] -= boxes[:, 2] / 2 boxes[:, 1] -= boxes[:, 3] / 2 indices = cv2.dnn.NMSBoxes( boxes.tolist(), scores.tolist(), conf_thres, iou_thres ) # 根据indices筛选即可 return boxes[indices], class_ids[indices], scores[indices]这只是示意代码,实际用的时候要注意输出维度在不同ultralytics版本里可能不一样。我建议你导出后先用一张测试图跑一遍模型,打印output.shape,再对着解析,不要盲抄。
4.4 封装检测器类:输入图片输出结构化结果
为了让后续UI调用更舒服,我写了一个Detector类,核心设计是“对外只暴露两个方法,内部屏蔽模型细节”:
class SteelDetector: def __init__(self, onnx_path, conf_thres=0.25, iou_thres=0.45): self.session = ort.InferenceSession(onnx_path, providers=["CUDAExecutionProvider", "CPUExecutionProvider"]) self.conf_thres = conf_thres self.iou_thres = iou_thres def detect(self, image: np.ndarray): resized = self._preprocess(image) output = self.session.run(None, {self.input_name: resized})[0] boxes, class_ids, scores = post_process(output, self.conf_thres, self.iou_thres) return self._scale_boxes_to_original(boxes, image.shape, resized.shape[2:]) def draw(self, image, boxes, class_ids, scores): # 绘制检测框、类别标签和置信度 ...这里有个前后端分离的思想:Detector负责“拿到图片、返回结果”,UI只负责“展示结果”。这样后面即使把ONNX换成TensorRT,或者换一个检测模型,UI代码一行都不用改。
5. 登录注册界面与检测主界面:把模型包装成能交付的系统
5.1 UI技术选型:为什么用PyQt5而不是Web方案
训练和模型封装都搞定了,接下来是让系统“有人样”。UI技术我对比过两个方向:Web前端(Flask + 浏览器页面)和桌面应用(PyQt5)。
Web方案的好处是布局灵活,浏览器里显示方便,远程访问也容易,但坏处是你得维护一个后端服务,还要考虑端口占用、浏览器兼容等一堆杂事。对一个单机质检工具来说,PyQt5更合适:启动即用、不依赖网络、图片拖拽上传顺手,而且和OpenCV、matplotlib的配合非常自然。
UI界面我整体分两大部分,一是登录注册窗口,二是检测主窗口。代码结构上用两个独立的类来写,通过一个全局登录状态来控制窗口切换。
5.2 登录注册模块:SQLite存储、密码哈希与会话保持
用户信息存储选了SQLite,Python自带sqlite3模块,零配置,适合桌面应用。建表语句很简单:
CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, salt TEXT NOT NULL, password_hash TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );密码绝对不能明文存。我用的是加盐PBKDF2哈希,每次注册时生成随机盐,把盐和哈希值分开存。校验登录时用相同的盐重新计算哈希,比对字符串即可:
import hashlib, os def hash_password(password: str, salt: bytes): return hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 100000).hex() def register(username, password): salt = os.urandom(16) pwd_hash = hash_password(password, salt) # 写入数据库,salt转hex存储登录注册界面的交互逻辑比较常规:注册时检查用户名是否重复、密码长度是否满足要求;登录时校验哈希;登录成功后close登录窗口,实例化主窗口。这里有一个小细节——如果用sys.exit(app.exec_())作为主循环,切换窗口的方式最好用main_window.show()然后把自己close(),不要destroy()整个QApplication,否则窗口切换会有控制台报错,甚至直接闪退。
5.3 检测主界面设计:图片上传、结果绘制、统计看板
检测主界面我用QMainWindow搭了一个经典布局:
- 左侧区域:图片显示区,支持“打开图片”按钮上传本地图片,也支持“摄像头截图”入口(有需要的话可以扩展)。
- 右侧区域:QTableWidget结果表格,实时显示检测类别、置信度、位置坐标。
- 底部区域:统计标签,汇总当前图片每一类缺陷的数量。
上传图片后,点击“开始检测”,系统把图片交给Detector处理,拿到检测框、类别、置信度后,在图片上绘制矩形框和标签。绘制我直接用OpenCV的rectangle和putText,然后转成QPixmap显示到QLabel上:
# 在Detector.draw内部 for box, cls_id, score in zip(boxes, class_ids, scores): x1, y1, x2, y2 = box.astype(int) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) label = f"{CLASS_NAMES[cls_id]} {score:.2f}" cv2.putText(img, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)这里必须多说一句:推理是耗时操作,千万不能放在UI线程里直接跑,否则界面会“卡死”。我用了QThread,在后台线程执行检测,检测完成后再通过信号把结果传回主线程更新界面。PyQt里线程与界面通信的正确方式是用Signal/Slot机制,而不是在线程里直接操作控件。
5.4 打包发布的常见问题
开发机上跑没问题,交付给用户就得打包成exe或app。我用的是PyInstaller,打包命令是这个样子:
pyinstaller -w --add-data "models/best.onnx;models" --add-data "resources;resources" main.py两个高频坑:
- 模型文件和UI资源文件没打包进去:必须用
--add-data显示带上,而且运行时路径要用相对路径加资源目录判断。直接写死绝对路径,换台电脑就崩。 - 在Windows上打包PyQt程序时,系统字体缩放可能导致界面错位。我在程序入口加了Qt高DPI支持的设置,代码是:
from PyQt5.QtCore import Qt QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)打包后在我的测试机上启动正常,界面清晰度也基本没问题。如果你的运行环境是Windows并且屏幕分辨率偏高,这个设置基本是必备的。
6. 实测结果与缺陷案例分析:模型在真实样本上的边界
6.1 六类缺陷的检测效果汇总
模型训练完,我在验证集上跑了一遍,整体结果在可接受范围,但我更在意的是单类表现差异,因为这对后续优化方向有直接指导意义。汇总下来大概是这样的(不同随机种子会有波动,数值仅供参考):
| 缺陷类别 | mAP50 | 主观感受 |
|---|---|---|
| crazing | 78%左右 | 细纹目标偏小,偶有漏检 |
| inclusion | 90%左右 | 颗粒目标清晰,检测稳定 |
| patches | 87%左右 | 边界模糊,框贴合度一般 |
| pitted_surface | 86%左右 | 密集场景有漏检,NMS需要调优 |
| rolled_in_scale | 88%左右 | 形态度量大,表现尚可 |
| scratches | 92%左右 | 细长条目标,一旦检出置信度很高 |
可以看到,最好检的是划痕和夹杂这类“轮廓锐利”的缺陷,最头疼的果然还是裂纹。这个规律和我在其他缺陷检测项目里的经验高度一致——形态越接近背景纹理,检测难度越大。
6.2 典型的误检与漏检场景
漏检案例里最有代表性的是crazing裂纹。这类缺陷是由极细的裂缝网络组成,在灰度图上往往只是一些低对比度的暗纹理,别说模型,人眼不放大都容易看漏。我当时试过在backbone引入注意力机制,也想过用图像增强把对比度拉高,但最终真正有效的操作是——在离线增强里对裂纹样本做了局部对比度拉伸,并适当提高了这类样本在训练集中出现的比例。这个操作用了之后,裂纹的mAP50提升了5个百分点左右。
误检案例里比较麻烦的是patches(斑块)和rolled_in_scale(氧化皮)之间的混淆。两者都是片状暗色区域,灰度特征非常接近。我在UI里加了“详情查看”功能,点开误检样本看,很多被识别成氧化皮的区域,其实人工也很难一口咬定是哪种缺陷。这类情况正确的态度是:如果人工都分不清,模型分不清不完全是模型问题,可能是标注标准本身就不统一。建议你在项目里也检查一下标注文件,看看同一类缺陷的标注框风格是否一致,标注质量对模型的影响我怎么说强调都不为过。
6.3 推理耗时与硬件相关经验
推理耗时和硬件强相关,我这里给一组实测量级仅供参考:同样的ONNX模型,640x640输入,在入门级独立显卡上推理大约20到30毫秒,在CPU上大约200到300毫秒。如果部署机没有GPU,系统还能跑,但最好用640的输入尺寸加ONNXRuntime的CPU优化,别用PyTorch原生推理。
这里有一个性能优化的思路值得提:如果质检场景对速度要求高,把batch size从1调大,用一次推理处理多张图片,吞吐量提升非常明显。虽然UI场景目前只是单张图片检测,但我在后台做了一个小测试:一次喂入8张图,单张平均耗时从200毫秒降到了60毫秒左右。这个思路在后面做“批量导入检测”功能时非常管用。
7. 项目源码使用指南与目录结构说明
7.1 完整目录结构
项目源码我按“训练、推理、界面”三个层次组织,这样结构清晰,其他人接手也容易看懂。最终长这样:
steel_defect_detector/ ├── main.py # 程序入口,登录注册 + 主窗口 ├── requirements.txt ├── detectors/ │ ├── __init__.py │ └── detector.py # Detector类,ONNX推理封装 ├── ui/ │ ├── __init__.py │ ├── login_window.py # 登录窗口 │ ├── register_window.py # 注册窗口 │ ├── main_window.py # 检测主窗口 │ └── resources.py # 图标资源导入 ├── models/ │ ├── best.onnx # 导出后的部署模型 │ └── best.pt # 原始训练权重 ├── data/ │ └── users.db # SQLite用户数据库 ├── train/ │ ├── train.py # 训练脚本 │ └── export_onnx.py # 导出脚本 └── docs/ └── 使用说明.md7.2 从零运行项目的步骤
如果是拿到源码从头跑起来,我建议按下面几步来:
- 创建虚拟环境,安装依赖:
pip install -r requirements.txt。 - 如果不想用我训练的模型,可以先用已有的best.pt直接跑UI;想自己训练,运行
python train/train.py,把data.yaml路径改成你自己的数据集。 - 训练完把
runs/train/exp/weights/best.pt拷贝到models/,需要优化推理速度就运行python train/export_onnx.py导出ONNX。 - 最后执行
python main.py,注册一个账号、登录、进入检测主界面,上传钢材表面图片查看检测结果。
requirements.txt里我锁的核心依赖包括ultralytics、onnxruntime-gpu、PyQt5、numpy、opencv-python。注意:如果机器没有对应显卡,把onnxruntime-gpu换成onnxruntime即可,Detector类会自动回退到CPUExecutionProvider。
7.3 高频运行错误与解决方式
运行过程中我自己和让几个朋友帮忙测试时,收集到了一些高频错误,整理成下面这个表格:
| 错误现象 | 根本原因 | 解决方式 |
|---|---|---|
打开UI提示No module named 'PyQt5' | 环境没装PyQt5 | pip install PyQt5,注意虚拟环境是否启用 |
| 点击检测后界面卡死 | 推理跑了UI线程 | 按前面说的用QThread后台推理 |
| 中文路径的图片打不开 | OpenCV/PyQt对中文路径支持差 | 用QFileDialog拿到路径后,先用open()读字节,再用cv2.imdecode解码 |
| ONNX推理报错shape不匹配 | 输入尺寸和导出时imgsz不一致 | 统一训练、导出、推理的imgsz |
| 登录成功后窗口闪退 | 窗口切换时错误关闭了QApplication | 切换时用self.close()而不是对整个应用执行退出 |
| 换电脑运行提示找不到模型文件 | PyInstaller打包时资源文件缺失 | 用--add-data并处理相对路径获取 |
8. 几个让系统更稳的经验技巧与扩展方向
8.1 项目过程中最有价值的三个细节
项目做完复盘,有几个细节我觉得比哪个模块本身都值钱。
第一,数据质量的优先级永远高于模型调参。我花了一整天清洗标注、统一标准,效果比后面试各种模型结构要好得多。标注框边界是否贴合、类别是否统一,这些“笨功夫”才是决定上限的东西。
第二,验证集的构建要贴合真实部署场景。我一开始从公开数据里随机抽验证集,效果很漂亮,但一到现场就崩。后来改成按照“不同批次图片分来源”来划分验证集,让验证集和训练集的分布有真实差异,这才反映出模型的实际泛化能力。
第三,UI的“死等”问题是体验杀手。第一次检测跑在UI线程,界面转圈假死,像个没写好的半成品。换成QThread之后,体验天壤之别。任何可能超过几百毫秒的操作,在桌面应用里都该走后台线程。
8.2 后续可以扩展的方向
这个项目做完后,可扩展的空间其实还很大,我列一下自己觉得有现实意义的方向:
- 半监督学习:钢材产线每天都产生海量无标注图像,手动标注成本高。用半监督方案,拿少量标注数据+大量无标注数据,理论上能显著提升检测上限。
- 轻量化部署:把模型从ONNX进一步量化到TensorRT或INT8,推理速度还能再上一个台阶,方便塞进边缘盒子。
- 缺陷分割:YOLOv11本身支持分割任务,如果要精确统计缺陷面积、评估缺陷等级,把检测头换成分割头会更有价值。
- 批量检测功能:一次性导入一批钢材图像,后台做成流式处理,输出一个统计报表。这种功能在工厂抽检场景特别实用。
最后再分享一个小技巧吧:对这个系统来说,如果想长期维护,记得把“训练的数据分布”和“实测的数据分布”差异记录成文档。工业视觉系统最怕的不是模型效果差,而是现场数据漂移了你还不知道。每过一段时间,用新采集的现场图重新评估一遍模型,如果有明显掉点,就说明该补充数据、微调模型了。这个习惯,比我前面写的所有代码都更能决定一个视觉项目能走多远。