前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【失败也分享】C++ OpenCV人脸Delaunay三角形提取及仿射变换的使用

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

作者头像
Vaccae
发布2021-03-12 14:28:46
1.4K0
发布2021-03-12 14:28:46
举报
文章被收录于专栏:微卡智享微卡智享

前言

最近这几篇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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-03-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 微卡智享 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 计算Delaunay代码
  • 核心代码
  • DelaunayCore.h
  • DelaunayCore.cpp
  • 总结
相关产品与服务
人脸识别
腾讯云神图·人脸识别(Face Recognition)基于腾讯优图强大的面部分析技术,提供包括人脸检测与分析、比对、搜索、验证、五官定位、活体检测等多种功能,为开发者和企业提供高性能高可用的人脸识别服务。 可应用于在线娱乐、在线身份认证等多种应用场景,充分满足各行业客户的人脸属性识别及用户身份确认等需求。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档