java opencv 实现换脸

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

人脸交换示例图

网上一般都是基于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<Point> hull1 = new LinkedList<>();
List<Point> hull2 = new LinkedList<>();

// 保存组成凸包的关键点
for (int i = 0; i < hullIndexLen; i++) {
    hull1.add(points1[hullIndexArray[i]]);
    hull2.add(points2[hullIndexArray[i]]);
}

计算凸包
注:该图来自原作

3. 三角剖分和变换仿射

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

// 获取Delaunay三角形的列表
public static List<Correspondens> delaunayTriangulation(List<Point> 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<Correspondens> delaunayTri = new LinkedList<>();
        for(int i = 0; i < cnt; ++i) {
            List<Point> 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<Point> t1Rect = new LinkedList<>();
         List<Point> t2Rect = new LinkedList<>();
         List<Point> 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
所有的三角形区域都如此处理之后,整张脸就扣到目标图上了。

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:
这里写图片描述
这里写图片描述

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

这里写图片描述

  • 0
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
运动跟踪是计算机视觉中的一个重要应用,OpenCV是一款常用的计算机视觉库,可以用Java语言进行开发。下面是Java OpenCV实现运动跟踪的基本步骤: 1. 导入OpenCV库和JavaCV库。 2. 读取视频或者摄像头捕获的实时图像。 3. 将连续的帧转换为灰度图像,并使用高斯滤波器进行模糊处理。 4. 计算当前帧和上一帧之间的差异图,并进行二值化处理。 5. 对二值化图像进行形态学操作,如腐蚀和膨胀,以去除噪声和填充空洞。 6. 使用轮廓检测算法找到二值图像中的轮廓,并筛选出符合条件的运动物体轮廓。 7. 绘制矩形框或者圆圈标记出运动物体的位置和大小。 下面是Java OpenCV实现运动跟踪的示例代码: ```java import org.bytedeco.javacpp.opencv_core.*; import org.bytedeco.javacpp.opencv_imgproc.*; import org.bytedeco.javacpp.opencv_videoio.*; import org.bytedeco.javacpp.opencv_highgui.*; public class MotionDetection { public static void main(String[] args) { String filename = "test.mp4"; VideoCapture capture = new VideoCapture(filename); Mat currentImage = new Mat(); Mat previousImage = new Mat(); Mat grayImage1 = new Mat(); Mat grayImage2 = new Mat(); Mat differenceImage = new Mat(); Mat thresholdImage = new Mat(); if (!capture.isOpened()) { System.out.println("Error loading video!"); return; } while (true) { capture.read(currentImage); if (currentImage.empty()) { System.out.println("End of video"); break; } cvtColor(currentImage, grayImage1, COLOR_BGR2GRAY); GaussianBlur(grayImage1, grayImage1, new Size(21, 21), 0); if (previousImage.empty()) { previousImage = grayImage1.clone(); continue; } absdiff(previousImage, grayImage1, differenceImage); threshold(differenceImage, thresholdImage, 25, 255, THRESH_BINARY); Mat kernel = getStructuringElement(MORPH_RECT, new Size(3, 3)); erode(thresholdImage, thresholdImage, kernel, new Point(-1, -1), 2); dilate(thresholdImage, thresholdImage, kernel, new Point(-1, -1), 2); MatVector contours = new MatVector(); findContours(thresholdImage, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); for (int i = 0; i < contours.size(); i++) { double area = contourArea(contours.get(i)); if (area < 500) continue; Rect rect = boundingRect(contours.get(i)); rectangle(currentImage, rect, new Scalar(0, 0, 255), 2); } imshow("Motion Detection", currentImage); if (waitKey(25) == 27) break; previousImage = grayImage1.clone(); } } } ``` 在这个例子中,我们使用VideoCapture类读取视频文件,将连续的帧转换为灰度图像,并对相邻帧之间的差异图进行二值化处理和形态学操作,最后使用轮廓检测算法找到运动物体的轮廓并标记出来。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值