赞
踩
学更好的别人,
做更好的自己。
——《微卡智享》
本文长度为4839字,预计阅读9分钟
前言
最近这几篇OpenCV相关的文章都是与人脸有关,其实最主要是就是想做人脸替换的小试验,大概流程是:
人脸检测
人脸特征点提取
计算Delaunay三角形
得到的三角形进行区域对应的仿射变换
图像融合
今天这篇算是流程上第3和第4步的做法,不过效果失败了,主要是暂时还未想到新的解决方法,正好也要准备做别的东西,所以等有时间想到了再回来继续。
实现效果
从上面的动图中可以看到,我们在提取出人脸后,把人脸用Delaunay进行三角形分割,然后再用仿射变换的对每个三角形进行处理,最左边一块一块的拼接的过程可以看出,不过也很明显,有不少的三角形对应的不对,所以整个人脸也都变形了。
Delaunay三角剖分
微卡智享
给定平面中的一组点,三角测量指的是将平面细分为三角形,将点作为顶点。在图1中,我们在左图像上看到一组界标,以及在中间图像中的三角测量。一组点可以有许多可能的三角剖分,但Delaunay三角剖分出众,因为它有一些不错的属性。在Delaunay三角剖分中,选择三角形使得没有点在任何三角形的外接圆内。图2示出了4点A,B,C和D的Delaunay三角剖分。在顶部图像中,为了使三角剖分是有效的Delaunay三角剖分,点C应该在三角形ABD的外接圆外,并且点A应该在三角形BCD的外接圆。
Delaunay三角形的一个有趣的属性是它不喜欢“瘦”三角形(即具有一个大角度的三角形)。
图2显示了当移动点时,三角形如何改变以选择“胖”三角形。在顶部图像中,点B和D的x坐标在x = 1.5,在底部图像中,它们向右移动到x = 1.75。在顶部图像中,角度ABC和ABD大,并且Delaunay三角剖分在B和D之间创建边缘,将两个大角度分割成更小的角度ABD,ADB,CDB和CBD。另一方面,在底部图像中,角度BCD太大,并且Delaunay三角剖分产生边缘AC以划分大角度。
有很多算法来找到一组点的Delaunay三角剖分。最明显的(但不是最有效的)一个是从任何三角形开始,并检查任何三角形的外接圆包含另一个点。如果是,翻转边缘(如图2所示),并继续,直到没有三角形的外接圆包含一个点。
上述的Delaunay三角部分说明摘自,CSDN博主「wi162yyxq」的原创文章,原文链接:https://blog.csdn.net/wi162yyxq/article/details/53762617
OpenCV中实现Delaunay三角剖分可以使用Subdiv2D,先定义一个分析的Rect空间,然后将要剖分的点都insert进去,使用getTriangleList获取Delaunay三角形的列表。
- vector<vector<Point2f>> DelaunayCore::GetTriangleList(Mat& frame, vector<T> facemarkmodel, string showname)
- {
- Mat testframe = frame.clone();
- vector<vector<Point2f>> resvecpts;
-
-
- vector<Vec6f> triangleList;
- Rect rect = Rect(0, 0, frame.cols, frame.rows);
- Subdiv2D subdiv(rect);
- for (int i = 0; i < facemarkmodel.size(); ++i) {
- Point2f p = facemarkmodel[i];
- if (p.x >= rect.x && p.y >= rect.y && p.x < rect.x + rect.width
- && p.y < rect.y + rect.height) {
- subdiv.insert(p);
- }
- }
-
-
- subdiv.getTriangleList(triangleList);
-
-
- vector<Point2f> pt(3);
- for (int i = 0; i < triangleList.size(); ++i) {
- Vec6f t = triangleList[i];
-
-
- pt[0] = Point2f(t[0], t[1]);
- pt[1] = Point2f(t[2], t[3]);
- pt[2] = Point2f(t[4], t[5]);
-
-
- resvecpts.push_back(pt);
-
-
- line(testframe, pt[0], pt[1], Scalar(0, 255, 0), 1, LINE_AA, 0);
- line(testframe, pt[1], pt[2], Scalar(0, 255, 0), 1, LINE_AA, 0);
- line(testframe, pt[2], pt[0], Scalar(0, 255, 0), 1, LINE_AA, 0);
- }
-
-
- CvUtils::SetShowWindow(testframe, showname, 500, 20);
- imshow(showname, testframe);
-
-
- return resvecpts;
- }
仿射变换
微卡智享
仿射变换的介绍可以看《Android OpenCV(十一):图像仿射变换》,其中最关系的计算仿射矩阵getAffineTransform,是通过3个点来计算的,正好用我们剖分好的三角形的三个顶点计算。
- Mat DelaunayCore::WarpAffineFaceMark(Mat& frame, Mat& dst, vector<vector<Point2f>> srcdelaunay, vector<vector<Point2f>> dstdelaunay)
- {
- Mat resdst = Mat::zeros(dst.size(), CV_8UC3);
- for (int i = 0; i < srcdelaunay.size(); ++i) {
- Mat dstarea = Mat::zeros(frame.size(), CV_8UC1);
- fillConvexPoly(dstarea, CvUtils::Vecpt2fToVecpt(srcdelaunay[i]), Scalar(255, 255, 255));
- imshow("fillarea", dstarea);
- Mat tmpdst = Mat::zeros(dst.size(), CV_8UC3);
- frame.copyTo(tmpdst, dstarea);
-
-
- Mat transform = getAffineTransform(srcdelaunay[i], dstdelaunay[i]);
- warpAffine(tmpdst, tmpdst, transform, dst.size());
-
-
- dstarea = Mat::zeros(dst.size(), CV_8UC1);
- fillConvexPoly(dstarea, CvUtils::Vecpt2fToVecpt(dstdelaunay[i]), Scalar(255, 255, 255));
- tmpdst.copyTo(resdst, dstarea);
-
-
- imshow("tmpsfacemark", resdst);
- waitKey(200);
- }
-
-
-
-
- CvUtils::SetShowWindow(resdst, "tmpsfacemark", 10, 20);
- imshow("tmpsfacemark", resdst);
- return resdst;
- }
-
-
Demo源码说明
微卡智享
这次提交的代码里面,加了两个类,一个CvUtils和一个DelaunayCore。CvUtils中的主要是写了几个通用的函数,一个是图像显示的位置,还有就是检测人脸特征点时的类型为Point2f,而凸包要求的是Point,所以加了个两个相互转化的方法,而DelaunayCore类就是处理获取Delaunay三角形和做仿射变换的类。
01
CvUtils类
- #include "CvUtils.h"
-
-
- void CvUtils::SetShowWindow(Mat img, string winname, int pointx, int pointy)
- {
- //设置显示窗口
- namedWindow(winname, WindowFlags::WINDOW_NORMAL);
- //设置图像显示大小
- resizeWindow(winname, img.size());
- //设置图像显示位置
- moveWindow(winname, pointx, pointy);
- }
-
-
- vector<Point> CvUtils::Vecpt2fToVecpt(vector<Point2f>& vecpt2f)
- {
- vector<Point> vecpt;
- for (Point2f item : vecpt2f) {
- Point pt = item;
- vecpt.push_back(pt);
- }
- return vecpt;
- }
-
-
- vector<Point2f> CvUtils::VecptToVecpt2f(vector<Point>& vecpt)
- {
- vector<Point2f> vecpt2f;
- for (Point item : vecpt) {
- Point2f pt = item;
- vecpt2f.push_back(pt);
- }
- return vecpt2f;
- }
02
DelaunayCore类
DelaunayCore类中,在获取三角形还有插入矩形点里都用到了泛型模版,主要原因也同上面一样,获取到人脸68个特征点的数据为vector<Point2f>,而凸包的数据为vector<Point>,如果按两个不同的类型计算获取三角形,就要写两个函数,这里直接用泛型的方式就直接写一个函数同时调用即可。
在泛型模版中要注意的问题就是实现的函数方法也要写在头文件.h中,而无法写入.cpp,因为模版需要单独编译,模板是实例化式多态。当然也可以在头文件中声明模板,在一个模板文件中实现那些类。然后在头文件的尾部包含具体实现的文件,如:#include “xxxx.cpp” 。
- #pragma once
- #include<opencv2/opencv.hpp>
- #include"CvUtils.h"
-
-
- using namespace std;
- using namespace cv;
-
-
- class DelaunayCore
- {
- public:
- //加入矩形计算点
- template<typename T> static void InsertRectPoint(vector<T>& vecmodels, Rect rect);
-
-
- //获取三角形区域
- template<typename T> static vector<vector<Point2f>> GetTriangleList(Mat& frame, vector<T> facemarkmodel, string showname="testframe");
-
-
- //根据两组人脸点进行仿射变换
- static Mat WarpAffineFaceMark(Mat& frame, Mat& dst, vector<vector<Point2f>> srcdelaunay, vector<vector<Point2f>> dstdelaunay);
- };
-
-
- template<typename T>
- inline void DelaunayCore::InsertRectPoint(vector<T>& vecmodels, Rect rect)
- {
- //获取矩形的4个点加入vecmodels
- vecmodels.push_back(T(rect.x, rect.y));
- vecmodels.push_back(T(rect.x + rect.width, rect.y));
- vecmodels.push_back(T(rect.x + rect.width, rect.y + rect.height));
- vecmodels.push_back(T(rect.x, rect.y + rect.height));
-
-
- //再加上四条边的中点
- vecmodels.push_back(T(rect.x + rect.width / 2, rect.y));
- vecmodels.push_back(T(rect.x + rect.width, rect.y + rect.height / 2));
- vecmodels.push_back(T(rect.x + rect.width / 2, rect.y + rect.height));
- vecmodels.push_back(T(rect.x, rect.y + rect.height / 2));
- }
-
-
- template<typename T>
- inline vector<vector<Point2f>> DelaunayCore::GetTriangleList(Mat& frame, vector<T> facemarkmodel, string showname)
- {
- Mat testframe = frame.clone();
- vector<vector<Point2f>> resvecpts;
-
-
- vector<Vec6f> triangleList;
- Rect rect = Rect(0, 0, frame.cols, frame.rows);
- Subdiv2D subdiv(rect);
- for (int i = 0; i < facemarkmodel.size(); ++i) {
- Point2f p = facemarkmodel[i];
- if (p.x >= rect.x && p.y >= rect.y && p.x < rect.x + rect.width
- && p.y < rect.y + rect.height) {
- subdiv.insert(p);
- }
- }
-
-
- subdiv.getTriangleList(triangleList);
-
-
- vector<Point2f> pt(3);
- for (int i = 0; i < triangleList.size(); ++i) {
- Vec6f t = triangleList[i];
-
-
- pt[0] = Point2f(t[0], t[1]);
- pt[1] = Point2f(t[2], t[3]);
- pt[2] = Point2f(t[4], t[5]);
-
-
- resvecpts.push_back(pt);
-
-
- line(testframe, pt[0], pt[1], Scalar(0, 255, 0), 1, LINE_AA, 0);
- line(testframe, pt[1], pt[2], Scalar(0, 255, 0), 1, LINE_AA, 0);
- line(testframe, pt[2], pt[0], Scalar(0, 255, 0), 1, LINE_AA, 0);
- }
-
-
- CvUtils::SetShowWindow(testframe, showname, 500, 20);
- imshow(showname, testframe);
-
-
- return resvecpts;
- }
- #include "DelaunayCore.h"
-
-
- Mat DelaunayCore::WarpAffineFaceMark(Mat& frame, Mat& dst, vector<vector<Point2f>> srcdelaunay, vector<vector<Point2f>> dstdelaunay)
- {
- Mat resdst = Mat::zeros(dst.size(), CV_8UC3);
- for (int i = 0; i < srcdelaunay.size(); ++i) {
- Mat dstarea = Mat::zeros(frame.size(), CV_8UC1);
- fillConvexPoly(dstarea, CvUtils::Vecpt2fToVecpt(srcdelaunay[i]), Scalar(255, 255, 255));
- imshow("fillarea", dstarea);
- Mat tmpdst = Mat::zeros(dst.size(), CV_8UC3);
- frame.copyTo(tmpdst, dstarea);
-
-
- Mat transform = getAffineTransform(srcdelaunay[i], dstdelaunay[i]);
- warpAffine(tmpdst, tmpdst, transform, dst.size());
-
-
- dstarea = Mat::zeros(dst.size(), CV_8UC1);
- fillConvexPoly(dstarea, CvUtils::Vecpt2fToVecpt(dstdelaunay[i]), Scalar(255, 255, 255));
- tmpdst.copyTo(resdst, dstarea);
-
-
- imshow("tmpsfacemark", resdst);
- waitKey(200);
- }
-
-
-
-
- CvUtils::SetShowWindow(resdst, "tmpsfacemark", 10, 20);
- imshow("tmpsfacemark", resdst);
- return resdst;
- }
-
-
失败的问题
微卡智享
上图中蓝框可以看到,虽然两个人脸都是同一张图像上的,但是我们开始已经把相关的人脸图像提取出来了,然后重新检测的特征点和三角区域。导致的仿射变换后面也肯定出了问题。
考虑到上面只是把面部提取出来后出现的这个问题,那我们再试试不截取面部,而是整个脸的图像。
改了一下代码,感觉三角部分获取的效果要比原来的好多了,但是还有问题,并且左边仿射变换的效果还不如第一个,没有一个对应上的。这块需要单独找个时间研究一下问题,当然如果有了解怎么解决的朋友也可以留言给我,不剩感激。
虽然说Demo是个半成品,不过对自己现在来说也是有收获的,了解了Delaunay三角剖分,仿射变换的简单使用以及C++的模版函数的使用。所谓经验,就是经历的过程+经历的结果,只有这两点自己都经历过后,才算是得到的经验。最后放一下代码地址:
https://github.com/Vaccae/OpenCVDnnfacedecet.git
完
扫描二维码
获取更多精彩
微卡智享
「 往期文章 」
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。