☰
240张火焰烟雾图像数据集:从数据审计到YOLOv8训练部署
2026/10/1 3:37:09 网站建设 项目流程

简介:这是一份面向图像分类学习与实战的小型标注数据集,聚焦火焰、烟雾、正常三个类别,共约240张图片。作为入门级视觉识别项目的数据来源,它适合用于训练CNN或YOLOv5分类模型,也可用于验证分类算法、调试训练流程,尤其适用于消防预警、安全生产监控等场景的算法预研。资源包共243个文件,主体为240张jpg图像,另附1个json标注文件、1个可视化python脚本和1张示意png,压缩包仅504KB,结构轻量,便于快速下载与实验。目前已有112人学习下载,数据已按训练集和测试集划分,同类别图片分目录存放,json文件记录了具体分类信息;运行附带的show脚本即可直观查看样本,无需额外整理即可接入常见深度学习框架。对于希望快速跑通分类流程、积累火灾烟雾样本或开展数据增强实验的开发者,这份资源提供了干净、可直接使用的起步数据集,省去自行采集与标注的时间成本。

1. 这个“240张已标注”数据集,距离跑通火焰烟雾识别还有多远

晚上八点的厂房监控、凌晨三点的森林防火视频、无人值守的配电房——火焰烟雾图像识别要解决的从来不是“认识一张图”,而是“在错报和漏报之间找平衡”。这套数据集把画面分成了火焰、烟雾、正常三类并做了标注,约240张的规模在真实项目里属于典型的“demo级数据”:直接训练必然过拟合,但配合迁移学习、数据增强和正确的验证集划分,完全能训练出一个在固定场景下可用的识别模型。适合刚接触深度学习图像识别的学生、要快速给客户演示消防预警的集成商,以及想评估“小样本能不能做”的算法工程师。下面我按自己处理小数据集的完整流程来讲,从审计到部署每一步都给你能直接抄的脚本和参数。

2. 先搞清楚手头数据:用审计脚本摸清类别分布、标注格式与图像规格

拿到数据集的第一步不是写训练代码,而是审计。240张图听着不多,目录结构却可能五花八门:有的按类别建文件夹,每张图一个标签;有的是一堆jpg配一个JSON文件;还有的是XML和图片混在一起。最常见也最稳妥的做法,是先写个脚本把三件事查清楚——样本量、标注格式、类别分布。这三样直接决定后面走分类还是检测路线,也决定数据怎么划分、参数怎么设。我见过有人跳过这一步直接跑YOLO,跑到一半才发现手头数据是分类标签,白白浪费一晚上。

2.1 先回答三个问题:这是分类数据集,还是检测数据集

标题写着“已标注”,但没写标注粒度。这里要区分两件事:如果每张图只有一个类别标签,那是图像分类数据集;如果每张图里有矩形框、多边形框,或者一张图同时标了多个目标,那是目标检测数据集。打开一个标注文件就能分辨——XML里的object节点带bndbox是检测,txt里只有一行数字的是分类标签。另一个判断依据是文件组织:文件夹名就是类别名的,多半是分类;图片和标注文件同目录、每张图对应一个标注文件的,多半是检测。

判断结果直接影响模型选型:分类数据集用ResNet、EfficientNet就能跑,检测数据集就走YOLO、Faster R-CNN。从我做过的消防类项目看,告警系统需要知道“哪里着火了”,所以九成会配检测标注。但那九成里又有不少标注质量堪忧——框得松、类别错、漏标目标。我的习惯是审计时顺手抽10张图,用labelimg打开,把标注框可视化出来逐张核对一遍,肉眼扫一遍比任何统计指标都直观。重点看三处:框是不是包住了目标主体,火焰和烟雾的类别有没有标反,图里明显有火却漏标的占多少。

对这套火焰、烟雾、正常图像识别数据集来说,还有一个特殊问题:烟雾是半透明的,边界天然模糊。审计时如果把同一张烟雾图反复放大看,会发现标框的标准在不同标注员手里差很多,有的框只包住浓烟核心,有的框把淡淡的烟带也圈进去。这个差异后面会直接影响mAP,我在第5章会展开讲。

2.2 用一段Python脚本审计数据集:统计样本量、标注类型与类别数量

下面这段脚本不依赖特定目录结构,你先把数据集根目录换成实际路径,跑完就能看到一份汇总报告。需要Python环境和两个库,命令行里执行pip install pillow tqdm装一下就行。

import os, json from collections import Counter from PIL import Image from tqdm import tqdm root = "fire_smoke_dataset" # 数据集根目录,按实际路径修改 img_exts = {".jpg", ".jpeg", ".png", ".bmp", ".webp"} images, label_files = [], [] for dirpath, _, files in os.walk(root): for f in files: ext = os.path.splitext(f)[1].lower() if ext in img_exts: images.append(os.path.join(dirpath, f)) elif ext in {".xml", ".json", ".txt"}: label_files.append(os.path.join(dirpath, f)) print(f"图片数量: {len(images)}") print(f"标注文件数量: {len(label_files)}") # 统计图片尺寸分布,只采样前50张,避免全量解码太慢 widths, heights = [], [] for img_path in tqdm(images[:50], desc="检查图片尺寸"): with Image.open(img_path) as im: w, h = im.size widths.append(w); heights.append(h) print(f"尺寸采样: 宽 {min(widths)}~{max(widths)}, 高 {min(heights)}~{max(heights)}") # 标注文件扩展名集合,判断标注格式 label_exts = Counter(os.path.splitext(f)[1].lower() for f in label_files) print(f"标注格式分布: {dict(label_exts)}") # 尝试读取每个标注文件里的类别名 cls_counter = Counter() for lf in label_files: if lf.endswith(".xml"): with open(lf, encoding="utf-8") as f: content = f.read() for obj in content.split("<object>")[1:]: cls = obj.split("<name>")[1].split("</name>")[0] cls_counter[cls] += 1 elif lf.endswith(".json"): with open(lf, encoding="utf-8") as f: data = json.load(f) for ann in data.get("annotations", []): cls_id = ann.get("category_id") for cate in data.get("categories", []): if cate["id"] == cls_id: cls_counter[cate["name"]] += 1 # txt文件是分类标签还是YOLO检测框,统一按行读取后统计 elif lf.endswith(".txt"): with open(lf, encoding="utf-8") as f: for line in f: parts = line.strip().split() if len(parts) >= 5: # YOLO格式: cls x_center y_center w h cls_counter["yolo_cls_" + parts[0]] += 1 elif len(parts) == 1: # 分类格式: 单一类别编号 cls_counter["cls_" + parts[0]] += 1 print(f"类别统计: {dict(cls_counter)}")

这段脚本的逻辑分四步:先全量扫描目录,把图片文件按扩展名收集起来,同时记录标注文件的扩展名;再抽前50张图,读它的宽高统计尺寸区间;根据标注文件的扩展名分布判断格式是XML、JSON还是TXT;最后分别按三种格式解析类别名。TXT的解析我故意写成能区分两种情况:一行只有一列数字是分类标签,一行五列以上是YOLO检测框的类别加坐标。

参数上注意两个地方。images[:50]是尺寸采样数,数据上百张时50张够用,如果图片特别杂、横竖屏混着来,建议提到images[:100]。XML解析用的是字符串split,它要求标注文件结构规整、标签里没有嵌套的同名节点;如果你的XML是LabelImg生成的,这样解析没问题,如果是其他工具生成的复杂结构,用xml.etree.ElementTree解析更稳。

跑完脚本,你大概率会看到类似这样的汇总:图片240张,标注文件240个,XML格式为主,类别统计里火焰六七十个、烟雾四五十个、正常图像占一半。到这里才算真正“认识”了这份数据,后面所有操作都有了依据,而不是对着一个黑匣子瞎调。

2.3 240张图的现实:类别不均衡、尺寸不统一、算力反而好说

审计报告出来后,通常还会发现三个共性问题。

第一个是类别不均衡。火焰和烟雾在真实场景里是少数事件,正常画面占了大头。240张图里正常图像可能占120张,火焰只有70张,烟雾只有50张。训练时模型天然偏向多数类,推理时最先漏掉的是烟雾。处理办法不是删正常图,而是第3章的数据增强配合第4章的类别权重,让少数类在loss计算里获得更高的话语权——这个动作直接决定模型能不能在真实场景里“看见”那一小缕烟。

第二个是图片尺寸不统一。有的从监控抽帧,1920×1080;有的是网上收集的缩略图,640×480。训练时网络会统一resize,小图拉伸后目标变形,大图缩小后烟雾细碎纹理丢失。一个稳妥做法是训练尺寸定在640或1280,部署时按同一尺寸推理,避免训练和推理分辨率不一致导致玄学掉点。

第三个反而是好消息:算力完全不是瓶颈。240张图,哪怕全是1920×1080,用6GB显存的消费级显卡跑YOLOv8n,一轮epoch也就几十秒,完整调参一轮用不了一小时。这个规模不需要服务器集群,笔记本就能干完,调参的等待成本很低,可以放开手试参数。

3. 把数据整理成YOLO能吃的格式:标注转换、按场景划分与三倍数据增强

审计做完,数据长什么样心里有数了,接下来就是把原始目录整理成训练能直接吃的形态。这里的核心动作有三个:统一标注格式、划分训练验证集、做增强。每一步都有一次性做对的细节,错了后面训练全白跑。

3.1 分类与检测两条路线怎么选:看标注粒度,别想当然

如果审计确认标注文件里全是边界框,就走检测路线,模型输出每个目标的坐标和类别,告警联动时能告诉监控中心“画面左上方有火焰”。如果标注只有类别编号,就走分类路线,模型只输出整张图的类别。两者训练流程差别很大:检测要处理坐标归一化、锚框匹配,分类只需要目录结构和标签文件。

对240张这种规模,如果数据支持检测,我一般建议做检测。理由不是技术上的,而是交付上的:客户问“哪里起火了”,分类模型答不上来,检测模型可以。而且YOLO的预训练权重对小数据集友好,直接用官方提供的yolov8n.pt,再冻结浅层做微调,几十分钟就有基础效果。分类路线的价值在于模型更轻、部署更快,适合只需要判断“有没有火情”的室内小场景,我在第6章会提一句。

还有一种常见情况是:标注文件里既有框也有类别,但框的质量参差不齐,个别框严重偏离目标。别急着删,把明显标错的几十张挑出来,用labelimg重新标一遍。240张图里重标30张,一小时左右,这比任何调参技巧都值——数据标注的质量就是小数据集的上限,标注是错的,模型再调也是白搭。血泪经验是:先花时间把标注统一了,后面的训练曲线会平滑很多。

3.2 把XML标注转成YOLO训练格式:一个脚本和三个边界参数

LabelImg导出的XML标注是最常见格式,而YOLO训练要吃的是txt文件,每行一个目标,格式是类别id x_center y_center width height,四个坐标全部归一化到0~1。下面这段脚本把XML批量转成txt,顺便把越界框钳制回图像范围内。

import os import xml.etree.ElementTree as ET xml_dir = "dataset/labels_xml" # XML文件目录 img_dir = "dataset/images" # 图片目录 out_dir = "dataset/labels_yolo" # 输出txt目录 class_names = ["fire", "smoke", "normal"] # 按你的类别顺序定义,顺序就是id os.makedirs(out_dir, exist_ok=True) def xml_to_yolo(xml_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.strip() if name not in class_names: continue # 跳过没在类别表里的目标 cls_id = class_names.index(name) box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) # 归一化,并钳制到[0,1]防止越界坐标产生nan损失 x_c = max(0.0, min((x1 + x2) / 2 / img_w, 1.0)) y_c = max(0.0, min((y1 + y2) / 2 / img_h, 1.0)) w = max(0.0, min((x2 - x1) / img_w, 1.0)) h = max(0.0, min((y2 - y1) / img_h, 1.0)) lines.append(f"{cls_id} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}") return lines for xml_file in os.listdir(xml_dir): if not xml_file.endswith(".xml"): continue stem = os.path.splitext(xml_file)[0] # 读取同名图片拿真实宽高,不要用XML里记录的size,避免被缩放过的假尺寸带偏 img_path = os.path.join(img_dir, stem + ".jpg") if not os.path.exists(img_path): img_path = os.path.join(img_dir, stem + ".png") if not os.path.exists(img_path): print(f"跳过 {stem}:找不到同名图片") continue from PIL import Image with Image.open(img_path) as im: img_w, img_h = im.size lines = xml_to_yolo(os.path.join(xml_dir, xml_file), img_w, img_h) with open(os.path.join(out_dir, stem + ".txt"), "w", encoding="utf-8") as f: f.write("\n".join(lines)) print("转换完成,检查输出目录里的txt数量是否与图片数量一致")

三个边界参数值得单独说。第一个是class_names的顺序,它在转换脚本里直接决定类别id,训练时data.yaml里的names必须和它一一对应,顺序错一位所有标签全错。第二个是坐标钳制,烟雾框经常画出图像边界,x2比图片宽度还大,不钳制的话归一化坐标可能超过1,loss出现nan只是时间问题。第三个是图片尺寸来源,一定要以真实图片的宽高为准,而不是XML里记录的size节点,因为有些图片被预处理缩放过后,XML里记的还是旧尺寸,用错尺寸会导致框的位置整体偏移。

转换完看一眼输出:240张图应该对应240个txt,每个非空txt里每行五个数字。如果某个txt是空的,说明这张图没有标注目标,要么删掉这张图,要么保留它作为负样本——在检测训练里,一张没有目标的正常图对抑制误报很有价值,可以留着。

3.3 训练验证集划分:按场景隔离,防止数据泄漏

划分训练集和验证集是240张小数据集里最容易被忽视、却最能决定mAP真实性的步骤。错误做法是直接用train_test_split按图片随机分,同一监控画面抽帧得到的十几张连续图片,可能七八张进了训练集、两三张进了验证集。模型在训练时已经见过几乎一样的像素,验证集loss虚低,部署到新场景立刻打回原形。

正确做法是按“拍摄源”分组。火焰烟雾数据通常来自有限几个场景:某工厂的监控、某段公开视频的抽帧、某次火灾演练的照片。划分时把这些同源图片归到一个组,整组进训练或整组进验证。实操上,很多数据集的命名本身带了场景信息,比如文件名前缀是factory_001_0001.jpg这样的,按前缀分组再切分就行。

import os, random from collections import defaultdict img_dir = "dataset/images" train_dir, val_dir = "dataset/train", "dataset/val" os.makedirs(train_dir, exist_ok=True) os.makedirs(val_dir, exist_ok=True) groups = defaultdict(list) for f in os.listdir(img_dir): if not f.lower().endswith((".jpg", ".png")): continue prefix = f.split("_")[0] # 按文件名前缀分组,场景不同就换规则 groups[prefix].append(f) all_groups = list(groups.keys()) random.seed(42) # 固定随机种子,保证每次复现同样划分 random.shuffle(all_groups) val_groups = set(all_groups[: max(1, int(len(all_groups) * 0.2))]) # 20%场景进验证 for prefix, files in groups.items(): target_dir = val_dir if prefix in val_groups else train_dir for f in files: os.rename(os.path.join(img_dir, f), os.path.join(target_dir, f)) print(f"训练集 {len(os.listdir(train_dir))} 张, 验证集 {len(os.listdir(val_dir))} 张")

这段脚本按文件名第一个下划线前面的词分组,把20%的场景整组划到验证集,其余进训练集。random.seed(42)固定随机种子,保证多次运行结果一致;val_groups取20%的场景,数量少的数据集可以放宽到25%——验证集少于20张时mAP波动会非常大,一次训练涨跌几个点都是噪声。注意脚本是直接os.rename移动文件,跑之前先备份一份原始目录,移动错了至少还有后悔药。

划分完建议做一次交叉验证:把划分结果反向推一下,看看每个验证集场景里三类图片是否都有。如果某个验证集场景全是正常画面,验证时火灾检测能力就没人检验,mAP好看但没意义。

3.4 数据增强:240张翻成700张的常见组合

数据增强的目的是给模型“加样本”,但要加在合理范围内。用YOLOv8训练自带增强:随机翻转、HSV色域扰动、缩放、平移、马赛克。启动马赛克增强后,模型每次迭代看到的都是四张图拼起来的新图,相当于样本组合爆炸,对小数据集帮助很大。

我一般会配合albumentations做一批离线增强,把少数类别补起来。离线增强不需要写复杂的采样逻辑,直接用现成库定义转换器:

import albumentations as A AUG = A.Compose([ A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=0.5), A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=30, p=0.5), ])

这段增强配置只做三类改动:水平翻转、亮度对比度扰动、色调饱和度扰动,全部五五开概率。对火焰烟雾这类目标,亮度扰动的意义最大——真实监控里白天黑夜光照差异巨大,模型容易把高光误判成火焰,训练时见过各种亮度下的样本,误报能压下去不少。增强后的图片保存时要注意同步修改对应的txt标注,翻转后框的x坐标要做镜像变换;如果觉得麻烦,直接用ultralytics训练时内置的增强,它能自动处理标注同步,省心。

离线增强的目标不是无限翻倍,一般翻到原样本的两到三倍就够。翻得太多会让模型反复记忆同一批目标的变换,边际收益递减。增强完再跑一遍3.2的转换脚本,确认新图和新txt成对出现,再去训练。

4. 用YOLOv8把240张图训出可用模型:迁移学习、训练参数与指标解读

数据整理好了,接下来是训练。240张图直接从头训练会过拟合,正确打开方式是加载预训练权重做迁移学习——YOLO官方权重在COCO上见过几百万张自然图像,浅层的边缘、纹理、颜色特征已经训练得很成熟,我们需要让它把注意力放到“火焰的橙红色纹理”和“烟雾的半透明形状”上,而不是重新学怎么看一张图。

4.1 为什么要用预训练权重:迁移学习不是可选项,是必选项

从零训练一个检测头需要大量数据,而240张图连给backbone提供稳定的梯度都不够。迁移学习的逻辑是:模型浅层学习的是通用特征,比如边缘、颜色斑块,这些在任何视觉任务里都通用;只有深层才和具体任务强相关。所以加载yolov8n.pt之后,浅层特征直接复用,我们只训练深层的检测头和部分骨干,让模型把“已经会看图的网络”快速改造成“会看火的网络”。实践中,这个做法能让mAP从可能只有0.1的水平直接跳到0.6以上,而且训练时间缩短一个量级。

4.2 跑通最小训练命令:data.yaml、建目录结构、执行训练

先把数据集整理成YOLO约定目录结构:图片放train和val,标注txt按同名放labels/train和labels/val。然后写一个数据集配置文件,文件名随意,内容如下:

path: ./dataset # 数据集根目录,相对路径基于当前命令行位置 train: images/train # 训练图片目录 val: images/val # 验证图片目录 nc: 3 # 类别数:fire、smoke、normal names: ['fire', 'smoke', 'normal']

这里的path是根目录,train和val是相对它的子目录。nc必须等于names列表长度,类别名顺序要和3.2转换脚本里的class_names完全一致——我在第3章强调过,顺序错一位,训练出来的是个废模型。保存为fire_smoke.yaml后,执行训练命令:

yolo detect train data=fire_smoke.yaml model=yolov8n.pt epochs=60 imgsz=640 batch=8 freeze=10

命令里的freeze=10表示冻结前10层参数,浅层不再更新,只训练检测头。小数据集上这个参数能大幅降低过拟合风险。epochs=60不是拍脑袋定的,240张图在增强后约700张,60轮足够让检测头收敛;轮数再多,验证loss通常在第40到50轮就开始回升了。

4.3 小数据集的参数逻辑:epochs、batch、imgsz、patience

训练参数在小数据集上有和常规数据集完全不同的取舍,我把最关键的四个参数列成一张参考表:

参数240张数据建议值设置逻辑
imgsz640大于640会放大烟雾细节,但显存占用翻倍;小于640小目标容易丢
batch4~8240张图batch=8时每轮只有30次迭代,batch再小梯度噪声太大
epochs60~80配合早停,实际通常跑30~50轮就触发收敛
patience15~20验证指标连续这么多轮不涨就停,省时间
freeze10~12冻结浅层防过拟合,数据越少冻结层数越多

batch是最容易反复横跳的参数。显存6GB跑imgsz=640时batch只能开到8,再大就爆显存;如果改用yolov8n这种轻量模型,batch=16也能跑,但收益不明显——240张图本身迭代次数少,更大的batch只会让每轮梯度更新次数更少。我的经验是batch=8就是甜点,不必强求更大。

patience是yolo命令里默认带的行为,验证集mAP连续多少轮不提升就自动停。小数据集上这个机制特别有用,因为过拟合通常发生在某几轮突然出现,你不可能整夜盯着loss曲线,设个patience=20让它自己停,早上起来看结果就行。

4.4 训练日志里盯什么:loss曲线、mAP50与过拟合信号

训练跑起来后,控制台会滚动输出每轮的box_loss、cls_loss和mAP50。新手容易犯的错是只盯train loss:train loss降了不代表模型变好,它完全可能是在死记训练集。真正要盯的是验证集上的mAP50——这是模型对没见过的图的检测能力最直接的度量。

过拟合信号有两个:一是train_loss持续下降但val_loss在某个轮次后掉头向上,二是mAP50曲线爬到一个峰值后开始抖动下滑。这时候直接以mAP50最高的那个轮次为准,YOLO训练结束后会自动保存best.pt,用的就是验证集表现最好的权重,不用手动干预。

如果训练结束best.pt的mAP50在0.5以下,别急着调参,拿混淆矩阵看一下到底是哪一类拉胯——这正好接上第5章的第一个翻车现场。

5. 小数据集训练避坑:五个真实翻车现场与排查路径

240张数据量小,训练迭代快,试错成本低,但坑也不少。我按自己踩过的顺序,把最典型的五个坑列出来,每条都是“现象 → 原因 → 解决”的结构,新手可以对照排查。

5.1 烟雾半透明边界让标注框互相打架

现象:训练时loss正常下降,但mAP50始终在0.3~0.4之间波动,把预测框可视化出来发现烟雾框忽大忽小,同一团烟的框每次预测都不一样。

原因:烟雾没有清晰轮廓,标注框的标准在数据里不一致。有的框只圈住浓烟核心,有的框把淡淡烟带全包进去,模型在训练时反复看到同一种目标的两种答案,不知道该信哪个。

解决:统一标注规范,只框浓烟核心区域,稀疏烟带靠模型自己去泛化。同时把第2章审计脚本的类别统计跑一遍,找出哪些框的宽度或高度异常大——超过图像尺寸一半的框基本是标注员把整片区域圈进去了,用labelimg重新修一遍。这条不改,后面所有训练都建立在错误标准上。

5.2 类别不均衡:火焰样本太少,模型只认识正常画面

现象:验证集上正常图像几乎全对,火焰碰运气,烟雾大量漏检。看类别统计,火焰可能只有30个标注实例,烟雾也不到50个。

原因:240张图里多数类占大头,loss被多数类主导,少数类的检测头得不到足够梯度更新。

解决:三步走。第一步确认3.4的增强有没有把少数类至少翻倍;第二步在训练配置里给少数类加权重,yolo命令里用class_weights参数调整,让火焰的误检代价更高;第三步把验证集里少数类的标注单独可视化,确认不是标注漏标导致模型“没东西可学”。我见过的最极端案例是烟雾标注漏了三分之二,补标完mAP直接涨了0.2。

5.3 特征泄漏:训练集验证集来自同一场景,mAP虚高

现象:训练时mAP50一路飙到0.9,看着很兴奋;拿去测一段没见过的监控视频,立刻漏报一片,和验证集表现完全不像同一个模型。

原因:3.3里说的那种情况——随机划分导致同一监控源的不同帧同时出现在训练和验证里。模型见过几乎相同的像素,验证时等于在开卷考试。

解决:按拍摄源分组重划,整组隔离。如果文件名没有明显分组规律,用感知哈希把相似帧聚类,同一聚类整组划到一边。验证集的意义是模拟“没见过的场景”,宁可少几张图也要保证来源干净。

5.4 高光误报:把窗户反光、路灯、浅色墙面当火焰

现象:白天误报多,晚上还好。查看误报图片,全是白色的车窗反光、阳光直射的浅色墙面。

原因:火焰颜色集中在橙红区间,但高光区域在亮度通道上和火焰的亮斑很像。如果训练数据里缺少这类负样本,模型就只能靠颜色判断,没有任何形状和纹理约束。

解决:数据增强加大亮度扰动范围,让模型见过更多异常亮度的正常物体;同时专门采集一批“像火的不是火”的图片做负样本,不需要标注,直接放进数据集当背景。部署时再给框加一个色相约束——预测框内橙色像素占比例太低就丢弃,这一招能砍掉大半高光误报。

5.5 loss降了但mAP不涨:先查混淆矩阵,再找数据问题

现象:train loss从2.0降到0.5,mAP50却纹丝不动,像撞上一堵墙。调学习率、改batch都不管用。

原因:mAP是检测和分类的复合指标,loss下降只说明网络在往某个方向走,但可能走到了错误方向——比如把所有目标都预测成正常,或者把火焰和烟雾混为一类。调任何参数之前,先跑一次验证集推断,生成混淆矩阵看错在哪。

解决:用yolo val命令的plot=True参数导出混淆矩阵。如果火焰被大量预测成烟雾,通常是两类标注边界不清晰,回到5.1统一标准;如果大多数漏检集中在小目标,把imgsz从640提到960试试;如果矩阵显示正常图像也输出了一堆框,说明3.4的负样本不够,回去补负样本比调参有用。记住,小数据集上mAP不涨,大概率是数据问题,不是网络结构问题。

6. 让模型在真实监控里输出可用告警:置信度校准、帧间去抖与导出部署

模型训完,别急着上生产。先拿一段真实监控视频做端到端验证,再解决误报和漏报的最后一段距离。我的做法是用OpenCV写个几十行的推理脚本,把best.pt的权重读进来,一帧一帧过,实测真机表现。

import cv2 from ultralytics import YOLO model = YOLO("runs/detect/train/best.pt") cap = cv2.VideoCapture("demo.mp4") conf_thres = 0.35 # 置信度阈值,白天场景调高到0.5,夜间调低到0.25 while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model(frame, imgsz=640, conf=conf_thres, verbose=False)[0] for box in results.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) x1, y1, x2, y2 = map(int, box.xyxy[0]) if cls_id in (0, 1): # fire或smoke cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(frame, f"{results.names[cls_id]} {conf:.2f}", (x1, y1-8), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imshow("fire smoke demo", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

这段脚本的conf_thres是关键参数,它控制漏报和误报的平衡。240张数据训出来的模型,置信度普遍偏低,阈值设0.5很可能把真火漏掉;设0.2又会带来大量误报。我一般会拿10段真实视频各跑一遍,选一个在“漏掉最少”和“误报最多能接受”之间的值,这个过程没什么公式可套,就是拿真实数据试出来。

误报的第二道闸门是帧间去抖:单帧检测到火焰不告警,连续N帧都检测到才算告警。代码层面只需维护一个计数器,检测到框就加一,没检测到就清零,超过阈值再触发报警。我习惯设5帧去抖,也就是大约0.2秒的持续确认,能滤掉大部分因闪烁、镜头抖动产生的伪阳性。部署格式上,追求同等精度的轻量推理时优先导出ONNX再转TensorRT;如果只是做演示或小流量场景,直接用PyTorch权重跑也够。我现在拿到任何小数据集,第一件事都是先花半小时做审计和数据划分,把验证集按场景隔离,再聊训练——这个习惯让我少走了很多弯路。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询