前言
最近这几篇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三角形的列表。
计算Delaunay代码
代码语言:javascript复制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个点来计算的,正好用我们剖分好的三角形的三个顶点计算。
核心代码
代码语言:javascript复制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类
代码语言:javascript复制#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” 。
DelaunayCore.h
代码语言:javascript复制#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;
}
DelaunayCore.cpp
代码语言:javascript复制#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