【失败也分享】C++ OpenCV人脸Delaunay三角形提取及仿射变换的使用

2021-03-12 14:28:46 浏览数 (1)

前言

最近这几篇OpenCV相关的文章都是与人脸有关,其实最主要是就是想做人脸替换的小试验,大概流程是:

  1. 人脸检测
  2. 人脸特征点提取
  3. 计算Delaunay三角形
  4. 得到的三角形进行区域对应的仿射变换
  5. 图像融合

今天这篇算是流程上第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

0 人点赞