java opencv 实现换脸

最近碰到个项目,要求是实现人脸交换,即如下图所示,将右边汤唯的脸换成左边鹿晗的脸,变成中间的照片,就是人脸交换。

java opencv 实现换脸_第1张图片

网上一般都是基于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,下面是具体步骤。

1. 人脸关键点检测

由于没有找到java版的dlib库,就用百度人脸检测替代了,百度人脸检测接口返回人脸关键点72个,不影响后面的凸包计算和三角剖分。

2. 计算凸包

// 计算凸包
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]]);
}

java opencv 实现换脸_第2张图片
注:该图来自原作

3. 三角剖分和变换仿射

就有如上图中间图片所示(是否可以直接提取凸包包围的整个区域,然后变换角度仿射到目标图片上,需要再研究)。这个部分在调试的过程中遇到了挺多的坑,就分步解析记录下,以免自己忘记。

// 获取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
java opencv 实现换脸_第3张图片
图5
所有的三角形区域都如此处理之后,整张脸就扣到目标图上了。

4. 无缝融合

这个步骤很简单,就一个方法:

Photo.seamlessClone(imgCV1Warped, imgCV2, mask, center, output, Photo.NORMAL_CLONE);

5.备注

1、必须有下面代码来保证加载opencv库:

static {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

2、然而,java web项目下直接这么写不行会报错找不到库。于是我是这样处理的:
添加User Libraries,并添加到build path:
java opencv 实现换脸_第4张图片
java opencv 实现换脸_第5张图片

然后使用@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解压了直接放在项目目录中来解决这个问题。

java opencv 实现换脸_第6张图片

你可能感兴趣的:(接触新世界)