Paper reading:Fine-Grained Head Pose Estimation Without Keypoints (CVPR2018)
一、 背景
为什么要读这篇论文,因为LZ之前要做头部姿态估计,看到一些传统的方法,都是先进行人脸检测,然后再进行关键点定位,当然现在可以一起做,anyway,得到最后的关键点位置,再使用一个通用的3D人脸模型,通过solvePnP来得到最终的头部姿态,但是不管是脑子中考虑还是最后的动手实践,得到的结论就是这种方式的头部姿态方法不robust。可以想一下:每个人的脸型不一样吧,物管肯定也有差异,3D通用模型也有很多方式,关键点定位也有偏差,这些都是不确定的,只能说当精度要求不高,并且关键点定位足够准确,且头部姿态估计的对象和3D的通用人脸模型相对匹配的情况下,这种方式才比较好,那么问题来了,算法的泛化能力呢。。。
于是乎,还是往深度学习的方法上瞅瞅,就看到了题目中的文章,简单测试了下,觉得效果可行,那么就开始阅读论文和代码吧。
二、 数据集准备
主要使用的数据是300W-LP,下载的地址为: http://www.cbsr.ia.ac.cn/users/xiangyuzhu/projects/3ddfa/main.htm
大概有2.6个G,下载可能需要一段时间,所以有的时候LZ如果确定要尝试一种方法,首先就要开始准备下载数据集,在下载数据集的时候可以在慢慢阅读下论文。
当然这些数据都是合成的,所以有些图片看起来会有点奇怪
三、 训练代码运行的一些问题
1. python2和python3的兼容性问题
LZ用的是python3,原始论文使用的是python2,所以会存在一些兼容性的问题,这些都比较好修改,例如把xrange替换成range这种。
2. pytorch的版本问题
因为是两三年前的代码了,pytorch可能版本比较旧,也会存在一些代码的修改
- utils.py中
# 直接注释掉这一行# from torch.utils.serialization import load_lua- 训练代码以train_hopenet.py为例吧
error:
RuntimeError: Mismatch in shape: grad_output[0] has a shape of torch.Size([1]) and output[0] has a shape of torch.Size([]).solution:
# grad_seq = [torch.ones(1).cuda(gpu) for _ in range(len(loss_seq))]grad_seq=[torch.tensor(1.0).cuda(gpu)for_inrange(len(loss_seq))]error:
IndexError:invalid index of a0-dim tensor.Use tensor.item()to convert a0-dim tensor to a Python numbersolution:
# print('Epoch [%d/%d], Iter [%d/%d] Losses: Yaw %.4f, Pitch %.4f, Roll %.4f'# % (epoch + 1, num_epochs, i + 1, len(pose_dataset) // batch_size, loss_yaw.data[0],# loss_pitch.data[0], loss_roll.data[0]))print('Epoch [%d/%d], Iter [%d/%d] Losses: Yaw %.4f, Pitch %.4f, Roll %.4f'%(epoch+1,num_epochs,i+1,len(pose_dataset)//batch_size,loss_yaw.item(),loss_pitch.item(),loss_roll.item()))运行就没啥问题了但是這個後面得看一下,爲什麼loss會突然增到這麼大。。。
四、测试结果
因为这个算法的流程是要先进行人脸检测,然后在人脸检测框四周扩充一定的范围后进行头部姿态估计的,按照上述的方法,经过测试,确实效果还可以,但是如果是一整张大图,直接回归出头部姿态,这个结果就是非常不准确的了,下面我们来看下代码,看看是否有值得借鉴的信息。
五、代码部分
5.1 训练代码
我们就以train_hopenet.py为例,其他只是换了backbone,原理都是一样的,当然LZ还是小小改动了一下源码
- 一些常规设置
defparse_args():"""Parse input arguments."""parser=argparse.ArgumentParser(description='Head pose estimation using the Hopenet network.')parser.add_argument('--gpu',dest='gpu_id',help='GPU device id to use [0]',default=0,type=int)parser.add_argument('--num_epochs',dest='num_epochs',help='Maximum number of training epochs.',default=5,type=int)parser.add_argument('--batch_size',dest='batch_size',help='Batch size.',default=16,type=int)parser.add_argument('--lr',dest='lr',help='Base learning rate.',default=0.001,type=float)parser.add_argument('--dataset',dest='dataset',help='Dataset type.',default='Pose_300W_LP',type=str)parser.add_argument('--data_dir',dest='data_dir',help='Directory path for data.',default='',type=str)parser.add_argument('--filename_list',dest='filename_list',help='Path to text file containing relative paths for every example.',default='',type=str)parser.add_argument('--output_string',dest='output_string',help='String appended to output snapshots.',default='',type=str)parser.add_argument('--alpha',dest='alpha',help='Regression loss coefficient.',default=0.001,type=float)parser.add_argument('--snapshot',dest='snapshot',help='Path of model snapshot.',default='',type=str)args=parser.parse_args()returnargs- 主函数
5.2 Hopenet部分
classHopenet(nn.Module):# Hopenet with 3 output layers for yaw, pitch and roll# Predicts Euler angles by binning and regression with the expected valuedef__init__(self,block,layers,num_bins):self.inplanes=64super(Hopenet,self).__init__()self.conv1=nn.Conv2d(3,64,kernel_size=7,stride=2,padding=3,bias=False)self.bn1=nn.BatchNorm2d(64)self.relu=nn.ReLU(inplace=True)self.maxpool=nn.MaxPool2d(kernel_size=3,stride=2,padding=1)self.layer1=self._make_layer(block,64,layers[0])self.layer2=self._make_layer(block,128,layers[1],stride=2)self.layer3=self._make_layer(block,256,layers[2],stride=2)self.layer4=self._make_layer(block,512,layers[3],stride=2)self.avgpool=nn.AvgPool2d(7)self.fc_yaw=nn.Linear(512*block.expansion,num_bins)self.fc_pitch=nn.Linear(512*block.expansion,num_bins)self.fc_roll=nn.Linear(512*block.expansion,num_bins)# Vestigial layer from previous experimentsself.fc_finetune=nn.Linear(512*block.expansion+3,3)forminself.modules():ifisinstance(m,nn.Conv2d):n=m.kernel_size[0]*m.kernel_size[1]*m.out_channels m.weight.data.normal_(0,math.sqrt(2./n))elifisinstance(m,nn.BatchNorm2d):m.weight.data.fill_(1)m.bias.data.zero_()def_make_layer(self,block,planes,blocks,stride=1):downsample=Noneifstride!=1orself.inplanes!=planes*block.expansion:downsample=nn.Sequential(nn.Conv2d(self.inplanes,planes*block.expansion,kernel_size=1,stride=stride,bias=False),nn.BatchNorm2d(planes*block.expansion),)layers=[]layers.append(block(self.inplanes,planes,stride,downsample))self.inplanes=planes*block.expansionforiinrange(1,blocks):layers.append(block(self.inplanes,planes))returnnn.Sequential(*layers)defforward(self,x):x=self.conv1(x)x=self.bn1(x)x=self.relu(x)x=self.maxpool(x)x=self.layer1(x)x=self.layer2(x)x=self.layer3(x)x=self.layer4(x)x=self.avgpool(x)x=x.view(x.size(0),-1)pre_yaw=self.fc_yaw(x)pre_pitch=self.fc_pitch(x)pre_roll=self.fc_roll(x)returnpre_yaw,pre_pitch,pre_roll5.3 datasets部分
这里LZ就选择其中的一个数据集Pose_300W_LP来进行解释
classPose_300W_LP(Dataset):# Head pose from 300W-LP datasetdef__init__(self,data_dir,filename_path,transform,img_ext='.jpg',annot_ext='.mat',image_mode='RGB'):self.data_dir=data_dir self.transform=transform self.img_ext=img_ext self.annot_ext=annot_ext filename_list=get_list_from_filenames(filename_path)self.X_train=filename_list self.y_train=filename_list self.image_mode=image_mode self.length=len(filename_list)def__getitem__(self,index):#这个比较重要的是数据处理部分img=Image.open(os.path.join(self.data_dir,self.X_train[index]+self.img_ext))img=img.convert(self.image_mode)mat_path=os.path.join(self.data_dir,self.y_train[index]+self.annot_ext)# Crop the face looselypt2d=utils.get_pt2d_from_mat(mat_path)#这个是从mat中得到对应的68个关键点的坐标x_min=min(pt2d[0,:])y_min=min(pt2d[1,:])x_max=max(pt2d[0,:])y_max=max(pt2d[1,:])# k = 0.2 to 0.40k=np.random.random_sample()*0.2+0.2x_min-=0.6*k*abs(x_max-x_min)y_min-=2*k*abs(y_max-y_min)x_max+=0.6*k*abs(x_max-x_min)y_max+=0.6*k*abs(y_max-y_min)img=img.crop((int(x_min),int(y_min),int(x_max),int(y_max)))# We get the pose in radianspose=utils.get_ypr_from_mat(mat_path)# And convert to degrees.pitch=pose[0]*180/np.pi yaw=pose[1]*180/np.pi roll=pose[2]*180/np.pi# Flip?rnd=np.random.random_sample()ifrnd<0.5:yaw=-yaw roll=-roll img=img.transpose(Image.FLIP_LEFT_RIGHT)# Blur?rnd=np.random.random_sample()ifrnd<0.05:img=img.filter(ImageFilter.BLUR)# Bin valuesbins=np.array(range(-99,102,3))binned_pose=np.digitize([yaw,pitch,roll],bins)-1# Get target tensorslabels=binned_pose cont_labels=torch.FloatTensor([yaw,pitch,roll])ifself.transformisnotNone:img=self.transform(img)returnimg,labels,cont_labels,self.X_train[index]def__len__(self):# 122,450returnself.length数据集中的mat主要包含这几个部分: