当前位置:   article > 正文

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

delaunaycore.h

学更好的别人,

做更好的自己。

——《微卡智享》

本文长度为4839,预计阅读9分钟

前言

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

  1. vector<vector<Point2f>> DelaunayCore::GetTriangleList(Mat& frame, vector<T> facemarkmodel, string showname)
  2. {
  3. Mat testframe = frame.clone();
  4. vector<vector<Point2f>> resvecpts;
  5. vector<Vec6f> triangleList;
  6. Rect rect = Rect(0, 0, frame.cols, frame.rows);
  7. Subdiv2D subdiv(rect);
  8. for (int i = 0; i < facemarkmodel.size(); ++i) {
  9. Point2f p = facemarkmodel[i];
  10. if (p.x >= rect.x && p.y >= rect.y && p.x < rect.x + rect.width
  11. && p.y < rect.y + rect.height) {
  12. subdiv.insert(p);
  13. }
  14. }
  15. subdiv.getTriangleList(triangleList);
  16. vector<Point2f> pt(3);
  17. for (int i = 0; i < triangleList.size(); ++i) {
  18. Vec6f t = triangleList[i];
  19. pt[0] = Point2f(t[0], t[1]);
  20. pt[1] = Point2f(t[2], t[3]);
  21. pt[2] = Point2f(t[4], t[5]);
  22. resvecpts.push_back(pt);
  23. line(testframe, pt[0], pt[1], Scalar(0, 255, 0), 1, LINE_AA, 0);
  24. line(testframe, pt[1], pt[2], Scalar(0, 255, 0), 1, LINE_AA, 0);
  25. line(testframe, pt[2], pt[0], Scalar(0, 255, 0), 1, LINE_AA, 0);
  26. }
  27. CvUtils::SetShowWindow(testframe, showname, 500, 20);
  28. imshow(showname, testframe);
  29. return resvecpts;
  30. }

仿射变换

微卡智享

仿射变换的介绍可以看《Android OpenCV(十一):图像仿射变换》,其中最关系的计算仿射矩阵getAffineTransform,是通过3个点来计算的,正好用我们剖分好的三角形的三个顶点计算。

核心代码

  1. Mat DelaunayCore::WarpAffineFaceMark(Mat& frame, Mat& dst, vector<vector<Point2f>> srcdelaunay, vector<vector<Point2f>> dstdelaunay)
  2. {
  3. Mat resdst = Mat::zeros(dst.size(), CV_8UC3);
  4. for (int i = 0; i < srcdelaunay.size(); ++i) {
  5. Mat dstarea = Mat::zeros(frame.size(), CV_8UC1);
  6. fillConvexPoly(dstarea, CvUtils::Vecpt2fToVecpt(srcdelaunay[i]), Scalar(255, 255, 255));
  7. imshow("fillarea", dstarea);
  8. Mat tmpdst = Mat::zeros(dst.size(), CV_8UC3);
  9. frame.copyTo(tmpdst, dstarea);
  10. Mat transform = getAffineTransform(srcdelaunay[i], dstdelaunay[i]);
  11. warpAffine(tmpdst, tmpdst, transform, dst.size());
  12. dstarea = Mat::zeros(dst.size(), CV_8UC1);
  13. fillConvexPoly(dstarea, CvUtils::Vecpt2fToVecpt(dstdelaunay[i]), Scalar(255, 255, 255));
  14. tmpdst.copyTo(resdst, dstarea);
  15. imshow("tmpsfacemark", resdst);
  16. waitKey(200);
  17. }
  18. CvUtils::SetShowWindow(resdst, "tmpsfacemark", 10, 20);
  19. imshow("tmpsfacemark", resdst);
  20. return resdst;
  21. }

Demo源码说明

微卡智享

这次提交的代码里面,加了两个类,一个CvUtils和一个DelaunayCore。CvUtils中的主要是写了几个通用的函数,一个是图像显示的位置,还有就是检测人脸特征点时的类型为Point2f,而凸包要求的是Point,所以加了个两个相互转化的方法,而DelaunayCore类就是处理获取Delaunay三角形和做仿射变换的类。

01

CvUtils类

  1. #include "CvUtils.h"
  2. void CvUtils::SetShowWindow(Mat img, string winname, int pointx, int pointy)
  3. {
  4. //设置显示窗口
  5. namedWindow(winname, WindowFlags::WINDOW_NORMAL);
  6. //设置图像显示大小
  7. resizeWindow(winname, img.size());
  8. //设置图像显示位置
  9. moveWindow(winname, pointx, pointy);
  10. }
  11. vector<Point> CvUtils::Vecpt2fToVecpt(vector<Point2f>& vecpt2f)
  12. {
  13. vector<Point> vecpt;
  14. for (Point2f item : vecpt2f) {
  15. Point pt = item;
  16. vecpt.push_back(pt);
  17. }
  18. return vecpt;
  19. }
  20. vector<Point2f> CvUtils::VecptToVecpt2f(vector<Point>& vecpt)
  21. {
  22. vector<Point2f> vecpt2f;
  23. for (Point item : vecpt) {
  24. Point2f pt = item;
  25. vecpt2f.push_back(pt);
  26. }
  27. return vecpt2f;
  28. }

02

DelaunayCore类

DelaunayCore类中,在获取三角形还有插入矩形点里都用到了泛型模版,主要原因也同上面一样,获取到人脸68个特征点的数据为vector<Point2f>,而凸包的数据为vector<Point>,如果按两个不同的类型计算获取三角形,就要写两个函数,这里直接用泛型的方式就直接写一个函数同时调用即可。

在泛型模版中要注意的问题就是实现的函数方法也要写在头文件.h中,而无法写入.cpp,因为模版需要单独编译,模板是实例化式多态。当然也可以在头文件中声明模板,在一个模板文件中实现那些类。然后在头文件的尾部包含具体实现的文件,如:#include “xxxx.cpp” 。

DelaunayCore.h

  1. #pragma once
  2. #include<opencv2/opencv.hpp>
  3. #include"CvUtils.h"
  4. using namespace std;
  5. using namespace cv;
  6. class DelaunayCore
  7. {
  8. public:
  9. //加入矩形计算点
  10. template<typename T> static void InsertRectPoint(vector<T>& vecmodels, Rect rect);
  11. //获取三角形区域
  12. template<typename T> static vector<vector<Point2f>> GetTriangleList(Mat& frame, vector<T> facemarkmodel, string showname="testframe");
  13. //根据两组人脸点进行仿射变换
  14. static Mat WarpAffineFaceMark(Mat& frame, Mat& dst, vector<vector<Point2f>> srcdelaunay, vector<vector<Point2f>> dstdelaunay);
  15. };
  16. template<typename T>
  17. inline void DelaunayCore::InsertRectPoint(vector<T>& vecmodels, Rect rect)
  18. {
  19. //获取矩形的4个点加入vecmodels
  20. vecmodels.push_back(T(rect.x, rect.y));
  21. vecmodels.push_back(T(rect.x + rect.width, rect.y));
  22. vecmodels.push_back(T(rect.x + rect.width, rect.y + rect.height));
  23. vecmodels.push_back(T(rect.x, rect.y + rect.height));
  24. //再加上四条边的中点
  25. vecmodels.push_back(T(rect.x + rect.width / 2, rect.y));
  26. vecmodels.push_back(T(rect.x + rect.width, rect.y + rect.height / 2));
  27. vecmodels.push_back(T(rect.x + rect.width / 2, rect.y + rect.height));
  28. vecmodels.push_back(T(rect.x, rect.y + rect.height / 2));
  29. }
  30. template<typename T>
  31. inline vector<vector<Point2f>> DelaunayCore::GetTriangleList(Mat& frame, vector<T> facemarkmodel, string showname)
  32. {
  33. Mat testframe = frame.clone();
  34. vector<vector<Point2f>> resvecpts;
  35. vector<Vec6f> triangleList;
  36. Rect rect = Rect(0, 0, frame.cols, frame.rows);
  37. Subdiv2D subdiv(rect);
  38. for (int i = 0; i < facemarkmodel.size(); ++i) {
  39. Point2f p = facemarkmodel[i];
  40. if (p.x >= rect.x && p.y >= rect.y && p.x < rect.x + rect.width
  41. && p.y < rect.y + rect.height) {
  42. subdiv.insert(p);
  43. }
  44. }
  45. subdiv.getTriangleList(triangleList);
  46. vector<Point2f> pt(3);
  47. for (int i = 0; i < triangleList.size(); ++i) {
  48. Vec6f t = triangleList[i];
  49. pt[0] = Point2f(t[0], t[1]);
  50. pt[1] = Point2f(t[2], t[3]);
  51. pt[2] = Point2f(t[4], t[5]);
  52. resvecpts.push_back(pt);
  53. line(testframe, pt[0], pt[1], Scalar(0, 255, 0), 1, LINE_AA, 0);
  54. line(testframe, pt[1], pt[2], Scalar(0, 255, 0), 1, LINE_AA, 0);
  55. line(testframe, pt[2], pt[0], Scalar(0, 255, 0), 1, LINE_AA, 0);
  56. }
  57. CvUtils::SetShowWindow(testframe, showname, 500, 20);
  58. imshow(showname, testframe);
  59. return resvecpts;
  60. }

DelaunayCore.cpp

  1. #include "DelaunayCore.h"
  2. Mat DelaunayCore::WarpAffineFaceMark(Mat& frame, Mat& dst, vector<vector<Point2f>> srcdelaunay, vector<vector<Point2f>> dstdelaunay)
  3. {
  4. Mat resdst = Mat::zeros(dst.size(), CV_8UC3);
  5. for (int i = 0; i < srcdelaunay.size(); ++i) {
  6. Mat dstarea = Mat::zeros(frame.size(), CV_8UC1);
  7. fillConvexPoly(dstarea, CvUtils::Vecpt2fToVecpt(srcdelaunay[i]), Scalar(255, 255, 255));
  8. imshow("fillarea", dstarea);
  9. Mat tmpdst = Mat::zeros(dst.size(), CV_8UC3);
  10. frame.copyTo(tmpdst, dstarea);
  11. Mat transform = getAffineTransform(srcdelaunay[i], dstdelaunay[i]);
  12. warpAffine(tmpdst, tmpdst, transform, dst.size());
  13. dstarea = Mat::zeros(dst.size(), CV_8UC1);
  14. fillConvexPoly(dstarea, CvUtils::Vecpt2fToVecpt(dstdelaunay[i]), Scalar(255, 255, 255));
  15. tmpdst.copyTo(resdst, dstarea);
  16. imshow("tmpsfacemark", resdst);
  17. waitKey(200);
  18. }
  19. CvUtils::SetShowWindow(resdst, "tmpsfacemark", 10, 20);
  20. imshow("tmpsfacemark", resdst);
  21. return resdst;
  22. }

失败的问题

微卡智享

上图中蓝框可以看到,虽然两个人脸都是同一张图像上的,但是我们开始已经把相关的人脸图像提取出来了,然后重新检测的特征点和三角区域。导致的仿射变换后面也肯定出了问题。

考虑到上面只是把面部提取出来后出现的这个问题,那我们再试试不截取面部,而是整个脸的图像。

改了一下代码,感觉三角部分获取的效果要比原来的好多了,但是还有问题,并且左边仿射变换的效果还不如第一个,没有一个对应上的。这块需要单独找个时间研究一下问题,当然如果有了解怎么解决的朋友也可以留言给我,不剩感激。

总结

虽然说Demo是个半成品,不过对自己现在来说也是有收获的,了解了Delaunay三角剖分,仿射变换的简单使用以及C++的模版函数的使用。所谓经验,就是经历的过程+经历的结果,只有这两点自己都经历过后,才算是得到的经验。最后放一下代码地址:

https://github.com/Vaccae/OpenCVDnnfacedecet.git

扫描二维码

获取更多精彩

微卡智享

「 往期文章 」

C++ OpenCV人脸图像提取

C++ OpenCV Contrib模块LBF人脸特征点检测

OpenCV源码Android端编译,用时三天,我编了个寂寞。。。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/324828?site
推荐阅读
相关标签
  

闽ICP备14008679号