一篇关于小白实习的随笔

前言

大一暑假有幸去某某公司的人工智能部门做了一个关于人脸识别的小项目,007的工作的环境让我倍感压力。很多985优秀毕业生和研究生和我共处一个工作室,在他们的带领下,我在一步一步努力前行。

项目简介

为了能够在很短的时间内将大批量的非证件照甚至非规则的照片裁剪成符合要求的并且看着舒服的标准证件照。

这个暑假我在同事和领导的帮助下试着对现有的代码进行了修改和扩充已实现其最优功能。

其实这个项目的主要核心并不多,简单来说分为以下两点:

第一,从一张不规则的照片中抠出只带有人脸的正方形图像

第二,找到最适合的扩大比例,对人脸进行外扩,得到以人脸为中心,四周均有空隙的一张标准证件照

项目基本思想

最开始的思路

由于图片大小不一,所以首先我对各类图片的大小进行了统一,我把照片中长宽较大像素的边且大于等于800像素的边规定为800像素,那么其对应的长或宽则等比例缩小,同理,把长和宽都小于800像素的照片中的较大的边规定为800像素,其对应边同比例扩大。这样就统一了所有不规则尺寸照片的大小了。接下来,对这些照片进行人脸的裁剪,因为这部分函数在我之前已经有人写好了,所以就直接调用了他们的函数,得到了只有人脸的人像。接下来就是对人脸照片的外扩,首先设裁剪出来人脸的宽=fw,高=fh,根据计算,人脸四周需要扩大的长度ext_dist=2/5*fw-(fw>16?4:0)最为合适。这里先把试验基本的参数说明一下,原图片的宽和高分别为img_wimg_h;设原图片中长和宽较小的边min_len=std::min(img_h,img_w);设ext_len=std::min(fw + 2 * ext_dist, min_len).

第一次试验的思路很简单,我们设人脸图片左上顶点的横纵坐标分别为f.x,f.y,则生成的标准证件照的左上顶点的坐标就为ret.x=std::max(0,f.x-ext_dist)ret.y=std::max(0,f.y-ext_dist).也很容易得出标准证件照的宽和高分别为ret.width = (std::min(img_w - ret.x, ext_len))ret.height = (std::min(img_h - ret.y, ext_len)).

进一步思考

这样本以为此次试验的目的就已经达到了,但从测试结果来看,很多图片并没有达到我们预期的效果。分析代码来看,我们以左上顶点为基点,取标准证件照的宽和高时,如果fw+2*ext_dist大于原图片的宽和高,那么这张证件照的边长就取了原图片中较小的边作为证件照的边长,所以这时候就出现了有的证件照没有人下巴的状况了。

为了改善这一现状,我必须继续完善我的代码。在已经可以运行的代码处加入以下代码:

if ((ret.y + fh / 2 + ext_dist) > (ret.height - fh / 2 - ext_dist))

{
int yuantuwid = (img_w - fh) / 2;

ret.x = std::max(0, f.x - ext_dist);
ret.y = std::max(0, f.y - yuantuwid);
ret.width = (std::min(img_w - ret.x, ext_len))&~3;
ret.height = (std::min(img_h - ret.y, ext_len))&~3;

cout << "裁剪前人脸左上" << f.x << " " << f.y << "裁剪前人脸右上" << f.x + fw << " " << f.y << endl;
cout << "裁剪前左下" << f.x << " " << f.y + fh << "裁剪前右下" << f.x + fw << " " << f.y + fh << endl;
cout << "裁剪后左上顶点" << ret.x << " " << ret.y << "裁剪后右上顶点" << ret.x + ret.width << " " << ret.y << endl;
cout << "裁剪后左下顶点" << ret.x << " " << ret.y + ret.height << "裁剪后右下顶点" << ret.x + ret.width << " " << ret.y + ret.height << endl;
}

这段代码的意义大致为,我们不再以人像的左上顶点为基点,而是以人脸的中心点作为基点,向四周进行扩大,已得到最优证件照。这种方法很好的解决了部分证件照没有下巴的问题,并且在运行的过程中可以清楚的看到每一张人像和标准证件照的坐标点。在进行测试的过程中,基本上每张证件照都是人脸完整且居中的,但是我也清楚的发现有的证件照中的人脸图像所占比例比较大,而有的证件照中的人脸图像所占比例就比较小。究其原因分为以下两点,第一,原图片中人脸所占比例就已经很大,无法实现人脸四周分别外扩ext_dist长度的照片;其次,原图片的长宽比例有可能很不均衡,导致裁剪出来的照片人像占比太大。

最终算法

为了能让所有人像占证件照的比例相同,我们不得不在原图的基础上加上一张比原图大的白图,让原图覆盖在较大的白图上,这样便可以使我们的裁剪工作变得更加顺利。首先,根据现有原图的大小来生成一张白图,我们先按原图9/5高和9/5宽的大小来确定白图的宽和高,简单的运用一个for循环和一个if语句,把所有白图存放到一个新的文件夹中去。接下来就是把所有的原图拷贝到白图中去,这个过程的原理大致为,将原图的所有像素点以二维数组的形式存放,运用双重for循环进行逐一拷贝,以得到原图在白图上覆盖的情况。

代码如下:

int height = img.rows;
int width = img.cols;
cv::Mat img_new((int)height* 9.0 / 5, (int)width * 9.0 / 5, CV_8UC3, cv::Scalar(255, 255, 255));
//const char* temp_path = "C:\Users\123\Desktop\1.jpg";
cv::imwrite(dst_path_new, img_new);
int m = width * 2 / 5;
int n = height * 2 / 5;
for (int i = 0; i < height; i++)
{
uchar* d = img.ptr<uchar>(i);
uchar* d_b = img_new.ptr<uchar>(i+n);

for (int j = 0; j < width; j++)
{

int b = d[j * 3];
int g = d[j * 3 + 1];
int r = d[j * 3 + 2];

d_b[j * 3+m*3]= b;
d_b[j * 3 + 1+m*3]=g;
d_b[j * 3 + 2+m*3]=r;

}
}

看一下运行效果:

批量照片的图片在这里就不贴上去了,感兴趣的师傅们可以和我私下交流经验!

项目总结

通过上述代码我们便可以得到一张以原图为中心,四周均为白图的一张较大的图片,在这张图片的基础上我们便可以轻松的根据上边的裁剪代码来得到我们想要得到的完美证件照。经过几万张测试照片的测试,基本上这个程序可以很好的执行证件照裁剪的工作了。

其实这个项目的功能还有很多,比如对人脸性别的检测,人脸质量分数,人脸位置检测,人脸特征识别,以及对高糊图像的识别处理等等一系列算法

下面我贴一下我做的这个项目的main.cpp,还有很多其他代码没有贴,感兴趣的朋友欢迎来和我交流讨论!

#include <time.h>
#include<iostream>
#include <sstream>
#include <string>
#include <vector>
#include <algorithm>



#ifdef linux //获取目录下的指定文件
#include <unistd.h>
#include <dirent.h>
#endif
#ifdef _WIN32
#include <direct.h>
#include <Windows.h>
#include <io.h>
#endif

#include <facesdk.h>
#include <opencv2/opencv.hpp>
using namespace std;

#define OUTPUTMAXSIZE 384



float face_quality(const Image& image, const Face& face);
/**
* @function: 获取cate_dir目录下的所有文件名
* @param: cate_dir - string类型
* @result:vector<string>类型
*/
int GetFilesInDirectory(std::vector<string> &out, const string &directory)
{
if ((&directory) == nullptr || directory.length() == 0)return -1;
HANDLE dir;
WIN32_FIND_DATA file_data;

if ((dir = FindFirstFile((directory + "/*").c_str(), &file_data)) == INVALID_HANDLE_VALUE)
return -1; /* No files found */

do {
const string file_name = file_data.cFileName;
const bool is_directory = (file_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;

if (file_name[0] == '.')
continue;
if (file_name.length() < 5)
continue;

//printf("%s\n", file_data.cFileName);
string file_ext = &file_data.cFileName[file_name.length() - 3];
// printf("%s\n", &file_ext[0]);
if (file_ext[0] >= 'A' && file_ext[0] <= 'Z')
file_ext[0] += 'a' - 'A';
if (file_ext[1] >= 'A' && file_ext[1] <= 'Z')
file_ext[1] += 'a' - 'A';
if (file_ext[2] >= 'A' && file_ext[2] <= 'Z')
file_ext[2] += 'a' - 'A';

if (0 == strcmp(&file_ext[0], "jpg") ||
0 == strcmp(&file_ext[0], "png") ||
0 == strcmp(&file_ext[0], "bmp")
)
{
}
else
{
continue;
}

if (is_directory)
continue;

out.push_back(file_name);
} while (FindNextFile(dir, &file_data));

FindClose(dir);
return out.size();
} // GetFilesInDirectory

Rect ext_face_(int img_h, int img_w, Rect& f)
{
int min_len = std::min(img_h, img_w);
int fw = f.width;
int fh = f.height;
int ext_dist = 2 / 5.0 * fw - (fw > 16 ? 4 : 0);
int ext_len = std::min(fw + 2 * ext_dist, min_len);

Rect ret = { 0 };

ret.x = std::max(0, f.x - ext_dist);
ret.y = std::max(0, f.y - ext_dist);
ret.width = (std::min(img_w - ret.x, ext_len))&~3;
ret.height = (std::min(img_h - ret.y, ext_len))&~3;
cout << "没下巴裁剪后左上顶点" << ret.x << " " << ret.y << "没下巴裁剪后右上顶点" << ret.x + ret.width << " " << ret.y << endl;
cout << "没下巴裁剪后左下顶点" << ret.x << " " << ret.y + ret.height << "没下巴裁剪后右下顶点" << ret.x + ret.width << " " << ret.y + ret.height << endl;
if ((ret.y + fh/ 2+ext_dist)>(ret.height-fh/2-ext_dist))



{
ret.x = std::max(0, f.x - ext_dist);
ret.y = std::max(0, f.y - ext_dist);
ret.width = (std::min(img_w - ret.x, fw + 2 * ext_dist))&~3;
ret.height = (std::min(img_h - ret.y, fw + 2 * ext_dist))&~3;

cout << "裁剪前人脸左上" << f.x << " " << f.y << "裁剪前人脸右上" << f.x + fw << " " << f.y << endl;
cout << "裁剪前左下" << f.x << " " << f.y + fh << "裁剪前右下" << f.x + fw << " " << f.y + fh << endl;
cout << "裁剪后左上顶点" << ret.x << " " << ret.y << "裁剪后右上顶点" << ret.x + ret.width << " " << ret.y << endl;
cout << "裁剪后左下顶点" << ret.x << " " << ret.y + ret.height << "裁剪后右下顶点" << ret.x + ret.width << " " << ret.y + ret.height << endl;
}



return ret;





}
void cut_one_image(IFaceDetector* detector, IFaceMatcher* matcher, const char* src_path, const char* dst_path, const char* failed_path = nullptr)
{
cv::Mat img = cv::imread(src_path);
struct Image m_img;
m_img.data = (char *)img.data;
m_img.height = img.rows;
m_img.width = img.cols;
m_img.format = IMAGE_FORMAT_BGR;
Face face;
int ret = detector->DoDetect(m_img, NULL, face);
if (ret != 1)
{
printf("[E] Cannot detect face! file: %s\n", src_path);
if (failed_path != nullptr)cv::imwrite(failed_path, img);
return;
}
float feature[2048];
bool r = matcher->ExtractFeature(m_img, face, (char*)feature, 2048);
ErrorCode err = matcher->LastError();
float nm = feature[256];
//if(nm <15.5)
// printf("nm ===%f,%d,%d,%d,%d!!!!!!!!!!!!!!!!!\n", nm,r,err, m_img.width, m_img.height);
//else
// printf("nm ===%f,%d,%d,%d,%d\n", nm,r,err, m_img.width, m_img.height);
printf("feature norm:%f\t", nm);
float score = face_quality(m_img, face);

if (nm < 12)
{
score = 15 + nm * 0.01;
// printf("face feature is too poor,%d,%d,%d\n", nm,r,err, m_img.width, m_img.height);
}

if (score < 70)
{
if (score < 12)
{
printf("[E!!!] Face Quality is too low! score:%f[eyes distance must big than 55]\t file:%s\t", score, src_path);
}
else if (score < 15)
{
printf("[E!!!] Face Quality is too low! score:%f[blur value must big than 30]\t file:%s\t", score, src_path);
}
else if (score < 17)
{
printf("[E!!!] Face Quality is too low! score:%f[face feature norm must big than 12]\t file:%s\t", score, src_path);
}
else
{
printf("[E!!!] Face Quality is too low! score:%f[pose score must big than 70]\t file:%s\t", score, src_path);
}
if (failed_path != nullptr)
{
printf("move to %s\n", failed_path);
cv::imwrite(failed_path, img);
}
else
{
printf("\n");
}
return;
}
Rect f = ext_face_(img.rows, img.cols, face.region);
cv::Mat dst = img(cv::Rect(f.x, f.y, f.width, f.height));
if (f.width < OUTPUTMAXSIZE)
{
cv::imwrite(dst_path, dst);
}
else
{
cv::Mat dst2;
cv::resize(dst, dst2, cv::Size(OUTPUTMAXSIZE, OUTPUTMAXSIZE));
cv::imwrite(dst_path, dst2);
}
}
int main(int argc, char** argv) {
const char* inp_dir = nullptr;
const char* out_dir = nullptr;
const char* err_dir = nullptr;
printf("Version:1.0\nBuilt Date:%s\n", __DATE__);
printf("Support:jpg,bmp,png\n");
printf("Usage: %s[input_dir][output_dir][failed_dir]\n", argv[0]);
printf("=========================================================\n");
#if 1
//if (argc != 3 && argc != 4)
//{
// printf("Usage: %s input_dir output_dir failed_dir\n",argv[0]);
// return 0;
//}
if (argc > 1)inp_dir = argv[1]; else inp_dir = "Input";
if (argc > 2)out_dir = argv[2]; else out_dir = "Output";
if (argc > 3)err_dir = argv[3]; else err_dir = "Error";
IFaceDetector* detector = Create(IFaceDetector::DETECTOR_MODE_NORMAL);
IFaceMatcher* matcher = Create(IFaceMatcher::TVT_MATCHER_MODE_19V101_NORMAL);
detector->Init(NULL, NULL);
matcher->Init(NULL, NULL);
vector<string> files;
int fcnt = GetFilesInDirectory(files, inp_dir);
if (fcnt < 0)
{
return 0;
}
CreateDirectory(out_dir, NULL);
CreateDirectory(err_dir, NULL);
int i = 0;
for (auto file_name : files)
{
i++;
printf("\n=========Processing %d%% [%d of %d]:%s==========\n", i * 100 / fcnt, i, fcnt, file_name.c_str());
string src = inp_dir;
src += "/" + file_name;
string dst = out_dir;
dst += "/" + file_name;
if (err_dir != nullptr)
{
string failed = err_dir;
failed += "/" + file_name;
cut_one_image(detector, matcher, src.c_str(), dst.c_str(), failed.c_str());
}
else
{
cut_one_image(detector, matcher, src.c_str(), dst.c_str());
}
}
Destroy(detector);
Destroy(matcher);
#endif
system("pause");
return 0;
}

实习感悟

整个工程到这里基本上是完成了,经过半个月的尝试和无数次的失败,终于在领导和同事的大力帮助下,完成了这个项目。感谢公司给了我这次实习的机会,也很喜欢这里的工作环境,希望我能通过我的努力,一步一步的向各位优秀的哥哥姐姐们靠拢!加油!奥里给!!