
简介一套完整的计算机视觉毕业设计项目包聚焦基于深度学习的黑烟车自动识别。面向计算机视觉方向高校毕业生、目标检测学习者和智慧环保项目开发者针对黑烟车人工监管成本高、效率低的痛点提供从数据标注、图像增广、模型搭建到训练调优的全流程可复现方案。系统以残差网络为骨干网络引入迁移学习最终在测试集上mAP达到0.9752并附有完整实验记录与结果分析。压缩包共2000个文件以标注文件和图像为主体配合源码、说明文档、演示文稿和PDF等格式整体约75MB内容涵盖代码、带标注数据集、使用文档、答辩PPT和毕业论文尤其包含开题报告、任务书、中期检查表等完整学术档案便于参考毕业设计全流程。已有358人学习下载适合需要扎实可复现的目标检测项目作为毕设或课题基础的研究者。1. 黑烟车识别不是普通目标检测先看清毕设的真实分量一辆大货车在路口起步排气管瞬间拉出一条黑色烟柱人眼一眼就能看见。可如果要让程序在视频里“看见”它而且不把洒水车水雾、路边树影和黑色货车车厢一起当成冒烟整条链路就不再只是一张静态图片做目标检测那么单纯。基于深度学习的黑烟车自动识别系统是计算机视觉毕设里少数能把目标检测、图像分类、视频序列判断串成一个完整工程的项目你既要回答“车在哪”也要回答“烟是不是黑的”还要回答“连续几帧都冒烟才能算抓拍”。适合拿到这个题目的本科或硕士学生把源码、数据集、使用文档和论文四件套整理成能演示、能复现、能答辩的完整课题。2. 黑烟车识别先拆题车辆框、黑烟框与帧级确认的边界在哪里2.1 先定义清楚“黑烟”的视觉边界再决定要不要单独标注烟很多学生拿到题目第一反应是直接去训练一种 black_smoke 目标检测器。这个方向不是不能做但落地时很尴尬黑烟没有固定形状边缘半透明随风扩散还会被车体遮挡标注框的边界主观性很强。同一个烟柱两个人标框的面积可能差出三倍模型也就跟着学了三个版本的特征。更常见的情况是模型把树影、桥墩阴影和黑色车顶也当成黑烟框出来因为训练样本里的黑烟区域确实包含大量“暗色块”。我做这套系统习惯把任务拆成两级第一级检测车辆第二级判断车尾区域是否存在黑烟。让车辆当 anchor烟只作为“这辆车是否冒烟”的属性不单独追求全图找烟。好处有两个一是黑烟被车体局部挡住时只要车辆还在冒烟判断就不会凭空消失二是黑衣服行人、路边阴影这类干扰不会因为没有车辆框而成为误报来源。对应到数据集的组织方式车辆框用普通目标检测形式标黑烟只落在车辆框内的车尾区域这样一个目标同时带两类信息训练起来更稳也更容易在论文里解释“为什么这辆车被判成冒烟”。那标注时到底怎么画烟框我的原则是“宁可少框不要多框”。烟柱本体可见的部分才标看不见的边缘不强行补全车顶、车窗的黑色纹理绝不圈进烟区。如果你的数据集里本身只有“车辆冒烟”二分类标签没有独立的烟区框也能用但训练目标就退化成“判断车尾区域有无黑烟”后处理阶段需要额外处理置信度。拿到一个现成数据集时先跑一段统计脚本看看每一类的标注框数量分布如果 black_smoke 框的数量只有 vehicle 的十分之一先做挖掘补充不要直接开训。2.2 模型选型YOLO 系做主路径两阶段检测器留作对比实验黑烟车识别对实时性的要求没有自动驾驶那么苛刻但仍然希望单张图能跑到几十毫秒量级。我一般先用 Ultralytics YOLOv8 作为主路径因为它在源码组织、文档和验证接口上都比较成熟单卡就能训完可演示的模型。背靠成熟框架还有个隐性优势使用文档里可以少写一半框架原理把篇幅让给数据标注和阈值调优这些才是真正影响毕设结果的地方。两阶段检测器如 Faster R-CNN 在低对比度目标上召回更好但训练时间和显存开销明显更适合作为论文中的对比组而不是主力模型。如果机位固定、场景基本静止有人会顺手加一层背景差分做运动区域过滤先找出变化区域再跑检测器。这个思路确实能省算力但会漏掉静止排队时冒黑烟的车辆因为排队车辆在像素级几乎没有位移。毕设里最稳的路线是“全图检测车辆→车尾区域判断黑烟→连续帧确认”中间不要加背景差分这种前置门槛。不同路线的取舍可以看这张表方案训练成本静态排队车识别毕设复现难度YOLOv8 直接检测车辆烟低单卡可训正常低Faster R-CNN 两阶段检测高训练时间长正常中背景差分 YOLO低漏检明显中结论是主线方案越简单越稳。毕设答辩的提问大多集中在“为什么这样设计”与其在流程里堆多层过滤造成难排查不如把核心工作放在数据和阈值调优上留出清晰的实验对比组。2.3 交付结构先定好源码、数据集、使用文档、论文怎么互相咬合动手写代码前先把 zip 的目录结构定出来后面每一步都往预留位置里填内容。比较标准的组织方式如下black_smoke_project/ ├── src/ # 训练、推理、可视化脚本 ├── data/ # 抽帧结果、标注文件和 data.yaml ├── docs/ # 环境依赖、使用说明、复现步骤 ├── weights/ # 预训练权重和训练输出目录 └── report/ # 论文、开题、答辩材料这个结构的意义不只是收纳。论文里实验部分通常会有“训练配置表”“数据集构成表”如果脚本里的数据集路径是写死的评审拿到 zip 后换台机器根本跑不动使用文档写再多也填不上这个坑。所以我自己的项目会强制所有脚本用命令行参数接收路径并用相对项目根目录的写法组织数据引用README 第一行写清楚“环境要求从哪个文件开始”。这样源码、文档、论文三处描述的是同一套路径复现时少很多玄学报错。每个交付物在实际验收时盯四个点源码要能按步骤重训和重推理数据集要能对上 data.yaml 的类别编号使用文档要能支撑“新机器从零到训练出结果”论文里的每个指标都要能从实验截图和日志里找到出处。这四个点对齐后毕设工作量的呈现就不会是零散的“好像做了很多”而是一条能被逐步验证的完整链路。3. 从数据集制作到跑通训练参数怎么影响结果代码怎么放才稳3.1 环境搭建先固定依赖再谈训练黑烟车识别会用到 PyTorch、目标检测框架、OpenCV 和标注工具如果直接装进 base 环境同组同学大概率会因 CUDA 版本不一致而翻车。独立 conda 环境是第一道保险# 创建专用环境Python 3.10 的兼容性比最新版更稳 conda create -n black_smoke python3.10 -y conda activate black_smoke # 先装 PyTorch再装检测框架顺序反了可能把 CPU 版 torch 拉进来 pip install torch torchvision pip install ultralytics opencv-python pandas装完立刻验证 GPU 是否可用python -c import torch; print(torch.cuda.is_available())输出 True 再继续输出 False 时不要急着重装。先用 nvidia-smi 看驱动支持的 CUDA 版本再检查 conda 环境是否被自带的 cudatoolkit 覆盖成低版本。笔记本双显卡机器还有个常见情况PyTorch 默认选了核显需要设置 CUDA_VISIBLE_DEVICES 指定独立显卡编号。如果没有 NVIDIA 环境CPU 版 torch 跑完整训练也能完成毕设只是时间从半小时变成几小时。环境问题卡超过半天就直接切 CPU 版先跑通流程后续再考虑算力。3.2 建数据目录与抽帧让训练集和验证集尽量“不熟”数据集制作的关键不在数量而在分布。从监控视频抽帧时如果连续帧被大量抽进训练集模型会把“这一辆车、这一段逆光”当成主要特征验证集却来自另一段完全不同的视频精度自然掉得难看。抽帧间隔建议落在 815 帧之间并且跨时间段、跨天气采集。下面是我常用的抽帧脚本# 抽帧脚本每个视频每隔 10 帧保存一帧避免相邻帧成对出现 import cv2 import os video_root raw_videos out_dir data/images/train os.makedirs(out_dir, exist_okTrue) for vname in os.listdir(video_root): vpath os.path.join(video_root, vname) cap cv2.VideoCapture(vpath) frame_id 0 keep_id 0 while cap.isOpened(): ret, frame cap.read() if not ret: break if frame_id % 10 0: out_path os.path.join(out_dir, f{os.path.splitext(vname)[0]}_{keep_id:04d}.jpg) cv2.imwrite(out_path, frame) keep_id 1 frame_id 1 cap.release()frame_id % 10决定抽帧间隔。监控视频常见 25fps隔 10 帧约等于每 0.4 秒取一张训练数据冗余不大也保留了目标的连续性。夜间和雨天的片段要单独建子目录方便后续做难例子集。标注工具选支持 YOLO 导出格式的开源标注器即可每张 jpg 对应一个同名 txt。标签编号定下来后不要改0 通常是 vehicle1 是 black_smokedata.yaml 里的类别必须与之一致。# data.yaml注意 train/val 是相对数据集根目录的路径 path: data train: images/train val: images/val nc: 2 names: 0: vehicle 1: black_smokepath表示数据集根目录train和val是相对它的图片目录。nc是类别总数names把数字标签映射成可读名称。这里最容易踩的坑是坐标格式YOLO 的 txt 里存的是归一化的中心点坐标和宽高不是左上角右下角坐标也不是像素绝对值。标注工具导出后最好再用脚本扫一遍检测归一化范围是否越界。3.3 训练命令与三个必调参数epochs、batch、patience数据准备好了训练命令本身不长但每个参数都有实际后果。用 Python 接口写训练配置便于把完整参数保存下来写进论文实验表from ultralytics import YOLO # 用预训练权重起步比从零随机初始化收敛快得多 model YOLO(yolov8s.pt) results model.train( datadata/data.yaml, epochs80, # 小数据集 80 轮足够观察收敛趋势 imgsz640, # 监控视频多为 1080p640 是算力和效果折中 batch16, # 显存吃紧时降到 8 或 4 patience10, # 验证集连续 10 轮不涨就自动停止 projectoutputs, nameblack_smoke, lr00.01, # YOLO 系列默认学习率通常够用 devicecuda, )epochs不是越大越好。几百张数据时五十轮以后验证集可能已经过拟合patience的作用就是自动拦住。batch影响梯度稳定性也决定显存上限16 不行就 8不要硬撑。imgsz在这类任务里尤其关键黑烟是低纹理目标分辨率太低会把烟和背景糊在一起显存允许时优先提到 800。训练结束后对比outputs/black_smoke/weights/best.pt和last.pt。两者差距大说明后段过拟合几乎一致说明训练还没收敛透。正常情况用 best 推理删掉 last避免答辩演示时拿错权重。日志里的 P、R、mAP50 三个指标都要记录只用 mAP50 会漏掉大量误报信息。3.4 第一次推理单图优先视频其次训练完先做单张图推理既验证模型质量也验证路径和环境是否正常yolo predict modeloutputs/black_smoke/weights/best.pt \ sourcedata/images/val/frame_001.jpg \ imgsz640 conf0.35 save_txtTrueconf0.35是演示用的宽松阈值尽量多地把疑似黑烟框出来便于人工判断模型是否学到“烟”的特征。正式统计准确率和召回率时我会用验证集跑完整指标分析把conf调回 0.5再分别统计车辆检测与黑烟判定两个独立结果。单图推理出现大量漏检时先查 data.yaml 的nc和标签编号再查imgsz结构性错误优先排查不要先质疑模型结构。4. 黑烟车识别避坑与常见问题灰烟误报、连续帧漏检、过拟合这章是我自己在类似项目里消耗时间最长的地方。很多项目跑通的节点只占一半工作量剩下的一半全在处理下面这些现象。4.1 模型把灰色天空和黑色车顶误报成黑烟现象验证集里出现大量误报区域集中在天空、桥墩阴影、黑色车顶。原因黑烟在图片里是低饱和度、边缘模糊的暗色区域。如果标注时把黑烟框扩大把车顶和车窗的黑色纹理圈进去模型学到的主特征就变成“任何暗色区域”。黑烟本身边缘扩散标注边界主观这个误差会被模型放大。解决重新检查所有 black_smoke 标注框把边界收缩到烟柱本体可见范围内坚决不框深色车体。训练时增加亮度、对比度随机抖动增强破坏“暗色即烟”的捷径。误报压不干净时不要只调高 conf那会同时牺牲真阳性先回看标注再动阈值。4.2 静止排队车辆冒烟漏检现象动态检测一切正常视频里静止排队车辆明明在冒黑烟模型却没有输出。原因如果流程里用了背景差分或帧差法做前级过滤静止车辆的相邻帧差异接近零还没走到检测器就被丢弃了。这类漏检不是模型能力问题而是流程设计把有效信号前置截断。解决不要把背景差分当作识别黑烟的前置条件。算力紧张的折中方案是先用轻量检测器筛出车辆候选框再对候选框内区域判断黑烟。静止车辆只要本身能被检测到车尾冒烟就能继续判定。4.3 训练集 mAP 高、验证集掉点严重现象训练阶段指标不错换一段新视频去跑各种漏报误报冒出来。原因黑烟车样本量小模型背住了训练帧里的特定道路、特定时段光照没有学到“烟”的本质。监控场景背景高度相似这个问题更明显。解决抽帧时跨视频、跨时段、跨天气混合划分训练集和验证集时尽量按“视频来源”整体划分而不是把同一段视频的帧散落到两侧。训练日志里若验证集 loss 在中期二次抬头就是过拟合信号直接加大增强、减少 epochs。不要继续堆 epochs大多数情况下只会更差。4.4 视频里同一辆车被重复框出黑烟框一会有、一会无现象单张图片没问题连续视频帧里检测框跳来跳去同一个烟柱有时有框有时没框。原因单帧模型没有时序记忆帧与帧之间 bbox 抖动、置信度波动是常态直接输出自然闪烁。检测框跳变还会让抓拍逻辑没法写硬塞 if 判断越写越多越不稳。解决在推理脚本中做相邻帧 IoU 匹配把同一辆车在连续帧中的判定结果投票连续若干帧命中再输出。参数可以参考 MIN_HITS3、IOU_MATCH0.3、CONF_THRESHOLD0.4实现细节在第 5 章。4.5 训练时报错 CUDA out of memory现象训练刚启动甚至加载数据时就报显存不足但用系统工具看显存还剩不少。原因PyTorch 是按显存块申请的其他进程已经占用了零碎空间高分辨率视频帧一次性载入 batch 也会瞬间撑满。放大 imgsz 而不降 batch 是常见的翻车组合。解决先把 batch 降到 8 或 4imgsz 降到 544关掉浏览器和可视化工具再重跑。训练脚本里可以加torch.cuda.empty_cache()但清缓存治标不治本真正原因是 batch 和分辨率超出可用显存。注意报错日志里通常会打印显存申请量分布先看哪一步申请最大再决定降 batch 还是降分辨率。盲目重装 PyTorch 几乎不会解决显存问题。5. 从单帧检测到连续监控黑烟车识别如何过时间这一关5.1 相邻帧 IoU 匹配先让目标在时间上连续毕设演示通常用一段 3060 秒监控视频期望效果是车辆开过时稳定框出黑烟而不是每一帧重新“猜一次”。把单帧结果串成连续目标最简单的做法是 IoU 匹配前后两帧的检测框重叠面积超过阈值就视为同一辆车。不必引入多目标跟踪模型代码量小效果足够。import cv2 from ultralytics import YOLO model YOLO(outputs/black_smoke/weights/best.pt) cap cv2.VideoCapture(video/test_clip.mp4) MIN_HITS 3 # 连续命中帧数 FRAME_SKIP 2 # 每两帧算一次 IOU_MATCH 0.3 # 目标匹配阈值 CONF 0.4 smoke_hits {} frame_id 0 while cap.isOpened(): ret, frame cap.read() if not ret: break if frame_id % FRAME_SKIP ! 0: frame_id 1 continue results model(frame, confCONF, imgsz640) boxes results[0].boxes.xyxy.cpu().numpy() scores results[0].boxes.conf.cpu().numpy() classes results[0].boxes.cls.cpu().numpy() updated {} for box, score, cls in zip(boxes, scores, classes): x1, y1, x2, y2 [int(v) for v in box] if cls 1 and score CONF: cur_box (x1, y1, x2, y2) for prev_box, hits in smoke_hits.items(): # 显式计算两个框的 IoU大于阈值认为同一目标 inter (max(0, min(x2, prev_box[2]) - max(x1, prev_box[0])) * max(0, min(y2, prev_box[3]) - max(y1, prev_box[1]))) union ((x2 - x1) * (y2 - y1) (prev_box[2] - prev_box[0]) * (prev_box[3] - prev_box[1]) - inter) iou inter / (union 1e-6) if iou IOU_MATCH: updated[prev_box] hits 1 break else: updated[cur_box] 1 smoke_hits updated for box, hits in smoke_hits.items(): if hits MIN_HITS: x1, y1, x2, y2 box cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(frame, black smoke, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.imshow(video, frame) if cv2.waitKey(1) 0xFF ord(q): break frame_id 1 cap.release() cv2.destroyAllWindows()这里 IoU 计算写成显式公式是为了让你看清匹配逻辑互不重叠的两个框 iou 趋近 0不会被误匹配两辆车并排行车时0.3 的阈值也足够区分多数情况。代码中FRAME_SKIP2让每秒实际推理次数减半处理速度翻倍黑烟这种慢变化目标不受影响。MIN_HITS3的含义是连续若干次采样都出现黑烟才确认单帧噪声很难连续三次出现误报被天然压低。如果你希望输出可复现的实验记录把每一帧的检测框坐标和置信度落成 json 或 csv 再画框而不是只输出标注视频。这样论文里的“连续帧命中率”能从记录里重新算不靠截图讲故事。5.2 滑动时间窗口把单帧置信度换成“窗口内命中率”只统计连续命中还有一个小问题黑烟被前车遮挡或随风散开一瞬连续帧判定就会中断。更鲁棒的做法是维护一个滑动时间窗口统计最近窗口内的命中比例。窗口参数建议写成独立配置方便在使用文档和论文里引用MIN_HITS3 # 窗口内最少命中帧数 WINDOW_SIZE10 # 时间窗口长度 IOU_MATCH0.3 # 车辆匹配阈值 CONF_THRESHOLD0.4 # 黑烟检测置信度WINDOW_SIZE10配合MIN_HITS3的含义是最近 10 次采样里出现 3 次黑烟就报警。它比“连续 3 次命中”更宽容也更贴近真实。真实黑烟是间歇性浓淡交替的烟柱可能被前车挡一下也可能在风里散开半秒。窗口判定能容忍这种短暂中断同时保住误报过滤能力。答辩时这个参数组可以直接写进实验表每一个值都能解释为什么这样取。5.3 夜间和雨天的边界数据分布决定系统上限黑烟车识别最容易被问“夜间怎么办”。车灯、路灯、反光贴会把画面局部照得过亮黑烟没有反光几乎消失在暗背景里雨天路面反光也让暗色烟柱和地面难分。这些都是数据分布问题不是换个模型结构能解决的。训练集里没有夜间和雨天样本系统上限就停在晴天白天。我的习惯是抽帧时按时间段切分样本子集黄昏、阴天各留一部分训练后单独做一组难例测试把夜间、雨天场景的真实指标量出来。如果数据确实拿不到夜间素材论文里如实写“夜间场景待扩展”并用白天指标的完整对比支撑结论。评审不会因为你承认边界而否定工作反而会因为边界意识给更高分。6. 收尾验收使用文档、论文与演示怎么把模型讲清楚6.1 使用文档的写法以“换一台机器能复现”为标准使用文档不是把命令堆一遍而是写清依赖顺序、数据格式和参数含义三件事。依赖顺序要解释为什么先装 torch 再装检测框架数据格式要写清抽帧间隔、标签编号、data.yaml 的路径规则参数含义至少要覆盖 conf、batch、imgsz、epochs 和 IoU 阈值。组织方式按“新机器拿到 zip 后从第 1 步做到最后 1 步”的视角来写每个可能卡住的地方都给一条检查命令。README 不要只写 train.py要写清从哪个目录运行、输出到哪个目录、失败时看哪个日志。6.2 答辩演示里我常用的一个技巧并排放原始视频和识别视频答辩演示效果最稳的配置是一段 30 秒原始视频和识别结果视频并排播放。左边展示原画面让老师确认那辆车确实在冒烟右边展示检测框和置信度先放默认 conf 的原始输出再放阈值调优后的结果把“调参确实改变了输出”直观呈现。同时准备三个失败案例截图分别对应误报、漏报和夜间失效讲清每个案例的数据原因或模型原因。这个环节准备到位比反复强调 mAP 数字更能说服评委。我自己交毕设前还有一个固定习惯把随机种子、数据划分版本、权重备份路径全部写进使用文档附录。答辩前两周通常还会调一轮模型若忘了记录当时的配置换台机器重新训练复现不出同一档精度论文里的指标就成了空话。这类细节才是毕设项目真正的后悔药。希望这份从任务拆解到参数调优的完整路线能帮你的黑烟车识别项目把源码、数据集、使用文档和论文四件套稳稳落地。本文还有配套的精品资源点击获取