OpenPifPaf decode源码解析

openpifpaf 的decode过程:

网络的输出:

  1. pif, 原始的输出共有4个, 分别为:

    1. joint_intensity_fields, shape 为 [17, output_h, output_w]. 其实就是输出的每个位置上的confidence map, 17表示channel数, 在pose检测里面表示总共有多少个关键点需要检测.
    2. joint_offset_fields, shape 为[17, 2, output_h, output_w]. 为对应位置上的离其最近的关节点位置的偏移量. 这个是学习得到的, 2表示是两个方向(x, y)的偏移量. 所以关节点的真正位置需要把该位置的(x, y)和其两个方向的(x_offset, y_offset)相加起来得到.
    3. joint_b, shape为[17, output_h, output_w]. 论文里提到的spread b,是自适应并且经过网络学习得到的, 用来参与loss计算, 在decode的时候并没有用到.
    4. joint_scale_fields. shape为[17, output_h, output_w]. 自适应的scale值, 用来表明该关键点的scale大小.不确定是否有用在loss计算里. decode的时候则是作为类似gaussian的sigma值参与decode过程.
  2. paf, 原始的输出共有5个, 按照顺序为: (首先说明下, 论文提出的paf和之前OpenPose及PersonLab提出的连接方式都不一样. 该论文提出的paf连接为, 每个位置预测出哪两个点需要连接在一起, 因此不是单纯的两个关节点之间的直接连接, 而是经过了另外一个位置进行第三方连接)

    1. joint_intensity_fields, shape为[19, output_h, output_w]. 19表明共有多少个连接需要学习, 对应的是每个输出位置上的paf的confidence值

    2. joint1_fields, shape为[19, 2, output_h, output_w]. 这个位置表明的两个可以连接在一起的点中的第一个点的信息, 其实就是偏移值, (x_offset, y_offset).

    3. joint2_fields, shape为[19, 2, output_h, output_w]. 同上, 表示的是一条线段上的第二个点的偏移值.

    4. joint1_fields_logb, shape为[19, output_h, output_w]. 论文里提到的spread b,是joint1的, 用来参与loss计算和decode. 根据decode的过程来看, 网络输出的这个值是经过log计算后的, 所以叫做logb,在decode的时候需要先exp还原.

    5. joint2_fields_logb, shape为[19, output_h, output_w]. 同上, 只不过变成是第二个点的b了.

decode过程:

  1. normalize_pif. 就是把网络的pif 4个输出整合在一起, 首先是对joint_intensity_fieldsjoint_scale_fields进行扩维, 把shape从[17, output_h, output_w]变成[17, 1, output_h, output_w]. 接着是根据joint_offset_fields对[output_h, output_w]这么大的矩阵上, 对应的位置(x, y) + offset. 举例来说, 原来的joint_offset_fields, 其中[1, :, 4, 5] 这个位置表示的offset值为(1, 2), 那么意思就是在(4, 5)这个位置坐标(4, 5)上, 需要加上偏移(1, 2)才是真正的这个位置所表示的关键点坐标值, 也即是(5, 7), 因此最后[1, :, 4, 5] == [5, 7]. 最后把更新后的这三个矩阵concatenate一起, 变成[17, 4, output_h, output_w]的pif信息, 4表示每个位置上都有四个值, 分别是[confidence, x, y, scale]. 这个时候的[x, y]就是真正的这个点表示的关键点的坐标值.

  2. normalize_paf. 同pif一样, 也是把网络的paf 5个输出整合在一起. 除了对joint1_fieldsjoint2_fields进行同样的offset相加外(和pif的操作一样), 还对两个logb进行了exp操作, 然后对joint_intensity_fields和两个b进行扩维成[19, 1, output_h, output_w]. 最后对更新后的joint_intensity_fields, joint1_fields, joint1_logb进行concatenate一起, 得到[19, 4, output_h, output_w]大小的矩阵, 这个存储的全是有关joint1的, 同理, 对joint2做同样的操作, 同样得到[19, 4, output_h, output_w]大小的矩阵. 4表示[confidence, x, y, b]. 需要注意的是joint1和joint2共享同样的confidence值. 最后, 对这两个矩阵进行stack一起,得到最终的[19, 2, 4, output_h, output_w]输出.

  3. _target_intensities: 根据上面得到的新的pif信息, 利用文章提出的公式1, 得到在高维空间(也就是网络的输入分辨率下)的pif_hr, pifhr_scales信息. hr表示的就是high resolution的意思. 方法就是首先找到pif里所有confidence > v_th的值, 先把(x, y, s) 都乘上网络的scale值, 然后针对这些位置, 以这些位置为中心, 位置对应的scale值为gaussian的sigma, 范围为当前位置对应的confidence/16(不是很理解为什么是这个值), 接着对这个范围内的值做高斯变换, 和pif里原来的位置信息相加, 使得本来confidence就很高的位置值更高, confidence值低的位置值更低. (具体图示可以参考文章图3). 源码里的scalar_square_add_gaussian函数就是这个作用, 这样就得到了pif_hr. cumulative_average函数的作用好像是对这个区域的scale求个平均? 没有很理解这个函数的作用, 但结果是得到对应的scale值.

  4. _score_paf_target: 这个函数的作用就是根据上面得到的paf信息, 得出哪些点是连在一起的. 因为paf的shape是[19, 2, 4, output_h, output_w], 因此对于每个连接, 其对应的paf field shape均为[2, 4, output_h, output_w]. 首先, 对于单个线段的信息, 假设为第一条线段的信息, 为fourds, shape = [2, 4, output_h, output_w]. 首先, 找到fourds里每个位置上的confidence值最小的那个(源码是scores = np.min(fourds[:, 0], axis=0), 但我感觉因为对于同一个feature map上的位置而言, joint1 和 joint2的confidence是一致的, 因此其实就是把feature map上对应位置的score值去出来). 然后, 找到满足scores > score_th条件的位置, 把这些位置取出来, 组成个新的fourds. 同理, scores也取出来, 组成个新的scores. 这时fourds的维度就是[2, 4, n], scores的维度是[n, ], n为满足前面score条件的点的个数. 然后, 找到第一条线段对应的pif的channel位置, 例如第一条线段在coco是[15, 13], 那么就把pif_hr[15]当作是这个线段的joint1所在的featuremap, pif_hr[13]就是这个线段的joint2所在的featuremap. 因为此时fourds的shape为[2, 4, n], 那么fourds[0]就是所有满足刚才那个条件的joint1集合, fourds[1]就是joint2集合. 地一个函数scalar_values的意思, 就是找到在pif_hr[15]上, 位置为(fourds[0, 1] * self.stride, fourds[0, 2]*self.sride)的confidence值,(注意这个confidence是指在pif_hr上的confidence, 不知道为啥程序里变量名起做pifhr_b.) 此时得到的pifhr_b就是joint1对应的在pifhr上的confidence值, 接着执行代码scores_b = scores * (pifhr_floor + (1.0 - pifhr_floor) * pifhr_b), 我的理解是这行代码的作用就是根据confidence对scores进行更新, pifhr_floor是0.1, 如果本身的pifhr_b值很大, 那么其对应的在paf的score就还是很大, 如果小, 相应的也缩小了其对应的paf的score值. 更新完后得到scores_b, 再根据这个值进一步过滤找到符合条件的joint1, joint2点, 最后把符合条件的点集合, 得到scored_backward里的一个元素. 因为paf总共有19个channel, 所以scored_backward总共有19个元素, 每个元素都是[7, m]大小的矩阵, 存储的信息分别是[score_b, joint2_x, joint2_y, joint2_b, joint1_x, joint1_y, joint1_b]. m是scores_b中符合条件的点的个数. 因为元素是先joint2元素信息再是joint1信息, 所以叫做scored_backward. 下面还会有在**_pifhr[j2i] (_pifhr[13])**搜索符合条件的joint2的信息, 仿照上面的步骤, 同样得到一个[7, n]的矩阵, 信息为[score_b, joint1_x, joint1_y, joint1_b, joint2_x, joint2_y, joint2_b], 因为这个是先jioint1的信息再是joint2的信息, 因此其组成的列表又叫做scored_forward, 其实就是看是以joint1为出发点还是终止点表示的这个线段信息.

  5. 总结下先, 3执行完之后, 得到了一个在高分辨率下的pifhr信息和pif_scales信息, shape均为[17, hr_h, hr_w]. 4执行完后, 得到了两个均含有19个元素的列表, 分别叫做scored_forward 和 scored_backward. 列表里的每个元素, 都是满足一系列条件的点的信息, 例如对于scored_forward[0]来说, shape大小为[7, m], m为点的个数, 7表示[score_b, joint1_x, joint1_y, joint1_b, joint2_x, joint2_y, joint2_b], 形象话说, 就是对于7*m的矩阵, 每一列都能找到两个点用来表示线段0. 需要明确的一点, 这里面的两个点, 都还是通过paf信息得到的点的位置, 并不是在pif信息上的点的位置

  6. 接着就是根据3和4得到的结果, 用来连线, 判断哪些线段应该连起来组成一个人**.**

  7. _pif_seeds函数: pif_seeds函数用来在self.pif上找到confidence符合阈值的点的信息, , 然后再在pifhr上找到该点对应的confidence值, 接着把(v, field_i, xx, yy, ss)组成一个seed放入seeds中, v是在pifhr上的confidence, field_i是用来表明这个点在第几个channel上, (xx, yy, ss)都是在pif上的位置和scale信息. 最后, 对seeds按照v的值降序排列, 越靠近前面的seed, confidence值更大.(比较奇怪的是为什么confidence是在pifhr找, 但(xx, yy, ss)都是在pif上面找)

  8. 首先根据3得到的pifhr_scales生成同样大小的矩阵occupied, 找到了pif_seeds之后, 按照排过序的seeds序号, 先从confidence值最大的seed进行寻找. 首先判断当前seed的(x, y)是否超过了occupied的范围, 如果超过就寻找下一个seed, 没有就返回当前occupied[y, x]的值. (这边同样有个问题, occupied初始化为0, 那寻找第一个seed的时候, 无论是否超出范围, 返回的值都为0, 也就是说按照源码来说, 第一个seed是没有用的?我尝试了把第一个seed当作正常运行放进去, 发现结果没有太大差别, 就是最后的anno score值变大了一些. 不过python格式的程序速度慢了将近一倍.) 然后会执行函数scalar_square_add_single, 这个函数的作用就是让occupied矩阵在(x*stride, y*stride) 为中心, 范围为max(4, s*stride)的范围内加1. 这样就相当于更新了以(x, y)为中心的occupied值. 接着, 根据seed的(x, y, v) 和 当前所处的channel编号f, 构建Annotation类. ann.data是一个[17, 3]的矩阵, 存储的就是一个完整的人体pose应该有的关节点个数. ann.skeleton_m1就是下标从0开始的[19, 3]的skeleton连接编号. 初始化ann的时候, 会根据f的值把传入的(x, y, v) 放进去ann.data[f]上. 接着, 就会以当前ann为已有的skeleton信息, 以前面得到的_paf_forward, _paf_backward连接信息进行_grow操作.

  9. _grow函数: 在进行从_paf_forward, _paf_backward抽取信息前, 会先对当前的ann执行ann.frontier_iter()函数. ann.frontier_iter()函数的意思就是找到当前ann.data里, 应该和ann.data里已有值的点相连的点, 但是却没有在ann.data里面的点. 然后, 根据这个点是在已有点的后面(即已有点是joint1, 未找到点是joint2)还是在前面, 判断该点是处于forward状态还是backward状态. 接着, 对这些所有点进行按照已有值的点的confidence值降序排列. (函数最终会得到一个列表的集合, 每个列表的元素都是[confidence, connection_i, True/False, j1i, j2i], confidence是已有点的confidence, connection_i是这个点应在的线段连接编号, True表明已有点是joint1, 放进去的这个点是joint2, j1i 和 j2i就是这个connection连接的两个点的channel编号). 最后, 从这个列表frontier里取出第一个值, 表明是当前已有的ann.data里, confidence值最高的那个点应该连接的点的信息. 取出的值就是上面所讲的列表的元素值信息, 如果是True(表明为forward), 则xyv = ann.data[j1i], directed_paf_field = paf_forward[i], directed_paf_field_reverse = paf_backward[i], 否则的话, xyv = ann.data[j2i], directed_paf_field = paf_forward[i], directed_paf_field_reverse = paf_backward[i]. xyv是从ann.data里取出的目前confidence值最高且需要额外的一个点和它连接的点(new_xyv), directed_paf_field就是new_xyv所在的paf_field, directed_paf_field_reverse就是刚好xyv所在的paf_field. (刚好一个是paf_forward, 一个是paf_backward). 然后, 根据得到的directed_paf_field, 执行_grow_connection函数.

  10. **_grow_connection**函数. 这个函数比较简单, 首先是在paf_field上, 以(x,y)为中心, 找到在paf_field里的所有点符合条件的点, 拿出来构成新的paf_field, 然后, 在这个新的paf_field里面, 计算这些点和xy这个点的距离, 并根据距离更新里面点的score值, 最后选择score值最大的那个点作为返回值. 为了保证找的这个点是正确的, 会根据找到的这个点进行reverse match, 步骤和前面的一致, 判断通过reverse找到点是否和(x,y)满足距离阈值条件. 如果是, 就把找打的这个点当作新的一个点放进ann.data里面. 这样循环遍历, 就能把ann给尽量的填充满.

  11. 当对当前的seed点进行grow_connection之后, 会根据之前得到的pifhr_scalesann.data的每个点加上对应的scale值, 然后根据目前已有的ann.data值, 再更新一遍每个ann.data里的点对应的occupied矩阵, 就是执行函数scalar_square_add_single函数. 最后选择下一个seed点进行grow, 直到所有的seed点都遍历过一次. 最后再对所有的anns执行complete_annotations操作, 就是寻找对于每个ann上额外的空置的点(应该有点和它连接的实际上没有)是否还有可能有其它点和它连在一起, 就是再执行一边_grow操作.

  12. **soft_nms**函数: 对11得到的annos执行nms操作, 过滤掉一些有可能不符合要求的ann. ann.score值是其中ann.data中每个点的score值的加权平均.

你可能感兴趣的:(OpenPifPaf decode源码解析)