动作识别系列更新到第12篇,前面几篇把目标检测、目标跟踪这些前置基础讲得七七八八了,这期开始正式进入 yolo26s-pose + PoseC3D 的组合方案。按照惯例,子系列的第一篇不讲模型结构,先把数据管线的源头——标注工具——从1.0升级到2.0这件事捋清楚。
这套方案里,yolo26s-pose 负责从视频帧里把人体关键点稳定地检测出来,PoseC3D 负责把这些关键点组成的骨架序列识别成具体动作类型。两者配合需要大量“帧到帧”的关键点标注数据,标注结果的质量直接决定 PoseC3D 训练时能不能收敛、准确率能到多高。1.0 版本的标注工具只能画矩形框,对动作识别来说远远不够用,所以必须升级。
如果你现在也在做动作识别,或者正准备整理自己的姿态数据集,这篇文章应该能帮你省不少重复造轮子的时间。下面内容涉及我升级标注工具时的功能设计、数据格式选型、操作流程,以及最终怎么导出 PoseC3D 能直接吃掉的训练数据,全程按实操记录来讲。
1. 为什么动作识别项目必须先升级标注工具
1.1 从“框住人”到“盯住关键点”的跨越
以前做检测任务,标注一个行人只要给一个矩形框就够了。但动作识别要的不是“人在哪”,而是“人的动作是什么”。同样一个“举手”动作,判定依据往往是手腕、肘部、肩膀这几个关键点的相对位置随时间的变化。如果标注数据里关键点位置偏了几个像素,或者某几帧漏了某个关节点,训练出来的模型就会学到错误的动作模式。
我实测过一个对比实验:用同一份视频,一套用粗糙的关键点标注(关键点抖动比较大),一套用认真修正过的标注,最终 PoseC3D 在验证集上的准确率差距超过8个百分点。这还没算上训练时的收敛速度差异。所以标注工具的升级,本质上是为了把数据质量这个地基打牢。
另外一个容易被忽略的点是:目标检测的标注错误通常是“框位置偏了”,人眼还能看出大概区域;但关键点的标注错误经常是“点跑到身体外面去”或者“左右关节搞混”,这类错误在训练时会被模型当成真实分布的一部分,影响比框的偏移更大。工具必须在交互层面尽量降低这类低级错误的发生概率。
1.2 yolo26s-pose 和 PoseC3D 各自需要什么数据
yolo26s-pose 的输入是一帧普通的RGB图像,输出是图像中每个目标人的关键点坐标列表,以及每个关键点的置信度。这个模型可以用现成的姿态估计权重点来做预标注,但它的预测结果在某些场景下不可靠,比如运动员快速转身、双手交叉、多人互相遮挡等。工具需要让标注人员能快速修正这些场景下的关键点。
PoseC3D 则完全不一样。它吃的是“关键点序列”,常见做法是把每一帧的17个关键点坐标按人体骨架结构渲染成2D热图,再把T帧的热图叠成一个3D张量,交给3D卷积网络做动作分类。换句话说,标注工具必须能稳定输出“某一段连续帧内同一个人的骨架序列”,而且要保证帧与帧之间的ID不串、坐标不丢。这就对工具的序列管理和导出能力提出了更高要求。
实际项目里还有一类数据需求容易漏掉:负样本。PoseC3D 训练时除了正样本动作序列,还需要大量“无动作”或“无关动作”的片段作为背景类。标注工具2.0必须支持快速标记“背景”片段,否则后面训练时类别不平衡问题会非常棘手。
1.3 2.0版本的升级目标清单
我升级到2.0时,给自己定了几个硬性指标:
- 支持视频抽帧浏览,并且能在任意帧上直接标注和修正关键点;
- 自动调用 yolo26s-pose 做预标注,减少人工劳动量;
- 支持按动作实例切分序列,给每一段序列打动作类别标签;
- 支持多人场景下的个体跟踪ID管理,避免序列串人;
- 导出格式既能输出关键点坐标JSON,也能输出PoseC3D热图训练数据。
这五条全部落地之后,2.0版本才算是值得替换的版本。后面我会逐条拆解怎么实现,也会把升级过程中遇到的坑一并写出来。
2. 标注工具2.0的功能设计与技术选型
2.1 功能模块怎么拆
标注工具2.0的总体结构分成五个模块,每个模块各管一件事:
- 视频管理模块:负责打开视频文件、按指定间隔抽帧、维护帧索引和缓存;
- 预标注模块:负责加载 yolo26s-pose 模型,对选中片段跑推理,生成初始关键点;
- 人工修正模块:提供图像显示画布、关键点拖拽、骨骼连线绘制、置信度着色;
- 序列管理模块:管理动作实例的起止帧、动作标签、人员ID;
- 导出模块:按照PoseC3D训练需求生成热图张量、坐标JSON或训练列表文件。
模块拆得越清楚,后面改起来就越省事。很多人标注工具写着写着就变成一盘散沙,各种功能互相纠缠,最后想加一个新动作类别都要动半天代码。我这版坚持模块化设计,每个模块之间只通过明确的接口传递数据,后面扩展复用时基本只改单个模块。
2.2 技术选型:为什么用这套组合
界面这边我选了 PySide6。理由很简单:它支持在 QGraphicsScene 上叠加载入的图像和矢量图形,关键点可以用圆形 Item、骨骼可以用线条 Item,而且鼠标拖动、缩放、平移的事件处理都有成熟方案,不需要自己从头写一套图形引擎。
模型推理这边用 PyTorch 加载 yolo26s-pose 的权重。其实推理部分不占用标注员太多时间,预标注跑一遍就完事,关键是速度。我用的是一个轻量化版本,在普通显卡上单帧推理能到40毫秒左右,一个30秒的视频按5帧抽帧,大概180帧,跑完预标注只要几秒钟,完全够用。
视频帧处理用 OpenCV。读帧的时候要注意编码格式,H.265 编码的视频 OpenCV 直接按帧读会存在个别解码失败的情况,我在工具里加了一个帧级容错,读不到的帧自动用上一帧补充,并且记录异常日志,方便后续排查。
2.3 标注数据的核心结构
为了让标注工具和训练代码之间不互相绑架,我采用了一个中间 JSON 格式。一个 JSON 文件对应一个视频片段,核心结构如下:
{ "video_id": "clip_001", "fps": 30, "frame_step": 5, "width": 1920, "height": 1080, "actions": [ { "action_id": 0, "label": "raise_hand", "track_id": 1, "start_frame": 12, "end_frame": 48, "keypoints": { "12": [540, 320, 0.92, 560, 310, 0.88], "13": [541, 321, 0.90, 559, 311, 0.85] } } ] }这里 keypoints 按帧索引保存,每个关键点的内容是 [x1, y1, conf1, x2, y2, conf2],对应同一个人的多个关键点序号。我没把17个点的顺序写死,而是用单独的骨架定义文件来描述关节索引,这样以后换模型、换关键点数量时不用重构标注数据。
为什么坚持用中间 JSON 而不是直接出 COCO 格式?因为 COCO 格式为了统一,关键点缺失时要用0补齐,而不同模型对缺失关键点的定义不一样。中间格式先把原始信息完整保存下来,导出时再按目标模型要求做转换,灵活得多。
2.4 旧版1.0数据的迁移兼容策略
升级工具最现实的问题是怎么处理历史标注数据。1.0版本的标注数据只有 class_id、box 坐标和视频路径。我写了一个迁移脚本,把旧的 box 中心点作为初始化的“粗略关键点”,然后全部重新跑一遍预标注,把生成的17点结果覆盖进去。这样做可以减少大量重新标注工作量,但一定要人工复核,因为 box 中心点不能代表关键点位置。
迁移之后我发现一个细节值得注意:旧视频的抽帧间隔如果和2.0不一致,会导致同一个动作序列的帧数对不上。所以迁移脚本里必须保留原视频的 fps 和 frame_step,并用一个额外字段标记“来自1.0迁移”,方便之后筛选数据时做区分。
3. 标注工具2.0的实操步骤与核心实现
3.1 环境准备与运行方式
为了方便团队里其他人使用,我把标注工具打包为独立的 Python 工程,按以下方式运行:
- 创建虚拟环境并安装依赖:
python -m venv venv source venv/bin/activate pip install pyside6 opencv-python torch numpy- 启动工具:
python label_tool_v2.py --config configs/action_labels.yamlconfigs/action_labels.yaml 里定义动作类别列表、关键点数量、模型权重路径等。把这些写在配置里而不是硬编码在代码里,是我强烈建议的做法——团队里其他人要加一个新动作类别,只要改一行配置,不用碰代码。
3.2 视频抽帧与界面交互
视频打开后,工具默认按 frame_step=5 抽帧,也就是每秒6帧左右。这个抽帧间隔不是随便定的,它和动作的时间粒度有关:对于挥拳、举手这类动作,5帧不会漏掉关键姿态;对于特别快的动作比如乒乓球挥拍,建议改成2帧甚至1帧。抽帧太密会让标注工作量成倍增加,太疏又会丢失动作过程,要根据实际数据集来调。
界面左侧是帧列表,右侧是当前帧的标注画布。画布上叠加显示:
- 人的检测框(浅蓝色半透明);
- 17个关键点(根据置信度着色);
- 骨架连线(灰色线段)。
拖动关键点时会实时更新坐标,松开鼠标自动保存。这个交互看着简单,但其实要处理好“鼠标点击命中”和“缩放平移后的坐标换算”两个细节。我用的是 QGraphicsScene 的 ItemChange 事件,拖拽结束后直接把坐标从场景坐标转换回图像坐标,再写回内存中的数据结构。
3.3 预标注的批量处理流程
选中一段视频后,我可以一键跑预标注。核心步骤是:
- 按 frame_step 遍历所有抽帧索引;
- 对每一帧调用 yolo26s-pose 推理,得到所有人体的关键点数组;
- 根据检测置信度过滤掉明显误检的低置信度结果;
- 把关键点结果写入内存中的 action 结构,并显示到画布上。
置信度阈值我默认设0.5。太低会引入一堆垃圾关键点,人工修正反而更累;太高会漏检,导致序列断层。0.5 在实际手持相机、监控摄像头等场景下是一个相对平衡的值。
有一个细节:跑预标注前一定要先确认视频构图和人体尺度。如果画面里人特别小,比如监控全景下每个人只占几十个像素,yolo26s-pose 输出的关键点基本不可用,这时候标注工具应该提示“目标尺寸过小,预标注结果仅供参考”,不然标注员对着错误点修,比从零开始标还慢。
3.4 怎么把标注序列导出成PoseC3D的训练数据
这是升级工具的核心目标之一。导出时我提供两种模式:
模式A:输出关键点坐标数组。会生成一个 .npz 文件,里面包含每段动作的 keypoints 数组和 labels 数组。keypoints 的形状是 (T, K, 2),T 代表帧数,K 代表关键点数,2 代表 x/y 坐标。坐标会在导出时按图像宽高归一化到0-1,同时额外保存每个视频的尺度因子,用于恢复实际像素大小。
模式B:输出PoseC3D热图张量。做法是先把坐标归一化到热图尺寸(比如64x64),然后用高斯核把每个关键点渲染成一个小亮点,堆叠T帧得到 (K, T, 64, 64) 或者 (T, K, 64, 64) 的张量,保存为 .npy 文件。
为什么要提供两种模式?因为模式A方便调试和可视化,模式B喂给PoseC3D时收敛更稳定。我在训练时默认用模式B,但调试数据问题时用模式A更直观。工具导出时还会生成一个 train.txt 的列表文件,每行是一组训练样本的路径和标签,这样训练脚本可以直接读列表,不用再去处理原始视频。
导出时还有一步容易被忽略:对动作序列做长度对齐。PoseC3D 通常需要固定帧数的输入,比如32帧或64帧。如果一段动作标了45帧,另一段标了28帧,直接堆叠会报维度错误。我在导出模块里内置了线性插值对齐,把任意长度的序列统一插值到预设帧数,并保留对齐后的置信度信息。这样后面训练脚本会省很多事。
3.5 多人场景的ID管理实操
多人场景是动作数据标注最头痛的环节。比如打篮球的视频,画面里多个人重叠,预标注结果里 ID 经常在遮挡后发生跳变。我的解决办法是:
在序列管理模块里给每个动作实例分配一个 track_id,当检测到同一个位置附近突然出现新 ID 时,工具会自动提示“检测到ID跳变”。标注员可以手动把当前 ID 合并回之前的 ID,或者干脆忽略这次跳变。关键是要保证一段动作序列内的关键点都属于同一个人,否则 PoseC3D 会把两个人的骨架拼成一个人的序列,学出来的动作完全没意义。
除了 ID 合并,我还给每个实例加了一个“visible”标记,用于记录某些帧里某个关键点被遮挡看不到的情况。导出时这些帧可以按模型要求保留或掩码处理,比直接删掉帧要科学。
3.6 快捷键设计与标注效率
标注工具2.0的快捷键设计我花了不少心思。标注员一天要处理几百上千帧,每多一次鼠标点击都会变成巨大的工作量。我设计了这样一套快捷键逻辑:
- A 键切到上一帧,D 键切到下一帧;
- W/S 键切换同一帧内的不同人员实例;
- 选中关键点后按方向键微调位置;
- 数字键1-9直接打动作类别标签;
- 空格键标记当前帧为“动作起始/结束帧”。
这样一套动作下来,标注一个序列基本不用离开键盘。刚开始团队里有人觉得快捷键记不住,但用了半天之后就明显比纯鼠标操作快得多。我建议任何标注工具都要认真设计快捷键方案,这比模型优化带来的效率提升还直接。
4. 升级过程中的常见坑与排查方案
4.1 视频帧索引错了,标注全白做
第一个坑是 OpenCV 读视频会偶发跳帧。这个问题特别隐蔽,标注的时候看着帧图像好像是对的,但导出序列和实际视频里的动作对不上。排查办法是在抽帧时记录时间戳,并与帧索引做交叉验证。如果发现同一视频里两帧之间的时间戳间隔突然变大,就在日志里标记这一帧为异常帧,导出时直接拒绝导出或者提示重标。
我在工具里放置了一个小的质控模块,专门检查抽帧间隔的一致性。实测下来,H.264 编码的监控视频大概率没这个问题,但某些源素材转码后的 H.265 视频跳帧概率明显高不少。
4.2 关键点坐标的绝对值和相对值都要存
最开始我图省事,只保存了归一化后的坐标。结果后来发现,不同人的身材比例差异很大,同样是“伸手”动作,高个子和矮个子的关键点坐标分布差别很大,如果只用绝对坐标,PoseC3D 很容易过拟合到训练集里的身高分布。后来我在导出里同时保存图像宽高和人体尺度因子,训练时再做一次关节长度归一化。不要省这一步,它对动作分类准确率的影响很明显。
具体操作上,我计算人体躯干长度时用的是“颈部到髋部中心”的距离,而不是肩膀到髋部。因为颈部关键点在姿态估计模型里通常更稳定,受手臂摆动影响更小。用稳定参考点算出来的尺度因子,在整段序列里才会比较平滑。
4.3 动作标签类别的粒度没统一
另一个常见问题是同一类动作的边界不统一。比如“挥手”这个动作,有人标从手抬起到落下,有人标到手举到最高点就结束,这种标签歧义会让 PoseC3D 学到的动作起止位置不稳定。我的做法是给每个动作类别写一份标注规范文档,明确动作起始帧、结束帧、以及动作完成标志。比如“挥手”约定为:手掌从身体侧方抬起过肩为起始,手掌回落到侧方为结束。规范定了之后,不同人标注的一致性明显提高。
规范文档里还要写明“易混淆动作”的处理方式。比如“挥手”和“招手”,很多人认为差不多,但 PseC3D 会根据关键点轨迹区分。如果标注员把这两个混在一起标,模型训练出来大概率也是混乱的。我建议每个类别配2到3个典型的参考视频,标注员遇到不确定时直接对照参考视频判断。
4.4 数据质量标记让坏数据不混进训练集
我还在导出模块里加入三个质量级别字段:good / bad / questionable。标注员完成一段序列后,按整体质量给一个评级。good 直接进训练集;bad 直接剔除;questionable 单独分到一个目录,训练时用于验证集或人工复核。这个做法的好处是即使标注过程中出现了漏网之鱼,也不会直接污染训练集,排查问题时会省很多时间。
一个小细节:质量标记要在导出的时候自动写入文件名,比如 clip_001_good.npz。如果写进 JSON 里,后期整理数据还要再解析 JSON,麻烦。
4.5 我是怎么检查整个标注管线的
最后提供一个我常用的端到端质检查方法:随机挑5段已标注的动作序列,用工具的可视化模式播放,观察关键点是否平滑、是否稳定跟随人体。如果某帧的关键点突然跳了大半个画面,多半就是预标注漏了导致人工修正没注意。可视化检查比看数字靠谱得多,我建议每完成一批标注就抽检一次。
播放检查时还有一个技巧:把关键点绘制成实心点,同时打开骨架连线,连续按帧播放。重点观察人体躯干部位的关键点(颈部、髋部、肩膀),这三个点如果出现抖动,说明这一帧的修正可能出了问题。四肢末端的关键点偶尔偏一两帧可以容忍,但躯干关键点必须稳定。
4.6 多人协作时的标注一致性控制
如果你和我一样需要多人协作标注,只靠口头约定是不够的。我给每个标注员分配独立的视频文件目录,并在导出的 JSON 文件名里带上年月日和标注员ID。每日收数据时跑一次脚本,统计所有标注文件的动作类别分布、平均序列长度、质量标记比例。一旦发现某个人标注的“甩手”序列长度明显比其他人大,就要主动找对方核对规范。
还有一个容易被忽视的点:多人协作时,每个人的标记习惯会互相传染。如果有人习惯把轻微的动作幅度也标成动作,整个数据集的类别边界就会被拉宽。我每周做一次交叉抽检:把某个人的标注结果交给另一个人复核,重点看动作起止帧和类别判定。这个流程看起来很繁琐,但能有效保证数据质量,避免训练后期才发现数据分布有问题。
我个人实际操作下来的最大感受是,标注工具的升级,难点不在某个算法多高大上,而在把视频加载、模型预标注、人工修正、序列导出这些琐碎环节串成一条顺畅的流水线。这条线顺了,后面 yolo26s-pose 微调和 PoseC3D 训练才会省心。这篇先到这,下一期我打算继续写预标注模型在真实验证集中暴露出的问题,以及怎么针对性地补充标注策略。如果你也在搭动作识别的数据管线,欢迎一起聊聊自己踩过的坑。