简介:面向计算机视觉目标检测入门与实战人群,这份抽烟检测数据集采用YOLO标注格式,可直接用于模型训练与验证。数据按训练集与测试集划分:训练集627张图片及对应txt标签,测试集156张图片及对应txt标签,并附带检测类别字典文件。为便于预览,提供可视化Python脚本,随机传入一张图片即可自动绘制边界框并保存结果,无需修改即可运行。压缩包共1569个文件,包含783张jpg图片、784个txt数据文件(图片标签与类别字典)、1个py脚本和1个png示例图,整体约31.94MB,体积轻量,适合快速部署与实验。已有106人学习下载。这份数据集省去自行标注的繁琐流程,压缩包内文件组织清晰,标签与图片一一对应,可直接接入YOLO系列模型,有助于快速验证网络结构或开展抽烟检测相关课题研究。
1. 抽烟检测数据集:为什么YOLO格式的txt文件是主流
抽烟检测是目标检测在安防、生产安全、控烟管理中最常见的落地场景之一,任务本质是从监控画面或手持设备图像里定位出正在吸烟的人,或者直接框出烟支和烟头。早期这类数据集多以VOC或COCO格式发布,但你现在搜索“抽烟检测数据集”,大概率下载回来的是YOLO格式的txt文件——每一个jpg旁边带一个同名txt,里面是归一化后的目标框信息。原因很简单:YOLO系列算法在工程实践里已经成了事实标准,从v5到v8,训练入口都直接吃这种标注,转换成本最低。与此同时,YOLO标注对坐标做了归一化,所以同一份txt可以在不同分辨率下无缝复用,这一点让数据集的分发变得特别轻量。不聊算法本身,就围绕这份txt文件,把格式细节、制作流程、校验方法和训练前该注意的坑完整讲一遍,适合刚接触目标检测的工程师,也能给正在整理自定义数据集的熟手提供一些可参照的脚本。
2. 剖析YOLO标注txt:归一化坐标与类别编号的规则
2.1 一行标注在txt里到底存了什么
一个标准的YOLO标注txt文件,每一行代表一个目标对象,一行有五个数字,用空格分隔:
0 0.520833 0.625000 0.080000 0.150000从左到右分别是:
0:类别ID,整数,从0开始。这个ID必须在训练配置里定义,例如data.yaml里的names列表。0.520833:目标中心点的x坐标,是相对于图片宽度的比例。0.625000:目标中心点的y坐标,相对于图片高度的比例。0.080000:目标框的宽度,相对于图片宽度的比例。0.150000:目标框的高度,相对于图片高度的比例。
注意,所有坐标都是浮点数,范围理论上在0到1之间,但由于标注工具或转换脚本的误差,偶尔会出现大于1或负数。训练本身能容忍一部分,但数据校验阶段最好把它们过滤掉。举一个实际例子:如果图片分辨率为1920×1080,目标框左上角为(960, 600),右下角为(1113, 762),那么中心点x等于(960+1113)/2=1036.5,除以1920得到0.5398;中心点y等于(600+762)/2=681,除以1080得到0.6306;宽度等于1113-960=153,除以1920得到0.0797;高度等于762-600=162,除以1080得到0.15。所以那一行是0 0.5398 0.6306 0.0797 0.1500。这个计算思路是理解一切转换脚本的基础。
2.2 从VOC和COCO标注转换到YOLO格式的通用代码
很多公开的抽烟检测数据集最初是VOC的XML格式,或者COCO的JSON格式。你需要把它们转成txt。下面这段Python脚本把VOC XML批量转换为YOLO txt,是常见的做法:
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_file, out_txt): tree = ET.parse(xml_file) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.findall('object'): cls_name = obj.find('name').text # 类别字符串转ID,这里省略映射表 cls_id = class_name_to_id[cls_name] bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) # 中心点坐标归一化 x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h # 裁剪到0-1之间,防止越界 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) box_w = min(max(box_w, 0.0), 1.0) box_h = min(max(box_h, 0.0), 1.0) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") with open(out_txt, 'w') as f: f.write('\n'.join(lines)) # 遍历目录 for xml in os.listdir('voc_annotations'): if xml.endswith('.xml'): voc_to_yolo(os.path.join('voc_annotations', xml), os.path.join('yolo_labels', xml.replace('.xml', '.txt')))代码中用到了root.find('size/width')这类路径查找,直接取XML里的原始尺寸。注意,class_name_to_id需要根据你的类别列表定义,例如{'smoking': 0, 'cigarette': 1}。如果某个目标框面积太小,小于图片面积的0.06%,或者宽高小于1像素,通常建议直接跳过,因为这些框对训练没有帮助,还可能让模型对噪声产生过拟合。
2.2.1 数据集目录的结构约定
YOLO训练时要求图片和标注放在两个平行目录下,例如:
dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamldata.yaml里写明路径和类别名,训练时框架会自动寻找同名txt。目录结构错乱是新手最常见的失败原因,所以先确认目录再检查内容。
2.3 转换后最容易踩的三个坑
先看一个表格,整理了我自己处理数据集时最常遇到的三个问题:
| 问题现象 | 原因 | 排查方法 |
|---|---|---|
| 训练时loss不降或明显偏低 | txt里的坐标没有归一化,变成了像素值 | 随机打开一个txt,如果坐标值大于1,就是像素坐标 |
| 检测框位置偏移,上下颠倒 | y坐标用的是左上角原点,但转换时除以了图片高度,算出的中心点和实际框对不上 | 用可视化脚本画框检查 |
| 类别错乱,比如把烟盒识别成猫 | 类别ID和names列表顺序不一致 | 对比data.yaml里的names和txt里的第一列类别索引 |
最常见的错误发生在坐标计算上:VOC的坐标系是左上角为原点,x向右、y向下,而YOLO的归一化坐标系也是同样的方向,所以方向本身不需要翻转。但有一些人参照旧博客会做很多无关的变换,比如把y翻转成1-y,就会导致框跑到图片下方。正常转换不需要翻转。
提示:拿到任何数据集,第一件事是随机挑10个txt,对照对应图片人工画框检查,而不是直接开始训练。
3. 从零构建抽烟检测数据集:采集、清洗与标注实践
3.1 图像采集:场景决定数据集质量
自制抽烟检测数据的第一个步骤是确定场景。同一颗烟头,在办公室暖光下和室外逆光下的视觉特征差异非常大,所以你需要先想清楚模型最终用在哪里。如果用在工厂车间,采集时就要覆盖不同厂服、不同头盔、不同工位的画面;如果用在楼宇控烟,则要包含走廊、楼梯间、洗手间等背景。
常见做法是用摄像头连续抽帧,生成视频帧后再做相似度去重。下面这段脚本基于感知哈希对图像做近似去重,避免大量重复画面撑爆标注工作量:
import os import hashlib from PIL import Image def dhash(img, hash_size=8): gray = img.convert('L').resize((hash_size + 1, hash_size)) diff = [] for row in range(hash_size): for col in range(hash_size): left = gray.getpixel((col, row)) right = gray.getpixel((col + 1, row)) diff.append(left > right) return hashlib.md5(str(diff).encode()).hexdigest() seen = set() for fname in sorted(os.listdir('frames')): path = os.path.join('frames', fname) h = dhash(Image.open(path)) if h not in seen: seen.add(h) os.rename(path, os.path.join('deduped', fname))这里使用了感知哈希,但相似画面对应的哈希值可能不完全一样,所以更严谨的做法是用汉明距离小于5来判断。上面的代码简化了去重逻辑,实际生产中建议用imagededup这类现成库,直接用CNN特征做相似度过滤。去重之后,需要人工剔除模糊、过曝、暗光下完全看不清目标的图像,宁可少也不能用脏数据。
3.2 标注工具与标注规范的制定
图像准备好之后,用LabelImg这类图形化工具打开,只需要把标注模式设为YOLO,保存时就会直接产生txt文件而不是XML。标注工具不是重点,重点是人人都遵守的规范。对于抽烟检测,常见的标注对象有两种:第一种框整个正在吸烟的人,含手和烟,适合做人员行为识别,模型学习的是人手持烟的状态;第二种只框香烟或烟头,适合做细节检测,难度更高,因为目标小。如果你的业务需要判断是否在抽烟,个人建议选择第一种,因为上下文信息更丰富,模型不容易被雪茄、电子烟这些相似物体带偏。但如果你还要做烟头遗留检测,那第二种也需要单独标注。一个折中方案是同时标注人和烟,用两个类别,让模型输出两个框,后处理再判断关联。
标注时要固定几条规则并写进团队文档:
- 遮挡超过50%的目标不标。
- 被身体完全挡住但烟头露出的,只标烟头。
- 图片中人小于24×24像素时不标,因为即使标了也很难学习到有效特征。
这些规则直接影响训练效果。不同人标注同一张图,框的位置偏差如果超过10个像素,模型学到的东西就会很拧巴。
3.3 划分训练集时的分类采样
数据划分不能简单用random函数,因为监控视频的连续帧高度相关,同一个人的不同帧算同一个场景深拷贝。如果随机混入train和val,验证集指标会虚高。正确做法是把同一个视频来源或同一场景的图片归为一个分组,按组划分。
这里给一个示例脚本,按文件名前缀分组划分:
import os import random from collections import defaultdict groups = defaultdict(list) for f in os.listdir('images'): prefix = f.split('_')[0] # 比如 camera1_0001.jpg -> camera1 groups[prefix].append(f) group_names = list(groups.keys()) random.shuffle(group_names) train_groups = group_names[:int(len(group_names) * 0.7)] val_groups = group_names[int(len(group_names) * 0.7):int(len(group_names) * 0.85)] test_groups = group_names[int(len(group_names) * 0.85):] train_files = [] for g in train_groups: train_files.extend(groups[g])最终划分比例建议:训练集70%、验证集15%、测试集15%。测试集一定不能参与调参和早停,只用来做最终评估。标注时建议把不同场景分开存目录,方便后续做增量训练和错误分析。表格化一个划分记录:
| 集合 | 占比 | 用途 | 参与调参 |
|---|---|---|---|
| train | 70% | 梯度更新 | 是 |
| val | 15% | 早停、选超参 | 是 |
| test | 15% | 最终模型评估 | 否 |
对整个任务来说,标注数据是模型的上限,模型只是去逼近这个上限。所以花在规范制定上的时间,远比花在调参上的时间有价值。
4. 用脚本校验数据集:漏标、越界与类别不平衡的排查
4.1 图像与标注文件的一一对应检查
拿到或者构建好数据集后,第一步要做完整性检查。常见现象是图像存在但txt缺失,或者txt存在但图像被误删。下面脚本快速扫描出有图像无标注、有标注无图像的文件:
import os img_dir = 'images' label_dir = 'labels' img_names = set(os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith('.jpg')) label_names = set(os.path.splitext(f)[0] for f in os.listdir(label_dir) if f.endswith('.txt')) missing_label = img_names - label_names missing_image = label_names - img_names print('missing label:', len(missing_label)) print('missing image:', len(missing_image))注意,YOLO训练时文件命名要求图片和txt同名,并且放在对应目录下,比如images/train/xxx.jpg和labels/train/xxx.txt。如果数据集来源混杂,还有可能大小写不一致,比如JPG与jpg。在Linux上,大小写敏感,必须先统一扩展名。
4.2 越界和目标尺寸异常的检测
YOLO标注坐标应该都在[0,1]范围内,但转换脚本或手动修正时会产生越界值。下面这段代码统计每张图中的越界框和异常尺寸:
import os labels_dir = 'labels' bad = [] too_small = [] for fn in os.listdir(labels_dir): path = os.path.join(labels_dir, fn) with open(path) as f: for line in f: parts = line.strip().split() if len(parts) != 5: bad.append((fn, 'format')) continue _, cx, cy, w, h = parts cx, cy, w, h = map(float, (cx, cy, w, h)) if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 <= w <= 1 and 0 <= h <= 1): bad.append((fn, 'out_of_range')) if w < 0.001 or h < 0.001: too_small.append((fn, w, h)) print('bad:', len(bad)) print('too small:', len(too_small))这里的阈值0.001是经验值,对应一张1920×1080图片就是大约2×2像素,比这更小的框没有实际意义。批量打印这些异常文件后,需要回到标注工具里手动修正,而不是在脚本里强行裁剪掉。强行裁剪会把框变成图片边缘的残废框,同样影响训练。
4.3 类别不平衡的统计与处理思路
抽烟检测数据集的另一个典型问题是类别不平衡,比如正常行为的背景图数量远大于抽烟样本,或者标注了人的框比标注烟的框多出10倍。模型会倾向于把目标预测为数量多的类别,导致召回率偏低。
用下面的脚本统计每个类别出现的bbox数量:
import os from collections import Counter labels_dir = 'labels' class_counter = Counter() for fn in os.listdir(labels_dir): with open(os.path.join(labels_dir, fn)) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls_id = int(parts[0]) class_counter[cls_id] += 1 print(class_counter)如果类别严重不平衡,优先考虑这几条路:
- 对样本量小的类别做更多的图像增强,尤其是水平翻转、小角度旋转、亮度变化。
- 在损失函数上加类别权重,简单做法是在YOLOv5的
data.yaml里增加weight键,或者直接调整每一个类的损失系数。 - 采用过采样,让每次epoch里小类别样本被反复看到,同时配合多尺度训练避免过拟合。
4.4 可视化抽查:不要相信统计,要亲眼看
统计只能发现数值异常,无法发现框形状不对这类语义错误。所以必须写一个可视化脚本,把标注框画到原图上,按固定间隔抽取样本人工查看:
import cv2 img_path = 'images/train/cam1_0001.jpg' label_path = 'labels/train/cam1_0001.txt' img = cv2.imread(img_path) h, w = img.shape[:2] with open(label_path) as f: for line in f: cls_id, cx, cy, bw, bh = map(float, line.split()) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite('check.jpg', img)这段代码通过反归一化把txt坐标还原成像素坐标,然后画框。注意cv2.rectangle要求坐标是整数,且必须保证x2大于x1、y2大于y1。如果画出来的框明显偏离烟头或者包裹了半个场景,那就要回到标注阶段修正,而不是指望模型自己去学。
5. 针对抽烟检测的数据增强与模型调优技巧
5.1 数据增强参数怎么设
YOLOv8和YOLOv5都内置了增强策略,但针对抽烟检测,有几个参数值得手动调整:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| hsv_h | 0.015 | 色相偏移,保留烟嘴和烟支的固有颜色线索 |
| hsv_s | 0.7 | 饱和度偏移,模拟不同光源 |
| degrees | 5 | 旋转角度,过大容易引入无效背景 |
| scale | 0.5 | 缩放,配合多尺度增强小目标泛化 |
用YOLOv8命令行训练,把这些参数直接传进去:
yolo train data=smoking.yaml model=yolov8n.pt epochs=100 imgsz=1280 patience=15 hsv_h=0.015 hsv_s=0.7 degrees=5 scale=0.5其中imgsz=1280是关键。烟头在原始监控画面里往往只有几十像素,使用1280输入分辨率比默认640能显著提高小目标召回率,但训练时间也会更久。如果显存有限,可以改用yolov8s并开启rect=True减少无效填充。
5.2 用难例挖掘补充数据集
这是一个常被忽略的细节。训练第一版模型之后,把验证集上预测失败,包括漏检或误检的图像全部挑出来,人工标注后并入训练集,再训练第二轮。这种做法比一次性盲目扩充一万张随机图像高效得多,因为抽烟检测往往卡在特定姿势或光线上,难例挖掘是在用模型自己的弱点指导数据采集。
yolo detect predict model=runs/detect/train/weights/best.pt source=unlabeled_images save_txt=True对预测结果做筛选,凡是标注文件为空但真实图像里有人物的,就该考虑是否为漏标。注意不要用模型预测结果直接作为标注,只能作为候选难例。
5.3 类别权重的设置方法
训练完第一轮后,用验证集看各类别的precision和recall。如果烟头类别recall明显低于人员类别,可以在训练配置里为烟头类别提高损失权重。YOLOv8的data.yaml里names列表的顺序必须和txt中的类别ID一致,同时可以通过model.loss模块修改权重,但更简单的做法是先增加该类样本的过采样倍数。权重数值不要超过2,否则会导致框位置偏移或重复检测。
最后补充一个通用技巧:把val和test数据按照不同时间段、不同摄像头分开,不要混在一个文件夹里。这样最终评估的mAP才是真实部署性能的近似。
本文还有配套的精品资源,点击获取