简介这是一份面向计算机视觉目标检测学习者的抽烟检测数据集适用于深度学习、机器学习相关研究与项目实践。资源围绕JPEGImages、Annotations、Imagenet三个目录组织已用labelImg完成标注可直接用于训练YOLO、SSD、Faster R-CNN等常见检测模型也可作为课堂实验或毕业设计的训练素材。压缩包共2000个文件以4650张jpg图片和3235个xml标注文件为主体另有少量txt列表、Python转换脚本及说明文档可帮助读者快速划分训练集、验证集和测试集。整包约298.09MB结构清晰便于按需索引。目前已有7526人学习下载。对于需要现成标注数据、又想避免重复标注的同学而言拿到批注后的图片与xml文件即可展开模型训练脚本还能辅助完成VOC到YOLO格式的转换降低了入门门槛适合快速搭建抽烟检测实验流程。1. 抽烟检测数据集先懂标注边界再谈开训练一份标注好的抽烟检测数据集拿到的第一反应往往是开训练但真正决定模型现场表现的是标注框画在哪、负样本有没有、场景覆不覆盖夜间和遮挡。这组标注图片的价值不在张数多少而在于标注的边界和组合是否吻合一线的真实机位。尤其做工地或园区禁烟巡检的开发者别急着把图片喂给训练脚本先用几十分钟把格式盘清楚、把类别边界定死再动手会顺得多。这篇文章就围绕抽烟检测数据集展开从格式体检、训练参数、增强策略到踩坑记录给一整套可复制的链路适合正在做安全巡检、行为分析模型或者想自建烟感识别能力的开发者参考。2. 拿到标注图片先别急着训练格式、场景与类别体检2.1 标注格式三形态VOC、COCO、YOLO 的检查脚本这类数据集给到手里最常见的格式有三种VOC 的 XML、COCO 的 JSON、YOLO 的 TXT。如果你的标注文件是 JSON 嵌套嵌套再嵌套别慌先确认里面有没有 categories 和 annotations 字段有的话就是 COCO如果每个图片旁边挂一个 XML那就是 VOC如果 labels 目录里躺着同名的 txt每行五个数字这是 YOLO 格式最省事。我拿到数据后的第一个动作不是看图片而是把标签目录完整扫一遍。下面这个脚本统计 YOLO 格式标签里每个类别有多少框、每张图有几个框、有没有空标签十分钟就能把一份数据集的健康水平摸清楚# 扫描 YOLO 格式标签目录统计类别和框数 from pathlib import Path from collections import Counter label_dir Path(labels) # 标签目录txt 与图片同名 img_box_counts [] # 每张图的框数记录 cls_counts Counter() # 类别 - 框总数 empty_labels [] # 空标签文件名列表 for label_path in label_dir.glob(*.txt): boxes 0 for line in label_path.read_text(encodingutf-8).splitlines(): line line.strip() if not line or line.startswith(#): continue parts line.split() if len(parts) 5: cls_counts[int(parts[0])] 1 boxes 1 img_box_counts.append(boxes) if boxes 0: empty_labels.append(label_path.stem) print(类别 - 框数:, dict(cls_counts)) print( 单图框数: 最少%d, 最多%d, 平均%.2f % (min(img_box_counts), max(img_box_counts), sum(img_box_counts) / len(img_box_counts)) ) print(空标签文件数:, len(empty_labels))这段脚本的逻辑很直白按行读取 txt五行一组完整标注才算一个框。统计完你会看到三件关键信息类别分布是否悬殊、单图框数是否离谱、空标签有多少。如果某个类别只占全量框数的 1%训练时就会被大类别淹没如果单图平均框数超过 5大概率是标注人员把远处模糊目标也画上了这类框会在训练中带来大量背景噪声。YOLO 格式的坐标是归一化浮点数顺序是 class, x_center, y_center, width, height所有数值除以了图片宽高取值范围 0 到 1。检查时如果发现某个坐标跑到 1.2 或者 -0.1说明转换脚本有 bug这种标签在增强阶段会直接让训练崩溃。格式问题别靠肉眼盯把统计跑一遍发现问题再决定是修脚本还是回去找数据源核对。COCO JSON 的检查思路类似用 json 库解析后看一眼 annotations 里每个框的 bbox 字段是否是 [x, y, w, h] 的像素坐标再看看 segmentation 是不是空的。很多所谓的 COCO 格式只保留了 bbox 没保留分割多边形这本身不影响检测训练但如果脚本里同时读取 segmentation 就会报 KeyError。先确认字段再写转换能省掉一大半调试时间。格式典型文件坐标类型最容易踩的坑VOC.xmlxmin, ymin, xmax, ymax像素width/height 取错导致转换后框偏移COCO.jsonx, y, w, h像素左上角坐标被误当中心点YOLO.txtcx, cy, w, h归一化坐标超出 0~1 范围2.2 场景与类别体检亮度、框大小和分布不是靠猜格式检查只是第一步第二步要回答一个问题这份数据集到底覆盖了几种现场环境。很多数据集图片量看着不少但 80% 是白天晴天同一个机位拍的夜间、背光、雨雾场景只有零星几十张。这种分布喂出来的模型白天检测效果唬人一换环境就露馅。我一般会写一个简单的图片体检脚本统计每张图的尺寸和平均亮度把接近全黑或全白的坏图直接筛出来。这个脚本不要依赖深度学习框架PIL 就够了# 用 PIL 检查图片尺寸与亮度筛出全黑/全白坏图 from PIL import Image from pathlib import Path image_dir Path(images) sizes [] brightness [] bad_images [] for img_path in image_dir.glob(*.jpg): with Image.open(img_path) as im: sizes.append(im.size) gray im.convert(L) hist gray.histogram() total sum(hist) avg sum(i * c for i, c in enumerate(hist)) / total brightness.append(avg) if avg 12 or avg 243: # 接近全黑或全白 bad_images.append(img_path.name) print(尺寸种类:, sorted(set(sizes))) print(亮度范围: %.1f ~ %.1f % (min(brightness), max(brightness))) print(疑似坏图:, bad_images[:20])这个脚本跑出来的亮度范围很有参考价值。平均亮度集中在 120 到 180说明数据集主体是白天正常光照如果最小值只有 20 多意味着有夜间样本和弱光样本但到底有多少、占比如何还得再按目录统计。我不建议直接删掉低亮度图片相反夜间样本稀缺时它们是宝。关键是你要知道数了多少张后面增强和补充采集才能有的放矢。图片尺寸也是重要指标。监控画面常见的是 1920x1080 或 1280x720如果数据集里混着一堆 300x300 的小图说明部分是网络爬取或视频截帧缩放而来的这类图片本身清晰度不够标注框对应的目标在训练时很难学到有效特征。抽烟检测的目标本身偏小图片分辨率低等于雪上加霜。我会优先把分辨率低于 640x640 的图片单独分出来先看标注框是否还能辨识再决定是丢弃还是作为困难样本保留。2.3 重复样本与标注一致性数据去重不是可选项重复样本是这种视频抽帧型数据集的高发问题。同一个机位同一段监控视频按每秒抽帧得到的连续几帧内容几乎没变化放进训练集后模型很容易“记住”特定画面验证指标虚高。更隐蔽的是抽帧后标注也被复制了一份训练集和验证集里同时出现高度相似的画面验证指标直接失真。这种问题靠肉眼根本看不出来得靠脚本。最简单的方式是用文件 MD5 去重能去掉完全相同的副本。但视频抽帧的连续帧其实 MD5 不一样只是内容相似这时候按文件名抽帧间隔检查往往更有效。我更常用的做法是感知哈希把每张图缩放成 8x8 灰度图算 64 位哈希汉明距离小于 5 的视为近似重复# 感知哈希去重找出近似重复的图片 from PIL import Image from pathlib import Path def phash(image_path, hash_size8): with Image.open(image_path) as im: im im.convert(L).resize((hash_size, hash_size)) pixels list(im.getdata()) avg sum(pixels) / len(pixels) return sum( (1 i) for i, p in enumerate(pixels) if p avg ) image_dir Path(images) hashes {} duplicates [] for img_path in image_dir.glob(*.jpg): h phash(img_path) for prev_h, prev_name in hashes.items(): if bin(h ^ prev_h).count(1) 5: duplicates.append((img_path.name, prev_name)) break hashes[h] img_path.name print(疑似重复对:, duplicates[:30])这段代码的核心理解点有两个。第一感知哈希比对的是图像的低频结构不是像素级相等所以连续几帧、光照稍微变化的重复帧也能被找出来。第二阈值 5 意味着 64 位里有 5 位不同就认为近似这个值对监控画面偏宽松对场景差异大的图片会误判所以输出结果要抽几张确认别盲目删除。我还习惯检查标注框的一致性。比如同一个人在同一画面里两个标注员一个框住整只手一个只框住烟头模型就会收到互相矛盾的监督信号。这种问题脚本很难完全自动判断我的做法是从数据集里随机抽 30 张图把标注框画回图上人工扫一遍。这件事虽然土但比训练完再回头查标注可靠得多后面第 4 章会给出可视化抽查脚本。提示数据体检花的时间不是浪费。大部分训练翻车现场根源是数据分布或标注格式的问题而不是模型结构的问题。3. 把这份标注图片跑通 YOLO 训练转换脚本与关键参数3.1 格式转换VOC XML 转 YOLO TXT 的完整脚本体检通过后第一道正式工序是格式转换。现在主流检测框架最省事的是 YOLO 格式一个 txt 对应一张图不用解析嵌套 JSON训练时直接按路径读文件。如果手里是 VOC XML我习惯先转成 YOLO txt脚本如下# VOC XML 转 YOLO TXT支持多类别映射 import xml.etree.ElementTree as ET from pathlib import Path class_map {smoke: 0, smoke_with_hand: 1} # 按数据集定义调整 def voc_to_yolo(xml_path, out_path, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_map: continue # 跳过不在类别表中的目标 box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) cx ((xmin xmax) / 2) / img_w cy ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{class_map[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) Path(out_path).write_text(\n.join(lines), encodingutf-8) xml_dir Path(Annotations) img_dir Path(JPEGImages) out_dir Path(labels) out_dir.mkdir(exist_okTrue) for xml_path in xml_dir.glob(*.xml): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w float(size.find(width).text) img_h float(size.find(height).text) out_path out_dir / (xml_path.stem .txt) voc_to_yolo(xml_path, out_path, img_w, img_h)这段脚本的坐标转换逻辑是VOC 的 bndbox 给出左上角 xmin、ymin 和右下角 xmax、ymax先求中心点再除以图片宽高归一化。最容易出错的地方就在除以的到底是宽还是高——x 相关的一律除以图片宽度y 相关的一律除以图片高度。混用一次框的位置就会偏移轻则 mAP 掉几个点重则训练 Loss 直接 Nan。还有两个细节值得注意。第一个如果脚本里出现某个类别被跳过说明数据集的类别命名和你的 class_map 对不上先回去看原始 XML 里的 name 文本很多数据集会用中文标签或带序号后缀。第二个如果图片是 PNG 且有透明通道读尺寸时别用 split 文件名的方式猜直接从 XML 的 size 节点里读最可靠因为标注时的图片尺寸和文件实际尺寸理论上是一致但偶尔被压缩工具改过。COCO JSON 转 YOLO 的原理一样只是解析方式不同。COCO 的 bbox 字段直接给的是 [x, y, w, h]x 和 y 是左上角中心点就是 x w/2、y h/2。转的时候要注意 annotations 里的 category_id 和 categories 里的 id 映射关系这个映射错一位整个类别标签就全错了。转换完成后我会顺手用第 2 章的统计脚本再跑一遍确认每个类别的框数和原标注对得上这叫等值校验。3.2 训练参数怎么设五个随便照抄会翻车的参数格式转完进入训练环节。很多人喜欢直接抄默认参数但烟火检测这种小目标场景默认参数大概率不给力。以常见的 YOLO 训练命令为例我一般会这样起手# 训练命令示例参数按本数据集的量级和场景特点调整 python train.py --data smoke.yaml --imgsz 960 \ --epochs 200 --batch 16 --lr0 0.01 \ --mosaic 1.0 --mixup 0.15 --patience 50这里五个参数是关键。第一个是imgsz也就是训练分辨率抽烟检测的目标在一个 1080p 画面里往往只有几十像素宽640 的默认值会让目标缩小到几乎不可见我建议拉到 960 甚至 1280。代价是显存上涨、训练变慢但检测效果通常是实打实的提升这个交换很值。第二个是epochs这种垂直场景数据集一般不大几千张的量级模型很快就能收敛但早停 patience 要留够不然验证集指标还在缓慢爬升就被掐断。我的习惯是把 epochs 设成 200patience 设成 50让模型自己决定什么时候停。第三个是batch它直接受显存限制但要注意多卡训练时全局 batch 才是真正影响梯度更新的值单卡 16 和双卡 8 的效果不一样。第四个是lr0迁移学习下 0.01 通常没问题但如果训练开始 Loss 就在 3 附近震荡不下降把 lr0 调到 0.005 再试。最后一个容易被忽略的是mosaic和mixup这两个是增强参数而不是结构参数对小目标影响很大mosaic 在目标较小时会进一步缩小目标尺寸后文会详细展开。参数常见默认抽烟检测场景建议原因imgsz640960~1280目标小高分辨率保留细节epochs100200 或早停小数据量需要更充分拟合batch8-16按显存尽量大全局 batch 影响收敛跳动lr00.010.005~0.01数据噪声大时降低初始学习率mosaic1.00.8~1.0样本少时提升明显但小目标需谨慎参数不是一次调出来的。我先按上面这组跑一个短实验只看前 20 轮的 Loss 曲线如果 Loss 能稳步下降、验证集 mAP 在波动中上升就继续跑到早停如果 Loss 炸了或者验证集纹丝不动优先检查数据集路径和标签格式别急着调参。很多所谓参数问题实际是标签路径写错导致模型在学空气。3.3 验证指标与验证集划分mAP 高不等于现场好用训练结束先看指标。检测模型最常看的两个值是 mAP50 和 mAP50-95前者是 IoU 阈值 0.5 下的平均精度后者是 0.5 到 0.95 之间多个阈值下的综合值。对抽烟检测这种小目标场景mAP50-95 通常偏低因为小目标框的 IoU 计算很吃亏稍微偏一点就掉档。我一般以 mAP50 为主要验收指标同时关注 PR 曲线在召回率端的走势。但最误导人的指标恰恰来自验证集划分不严谨。随机划分训练集和验证集会让同一机位同一时段的连续帧同时出现在两边模型在验证集上的表现基本等于开卷考试。换到另一个机位、另一个角度的现场mAP 可能直接腰斩。正确的做法是按场景分组划分保证同一个机位或同一天的画面只进入一边# 按目录分组划分训练/验证集避免同场景画面泄漏 from pathlib import Path import hashlib image_dir Path(images) val_ratio 0.15 train_lines, val_lines [], [] for img_path in image_dir.glob(*.jpg): group img_path.parent.name # 以所在目录作为场景分组 h int(hashlib.md5(group.encode()).hexdigest(), 16) if h % 100 val_ratio * 100: val_lines.append(str(img_path)) else: train_lines.append(str(img_path)) print(ftrain: {len(train_lines)}, val: {len(val_lines)})这段脚本用目录名做分组哈希同一个目录下的所有图片都会进入同一边从机制上杜绝了同场景跨集合的问题。它的好处是不依赖额外库而且只要分组名不变每次划分结果一致可复现性很好。如果数据集本身没有按目录分组也可以用文件名的前缀来分比如某摄像头点位的前缀本质一样。划分完成后还要做一件事确认验证集里每个类别都有足够样本。如果验证集里某一类的框少于 20 个计算出的该类 PR 曲线没有统计意义看起来 0.9实际没法代表现场表现。我通常会把训练集和验证集的类别分布各打一份人工核对差异分布差异超过 30% 就重新划分。这一步虽然繁琐但能帮你判断 mAP 到底是真实能力还是数据划分的巧合。注意验证集的目的不是让指标好看而是模拟没见过的现场。划分造假比训练不足更容易毁掉一个项目。4. 增强、负样本与自建补充夜间和遮挡场景的救命组合4.1 增强参数的真实建议值哪些要开大哪些要保守公共数据集的增强配置大多是通用场景调出来的直接套用到抽烟检测上有些会适得其反。比如色相扰动烟的颜色本身就是识别的重要线索如果 hsv_h 调太大模型会把颜色学得模棱两可反而分不清黄色烟嘴和肤色。我的建议值如下# 数据增强配置YOLO 风格配置写法 hsv_h: 0.02 # 色相扰动不要大烟的颜色是强特征 hsv_s: 0.6 # 饱和度扰动模拟不同灯光下的烟体 hsv_v: 0.45 # 亮度扰动夜间场景需要大一点 mosaic: 0.8 # 样本少时提升明显但小目标会被缩小 mixup: 0.15 # 场景混合过量会混淆小目标特征 copy_paste: 0.3 # 把标注物体贴到新背景适合遮挡样本 fliplr: 0.5 # 水平翻转左右手对称 scale: 0.5 # 尺度扰动模拟机位远近变化这里每个参数都有讲究。hsv_v 亮度扰动在夜间样本少时要加大因为实地场景里夜间监控画面亮度差异很大不加足的话模型会把暗光下的烟漏掉。mosaic 是双刃剑它把四张图拼在一起既增加了样本多样性又相当于把每个目标缩小了一半对本来就小的烟头目标很不友好。所以我不会把 mosaic 拉满到 1.0而是留一点余量。mixup 同理把两张图半透明叠加如果叠加后两个烟的目标重叠在一起标注框里的内容语义就乱了。我的经验是 mixup 不超过 0.2主要让模型耐受背景混杂而不是依赖它生成新的烟样本。copy_paste 反而是这类数据集最好用的增强把手工标注的“手烟”区域粘贴到没有烟的背景图上直接扩充样本数尤其适合手部遮挡样本的补充。增强参数定下来后还要注意增强在训练后期是否需要关掉。有些框架支持在最后几十轮关闭 mosaic 和 mixup让模型回到真实图片分布上微调这是防止增强分布和真实分布偏离太远的重要手段。如果训练日志里验证集指标在最后阶段反而下降优先怀疑是不是增强过强导致模型学到的全是合成分布。4.2 用模型自带难样本挖掘来补标注数据集样本不够怎么办与其靠人工拍摄我更喜欢用难样本挖掘的循环来补。先把当前模型在未标注视频上跑一遍推理把置信度落在 0.3 到 0.5 之间、看起来像烟但模型不敢确认的候选框截出来人工复核后回填到训练集。这种方式补的样本质量比随机采集高得多因为它专挑模型搞不定的场景# 批量推理未标注视频帧输出低置信度候选框到文件 python predict.py --source unlabeled_video/ --conf 0.25 \ --save-txt --project hard_mining/这段命令的作用是把所有低于正常告警阈值、但又高于 0.25 下限的检测结果保存下来后续脚本按置信度区间分类导出。我通常分两档置信度 0.4 到 0.5 的框优先人工复核因为它们最可能是模型“差一点就认出来”的正样本置信度 0.5 以上的大概率是错误检测反而可以拿去做负样本分析。人工复核的动作要快。我的习惯是把候选框按图片切片每张切片存成小图一次看 200 张把认为是烟的拖到 positive 文件夹认为是误检的拖到 negative 文件夹然后分别回填。这个过程比从头标注效率高一倍以上而且模型不认识的场景往往就是现场最难的场景补进去立竿见影。难样本挖掘要跑几轮每轮补 100 到 300 个样本重新训练后模型继续暴露新的盲区。这个循环一般跑两到三轮模型的现场表现会明显上一个台阶。值得提醒的是挖出来的样本在划分训练集验证集时同样不能随机分要继续按场景分组否则又会出现数据泄漏。4.3 错框修正与标注一致性抽检 30 张比训练 100 轮有用训练之前我强烈建议把标注框可视化抽查一遍。这一步针对的是标注质量尤其是错框和漏框——框只包住烟头、把整只手框成全身、同一类目标框大小差异巨大这些都是标注层面的硬伤增强和调参都救不回来。下面这个脚本能把标签画回图上抽查 30 张就够看出问题# 把 YOLO 标注框画回原图人工抽检错框 from PIL import Image, ImageDraw from pathlib import Path label_dir Path(labels) image_dir Path(images) out_dir Path(vis_annotations) out_dir.mkdir(exist_okTrue) for label_path in sorted(label_dir.glob(*.txt))[:30]: img_path image_dir / (label_path.stem .jpg) if not img_path.exists(): continue im Image.open(img_path) w, h im.size draw ImageDraw.Draw(im) for line in label_path.read_text().splitlines(): parts line.split() if len(parts) 5: continue _, cx, cy, bw, bh map(float, parts[:5]) x1 (cx - bw / 2) * w y1 (cy - bh / 2) * h x2 (cx bw / 2) * w y2 (cy bh / 2) * h draw.rectangle([x1, y1, x2, y2], outlinered, width3) im.save(out_dir / (label_path.stem .png))抽检时重点看三类问题框是否完整覆盖了手和烟、是否明显大于实际目标、是否把无关背景也框了进来。抽烟检测的目标定义我建议统一为“手部与烟的接触区域”包含手指和烟体的重叠部分而不是只框烟头或只框整只手。这样定义的好处是目标语义一致模型学到的特征既包含烟的长条形状也包含手部姿态检测结果在遮挡时会更鲁棒。如果抽检发现某一批标注的框风格明显不一致比如前面几百张是全身框、后面的图全是烟头框这通常说明标注任务分给了多个人且没有统一规范。这时的解决方式不是返工全部重标而是把规范统一后用模型难样本挖掘补一轮让新标注覆盖模型最迷茫的区间。标注一致性对检测精度的影响很多时候大过模型结构的选择这一点在抽烟检测这种细长目标上尤其明显。5. 抽烟检测数据集避坑清单现象、原因与解决5.1 电子烟与真烟混标模型现场岔子不断现象部署后发现有人拿电子烟也被报警或者某个角度抽真烟反而被漏掉。训练指标正常一上现场就频繁出问题。原因很多标注人员在标注时把“只要是嘴里叼着的管状物”都归为烟电子烟和真烟混在同一个类别里。模型学到的特征于是变得模糊电子烟的金属质感、LED 灯和真烟的燃烧状态互相干扰。解决把类别定义彻底分开。真烟是一个类电子烟是另一个类或者直接归入负样本。如果场景要求电子烟也禁那就在后处理里把两个类都当成告警对象而不是在模型层面混成一个类。标注规范里必须写明电子烟和真烟的图形区别最好配几张对比图发给标注方。5.2 手部遮挡漏检框得住手也框不住烟现象训练时白天场景 mAP 很高但夜间或手部握拳遮挡时模型大面积漏检。可视化检测结果发现框要么没打上要么只框住半只手。原因训练样本里手部展开、烟完整露出的图片占绝大多数遮挡和握拳姿态占比太少模型没有见过“手把烟包住但指尖有烟头露出”的形态。解决手工补一批遮挡样本再利用 copy_paste 增强把烟和手一起贴到纯背景上制造更多遮挡姿态。也可以把目标重新定义为“手部烟的接触区”让模型在看不到完整烟体时靠手部动作和烟头露出的组合也能判断。5.3 白色长条物体误报麦克风成了烟现象监控画面里有人拿白色麦克风讲话系统报警有人拿白笔写字也被报警。误报集中在细长白色物体上。原因部分标注框只包住烟体没有包手模型学到的是“白色长条 烟”这种粗糙特征。真烟的烟体本身颜色确实偏白市面上的白笔、麦克风、勺子在外形上高度相似。解决把标注框扩大到包含手指强制模型学“手部 长条物体”的组合特征补充一批白色长条物体的难负样本推理时将置信度阈值从 0.25 提高到 0.4 以上过滤掉低置信度的边缘误报。5.4 随机划分导致指标虚高换机位就崩现象训练日志里验证集 mAP50 达到 0.92信心满满去现场测试换一个摄像头角度后 mAP 掉到 0.6 以下。原因训练集和验证集是随机划分的同一个点位同一个下午的视频帧被拆到了两边模型在验证集上见过几乎所有画面指标自然好看。真实现场的角度、距离、光线全变了模型没有泛化能力。解决按点位或时间段分组划分训练集验证集保证同一个机位只出现在一边。更严格的做法是按天划分拿某一天的数据当验证集因为真实项目的模型更新就是这么发生在未来数据上的。5.5 模糊小目标乱标训练损失一直跳现象训练时 Loss 曲线波动剧烈验证集指标忽高忽低画出的检测框经常只有火柴棍那么宽。原因数据集里包含大量极远距离或低分辨率图片标注员为了“画全”目标把只有十几个像素的模糊区域也标了框这些框对应的特征根本不可学模型反复在这些样本上震荡。解决在标签预处理阶段过滤掉过小的框比如宽或高小于图片尺寸 1% 的标注直接删除。如果删除后某些图片变成空标签把整张图移出数据集。先跑一轮清洗再训练Loss 曲线的稳定性通常会明显改善。6. 进阶联合多模型与帧级后处理把数据集的价值吃到最后一公里6.1 联合安全帽检测与抽烟检测的规则输出单模型能出框但现场要的是决策。工地禁烟场景里我一般会把抽烟检测模型与安全帽检测模型的输出做一次规则合并两个条件同时成立才触发最高级别告警。这样能显著降低单模型的误报率因为两个模型同时误检同一帧的概率很小# 后处理规则未戴安全帽 抽烟同时成立组合触发告警 def trigger_rule(helmet_result, smoke_result, frame_id): missing_helmet helmet_result[missing_count] 0 smoke_detected smoke_result[detected_count] 0 if missing_helmet and smoke_detected: return fframe {frame_id}: 未戴安全帽且抽烟 return None这段规则代码本身简单但背后的设计思路值得注意用多个弱信号组合成一个强信号。单个模型可能存在 5% 的误报率两个模型都是 5%同时误报的概率大幅下降。部署时还可以再加一个时间维度的条件比如连续 3 帧都检测到才算有效告警效果会更稳。6.2 十分钟现场置信度标定法模型训练完指标再好看也要过现场这一关。我的习惯是第一次部署时采集半小时现场视频跑一遍推理把每个框的置信度按从小到大排个序看前 20 个“模型认为最像烟”的结果里有没有误检。如果有就把置信度阈值往上提如果一个真烟都没漏但误报还很多就在后处理里加规则过滤。这十分钟的调阈值流程比在办公室调一整天参数都有用。做这类数据驱动的项目久了我养成一个习惯每次拿到新数据集永远先跑统计脚本再谈训练把数据分布和标注质量当成第一优先级。这份标注图片哪怕张数不多只要框定义统一、场景覆盖对路训练出来的模型就比“量大但乱标”的数据集可靠得多。希望这篇实战笔记能帮你在抽烟检测这条路上少走几步弯路把数据集的价值真正吃到最后一公里。本文还有配套的精品资源点击获取