ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于YOLO的黑烟车自动识别:从数据集构建到部署全流程

基于YOLO的黑烟车自动识别:从数据集构建到部署全流程 简介面向计算机视觉毕业设计及环保监管需求一套基于深度学习的黑烟车自动识别系统完整项目可直接用于目标检测实战学习或毕设方案复现。项目以主流目标检测方法为核心自建带标注黑烟车数据集配合大量图像增广、ResNet骨架网络搭建、迁移学习与网络参数调优等环节最终模型在测试集上达到0.9752的mAP展现了从数据处理到模型评估的完整技术路线。压缩包共2000个文件大小75.27MB结构上既有902张jpg图像与988份xml标注组成的数据集、18个py格式的模型训练与推理源码也有多份doc/docx格式的毕业论文、开题报告、中期检查表及pptx答辩PPT、pdf论文等材料覆盖毕业设计各阶段所需文档。目前已有358人学习适合需要完整参考项目、希望复现黑烟车识别效果或想学习ResNet目标检测调优思路的高年级本科生与研究生。1. 黑烟车自动识别计算机视觉毕设里少有的“能直接拿去答辩”的方向每年计算机视觉毕业设计选题翻来覆去就是车牌识别、人脸检测、垃圾分类。黑烟车自动识别这个方向相对冷门但它恰好踩中了两个关键点一是环保监管有真实需求不少城市已经部署了黑烟车电子抓拍系统二是技术栈完整覆盖“目标检测 视频时序判定”从数据集构造到模型部署能形成一条闭环论文也好写。这套基于深度学习的黑烟车自动识别系统核心是让模型在卡口画面里同时完成两个任务定位车辆位置再判断尾气排放区域是否有黑色烟羽。做完之后你拿到的不仅仅是一个能跑的检测脚本而是一整套可以写进毕业论文的数据处理流程、训练配置、评估指标和部署方案。新手能靠它把深度学习目标检测的完整链路走通熟手则能把注意力放在误报抑制和场景泛化这些真正有区分度的问题上。2. 黑烟车识别到底是个什么任务检测目标、算法选型与数据集来源2.1 为什么不能简单用图像分类黑烟识别的任务本质是“定位 判定”很多人第一次拿到这个题目第一反应是“用 CNN 判断一张图里有没有黑烟”。这个思路方向没错但在真实卡口场景里根本没法落地。原因是黑烟不是独立存在的东西它附着在车辆尾部你必须先知道车在哪、尾气口在哪才能判断那片黑色区域是烟还是阴影、是车身上的黑色涂装还是后方建筑的暗部。图像分类给不了位置信息模型只会告诉你“这张图有黑烟概率 87%”但执法和审核人员需要的是“哪辆车在冒黑烟”这就要求算法输出包围框级别的结果。所以黑烟车自动识别在技术实现上是典型的目标检测任务输入一张卡口抓拍图或者视频帧输出若干个 bounding box每个框要么是“vehicle”车要么是“smoke”黑烟再通过框之间的空间关系——烟框是否出现在车框的尾部区域——决定这辆车是否属于黑烟车。这里的核心模型选型我一般建议直接上 YOLO 系列原因有三个检测速度够快卡口场景要求实时处理多路视频流YOLO 在 GPU 上轻松跑过 60 FPS训练生态成熟从数据标注到部署工具链都齐全论文里好讲故事跟 Faster R-CNN 做对比实验时YOLO 在帧率上的优势明显能体现你的工程判断。2.2 数据集从哪来公开数据、自采视频抽帧和“绕不开的人工标注”黑烟车识别没有像 COCO 那种现成的大规模公开数据集这是这个课题最现实的门槛。常见的做法是三条路并行第一条从环保部门或科研机构公开的黑烟车抓拍样本里收集图片这类数据质量高但数量有限通常就几百到一千张。第二条去网上找柴油车冒黑烟的短视频按 5 到 10 帧间隔抽帧能快速扩充样本量但画面背景复杂很多含烟帧里的烟淡得人眼都难判定。第三条也是工作量最大的自己标注。我一般用 LabelImg 或者 LabelStudio标注类别就两类——vehicle 和 smoke输出成 YOLO 格式的 txt 文件每行是“class x_center y_center width height”坐标全部归一化到 0 到 1 之间。# 用 LabelImg 标注后生成的单个 txt 文件内容示例 # 第一列是类别索引0 代表 vehicle1 代表 smoke # 后面四列分别是归一化后的中心点 x、中心点 y、框宽、框高 0 0.512 0.385 0.238 0.172 1 0.548 0.412 0.083 0.064python# 检查标注文件是否越界的脚本片段 # YOLO 格式要求所有坐标归一化到 [0,1]越界会导致训练损失异常 def check_labels(label_path, img_w, img_h): with open(label_path, r) as f: lines f.readlines() for line in lines: parts line.strip().split() cls int(parts[0]) x_c, y_c, w, h map(float, parts[1:]) # 越界检查中心点和宽高都必须落在合法区间 if not (0 x_c 1 and 0 y_c 1 and 0 w 1 and 0 h 1): print(f越界标注: {label_path}, class{cls}, box({x_c},{y_c},{w},{h})) return False return True # 使用示例标注文件路径、对应图片的宽高作为参数传入 # check_labels(labels/0001.txt, 1920, 1080)代码里这个越界检查脚本虽然简单但几乎每次整理黑烟数据集都会用到。因为采集的视频帧分辨率不统一有的标注工具导出时坐标没做归一化或者标注过程中手滑拉出了画布边界这些脏标注不会直接报错但会让训练开始后 loss 出现诡异波动。你调试半天找不到原因最后发现只是几十个框的坐标超了边界。需要提醒的是抽烟区域标注有很强的主观性。同一团烟A 标成一个大框B 标成三个小框模型学到的特征就会不一致。我自己的经验是定一条简单的规则烟框必须完整包含尾气排放口周围最浓的烟羽区域宁可框大一点也不要只框烟柱的一部分。框太碎会让模型学到一堆局部纹理检测时经常把一个烟框炸成好几个小框后续按空间关系判定“烟是否在车尾”时逻辑会变得很难写。2.3 YOLOv5 还是 YOLOv8毕设场景下怎么选不给自己挖坑算法版本选择看起来是小事实际影响整个训练和部署节奏。YOLOv5 虽然官方仓库已经停止更新但它的生态积累最厚网上的中文教程、权重文件、部署案例都最多遇到报错搜一下基本都有答案。YOLOv8 的优势是训练代码更规范、默认参数更省心还内置了旋转框和关键点检测支持但相对的如果你要改检测头或者做自定义后处理源码结构比 v5 复杂一截。对毕设来说我认为选 YOLOv5 更稳妥不是因为 v5 精度更高而是因为“遇到问题能搜到解决方案”这个优势在赶论文的时候比什么都重要。选 v5 的话版本锁 6.0 或 7.0 都行注意别用最新的 commit有时候上游改动会带来兼容性问题。数据、标注、配置文件这套流程在两个版本间基本通用后面论文里写“采用 YOLOv5s 作为基线模型”评审老师也不会挑毛病。如果你想把 v8 写进论文增加工作量那也完全可以但要预留至少一周专门处理 v8 新增的数据增强策略带来的训练行为变化。3. 跑通训练全流程从目录结构到 YOLOv5 训练命令的完整复现3.1 数据集目录怎么摆最不容易出错的 images/labels 双目录结构训练前的数据整理是黑烟车项目里最枯燥但最重要的一步。我见过太多同学把图片和标注文件放在同一个目录然后在 data.yaml 里写路径时把自己绕晕。YOLOv5 的官方约定是图片和标注文件分目录存放标注目录结构和图片目录完全镜像。下面这个结构我用了很多次直接抄就行black_smoke_dataset/ ├── images/ │ ├── train/ # 训练集图片约 70% │ ├── val/ # 验证集图片约 20% │ └── test/ # 测试集图片约 10% ├── labels/ │ ├── train/ # 训练集标注跟 images/train 一一对应 │ ├── val/ │ └── test/ ├── data.yaml # 数据集配置文件 └── 数据集划分说明.txtbash# 一键完成数据集随机划分的脚本在数据集根目录执行 # 按 7:2:1 比例把图片和对应的 txt 标注同步划分到 train/val/test python split_dataset.py \ --image_dir images_all \ --label_dir labels_all \ --output_dir black_smoke_dataset \ --train_ratio 0.7 \ --val_ratio 0.2python# split_dataset.py 核心逻辑 import os, random, shutil def split_dataset(image_dir, label_dir, output_dir, train_ratio0.7, val_ratio0.2): images [f for f in os.listdir(image_dir) if f.endswith((.jpg, .jpeg, .png))] random.shuffle(images) # 随机打乱避免连续帧全部进入同一集合 n_train int(len(images) * train_ratio) n_val int(len(images) * val_ratio) splits [ (train, images[:n_train]), (val, images[n_train:n_train n_val]), (test, images[n_train n_val:]) ] for split_name, split_images in splits: os.makedirs(f{output_dir}/images/{split_name}, exist_okTrue) os.makedirs(f{output_dir}/labels/{split_name}, exist_okTrue) for img in split_images: base os.path.splitext(img)[0] label_file f{base}.txt # 只拷贝图片和标注都存在的样本避免训练时读到空标注 if os.path.exists(os.path.join(label_dir, label_file)): shutil.copy(os.path.join(image_dir, img), os.path.join(output_dir, fimages/{split_name}/{img})) shutil.copy(os.path.join(label_dir, label_file), os.path.join(output_dir, flabels/{split_name}/{label_file})) print(数据集划分完成)这份脚本逻辑上是常规操作但有一个细节值得说拷贝时判断标注文件是否存在。实际标注过程中总会漏掉几张图没标如果硬把它们放进训练集YOLOv5 会把空标注文件当成“没有目标”的负样本处理看起来没问题但其实那张图上明明有车模型就被错误信息干扰了。宁可漏标图直接不放进去也别让它进训练集。3.2 data.yaml 配置文件类别名、路径和验证集指向YOLOv5 启动训练前只需要一个 yaml 文件说明数据集在哪、一共有几类。黑烟车数据集就是两个类别vehicle 和 smoke。文件内容如下# data.yaml — 黑烟车数据集的配置文件 # train 和 val 是必须的test 可选推理评估时用不到 path: /home/user/black_smoke_dataset # 数据集根目录建议写绝对路径 train: images/train # 训练图片目录相对 path val: images/val # 验证图片目录相对 path test: images/test # 测试图片目录相对 path nc: 2 # 类别总数vehicle 和 smoke names: [vehicle, smoke] # 类别名称顺序必须和标注文件里的索引一致注意names的顺序绝对不能乱。如果标注文件里 0 是 vehicle、1 是 smoke但 yaml 里把顺序写反了模型会正常训练、mAP 看着也不低但推理时输出的类别就全反了。这种错误排查起来特别费时间因为它不报错只是结果错得离谱。路径方面path字段建议写绝对路径不同机器之间虽然不方便迁移但对新手来说绝对路径少踩很多相对路径解析的坑。3.3 训练命令与关键参数batch-size、epochs、imgsz 怎么定数据集准备好配置文件写好就可以启动训练了。下面是完整的训练命令我加了一些黑烟车场景特有的参数调整逻辑# 在 YOLOv5 源码目录下执行项目根目录 python train.py \ --data /home/user/black_smoke_dataset/data.yaml \ # 数据集配置 --cfg models/yolov5s.yaml \ # 模型结构配置 --weights yolov5s.pt \ # 预训练权重 --batch-size 16 \ # 按 GPU 显存调整 --epochs 100 \ # 黑烟检测建议 100 起步 --imgsz 640 \ # 输入分辨率 --device 0 \ # GPU 编号 --name black_smoke_exp参数方面重点解释四个。batch-size的默认值 16 在 8G 显存的卡上能用如果显存 6G 就降到 8否则会出现 CUDA out of memory。epochs设 100 是黑烟数据集的特性决定的——样本量通常就一两千张数据集小模型学得太快容易过拟合100 轮能让损失曲线下降得更平稳。imgsz用 640 是速度和精度的折中卡口原图很多是 1920x1080直接输入 640 会丢失远处小目标的细节但如果硬上 1280显存和推理速度都会很难看。我的经验是先用 640 跑通全流程最后如果有时间再用 1280 微调几轮做对比实验论文里能多一个结论。# 如果训练中断想接着上次的权重继续 python train.py \ --data /home/user/black_smoke_dataset/data.yaml \ --cfg models/yolov5s.yaml \ --weights runs/train/black_smoke_exp/weights/last.pt \ --batch-size 16 \ --epochs 100 \ --imgsz 640 \ --resume--resume参数配合last.pt权重可以断点续训这在数据集大、单次训练跑好几个小时的时候几乎是保命功能。跑着跑着电脑休眠了、显存炸了、机房断电了不用从头来过。3.4 训练过程看什么loss 曲线和 mAP 曲线怎么判断“学没学好”训练开始后YOLOv5 会在runs/train/black_smoke_exp/目录下生成训练日志和图表重点看两个文件results.csv和results.png。results 里包含 box_loss、obj_loss、cls_loss 以及验证集的 mAP_0.5 和 mAP_0.5:0.95。黑烟车项目里我判断训练是否正常的经验性标准是三条第一训练集 box_loss 应该在 0.08 以下如果反复降到 0.05 以下要考虑模型过拟合。第二验证集 mAP_0.5 至少要达到 0.75 以上否则说明模型没学到有效的烟羽特征。第三mAP_0.5:0.95 和 mAP_0.5 的差距如果超过 0.2说明检测框的位置精度偏差烟的边界本来就模糊框和真实标注贴不紧很正常但差距太大就要检查标注质量。最后还要做一件事打开验证集预测结果图看一眼。YOLOv5 会保存一批带预测框的验证集图片肉眼看一遍比任何指标都直观。黑烟检测特别要看的是——模型有没有把路边的黑色标牌、桥墩阴影、货车车厢误检成烟。如果这类误检多说明数据集中负样本不够得专门补充“有黑色物体但没有烟”的图片进数据集。4. 模型评估与导出部署从测试集指标到 ONNX 落地4.1 用测试集算指标mAP、Precision、Recall 分别说明什么问题验证集用于训练过程中实时评估但论文里真正有说服力的是测试集指标——测试集是训练过程中从未见过的数据指标才能反映模型的真实泛化能力。YOLOv5 跑测试集直接用 val.py 脚本指定测试集数据配置和训练好的权重python val.py \ --data /home/user/black_smoke_dataset/data.yaml \ --weights runs/train/black_smoke_exp/weights/best.pt \ --imgsz 640 \ --task test \ --save-json \ --conf-thres 0.25 \ --iou-thres 0.5--save-json会额外输出 COCO 格式的评估结果方便你用第三方工具做更细的分析。--conf-thres是置信度阈值默认 0.25检测黑烟时我建议调到 0.3因为烟的外观不确定性强低置信度的预测框里混着大量误检把它调高一点能换到更干净的 Precision 曲线。下面是可能得到的验证输出示例帮你建立一个数值预期Class Images Instances P R mAP50 mAP50-95 all 200 356 0.841 0.792 0.813 0.601 vehicle 200 214 0.906 0.883 0.931 0.748 smoke 200 142 0.776 0.701 0.695 0.454这个结果其实是典型的黑烟车模型表现车辆检测的 mAP50 能到 0.9 左右因为车的外形规整、特征清晰烟的 mAP50 能到 0.7 就算不错了因为烟的形状、浓度、光照变化太大。论文里写指标时最好把两类的指标拆开写不要只写一个 total因为“烟类指标明显低于车辆类”这件事本身就能引出后续的改进方向比如加入视频时序信息来提升烟类检测的可靠性。4.2 导出 ONNX 与部署到推理服务毕设做完模型训练后通常得有部署演示才有完整性。最常见的部署形态是把模型导出成 ONNX再通过 ONNX Runtime 或 OpenCV DNN 模块加载做推理。导出 ONNX 的命令如下python export.py \ --weights runs/train/black_smoke_exp/weights/best.pt \ --include onnx \ --imgsz 640 \ --dynamic--dynamic参数让 ONNX 模型支持动态输入尺寸部署时比较灵活但推理性能会比固定尺寸稍慢。如果只需要固定 640x640 输入去掉--dynamic就行。导出完成后用 ONNX 文件做单张图片推理的 Python 代码也不复杂import cv2 import numpy as np import onnxruntime as ort # 加载 ONNX 模型开启 GPU 加速如果安装了 GPU 版 onnxruntime session ort.InferenceSession(best.onnx, providers[CUDAExecutionProvider, CPUExecutionProvider]) def preprocess(img): img cv2.resize(img, (640, 640)) img img[:, :, ::-1].transpose(2, 0, 1) # BGR 转 RGB并调整通道顺序为 CHW img np.ascontiguousarray(img, dtypenp.float32) img / 255.0 # 归一化到 [0,1] img img[np.newaxis, :] # 增加 batch 维度 return img def postprocess(outputs, conf_thres0.3, iou_thres0.5): # outputs shape: [1, num_anchors, 5num_classes] # 需要做 NMS 过滤冗余框YOLOv5 源码里的非极大值抑制逻辑可直接复用 # 这里省略 NMS 的完整实现实际部署时直接调用 utils/general.py 里的 non_max_suppression boxes outputs[0] # 剩下就是按置信度过滤、NMS、按类别区分 vehicle/smoke return boxes img cv2.imread(test_frame.jpg) input_tensor preprocess(img) outputs session.run(None, {session.get_inputs()[0].name: input_tensor}) results postprocess(outputs) for box in results: x1, y1, x2, y2, conf, cls box label vehicle if int(cls) 0 else smoke cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(img, f{label} {conf:.2f}, (int(x1), int(y1) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(result.jpg, img)部署层面的建议是如果机器上没装 CUDA就用 CPUExecutionProvider单帧推理时间在 100 到 200 毫秒之间作为毕设演示完全够用。后续如果想做视频流的逐帧检测直接用一个循环不断读取视频帧、推理、画框、写回输出视频就行不需要引入复杂的流媒体框架。5. 黑烟车识别系统的 5 个典型“翻车”场景现象、原因与解决记录做黑烟车识别的过程里我踩过的坑比写出来的代码还多。下面五条是最典型、最容易反复出现的每一条都按现象、原因、解决三步写清楚看到相似问题直接照做就行。踩坑一训练 loss 一直在降但验证集 mAP 几乎为零。这个现象最诡异的地方在于——数值上模型学得很好但实际一个烟框都检测不出来。后来排查发现是标注坐标归一化出了问题。收集的数据集里有一部分图片是从视频里抽帧出来的原始分辨率是 1920x1080而另一部分截图是 1280x720标注工具导出的坐标是像素绝对值没有统一归一化。模型同时看到两种尺度下的坐标分布直接学崩了。解决办法是写统一的数据预处理脚本把像素坐标统一除以各自图片的宽高重新生成归一化标注然后再次验证坐标范围全部落在 [0,1] 区间内。踩坑二车辆检测正常但烟类 mAP 低得离谱训练到 60 轮后完全不涨。版本上一版 mAP 还是 0.6 多加了一百多张新标注的烟图后反而掉到 0.3。原因很意外——新标注的烟框风格跟旧数据不一致。旧数据是框中度烟羽新数据里很多是刚冒头的一小团烟框特别小。小目标框在 YOLOv5 的 640 输入下只占十几个像素特征极弱模型学不到规律。这个问题的常规解法是把小目标框对应的原图区域裁剪放大后单独做一份训练数据让模型见过“放大的烟”。我实际做的时候是把这类小目标样本复制三份放到训练集里参与过采样再把--img-size从 640 提到 896烟类 mAP 从 0.42 升到了 0.55。踩坑三模型把黑色的卡车车厢误检成黑烟。这是黑烟车识别最典型的语义混淆。深色货柜车在逆光环境下车身跟黑色烟羽在视觉特征上非常接近模型很容易混淆。问题的根源是训练数据里缺少“深色车辆但没有烟”的负样本。解决办法是刻意从所有采集视频里筛选出深色车辆的画面把它们标注为只有 vehicle 没有 smoke作为负样本补充进数据集。这类样本我加了大约 200 张误检率显著下降。这块经验写在论文里也非常加分因为负样本挖掘是深度学习落地的核心技巧。踩坑四验证集 mAP 很高但实际测试视频里漏检严重。现象是静态测试集指标很好看一跑视频就发现远处的黑烟车完全检测不到。原因在于数据分布不一致——测试集里多数图片是在卡口近处拍的烟羽大、清晰而视频里的烟车出现在视野远端烟羽占图面积小纹理模糊。这个问题的本质是尺度泛化解决思路是在训练数据里主动混入不同距离下的样本并根据距离对烟框面积做统计看看模型在哪些尺度上表现差。另外对远景帧做一次双线性放大再送入模型也能有效提升检出率。踩坑五视频流推理时 GPU 显存不断增长最后程序崩溃。单帧图片推理完全正常但把代码改成摄像头视频流逐帧推理后显存每帧涨几百兆跑几分钟就 OOM。问题几乎肯定出在代码里每帧推理后没有释放中间张量或者数据加载的队列积压过多。解决方法是确保每帧推理循环里session.run()的输出变量被复用而不是反复创建新列表另一个常被忽略的点是 OpenCV 的VideoCapture如果读取速度大于推理速度会不断往队列里塞帧必须在循环里加帧丢弃逻辑只处理最新的那一帧。显存涨但是没到 OOM 的情况下可以用下面这段代码手动清一次缓存但不能依赖它解决根本问题import gc import torch # 每处理 200 帧后清理一次临时张量防止显存碎片化累积 def clean_memory(): gc.collect() torch.cuda.empty_cache()解决完上面这几个问题之后黑烟车识别系统基本就到了可以稳定运行的状态。剩下的工作就是围绕论文把实验补齐、把系统演示录好。6. 进阶技巧用车辆尾部的时序判定把误报率再降一半基础版模型做到能检测烟、能画框、能跑视频是一个合格的毕设。但如果你想在答辩里多一个亮点或者论文里多一个创新点我建议在检测结果之上加一层时序判定逻辑。思路很简单——单帧画面里烟的形状跟很多物体的局部特征相似误检免不了但如果连续几帧都在同一个位置出现形状、位置连贯变化的烟框那几乎不可能是误检。反过来只有一帧出现烟框、下一帧就消失的大概率是树叶阴影或者车顶反光的误检。我的做法是在检测器之上维护一个烟框轨迹列表。对每一帧检测出的 smoke 框跟上一帧的烟框做 IoU 匹配如果 IoU 超过 0.3 就认为是同一团烟更新轨迹的持续时间如果连续 N 帧通常设 3 到 5都匹配到同一位置就确认这是真实黑烟事件触发报警。这里的 N 值是一个关键参数设太大会漏掉短暂的黑烟排放设太小又会有不少瞬时误检混进来。我自己的经验是卡口场景取 3 帧最好车辆在画面里的时间窗口也就几秒3 帧既不会误伤真实排放又能滤掉大多数闪烁误检。这个时序层还能做一个额外的好处给论文提供“单帧检测 时序确认”的完整方法叙述。你可以在论文里画一张流程对比图左边是单帧直接检测输出的结果右边是经过时序过滤后的结果误检数量一目了然。实验部分对比一下两种方式的 Precision 和 Recall用数据证明时序判定的价值。做完这一步这个毕设的完成度就从“训练了一个深度学习模型”升级到了“实现了一套可落地的黑烟车自动识别系统”答辩时也更有底气讲清楚每一层的作用和参数选择依据。如果时间有富余建议再录一段完整路测视频作为系统演示材料画面里包含正常车、黑烟车、深色车误检场景各一段。调整 N 值让系统在演示中做到不误报、不漏报这段录屏比任何图表都有说服力。我自己当年就是在最后一周把这个时序判定加到系统里然后反复看跑测视频盯误报每天调参数调到头昏但答辩时这段功能展示确实值回票价。希望这套流程能帮你少走一些我已经趟过的弯路。本文还有配套的精品资源点击获取
返回列表