Ultralytics YOLO27: 加入候补名单。
Get Started
课程中级

自定义关键点与 OKS 指标

为非人类对象设计关键点方案,编写并检查数据集 YAML,并根据每个点可被定位的精度设置 OKS sigma。

姿势估计并非只适用于人。叉车货叉的尖端、包裹标签的角、动物的鼻子和尾巴——任何具有稳定且可命名点位的对象都可以构成姿势数据集。你在第一天编写的方案将永久沿用:每个标签、每组翻转配对和每项指标都取决于它,之后更改就意味着要重新标注所有数据。本课会在任何人标注上千张图像之前,帮你确定正确的方案,并准确说明指标将如何评估它。

学习成果

生成一份包含翻转配对和 OKS sigma 的自定义关键点方案数据集 YAML、一份能通过真实数据检查的标签校验器,以及对每个 sigma 所容许像素误差的预测。

快速通道
如果你已经熟悉相关内容,以下是简要版。
  1. 选择 3–12 个明确无歧义、每种姿势中都存在且下游代码会读取的点。

  2. 确定这些点的顺序,然后编写 flip_idx,将左右对应点互换;中心点映射到自身。

  3. 如果不同点的标注精度不同,就添加 kpt_oks_sigmas。

  4. 训练前运行标签校验器,检查列数、可见性取值以及 flip_idx 是否为自身的逆映射。

实践操作

设计关键点方案#

好的关键点有三个共同特征:明确无歧义(两位标注者会把点放在同一位置)、稳定(对象呈现的每种姿势中都存在该点)以及有用(有下游计算会读取该点)。以顶置摄像头拍摄的叉车为例,产品要回答的问题是“卡车移动时货叉是否抬起?”:

索引关键点存在原因镜像对应点
0left_fork_tip货叉的长度和方向1
1right_fork_tip货叉的长度和方向0
2mast_top货叉相对于门架的高度2(自身)
3mast_base车架上的参考点3(自身)

每个点都有其用途,因为它们都为计算提供信息:货叉尖端用于确定方向,门架上的一对点用于确定高度。对每个候选点都应用相同的筛选标准:

克制住“以防万一”而添加点的冲动:每个点都会增加每个实例的标注时间,而无人读取的点只会给指标增加噪声。公开数据集展示了不同规模:Tiger-Pose 使用 12 个点且不包含可见性,Dog-Pose 使用 24 个点,Hand Keypoints 使用 21 个点:

手部关键点方案,包含 21 个带编号的标志点,从 0 号手腕到每根手指的关节,再到 20 号小指指尖

Dog-Pose 示例:一只小狗带有狗的边界框,脸部、腿部和身体上标注了彩色关键点

编写数据集 YAML#

path: forklift-pose
train: images/train
val: images/val

kpt_shape: [4, 3] # 4 keypoints, each x, y, visibility
flip_idx: [1, 0, 2, 3] # fork tips swap, mast points stay
kpt_oks_sigmas: [0.05, 0.05, 0.08, 0.08] # tips are sharp, mast ends are fuzzier

names:
  0: forklift

kpt_names:
  0:
    - left_fork_tip
    - right_fork_tip
    - mast_top
    - mast_base
  • kpt_names 按类别记录人员和工具的关键点顺序。
  • flip_idx 必须能够还原自身:应用两次后,每个索引都应映射回自身。
  • kpt_oks_sigmas 是可选项,会同时对训练中的关键点损失和验证中的 OKS 加权;如果使用,它必须为每个关键点提供一个正值,否则运行会因 ValueError 而停止。

在 Ultralytics Platform 上,同一套方案会成为骨架模板。你可以从已有模板开始创建新模板(从……开始),这样只需重命名和删减关键点,而不必手动逐一放置所有关键点——请保持与方案表一致的索引顺序:

Ultralytics Platform 的“新建骨架模板”对话框:从现有模板开始创建的 17 点人体骨架,列出了从 nose 到 left_wrist 的关键点,供你重命名

训练前检查标签#

姿势数据集出错时往往不会明显报错:某行少了一个关键点,就会导致之后的所有值错位;有缺陷的导出器输出可见性值 3 时,程序也会像读取其他数字一样读取它。这个校验器会使用 Ultralytics 自带的加载器加载 YAML,并验证每一行。它可以直接用于 COCO8-pose;要换成你自己的 YAML 路径:

from pathlib import Path

from ultralytics.data.utils import check_det_dataset

data = check_det_dataset("coco8-pose.yaml")  # resolves paths, downloads if needed
k, dims = data["kpt_shape"]
flip = data.get("flip_idx", [])
assert not flip or sorted(flip) == list(range(k)), "flip_idx must list every keypoint index once"
assert all(flip[flip[i]] == i for i in range(len(flip))), "flip_idx must undo itself: pairs swap, center points stay"

expected = 5 + k * dims  # class + 4 box values + k keypoints of `dims` values
bad = 0
for label in Path(data["path"], "labels").rglob("*.txt"):
    for n, row in enumerate(label.read_text().splitlines(), 1):
        values = row.split()
        if len(values) != expected:
            bad += 1
            print(f"{label.name}:{n} has {len(values)} values, expected {expected}")
        elif dims == 3 and not {float(v) for v in values[7::3]} <= {0, 1, 2}:
            bad += 1
            print(f"{label.name}:{n} has a visibility flag outside 0/1/2")
print(f"kpt_shape={data['kpt_shape']}, flip_idx ok, {bad} bad rows")
kpt_shape=[17, 3], flip_idx ok, 0 bad rows

values[7::3] 从第一个可见性标志开始(类别、四个框值,然后是 x y v),之后每隔两个值取一个值。

OKS 如何为关键点评分#

姿势 mAP 用对象关键点相似度(OKS)取代边界框 IoU。对于每个已标注的关键点,OKS 会将预测值与标签之间的像素距离转换为 0 到 1 的分数,并根据对象大小和该关键点的 sigma 进行缩放,然后对已标注的关键点取平均值。Pose mAP50-95 会对 OKS 阈值从 0.50 到 0.95 的精确率取平均值,和框 mAP 对 IoU 阈值取平均值完全相同;性能指标指南介绍了两者共用的机制。

通过测量最容易理解 sigma。下面的脚本使用两个 sigma 对同一个叉车预测进行评分,并调用姿势验证器使用的 kpt_iou 函数:

import torch

from ultralytics.utils.metrics import kpt_iou

# One labeled forklift, 4 keypoints (x, y, visibility) in pixels, inside a 200 x 120 px box
label = torch.tensor([[[40.0, 30, 2], [40, 90, 2], [170, 20, 2], [170, 100, 2]]])
area = torch.tensor([200 * 120 * 0.53])  # Ultralytics scales box area by 0.53 to approximate object area

for offset in (2, 5, 10, 20):
    pred = label.clone()
    pred[..., 0] += offset  # every predicted keypoint lands `offset` pixels to the right
    sharp = kpt_iou(label, pred, area, sigma=[0.025] * 4).item()
    loose = kpt_iou(label, pred, area, sigma=[0.10] * 4).item()
    print(f"{offset:2d} px off: OKS {sharp:.2f} with sigma 0.025, {loose:.2f} with sigma 0.10")
 2 px off: OKS 0.94 with sigma 0.025, 1.00 with sigma 0.10
 5 px off: OKS 0.67 with sigma 0.025, 0.98 with sigma 0.10
10 px off: OKS 0.21 with sigma 0.025, 0.91 with sigma 0.10
20 px off: OKS 0.00 with sigma 0.025, 0.67 with sigma 0.10

对于一辆 200 × 120 像素的叉车,预测误差为 10 像素时,sigma 为 0.025 会导致其无法通过 0.50 OKS 阈值;sigma 为 0.10 时,则能通过所有不高于 0.90 的阈值。同一个模型、相同的误差,姿势 mAP 却大不相同。

Sigma效果适用情况
较小(≈ 0.025–0.05)只偏差几像素就会降低分数边界清晰的点:眼睛、货叉尖端、角点
较大(≈ 0.08–0.10)容差较大边界模糊的点:髋部、门架中部
默认kpt_shape: [17, 3] 使用 COCO sigma;否则每个点使用 1/K从这里开始,然后进行测量

Sigma 应反映标注者定位每个点的一致程度。实际测量一下:让两个人标注相同的 50 张图像,并比较每个关键点的距离。对于有分歧的点,应使用较大的 sigma;否则,指标就会因人为噪声而惩罚模型。

只有使用同一套规范时,指标才可比较

如上表所示,改变 kpt_oks_sigmas 会改变姿态 mAP,但不会改变模型。首次建立基准前就应确定 sigma,并在每个结果中记录;绝不要比较使用不同数值进行验证的运行结果。

试试看

为你所在领域中的一个对象设计关键点方案:列出 3–8 个关键点,为每个点写一句理由、指定镜像对应点和 sigma。编写 YAML,先对 COCO8-pose 运行 check_pose_labels.py,再对少量自己的标签运行,然后使用 oks.py 找出最清晰的点在 OKS 低于 0.5 之前能够容忍多少像素的误差。

完成条件
满足以下所有条件,即表示你已完成本课程。
  • 你的方案表为每个关键点列出了名称、用途、镜像对应点和 sigma。

  • 你的 YAML 包含 kpt_shape、自身为逆映射的 flip_idx,以及(如果使用)每个关键点对应的一个正 sigma;校验器在你的样本中报告的错误行数为 0。

  • 你可以说明最清晰的关键点误差达到多少像素时,就不再计为 OKS 0.5。

显示解答

例如,从上方观察传送带上的包裹时,问题是“标签面朝上吗?标签旋转了多少?”:

索引关键点用途镜像对应点Sigma
0label_top_left标签方向10.04
1label_top_right标签方向00.04
2label_bottom_right标签方向30.04
3label_bottom_left标签方向20.04
kpt_shape: [4, 3]
flip_idx: [1, 0, 3, 2]
kpt_oks_sigmas: [0.04, 0.04, 0.04, 0.04]

flip_idx 通过了自身为逆映射的检查:0 → 1 → 0,2 → 3 → 2。镜像后的标签会交换左右,但保持上下不变,因此左上角应与右上角配对,而不是与右下角配对。

要确定容差,请在 oks.py 中设置 sigma=[0.04] * 4 并扩展偏移范围;标签边界框决定缩放比例,因此请使用与你的图像中典型包裹标签大小相同的边界框。OKS 首次低于 0.5 时的偏移量,就是在标注者精度范围内模型能够容忍的最大误差。

接下来做什么

接下来,我们会处理方向很重要、形状却是简单矩形的对象:有向边界框。

登录以保存学习进度