简介:这份吸烟、跌倒行为目标检测数据集面向计算机视觉初学者与行为识别方向的开发者,采用YOLO标注格式,可直接用于YOLOv5、YOLOv7、YOLOv8等主流框架的训练与验证,帮助解决公共场所异常行为监测中样本获取困难的问题。资源包共2000个文件,以txt标注文件为主,另含1个yaml配置文件,压缩包约98.47MB,目录按trainset与testset划分,各自包含images与labels子目录,并附带smoke_fall.yaml用于定义类别与数据路径,结构规范、开箱即用。目前已有417人学习下载。数据集覆盖吸烟与跌倒两类目标,标注文件命名清晰,便于快速划分训练集与测试集,读者可据此完成模型训练、精度评估与推理部署,也可结合注意力机制改进、小目标检测层等技巧做进一步优化,适合作为行为检测课程设计、毕业设计或算法验证的实用数据基础。
1. 吸烟、跌倒行为目标检测数据集:为什么你的模型总在真实场景里翻车
做过行为识别落地的工程师大概都有过这种体验:在公开数据集上 mAP 跑到 0.85,信心满满部署到实际场景,结果吸烟检测把叼牙签的、打电话的、甚至手托下巴的全报成 positive,跌倒检测更离谱,蹲下系鞋带能触发告警,真正躺地上不动的人反而漏检。问题往往不在模型结构,而在数据集本身——你喂给网络的标注框,和真实场景里目标的姿态、遮挡、尺度分布根本对不上。
吸烟、跌倒行为目标检测数据集(YOLO格式)这个方向,解决的就是这类「行为语义 + 目标检测」交叉场景的数据供给问题。它面向的是需要做室内安全监控、养老看护、禁烟区域管理的算法工程师和产品落地团队。核心诉求很直接:用 YOLO 系列检测器,把「人」和「人的行为状态」绑定在一个可训练的标注体系里,让模型输出的不只是 person 框,而是 smoking 框和 fall 框。这篇文章不讲空泛的综述,只讲我实际搭这套数据管线时踩过的坑、参数怎么定、标注怎么统一、增强怎么做才不会把行为语义破坏掉。
2. 行为类数据集和通用检测数据集到底差在哪:标注体系与选型理由
2.1 行为框和人体框的绑定关系决定了标注策略
通用 COCO 那套 person 框标注,框的是整个人体。但吸烟行为的核心视觉线索在头部和手部区域——烟支、夹烟手势、烟雾。如果你直接拿 person 框去训 smoking 类,模型学到的是「人 = 吸烟」,精度必然崩。常见做法是双框体系:一个 person 框标全身,一个 smoking 框标头部到手部的局部区域,两个框通过 IoU 或中心点归属建立关联。
跌倒行为又不一样。跌倒的视觉定义是「人体从直立状态转为水平状态且持续一定时间」,单帧图像里它和「躺下」「蹲下」「弯腰」的边界非常模糊。所以跌倒类标注不能只标一个框,还要在标注文件里附带姿态标签或关键点信息。YOLO 格式本身只支持class x_center y_center width height,要承载这些附加信息,通常有两种做法:一是把姿态编码进类别名,比如fall_lying、fall_sitting、fall_standing分成子类;二是外挂一个 JSON 元数据文件,用图像文件名做 key 关联。
我一般会选第一种,因为 YOLO 训练管线对类别名是透明的,不需要改 dataloader。代价是类别数膨胀,后面会讲怎么控制。
2.2 为什么选 YOLO 格式而不是 COCO 或 VOC
YOLO 格式的 txt 标注文件,每行class_id x_center y_center w h,坐标全部归一化到 0-1。它的优势在行为检测场景里特别明显:
- 解析成本极低。行为数据集往往需要频繁做子集切分、类别重映射、在线增强,txt 格式用 Python 原生读写就能改,不需要引入 pycocotools 那套重依赖。
- 和主流 YOLO 训练框架(ultralytics 系、YOLOX、PP-YOLOE)天然兼容,
data.yaml里写几行路径就能开训。 - 归一化坐标让图像 resize 后的标注变换变成纯乘法,增强管线里不容易出坐标越界 bug。
但 YOLO 格式也有硬伤:它不存储图像尺寸、不存储分割掩码、不存储关键点。所以如果你的跌倒检测需要骨架信息辅助,得额外维护一份关键点标注,或者干脆走 RTMPose + 检测框的两阶段方案。
2.3 类别体系设计:3 类还是 7 类
我见过两种极端。一种是极简派:person、smoking、fall三类,标注快,但模型分不清「坐着抽烟」和「站着抽烟」,也分不清「跌倒后躺着」和「本来就躺着」。另一种是极细派:把吸烟分成smoking_left_hand、smoking_right_hand、smoking_mouth,跌倒分成fall_forward、fall_backward、fall_sideways,类别数冲到十几类,结果每类样本不足 500,训练直接过拟合。
我的经验值是 5 到 7 类比较稳:
| 类别名 | 标注对象 | 典型样本量建议 |
|---|---|---|
| person | 完整人体框 | 全量 |
| smoking | 头部至手部局部框 | ≥ 3000 |
| fall | 跌倒姿态人体框 | ≥ 2000 |
| lying | 非跌倒的躺卧 | ≥ 1500 |
| sitting | 坐姿(用于负样本对比) | ≥ 2000 |
| bending | 弯腰(跌倒易混类) | ≥ 1500 |
把lying、sitting、bending显式列出来当负样本类,是降低跌倒误报最有效的手段。模型见过足够多的「像跌倒但不是跌倒」的样本,才不会把蹲下系鞋带报成 fall。
3. 从零构建 YOLO 格式行为数据集:目录结构、标注转换与增强参数
3.1 目录结构和 data.yaml 的最小可用配置
行为数据集比通用检测数据集更需要严格的目录约定,因为后面要做类别平衡采样和时序切分。我常用的结构是这样:
behavior_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── meta/ │ ├── smoking_meta.json │ └── fall_meta.json └── data.yamldata.yaml的写法:
# 行为检测数据集配置 path: /data/behavior_dataset train: images/train val: images/val test: images/test # 类别数必须和 names 长度严格一致 nc: 6 names: 0: person 1: smoking 2: fall 3: lying 4: sitting 5: bending这里有个容易翻车的点:nc和names长度不一致时,ultralytics 不会报错,而是静默按nc截断,导致最后几类永远训不到。我一般会在训练前跑一段校验脚本,把nc和len(names)断言一遍。
3.2 从 VOC/COCO 标注转 YOLO 格式的转换脚本
很多行为数据集原始标注是 VOC XML 或 COCO JSON,转 YOLO 时坐标归一化最容易出错。下面这个脚本处理 VOC 转 YOLO,重点在边界裁剪和类别映射:
import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image # 原始类别名到 YOLO 类别 id 的映射,必须和 data.yaml 一致 CLASS_MAP = { "person": 0, "smoke": 1, # 原始标注里可能叫 smoke "smoking": 1, "fall_down": 2, # 原始标注里可能叫 fall_down "fall": 2, "lie": 3, "lying": 3, "sit": 4, "sitting": 4, "bend": 5, "bending": 5, } def voc_to_yolo(xml_path, img_dir, out_label_dir): tree = ET.parse(xml_path) root = tree.getroot() img_name = root.find("filename").text img_path = Path(img_dir) / img_name if not img_path.exists(): return False w, h = Image.open(img_path).size lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text.strip().lower() if cls_name not in CLASS_MAP: continue # 未映射类别直接丢弃,避免污染训练 cls_id = CLASS_MAP[cls_name] bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 关键:裁剪到图像边界内,VOC 标注经常有越界框 xmin = max(0.0, min(xmin, w - 1)) ymin = max(0.0, min(ymin, h - 1)) xmax = max(0.0, min(xmax, w - 1)) ymax = max(0.0, min(ymax, h - 1)) if xmax <= xmin or ymax <= ymin: continue # 裁剪后无效的框丢弃 x_center = (xmin + xmax) / 2.0 / w y_center = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}") out_path = Path(out_label_dir) / (Path(xml_path).stem + ".txt") out_path.write_text("\n".join(lines)) return True逻辑说明:VOC 的bndbox是绝对像素坐标,YOLO 要的是归一化中心点加宽高。转换时最容易忽略的是越界框——VOC 标注里xmax超过图像宽度的比例不低,不裁剪的话归一化后坐标大于 1,训练时会被框架静默截断或产生异常梯度。类别映射表里保留多个别名,是因为不同标注批次的人可能用不同词,统一映射能省掉大量人工清洗。
参数说明:CLASS_MAP必须和data.yaml的names严格对应,建议把映射表单独抽成一个 JSON 文件,转换脚本和训练配置共用同一份。:.6f保留六位小数,YOLO 官方推荐精度,再高没必要,再低小目标框会漂。
3.3 行为数据增强:哪些增强能用,哪些会把语义毁掉
通用检测的增强策略直接搬到行为数据集上,很容易把行为语义破坏掉。下面这张表是我实际验证过的:
| 增强方式 | 是否可用 | 原因 |
|---|---|---|
| 水平翻转 | 可用 | 吸烟左右手互换不影响语义 |
| 垂直翻转 | 禁用 | 跌倒翻转后变成倒立,语义完全错 |
| 随机旋转 ±15° | 谨慎 | 超过 15° 跌倒姿态失真 |
| Mosaic | 可用 | 但要注意 smoking 小框拼接后可能被裁掉 |
| MixUp | 谨慎 | 两个半透明人体叠加,行为线索互相干扰 |
| HSV 色调扰动 | 可用 | 烟雾颜色变化,增强泛化 |
| 随机遮挡 | 可用 | 模拟遮挡场景,但遮挡比例不超过 30% |
垂直翻转是行为检测里最隐蔽的坑。跌倒类样本垂直翻转后,人体变成头下脚上,模型学到的「水平人体 = 跌倒」特征被破坏,但标注框位置没变,等于在教模型学错的东西。我一般会在data.yaml同级放一个augment.yaml,把flipud显式设为 0.0。
Mosaic 增强对 smoking 类要特别小心。smoking 框通常只占图像 5% 到 10% 面积,Mosaic 四图拼接后,如果随机裁剪中心落在 smoking 框上,这个框可能被裁掉一半甚至完全消失,但标签文件里还留着,变成脏标注。常见做法是 Mosaic 之后加一步标注有效性校验,把裁剪后面积小于原面积 30% 的框丢弃。
4. 训练参数怎么设:从 anchor 到 loss 权重的行为检测调参清单
4.1 anchor 尺寸要按行为框分布重新聚类
YOLO 默认 anchor 是在 COCO 上聚类的,COCO 的 person 框偏大,smoking 这种小框的 recall 会很低。行为数据集必须重新跑 k-means 聚类。用 ultralytics 自带的工具:
# 对训练集标注跑 anchor 聚类,输入是 labels/train 目录 yolo detect train data=data.yaml model=yolov8n.pt epochs=1 \ kmeans=9 kmeans_path=labels/train如果框架不带这个功能,可以自己写:
import numpy as np from pathlib import Path def load_boxes(label_dir): boxes = [] for txt in Path(label_dir).glob("*.txt"): for line in txt.read_text().strip().split("\n"): if not line: continue parts = line.split() w, h = float(parts[3]), float(parts[4]) if w > 0 and h > 0: boxes.append([w, h]) return np.array(boxes) def iou(box, clusters): # box 和 clusters 都是 (w, h),用 1-IOU 作为距离 x = np.minimum(box[0], clusters[:, 0]) y = np.minimum(box[1], clusters[:, 1]) inter = x * y union = box[0] * box[1] + clusters[:, 0] * clusters[:, 1] - inter return inter / union def kmeans_anchors(boxes, k=9, iters=300): np.random.seed(42) clusters = boxes[np.random.choice(len(boxes), k, replace=False)] for _ in range(iters): distances = 1 - np.array([iou(b, clusters) for b in boxes]) nearest = distances.argmin(axis=1) for i in range(k): if (nearest == i).any(): clusters[i] = boxes[nearest == i].mean(axis=0) return clusters[np.argsort(clusters[:, 0] * clusters[:, 1])] boxes = load_boxes("labels/train") anchors = kmeans_anchors(boxes, k=9) print("聚类 anchor (w, h):") for a in anchors: print(f" {a[0]:.4f} {a[1]:.4f}")逻辑说明:行为框的宽高比分布和通用目标差异很大。smoking 框接近正方形但很小,fall 框是宽扁形。聚类出来的 anchor 里应该能看到明显的小尺寸簇和宽扁簇。如果聚类结果和默认 anchor 差不多,说明你的数据分布其实和 COCO 接近,那用默认的也行。
参数说明:k=9对应 YOLOv5/v8 的三尺度各三个 anchor。iters=300足够收敛,再多收益很小。np.random.seed(42)固定随机种子,保证每次聚类结果可复现。
4.2 分类 loss 权重和类别不平衡处理
行为数据集天然类别不平衡:person 框数量通常是 smoking 的 5 到 10 倍,fall 更少。直接训会让模型偏向多数类。ultralytics 框架里可以通过cls权重和focal_loss调节:
# 训练超参配置片段 lr0: 0.01 lrf: 0.01 momentum: 0.937 weight_decay: 0.0005 box: 7.5 # box loss 权重,行为检测建议略降 cls: 1.2 # cls loss 权重,类别不平衡时上调 dfl: 1.5 fl_gamma: 1.5 # focal loss gamma,缓解简单样本主导cls从默认 0.5 提到 1.2,是让分类分支在总 loss 里占比更高,逼模型认真区分 smoking 和 person。fl_gamma=1.5是 focal loss 的聚焦参数,大于 1 时对易分类样本降权,适合负样本(sitting、bending)远多于正样本的场景。但 gamma 不要超过 2.0,否则训练初期 loss 震荡剧烈,收敛变慢。
另一个手段是过采样。把 smoking 和 fall 的标注文件在训练集里复制多份,图像路径不变,只增加采样概率。我一般把稀有类过采样到和 person 类 1:2 的比例,再高容易过拟合。
4.3 验证指标怎么看:mAP 之外必须盯的三个数
行为检测只看 mAP50 会骗自己。我固定会看这三个:
- 每类 recall:smoking 的 recall 低于 0.6 时,说明小框漏检严重,要回去查 anchor 和输入分辨率。
- fall 类的 FP 率:跌倒误报是产品最不能忍的,FP 率高于 5% 就要加负样本。
- 混淆矩阵里 fall 和 lying 的交叉量:这两个类混淆是常态,交叉量超过 fall 样本的 20% 就要考虑合并或加关键点辅助。
验证时用conf=0.25和conf=0.5各跑一遍,看 recall 掉多少。掉超过 15% 说明模型置信度分布不好,需要调 NMS 的 IoU 阈值或加训练数据。
5. 避坑与排查:行为数据集标注和训练里最常见的 5 个翻车现场
5.1 现象:smoking 类 mAP 虚高但实际漏检严重
原因:标注时 smoking 框画得太大,把整个上半身都框进去了。模型学到的是「上半身 = smoking」,验证集里只要上半身完整就报 positive,但实际场景里手部被遮挡时反而漏检。
解决:重新定标注规范,smoking 框的边界定义为「烟支可见部分 + 夹烟手部 + 嘴部区域」,框面积不超过 person 框的 25%。标注完成后跑一遍面积分布统计,超过 30% 的框全部返工。
5.2 现象:跌倒检测在夜间红外图像上几乎全漏
原因:训练集全是可见光图像,模型学到的跌倒特征依赖颜色和纹理,红外图像里这些线索全没了。
解决:训练时加 HSV 扰动和灰度化增强,让模型对颜色不敏感。更彻底的做法是混入至少 20% 的红外或低照度样本。如果拿不到红外数据,用随机灰度化加对比度扰动模拟,但效果有限,只能救急。
5.3 现象:训练 loss 正常下降但验证 mAP 卡在 0.3 不动
原因:标注文件里有大量空 txt 或只有 person 框的图像,模型在验证时被这些简单样本拉高了 loss 均值,掩盖了稀有类的糟糕表现。
解决:训练前统计每个类别的框数量,把只有 person 框的图像单独抽出来做负样本集,不要混在正常训练集里。验证集必须保证每个类别至少有 100 个实例,否则 mAP 波动极大。
5.4 现象:Mosaic 增强后 smoking 框坐标越界
原因:Mosaic 拼接时图像被 resize 和 pad,标注坐标没有同步更新,或者更新时用了错误的缩放比例。
解决:不要自己手写 Mosaic,用框架自带的。如果必须自己实现,在拼接完成后对每个框做一次边界校验,x_center ± w/2超出 [0,1] 的直接丢弃。我一般会在 dataloader 里加一个 assert,训练时一旦发现越界框就打印图像名,方便回溯。
5.5 现象:模型把「蹲下捡东西」大量报成 fall
原因:训练集里 fall 类样本的多样性不足,大部分是向后倒,模型没学过前倾、侧倒、蹲下后失衡这些变体。同时 bending 负样本太少。
解决:fall 类至少覆盖前倒、后倒、侧倒、滑倒四种姿态,每种不少于 300 样本。bending 类要包含蹲下、弯腰、单膝跪地等子姿态。如果数据不够,用姿态合成工具生成一部分,但合成样本比例不要超过真实样本的 30%。
6. 进阶技巧:用时序投票把单帧跌倒检测的误报压下去
单帧检测做跌倒,误报率很难压到产品可接受的水平,因为「躺下」和「跌倒后躺着」在单帧里几乎无法区分。我实际项目里最后都会加一层轻量时序投票,不需要额外训练模型,纯后处理就能把 FP 率降一半以上。
思路很简单:对同一路视频流,维护一个长度为 N 的滑动窗口,记录每帧检测到的 fall 框。只有当连续 M 帧都出现 fall 且框的 IoU 与前一帧大于阈值时,才触发告警。参数用下面这段代码调:
from collections import deque class FallTracker: def __init__(self, window=15, trigger=8, iou_thr=0.3): # window: 滑动窗口帧数,对应约 0.5 秒(30fps) # trigger: 触发告警所需连续命中帧数 # iou_thr: 相邻帧 fall 框的 IoU 下限,防止不同人误关联 self.window = window self.trigger = trigger self.iou_thr = iou_thr self.history = deque(maxlen=window) def _iou(self, box_a, box_b): x1 = max(box_a[0], box_b[0]) y1 = max(box_a[1], box_b[1]) x2 = min(box_a[2], box_b[2]) y2 = min(box_a[3], box_b[3]) inter = max(0, x2 - x1) * max(0, y2 - y1) area_a = (box_a[2] - box_a[0]) * (box_a[3] - box_a[1]) area_b = (box_b[2] - box_b[0]) * (box_b[3] - box_b[1]) union = area_a + area_b - inter return inter / union if union > 0 else 0 def update(self, fall_boxes): # fall_boxes: 当前帧所有 fall 框,格式 [x1, y1, x2, y2] matched = False if self.history and fall_boxes: last = self.history[-1] for fb in fall_boxes: for lb in last: if self._iou(fb, lb) > self.iou_thr: matched = True break if matched: break self.history.append(fall_boxes if matched or not self.history else []) # 统计最近 window 帧里连续命中的最大长度 max_consec = 0 cur = 0 for frame_boxes in self.history: if frame_boxes: cur += 1 max_consec = max(max_consec, cur) else: cur = 0 return max_consec >= self.trigger逻辑说明:history存每帧的 fall 框列表,update先判断当前帧的框和上一帧是否有 IoU 匹配,有匹配才计入历史,否则视为新目标重新计数。最后统计连续命中帧数,达到trigger才返回 True。这样单帧的偶发误检会被窗口过滤掉,而真实跌倒因为持续多帧,能稳定触发。
参数说明:window=15在 30fps 下约 0.5 秒,太短过滤效果差,太长告警延迟高。trigger=8意味着需要约 0.27 秒的持续检测,实测能过滤掉大部分蹲下起立的瞬时误报。iou_thr=0.3是经验值,低于 0.2 容易把不同人的框关联到一起,高于 0.5 则真实跌倒时人体快速位移会导致匹配失败。
这套后处理不需要重新训练,直接在推理管线里加一层,代价是引入约 0.3 秒的告警延迟。对养老看护场景,这个延迟完全可以接受,换来的是误报率从每几小时一次降到每天一两次。
我自己的习惯是,任何行为检测模型上线前,先用一段包含 50 次真实跌倒和 200 次易混动作的测试视频跑一遍,把 FP 和 FN 逐条标出来看。十次里有八次,问题不在模型,而在标注规范没统一或者负样本不够。数据集这件事,偷懒省下的时间,最后都会在排错时加倍还回来。希望帮到你。
本文还有配套的精品资源,点击获取