最近碰到个项目,要求是实现人脸交换,即如下图所示,将右边汤唯的脸换成左边鹿晗的脸,变成中间的照片,就是人脸交换。
网上一般都是基于opencv和Dlib来实现,且多为c++或python语言,或app,我要用java语言来实现,且为web版本,于是就开始了漫长的资料查找筛选和代码理解、修改过程。
这篇文章主要参考[http://blog.csdn.net/wangxing233/article/details/51771265],作者给出在文章中给出了c++的源码,以及具体步骤和讲解,这篇文章改为java版本并提供源码[https://github.com/ttshen1029/faceswap],主要参考了c++版的代码、java opencv的api,下面是具体步骤。
由于没有找到java版的dlib库,就用百度人脸检测替代了,百度人脸检测接口返回人脸关键点72个,不影响后面的凸包计算和三角剖分。
// 计算凸包
Mat imgCV1Warped = imgCV2.clone();
imgCV1.convertTo(imgCV1, CvType.CV_32F);
imgCV1Warped.convertTo(imgCV1Warped, CvType.CV_32F);
MatOfInt hullIndex = new MatOfInt();
Imgproc.convexHull(new MatOfPoint(points2), hullIndex, true);
int[] hullIndexArray = hullIndex.toArray();
int hullIndexLen = hullIndexArray.length;
List hull1 = new LinkedList<>();
List hull2 = new LinkedList<>();
// 保存组成凸包的关键点
for (int i = 0; i < hullIndexLen; i++) {
hull1.add(points1[hullIndexArray[i]]);
hull2.add(points2[hullIndexArray[i]]);
}
就有如上图中间图片所示(是否可以直接提取凸包包围的整个区域,然后变换角度仿射到目标图片上,需要再研究)。这个部分在调试的过程中遇到了挺多的坑,就分步解析记录下,以免自己忘记。
// 获取Delaunay三角形的列表
public static List delaunayTriangulation(List hull, Rect rect) {
Subdiv2D subdiv = new Subdiv2D(rect);
for(int it = 0; it < hull.size(); it++) {
subdiv.insert(hull.get(it));
}
MatOfFloat6 triangles = new MatOfFloat6();
subdiv.getTriangleList(triangles);
int cnt = triangles.rows();
float buff[] = new float[cnt*6];
triangles.get(0, 0, buff);
List delaunayTri = new LinkedList<>();
for(int i = 0; i < cnt; ++i) {
List points = new LinkedList<>();
points.add(new Point(buff[6*i+0], buff[6*i+1]));
points.add(new Point(buff[6*i+2], buff[6*i+3]));
points.add(new Point(buff[6*i+4], buff[6*i+5]));
Correspondens ind = new Correspondens();
if (rect.contains(points.get(0)) && rect.contains(points.get(1)) && rect.contains(points.get(2))) {
int count = 0;
for (int j = 0; j < 3; j++) {
for (int k = 0; k < hull.size(); k++) {
if (Math.abs(points.get(j).x - hull.get(k).x) < 1.0 && Math.abs(points.get(j).y - hull.get(k).y) < 1.0) {
ind.add(k);
count++;
}
}
}
if (count == 3)
delaunayTri.add(ind);
}
}
return delaunayTri;
}
public static Mat warpTriangle(Mat img1, Mat img2, MatOfPoint t1, MatOfPoint t2) {
Rect r1 = Imgproc.boundingRect(t1);
Rect r2 = Imgproc.boundingRect(t2);
Point[] t1Points = t1.toArray();
Point[] t2Points = t2.toArray();
List t1Rect = new LinkedList<>();
List t2Rect = new LinkedList<>();
List t2RectInt = new LinkedList<>();
for (int i = 0; i < 3; i++) {
t1Rect.add(new Point(t1Points[i].x - r1.x, t1Points[i].y - r1.y));
t2Rect.add(new Point(t2Points[i].x - r2.x, t2Points[i].y - r2.y));
t2RectInt.add(new Point(t2Points[i].x - r2.x, t2Points[i].y - r2.y));
}
// mask 包含目标图片三个凸点的黑色矩形
Mat mask = Mat.zeros(r2.height, r2.width, CvType.CV_32FC3);
Imgproc.fillConvexPoly(mask, list2MP(t2RectInt), new Scalar(1.0, 1.0, 1.0), 16, 0);
// [图1]-- mask
Mat img1Rect = new Mat();
img1.submat(r1).copyTo(img1Rect);
// img2Rect 原始图片适应mask大小并调整位置的图片
Mat img2Rect = Mat.zeros(r2.height, r2.width, img1Rect.type());
// [图2]-- img2Rect
img2Rect = applyAffineTransform(img2Rect, img1Rect, t1Rect, t2Rect);
// [图3]-- img2Rect
Core.multiply(img2Rect, mask, img2Rect); // img2Rect在mask三个点之间的图片
// [图4]-- img2Rect
Mat dst = new Mat();
Core.subtract(mask, new Scalar(1.0, 1.0, 1.0), dst);
Core.multiply(img2.submat(r2), dst, img2.submat(r2));
Core.absdiff(img2.submat(r2), img2Rect, img2.submat(r2));
// [图5]-- img2
return img2;
}
图1
图2
图3
图4
图5
所有的三角形区域都如此处理之后,整张脸就扣到目标图上了。
这个步骤很简单,就一个方法:
Photo.seamlessClone(imgCV1Warped, imgCV2, mask, center, output, Photo.NORMAL_CLONE);
1、必须有下面代码来保证加载opencv库:
static {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}
2、然而,java web项目下直接这么写不行会报错找不到库。于是我是这样处理的:
添加User Libraries,并添加到build path:
然后使用@PostConstruct语法糖,在Spring容器启动的时候去执行加载opencv动态库的操作【参考文章:http://blog.csdn.net/chenxiao_ji/article/details/52807561】
private void addDirToPath(String s) {
try {
//获取系统path变量对象
Field field=ClassLoader.class.getDeclaredField("sys_paths");
//设置此变量对象可访问
field.setAccessible(true);
//获取此变量对象的值
String[] path=(String[])field.get(null);
//创建字符串数组,在原来的数组长度上增加一个,用于存放增加的目录
String[] tem=new String[path.length+1];
//将原来的path变量复制到tem中
System.arraycopy(path,0,tem,0,path.length);
//将增加的目录存入新的变量数组中
tem[path.length]=s;
//将增加目录后的数组赋给path变量对象
field.set(null,tem);
} catch (Exception e) {
e.printStackTrace();
}
}
@PostConstruct
public void init(){
//获取存放dll文件的绝对路径
String path = System.getProperty("webapp.test.root");
System.out.println("######## 路径为 : " + path + " ########");
//将此目录添加到系统环境变量中
addDirToPath(path);
//加载相应的dll文件,注意要将'\'替换为'/'
System.load(path.replaceAll("\\\\","/")+"/opencv_java330.dll");
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
System.out.println("######## Opencv加载完毕 ########");
}
3.在运行过程中发现还是报找不到类的错误,就把opencv.jar解压了直接放在项目目录中来解决这个问题。