一、前言

Face Alignment Network

《How far are we from solving the 2D & 3D Face Alignment problem? (and a dataset of 230,000 3D facial landmarks)》

主要记一下Adrian Bulat这个人。

论文:https://arxiv.org/abs/1703.07332

作者主页:https://www.adrianbulat.com/

刚接触这个领域,尝试从这篇17年的工作开始阅读。

FaceAlignment,直译人脸对齐。(不知道为什么默认就人脸了,似乎其他生物的脸被忽略了)

做人脸对齐的关键步骤在于facial landmark localization,即面部关键点定位。更具体一点还要分2D和3D。

看上去似乎自从16~17达到顶峰之后,研究热度就没那么高了。

想一想iPhone什么时候引入的面部解锁,什么时候直播软件、抖音、美图广泛引入美颜滤镜。脸萌走红又暴死的年份,微信最美制服照活动刷屏的年份。

二、2个前置工作。

2.1 HourGlass沙漏模型

《Stacked hourglass networks for human pose estimation》 ECCV, 2016.

A. Newell, K. Yang, and J. Deng.

https://arxiv.org/pdf/1603.06937.pdf

一图流就是:

当然不懂的人看这个图也不会懂。请结合以下代码和流程图学习,取depth=4。

#pytorch版本1.3.1通过class HourGlass(nn.Module):def __init__(self, num_modules, depth=4, num_features):super(HourGlass, self).__init__()self.num_modules = num_modules#这个参数根本用不到self.depth = depthself.features = num_featuresself._generate_network(self.depth)def _generate_network(self, level):self.add_module('b1_' + str(level), ConvBlock(self.features, self.features))self.add_module('b2_' + str(level), ConvBlock(self.features, self.features))if level > 1:self._generate_network(level - 1) #用递归写感觉上不舒服else:self.add_module('b2_plus_' + str(level), ConvBlock(self.features, self.features))self.add_module('b3_' + str(level), ConvBlock(self.features, self.features))def _forward(self, level, inp):# Upper branchup1 = inpup1 = self._modules['b1_' + str(level)](up1)# Lower branchlow1 = F.avg_pool2d(inp, kernel_size=2, stride=2)low1 = self._modules['b2_' + str(level)](low1)if level > 1:low2 = self._forward(level - 1, low1) #又来递归else:low2 = low1low2 = self._modules['b2_plus_' + str(level)](low2)low3 = low2low3 = self._modules['b3_' + str(level)](low3)up2 = F.interpolate(low3, scale_factor=2, mode='nearest')return up1 + up2def forward(self, x):return self._forward(self.depth, x)

2.2 H,P&MS Block (Hierarchical Parallel and Multi-Scale Block)

名字很浮夸......仿佛看到了英文版的“超级无敌究极宇宙第一”这种前缀。

如其名彰显的,是一种基础的残差块结构,可以类比为Resnet里的Bottleneck。

是Adrian Bulat这个人自己的另一份工作成果,属于“我引用我自己”。

《Binarized Convolutional Landmark Localizers for Human Pose Estimation and Face Alignment with Limited Resources》

https://arxiv.org/pdf/1703.00862v2.pdf

除了他自己好像么见过别人用这种残差块了,所以原论文可以不用读......

很奇怪的是,这个人都是先BatchNorm,再Conv,俺也不知道为什么他要这么做。

核心思路如图,约定好out_channel为某个常数,然后中间层分别输出通道out/2,out/4,out/4,最后concat得到通道数out。

再与经过downsample通道数也为out的input相加。(当且仅当in_channel!=out_channel时,需要downsample)

---插播一个题外话

搜索相关信息的时候正好看到某自媒体X智元的旧闻。

【世界最大人脸对齐数据集】ICCV 2017:距离解决人脸对齐已不远

看来当时的小编没有发现并不是“其他研究人员”,完全是他本人。

三、核心网络

上述2个前置工作与核心网络的关系体现在。

1) A.B.先生把HourGlass沙漏网络里的所有基础块都替换成了他自己H,P,M&S Block。引用自己,这很合理。

2) A.B.先生使用自己的MS Block改装后的全新版本HourGlass,又搭配了几个自己的MS Block,组成了最终的核心网络FAN。这同样很合理,值得学习。这样大家如果引用了FAN就得顺便引用一下HPMSB那篇。所以说提出一种基础block可以做到一次发明终身受益。

核心网络:

开头一段从通道3到通道256的处理,经过一次k=7,stride=2和一次avgpool,缩放1/4。

中间部分是一个循环。

若只有一个工作层,直接输出heatmap_0就结束了。

如果有多个工作层,每2层之间要进行一次“过渡操作”,即取本层特征图(BN结果)卷一下+本层输出(heatmap_i)卷一下+本层输入(input_i),相加的结果,作为下一层输入input_i+1。

最终得到k张heatmap。

四、数据处理的关键步骤

4.1 target_heatmap的获得步骤

约定好输入图片的尺寸size=256,heatmap的尺寸heat=64,正好形成1/4关系。

获得原始图像的高宽,h,w,c=img.shape

缩放原始图像 img = cv2.resize(img, (size, size)) #到(size,size)

等比例缩放标注landmark*=np.array([heat/w,heat/h]),相当于把标注缩到了(size/4,size/4)的图片上。

然后heatmap = np.zeros((heat,heat,n_landmarks))

再根据landmark的位置,将对应方格置为1。

于是得到了target_heatmap。

上文说过经核心网络FAN,输出的pred_heatmap尺寸正好是(size/4,size/4) =(heat,heat),

跟我们利用标注生成的target尺寸一致。

于是可以进行逐pixel的MSE计算。

for heatmap in heatmaps:#逐pixel与标注的landmarks计算MSEloss += get_MSE(heatmap,target)

输入数据的处理思想,就是无脑resize到一个正方形,例如源代码的默认值size=256。

当然实际上还有一些旋转,噪声,翻转,这些属于锦上添花。

这样坏处显而易见,对一些长宽比不接近1:1的图片,无脑resize可能反而会破坏图像原本的形状特征导致不可识别。

即这种模型默认了输入是接近正方形的。

仗着训练时有bbox帮助crop,人的头部作为类球体,任意角度拍摄都应该含于近似正方形的框。

这很合理。但我不喜欢。也不方便迁移。

如果没有bbox让你crop,直接16:9整张图片输入,还能resize到正方形吗?

4.2 对prediction的处理

按源代码,预测的时候,我们对k张输出的heatmap,只取最后一张。(这同样很合理)

prediction = output[-1]  #(bz,n_ldmk,heat,heat)

然后采用取找最大值的方式,从heatmap中拿到landmark

#这似乎是原作者采用的函数
#heatmap通道数是n_landmark
#每个通道即submap中取数值最大的点,作为一个landmark预测值。
#总计获得n_landmark个(x,y)坐标。
#对上ratio缩放
def get_pts(heatmaps, ratio):## Get landmarks from heatmaps with scaling ratios #### heatmaps: N x L x H x H #### ratio: N ### Get x and y of landmarkslmx = torch.argmax(torch.max(heatmaps, dim=2)[0], dim=2).type(torch.cuda.FloatTensor)lmy = torch.argmax(torch.max(heatmaps, dim=3)[0], dim=2).type(torch.cuda.FloatTensor)# Stack them and scale with ratioslandmarks = torch.stack((lmx,lmy), dim=2)landmarks *= ratioreturn landmarks

4.3 evaluation做法

基于Heatmap的pose estimation似乎已经广泛应用了。

这种算法的本质还是CNN的老问题,size不敏感,这导致了对位置信息的读取还是不够。

所以不能通过几个FC来直接输出2个int型变量x,y。我试了一下当成纯回归来做,效果非常差。

改用heatmap之后就变成了“另类”的分类问题。在一张pred_heatmap找一个预测的概率值最大的点,输出的就是概率值而不是回归值。

最终x,y来自点的坐标,而非网络的输出本身。

所以训练的时候不可能拿x,y的坐标去算loss,因为x,y身上不绑定梯度,没法BP。

但是,最终evaluation的计算却又可以当成回归来算NME,因为eval过程不需要BP,可以提取x,y坐标值,与landmark标注坐标算距离。

这很灵性。

图像任务或者说CNN的特殊性导致了train和eval可以用2种截然不同思路的效果衡量方式。

#eval过程的评价指标计算
def NME(preds, targets, boxes):## Compute Normalized Mean Error using predicted heatmaps, ground-truth landmarks and bounding boxes #### preds: N x L x H x H; N = batch size; L = number of landmarks; H = heatmap dimension## targets: N x L x 2; [x y]## boxes: N x 2 x 2; [top-left bottom-right]; [x y]D = torch.squeeze(boxes[:, 1] - boxes[:, 0] + 1, dim=1)  # N x 2w, h = D[:, 0], D[:, 1] RAs = torch.sqrt(w*h)  # Square root of box area, Nif len(preds.shape)>3 :ratios = D.unsqueeze(dim=1)/preds.shape[2]  # scale, from 64 to w, hpreds = get_pts(preds, ratios)  # convert heatmaps to landmarksmse = torch.sqrt((preds - targets)**2).sum(2) # L2-norm / sum-squared error; N x Lnme = torch.mean(mse/RAs.reshape(-1,1)) # N x L divide Nreturn float(nme) 

4.4 预测结果修正

由于FAN是用original_img*scale -> input_img(H,W)  经过模型-> heatmap(H/4,W/4) -> 得到pred_landmark (x,y)

在低尺寸上预测出坐标(x,y),而实际场景中还需要还原到original_img上。

最简单的方式是 (x,y)*4/scale。

但是这样会带来精度损失

例如原图上的坐标(15,15)和(12,12),经过scale=1/4后,再取整,都会得到(3,3)。

而我们利用一个预测出来的(3,3),若简单地*4,得到(12,12),可能会偏左上方一点,实际坐标值也许是(15,15)。

如下图所示,原图蓝色区域内的任意一个方块,都可以浓缩到小尺度heatmap的坐标(3,3)方块上。

而我们不知道(3,3)这个方块,在蓝色区域[12~15,12~15]内的offset。

表现在具体任务上。

可以看到offset信息的缺失,会让结果产生巨大的偏差,极大的影响准确率。

尤其是缩放比例scale越大的情况下,最终误差越大。

设缩放倍数= n,n为正整数,则从x=3可以映射的范围是[3*n,3*n+(n-1)],最大的坐标偏差值max_diff = n-1。

怎么解决offset这个face alignment领域的“最后一公里”问题呢?

问题定义:我们希望对每一个预测关键点pred_landmark_i =(x,y),得到一个offset_i=(x_offset_i,y_offset_i),

使得gt_landmark == pred_landkmark_i + off_set_i 。

原作者A.B.先生的代码里这样处理的。

对于预测出来的(pred_x,pred_y),提取heatmap上其左右两个相邻点,即点(pred_x-1,pred_y)与点(pred_x+1,pred_y)。

再提取上下两个相邻点,即(pred_x,pred_y-1)与(pred_x,pred_y+1)。

然后右减左,下减上,分别通过示性函数sign_(),若大于0得到1,小于0得到-1。

最后乘以补偿系数0.25。即,如果heatmap上右侧概率值大于左侧,则pred_x += 0.25*1,反之 pred_x += 0.25*(-1)。

pred_y同理处理。

def get_preds_fromhm(hm, center=None, scale=None):"""Obtain (x,y) coordinates given a set of N heatmaps. If the centerand the scale is provided the function will return the points also inthe original coordinate frame.Arguments:hm {torch.tensor} -- the predicted heatmaps, of shape [B, N, H, W]Keyword Arguments:center {torch.tensor} -- the center of the bounding box (default: {None})scale {float} -- face scale (default: {None})"""hi = hm.shape[2] wi = hm.shape[3]max, idx = torch.max(hm.view(hm.size(0), hm.size(1), hm.size(2) * hm.size(3)), 2)idx += 1preds = idx.view(idx.size(0), idx.size(1), 1).repeat(1, 1, 2).float()preds[..., 0].apply_(lambda x: (x - 1) % hm.size(3) + 1)preds[..., 1].add_(-1).div_(hm.size(2)).floor_().add_(1)#polishfor i in range(preds.size(0)):for j in range(preds.size(1)):hm_ = hm[i, j, :]pX, pY = int(preds[i, j, 0]) - 1, int(preds[i, j, 1]) - 1if pX > 0 and pX < wi-1 and pY > 0 and pY < hi-1:diff = torch.FloatTensor([hm_[pY, pX + 1] - hm_[pY, pX - 1],hm_[pY + 1, pX] - hm_[pY - 1, pX]])preds[i, j].add_(diff.sign_().mul_(.25))preds.add_(-.5)preds_orig = torch.zeros(preds.size())if center is not None and scale is not None:for i in range(hm.size(0)):for j in range(hm.size(1)):preds_orig[i, j] = transform(preds[i, j], center, scale, hm.size(2), True)return preds, preds_orig

我测试了这个函数,在“最后一公里”问题上效果不是很好。

这个方法的缺点首先在于,补偿系数是完全固定的。

我们有nlandmarks个关键点,每个点的offset都一样吗?这显然不太合理。

其次,最后一公里问题上应该是不存在负值的,全部offset应该都是相对于 (x*n,y*n)的偏移。

但我们可以借鉴A.B.先生的这种思想。

考虑一种极端情况,我们想预测一只眼睛的landmark,它真实位于原图的(16,16)处。

那么缩小后, 对应点(4,4)。 而左侧点(4,3)占有的相关信息量,即预测的heatmap值,应该无限趋近点(4,4)。

即heatmap[4,3]/heatmap[4,4] 趋于1时,offset_x趋于0。此时heatmap[4,5]/heatmap[4,4]趋于0。

另一种极端情况是,左侧点heatmap[4,3]趋于0,而右侧点heatmap[4,5]近似等于heatmap[4,4]。

此时应该认为offset_x趋于n。

还有一种就是左右点占有信息量近似相等,则认为offset=0.5*n。

我们提炼一下规则。

左1右0,offset=0;

左右相等,offset = 0.5n;

左0右1,offset=n;

写成算法形式

设左侧点hm_left,右侧点hm_right,中间点hm_center。

offset_x_default = 0.5*n,

diff_percent = (hm_right/hm_center-hm_left/hm_center) = (hm_right-hm_left)/hm_center,范围(-1,1)

offset_x = offset_x_default +diff_percent*0.5*n,

=0.5*n+diff_percent*0.5*n

= 0.5*n(1+diff_percent),范围(0,n)

这看起来很合理。

这样得到的offset带小数,正是我所期望的。

如果要打印在图片上,自然需要先取整。

但是如果用来打比赛刷排行榜,有小数就非常重要了,有效逼近精度。

示例代码

def get_landmarks(heatmaps, ratio):# Get landmarks from heatmaps with scaling ratios# heatmaps: Batchsize x N_landmarks x H_h x H_w# ratio: n = original_size / heatmap_size# select one max-point from each heatmap # qqH_h,H_w = heatmaps.shape[2],heatmaps.shape[3]# Get x and y of landmarkslmx = torch.argmax(torch.max(heatmaps, dim=2)[0], dim=2) #(b,n_landmk)lmy = torch.argmax(torch.max(heatmaps, dim=3)[0], dim=2) #(b,n_landmk)# Stack themlandmarks = torch.stack((lmx,lmy), dim=2) #(b,n_landmk,2)# Cal offsetoffsets = torch.zeros(heatmaps.shape[0],heatmaps.shape[1],2).float() #(b,n_landmk,2)offset_default = 0.5*ratio #0.5n as defaultfor i in range(heatmaps.shape[0]):for j in range(heatmaps.shape[1]):hm_ = heatmaps[i,j,:,:] #(H_h,H_w)px,py = landmarks[i,j,0],landmarks[i,j,1]hm_center = hm_[py,px]if px>0 and py>0 and py<H_h-1 and px<H_w-1:diff_percent_x = (hm_[py,px+1]-hm_[py,px-1])/hm_centerdiff_percent_y = (hm_[py+1,px]-hm_[py-1,px])/hm_centeroffsets[i,j,0] = (diff_percent_x+1)*offset_defaultoffsets[i,j,1] = (diff_percent_y+1)*offset_defaultelse:#out of bound,set to defaultoffsets[i,j,:] += offset_default# Scale with ratiolandmarks = landmarks.float()*ratio# Add offsetlandmarks += offsetsreturn landmarks

FAN- Face Alignment Network相关推荐

  1. 图像文字识别初探(二)-FAN(Focusing Attention Network)

    图像文字识别初探(一)-CRNN(Convolution Recurrent Neural Network)和DTRN(Deep-text Recurrent Network) 图像文字识别初探(二) ...

  2. [论文翻译]Pedestrian Alignment Network for Large-scale Person Re-Identification

    传送门: https://arxiv.org/pdf/1707.00408.pdf https://github.com/layumi/Pedestrian_Alignment 摘要 Person r ...

  3. 人脸关键点:DAN-Deep Alignment Network: A convolutional neural network for robust face alignment

    DAN-Deep Alignment Network,发表于CVPR-2017.很纳闷DAN取名中的D,为什么是deep,如果是深度学习的deep,岂不是很无区分性?有知道的朋友请告诉我这个D是什么意 ...

  4. 论文笔记004-《Knowledge Graph Alignment Network with Gated Multi-hop Neighborhood Aggregation》

    更多博客可以关注MyBlog,欢迎大家一起学习交流! 1. 简介 题目:<Knowledge Graph Alignment Network with Gated Multi-hop Neigh ...

  5. 用大白话讲Single-shot Alignment Network(S2A-NET)

    文章目录 前言 本文是对论文:Align Deep Features for Oriented Object Detection 的学习总结,通过个人理解,尽可能以通俗易懂的方式表述,以方便大家的理解 ...

  6. Knowledge Graph Alignment Network with Gated Multi-Hop Neighborhood Aggregation-学习笔记

    在图1中,美国是Wikidata中科比·布莱恩特的一跳(直接)邻居. 但是在DBpedia中,它是两跳邻居. 在AliNet中,通过门控机制通过在k跳内对其邻域信息进行控制的聚合来学习实体表示. 在不 ...

  7. Deep Alignment Network(人脸对齐)

    一,DAN 由于使用了关键点热力图的可视化信息,故可以将整张图输入网络. 网络分为多个阶段(STAGE),每个阶段的结构都是相同的(STAGE 1除外).第一阶段的输入仅有原始图片,和S0.面部关键点 ...

  8. 论文笔记:Multi-level Alignment Network for Domain Adaptive Cross-modal Retrieval

    域自适应跨模态检索的多级对齐网络 摘要 介绍 材料与方法 域自适应跨模态检索 网络体系结构 多级对齐 语义对齐 跨域对齐 跨模态对齐 联合训练与推理 结论 摘要 跨模态检索是多媒体领域中一项重要而富有 ...

  9. Paper:《How far are we from solving the 2D 3D Face Alignment problem? 》解读与翻译

    Paper:<How far are we from solving the 2D & 3D Face Alignment problem? >解读与翻译 目录 How far a ...

最新文章

  1. linux7怎样搭建zabbix,Centos7.0 搭建Zabbix环境
  2. java 监听 循环_java循环按键循环监听事件
  3. hibernate 最新 jar 下载
  4. AMD的AI策略与Intel和Nvidia有何不同?
  5. CLAMAV 杀毒软件安装及使用配置
  6. MyBatis中SQL语句相关内容
  7. Can you find it(HDU-5478)
  8. 对于String类型的深刻理解
  9. 计算机考试换机密码,Ami换机,让你轻松转移手机资料!
  10. 三道题套路解决递归问题
  11. 判定考试成绩程序java_请大家务必按照平时交作业的要求,按时提交!否则会影响您的期末考试成绩。_学小易找答案...
  12. Xadmin 常用插件
  13. 除了人工智能,霍金还担心“游牧外星人”会摧毁人类
  14. cin、cin.get()、cin.getline()、getline()、gets()等函数的用法 (转)
  15. 约会安排HDU - 4553
  16. 摩游世纪CEO宋啸飞:Html5增长趋势已可见
  17. 树莓派Raspberry pi 4B 运行 WuKong-Robot 智能语音对话机器人
  18. 2008考研数学辅导讲义理工类高等数学部分-蔡燧林
  19. U盘量产失败后无法找驱动U盘的解决方法。
  20. 服务器ftp查看文件,ftp命令查看文件列表 - 卡饭网

热门文章

  1. vtk中长度测量和角度测量
  2. 超级计算机-虚拟实验室,Mira超级计算机“最后的旅程”:阿贡实验室进行了一次超大规模宇宙模拟...
  3. 使用Tensorflow训练一元线性模型
  4. 联想拯救者y7000充电一闪一闪,接触不良
  5. Java23种设计模式——19.行为型模式之中介者模式
  6. Magento支付宝手机网站支付插件V6.0旗舰版发布,支持在微信中使用支付宝支付,订单重新支付功能!...
  7. WMS仓库管理系统---(16)订单管理--订单打印
  8. 松下PLC FP-XHC60T 程序 两个PLC通信控制11个轴 程序稳定已批量生产 注释完整 带威纶通触摸屏程序
  9. Python爬虫获取“房天下“房价数据(下)
  10. 黑马程序员--java基础--异常(二)