简介面向交通管理部门与计算机视觉研究者提供一套基于深度学习的黑烟车自动识别系统完整实现方案。项目针对黑烟车人工监管费时费力的问题运用目标检测方法自建黑烟车数据集结合图像增广、以ResNet为骨架的深度网络、迁移学习及参数调优最终在测试集达到0.9752的mAP。压缩包共2000个文件以XML标注、JPG图像、Python脚本及论文文档为主包含988个xml标注文件、902张jpg车辆图像、18个py训练与推理脚本以及doc、pptx、pdf、md等论文与使用说明整体约74.53MB。已有227人学习适合环保监管、智慧交通领域的研究人员与学生参考。资源涵盖从数据集构建、模型设计到实验对比的完整流程源码中另含数据预处理与增强操作脚本可直接用于复现实验、扩展检测类别或撰写相关论文。1. 黑烟车自动识别难在哪为什么阈值分割在尾气场景撑不住路侧黑烟车抓拍这个场景看起来就是“找到一团黑烟”这么简单实际做过的都知道尾气是半透明的形态随车速和风速随时在变排气管位置、路段坡度、光照角度一换传统灰度阈值和背景差分立刻崩盘——晴天把树影报成黑烟阴天把深灰色尾气直接漏掉。近几年做环保监测和毕业设计基于深度学习的黑烟车自动识别系统成了主流答案用目标检测网络直接回归黑烟区域再配合视频帧时序判定把“有没有冒黑烟”从玄学变成可落地的工程问题。这篇笔记按这类“黑烟车自动识别系统”资源包的常见组织方式把选型依据、数据整理、训练参数、验收指标、避坑案例和部署前验证一次讲透适合正在做相关毕业设计或环保视觉项目的读者照着复现。2. 把黑烟检测拆成四个模块从选型到主循环一个完整的黑烟车自动识别系统不是训练完一个模型就能交付的。见过不少项目把全部精力砸在网络结构上最后卡在误报和重复上报上。我的建议是先把系统拆成四个模块视频采集、黑烟检测、时序确认、记录回查。模块之间耦合越少后面调参数越省心。2.1 为什么是深度学习目标检测而不是传统图像处理传统做法主要靠三招背景差分、灰度阈值、边缘检测。背景差分在固定机位下确实能找出“变化区域”但黑烟车的场景里车辆本身在动、光照在变、树叶在摇差分出来的区域根本分不清是车还是烟。灰度阈值更直接设定一个暗度阈值把深色像素捞出来可黑烟的颜色从浅灰到深黑跨度极大深色车身、轮胎、路面阴影和它几乎无法区分。边缘检测也搞不定烟雾没有固定轮廓边缘是模糊扩散的检测出来的边缘断成一片。深度学习目标检测的优势在于端到端学习“黑烟”的表观特征和上下文模型看到的不是单一像素的灰度而是烟雾的纹理、透明度变化、与排气管和车身的相对位置关系。换车型、换光照时泛化能力明显强于手工特征。选型上这类系统最合适的是 YOLO 系检测器而不是分割模型。分割模型比如 DeepLab、U-Net能给像素级掩膜但标注成本高、推理速度慢而业务上只需要知道“黑烟出现在画面哪个区域”一个矩形框加置信度就够后续判定了。2.2 四个模块怎么划分采集、检测、判定与记录先看整体数据流我用一个表格把模块边界说清楚模块输入输出关键点视频采集RTSP 视频流或本地视频文件抽帧后的图像帧按场景调整抽帧频率一般 1~5 fps黑烟检测图像帧检测框 类别 置信度用 YOLO 推理设好置信度和 NMS 阈值时序确认连续若干帧的检测结果确认事件是/否连续多帧命中才上报抑制单帧误报记录回查确认事件截图、视频片段、时间戳、点位落盘并统计误报率便于后续调参主循环可以写成一个很短的脚本核心是把“检测”和“业务判定”分开# main_loop.py 黑烟车识别主循环结构示意 import cv2 from detector import SmokeDetector detector SmokeDetector(weightsbest.pt, conf_thres0.35) cap cv2.VideoCapture(rtsp://camera01/stream) frame_skip 2 # 每 3 帧取 1 帧推理控制负载 vote_window 5 # 滑动窗口长度 5 帧 min_hits 4 # 窗口内至少 4 帧命中才确认 hits [] while True: ok, frame cap.read() if not ok: break if int(cap.get(cv2.CAP_PROP_POS_FRAMES)) % (frame_skip 1) ! 0: continue dets detector.predict(frame) # 返回框、置信度、类别 now_hit len(dets) 0 hits.append(now_hit) if len(hits) vote_window: # 只保留最近一个窗口 hits.pop(0) if len(hits) vote_window and sum(hits) min_hits: record_event(frame, dets) # 确认后落盘截图和元数据 hits [] # 清窗防止同一事件重复上报逻辑说明这里detector.predict内部已经做了 YOLO 推理和 NMS对外只暴露检测框主循环只关心有没有命中。hits列表维护最近 5 帧的命中状态只有窗口满且命中数达到 4 才确认一次事件。很多人会忽略清窗操作导致同一辆车在视野内连续被记录十几次这个细节后面避坑章节还会展开。参数说明frame_skip2表示每 3 帧取 1 帧做检测。黑烟车从开始冒烟到离开视野通常持续 2 秒以上按 3 fps 抽帧能拿到约 6 帧容忍 1 帧漏检依然能触发如果机位是高速路场景车辆通过时间短可以把frame_skip降到 1。min_hits是灵敏度旋钮调高能压误报但也会漏掉持续时间短的黑烟。2.3 检测器选型与输入尺寸的工程取舍模型本身选 YOLOv8n 还是 YOLOv8s取决于部署设备。路侧抓拍机大多是嵌入式设备或普通工控机n 版本在确保召回的前提下帧率最高如果跑在带 GPU 的服务器上s 版本更稳对小雨、雾天的鲁棒性略好。我一般先用 n 跑通全流程再在验收阶段对比 s 是否值得。输入尺寸上imgsz 不要低于 640。黑烟区域在很多场景里只有 32×32 像素上下降分辨率会让小目标的特征在下采样中直接消失。后面第 4 章会专门讲这个参数怎么调这里先记住一个结论显存不够就减 batch不要减 imgsz。3. 整理黑烟车数据集VOC 转 YOLO 与增强边界数据决定上限这句话在黑烟车识别上体现得特别明显。这类资源包里最常见的问题有三个样本总量少几百到两三千张、类别单一只有 smoke 一类、标注标准不统一。很多人拿到数据集先急着训练结果模型对来源点位过拟合换一个路口就废。这一章把数据整理的完整路径走一遍。3.1 黑烟样本的特殊性半透明烟雾怎么框标注黑烟和标注车、人不完全一样。车是刚体边界清楚烟雾是半透明的边缘呈扩散状不同标注员画出的框差异很大。这会导致模型在训练时收到不一致的监督信号。我建议的标框原则是“框住不透明核心加少量紧贴的半透明边缘”以肉眼能明确识别为黑烟的区域为准不要贪多把整片扩散区包进去也不要只框最浓的一小团。太保守模型只能学到浓烟浅灰烟全漏太扩张框内包含大量背景模型学到的其实是路面和车体。第二个原则是只标排气管附近的烟区不标远处已经飘散开的烟雾因为业务抓拍只需要证明“这辆车在冒烟”。第三个原则是同一段视频的连续帧框要跟着烟区走不能图省事复制上一帧的框否则模型会把烟的位置当成固定特征来学。3.2 VOC 或自标注格式转 YOLO 格式的转换脚本主流的开源检测框架都用 YOLO 格式的 txt 标注如果你手里的数据集是 VOC 的 XML或者标注工具导出了类似结构需要用脚本转一次。下面这个转换脚本是这类项目里最常见的起点# voc2yolo.py: 把 VOC XML 标注转换成 YOLO txt import os import xml.etree.ElementTree as ET def convert(xml_path, out_dir, class_map): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_map: continue # 只保留有映射的类别 bnd obj.find(bndbox) x1 float(bnd.find(xmin).text) y1 float(bnd.find(ymin).text) x2 float(bnd.find(xmax).text) y2 float(bnd.find(ymax).text) # 坐标越界保护黑烟扩散到图像边缘很常见 x1 max(0, min(x1, img_w - 1)) y1 max(0, min(y1, img_h - 1)) x2 max(0, min(x2, img_w - 1)) y2 max(0, min(y2, img_h - 1)) # 归一化到 0~1YOLO 格式为中心点 宽高 cx ((x1 x2) / 2) / img_w cy ((y1 y2) / 2) / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{class_map[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) out_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(out_dir, out_name), w) as f: f.write(\n.join(lines)) if __name__ __main__: class_map {smoke: 0, car: 1} # 按数据集的类别清单修改 os.makedirs(labels, exist_okTrue) for xml in os.listdir(xmls): if xml.endswith(.xml): convert(os.path.join(xmls, xml), labels, class_map)逻辑说明脚本读取 XML 里的图像宽高把每个目标的左上角和右下角坐标转换成归一化的中心点与宽高并按class_map写入类别索引。注意class_map必须和训练配置里的data.yaml完全一致否则出现类别错位排查起来非常费劲。参数说明上面这段里比较容易被忽略的是越界保护。黑烟扩散到画面边缘时标注工具产生的 xmin/ymax 可能超出图像尺寸如果不 clipYOLO 训练时会把越界框当成非法标注丢弃导致样本白白减少。class_map里如果数据集只有 smoke 一类就写成{smoke: 0}但建议把 car 也标出来让模型同时学习“车”这个上下文误报率会明显更低。3.3 数据增强的取舍别把烟雾增强没了黑烟样本少数据增强是必须的但不能无脑套用。我用下来有效和无效的增强策略如下。有效的亮度与对比度抖动。黑烟在不同天气下的视觉差异极大晴天黑烟发黑阴天发灰亮度抖动能让模型学到“烟”的本质而不依赖具体亮度。随机小角度旋转和水平翻转也推荐但要记住如果对图像做了翻转标注框坐标也要同步转换。用 YOLO 训练时水平翻转增强是默认开启的框架会自动处理标注没问题自己离线增强就要小心。慎用的随机擦除cutout和重度模糊。cutout 会把烟区直接挖掉一块等于让模型学“烟中间有个黑洞”重度模糊则让本来纹理就弱的黑烟更不可辨。mosaic 增强对黑烟这种小目标有效但有副作用——mosaic 把多张图拼在一起每个目标都被缩小排气管附近的小烟区可能变成几个像素。我的做法是保留 mosaic 到训练后期最后 20 轮关闭让模型在接近真实尺寸的输入上收敛# data_aug.yaml 片段YOLOv8 训练时关闭 mosaic 的写法 mosaic: 0.0 # 前 130 轮用默认 1.0后 20 轮改成 0.0 close_mosaic: 20 # 最后 20 轮关闭 mosaic 增强另一个重点是负样本。黑烟车数据集的负样本不是随便拿普通视频帧就行而是要有针对性地收集与水雾、蒸汽、扬尘、深色车影有关的画面。模型误报黑烟多半是没见过这些“长得像烟但不是烟”的东西。把这类样本单独归为一个背景类或者在验证集里提高占比能直接压住部署后的误报。4. 训练黑烟识别模型命令、三个必调参数与验收指标数据整理完进入训练环节。这一章给出一套可以直接复现的训练流程重点讲三个必调参数和验收指标。很多人在训练时只关心 mAP但黑烟车识别是强正负样本不均衡的业务mAP 会骗人必须用业务指标来验收。4.1 环境配置与最小训练命令如果资源包里自带使用文档通常跑通训练环境是最耗时的一步。我建议用 conda 建一个干净的虚拟环境不要把依赖装进系统 Python# 创建虚拟环境并安装 Ultralytics YOLO conda create -n smoke python3.10 -y conda activate smoke pip install ultralytics opencv-python # 数据目录结构YOLO 常规约定 # datasets/smoke/images/train - 训练图像 # datasets/smoke/images/val - 验证图像 # datasets/smoke/labels/train - 训练标注 txt # datasets/smoke/labels/val - 验证标注 txt # datasets/smoke/data.yaml - 数据集配置 # 开始训练 yolo detect train modelyolov8n.pt datadatasets/smoke/data.yaml \ imgsz640 epochs150 batch16 patience30 \ projectruns/smoke nametrain_v1逻辑说明yolo detect train是 Ultralytics 的统一训练入口modelyolov8n.pt表示加载 COCO 预训练权重不是从头训练。黑烟场景与 COCO 里的目标差异很大但预训练权重提供了通用的底层特征边缘、纹理、颜色能显著加快收敛。参数说明imgsz640是图像输入尺寸epochs150是最大训练轮数patience30是早停阈值——验证集指标连续 30 轮没有提升就自动停止。batch16在 8GB 显存下接近极限如果你的显卡只有 6GB先把 batch 降到 8不要动 imgsz。配套的 data.yaml 也很关键# datasets/smoke/data.yaml path: datasets/smoke train: images/train val: images/val names: 0: smoke 1: car注意names的索引必须从 0 开始连续编号并且与第 3 章转换脚本里的class_map一致。如果数据集只有 smoke 一类写成0: smoke即可如果加了背景类别背景类不需要写进 namesYOLO 会把无标注区域当作背景。4.2 三个必调参数imgsz、conf、iou训练阶段只需要把模型权重训出来但推理阶段有 3 个参数直接决定系统好不好用。第一个是imgsz。训练和推理的输入尺寸最好一致。黑烟区域很多是小目标640 是底线960 会更稳。代价是推理耗时增加约一倍。如果部署设备是 Jetson 这类边缘设备先用 640 跑通再在验证集上对比 960 的召回提升决定要不要为小目标多花算力。第二个是置信度阈值conf。这个参数不在训练命令里在推理时传入。conf0.25和conf0.45的行为差异非常大。黑烟车业务和通用目标检测不一样宁可从 100 辆车里漏掉 2 辆也不能让系统一天误报 200 次否则审核人员会把系统关掉。我的经验是先从conf0.35起步然后用第 6 章的统计方法微调。第三个是 NMS 阈值iou。黑烟是连续扩散的同一团烟的多个检测框之间重叠度很高iou0.5时可能保留多个冗余框导致一次冒烟被当成多次。调高到iou0.6能更好合并同一团烟产生的框。注意这个参数不是越大越好iou0.7以上可能把两辆并排冒烟的车合并成一个框。4.3 模型评价mAP 之外要看什么训练日志里的 mAP50 和 mAP50-95 只能作为参考。黑烟车场景正负样本极不均衡训练集里可能只有一两千张带烟图片背景帧几千张此时 mAP 高不代表误报低。更好的做法是看混淆矩阵。Ultralytics 训练结束后会在runs/smoke/train_v1/下生成confusion_matrix.png打开后重点看 smoke 行对应背景列的数值。如果训练集本身负样本不足这张图会非常乐观所以要用独立采集的验证视频做业务级验证# evaluate_business.py: 用未参与训练的视频做业务级验证 from ultralytics import YOLO model YOLO(runs/smoke/train_v1/weights/best.pt) total_frames 0 hit_frames 0 manual_true_hits 0 # 人工确认真正的黑烟帧数 for frame in video_frames(clips/rain_day.mp4): # 独立场景雨天 total_frames 1 dets model.predict(frame, conf0.35, iou0.6, verboseFalse)[0] if len(dets.boxes) 0: hit_frames 1 # 这里人工标注当前帧是否为真黑烟 if is_true_smoke(frame): manual_true_hits 1 false_positives hit_frames - manual_true_hits print(f总帧数: {total_frames}, 命中帧数: {hit_frames}, f误报帧数: {false_positives}, 误报率: {false_positives / total_frames:.2%})逻辑说明这段代码跑一段完全没有参与过训练的视频把模型输出的命中帧数和人工标注的真烟帧数对比得到最接近部署效果的误报率。注意video_frames和is_true_smoke是我为示意写的占位函数实际项目中一个用 OpenCV 读视频另一个需要一个简单的标注工具逐帧打标。参数说明验证视频至少覆盖三种光照——晴天、阴天、雨天。只测晴天会高估模型表现因为雨天水面反光、路面变暗误报率通常会翻倍。conf和iou要和主循环里的参数保持一致否则验证结果没有意义。5. 黑烟车识别避坑五个让模型翻车的真实案例这一章是全篇最值得反复看的部分。黑烟车识别项目里模型训练本身通常不是瓶颈部署后暴露的问题才让人抓狂。以下五个坑按“现象 → 原因 → 解决”的结构写都是我或同行在类似项目里真实碰到过的。5.1 水雾和蒸汽被当成黑烟现象冬天早高峰排气管喷出的白色水汽和洒水车扬起的细雾被模型高置信度识别为黑烟系统一天上报几百条无效事件。原因水雾和黑烟在灰度图和边缘特征上高度相似模型如果没见过足够多的负样本很容易把“一团暗色半透明区域”学成黑烟。白汽在逆光下也会变暗和浅灰烟更接近。解决第一在数据集里加入白汽、蒸汽、扬尘场景做负样本让模型见过“非黑烟”第二在推理链路里加一个 HSV 后处理过滤器用颜色先验把白汽拦掉# post_filter.py: HSV 约束过滤白汽误报 import cv2 def is_black_smoke_region(frame, box, max_v180): x1, y1, x2, y2 map(int, box) roi frame[y1:y2, x1:x2] hsv cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) v_mean hsv[..., 2].mean() # 黑烟整体亮度偏低白汽偏高 return v_mean max_v逻辑说明把检测框对应的图像区域转到 HSV 色彩空间取亮度通道 V 的均值。黑烟是吸收光的区域整体偏暗白汽是散射光的亮度明显更高。max_v180是一个经验值实际项目里要先采集几十个白汽误报样本统计亮度分布再定。参数说明这个过滤器只能拦截“整体偏亮”的白汽对深色背景下的白汽无效。所以它不能替代数据负样本只能作为最后一道防线。注意处理顺序先过 HSV 过滤再进时序投票否则一些瞬间的误报可能会提前触发投票窗口。5.2 夜间车灯直射导致检测状态漂移现象夜间大灯亮起时车灯周围出现光晕模型报出大量烟雾框而真正排出的黑烟在夜间可见光相机下几乎看不见漏检严重。原因可见光相机在夜间靠环境光和车灯照明黑烟本身不反射光画面里就是一团“没有内容”的暗区模型和人眼一样看不到车灯光晕的特征反而容易触发一些 “半透明暗区” 的响应。解决把一天分成白天、黄昏、夜间三桶。夜间时段降低置信度阈值没有意义——不是调参能解决的需要从采集端下手。最直接的做法是夜间调低检测频率或让系统转入待机等天亮再恢复如果业务要求 24 小时覆盖可以换用红外相机但红外下黑烟特征又不一样需要单独采集数据训练。夜间数据不要硬标。我见过有数据集把夜间车灯光晕附近标成黑烟导致模型白天也开始误报。如果手里没有夜间真烟样本宁可不训练夜间场景。5.3 小目标黑烟漏检标注和输入尺寸两头挤压现象远处车道的大货车、摩托车烟区只有 20×20 像素模型完全没反应即使调低置信度也只看到近处的框。原因目标太小经过 YOLO 的下采样后特征图上的对应区域只剩 1~2 个像素点特征信息几乎丢光。标注环节的框如果不准小目标的 IoU 稍微偏一点就让正样本变成难以学习的模式。解决把推理和训练的imgsz提到 960。如果显存或算力不允许就做切片推理把一帧切成左上、右上、左下、右下四块每块独立推理后再把框坐标映射回原图。切片会带来 2~4 倍的推理耗时但对小目标召回提升非常明显。mosaic 增强也有帮助但要注意 mosaic 在拼图时会把小目标放进更小的区域比例控制在 0.5 左右比较稳。切片推理的坐标映射是个容易写错的地方。切分时记录每块的起始偏移合并检测框时把偏移加回去否则框位置全乱。这个逻辑建议单独写一个函数做单元测试不要在跑大批量视频时才发现。5.4 同一辆车在 5 秒内被记录 8 次现象系统确认一次黑烟事件后后台事件表里同一辆车被反复上报相同的截图和几乎相同的时间戳刷屏。原因主循环没有做事件去重。模型每帧都检测到黑烟每次都走一遍“确认逻辑”并落盘没有判断这次检测和上一次是否来自同一辆车。解决第 2 章主循环里的“清窗”是第一步确认后把hits清空这样至少能保证同一个滑动窗口只上报一次。但车辆在视野里停留时间超过一个窗口时清窗后还会再次触发。更稳的做法是加一个简易 IOU 跟踪记录上一帧确认事件的检测框位置如果当前帧检测框和它重叠度大于某个阈值判定为同一事件沿用原事件 ID 并刷新时间戳而不是新建记录# dedup.py: 用 IOU 判断是否为同一黑烟事件 def is_same_event(prev_box, curr_box, iou_thr0.3): # 计算两个框的 IoU小于阈值视为新事件 return box_iou(prev_box, curr_box) iou_thr逻辑说明prev_box是上一次确认事件的最大置信度检测框curr_box是当前帧的检测框。两者 IoU 大于 0.3 就认为是同一辆车同一团烟。这个阈值是比较宽容的因为车在行驶中框的位置会缓慢移动0.3 能容忍一定的位移又不至于把两辆并排车合并。参数说明0.3 这个值在高速场景可能要降到 0.2因为车辆帧间位移大在拥堵场景可以升到 0.5。实际项目中如果你拿到了车牌识别结果直接用车牌号去重更准但会增加一个 OCR 模块的复杂度。5.5 模型过拟合到背景树荫和路面阴影成重灾区现象模型在训练点位验证时表现很好换到另一个路口后树荫、斑马线阴影、深色路牌疯狂误报。原因黑烟样本量少模型找不到足够的“烟”特征就会抄近道学习训练集中与烟强相关的背景特征。如果训练集主要来自同一个点位机位、背景树、路面纹理都被模型记住了。解决训练时打开随机裁剪crop_fraction和透视变换增强强制模型看局部而不是整张图的全局布局。更治本的是混合至少 2~3 个不同点位的视频数据让每个点位的背景在训练集里只占一小部分。单独跑一轮只验证“跨点位迁移”用 A 点位数据训练B 点位数据验证如果 mAP 掉得惨说明过拟合严重。这个坑最难排查因为设备端看到的误报图在训练集里长得很像某些正样本的局部。排查时把误报图收集起来按“背景特征”归类看哪类背景反复出现再去增强对应的负样本。6. 部署前的验证技巧用连续帧投票和单车统计把误报降下来模型训练完真正让它像产品而不是玩具需要两个验证技巧。第一个技巧是连续帧投票第二个是单车级误报统计。这两个方法都不需要额外依赖纯粹是工程习惯。6.1 用连续帧投票抑制单帧误报单帧误报是随机出现的真烟在时间上是连续的。用滑动窗口统计最近 5 帧里的命中次数只有命中数达到阈值才确认事件能过滤掉大部分偶发误报。# vote.py: 滑动窗口投票可直接嵌入主循环 from collections import deque class SmokeVoter: def __init__(self, window5, min_hits4): self.window window self.min_hits min_hits self.buf deque(maxlenwindow) def update(self, hit: bool) - bool: self.buf.append(hit) if len(self.buf) self.window: return False return sum(self.buf) self.min_hits逻辑说明update每帧调用一次传入当前帧是否有检测到黑烟。内部维护一个长度为window的队列新帧进来时旧的自动弹出。队列满后统计窗口中命中帧数是否达到min_hits。返回True表示确认一次黑烟事件。参数说明window5, min_hits4意味着最多容忍 1 帧误报或漏检。如果你发现系统还过敏把min_hits提到 5如果漏检偏多把min_hits降到 3同时相应增加window到 7保持投票比例在 0.6~0.8 之间。这个比例是全系统最核心的灵敏度旋钮没有固定最优值按业务接受度调。6.2 用单车级事件统计校正置信度阈值第二个技巧是统计“上报事件”的正确率而不是统计“帧”的正确率。步骤是在画面中画一条虚拟检测线只有黑烟框的中心点穿过这条线的检测结果才计入事件然后人工回查最近 100 个上报事件统计其中真正冒黑烟的比例。置信度阈值上报事件数正确事件数正确率备注0.251208974%误报太多不可用0.35645890%推荐起点继续观察0.45363494%漏掉了 2 辆明显黑烟车从这张表可以看出来阈值上调会压缩误报但也会损失召回。黑烟车识别业务的正确率目标建议设在 90% 左右宁可从 100 辆车里漏掉 2 辆也不能让审核人员每天面对几百条无效告警。我自己的习惯是每次调整模型、阈值或投票参数后都保留一组固定的验证视频重新跑一遍把误报率变化记录下来。不要靠肉眼在设备端看几分钟就下结论黑烟车是低频事件没有统计数据的调试都是玄学。参数修改要一次只动一个变量否则出了问题根本不知道是哪个参数导致的。希望这些方法和踩坑记录能帮你在复现黑烟车自动识别系统时少走几段弯路。本文还有配套的精品资源点击获取