
简介面向计算机相关专业学生和深度学习实践者这套基于深度学习的黑烟车自动识别系统毕业设计资源完整覆盖数据准备、模型训练到结果分析的项目流程。项目原为个人毕业设计经导师指导并获得98分高分所有源码均已在本地编译运行通过经过严格调试可直接使用。资源包内含约2000个文件压缩后大小为74.61MB主要材料包括XML格式的标注文件、JPG格式的车辆图像以及可执行的Python脚本和论文写作参考文档标注文件可用于模型训练与评估车辆图像覆盖道路场景脚本包含完整的训练测试流程能够为复现实验和撰写毕业设计论文提供清晰支撑。此外资源附有详细使用说明降低上手门槛。目前已有64人学习下载整体难度适中特别适合正在准备大作业、毕业设计的高校学生也可供希望进行项目实战练习的开发者参考借鉴。1. 黑烟车识别用深度学习要过的不只是检测这一关路口的电子卡口和尾气遥感门架每天能拍下几万帧货车尾部画面。传统做法是人工回看视频对着林格曼色卡判断黑度等级这套流程放到海量车流面前根本不现实。黑烟车自动识别系统的目标是把“这辆车冒的是不是黑烟、黑烟到什么程度”这件事交给模型去干。模型本身并不神秘目标检测加分类就能覆盖大半需求真正把系统工程化卡住的往往是两件事一是烟羽这种半透明、无刚性轮廓的目标通用检测器对它很不友好二是这类专门的公开数据集太少很难做到下载即用。对想拿这套课题做毕业设计的人以及对环保、交管领域做视觉方案的工程师这篇文章把任务定义、数据集构造、训练参数和部署判定逻辑完整串起来讲。2. 任务定义先于算法烟羽检测与黑度分级怎么做2.1 黑烟在图像里到底长什么样很多人第一次接触这个课题时会直接套用行人检测、车辆检测的思路把“黑烟”当成一个普通目标物来框。但实际上黑烟烟羽在图像里的性质很特殊它没有稳定的边缘颜色从浓黑到浅灰连续过渡透明度随尾气浓度和拍摄角度变化还经常和车底阴影、路面水渍、柴油车刚启动时的水汽混在一起。拿通用目标检测的打法去硬套最大的问题是边界框的置信度会来回抖同一个车道上两辆外观几乎相同的车一辆冒烟一辆不冒模型很容易被车尾的反光带偏。从视觉特征上看黑烟区别于白烟和水汽的关键在于黑烟颗粒会同时压低 RGB 三个通道的亮度而且三个通道的响应相对均衡没有明显的色相偏向。白烟和水汽反射环境光往往局部过曝或带冷色调。也就是说模型要学的不是烟的形状而是烟羽区域的“材质感”。这个判断决定了后面数据标注、增强策略和模型输出的设计不能简单当作一个多分类问题来对待。2.2 把“黑烟车”拆成两个子任务定位烟羽、判定等级完整的黑烟车识别链路我一般拆成两个子任务来做第一是烟羽定位也就是找到画面里哪一个区域正在冒黑烟第二是黑度分级也就是判断这个烟羽的林格曼黑度等级。在实际环保抓拍场景里通常黑度达到林格曼 1 级及以上就会被判定为可见黑烟属于需要上报的范畴所以输出可以设计成“无烟 / 1级 / 2级 / 3级及以上”这样的离散标签也可以设计成连续黑度值回归。林格曼等级视觉表现类别标签建议0 级无可见烟羽或极轻微水汽smoke_01 级浅灰烟羽略微遮挡车尾特征smoke_12 级中灰烟羽车尾纹理已不清晰smoke_23 级以上浓黑烟羽轮廓明显且持续smoke_3把定位和分级拆开有一个实际好处如果你手头数据量有限可以先只做一个二分类模型有黑烟 / 无黑烟把黑度分级放到后处理阶段用图像统计特征去估算。在数据量不足的情况下这样比训练一个多类别检测器稳定得多。2.3 检测方案选型二阶段、端到端还是分割这个课题里常见的方案有三种实际效果和工程成本差别明显方案结构优点缺点二阶段检测分类YOLO 检测烟羽区域再裁剪区域给轻量分类器判断黑度数据需求低黑度分级可控性好链路稍长推理多一步端到端多类别检测直接让检测器输出 smoke_0/1/2/3 类别部署简单一次前向出结果需要足够的类别均衡数据分割黑度计算U-Net/DeepLab 分割烟羽像素再统计等效黑度黑度量化精细可输出连续值标注成本最高算力需求大对毕业设计或早期原型来说二阶段结构最稳。黑度分类器可以使用 ResNet 之类几十 MB 的轻量模型对算力要求很低。端到端方案更适合数据量已经超过 1 万张、且每个黑度等级都有较均衡样本的情况。分割方案虽然看起来很专业但如果最终交付物只需要上报“车牌 时间 黑度等级”它的复杂度可能是不必要的。2.4 从视频流里批量抽帧先攒出候选集无论选哪条路线你都需要先把监控视频按帧切出来。常见的做法是先用 OpenCV 按间隔抽帧再用一个已经具备一般目标检测能力的模型把完全不相关的帧滤掉只保留检测到车辆尾部的帧作为候选。这样能大幅减少后面人工看图的成本。import cv2 video_path traffic_cam_2024_12_01.mp4 cap cv2.VideoCapture(video_path) frame_idx 0 save_count 0 interval 25 # 每25帧取1帧相当于1秒取1帧25fps视频 while True: ret, frame cap.read() if not ret: break if frame_idx % interval 0: # 统一缩放到1280x720减少标注时的分辨率差异 resized cv2.resize(frame, (1280, 720)) cv2.imwrite(fcandidates/frame_{save_count:06d}.jpg, resized) save_count 1 frame_idx 1 cap.release() print(f共抽取 {save_count} 帧原始总帧数 {frame_idx})这里的关键参数是interval。我建议先按每秒 1 帧抽取而不是每帧都存。黑烟从排气管喷出到扩散消失通常持续 1 到 3 秒每秒 1 帧足够捕捉到烟羽形态变化的起始和峰值又能避免相邻帧高度相似造成的数据冗余。抽帧后如果发现正样本比例过低再针对出现黑烟的时间段加密抽样。抽出来的候选帧必须做一次跨摄像机场景去重。不同路口的背景差异很大如果训练集和验证集出自同一个机位会让指标虚高部署到新点位时模型能力被严重高估。3. 黑烟车数据集的核心难点公开数据集少正样本要自己挖3.1 能用的公开数据从哪里找做这个课题最常被问到的问题就是有没有现成的公开数据集可以直接下载现实情况是黑烟车识别不像 COCO、KITTI 这类通用视觉数据集或者自动驾驶数据集一样有成熟的大规模公开资源。搜索能找到的往往是一些论文附带的小规模样本或者环保部门发布的执法案例图片数量级在几百到几千张且没有统一的标注格式。所以大概率你要自己构造数据集。素材来源有几个相对靠谱的渠道一是公开的交通监控直播流二是货车聚集区的固定机位拍摄素材三是环保部门主动公开的冒黑烟车辆抓拍公示图。前两者能获得正负样本第三种虽然样本量小但往往是真实判定过的结果可以用来做验证集。采集时有一个容易被忽略的点负样本的多样性比正样本的数量更重要。晴天的烟、雨天的烟、隧道内灯光下的烟、逆光的烟差别非常大。宁可正样本只有 2000 张但覆盖了 10 种以上光线条件也比集齐 5000 张同一机位的正样本更有实用价值。3.2 标注口径定了模型才有讨论意义3.2.1 类别定义单类别还是多等级做标注之前先定标签方案。如果你走二阶段路线检测器只负责输出一个类别smoke黑度等级交给分类器如果你走端到端路线则需要按 L1、L2、L3 分多个类别标注。多类别标注的主观差异很大同一个烟羽不同标注员可能给出不同等级所以我会要求标注员优先保证框得准再保证等级大致对。等级误差在训练时可以容忍定位误差却会直接带崩检测头的收敛。3.2.2 框的边界紧贴烟羽还是包住车尾我见过有人把黑烟框得非常保守只框排气管出口那一小块高浓度区域也见过有人把整个车尾和烟羽全部圈进来。前者的问题是模型无法学到烟羽扩散的形态后者的问题是大量非烟羽的车辆像素被划入正样本区域降低判别力。建议框选烟羽整体外接矩形从排气管出口开始到烟羽可见轮廓的末端为止不包含车体。另外要注意标注文件的格式。如果最终用 YOLO 系列训练就使用class x_center y_center width height的归一化 txt 格式。整理一份标注统计表可以帮你快速发现数据问题统计项数值备注总帧数8600含正负样本正样本烟羽标签数3200单帧可能多车同时冒烟白天/夜间/黄昏占比70% / 15% / 15%夜间样本需要额外补雨天样本占比8%雨天干扰大需单独评估3.3 用半自动标注把人工成本压下来完全人工标注 3000 个烟羽框一个人至少需要两天多时间。半自动标注是更现实的路径先用第 2 章训练出的第一版弱模型跑一遍所有候选帧输出置信度预测低于阈值的直接丢弃高于阈值的生成伪标签。伪标签肯定有不少错的但把它当作人工复核的起点效率比从零画框高很多。from ultralytics import YOLO model YOLO(pretrained_initial.pt) results model.predict( sourcecandidates/, conf0.25, save_txtTrue, save_confTrue, classes[0] # 类别0为烟羽类 ) print(伪标签生成完毕请人工复核 conf 在 0.25-0.8 之间的框)conf参数控制伪标签的淘汰阈值。我通常把阈值设在 0.25低于 0.25 的框大概率是背景噪声不生成伪标签0.25 到 0.8 之间的框由人工逐一确认高于 0.8 的框可以直接保留。save_conf 保证每行 txt 标签末尾带置信度方便你在清洗时通过脚本批量删掉置信度低且面积小于某个像素阈值的框。3.4 增强策略里哪些真有用、哪些会帮倒忙增强策略参数建议对黑烟车任务的风险水平翻转概率 0.5安全烟羽形态左右对称上下翻转概率 0.3仅限无天空参照的早期训练标注框语义不受影响HSV 色彩抖动h 0.01, s 0.3, v 0.3hue 抖动过大会让黑烟带色相必须压得很低Mosaic 拼接概率 0.5关闭于最后 20 轮烟羽被切断的风险注意 close_mosaic随机裁剪scale 0.5-1.0不能裁掉排气管附近的烟羽起点色彩抖动里hue参数对黑烟检测伤害最大。黑烟之所以是黑烟就是因为它没有明显色相一旦把色相范围放大模型可能学到“偏蓝的暗色区域”这种伪特征部署到傍晚场景时会误报。灰度化增强反而是有效的因为黑烟判定的核心是亮度压暗灰度图保留了亮度信息还能丢掉一部分色彩干扰。关于 Mosaic黑烟烟羽往往超出单辆车的范围拼接图里很容易出现两个不同来源的烟羽被拼接到同一个框中。因此我会把 mosaic 概率降到 0.5并且在训练最后 20 个 epoch 关闭它让网络在接近真实分布的数据上收敛。4. 用YOLOv8跑通黑烟车训练数据划分、参数与调优4.1 YOLOv5还是YOLOv8以及数据目录怎么摆做黑烟车训练时YOLOv5 和 YOLOv8 的核心流程基本一致准备数据、写 yaml、训练、验证、导出。yolov5 的资料更多网上遇到报错时更容易搜到解决方案yolov8 的接口更简洁适合把精力集中在后处理逻辑上。无论你选哪个训练自己的数据集的第一步都是把目录结构摆对。black_smoke_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── data.yaml └── README.mddata.yaml 是数据集的唯一配置入口必须和目录结构严格匹配path: black_smoke_dataset # 数据集根目录相对路径或绝对路径 train: images/train # train 图片目录 val: images/val # val 图片目录 test: images/test # test 图片目录可以是空目录 nc: 1 # 类别数这里是单类别烟羽 names: [smoke]如果做多等级端到端就把nc改成 4names改成[smoke_0, smoke_1, smoke_2, smoke_3]。注意smoke_0并不是“没有烟”而是“有烟羽但未达到黑烟程度”的类别。类别 0 在 YOLO 中默认是背景如果你把“无烟”也当正样本类别会导致损失计算混乱无烟帧应该放到负样本集合里而不是单独设类。数据划分建议按“点位”切分而不是按“帧”切分。同一台摄像机相邻帧高度相似按帧随机划分会让模型在验证集上看到大量训练帧的“复刻版”指标虚高 5 到 10 个点。按摄像机点位划分才能测出模型换场景后的真实表现。4.2 训练命令与参数设定单类别烟羽检测显存 12GB 左右就能跑得动命令如下yolo detect train \ datadata.yaml \ modelyolov8s.pt \ epochs150 \ batch16 \ imgsz640 \ optimizerAdamW \ lr00.0005 \ weight_decay0.0005 \ mosaic0.5 \ close_mosaic20 \ patience20 \ seed42几个关键的参数按黑烟车场景做了解释参数建议值理由modelyolov8s.pts 尺寸在精度和速度间平衡比 nano 稳定imgsz640烟羽属于中小目标640 够用想抓排气管附近的小烟羽再升 768optimizerAdamW少样本时收敛比 SGD 稳lr0 记得降到 0.0005 以下mosaic0.5烟羽跨帧拼接容易标注错位不建议默认的 1.0close_mosaic20最后 20 个 epoch 关掉 mosaic让模型适应真实分布patience20验证集 mAP 连续 20 轮不提升就早停省时间正样本占比过低时比如烟羽框不到总框数 20%先不要急着改损失函数权重最简单的办法是复制正样本 2 到 3 份或对正样本做更强的几何增广。YOLO 的类别损失默认按频率加权少量复制正样本比暴力调cls_loss更可控。4.3 训练完看什么指标mAP50比mAP50-95更接近实际训练完成后YOLO 会输出 P、R、mAP50、mAP50-95 这些指标。对这个任务mAP50 比 mAP50-95 更值得关注。烟羽没有清晰的边界标注真值框本身带主观性用 IoU 0.75 以上的标准去评估一个本来就是半透明的目标意义不大。mAP50 能到 80% 以上mAP50-95 在 50% 上下已经是可用的模型。查看验证集指标yolo detect val \ modelruns/detect/train/weights/best.pt \ datadata.yaml \ conf0.25 \ iou0.5重点看recall而不是precision。黑烟车抓拍场景里漏掉一辆冒黑烟的车远比误判一辆无烟车严重。评估时如果 recall 低于 0.8优先去补充多样性的正样本而不是堆叠模型复杂度。训练完我习惯顺手把 test 目录里没有参与训练的视频片段跑一遍推理把预测框画到原图上用播放器看连续帧效果。这个步骤能发现指标看不出的问题预测框是不是在烟羽消失后还挂在车尾、会不会抖到旁边车道。4.4 推理与结果可视化yolo detect predict \ modelruns/detect/train/weights/best.pt \ sourcetest_videos/cross_road_A.mp4 \ conf0.25 \ saveTrue \ save_framesTrue保存出来的逐帧图片会直接告诉你模型的抖动程度。我建议把conf从 0.25 往下调到 0.15 看一遍如果低阈值下出现大量误检模型还需要更多负样本如果低阈值下依然很干净说明部署时可以在保证 recall 的前提下把阈值定在 0.3 左右减少计算压力。5. 部署时的三个坑导出、抽帧与阈值标定5.1 导出 ONNX把前处理一起带走训练好的 PyTorch 权重不能直接用在上位机或者边缘盒子。最稳妥的一步是导出成 ONNX 格式yolo export modelbest.pt formatonnx imgsz640 opset12导出的 ONNX 已经包含了归一化和 Resize 前处理用 onnxruntime 加载后输入直接喂 RGB 原始帧即可。这里要注意输入张量的通道顺序OpenCV 读出来的是 BGR需要用cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)反转后再传入。ONNX 推理速度在 CPU 上大约每帧 30 到 80 毫秒取决于 CPU 型号和输入分辨率。要是达不到实时就放弃逐帧处理改成每 3 帧抽 1 帧推理因为黑烟持续时间远大于单帧间隔。5.2 连续帧判黑别让单帧抖动害了系统单帧检测结果不能直接作为上报依据。烟羽在消散期间会有瞬间稀薄的情况单帧置信度正好落在阈值下方就会断裂。工程上常用滑动窗口判黑逻辑窗口内多帧置信度超过阈值才触发上报def should_report(scores, conf_thres0.3, min_hit3): scores: 最近5帧的烟羽置信度列表 conf_thres: 单帧置信度阈值 min_hit: 窗口内至少需要命中帧数 hits sum(1 for s in scores if s conf_thres) return hits min_hitmin_hit取 3 意味着 5 帧内至少有 3 帧判定为黑烟可以过滤掉水汽、阴影等瞬时干扰。上报后进入复核队列由平台决定是否直接推送现场执法人员。5.3 把使用文档当成可复现性承诺来写源码、数据集、使用文档三件套放在一起交付时使用文档最容易被随便写写应付过去但它恰恰决定了别人能不能把项目跑起来。我会在文档里固定记录几个必填项数据集的划分种子data.yaml 里的 seed、每轮训练的超参 yaml、验证集指标截图、测试视频的名单和判定阈值。没有这些信息数据集后续补充了样本别人就复现不出你当时的模型效果。对毕业设计而言一套能复现的训练配置加一份阈值标定记录比堆砌几段网络结构分析更能说明工作量。把冒烟指数调好比换更大的模型更管用。本文还有配套的精品资源点击获取