
简介数据标注是计算机视觉模型落地的基础尤其对于摔倒检测这类特定行为识别任务数据集的丰富度和标注质量直接决定模型在天花板上的表现。目标检测与姿态估计技术需要高质量的标注数据支撑而公开数据集往往场景单一、标注粗糙难以满足真实部署需求。通过合理设计标签体系、选型标注工具、规范标注流程并完成格式转换与数据增强可以构建覆盖多样场景的专属数据集。这一方法论在老人独居看护、养老院智能监控、工厂高危区域监测等场景中具有重要价值。本文以摔倒检测数据集构建为例详细介绍从图片采集、工具配置、关键点标注到YOLO格式转换的完整流程并分享实操中遇到的典型问题与解决策略为相关工程实践提供可复用的参考。 最近一直在折腾一个摔倒检测项目数据这块是最大的拦路虎。市面上的公开数据集要么场景单一要么标注质量参差不齐拿来训模型很容易出现过拟合或漏检。磨了小一个月我终于整理出一套自己标注的摔倒数据集目前已完成5000多张图片的标注覆盖室内、室外、夜间、遮挡等多种场景。这篇就好好聊聊这个数据集到底怎么建的、用什么工具标、标注规范怎么定以及我在实操中踩过的那些坑希望对正在做同类项目的人有点帮助。老人独居场景的安全监控、养老院智能看护、工厂高危区域人员状态监测这些落地场景都离不开摔倒检测。而模型的精度上限很大程度取决于数据集的丰富度和标注质量。我这次的目标不是做一个“能跑”的demo而是尽量贴近真实部署需求让模型在复杂环境下也能稳定工作。1. 数据集整体设计与标注策略1.1 5000张图片的构成逻辑很多刚入门的朋友会觉得5000张不多甚至有人一上来就问“为什么不用COCO那种百万级数据集”。这里面有个关键区别跌倒检测是特定姿态和行为的识别不是通用物体识别。摔倒这个行为本身就比较少公开数据集里基本都是实验室模拟的动作僵硬、场景单一。真实场景中摔倒瞬间是动态的身体姿态变化大还会出现被家具遮挡、光线昏暗、拍摄角度刁钻等情况。所以我这个数据集的5000张图片来源分了三块一是公开数据集里能用的、场景质量较好的图片大概1200张二是自己搭的模拟场景拍摄涉及6类家具布局、3种光照条件拍摄了大约1800张三是从公开视频中抽帧结合人工复核筛选出来的有效帧约2000张。这样的构成让数据本身就有了差异性和层次感。从标签层面来看我采用了两套标签体系并行一套是检测框标签负责告诉模型“人在哪里、处于什么状态”另一套是姿态关键点标签负责标注人体17个关键点这样模型不仅能判断“人倒了”还能进一步分析“倒的姿态是什么样的”。两套体系配合使用比单独用哪一种都要稳。1.2 标签体系的详细定义先说说检测框这边我定义了四个类别fall摔倒状态下的人人倒地、身体大面积水平或接近水平stand站立或行走状态sit坐姿状态含坐在椅子上、床边、地上但上身挺直lie主动躺下比如床上休息、地面上躺着但非摔倒为什么单独把lie列出来因为在真实场景里老人躺在床上被误判为摔倒这个问题非常常见。把主动躺卧和摔倒分开标注模型才能学到“躺”和“摔”的边界。这个细节是我在实际测试时发现的一开始没区分模型在卧室场景的误报率直接爆表。姿态关键点部分我参考COCO的17点标注规范鼻、左眼、右眼、左耳、右耳头部5点左肩、右肩、左肘、右肘、左腕、右腕上肢6点左髋、右髋、左膝、右膝、左踝、右踝下肢6点这个17点方案在公开生态里用得多后续换成YOLOv8-pose或者MMPose都很方便不用重新标注。2. 标注工具选型与配置2.1 我的工具选型对比这行做久了手头试过的标注工具不少。我直接说结论这套数据集我用的是LabelMe加一个自研的辅助检查脚本中途也对比过其他工具各有利弊工具名称是否支持关键点输出格式适合场景短板LabelMe支持JSONCOCO风格单机小团队数据量中等多边形耗时需配脚本转换CVAT支持COCO/YOLO/TFRecord多人协同时效高部署稍重学习成本略高Makesense.ai仅检测框YOLO/VOC快速验证、简单分类不支持关键点标注X-AnyLabeling支持COCO/YOLO半自动辅助标注模型推理依赖本地算力LabelImg仅检测框VOC/YOLO入门练手过于基础无法做关键点我最终选了LabelMe不是因为它功能最强而是它足够简单直接JSON格式透明可控后期想怎么处理都行。CVAT也试了几天多人协同确实方便但数据量没大到必须那一步反而多了一层管理和维护成本。2.2 LabelMe的环境搭建要点LabelMe安装其实很简单pip一键装完但有几个细节值得说conda create -n labelme python3.9 conda activate labelme pip install labelme装完后建议设置一下配置文件把自动保存打开。路径在~/.labelmerc没有就手动创建{ auto_save: true, flags: null, labels: [fall, stand, sit, lie], store_flags: true }auto_save必须开否则标完一张忘保存想死的心都有。flags可以用来标记图片是否模糊、是否被遮挡、光线是否正常这个对后期的数据清洗很有价值。我在实际标注过程中给每张图都打了场景标签后面筛选子集做专项训练时非常方便。2.3 关键点模板配置LabelMe默认不区分关键点和普通点需要手动约定。我的做法是每个关键点用一个独立label命名格式统一为kp_左肩_kp_右肩这种。执行时LabelMe会把这些点也存进JSON结构里且每个点都带坐标后面写脚本只需要根据label name做映射就行了。实际操作的时候遇到一个坑LabelMe里创建点工具的交互逻辑是先切换到“Create Point”模式然后逐个点击。一旦点错位置只能删掉重画没法拖动。这个对效率影响挺大所以我的经验是先标注框把人的大致位置框出来再放大图片去点关键点。3. 数据采集与预处理3.1 模拟场景拍摄的关键技巧自己拍模拟场景最怕的就是拍出来“一眼假”模型训练完泛化能力差。我总结下来有三个方面需要逼真还原一是动作要覆盖“倒地过程”里的多个阶段不仅仅是结果状态。摔倒是一个快速过程有失去平衡、身体坠地、倒地后短暂静止等阶段。我在拍摄时要求志愿者模拟出这三个阶段的姿态再逐帧留存。这样模型学到的不是单一结果而是动态过程中的不同形态。二是视角要多样。很多数据集都是从平视角度拍的但真实监控摄像头一般都挂在墙角的高处俯视角为主。所以拍摄时有至少一半的素材是用高机位拍的模拟2.8米到3米的安装高度。三是地面的材质、家具的形状、光照的条件尽量多换。实木地板、瓷砖、地毯同样一个摔倒动作视觉特征差异非常大。我就是因为一开始只在一间屋子拍摄模型换到另一个环境后误检率直接翻倍。3.2 视频抽帧的要点视频抽帧有个容易忽视的问题连续帧之间太相似导致数据冗余。如果一帧一帧全保留模型会严重过拟合到某个固定场景。我的做法是抽帧后按“帧间相似度”去重用平均哈希算法过滤掉视觉上几乎一样的图片。import cv2 import os def dhash(image, hash_size8): gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) resized cv2.resize(gray, (hash_size 1, hash_size)) diff resized[:, 1:] resized[:, :-1] return sum([2 ** i for i, v in enumerate(diff.flatten()) if v]) def dedup_by_hash(folder, threshold5): seen {} for fname in os.listdir(folder): path os.path.join(folder, fname) img cv2.imread(path) if img is None: continue h dhash(img) if any(abs(h - s) threshold for s in seen): os.remove(path) else: seen[h] path这段脚本让数据集的多样性好了不少我实测下来处理后的数据里同一场景的有效范围从原来的500帧涨到了接近900帧的独立样本量。3.3 夜间与低光场景的处理低光数据是摔倒检测里必不可少的一环但很多人会忽略。一来公开数据集里这类素材少二来模拟拍摄成本高。我在拍摄时用了一个折中方案正常光线下拍然后做后期模拟低光。把亮度降到30%、50%再加上GPU渲染的噪点这样就能扩展出一批夜间样本。但注意这里有个技术陷阱模拟低光和真实夜间画面的分布特征并不完全一致。真实夜间监控会有红外反光、过曝、动态范围压缩等问题。所以我的数据集里仍然留出了一部分真实夜间拍摄的素材模拟数据只作为补充。我的原则是模拟的可以增加数量但绝不能替代真实场景。4. 标注实操全流程4.1 标注的第一步规范文档先行真正动手标之前我写了一份标注规范文档内容不多但必须写得足够细致什么算“fall”什么算“lie”配了4组对照示例图人被遮挡时遮挡面积小于50%时照常标注大于50%时标记为occluded跳过多个人同时出现在画面里时每个人都要单独标注关键点被遮挡时用(0, 0)占位并且把visibility标记为0框的边界取人体可视部分的紧致外接矩形不额外加边距这个文档的关键意义在于无论标注多久、由谁来标标准是一致的。千万不要让标注者自由发挥不然最后统计标签分布时会发现各种奇怪的边界模型效果根本没法保证。4.2 检测框标注实操开始标注时我一般先看整张图的缩略图快速判断哪些人需要标。然后切换到“Create Rectangle”工具先画框尽量贴合人体边缘然后从视角垂直的方向微调。LabelMe里画完矩形后可以用鼠标拖动边缘点进行调整。对于有遮挡的场景框一定要覆盖遮挡物后面的完整身体轮廓。比如人蹲在茶几后侧茶几只挡住腰部框还是要画到能包含全部人体不然模型会学到“人被截断”的错误特征。另外标注时我建议顺手在图片标题或flags里记录“self_occlusion”自身遮挡情况比如摔倒后腿被身体压住这对后续模型评估时的细粒度分析很关键。4.3 关键点标注实操与盲点关键点标注比框要费心思得多。我的顺序是先点躯干肩、髋再点四肢末端腕、踝最后点头部这样大骨架先定下来不容易跑偏。这里有一个容易被忽略的细节跌倒状态下有些关键点会消失在画面里比如身体侧躺时靠近地面一侧的肘、膝、踝都看不到了。这时候不要硬标LabelMe里把该点坐标设为(0, 0)或者直接跳过后面格式转换时再对应处理。还要特别说说难点在 “手” 和 “脚” 的位置低分辨率监控里往往是模糊的一团。我的经验是按“关节连接”的逻辑来推断比如手腕大概率在肘部下方脚踝大概率在膝盖下方先连成骨架再调整具体位置。如果实在看不出宁可跳过也不要瞎标瞎标的关键点对姿态估计的伤害比缺失更大。4.4 多人场景的标注顺序图像里有多个人的时候标注顺序很关键。如果先把所有人的框都画完再去点关键点很容易出现框和点的对应关系搞混。我的做法是先给编号1的人画完框并点完关键点再开始下一个人。虽然来回切换工具会多一点操作但能避免“点串了”的严重错误。LabelMe里每添加一个新的多边形或点JSON里的label name是独立的。一旦出现两个人都叫“fall”但关键点数据连错人的情况后续脚本解析时会很头疼排查一遍要花好几倍的时间。所以宁可多花五分钟也不给后面埋雷。5. 从LabelMe到YOLO格式转换的实战5.1 数据目录结构规划在标注之前我就先把目录结构规划好了这样后面转换格式省心很多data/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── annotations/ │ ├── json/ # LabelMe输出原始JSON │ ├── coco/ # 转成COCO格式之后 │ └── yolo/ # 转成YOLO格式之后通常我会按 8:1:1 划分训练集、验证集和测试集。划分时有个原则同一个拍摄视频中抽出的帧必须全部放到同一个集合里不能既在训练集又在验证集否则模型在验证集上的表现会有虚高。5.2 关键脚本JSON转YOLO格式LabelMe的JSON结构相对自由需要自己写脚本去解析。下面分享一段我实际在用的转换核心逻辑同时处理检测框和关键点import json import os import cv2 import numpy as np LABEL_MAP {fall: 0, stand: 1, sit: 2, lie: 3} KEYPOINT_NAMES [kp_nose, kp_left_eye, kp_right_eye, kp_left_ear, kp_right_ear, kp_left_shoulder, kp_right_shoulder, kp_left_elbow, kp_right_elbow, kp_left_wrist, kp_right_wrist, kp_left_hip, kp_right_hip, kp_left_knee, kp_right_knee, kp_left_ankle, kp_right_ankle] def convert_labelme_json(json_path, img_width, img_height): with open(json_path, r, encodingutf-8) as f: data json.load(f) shapes data[shapes] persons {} kp_groups {} for shape in shapes: label shape[label] points shape[points] if label in LABEL_MAP: # 矩形框 [x_min, y_min, x_max, y_max] xs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) persons[len(persons)] { label: label, bbox: [x_min, y_min, x_max, y_max], keypoints: {} } elif label.startswith(kp_): # 记录到一个临时组后续按最近距离匹配到人 kp_groups[label] [points[0][0], points[0][1], 2] # 将关键点分配到最近的检测框 for kp_name, kp_pos in kp_groups.items(): best_person_id None best_dist float(inf) for pid, person in persons.items(): bx_min, by_min, bx_max, by_max person[bbox] cx (bx_min bx_max) / 2 cy (by_min by_max) / 2 dist (kp_pos[0] - cx) ** 2 (kp_pos[1] - cy) ** 2 if dist best_dist: best_dist dist best_person_id pid if best_person_id is not None: persons[best_person_id][keypoints][kp_name] kp_pos yolo_lines [] for pid, person in persons.items(): label person[label] x_min, y_min, x_max, y_max person[bbox] # 归一化 cx (x_min x_max) / 2 / img_width cy (y_min y_max) / 2 / img_height w (x_max - x_min) / img_width h (y_max - y_min) / img_height parts [LABEL_MAP[label], f{cx:.6f}, f{cy:.6f}, f{w:.6f}, f{h:.6f}] for kp_name in KEYPOINT_NAMES: if kp_name in person[keypoints]: kx person[keypoints][kp_name][0] / img_width ky person[keypoints][kp_name][1] / img_height vis 2 else: kx 0.0 ky 0.0 vis 0 parts.extend([f{kx:.6f}, f{ky:.6f}, str(vis)]) yolo_lines.append( .join(parts)) return yolo_lines这段脚本的核心逻辑是先解析出所有fall、stand等检测框再解析出所有kp_开头的关键点最后通过最近距离算法把关键点匹配到最近的检测框上。这个匹配方案在单人场景下很准多人场景下也基本靠谱因为关键点和人的位置必然是近的。5.3 格式转换后的验证脚本转换不是跑完脚本就完了一定要二次验证。我用了一个超简单的可视化脚本把YOLO格式的坐标还原到原图上画框和关键点连成的骨架然后用眼睛快速浏览一遍。def visualize_yolo(img_path, label_path, class_names): img cv2.imread(img_path) h, w img.shape[:2] with open(label_path, r) as f: lines f.readlines() for line in lines: parts line.strip().split() cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:5]) x_min int((cx - bw / 2) * w) y_min int((cy - bh / 2) * h) x_max int((cx bw / 2) * w) y_max int((cy bh / 2) * h) cv2.rectangle(img, (x_min, y_min), (x_max, y_max), (0, 255, 0), 2) cv2.putText(img, class_names[cls_id], (x_min, y_min - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imshow(check, img) cv2.waitKey(0)验证这一步一定要做而且要抽样不同场景的图来看不要只看自己刚标注的那几张顺眼的。我的习惯是按总图片数的5%抽检确保覆盖低光、遮挡、多人这三种最难的情况。6. 常见问题与踩坑速查6.1 标注阶段最典型的5个问题问题表现原因与解法戴了口罩的人脸关键点乱标嘴部关键点漂移到口罩边缘观察不到就标(0,0)不要强求背景人群误标为fall路人蹲下捡东西被标为摔倒明确约束非水平姿态不算fall俯视角下关键点大量缺失肩和髋重叠看不到手脚不要硬标按visibility0处理连续帧标注不一致同一人姿态在相邻帧标签不同先按视频片段统一标注再拆分遮挡时框画小了模型学到的特征残缺画框只按“人体延伸方向”补全不看可见边界6.2 训练后遇到的数据问题模型训练完反馈回来的错误样本往往也指向数据问题。比如我发现模型对“坐姿后仰”的场景误检率很高回看数据集才发现坐姿后仰的样本被分散标注到了sit和fall两个类别里标注口径不一致。我最后做了统一只要臀部还接触支撑面就算sit哪怕上半身后仰超过45度只有整个身体的支撑点全部离开支撑面才算fall。另一个常见问题dark等低光图片模型老是漏检。后来查了标注发现夜间图片里框边缘紧贴人物轮廓但真实夜间监控的人体边缘带有光晕模型学到的边界太锐利和实际分布不匹配。后来我在标注时对夜间图统一往外扩了3到5个像素作为框边缘这个问题就缓解了不少。6.3 多人协作标注的同步技巧如果团队多人协同用LabelMe在各自电脑上标最头疼的是文件版本冲突。我的经验是采用“认领制”每个人负责固定编号范围的图片比如A组人员标0001-1000B组人员标1001-2000各干各的最后合并时按编号拼接。这样既不会出现同一个文件多人改来改去也能通过编号快速定位问题样本。合并JSON时要注意一个细节不同标注工具生成的JSON里imageData字段有时带了base64编码的原图体积会爆炸。我们合并前统一用脚本把这个字段清空只保留imagePath和shapes数据整体文件大小能缩到原来的十分之一。7. 数据集的后续扩展与迭代目前5000多张只是一个起点。数据集的价值在于它能持续滚动优化。我现在的做法是先用现有数据训练一个初版模型部署到测试环境里然后把模型漏检和误检的图片收集起来人工复核后增量补充到这个数据集里。这种“训练-收集-补标-重训”的闭环迭代是让模型持续变强的关键。另外一个方向是加入3D姿态信息的扩展。热词里我看到有人提到“3D点云标注”和“自动驾驶数据集”这两块和摔倒检测虽然关联不大但背后的逻辑是一致的多模态数据联合标注能显著提升模型在复杂环境下的鲁棒性。我计划后续对部分场景加入深度相机的拍摄数据让模型能利用深度信息更好地区分“摔倒”和“蹲下”。数据集建设这件事没有一劳永逸的终点。每个新场景、每个新摄像头角度、每种新光照条件都可能是原本数据集中不存在的新分布。把数据基建做好后面想调模型的时候才有底气。我这次整理的这套标注流程和工具链下一步也准备共享给一起做养老看护项目的小团队减少他们重复踩坑的时间。最后分享一个我个人的小习惯不管数据集规模多大每次标注完我都在笔记本里记一行——今天标了多少张、遇到哪些疑难案例、最后的判定依据是什么。这些看似琐碎的记录在训练效果不佳时回看往往比任何调参技巧都更能帮你找到问题根源。本文还有配套的精品资源点击获取