发表于arxiv, 一个bottom-up的方法, 用来预测单张图片中多个人体的关节点位置, 已开源
文章比较难懂的原因是,这篇文章是多篇文章思想的集合,追根溯源,必须要去看这篇文章引用的文章,你才会看得懂:
带着问题看论文
-
1.不同类(头、身体,不同通道的)怎么连接的?
- 可以参考openpose文章去理解:《Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields》
- 输出特征图的维度是[b, 17, 7,h, w],17个关节的预测在不同channel,但是大小是一样,可以拼到一张图上,因为我们知道,脚和膝盖、膝盖和腰是连接,那先把所有检测出来的脚和所有的膝盖连接起来,之后再做NMS或其他后处理方法去掉不对的连接。
-
Pif
的offset
图为什么渐进的?而不是指向Ground True的方向?- 文章是在《Towards Accurate Multi-person Pose Estimation in the Wild》文章的基础上,添加和,公司也改了,但是含义没变
-
Paf
怎么产生的,怎么遍历成一个人的?
所以,这篇文章就是拼接的
一. 解决问题和贡献
1.1. 解决问题
- ` bottom-up`方案(`OpenPose`)在地低分辨率上表现差,文章在[《Towards Accurate Multi-person Pose Estimation in the Wild》](https://arxiv.org/abs/1701.01779)文章的基础上,添加$p_b^{i,j}$和$p_\sigma^{i,j}$,将特征图的定位转换到原图上做处理
- `up-bottom`(`Mask RCNN`)方案在多人有遮挡的场景上表现差,已经错检
红圈中是错检测,黑框中的是漏检的
Mask RCNN在拥挤的场景漏检
1.2. 贡献
- `bottom-up`, `box-free`, `end-to-end cnn architecture`
- 提出
Part Intensity Field (PIF)
用来定位人体关节点位置 - 提出
Part Association Field (PAF)
用来确定关节点之间的连接
1.3. 整体结构
二. 关节点定义
过早时期,谷歌提出直接对关键点做回归,通过多次迭代校准关键点的定位(x,y),这样定位还是不是很准确,后来的方法都是基于关键点热力图,将一个关键点Ground True坐标周围半径R内区域都设为1,之外的区域设为0,转化为分类问题。
三. PIF(Part Intensity Fields)
Pif label
是confidence map
和regression map
的结合
- Pif输出的是[b, 17, 5,h, w]纬度
- b:
batch
- 17:代表预测的关键点个数
h
,w
:特征图的长宽- 5:表示{,,,,}
- (i,j)是输出特征图上的坐标,表示输出的
- (x,y)表示(i,j)指向周围关键点真值的方向向量
- 是经过Pif的特征图上(i,j)位置的confidence预测值
- 、:是经过Pif的特征图上(i,j)位置的x,y方向上
offset
(偏置)向量 - :是
laplace loss
上的系数,控制关键点热力图的大小 - :不是高斯的,是用来将特征图还原到原图的比例
- b:
3.1. 的作用
在PIF中代表点(i,j)是否是关键点的置信度,用来判断关键点,在PAF里面用来衡量两点的关联性大小
3.2. 、的作用
有个这个Pif label
,就可以根据,,得到位置精度更高的confidence map
,公式如下:
公式展开就是
同样的,本篇论文的公式也是表示越近的点权重越大,只不过距离是offset
与(x,y)的距离。所以这就是为什么相近的offset(图像种的箭头)方向很接近,远的相差远,呈现渐变的感觉:
- 图(a)是
Pif label
里的 - 图(b)是Pif预测的结果offset:, ,放大后可以看出方向是渐变的,相近的向量方向相近,不仔细分析注意不到这点
- 图©是三者融合之后的结果. 可以看出来点的位置更精确, 中心处的响应值更强, 边缘处更弱
3.3. 的作用
用来控制同一张图片上不同大小的目标物的关键点热力图(Heatmap
)大小(大的物体,关键点热力图区域就应该大),文章中半径为,loss函数如下:
- 作为指数函数的幂次,就是,所以相当于是对值加权
3.4. 的作用
:不是高斯的,是用来将特征图还原到原图进行处理的比例系数,论文里没有详细说明,但是代码(openpifpaf-main/src/openpifpaf/csrc/src/cif_hr.cpp
)中可以看出:
void CifHr::accumulate(const torch::Tensor& cif_field, int64_t stride, double min_scale, double factor) {
if (ablation_skip) return;
auto cif_field_a = cif_field.accessor<float, 4>();
float min_scale_f = min_scale / stride;
float v, x, y, scale, sigma;
for (int64_t f=0; f < cif_field_a.size(0); f++) {
for (int64_t j=0; j < cif_field_a.size(2); j++) {
for (int64_t i=0; i < cif_field_a.size(3); i++) {
v = cif_field_a[f][1][j][i];
if (v < threshold) continue;
scale = cif_field_a[f][4][j][i]; // 每个点(j,i)的scale不一样,相当于权重
if (scale < min_scale_f) continue;
x = cif_field_a[f][2][j][i] * stride;
y = cif_field_a[f][3][j][i] * stride;
sigma = fmaxf(1.0, 0.5 * scale * stride); // 通过stride还原到原图尺寸,scale可以学习的参数
// Occupancy covers 2sigma.
// Restrict this accumulation to 1sigma so that seeds for the same joint
// are properly suppressed.
add_gauss(f, v / neighbors * factor, x, y, sigma, 1.0);
}
}
}
}
void CifHr::add_gauss(int64_t f, float v, float x, float y, float sigma, float truncate) {
auto accumulated_a = accumulated.accessor<float, 3>();
auto minx = std::clamp(int64_t(x - truncate * sigma), int64_t(0), accumulated_a.size(2) - 1); //x < truncate * sigma,等于0
auto miny = std::clamp(int64_t(y - truncate * sigma), int64_t(0), accumulated_a.size(1) - 1);
// -siga < x < sigma -> minx=0 -> x + sigma + 1 > 0 + 1 -> maxx = x + sigma + 1 (0, x + sigma + 1)
// x < -sigma -> minx=0 -> x + sigma + 1 < 0 + 1 -> maxx = min + 1 = 1 (0, 1)
// x > sigma -> minx= x - sigma -> x + sigma + 1 > x - sigma + 1 -> maxx = x + sigma + 1 (x - sigma, x + sigma + 1 || accumulated_a.size(1) - 1)
auto maxx = std::clamp(int64_t(x + truncate * sigma + 1), minx + 1, accumulated_a.size(2));
auto maxy = std::clamp(int64_t(y + truncate * sigma + 1), miny + 1, accumulated_a.size(1));
float sigma2 = sigma * sigma;
float truncate2_sigma2 = truncate * truncate * sigma2;
float deltax2, deltay2;
float vv;
for (int64_t xx=minx; xx < maxx; xx++) {
// x < -sigma -> xx = x = 0 -> deltax2 = 0
deltax2 = (xx - x) * (xx - x);
for (int64_t yy=miny; yy < maxy; yy++) {
deltay2 = (yy - y) * (yy - y);
if (deltax2 + deltay2 > truncate2_sigma2) continue;
if (deltax2 < 0.25 && deltay2 < 0.25) {
// this is the closest pixel
vv = v;
} else {
vv = v * approx_exp(-0.5 * (deltax2 + deltay2) / sigma2);
}
auto& entry = accumulated_a[f][yy][xx];
entry = fmaxf(entry, revision) + vv;
entry = fminf(entry, revision + 1.0);
}
}
}
- cif_field_a[f][1][j][i] :
- cif_field_a[f][2][j][i] 、cif_field_a[f][3][j][i]:,
- cif_field_a[f][4][j][i]
四. Paf label(Part Association Fields)
- Paf label的输出是[b, 17, 7,h, w]纬度
- 7:表示{,,,,,,}
- $a_c^{i,j}:代表(i,j)的confidence
- {,}和{,}:分别代表(i,j)该点到真正的两个groundtruth 关键点的(x_offset, y_offset)
- 、:是相对应的两个用来计算loss的系数
- 7:表示{,,,,,,}
不同的是,不是直接连线预测的关键点作为连接线,而是通过:对特征图中所有(i,j)点,首先找到离它最近的预测的关键点,如图(b)的短箭头:
然后通过先验知识知道第一个关键点和其他channel的关键点有连接关系,获取第二个关键点,通过如下关系计算得到通过(i,j)连接的两个预测的关键点之间的关联度PAF:
- x:(i,j)到关键点1的连线
- :paf输出的confidence
- a1:PAF输出的,
- :代表
- :利用PIF的公式更新得到的关键点2的新confidence
-
五. 解码后处理(Greedy Decoding)
得到PIF和PAF后,为了确保根据paf找到的两个点确实是应该连到一起的, 程序会执行reverse操作, 即如果通过point a找到了point b, 那么就会从point b找到point c, 如果point a和point c之间的距离小于设定好的阈值, 那么就说明这两个点应该被连在一起. 计算出来哪些点需要连在一起之后, 然后通过NMS方式将多连接的线去除掉,直到完整的pose被找到。
六.其他
文章对smoth L1和Laplace Loss做了对比实验,Laplace加上尺度因子b效果更好
参考文章:
【1】CSDN:PifPaf: Composite Fields for Human Pose Estimation