paper:https://arxiv.org/ftp/arxiv/p...
code:https://github.com/Star-Cloud...
1. Introduction
先前的基于anchor的方法具有一些缺点,首先,为了改善anchor box与ground truth之间的重叠,人脸检测器通常需要大量的密集anchor以实现良好的召回率。例如,对于640×640输入图像,RetinaFace中有超过10万个anchor box。另外,anchor是根据特定数据集进行统计计算的超参数设计,如果需要训练新的数据集,那么需要重新计算anchor,这与算法的通用性背道而驰。
本文的贡献主要有以下四点:
通过引入无anchor设计,与以前的检测器相比,仅使用较大的输出分辨率(输出步幅为4),将面部检测转换为标准的关键点估计问题。
基于多任务学习策略,提出了以人脸为点的设计,可以同时预测人脸框和五个关键点。
本文提出了使用通用图层的特征金字塔网络,以进行准确,快速的人脸检测。
基于流行的基准FDDB和WIDER FACE以及CPU和GPU硬件平台的综合实验结果证明了该方法在速度和准确性方面的优越性、
2. Related Works
2.1 Cascaded CNN methods
1)这些检测器的运行时间与输入图像上的人脸数量呈负相关。当人脸数量增加时,速度将急剧下降。2)因为这些方法分别优化了每个模块,所以训练过程变得极为复杂。
2.2 Anchor methods
我们的CenterFace只是通过其边界框中心的一个点来简单地表示人脸,然后直接从中心位置的图像特征中回归出脸框的大小和界标。这样,人脸检测就变成了标准的关键点估计问题,而且可以大幅缩短训练时间。
2.3 Multitask Learning
类似地,Mask RCNN采用mask分支来显著提高检测性能,联合人脸检测和关键点回归被广泛使用,因为与骨干平行的对齐任务为具有人脸点信息的人脸分类任务提供了更好的功能。
3. CenterFace
3.1 Mobile Feature Pyramid Network
我们采用Mobilenetv2作为主干,采用特征金字塔网络(FPN)作为后续检测的neck,所有金字塔等级都有C = 24个通道,网络架构如下图所示。
3.2 Face as Point
我们假设人脸框的坐标为[x1, y1, x2, y2],那么面部中心点的坐标为[(x1+y1)/2,(x2+y2)/2],输入图像大小是W H,得到的heatmap为W/R H/R,R是网络下采样的倍数,论文中取值为4,预测Y=1表示存在人脸,Y=0表示背景。
对于每一个ground truth,我们通过高斯核来计算得到实际训练使用的ground truth,实际训练采用的是focal loss,公式如下,α 和 β是超参数,文中分别取2和4。
网络为了获得全局信息并减少内存使用,对图像进行卷积下采样,输出的大小通常小于图像。因此,图像中的位置(x,y)映射到热图中的位置(x / n,y / n),其中n是下采样因子。当我们将位置从热图重新映射到输入图像时,某些像素可能未对齐,这会极大地影响人脸框的准确性。为了解决此问题,我们预测位置偏移以在将中心位置重新映射到输入分辨率之前稍微调整中心位置:
3.3 Box and Landmark Prediction
我们的目标是学习一种将网络位置输出(hˆ,wˆ)映射到特征图中中心位置(x,y)的变换,box的回归为:
与Box回归不同的是,五个面部关键点的回归采用基于中心位置的目标归一化方法:
针对box回归和landmark回归,采用L1 loss进行计算:
3.4 Training Details
Data augmentation:数据扩充对于提高泛化性很重要。我们使用原始图像的随机翻转,随机缩放,色彩抖动和随机裁剪正方形补丁,并将这些补丁的大小调整为800 * 800,以生成更大的训练脸,且小于8像素的人脸将直接丢弃。
4. Experiments
4.1 Running Efficiency
CenterFace模型大小仅为7.2M,在GTX 2080Ti上能达到(>100FPS)的速度。
在CPU上可以达到30FPS的速度。
4.2 Evaluation on Benchmarks
在WIDER_FACE验证集上的表现:
在WIDER_FACE测试集上的表现:
5. Conclusion
我们提出的方法通过将人脸检测和对齐转换为标准关键点估计问题,克服了以前基于anchor的方法的缺点。CenterFace通过脸部框的中心点表示脸部,然后从中心位置的图像特征直接回归脸部大小和脸部关键点。
后续:
论文中给出的github提供了一些模型,我把其中的onnx模型转换成caffe模型,并在PC端进行验证,进一步转换为海思的nnie模型,并在板子上实现后处理过程,每一个步骤都包含注释,运行成功并检测到人脸。
附上转换好的prototxt和caffe模型链接:https://github.com/wanglaotou/CenterFace_caffe
板端后处理代码:
// 得到CenterFace的输出layout
((nnie::CNN *)pNnieModel)->Run(pSrcSvpImage, layerout);
for (int j = 0; j < layerout.size(); j++)
{
int dtn = layerout[j].u32Num;
int dtw = layerout[j].unShape.stWhc.u32Width;
int dth = layerout[j].unShape.stWhc.u32Height;
int dtc = layerout[j].unShape.stWhc.u32Chn;
int dtsd = layerout[j].u32Stride;
int dten = layerout[j].enType;
HI_U8 * pResBlob = (HI_U8 *)(layerout[j].u64VirAddr);
// 方案三:根据heatmap找到对应的下标然后再去保存scale,offset和landmark的值
int ddd = dth*dtsd;
for (int n = 0; n < dtn; n++)
{
for (int c = 0; c < dtc; c++)
{
for (int h = 0; h < dth; h++)
{
for (int w = 0; w < dtw; w++)
{
float val = *((HI_S32 *)(pResBlob + n*dtc*ddd + c*ddd + h*dtsd + w*sizeof(HI_S32)))/4096.0;
// printf("%f ", val);
if (j == 0){
if (val > threshold){
hm[h][w] = val;
c0.push_back(h);
c1.push_back(w);
}
}
}
}
for (size_t i=0; i < c0.size(); i++){
int sx = c0[i];
int sy = c1[i];
float nval = *((HI_S32 *)(pResBlob + n*dtc*ddd + c*ddd + sx*dtsd + sy*sizeof(HI_S32)))/4096.0;
if (j == 1)
{
if (c==0){
scale0[sx][sy] = nval;
}
else{
scale1[sx][sy] = nval;
}
}
else if (j == 2)
{
if (c==0){
offset0[sx][sy] = nval;
}
else{
offset1[sx][sy] = nval;
}
}
else if (j == 3)
{
landmark[c][sx][sy] = nval;
}
}
}
}
printf("\n");
}
// 解析检测的人脸框坐标
// printf("c0.size and c1.size:%d, %d.\n", c0.size(), c1.size());
for (size_t i=0; i < c0.size(); i++){
cv::Rect rt;
cv::Point pt;
std::vector<cv::Point> pts;
int sx = c0[i];
int sy = c1[i];
float s0 = exp(scale0[sx][sy]) * 4; // 高
float s1 = exp(scale1[sx][sy]) * 4; // 宽
float o0 = offset0[sx][sy]; // 高
float o1 = offset1[sx][sy]; // 宽
float s = hm[sx][sy];
float x1 = ((sy+o1+0.5)*4-s1/2) > 0 ? ((sy+o1+0.5)*4-s1/2):0;
float y1 = ((sx+o0+0.5)*4-s0/2) > 0 ? ((sx+o0+0.5)*4-s0/2):0;
x1 = x1 < img_w_new ? x1:img_w_new;
y1 = y1 < img_h_new ? y1:img_h_new;
float x2 = (x1 + s1) < img_w_new ? (x1 + s1):img_w_new;
float y2 = (y1 + s0) < img_h_new ? (y1 + s0):img_h_new;
float w = x2 - x1 + 1;
float h = y2 - y1 + 1;
rt = cv::Rect(x1, y1, w, h);
boxes.push_back(rt);
scores.push_back(s);
for (int j=0; j<5; j++){
// int lx = (landmark[0][j*2+1][sx][sy] * s1 + x1);
// int ly = (landmark[0][j*2][sx][sy] * s0 + y1);
int lx = (landmark[j*2+1][sx][sy] * s1 + x1);
int ly = (landmark[j*2][sx][sy] * s0 + y1);
pt = cv::Point(lx, ly);
pts.push_back(pt);
}
lms.push_back(pts);
}
// 根据得分阈值和设定阈值进行nms过程
int num_dets = boxes.size();
bool flags[num_dets] = { 0 };
std::vector<int> indexs;
std::vector<float> res = GetIndexBySortValue(scores);
for (size_t _i=0; _i < boxes.size(); _i++){
float nms_thre = 0.3;
int i = res[_i];
if (flags[i]){
continue;
}
indexs.push_back(i);
cv::Rect ibox = boxes[i];
int ix1 = floor(ibox.x);
int iy1 = floor(ibox.y);
int ix2 = ceil(ibox.x + ibox.width);
int iy2 = ceil(ibox.y + ibox.height);
float iarea = ibox.width * ibox.height;
for (size_t _j=_i+1; _j < boxes.size(); _j++){
int j = res[_j];
cv::Rect jbox = boxes[j];
if (flags[j]){
continue;
}
int mx1 = ix1 > floor(jbox.x) ? ix1:floor(jbox.x);
int my1 = iy1 > floor(jbox.y) ? iy1:floor(jbox.y);
int mx2 = ix2 < ceil(jbox.x + jbox.width) ? ix2:ceil(jbox.x + jbox.width);
int my2 = iy2 < ceil(jbox.y + jbox.height) ? iy2:ceil(jbox.y + jbox.height);
float jarea = jbox.width * jbox.height;
int w = (mx2 - mx1 + 1) > 0 ? (mx2 - mx1 + 1):0;
int h = (my2 - my1 + 1) > 0 ? (my2 - my1 + 1):0;
int inter = w * h;
float ovr = inter / (iarea + jarea - inter);
if (ovr > nms_thre){
flags[j] = true;
}
}
}
// 保留经过nms之后的框坐标和得分
std::vector <cv::Rect> kpboxes;
// std::vector <cv::Point> kppoints;
std::vector<std::vector<cv::Point> > kppoints;
std::vector <float> kpscores;
for (size_t i=0; i < indexs.size(); i++){
cv::Rect rt = boxes[indexs[i]];
std::vector<cv::Point> pt = lms[indexs[i]];
float s = scores[indexs[i]];
kpboxes.push_back(rt);
kppoints.push_back(pt);
kpscores.push_back(s);
}
// 还原到原图上的框坐标
std::vector <cv::Rect> scboxes;
std::vector<std::vector<cv::Point> > scpoints;
for (size_t i=0; i < kpboxes.size(); i++){
// box框坐标
cv::Rect rt = kpboxes[i];
float x1 = rt.x / scale_w;
float y1 = rt.y / scale_h;
float x2 = (rt.x + rt.width) / scale_w;
float y2 = (rt.y + rt.height) / scale_h;
float scw = x2 - x1 + 1;
float sch = y2 - y1 + 1;
cv::Rect nrt = cv::Rect(x1, y1, scw, sch);
scboxes.push_back(nrt);
// landmark点坐标
std::vector<cv::Point> pts = kppoints[i];
std::vector<cv::Point> spt;
for (int j=0; j<5; j++){
int lx = (pts[j].x);
int ly = (pts[j].y);
cv::Point pt = cv::Point(lx, ly);
spt.push_back(pt);
}
scpoints.push_back(spt);
}
// 将检测的人脸框和得分值画在图像上面
for (size_t i=0; i < kpboxes.size(); i++){
float drawFactor = 1.0;
cv::Rect rt = kpboxes[i];
int kx1 = floor(rt.x);
int ky1 = floor(rt.y);
int kw = ceil(rt.width);
int kh = ceil(rt.height);
cv::Rect rect = cv::Rect(kx1, ky1, kw, kh);
cv::rectangle(tmpImg, rect, cv::Scalar(255, 255, 255), drawFactor, 8, 0);
std::vector<cv::Point> pts = scpoints[i];
for (int j=0; j<5; j++){
int lx = (pts[j].x);
int ly = (pts[j].y);
cv::Point pt = cv::Point(lx, ly);
cv::circle(tmpImg, pt, 3, CV_RGB(0, 0, 255), -1);
}
char str[512] = "";
sprintf(str, "%.3f", kpscores[i]);
cv::Point pt(kx1, ky1-10);
cv::putText(tmpImg, str, pt, CV_FONT_HERSHEY_SIMPLEX, 0.5f*drawFactor, CV_RGB(255, 255, 255), drawFactor);
}
作者:Mario
文章来源:知乎
推荐阅读
更多芯擎AI开发板干货请关注芯擎AI开发板专栏。欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。