사용자 정의 키포인트 및 OKS 지표
비인체 객체용 키포인트 스키마를 설계하고, 데이터셋 YAML을 작성 및 검사하며, 각 포인트의 배치 정밀도를 바탕으로 OKS 시그마를 설정합니다.
포즈는 사람에게만 적용되는 것이 아닙니다. 지게차의 포크 끝, 택배 라벨의 모서리, 동물의 코와 꼬리처럼 안정적이고 이름을 붙일 수 있는 포인트가 있는 객체라면 무엇이든 포즈 데이터셋이 될 수 있습니다. 첫날 작성한 스키마는 영구적으로 사용됩니다. 모든 라벨과 좌우 반전 쌍, 모든 지표가 스키마에 따라 달라지며, 나중에 변경하려면 모든 항목을 다시 라벨링해야 합니다. 이 강의에서는 천 장의 이미지에 라벨을 붙이기 전에 스키마를 올바르게 설계하고, 지표가 스키마를 정확히 어떻게 평가하는지 보여줍니다.
실습형
키포인트 스키마 설계하기#
좋은 키포인트는 세 가지 속성을 공유합니다. 모호하지 않음(두 주석 작업자가 같은 위치에 포인트를 배치함), 안정적임(객체가 취하는 모든 포즈에 포인트가 존재함), 유용함(다운스트림 계산에서 포인트를 사용함)입니다. 천장 카메라로 촬영한 지게차에서 제품 관련 질문이 "트럭이 움직일 때 포크가 올라가 있는가?"라고 가정해 보겠습니다.
| 인덱스 | 키포인트 | 존재하는 이유 | 대칭 포인트 |
|---|---|---|---|
| 0 | left_fork_tip | 포크의 범위와 방향 | 1 |
| 1 | right_fork_tip | 포크의 범위와 방향 | 0 |
| 2 | mast_top | 마스트를 기준으로 한 포크 높이 | 2(자기 자신) |
| 3 | mast_base | 차체의 기준점 | 3(자기 자신) |
모든 포인트는 계산에 기여하는 이유가 있어야 합니다. 포크 끝은 방향을 제공하고, 마스트 쌍은 높이를 제공합니다. 후보 포인트마다 다음 기준을 적용해 보세요:
"혹시 모르니" 포인트를 추가하지 마세요. 포인트 하나를 추가할 때마다 모든 인스턴스에 대한 라벨링 시간이 늘어나고, 아무도 사용하지 않는 포인트는 지표에 노이즈만 더합니다. 공개 데이터셋에서도 다양한 사례를 볼 수 있습니다. Tiger-Pose는 가시성 정보 없이 포인트 12개를 사용하고, Dog-Pose는 24개, Hand Keypoints는 21개를 사용합니다:


데이터셋 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_basekpt_names은 클래스별 사람 및 도구의 포인트 순서를 정의합니다.flip_idx은 자기 자신의 역변환이어야 합니다. 두 번 적용하면 모든 인덱스가 원래 값으로 돌아옵니다.kpt_oks_sigmas은 선택 사항이며 학습 중 키포인트 손실과 검증 중 OKS에 모두 가중치를 적용합니다. 이 값이 지정된 경우 키포인트마다 양수 값 하나가 있어야 하며, 그렇지 않으면 실행이ValueError과 함께 중단됩니다.
Ultralytics Platform에서는 동일한 스키마를 스켈레톤 템플릿으로 사용합니다. 새 템플릿은 기존 템플릿에서 시작할 수 있으므로(기존 템플릿에서 시작…) 모든 키포인트를 일일이 배치하는 대신 포인트 이름을 바꾸고 불필요한 포인트를 줄이면 됩니다. 스키마 표의 인덱스 순서를 유지하세요:

학습 전에 라벨 검사하기#
포즈 데이터셋은 조용히 실패합니다. 행에 키포인트 하나가 빠지면 이후의 모든 값이 밀리고, 버그가 있는 내보내기 도구가 출력한 가시성 값 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 rowsvalues[7::3]은 첫 번째 가시성 플래그(클래스, 바운딩 박스 값 4개, 그 다음 x y v)부터 시작해 이후의 세 번째 값마다 가져옵니다.
OKS가 키포인트를 평가하는 방법#
Pose mAP는 바운딩 박스 IoU를 Object Keypoint Similarity(OKS)로 대체합니다. 각 라벨링된 키포인트에 대해 OKS는 예측과 라벨 사이의 픽셀 거리를 객체 크기와 해당 키포인트의 시그마에 따라 조정해 0~1 사이 점수로 변환한 다음, 라벨링된 키포인트 전체의 평균을 계산합니다. Pose mAP50-95는 IoU 임계값에 대한 box mAP와 같은 방식으로 OKS 임계값 0.50~0.95에서 정밀도를 평균합니다. 공통 계산 체계에 대한 자세한 내용은 성능 지표 가이드를 참조하세요.
시그마는 직접 측정해 보면 가장 쉽게 이해할 수 있습니다. 이 스크립트는 포즈 검증기가 호출하는 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픽셀 벗어나면 시그마 0.025일 때 OKS 임계값 0.50을 통과하지 못하지만, 시그마 0.10일 때는 0.90까지 모든 임계값을 통과합니다. 같은 모델과 같은 오차인데도 Pose mAP는 크게 달라집니다.
| 시그마 | 효과 | 용도 |
|---|---|---|
| 작음(≈ 0.025–0.05) | 몇 픽셀만 벗어나도 점수가 감소합니다 | 눈, 포크 끝, 모서리와 같은 선명한 포인트 |
| 큼(≈ 0.08–0.10) | 넉넉한 허용 오차 | 엉덩이, 마스트 중앙과 같은 모호한 포인트 |
| 기본값 | kpt_shape: [17, 3]의 COCO 시그마를 사용하고, 그렇지 않으면 모든 포인트에 1/K를 적용합니다 | 여기서 시작한 다음 측정하세요 |
시그마에는 각 포인트를 주석 작업자가 얼마나 일관되게 배치하는지 반영해야 합니다. 이를 측정하려면 두 사람이 동일한 이미지 50장에 라벨을 붙이고 키포인트별 거리를 비교하세요. 의견 차이가 큰 포인트에는 더 큰 시그마를 부여해야 합니다. 그렇지 않으면 지표가 사람의 노이즈에 대해 모델을 부당하게 평가합니다.
위 표에 나온 것처럼 kpt_oks_sigmas을 변경하면 모델은 그대로여도 포즈 mAP가 달라집니다. 첫 기준선을 만들기 전에 시그마를 고정하고, 모든 결과에 시그마를 기록하며, 서로 다른 값으로 검증한 실행 결과를 비교하지 마세요.
정답 보기
컨베이어 위의 택배를 위에서 촬영한 예시입니다. 확인하려는 사항은 "라벨 면이 위를 향하고 있는가, 그리고 어느 방향으로 회전되어 있는가?"입니다.
| 인덱스 | 키포인트 | 목적 | 대칭 포인트 | 시그마 |
|---|---|---|---|---|
| 0 | label_top_left | 라벨 방향 | 1 | 0.04 |
| 1 | label_top_right | 라벨 방향 | 0 | 0.04 |
| 2 | label_bottom_right | 라벨 방향 | 3 | 0.04 |
| 3 | label_bottom_left | 라벨 방향 | 2 | 0.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 미만으로 떨어지는 오프셋이 주석 작업자의 정밀도를 고려할 때 모델이 허용할 수 있는 최대 오차입니다.