简介:面向深度学习与机器视觉方向的毕业设计,这套源码提供了基于深度学习的表面缺陷检测与可视化监管系统的完整实现,适合Python开发者、高校本科生及研究生参考,可快速搭建缺陷检测模型并对检测结果进行可视化监管。压缩包共241个文件,包含Python源码、模型权重pth、yaml/json配置文件、pickle数据文件、ui界面及html/css/js前端资源,还有大量bmp/png/jpg实验图像,总计约163.69MB,目录结构清晰,便于按模块阅读和复用。已有661人学习下载。项目代码完整,下载即可运行,内容涵盖模型训练、推理检测、监控可视化等环节,另附ipynb示例、说明文档与训练日志,可帮助理解缺陷检测流程、模型调参与系统部署思路,适合作为高分毕业设计项目直接参考或二次开发。
1. 拿到这个源码包,先别急着解压:它到底解决哪一类质检难题
表面缺陷检测是工业质检里的老问题,人工肉眼在流水线上看金属表面划痕、麻点、脏污,看久了眼睛就花,漏检率直线上升。用深度学习做自动检测,本质上是目标检测问题:计算机在图像里圈出缺陷的位置和类别。而可视化监管系统,是把检测结果、统计报表、实时报警放到Web页面上,让车间管理者不用一直盯着屏幕也能掌握整条产线的情况。这个源码包如果拆开看,无非就是训练端、检测服务端、展示端三块,但它恰好覆盖了深度学习毕业设计最容易出彩的两个点:能跑模型,能看结果。它适合三类人:毕设选了表面缺陷检测方向的学生、想快速在本地跑通一套检测demo的工程师、还有准备用python做质检方向预研的技术人员。拿到源码的第一件事不是解压运行,而是先把技术路线看懂,否则连python环境配置都能把你劝退。
2. 深度学习系统骨架:模型选型、数据流与跑通前的环境配置
2.1 缺陷检测模型选型:为什么几乎所有毕设都落在YOLO系
表面缺陷检测在工业现场大致分为两类任务:第一类是判断这个工件有没有缺陷,第二类是找出缺陷在哪。如果只需要前者,用ResNet、EfficientNet做一个二分类就够了,但毕业设计通常要展示定位能力,因为缺陷位置对后续工艺改进很重要。目标检测恰好能同时输出类别和位置,所以CNN目标检测模型成了主流。
那Faster R-CNN、SSD、YOLO那么多,为什么大家最终都选了YOLO?我给你的回答不复杂,就是均衡。Faster R-CNN精度高但慢,Mask R-CNN能做像素级分割但训练成本高,YOLO系列在速度和精度之间找到平衡点,而且社区资料多到爆炸。下面这个表格是我平时选型的参考,不是标准答案,但能帮你理解差距。
| 模型 | 推理速度 | 精度 | 显存占用 | 实现难度 | 适合场景 |
|---|---|---|---|---|---|
| YOLOv5/v8 | 快 | 中高 | 中等 | 低 | 产线实时检测、毕设首选 |
| Faster R-CNN | 慢 | 高 | 高 | 中 | 精度优先、不追求实时 |
| Mask R-CNN | 慢 | 高(分割) | 高 | 高 | 需要像素级缺陷轮廓 |
| EfficientDet | 中 | 中 | 中 | 中 | 算力受限 |
我一般会选YOLOv5而不是最新版,原因很朴实:v5的坑基本都被人踩过了,任何报错都能在站内搜到答案。等把v5这条路走通,再去换v8也不是难事,数据配置和训练流程一脉相承。如果有同学想做点创新的,可以在YOLOv5的neck或loss上动手脚,但要确保自己看得懂,答辩时最怕的是用了一个别人都没见过的网络结构还没法解释。
这里要插一句:如果你在查资料时看到“点云图金属表面缺陷检测”这个词,那是另一个分支,用激光或结构光获取三维点云再分析缺陷,数据获取成本极高,普通毕设拿不到那么贵的数据。绝大部分毕业设计用的都是2D图像数据集,比如NEU-DET带钢表面缺陷数据集,包含裂纹、夹杂、斑块、麻点、氧化铁皮压入、划伤六类缺陷。先把2D目标检测吃透,3D不过是换一种输入模态,特征提取和检测的核心思路是一样的。
2.2 数据流与目录结构:写代码前先把架子搭对
这类毕业设计源码包我经手过好几个,核心模块通常是固定的:训练脚本、模型定义、检测服务、Web前端。它们之间的数据流是这样的:生产线相机抓拍一张原始图像,图像送到检测服务,服务用训练好的权重做推理,得到缺陷类别、置信度和边框坐标,然后把坐标画到图上,结果写入数据库,前端每秒钟拉一次最新结果并渲染。
所以在你急着跑demo之前,先把目录结构理顺。我一般会这样规划,这也是最不容易返工的一种排列:
defect_system/ ├── data/ │ ├── images/ # 原始缺陷图像 │ ├── annotations/ # VOC格式XML标注 │ ├── labels/ # 转换后的YOLO txt标注 │ └── defect.yaml # 数据集配置文件 ├── models/ │ └── best.pt # 训练好的权重 ├── scripts/ │ ├── voc2yolo.py # 标注格式转换 │ └── train.sh # 训练脚本 ├── web/ │ ├── app.py # Flask检测服务 │ ├── templates/ │ └── static/ ├── uploads/ # 接收上传图片 └── requirements.txt注意data和models放同级目录,不要嵌套太深。之前见过有人把标注文件放在桌面路径下,训练脚本读不到,最后排查半天居然是路径分隔符写成了反斜杠,在Windows下能跑,换到Linux全挂。关于路径有一条铁律:项目里所有路径要么写相对路径,要么基于项目根目录拼接,不要写死绝对路径。因为答辩时要在老师电脑上跑,你的/Users/yourname/xxx到别人机器上就是灾难。
目录结构确定之后,你还会看到一些公开的源码会在data目录之外单独放一个runs目录,这是YOLO默认的输出目录,用来存放每次训练的日志和权重。不要手欠去删,后面分析训练结果靠它。
2.3 环境配置:最容易让新手在第一步翻车的环节
很多下载这个源码包的同学,第一反应是双击运行,然后被一连串ModuleNotFoundError打懵。这很正常,因为表面缺陷检测的深度学习环境不是装了python就行,你需要PyTorch、OpenCV、NumPy、Pandas这一串,版本还互相咬合。我按python安装教程的思路给你整理了一个我反复用过的环境配置顺序,照着做能少踩一半坑:
conda create -n defect python=3.8 -y conda activate defect pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 --extra-index-url https://download.pytorch.org/whl/cu117 pip install -r requirements.txt先解释第一个命令:conda创建名为defect的独立环境,python指定3.8。我知道Python已经出到3.12了,但深度学习生态不全都跟得上,很多依赖库的wheel包还是针对3.8到3.10,用3.8最稳妥。如果你机器上连conda都没有,先去装Anaconda,这个过程本身不难,但要注意安装路径不能带空格,否则后面一些底层库会莫名其妙报错。
第二个命令是安装GPU版PyTorch,cu117表示CUDA 11.7。如果你的电脑是纯CPU,没有NVIDIA显卡,那就把--extra-index-url那段去掉,但训练速度会慢到让你怀疑人生。这套命令适合Linux,Windows下把conda activate换成activate defect,其余不变。注意pip安装时如果提示certificate verify failed,多半是公司网络拦截,临时加--trusted-host pypi.org --trusted-host download.pytorch.org可以绕过。
第三个命令里的requirements.txt,如果源码包里没有,就自己建一个。最少需要opencv-python、numpy、pandas、flask、pyyaml、tqdm,如果走YOLOv5训练还要加matplotlib、seaborn。别一股脑pip install xxx,以后重装环境你就知道什么叫后悔药没处买。
装完环境之后,先跑一段验证代码,确认torch能调用设备,再继续往下走:
import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'CPU only')这段输出的意义很大:第一行看torch版本;第二行如果是False,后面训练会慢到无法忍受,但也能跑,只是100个epoch可能要几十小时;第三行能看见显卡型号。我之前遇到一个用户,装好后torch.cuda.is_available()是True,但训练时还是走CPU,最后发现是train.py里硬编码了device='cpu',这种低级错误在源码包里并不少见。
提示:环境配置失败时,不要急着把整个环境删掉重来,先用
pip list看已安装的包,再对照requirements逐个排查。这一环节本身就是深度学习项目的基本功,过了这一关,后面的流程才算顺畅。
3. 训练缺陷检测模型:标注转换、训练参数与loss曲线怎么看
3.1 把VOC标注转成YOLO格式:转换脚本与四个边界坑
表面缺陷检测数据集最常见的格式是Pascal VOC,也就是一张图片对应一个XML文件,XML里面是目标框的左上和右下坐标。而YOLO系列训练要求每个目标一行文本:类别索引、归一化中心点x、中心点y、归一化宽度w、归一化高度h。如果标注格式不对,训练能跑但loss永远不收敛,这是第一个大坑。
源码包里如果有voc2yolo.py,你也要自己检查一遍逻辑。如果没有,下面这段脚本可以直接复用,我几乎每个类似项目都靠它起步:
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, class_names): os.makedirs(out_dir, exist_ok=True) tree = ET.parse(xml_path) root = tree.getroot() img_w = float(root.find('size/width').text) img_h = float(root.find('size/height').text) lines = [] for obj in root.findall('object'): name = obj.find('name').text.strip() if name not in class_names: continue cls = 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) xmin = max(0, xmin) ymin = max(0, ymin) xmax = min(img_w, xmax) ymax = min(img_h, ymax) w = xmax - xmin h = ymax - ymin if w <= 0 or h <= 0: continue x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h w_norm = w / img_w h_norm = h / img_h lines.append(f"{cls} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}") out_path = os.path.join(out_dir, os.path.basename(xml_path).replace('.xml', '.txt')) with open(out_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) if __name__ == '__main__': xml_dir = 'data/annotations' out_dir = 'data/labels' class_names = ['crazing', 'inclusion', 'patches', 'pitted_surface', 'rolled_in_scale', 'scratches'] for xml_file in os.listdir(xml_dir): if xml_file.endswith('.xml'): voc_to_yolo(os.path.join(xml_dir, xml_file), out_dir, class_names)脚本逻辑分四步:解析XML,读取图片尺寸;遍历每个object;把左上右下坐标转成中心点宽高;写txt文件。注意我加了三个关键动作:name.strip()清掉类别名两边的空白;坐标越界夹取;宽高小于等于0的直接跳过。这三个动作不是为了好看,而是实际数据集里一定会遇到的问题。
四个边界坑,一个个说:
第一,类别名前后的空白字符。XML里经常出现<name>patches</name>旁边多一个换行或制表符,直接进class_names.index会抛ValueError。所以在取name后立刻strip。
第二,负坐标和越界坐标。缺陷紧贴图像边缘时,人工标注可能标出负的xmin或超过边界的xmax,不加处理会让归一化坐标落在0到1外面,训练时目标被忽略。夹取之后,小数精度损失可以忽略不计。
第三,图片宽高和XML里的size对不上。有些数据集在发布前压缩过图片,XML里存的却是原始分辨率。转换前抽三张图,用OpenCV读一下实际宽高,再和XML里的size对比,不一致就先统一图片分辨率。这个坑我曾吃过,800x600的图标注写成了1024x768,训练出来mAP只有0.2,当时还怀疑是模型问题,查了一天。
第四,空标注文件。部分图片确实没有缺陷,XML里没有object,转换后txt是空文件,这是合法的,YOLO允许空标注。但不要为了凑样本给这些图乱标一个框,会让模型学到“没有缺陷的图也有缺陷”,误检率直线上升。
3.2 训练命令与参数:batch_size、learning rate、epochs不是拍脑袋定的
标注转换完,检查一下data/labels目录,确认每个XML都有对应的txt,且非空文件每行正好五个数字。确认无误后,写一个data/defect.yaml:
train: data/images val: data/images nc: 6 names: ['crazing', 'inclusion', 'patches', 'pitted_surface', 'rolled_in_scale', 'scratches']如果训练集和验证集要分开,就把train和val分别写成两个目录路径。很多源码包偷懒,train和val指向同一目录,这样验证mAP虚高,答辩时一到真实数据就露馅。我一般按7:1:2划分成train、val、test,test完全不参与训练,只用来做最终评估。
接着是训练命令,以YOLOv5为例:
python train.py \ --data data/defect.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --device 0 \ --workers 4 \ --patience 15参数说明:--img是训练输入尺寸,缺陷检测建议至少640,因为很多缺陷只有几十像素,再小会被下采样抹掉。--batch根据显存来,我用8G显存跑yolov5s,batch 16刚好,再往上就会显存溢出。--device 0指定显卡,CPU训练就改成--device cpu,但百来个epoch可能要跑一两天,不太现实。--workers是读取线程数,Windows下建议设0或2,设大了经常报DataLoader worker崩溃。
训练过程中怎么看有没有问题?主要看每轮输出的box_loss、cls_loss、obj_loss和验证集的mAP。深度学习CNN不是黑匣子,loss曲线会告诉你趋势:box_loss前20轮快速下降,之后缓慢收敛,这是正常;如果loss反复震荡完全不降,优先怀疑学习率太大或者数据集里有大量错误标注。如果train_loss降到很低但val_loss往上走,就是过拟合,解决办法是增加数据增强、降低模型复杂度或者提前停下。
训练完成后,模型目录runs/train/exp里会生成best.pt和last.pt。记住一条:用best.pt做后续推理,它是验证集上表现最好的权重;last.pt是最后一轮的,通常比best差。我之前图省事直接用last.pt,漏检率肉眼可见变高,后来老老实实改回best.pt。
3.3 在验证集上先自评:不要拿训练集截图当成果
训练完不是马上接Web系统,先跑一次验证脚本,把每张图的预测结果画出来。YOLOv5自带val.py:
python val.py \ --data data/defect.yaml \ --weights runs/train/exp/best.pt \ --img 640 \ --conf-thres 0.25 \ --iou-thres 0.45 \ --save-txt \ --save-conf这里--conf-thres是置信度阈值,只有模型判定概率超过0.25的框才会保留。阈值越低,越容易把噪点当缺陷;阈值越高,越容易漏掉真实缺陷。--iou-thres是NMS去重阈值,同一个缺陷如果预测出多个重叠框,IoU超过0.45的会被合并。这两个参数在后面的监管系统里也会用到,建议测试时就固定下来,不要训练时用一组,部署时又换一组,那样实验数据完全不可比。
验证集自评的目的是生成带框图片,然后你一个人看,不要只看mAP。缺陷检测里mAP高但漏检的案例太多了,特别是小目标。把预测结果图一张张翻过去,重点关注裂纹这种细长缺陷,如果断断续续只检测出中间一段,说明模型学到了局部特征,没学到整体连贯性,后续可以靠提高推理分辨率或切patch解决。
4. 可视化监管系统:用Flask把检测模型包装成Web应用
4.1 写一个最小的检测服务:模型只加载一次
可视化监管系统拆开来看就三个部分:一个检测服务、一个网页、一个数据库。检测服务的任务是把训练好的模型权重暴露成HTTP接口,前端传图片过去,后端返回检测结果。这里最容易犯的错是每次请求都torch.load一次模型,直接导致页面卡死。正确做法是在Flask应用启动时把模型加载到内存全局变量,之后所有请求共享。
下面这个app.py是我常用的骨架,你可以直接对照源码包找对应部分:
import os import cv2 import torch import numpy as np from flask import Flask, request, jsonify, render_template from werkzeug.utils import secure_filename app = Flask(__name__) app.config['UPLOAD_FOLDER'] = 'uploads' os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) # 模型全局加载,只做一次 model = torch.hub.load('ultralytics/yolov5', 'custom', path='models/best.pt', force_reload=True) model.conf = 0.25 # 置信度阈值 model.iou = 0.45 # NMS阈值 model.classes = [0, 1, 2, 3, 4, 5] # 只检测这六类缺陷 @app.route('/') def index(): return render_template('index.html') @app.route('/detect', methods=['POST']) def detect(): f = request.files['image'] if 'image' in request.files else None if f is None: return jsonify({'error': 'no image'}), 400 path = os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(f.filename)) f.save(path) img = cv2.imread(path) results = model(img) detections = [] for *xyxy, conf, cls in results.xyxy[0].tolist(): detections.append({ 'box': [round(v, 2) for v in xyxy], 'confidence': round(conf, 4), 'class': int(cls) }) return jsonify({'detections': detections, 'count': len(detections)}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)说明几个关键点。model.conf和model.iou是YOLOv5推理时的默认阈值,这里提前设好,避免每次调用都传参。model.classes是类别过滤,如果你的源码包只训练了两类,就改成对应索引列表,不设的话会输出所有类别。secure_filename会把文件名里可能存在的路径分隔符去掉,防止有人通过文件名上传路径穿越文件。
图像读取使用cv2.imread,但它有个老毛病:Windows下读中文路径会返回None。如果你的上传目录或图片名是中文,需要在保存后改用cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR),我后续避坑章里再详细说。
接口写完,启动服务就是python web/app.py,默认跑在5000端口。你可以用curl测一下:
curl -X POST -F "image=@test.jpg" http://127.0.0.1:5000/detect如果返回的JSON里有count字段,说明检测链路已经通了。这一步是整个可视化系统的地基,地基没打牢,后面画什么图表都是空的。
4.2 前端可视化:轮询接口、画框与报警,不要在浏览器里跑模型
检测服务就绪后,前端要解决三件事:把图片显示在页面、把检测框画上去、把缺陷统计展示出来。看起来复杂,实际上用Flask内嵌模板加原生JavaScript就能做,不需要前端框架。Vue和ECharts可以加,但会增加演示时讲不清的风险,我建议先把最原始方式跑通,至少它能回答“可视化是怎么实现的”。
先写一个最简的index.html模板,用Canvas画框:
<video id="camera" autoplay muted></video> <canvas id="overlay"></canvas> <div id="alarm" style="display:none;color:red;">检测到缺陷,请查看</div> <script> const canvas = document.getElementById('overlay'); const ctx = canvas.getContext('2d'); async function poll() { const res = await fetch('/latest'); const data = await res.json(); if (!data.image_base64) return; const img = new Image(); img.onload = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); data.detections.forEach(d => { ctx.strokeStyle = '#ff0000'; ctx.lineWidth = 2; const [x1, y1, x2, y2] = d.box; ctx.strokeRect(x1, y1, x2 - x1, y2 - y1); ctx.fillStyle = '#ff0000'; ctx.fillText(`缺陷${d.class} ${d.confidence}`, x1, y1 - 5); }); }; img.src = 'data:image/jpeg;base64,' + data.image_base64; } setInterval(poll, 1000); </script>这段代码里,/latest接口返回最近一帧的base64图像和检测框列表。前端每秒钟拉一次,用Canvas把检测框叠加在图片上。data.detections里每一项的box是[x1, y1, x2, y2],直接用于画矩形。如果你要接摄像头实时画面,就把<video>标签加进来,用getUserMedia拿本地视频,但要注意浏览器安全策略,必须通过HTTP或HTTPS访问。
轮询间隔设1秒够用,设100毫秒会让Flask开发服务器疯狂刷日志,CPU占用拉满,而且浏览器同时发多个请求还会阻塞。想做实时视频流,可以走WebSocket加MJPEG流,但毕设演示不需要,反而越复杂越容易出问题。
后端要把图像转成base64,我在app.py里补上这个接口:
import base64 detections_db = [] @app.route('/latest') def latest(): if not detections_db: return jsonify({'image_base64': None, 'detections': []}) last = detections_db[-1] ret, buf = cv2.imencode('.jpg', last['image']) b64 = base64.b64encode(buf.tobytes()).decode('utf-8') return jsonify({'image_base64': b64, 'detections': last['detections']})这里用全局列表detections_db充当最近一帧的缓存,实际项目中要加线程锁,因为Flask多线程下两个请求同时修改列表可能错乱。更规范的做法是用queue.Queue扮演生产消费模型:一个后台线程不停从摄像头读帧并推理,把结果放进队列,前端接口从队列取最新结果。这个设计的优势是检测吞吐量和Web展示解耦,摄像头帧率再高,前端只看最新一帧。源码包里如果只有一个同步的/detect接口,我建议你改成这种后台轮询模式,答辩时老师问“你的系统能不能接实时产线”,你能答得上来就赢了一半。
4.3 结果落库与统计面板:可视化监管的核心是数据闭环
检测结果不能只显示在网页上,必须落库。不要用MySQL,毕设用SQLite足够,零配置,文件型数据库,交源码时把数据库一起给就行。我习惯在启动时执行建表:
import sqlite3 def init_db(): conn = sqlite3.connect('defect.db') conn.execute(''' CREATE TABLE IF NOT EXISTS defects ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, class TEXT, confidence REAL, image_path TEXT ) ''') conn.commit() conn.close()每次检测到缺陷时,除了在前端显示,还向表里写一条记录。统计面板就有了数据源,可以用最直接的方式在Flask里查数据库,再渲染到模板:
@app.route('/stats') def stats(): conn = sqlite3.connect('defect.db') rows = conn.execute( 'SELECT class, COUNT(*) FROM defects GROUP BY class' ).fetchall() conn.close() return render_template('stats.html', rows=rows)这段代码本身不复杂,但体现了可视化监管系统的本质:模型输出的一条条检测记录,经聚合后变成辅助决策的信息。比如哪种缺陷出现最多、今天比昨天增加了多少、哪个班次漏检率上升。把这些做成柱状图和折线图,就贴合了“可视化监管”题目的要求。如果你想用ECharts,把rows转成JSON传给JS,绑定到chart.setOption里去。但先把数据闭环打通再谈图表,这个顺序不能反。
5. 表面缺陷检测系统避坑手册:环境、显存、小目标和中文路径,5个真实翻车现场
下面这些坑我全都踩过,有的在换电脑演示时现场暴露,有的在训练跑到一半时翻车。每一条都按现象、原因、解决三个步骤写,方便你直接对照。
5.1 torch.cuda.is_available()返回False,显卡却明明存在
现象:按教程装完torch,跑print(torch.cuda.is_available()),输出False,但设备管理器里NVIDIA显卡正常显示。
原因:90%的情况是torch装成了CPU版本。很多人在默认PyPI源里执行pip install torch,拿到的就是CPU版。剩下10%是显卡驱动太旧,或者CUDA版本与驱动不兼容。
解决:先看torch版本号后面有没有+cu。pip show torch输出Version: 1.13.1+cu117表示带CUDA,如果只有1.13.1那就是CPU版。重装命令:
pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 --extra-index-url https://download.pytorch.org/whl/cu117装完再验证。如果还是False,去显卡厂商官网更新驱动,驱动版本要大于CUDA所需的最低版本。这一步通过之后,后面训练阶段的“玄学”问题能少一大半,因为很多libcudnn报错都源于torch与CUDA版本错配。
5.2 CUDA out of memory:先调参数,再考虑加钱
现象:训练第一个epoch还没跑完就报RuntimeError: CUDA out of memory. Tried to allocate 128.00 MiB,而且每次的报错都差不多。
原因:一是batch_size设得太大,8G显存跑yolov5s batch 16没问题,但跑yolov5x batch 32就是作死;二是显存被其他进程占用,训练时开着十几个浏览器标签页,显存早就不够了。
解决:把batch_size降到8或者4,同时把--img从640降到512。这两个参数对显存的影响是乘法的,图像尺寸缩一半,特征图面积缩小,显存占用明显下降。如果还不够,在训练命令里加--workers 0减少数据加载线程,然后运行nvidia-smi看是否有其他进程占用显存,有就清掉。还有一个trick:训练前在脚本里加torch.cuda.empty_cache(),释放PyTorch没回收的中间张量。
5.3 小缺陷总是检测不到:模型看见的和你看的不是一个尺度
现象:模型整体mAP超过0.85,但检测细小裂纹总是漏,或者把一条连续裂纹检测成断断续续的三段。
原因:缺陷目标在640x640输入下可能只有十几个像素,经过YOLO的下采样到特征图上只剩一两个格子,特征信息基本丢了。这是目标检测的通病,不是你的代码有问题。
解决:第一个方案是把训练和推理的img尺寸提高到1280,但显存占用会翻倍,小于8G不建议。第二个方案是切patch,把原图切成512x512的块,逐块检测再合并结果,小目标相对尺寸变大,检测率会明显提升,代价是单帧推理时间从30ms涨到120ms,毕设现场完全能接受。第三个方案是数据增强里加随机裁剪,让模型在训练时多看到局部特征。我在做金属表面麻点检测时,用切patch方案把小目标漏检率从18%降到了6%。
5.4 中文路径让程序直接停摆:OpenCV和PyTorch都读不了
现象:代码在英文路径下跑得好好的,放到D:\缺陷检测\项目源码\下,报FileNotFoundError,但文件明明存在。
原因:Windows控制台默认编码是GBK,OpenCV底层调用系统API,传中文路径会转换失败。PyTorch的DataLoader也沿用了类似限制,它期望纯ASCII路径。
解决:最省事的是项目路径和所有文件名全部用英文,这是我在每个环境配置教程里都强调的事。如果必须用中文,图像读取改为cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR),保存图像改为cv2.imencode('.jpg', img)[1].tofile(path)。模型权重路径如果含中文,也尽量复制到英文目录,torch.load对中文路径的支持各版本不一致,没必要赌运气。
5.5 Flask页面一直转圈:别在请求函数里加载模型
现象:第一次访问检测页面等了好久才出来,第二次直接超时,浏览器小圆圈一直转。
原因:代码把torch.load('best.pt')写进了@app.route('/detect')端点内部,每次请求都重新加载模型。torch.load一次要好几秒,如果还从远程hub下载权重,那就不是几秒而是几分钟。
解决:把模型加载放到全局作用域,在启动时一次完成。如果用的torch.hub.load,第一次会自动联网下载依赖,之后就会走本地缓存。这里还有一个隐蔽的坑:Flask的debug模式会自动reload,代码一改动整个进程重启,全局变量重新初始化,模型重新加载,也会造成卡顿。所以我建议调试时用debug=True方便观察,但演示时一定改成debug=False。如果推理耗时超过一两秒,最好把检测请求放到后台线程,用队列把结果传回接口,避免开发服务器的单进程阻塞所有请求。
这5条都属于“不会让模型变复杂,但能让系统从不可用变可用”的工程细节。把代码以外的这些问题解决掉,你才有资格去谈模型优化。
6. 进阶:把准确率再往上顶的三个手段与最终验证
训练结束不等于能交差,下面三个手段按性价比排序。第一个是调阈值,不增加任何计算成本;第二个是TTA,用时间换准确率;第三个是加二次分类,专门压误报。
6.1 用验证集网格搜索置信度阈值
model.conf=0.25是YOLO默认值,但不一定适合你的缺陷分布。表面缺陷检测中漏检的代价通常比误检大,所以可以把阈值降到0.1,让模型把不确定目标也框出来,再在验证集上数一下多出来的框到底是真缺陷还是误检。我常用的做法是让conf从0.05按步长0.05跑到0.5,每组记下precision和recall,选一个业务上能接受的平衡点,固定到Web服务里。这个过程半小时就能跑完,但效果往往比调半天网络结构都明显。
6.2 推理时开TTA,用时间换准确率
YOLOv5的val.py和detect.py都支持--augment,推理时对图像做缩放翻转等变换,再融合多个预测结果,能提升一点mAP尤其对小目标友好。代价是速度变为原来的2到4倍,但毕设演示完全够用。这一步只加参数,不碰代码:
python val.py \ --data data/defect.yaml \ --weights runs/train/exp/best.pt \ --img 640 \ --conf-thres 0.1 \ --iou-thres 0.45 \ --augment6.3 误报多?加一个轻量二次分类网络
如果模型把纹理阴影当成缺陷,不要急着调阈值,可以训练一个ResNet18,输入是检测框裁剪出来的图像块,输出“真缺陷”或“假缺陷”。检测服务先跑YOLO得到候选框,裁剪后送给分类网络,只保留被判定为真缺陷的框。这个方案能砍掉大部分误报,而且几乎不影响召回。我上一个项目用这个办法,误报率从15%压到2%。
最终验证我的习惯是:把测试集图片全部预测一遍,把预测错误的图打印到一个文件夹,一张张看,看是标注错了还是模型确实看不到。这个习惯帮我少交了好几次答辩返工,希望帮到你。
本文还有配套的精品资源,点击获取