1. 为什么我们需要这些“尺子”?——评价指标入门
大家好,我是老张,在AI和计算机视觉这行摸爬滚打了十几年,做过不少人体姿态估计的项目。今天想和大家聊聊一个看似枯燥、但实际项目中绕不开的话题:评价指标。
想象一下,你训练了一个模型,它能从照片里找出人的手、脚、头等关键点。你怎么知道它找得准不准呢?你说“我看着挺准的”,这显然不行。我们需要一把客观、精确的“尺子”来测量模型的性能。这把“尺子”,就是评价指标。
在人体姿态估计领域,最常用的几把“尺子”就是 OKS 和 PCK。你可能在论文里、项目报告里经常看到它们,但公式一堆,符号复杂,是不是感觉头大?别急,今天我就用大白话,结合我踩过的坑和实战经验,带你彻底搞懂它们。我们不仅要知道它们是什么,更要明白在什么场景下该用哪一把尺子,以及怎么用代码把它算出来。
对于刚入门的朋友,理解这些指标是读懂论文、复现模型、甚至改进算法的第一步。对于有经验的朋友,深入理解这些指标背后的设计逻辑,也能帮助你在模型调优、结果分析时事半功倍。咱们不玩虚的,直接上干货。
2. OKS:多人姿态估计的“黄金标准”
2.1 OKS到底是什么?一个生动的比喻
OKS,全称 Object Keypoint Similarity,翻译过来是“目标关键点相似度”。你可以把它理解为人脸识别里的“相似度分数”,或者目标检测里的 IoU。它的核心思想是:衡量预测的关键点集合和真实标注的关键点集合有多像。
我打个比方。假设我们要比较两个人画的同一个卡通人物。一个人画的是标准原图(Ground Truth),另一个人是初学者画的(模型预测)。我们不会只看鼻子画得像不像,或者眼睛位置对不对,因为这样太片面了。OKS的做法更聪明:它给每个身体部位(关键点)都分配了不同的“容错度”。
比如,画鼻子、眼睛这种位置固定、容易辨认的部位,我们要求很严格,差一点点就觉得不像。但对于手腕、脚踝这种本身活动范围大、在图片里也可能被遮挡的部位,我们允许的误差就大一些。OKS通过一个叫 σ 的参数来量化这个“容错度”,σ值越大,表示这个关键点越难标注,允许的误差范围也越大。
2.2 拆解OKS公式:告别数学恐惧
原始文章里给出了OKS的公式,看起来有点吓人。我们把它拆开,用“人话”解释一遍:
OKS = Σ [ exp( -距离² / (2 * 尺度² * σ²) ) ] / 关键点数量
别被吓到,我们一步步看:
- 距离:就是预测的关键点和真实关键点之间的直线距离(欧式距离)。距离越远,扣分越多。
- 尺度:这个很重要!它不是图片的尺寸,而是当前这个人所在检测框的面积开根号。为什么?因为对于远处的人(框小),几个像素的误差就很严重;对于近处的人(框大),同样的像素误差可以忽略。OKS用尺度来做归一化,让评价对大小目标都公平。
- σ:刚才说的“容错度因子”。在COCO数据集中,它已经被统计好了,比如鼻子是0.026,肩膀是0.079。肩膀的σ值更大,意味着同样的距离误差,对OKS分数的惩罚更小。
- 指数函数 exp(-x):这是一个魔法函数。它把距离误差映射成一个0到1之间的相似度分数。当误差为0时,得分为1(完全一样);误差越大,得分越趋近于0。
所以,OKS的计算结果是一个0到1之间的数,越接近1,说明你预测的关键点组和真实标注越相似。
2.3 手把手代码实战:计算两个“人”的OKS
光说不练假把式。我们来看一个简化版的代码,计算一组预测关键点和一组真实关键点的OKS。这里我们假设用的是COCO的17个关键点格式。
import numpy as np
# COCO数据集中17个关键点的σ值(已归一化)
sigmas = np.array([.26, .25, .25, .35, .35, .79, .79, .72, .72, .62, .62, 1.07, 1.07, .87, .87, .89, .89]) / 10.0
variances = (sigmas * 2) ** 2 # 公式中使用的方差
def compute_oks(dt_kpts, gt_kpts, area):
"""
计算一组检测关键点与一组真实关键点的OKS。
:param dt_kpts: 模型预测的关键点,形状为(51,),即17个点的[x1,y1,v1, x2,y2,v2,...]
:param gt_kpts: 真实标注的关键点,形状同上。
:param area: 真实标注中该人物检测框的面积 (w * h)
:return: oks分数
"""
# 1. 提取坐标和可见性
gt = np.array(gt_kpts).reshape(-1, 3) # 变成 (17, 3)
dt = np.array(dt_kpts).reshape(-1, 3)
xg = gt[:, 0] # x坐标
yg = gt[:, 1] # y坐标
vg = gt[:, 2] # 可见性 (0:未标记, 1:可见, 2:遮挡)
xd = dt[:, 0]
yd = dt[:, 1]
# 2. 只计算已标注(可见或遮挡)的关键点
visible_indices = vg > 0
if np.sum(visible_indices) == 0:
return 0.0 # 如果没有可见关键点,OKS为0
# 3. 计算欧式距离
dx = xd - xg
dy = yd - yg
e = (dx**2 + dy**2) / variances / (area + 1e-7) / 2 # 加1e-7防止除零
# 4. 只取可见关键点的误差进行计算
e = e[visible_indices]
# 5. 计算OKS
oks = np.sum(np.exp(-e)) / len(e)
return oks
# 示例数据(为了演示,数据是编的)
# 假设预测的鼻子(x=100, y=120),真实的鼻子(x=102, y=118),面积=10000
dt_example = [100, 120, 1] + [0]*48 # 只设置了第一个点(鼻子),其他点默认为0
gt_example = [102, 118, 1] + [0]*48
area_example = 10000.0
oks_score = compute_oks(dt_example, gt_example, area_example)
print(f"OKS分数为: {oks_score:.4f}")
这段代码是核心。在实际项目中,比如使用COCO评估工具时,它会遍历所有图片,为每个真实的人找到与之最匹配的预测结果(通过OKS矩阵),然后根据阈值计算AP。我们下面就会讲到。
3. 从OKS到AP/mAP:如何评价整个模型?
3.1 AP:当OKS遇到阈值
算出了两个人之间的OKS,那怎么评价一个模型在整个测试集上的表现呢?这就引入了 AP。
AP 就是 Average Precision,平均精度。它的计算和OKS紧密相关:
- 对测试集中的每一个真实的人,我们都能计算其与最佳匹配预测结果的OKS分数。
- 我们设定一个阈值T,比如 T=0.5。
- 如果一个真实的人的OKS分数 > 0.5,我们就认为这个人的姿态被“正确检测”了。
- AP@0.5 = (正确检测出的人数) / (测试集总人数)。
这听起来很像目标检测里的AP,对吧?没错,思路是相通的。但这里“检测”的标准不是框的重叠度,而是关键点的相似度。
3.2 多人场景下的匹配难题与OKS矩阵
在单人姿态估计中,一张图就一个人,匹配简单。但在多人姿态估计中,尤其是自底向上的方法,挑战就来了。模型先输出一堆零散的关键点,然后再拼成一个个“人”。你怎么知道模型拼出来的这个“人A”对应的是真实标注里的“人1”还是“人2”呢?
这里就用到了 OKS矩阵。假设一张图里有M个真人,模型预测出了N个人。我们就计算一个 M行 x N列 的矩阵,矩阵中每个元素 (i, j) 的值,就是第 i 个真人和第 j 个预测人之间的OKS分数。
然后,我们会使用一种匹配算法(比如匈牙利算法,或者简单的取最大OKS),为每个真人找到一个最匹配的预测人。这个匹配过程,OKS就是衡量“匹配程度”的代价或收益。匹配完成后,每个真人就有了一个属于自己的OKS分数,后续计算AP的步骤就和单人场景一样了。
def compute_oks_matrix(dt_list, gt_list):
"""
计算预测列表和真实列表之间的OKS矩阵。
:param dt_list: 列表,每个元素是一个dict,包含'keypoints'和'bbox'(用于计算面积)
:param gt_list: 同上。
:return: oks_mat, 形状为 (len(dt_list), len(gt_list))
"""
num_dt = len(dt_list)
num_gt = len(gt_list)
oks_mat = np.zeros((num_dt, num_gt))
if num_dt == 0 or num_gt == 0:
return oks_mat
# 计算每个gt与每个dt的OKS
for j, gt in enumerate(gt_list):
gt_kpts = np.array(gt['keypoints']).reshape(-1, 3)
gt_area = gt['bbox'][2] * gt['bbox'][3] # w * h
xg = gt_kpts[:, 0]
yg = gt_kpts[:, 1]
vg = gt_kpts[:, 2]
for i, dt in enumerate(dt_list):
dt_kpts = np.array(dt['keypoints']).reshape(-1, 3)
xd = dt_kpts[:, 0]
yd = dt_kpts[:, 1]
# 计算距离
dx = xd - xg
dy = yd - yg
e = (dx**2 + dy**2) / variances / (gt_area + 1e-7) / 2
# 只考虑可见点
visible_e = e[vg > 0]
if len(visible_e) > 0:
oks_mat[i, j] = np.sum(np.exp(-visible_e)) / len(visible_e)
else:
oks_mat[i, j] = 0.0
return oks_mat
# 假设我们有一组预测和真实数据
# dt_items = [{'keypoints': [...], 'bbox': [x,y,w,h]}, ...]
# gt_items = [{'keypoints': [...], 'bbox': [x,y,w,h]}, ...]
# oks_matrix = compute_oks_matrix(dt_items, gt_items)
# 接下来可以使用oks_matrix进行匹配,为每个gt分配一个最佳的dt及其oks分数。
3.3 mAP:综合考量不同严格程度
只用一个阈值(如0.5)来评判模型有点武断。一个在宽松标准下(OKS>0.5)表现好的模型,可能在严格标准下(OKS>0.75)表现很差。
所以,更全面的做法是计算 mAP。它的做法很直观:用多个阈值去评估,然后取平均。通常,我们从0.5到0.95,以0.05为步长,取10个不同的阈值(0.5, 0.55, 0.6, ..., 0.95),分别计算10个 AP,然后求这10个值的平均值,就得到了 mAP。
这也是为什么COCO数据集的关键点检测任务,官方主要看 mAP@[.5:.95] 这个指标。它反映了模型在不同严格程度下的综合稳健性。一个高mAP的模型,说明它既能在宽松条件下找到大部分人,也能在严格条件下把关键点定位得非常精准。
4. PCK/PCKh:更直观的“老派”指标
4.1 为什么有了OKS,还要了解PCK?
OKS是现在的主流,尤其是在COCO等大型挑战赛中。但在一些老的数据集(如MPII)或某些特定场景的论文里,你可能会遇到 PCK 或 PCKh。
PCK 的全称是 Percentage of Correct Keypoints,即正确关键点的百分比。它的思想比OKS更直接:预测的关键点只要落在以真实点为中心、某个半径的圆内,就算正确。
这个“半径”不是固定的像素值,而是根据一个参考尺度来定的。这就是PCK和PCKh的区别所在:
- PCK:通常以人的躯干直径(左肩到右臀的距离)作为参考尺度。阈值T是这个尺度的比例,比如T=0.2,那么半径就是
0.2 * 躯干直径。 - PCKh:以人的头部尺寸(头部边界框的对角线长度)作为参考尺度。这是MPII数据集的标准。
注意:PCK指标的计算方式在不同数据集间可能有细微差别,使用前一定要确认该数据集的官方评估代码是如何定义参考尺度和阈值的。
4.2 PCK计算实战与代码解读
PCK的计算比OKS更简单,也更容易理解。我们来看一下核心代码逻辑。
def compute_pck(dt_kpts, gt_kpts, ref_joint1, ref_joint2, threshold=0.2):
"""
计算PCK指标。
:param dt_kpts: 预测关键点,形状 (人数, 关键点数, 2) 或 (人数, 2*关键点数)
:param gt_kpts: 真实关键点,形状同上。
:param ref_joint1: 参考关节1的索引,用于计算尺度。
:param ref_joint2: 参考关节2的索引,用于计算尺度。
:param threshold: PCK阈值,如0.2。
:return: 每个关键点的PCK值,以及平均PCK值。
"""
# 确保输入形状一致
dt = np.array(dt_kpts).reshape(-1, 2) # 假设最终形状为 [总点数, 2]
gt = np.array(gt_kpts).reshape(-1, 2)
# 计算每个实例的尺度(例如躯干长度)
# 注意:这里假设输入是按人组织的,需要先还原人的维度
# 为了清晰,我们假设输入是列表,每个元素是一个人的关键点数组
# 以下为伪代码逻辑:
all_pck = []
for dt_person, gt_person in zip(dt_kpts_per_person_list, gt_kpts_per_person_list):
# 计算当前人的尺度
scale = np.linalg.norm(gt_person[ref_joint1] - gt_person[ref_joint2])
# 计算该人所有关键点的归一化误差
dist = np.linalg.norm(dt_person - gt_person, axis=1) / scale
# 判断每个关键点是否在阈值内
correct = (dist <= threshold).astype(float)
all_pck.append(correct)
# 将所有结果堆叠,计算平均值
all_pck = np.concatenate(all_pck)
pck_per_joint = np.mean(all_pck.reshape(-1, num_joints), axis=0) * 100 # 每个关节的PCK
mean_pck = np.mean(all_pck) * 100 # 平均PCK
return pck_per_joint, mean_pck
# 示例:使用左肩(索引5)和右臀(索引12)计算躯干尺度,阈值0.2
# pck_per_joint, mean_pck = compute_pck(all_dt_points, all_gt_points, ref_joint1=5, ref_joint2=12, threshold=0.2)
PCK的结果通常是一个百分比。比如 PCK@0.2 = 85%,意味着当阈值设为躯干直径的0.2倍时,有85%的关键点被正确预测了。
4.3 OKS vs. PCK:如何选择?
在实际项目中,你该用哪个指标呢?我的经验是:
- 看数据集和任务:如果你的模型要在COCO数据集上训练和评估,或者参加主流比赛,OKS和mAP是必须用的,这是学术界和工业界的共识标准。如果你处理的是MPII数据集,那就要用PCKh。
- 看需求:OKS更精细、更公平。它考虑了不同关节的标注难度,并且通过指数函数平滑地惩罚误差。PCK更直观、更容易解释,它直接告诉你“有多少比例的点落在误差范围内”。在内部项目快速验证模型效果时,PCK可能更容易让非技术背景的同事理解。
- 看场景:对于遮挡严重、小目标多的场景,OKS的尺度归一化和按点加权的特性使其更具优势。对于尺度变化不大、关注整体姿态的场景,PCK也足够有效。
我个人的习惯是,在模型开发初期,为了快速迭代,我会用一个简单的PCK来监控训练过程。但在最终评估和论文撰写时,一定会采用OKS/mAP这套标准体系,以确保结果的严谨性和可比性。
5. 实战中的坑与优化建议
5.1 数据标注质量是生命线
所有指标都基于一个假设:你的标注数据(Ground Truth)是准确的。如果标注本身就有很大误差,那么再高的OKS分数也没有意义。我遇到过标注数据中同一类关键点σ值设置不合理的情况,导致模型在某些关节上的优化方向是错的。务必检查你的数据集中关键点的可见性标签(visible flag)和遮挡标签是否正确,这在计算OKS时至关重要,因为只计算已标注的点。
5.2 尺度因子的计算要一致
无论是OKS里的 area(面积平方根),还是PCK里的 scale(躯干或头部长度),在训练、验证、测试以及最终评估时,必须使用完全相同的计算方法。例如,COCO评估中,面积是来自标注的bbox(bbox是[x, y, width, height]格式)。如果你在训练时自己生成了bbox(比如用检测模型),要确保其定义和评估代码里期望的定义一致,否则指标会失真。
5.3 处理“匹配失败”的情况
在多人姿态评估中,匹配完成后,可能有些真实的人没有匹配到任何预测(漏检),也有些预测的人没有匹配到任何真实标注(误检)。漏检会拉低召回率,误检会拉低精确率。标准的AP计算(如COCO的eval)会妥善处理这些情况。但如果你自己写评估脚本,一定要考虑清楚:一个未匹配的预测,其OKS应该视为0吗?这会影响最终的AP计算逻辑。
5.4 可视化与调试
当模型指标不理想时,别光看数字。一定要可视化! 把OKS分数低或PCK判断错误的具体案例图片和关键点画出来。你会发现很多问题:可能是模型在某些姿态(如侧身、蜷缩)下失效,可能是对遮挡处理不好,也可能是标注本身就有问题。这种针对性的分析,比盲目调参有效十倍。
例如,你可以写一个脚本,专门找出OKS低于0.5的样本,保存成图片。然后一张张看,是肩膀歪了,还是手腕飞了?是背景干扰,还是光照太暗?这些直观的反馈是优化模型最宝贵的线索。
5.5 自定义关键点与σ值
如果你的任务不是标准的人体17关键点,而是手部21关键点、面部68关键点,甚至是动物的关键点,怎么办?OKS公式仍然适用,但你需要为自己的关键点类型重新计算一套σ值。
原始文章末尾给出了计算方法:统计你标注数据集中,每个关键点预测误差(归一化后的距离)的标准差。这个标准差就代表了该类关键点的固有标注难度,可以作为σ值。虽然过程繁琐,但对于构建一个公平的评价体系来说是必要的。如果不想这么麻烦,也可以先为所有关键点设置一个相同的经验值(如0.05),但要知道这可能会让指标对不同关键点的敏感度不同。
说到底,评价指标是我们衡量模型的尺子。选对尺子、用好尺子,才能知道我们的模型到底走到了哪一步,下一步该往哪里使劲。希望这篇结合了原理、代码和实战经验的指南,能帮你把这把尺子握得更稳,量得更准。在实际项目中多算、多看、多思考,这些指标背后的故事,会让你对姿态估计任务有更深的理解。
198



被折叠的 条评论
为什么被折叠?



