译文: 魔镜炼成记:Google位姿估计应用Move Mirror架构与实现 - 2018.07.20

译文:从图像数据中提取非常精准的姿势数据

原文:Move Mirror: An AI Experiment with Pose Estimation in the Browser using TensorFlow.js

Demo:g.co/movemirrormove-mirror

更多应用:https://www.irenealvarado.com/

Google发布的一个有趣的AI应用,Move Mirror. Move Mirror使用摄像头捕捉人物动作,并实时地在8万张图像中匹配和动作相近的图像.

效果可见:https://v.qq.com/x/page/p1344cqzbsh.html

在发布Move Mirror的同时,Google Creative Lab的Jane Friedhoff和Irene Alvarado发表长文,分享了打造Move Mirror的经验.

1. PoseNet

Google Creative Lab 所制作Move Mirror 应用的核心是姿态估计(pose estimation)模型.

Google 在CVPR 2017上提交了PoseNet的论文. PoseNet可以检测多人的2D姿态,并在COCO数据集的关键任务上达到了当前最优表现.

图: PoseNet 关键点检测可用于辅助设备(如轮椅、假肢等).

图:PoseNet单人姿态检测过程

将根据上面的示意图简单介绍 PoseNet 的姿态检测算法. 我们看到,图中标明了网络架构是MobileNet. 实际上,在PoseNet的论文中,研究人员同时训练了ResNet和MobileNet网络. 尽管基于ResNet的模型精确度更高,但对实时应用来说,较大的尺寸和较多的网络层会是页面加载时间和推理时间不够理想. 因此,TensorFlow.js上的PoseNet使用了MobileNet模型.

网络输出关键点热图和偏移向量. 关键点热图用于估计关键点的位置,而偏移向量则用来在热图的基础上进一步预测关键点的精确位置.

和单人姿态估计算法相比,多人姿态估计算法的主要差别在于使用了贪婪方法分组关键点,具体而言,使用了Google在2018年发表的 PersonLab 论文中的高速贪婪解码算法.

Move Mirror 灵感源:

图:Land Lines gif via Awwwards

图:Gesture Match image via the Cooper Hewitt.

2. MoveMirror 构建

Move Mirror 团队选择 PoseNet作为应用背后的模型. 在原型开发阶段,团队通过简单的web API访问PoseNet模型,这极大地简化了原型开发流程. 只需向内部的 PoseNet API 接口发送一个HTTP POST请求,提交base64编码的图像,API就会传回姿态数据(基本无延迟). 若干行JavaScript代码,一份API密钥,搞定!

不过,考虑到不是所有人乐意把自己的图像发送到一个中央服务器,顺便也为了减少对后端服务器的依赖,团队决定把 PoseNet 移植 到TensorFlow.js上. TensorFlow.js 让用户可以在他们自己的浏览器中运行机器学习模型——无需服务器.

2.1. 构建数据集

虽然PoseNet已经解决了姿态估计问题,但为了根据用户的姿态查找匹配的图像,首先要有图像. 图像要符合以下两个要求:

[1] - 多样性 为了更好地匹配用户做出的各种各样的动作,图像的姿态需要尽可能多样化.

[2] - 全身像 从用户体验的一致性出发,决定只使用全身像.

最终,团队选择了包含多种动作,不同体型、肤色、文化的一组视频,将其切分为8万张静止图像. 使用PoseNet处理这些图像,并储存相应的姿态数据.

此外,不是所有的图像都能正确解析姿态,所以丢弃了一些图像.

2.2. 姿态匹配

姿态匹配的挑战在于相似性的定义. 这里主要采用余弦相似性(cosine similarity) 和关键点置信度加权匹配(a weighted match taking into account keypoint confidence scores).

PoseNet的姿态数据包括17个关键点的坐标,以及相应的置信度.

为了匹配关键点的相似度,很自然的一个想法是将17个关键点转换为向量,那么姿态匹配问题就转换为了高维空间中的向量相似性问题. 这一问题有现成的余弦距离方案可用.

图:将JSON格式的关键点数据转换为向量

2.2.1. 余弦相似度

弦相似度测量两个向量的相似程度:基本上,它测量两个向量之间的夹角,如果两个向量方向正好相反,则返回-1,如果两个向量方向一致,则返回1. 重要的是,它只测量向量的方向,而不考虑长度.

图:来源 - Christian Perone.

尽管说的是向量和角度,余弦相似度并不限于直线和图. 例如,余弦相似度可以得到两个字符串的相似度数值( get a numerical similarity between two equal-length strings). (如果曾经使用过Word2Vec,可能已经间接地使用过余弦相似度. )事实上,余弦相似度是一个极其有效的将高维向量的关系约减至单个数字的方法.

图:Nish Tahir’s excellent example.

在将 PoseNet 的输出转化为 34-float 向量后,理论上,即可采用余弦距离计算相似度度量,并得到 [-1, 1] 间的相似度分数.

但是,由于数据库中图片具有不同的尺寸,且每张图片中人体可能位于某个局部(如,左上角、右下角、中间,等等),因此需要在进行一些处理. 这里采用如下两步处理,以保持相似度计算的一致性.

[1] - 缩放 - 根据每个人体的边界框坐标进行裁剪,并将每张图片缩放到固定尺寸(对应的关键点坐标也同样处理);

[2] - 归一化 - 通过将缩放后的关键点坐标作为 L2 正则化向量(各分量的平方和等于1)来进行归一化.

其中,具体而言,采用 L2 normalization 是为了将向量缩放为单位范数,如下图,可以更直观的了解下向量的归一化变换:

可视化以上两个处理步骤:

对于归一化后的关键点坐标(向量数组形式),即可计算向量间的余弦距离:

$$ D(F_{xy}, G_{xy}) = \sqrt{2 * (1 - cosineSimilarity(F_{xy}, G_{xy}))} $$

其中,Fxy 和 Gxy 表示 L2 归一化后的姿态向量. Fxy 和 Gxy 为 17 个关键点的每个点的 x 坐标和 y 坐标.(这里并未考虑每个关键点的置信度.)

javascript 计算如 - moveMirrorCosineDistance.js

// Great npm package for computing cosine similarity  
const similarity = require('compute-cosine-similarity');

// Cosine similarity as a distance function. The lower the number, the closer // the match
// poseVector1 and poseVector2 are a L2 normalized 34-float vectors (17 keypoints each  
// with an x and y. 17 * 2 = 34)
function cosineDistanceMatching(poseVector1, poseVector2) {
  let cosineSimilarity = similarity(poseVector1, poseVector2);
  let distance = 2 * (1 - cosineSimilarity);
  return Math.sqrt(distance);
}

2.2.2. 置信度加权匹配

正如之前提到的,PoseNet的姿态数据包括17个关键点的坐标,以及相应的置信度.

由于 PoseNet 对于每个关键点都一定是 100% 的确定,显然,置信度是很重要的信息.

为了得到更准确的结果,应该给置信度高的关键点较高的权重,给置信度低的关键点较低的权重. 换句话说,增强置信度高的关键点对相似度的影响,削弱置信度低的关键点对相似度的影响.

因此,置信度加权匹配相似度公式:

其中,F 和 G 表示 L2 归一化后的两个姿态向量. $F_{c_{k}}$ 表示 F 的第 k 个关键点的置信度. Fxy 和 Gxy 表示每个向量中第 k 个关键点的 x 坐标和 y 坐标.

javascript 计算如 - moveMirrorWeightedDistance.js

// poseVector1 and poseVector2 are 52-float vectors composed of:
// Values 0-33: are x,y coordinates for 17 body parts in alphabetical order
// Values 34-51: are confidence values for each of the 17 body parts in alphabetical order
// Value 51: A sum of all the confidence values
// Again the lower the number, the closer the distance

function weightedDistanceMatching(poseVector1, poseVector2) {
  let vector1PoseXY = poseVector1.slice(0, 34);
  let vector1Confidences = poseVector1.slice(34, 51);
  let vector1ConfidenceSum = poseVector1.slice(51, 52);

  let vector2PoseXY = poseVector2.slice(0, 34);

  // First summation
  let summation1 = 1 / vector1ConfidenceSum;

  // Second summation
  let summation2 = 0;
  for (let i = 0; i < vector1PoseXY.length; i++) {
    let tempConf = Math.floor(i / 2);
    let tempSum = vector1Confidences[tempConf] * Math.abs(vector1PoseXY[i] - vector2PoseXY[i]);
    summation2 = summation2 + tempSum;
  }

  return summation1 * summation2;
}

加权匹配提供更精确的结果. 即使身体的部分被遮挡或位于画面之外,仍然能够匹配.

2.3. 匹配速度优化

由于数据集中总共有8万张图像,如果采用暴力搜索法,每次匹配需要计算8万次距离. 这对于实时应用来说不可接受.

为了优化匹配速度,需要将8万个姿态数据以某种有序的数据结构存储,这样,匹配的时候就可以跳过那些明显距离很远的姿态数据,从而大大加速匹配进程.

Move Mirror 选用的数据结构是 VP树(vantage-point tree).

2.3.1. VP-Tree

图: from Data Structures for Spatial Data Mining

[1] - 在数据点中选取一点(可以随机选取)作为根节点(上图中为5);

[2] - 绕着5画一个圈,将空间分割成圈内和圈外两部分;

[3] - 接着, 在圈内、圈外各选一点作为制高点(上图中为71);

[4] - 然后,绕着每个制高点各画一个圈,同样在圈内、圈外各选一点……

以此类推.

这里的关键在于,如果从点5开始,然后发现71更接近目标,那么就可以跳过1的所有子节点.

VPTree 实现 - moveMirrorVPTree.js

const similarity = require('compute-cosine-similarity');
const VPTreeFactory = require('vptree');

const poseData = [ […], […], […], …] // an array with all the images’ pose data
let vptree ; // where we’ll store a reference to our vptree

// Function from the previous section covering cosine distance
function cosineDistanceMatching(poseVector1, poseVector2) {
    let cosineSimilarity = similarity(poseVector1, poseVector2);
    let distance = 2 * (1 - cosineSimilarity);
    return Math.sqrt(distance);
}

function buildVPTree() {
  // Initialize our vptree with our images’ pose data and a distance function
  vptree = VPTreeFactory.build(poseData, cosineDistanceMatching);
}

findMostSimilarMatch(userPose) {
  // search the vp tree for the image pose that is nearest (in cosine distance) to userPose
  let nearestImage = vptree.search(userPose);

  console.log(nearestImage[0].d) // cosine distance value of the nearest match

  // return index (in relation to poseData) of nearest match. 
  return nearestImage[0].i; 
}

// Build the tree once
buildVPTree();

// Then for each input user pose
let currentUserPose = [...] // an L2 normalized vector representing a user pose. 34-float array (17 keypoints x 2).  
let closestMatchIndex = findMostSimilarMatch(currentUserPose);
let closestMatch = poseData[closestMatchIndex];

Move Mirror仅仅返回最匹配用户姿态的图像. 不过,通过遍历VP-Tree,不难返回更多结果,比如最接近的10张或20张图像. 这可以用来制作调试工具,发现数据集的问题.

图:MoveMirror调试工具

3. 结语

Move Mirror团队期待能看到更多类似的有趣应用,比如匹配舞蹈动作,匹配经典电影片段. 或者反向操作,基于姿态估计帮助人们在家中练习瑜伽或者进行理疗.

Last modification:June 9th, 2020 at 11:18 am