
简介一套面向Python毕业设计的深度学习表面缺陷检测与可视化监管完整实现适合计算机、人工智能相关专业学生用于课程设计、毕业设计或工程实践。压缩包共241个文件大小约163.69MB含86个bmp与66个png等样本图像、19个Python源码文件、模型权重pth及yaml配置配合7个网页模板与CSS/JS构建可视化监管界面另含事件日志、ipynb调试笔记等辅助材料。已有661人学习下载项目代码完整、下载即用从数据准备、模型训练到界面展示全链路覆盖。读者可在此基础上快速复现缺陷检测流程理解数据集划分、模型训练参数与前后端交互逻辑也可根据自身需求替换数据集或调整界面模块作为高分毕设的可靠起点。1. 表面缺陷检测毕设项目这套源码到底解决什么问题每年毕业设计季都能看到大量基于深度学习的表面缺陷检测系统源码文件名往往就是“python毕业设计基于深度学习的表面缺陷检测与可视化监管系统源码.zip”。这类项目之所以扎堆是因为它把计算机视觉、Python工程、Web后端和数据库串在了一个完整场景里既能体现深度学习建模能力又能演示工程落地答辩时有模型、有指标、有界面评分点很齐全。我拿到这类源码的第一步不是急着跑通而是先拆解它到底由哪几层组成检测模型负责从图像中定位缺陷区域并给出类别可视化监管系统把检测结果持久化到数据库再通过网页端实时展示和统计。这两部分一旦打通整条产线质检的demo就成立了。这适合两类人一是计算机视觉或软件工程方向的本科生需要尽快跑出能演示的系统二是刚接触工业视觉的工程师想用一套现成代码熟悉检测模型和Web端集成的套路。如果你正愁毕设题目或拿到源码后不知道从哪改起这篇内容会按“模型训练→系统搭建→排错→进阶”的顺序把每一步的关键参数和常见坑讲透。下面所有命令都以YOLO系列为基准因为它是目前缺陷检测方向资料最密、最容易复现的基线模型也是这套源码最常见的核心组件。2. 检测模型选型与训练为什么是YOLO以及你的第一个能用的模型2.1 缺陷检测场景下YOLO相比Faster R-CNN和SSD的优势表面缺陷检测本质上是一个目标检测任务需要在工业图像中找到划痕、麻点、氧化、夹杂等缺陷并标出位置。做毕设的时候模型选择直接决定你后面能走多远。Faster R-CNN精度上限高但两阶段结构的训练和推理速度都慢对于刚接触PyTorch的学生来说调参难度偏大SSD结构简单但对小目标缺陷的召回率明显不足——钢材表面的微小麻点往往只有十几个像素宽SSD很容易漏掉。YOLO系列之所以更适合这套毕设源码首先是工程生态成熟。Ultralytics提供的YOLOv5/v8代码库开箱即用训练命令封装得很好配置文件和权重下载都自动化对不熟悉工程细节的人非常友好。其次是速度和精度的平衡点很舒服在工业质检场景里一个摄像头每秒要处理几帧到几十帧图像YOLOv5s在GTX 1660上都能跑到60帧以上同时mAP保持在0.7左右对于展示来说完全够用。最后是部署方便导出ONNX或者用PyTorch直接加载都能和Flask后端快速集成。我做过的几个视觉质检项目里YOLO系是唯一一个“从零到有结果”耗时最少的方案。2.2 数据集准备NEU-DET还是自建数据目录结构怎么摆表面缺陷检测最常用的公开数据集是NEU-DET包含热轧带钢表面的六类缺陷 crazing、inclusion、patches、pitted_surface、rolled-in_scale、scratches。每类300张共1800张灰度图已经自带VOC格式标注。用这个数据集做完实验你再换自己的产品图像时只需要改标注格式和类别名。如果你的毕设题目指定了特定材质比如PCB板、锂电池薄膜就得自己采集和标注一套完整的数据划分脚本必不可少。常见的管理方式是在源码根目录下建一个datasets文件夹里面按YOLO惯例分成images和labels再按train、val、test拆开。目录树大致如下datasets/ neu_det/ images/ train/ img001.jpg ... val/ ... labels/ train/ img001.txt ...每个txt文件里每一行代表一个缺陷框格式为class_id x_center y_center width height坐标都是除以图片宽高后的归一化结果。这里有个容易忽略的坑NEU-DET原始标注是VOC的XML格式框坐标是绝对像素值必须转换后才能被YOLO训练脚本读取。等到了第4章我会给你一段完整的VOC转YOLO脚本以及坐标边界裁剪的细节。2.3 训练命令与关键参数从data.yaml到train.py下载YOLOv5源码后第一步是准备数据配置文件。打开源码里的data/custom.yaml改成指向你的数据集路径和类别列表# data/custom.yaml train: datasets/neu_det/images/train val: datasets/neu_det/images/val nc: 6 names: [crazing, inclusion, patches, pitted_surface, rolled-in_scale, scratches]train和val路径要填绝对路径YOLOv5对相对路径解析容易出问题。nc是缺陷类别总数names顺序必须和你txt文件里的class_id一致否则训练时标签会错位。我第一次用这个文件时就是names顺序没对齐导致训练了20轮才发现mAP一直是0。改完之后执行训练命令python train.py \ --data data/custom.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --cache \ --name neu_run1这里--img 640是输入图像分辨率。表面缺陷往往是小目标如果你的显卡允许我建议直接开608太高显存不够。--batch根据显存调整GTX 1080Ti以上可以开328G卡建议16。--cache的作用是预先加载图像到内存减少磁盘IO等待但内存小于16G时反而会卡可以不加。--weights如果指定yolov5s.pt会自动下载COCO预训练权重迁移学习的效果比从头训好很多。训练过程中要盯两个指标一个是val/obj_loss它反映框置信度损失另一个是mAP0.5这是答辩时最常被问到的指标。正常情况下mAP0.5在第30轮左右会达到0.8以上。如果到50轮还在0.3附近大概率是数据集或配置有问题别盲目加epoch数。2.4 一次性跑通的推理验证代码训练完成后模型权重保存在runs/train/neu_run1/weights/best.pt。为了确认系统能加载这个模型做推理建议先写一个独立的检测脚本不要直接跳到Web端。下面是我平时用来快速验证模型效果的最小代码import torch model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/neu_run1/weights/best.pt, force_reloadTrue) model.conf 0.25 # 置信度阈值 model.iou 0.45 # NMS IoU阈值 img data/images/test_scratches.jpg # 换成你的测试图 results model(img, size640) results.show() # 保存带标注的结果图 results.save(save_dirruns/infer/)这段代码用torch.hub加载本地权重模型会自动识别yaml里的类别名。model.conf设得越低召回越高但误检也越多工业场景一般设0.25如果你发现缺陷漏检就降到0.15再试。model.iou控制两个框重叠时保留哪个默认0.45对密集缺陷比较合适。如果推理时爆显存就把size从640降到480代价是对小缺陷的检出能力下降。3. 可视化监管系统搭建Flask后端和实时看板怎么接3.1 系统分层检测服务、业务后端、前端页面各负责什么可视化监管系统不是简单给检测结果画个框它要解决“缺陷被检测出来后如何被记录、统计、追溯”的问题。在一套毕设源码里最常见的架构是检测模块独立为一个服务接收图像路径或字节流返回标注结果Flask作为Web后端接收前端上传的图片调用检测模块把结果写入数据库前端页面用表格和图表展示历史记录。这样分层的好处是检测模型升级时不用动Web代码接口保持Json出入。这种设计也是工业界真实监控系统的简化版。实际产线上检测服务往往跑在独立GPU服务器上通过HTTP或消息队列与上位机通讯监管系统部署在另一台普通PC上。毕设版本不需要那么复杂但接口设计要留出替换空间。我见到的毕设翻车案例里最多的是把模型加载写在Flask路由里每个请求都重新加载一次权重结果页面卡成PPT。正确做法是把模型做成全局对象在Flask启动时加载一次。3.2 数据库表设计一张表就能满足毕设的监管需求可视化监管需要保存每次检测的原始图片、检测时间、每类缺陷的数量、最重要缺陷的位置和类别。SQLite是毕设首选单文件免安装Python自带驱动和Flask配合很顺手。建表语句如下CREATE TABLE IF NOT EXISTS inspection_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, image_path TEXT NOT NULL, detected_at DATETIME DEFAULT CURRENT_TIMESTAMP, defect_counts TEXT NOT NULL, worst_defect_class TEXT, worst_defect_conf REAL );defect_counts字段用JSON字符串保存例如{scratches: 2, pitted_surface: 1}这样不用为每类缺陷建列前端展示时解析一下即可。worst_defect_class和worst_defect_conf记录置信度最高的缺陷方便在监管主页上提示“重点关注”。如果想把每个框的坐标也存下来就在建一张defect_boxes关联表但毕设通常不需要因为原始标注图已经保存在磁盘上需要细看时直接打开图片。3.3 Flask后端模型加载、图片上传和结果入库后端用Flask写一个app.py核心是注册两个路由/detect接收图片并返回检测JSON/records返回历史记录列表。关键在于模型如何被复用。我在工程里是这样做的import json import torch from flask import Flask, request, jsonify, render_template from datetime import datetime import sqlite3 import os app Flask(__name__) # 在模块加载时初始化模型只初始化一次 model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/neu_run1/weights/best.pt, force_reloadFalse) model.conf 0.25 UPLOAD_FOLDER uploads os.makedirs(UPLOAD_FOLDER, exist_okTrue) def save_record(image_path, results): counts {} worst_conf 0.0 worst_class None for *_, cls, conf in results.xyxy[0]: cls_name model.names[int(cls)] counts[cls_name] counts.get(cls_name, 0) 1 if conf worst_conf: worst_conf float(conf) worst_class cls_name conn sqlite3.connect(inspections.db) conn.execute(INSERT INTO inspection_records (image_path, defect_counts, worst_defect_class, worst_defect_conf) VALUES (?, ?, ?, ?), (image_path, json.dumps(counts), worst_class, worst_conf)) conn.commit() conn.close() return counts, worst_class, worst_conf app.route(/detect, methods[POST]) def detect(): file request.files[image] path os.path.join(UPLOAD_FOLDER, datetime.now().strftime(%Y%m%d%H%M%S) .jpg) file.save(path) results model(path) counts, worst_class, worst_conf save_record(path, results) return jsonify({counts: counts, worst_class: worst_class, worst_conf: worst_conf, image_url: / path}) app.route(/) def index(): return render_template(index.html) if __name__ __main__: app.run(host0.0.0.0, port5000, threadedTrue)关键是model torch.hub.load(...)放在全局作用域Flask app启动后模型一直驻留内存。results.xyxy[0]中每行是x1, y1, x2, y2, confidence, class注意索引顺序不要记错。sqlite3.connect每次请求都打开连接SQLite对并发写支持弱不过毕设演示时单人操作完全够用如果同时访问的人多就得换MySQL并用连接池。threadedTrue允许Flask并发处理请求避免一个检测没跑完其他客户端全部卡住。3.4 前端展示表格加ECharts统计图表的实现要点前端页面不需要很复杂但要能看出“监管”的感觉。一个常用的布局是左边是上传检测区中间是历史记录表格底部是缺陷类别统计柱状图。用Bootstrap加ECharts几行代码就能做出像样的看板。table idhistory-table classtable table-striped thead tr th时间/thth缺陷图片/thth缺陷统计/thth重点关注/th /tr /thead tbody !-- 由JavaScript从 /records 接口动态填充 -- /tbody /table div idbar-chart styleheight: 300px;/div配套的JavaScript获取/records返回的JSON遍历后填表再聚合所有记录里的缺陷类别数量交给ECharts画柱状图。这里有个性能需要注意的点表格数据量变大后不要一次性加载全部记录而是在SQL里加LIMIT 100。我见过一个师弟把几千条记录全渲染出来浏览器直接卡死最后在SQL里加了个ORDER BY id DESC LIMIT 50才流畅。可视化监管系统的价值在“监管”而不在“堆数据”实时看到最近状态才是核心。4. 数据集处理与格式转换VOC转YOLO的脚本和四个边界坑4.1 为什么非转格式不可VOC的XML和YOLO的txt差别很大NEU-DET原始标注是VOC格式每个图像对应一个XML文件框坐标是xmin, ymin, xmax, ymax的绝对像素值。YOLO训练需要的是归一化后的x_center, y_center, width, height。如果直接拿XML喂给YOLO训练脚本根本读不到标签损失一直是NaN或者训练能跑但mAP为0。所以在训练前必须做一次格式转换。很多毕设源码里已经带了转换脚本但边界处理往往不完善。4.2 转换脚本从XML到txt的完整实现我常用的转换脚本如下路径都要根据实际项目调整import os import xml.etree.ElementTree as ET classes [crazing, inclusion, patches, pitted_surface, rolled-in_scale, scratches] def convert_xml_to_yolo(xml_dir, output_dir, img_dir): os.makedirs(output_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) txt_path os.path.join(output_dir, xml_file.replace(.xml, .txt)) with open(txt_path, w) as f: for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in classes: continue cls_id classes.index(cls_name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h f.write(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n) convert_xml_to_yolo(NEU-DET/XML, datasets/neu_det/labels/train, NEU-DET/IMAGES)脚本先读取XML里的图片宽高然后计算归一化的中心坐标和宽高。注意xmax - xmin和ymax - ymin是框的宽高不是对角线。写入txt时每个框占一行用空格分隔。类名不在classes列表里的对象直接跳过避免误把背景标签写进去。4.3 四个边界坑越界、空标签、路径错位和数据不平衡第一个坑是坐标越界。有些标注框的xmax或ymax会等于图片宽高归一化后得到1.0。YOLO训练时遇到x_center或width超过1的框计算损失时会出NaN。解决方法是把所有坐标clip到0到1之间x_center min(max(x_center, 0), 1) w min(max(w, 0.0001), 1)第二个坑是空标签文件。类别不在列表里时txt文件会生成但内容是空的。训练脚本遇到空txt会直接跳过这张图导致实际参与训练的图片数少于预期。所以转换后要统计一下txt的行数总和别到训练完才发现。第三个坑是路径错位。YOLO训练时会根据data.yaml里的train路径找到图片再在同目录下的labels子目录里找对应的txt。如果你的图片在images/train那标签就必须在labels/train不能混放。很多新手把标签全部放在一个文件夹里训练时一堆警告“image without labels”但训练还在继续最终效果自然很差。第四个坑是数据不平衡。NEU-DET里scratch类有较多样本而inclusion相对少。直接训练会让模型偏向多数类小样本类缺陷的mAP只有0.2。缓解办法是在train.py里加上--cls 0.5参数提高类别权重或者直接用utils/loss.py里的reweight机制。对毕设而言更简单的做法是分析每个类的图片数如果某类少于100张就手动复制几次制造简单的重复样本也能小幅提升。4.4 数据增强在缺陷检测里的特殊注意点YOLO自带马赛克、翻转、HSV扰动等增强但这些增强对工业缺陷图像并不全都友好。比如翻转对中心对称的缺陷没问题但方向性划痕翻转后类别语义不变可以开而颜色扰动对灰度图没有意义反而会造成模型过度依赖对比度。我一般在train.py里加--hsv 0 0 0关闭颜色扰动保留翻转和scale。马赛克增强对小缺陷很有效它把四张图拼在一起模型能看到更多目标但同时也让图片中的缺陷尺寸变小对最终评分有一定影响。如果你的测试集里全是原图建议把--mosaic 0.5调低到0.3左右效果会更贴近实际。5. 避坑与排查训练和部署中常见的5个翻车现场5.1 训练损失不降mAP一直为0现象训练到20轮box_loss和obj_loss一直纹丝不动mAP0.5始终是0。原因最常见的是标签路径不对YOLO在labels目录下找不到对应的txt文件所有图片都成了无标签样本模型只能跟着预训练权重瞎蒙。其次就是数据配置文件里nc和names不匹配class_id超出names列表下标标签解析出错。解决先检查data.yaml中的train和val路径是否绝对路径然后随机打开一张训练图片对应的txt看内容是否为0 0.5 0.5 0.1 0.1这种格式。再用YOLO自带的检查命令python train.py --data data/custom.yaml --weights yolov5s.pt --epochs 1跑一轮日志里会明确打印“train images X labels Y”和“val images M labels N”只要Y和N不等于图片总数说明标签有问题。还有一个隐藏点就是图片里某些缺陷太小比如只有3个像素宽即使标签正确下采样32倍后目标直接消失模型永远学不到这时要降低--img到960或改用P2检测头。5.2 误检太多背景被识别成缺陷现象模型能框出真实缺陷但也在纹理均匀的区域框出一堆假缺陷conf还不低。原因第一个原因是训练数据太少模型没有见过足够的负样本。YOLO不显式训练背景但anchor里包含大量背景框如果正样本太少模型倾向于把一切边缘纹理都当缺陷。第二个原因是置信度阈值太低我前面建议的0.25在某些数据集上还是低可以调高到0.35试试。解决先调高model.conf看误检是否减少。如果减少不明显就应该检查你的验证集和训练集图片是否同源。很多学生把同一批次的不同图片混入训练集导致模型学到的其实是拍摄角度和光照而不是缺陷特征。正确做法是确保训练集和验证集来自不同摄像头或不同时间段的采集。如果情况更严重就在训练集里加入纯无缺陷的负样本图片每张图生成空标签txt让模型学会“这里没有缺陷”。5.3 小目标缺陷漏检尤其是麻点、划痕起点现象大块划痕和夹杂都能被检测但图片里那个只有十几像素宽的麻点总是漏掉。原因YOLOv5默认下采样32倍后特征图只有20x20个网格一个在640x640图上12像素的目标对应到高层特征图不到1个像素几乎不可能被检出。工业缺陷里很多小目标这是目标检测的通用难题。解决第一个办法是把输入分辨率调大--img 1280小目标尺寸在特征图上变大漏检显著下降但对显卡要求高。第二个办法是启用YOLOv5的P2检测层在yolov5s.yaml里增加P2输出但这需要改模型结构毕设时间紧不推荐。第三个办法是使用SAHI切片推理把大图切成若干小图分别检测再合并结果这个我在第6章给出具体实现。5.4 可视化监管页面卡顿点击检测要等好几秒现象上传图片后浏览器转圈半天才出结果多开几个标签页后直接无响应。原因Flask应用默认单线程而模型推理是CPU或GPU上的长耗时操作所有请求串行排队。每个请求都等待前面的推理完成必然卡。另外如果模型是在GPU上跑的前端同时调用多个检测请求时显存共享冲突也会让速度雪崩。解决最简单的是在app.run里加threadedTrue让每个请求有独立线程至少不同浏览器标签页互不阻塞。更专业的做法是用线程池统一管理推理from concurrent.futures import ThreadPoolExecutor executor ThreadPoolExecutor(max_workers2) app.route(/detect, methods[POST]) def detect(): # 将推理任务提交到线程池立即返回任务ID future executor.submit(do_detect, request.files[image]) return jsonify({task_id: future.id})但这种方式前端需要轮询结果多做一轮交互。对毕设来说直接用threadedTrue加显式限制上传图片大小比如不超过2MB已经足够流畅。还有一个经常被忽略的点模型载入后别在每次推理前调用torch.hub.load先把model全局化。5.5 训练时显存溢出换小batch也报错现象CUDA out of memory甚至调到batch2还爆。原因--img太大或者训练时开了--cache把数据提前放入显存。另外并行开启的验证进程也占显存。解决先把--img降到512batch降到8把--workers 0关闭预加载进程减少CPU和GPU之间的数据缓存占用YOLOv5默认在训练最后10轮做自动锚框优化也会短暂增加显存消耗可以在train.py里加--noautoanchor跳过。如果仍然爆就放弃训练使用现成的预训练模型做迁移学习测试——毕设只需要证明“这个方法有效”不一定要从零训爆显存。6. 进阶用Grad-CAM解释缺陷定位并针对小缺陷做切片推理这一章解决两个进阶问题答辩时老师问“你怎么证明模型学的区域对得上缺陷”以及真实场景里小目标缺陷的漏检怎么兜底。第一个技巧是用Grad-CAM生成热力图。这个工具体现了深度学习在黑匣子里的可解释性很适合在论文里放一张对比图。需要安装pytorch_grad_cam库然后用一个能输出特征图的中间层来反传梯度。对于YOLOv5常用的是最后一个卷积层model.model.model[-2]。示例代码from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget from pytorch_grad_cam.utils.image import show_cam_on_image import cv2 import torch model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/neu_run1/weights/best.pt, force_reloadTrue) # 选择最后一个卷积层作为特征提取层 target_layer model.model.model[-2] cam GradCAM(modelmodel, target_layers[target_layer], use_cudaTrue) img cv2.imread(test_inclusion.jpg) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) / 255.0 tensor torch.from_numpy(img_rgb).permute(2, 0, 1).unsqueeze(0).float() targets [ClassifierOutputTarget(1)] # 假设类别1是inclusion grayscale_cam cam(tensor, targetstargets)[0] visualization show_cam_on_image(img_rgb, grayscale_cam, use_rgbTrue) cv2.imwrite(cam_result.jpg, visualization * 255)target_layer必须是卷积层Grad-CAM需要卷积输出的特征图。ClassifierOutputTarget的第二个参数是类别索引要和你data.yaml里的names对应。生成的visualization会叠加在原图上红色区域就是模型重点关注的区域。如果热力图集中在缺陷位置附近说明模型确实学到了缺陷特征如果热力图漫无边际说明模型在偷懒用全局纹理信息猜类别这时要多加训练数据或增强。第二个技巧是切片推理。把大图按重叠方式切成小块每块单独推理最后合并所有框。这个方式能大幅提高小目标召回率代价是推理时间翻几倍。核心实现如下def slice_inference(model, img_path, slice_size320, overlap0.2): img cv2.imread(img_path) h, w img.shape[:2] boxes [] stride int(slice_size * (1 - overlap)) for y in range(0, h, stride): for x in range(0, w, stride): x2 min(x slice_size, w) y2 min(y slice_size, h) slice_img img[y:y2, x:x2] # 推理切片图 results model(slice_img, sizeslice_size) for x1, y1, x2_, y2_, conf, cls in results.xyxy[0]: # 把坐标映射回原图 boxes.append([x1 x, y1 y, x2_ x, y2_ y, float(conf), int(cls)]) # 合并重叠框用NMS去重略 return boxes切片的overlap设0.2可以防止缺陷正好卡在切片边界上。合并框时要注意同一缺陷可能被多个切片检出需要用NMS去重。切图推理的策略在我做钢板缺陷检测时救过很多次3000x3000的大图在GPU上直接推理漏检率高达30%切片后降到8%以下。缺点是每张图推理时间从0.2秒变成5秒所以只适合离线质检不适合实时监控。如果你在毕设系统里做了这个功能记得标注“高精度模式”和“实时模式”两种选择能在答辩时很加分。最后说一个我的习惯拿到任何毕设源码先备份原始权重和数据集再修改代码。我吃过一次亏改了data.yaml后忘记备份还原时只能重新下载整个数据集浪费了半天。现在我会把每个实验的配置和权重单独存一个目录命名带上日期和改动说明这个习惯让我回滚实验时从不慌。希望这些避坑记录能让你少走几段弯路祝你的毕业设计顺利跑通、漂亮通过。本文还有配套的精品资源点击获取