本期我们将学习如何通过OpenCV实现图片中人脸的替换。
简介
下面是已经完成替换的图片,是不是很酷。
在原图片中位于中前方的实际上是布拉德利·库珀。我们首先使用C#的“换脸”程序将另外一张脸叠加到布拉德利的脸上,然后用数字得到方式将其插入到布拉德利奥斯卡自拍照中。
实现
图像获取
在C#中要解决这个问题,我们将使用Accord库、OpenCvSharp3以及DLib。Accord库非常适合创建计算机视觉应用程序。OpenCvSharp3是一个基于C#的OpenCV库,我们将使用这个库中的几个图像转换功能。在计算机视觉世界中,DLib则是人脸检测的首选库。虽然DLib完全用C 编写,但是DlibDotNet,将所有程序封装到C#中。
我们首先需要获得一张布拉德利的原始自拍照和单人照:
原始自拍
单人照
说明:使用以下代码可以将单人照与自拍照中的任何人交换面孔,但是就以上两幅图而言选择替换布拉德利·库珀效果最好,因为两个人具有相同的视线方向且脸型相似度很高。
界标点检测
接下来我们将使用Dlib库,对人脸进行检测。Dlib面部检测器可以识别出覆盖面部、下巴、眉毛、鼻子、眼睛和嘴唇的68个界标点。这些标记点预先确定的,并有给予其特定的标号,如下图所示。
Dlib运行速度很快,计算所有这些点的计算开销仅为1ms!因此它也可以实时跟踪这些点。以下C#代码,用于检测图片中脸上的所有界标点:
代码语言:javascript复制/// <summary>
/// Process the original selfie and produce the face-swapped image.
/// </summary>
/// <param name="image">The original selfie image.</param>
/// <param name="newImage">The new face to insert into the selfie.</param>
/// <returns>A new image with faces swapped.</returns>
private Bitmap ProcessImage(Bitmap image, Bitmap newImage)
{
// set up Dlib facedetectors and shapedetectors
using (var fd = FrontalFaceDetector.GetFrontalFaceDetector())
using (var sp = new ShapePredictor("shape_predictor_68_face_landmarks.dat"))
{
// convert image to dlib format
var img = image.ToArray2D<RgbPixel>();
// find bradley's faces in image
var faces = fd.Detect(img);
var bradley = faces[0];
// get bradley's landmark points
var bradleyShape = sp.Detect(img, bradley);
var bradleyPoints = (from i in Enumerable.Range(0, (int)bradleyShape.Parts)
let p = bradleyShape.GetPart((uint)i)
select new OpenCvSharp.Point(p.X, p.Y)).ToArray();
// remainder of code goes here...
}
}
界标点检测结果
在这段代码中,我们首先实例化FrontalFaceDetector和ShapePredictor。为此小伙伴们需要注意以下两个问题:
• 在Dlib中,检测面部和检测界标点(或者称为“检测形状”)是两件不同的事情,它们的性能差异很大。人脸检测速度非常慢,而形状检测仅需约1毫秒,并且可以实时进行。
• ShapePredictor实际上是一个从完成训练的数据文件中加载出来的机器学习模型。我们也可以用自己喜欢的任何物体重新训练ShapePredictor,像人脸、猫狗脸、植物等。
接下来Dlib使用的图片格式与NET框架所使用的图片格式不同,因此我需要在运行上述代码之前先转换自拍的图片格式。其中ToArray2D<>方法即可将位图转换为阵列RgbPixel结构,这中结构正好可用于Dlib。
完成图像格式转换以后,我们使用Detect() 来检测图像中的所有面孔。我们选取布拉德利·库珀的面孔提供后续使用,在本次检测中刚好为faces(0)。并且我们还用一个矩形来标识布拉德利的脸在图片中的位置。
接下来,我们在ShapePredictor上调用Detect() 并提供自拍照和用于识别位置的脸部矩形。该函数的返回值是GetPart() 方法的类,我们可以使用GetPart()方法来检索所有界标点的坐标。
我们的后续人脸交换工作将在OpenCV上完成,而OpenCV拥有自己特定的指针结构,因此在代码的最后我们将Dlib点转换为OpenCV点。
凸包提取
接下来,我们需要计算界标点的凸包。一种简单的表达方式即,链接最外面的点形成围绕脸部的平滑边界。
OpenCV的内置功能可以帮助我们计算凸包:
代码语言:javascript复制// get convex hull of bradley's points
var hull = Cv2.ConvexHullIndices(bradleyPoints);
var bradleyHull = from i in hull
select bradleyPoints[i];
// the remaining code goes here...
ConvesHullIndices() 方法可以计算所有凸包界标点的指数,因此我们需要做的就是运行一个LINQ查询,以获取布莱德利·库珀的这些界标点的枚举。
下图是布莱德利脸上的凸包外观。
完成上述内容后,我们需要对单人照中的脸重复这些步骤:
代码语言:javascript复制// find landmark points in face to swap
var imgMark = newImage.ToArray2D<RgbPixel>();
var faces2 = fd.Detect(imgMark);
var mark = faces2[0];
var markShape = sp.Detect(imgMark, mark);
var markPoints = (from i in Enumerable.Range(0, (int)markShape.Parts)
let p = markShape.GetPart((uint)i)
select new OpenCvSharp.Point(p.X, p.Y)).ToArray();
// get convex hull of mark's points
var hull2 = Cv2.ConvexHullIndices(bradleyPoints);
var markHull = from i in hull2
select markPoints[i];
// the remaining code goes here...
这里的代码完全相同,只是将newImage换成了image。下面是从单人照中检测到的凸包外观。
到目前为止,我们已经获得了两个凸包外观,第一个是布莱德利脸上的凸包外观,第二个是单人照上的外观。
Delaunay三角形变形
单人照与布拉德利的凸包点的坐标之间没有线性关系。如果我们尝试直接移动所有像素,则必须使用慢速非线性变换。但是,通过首先在Delaunay三角形中覆盖布莱德利的脸,然后分别对每个三角形进行变形,整个操作将变得线性(且速度很快!)。
因此我们将为两人的脸计算Delaunay三角形。获取单人照中的三角形以后,对它们进行一定的变形,使其与布莱德利的脸完全匹配。
Delaunay Triangulation是一个创建三角形网格的过程,该三角形网格完全覆盖了布莱德利的脸,每个三角形由凸包上的三个特定的界标点组成。结果如下,蓝线即组成了Delaunay三角形:
接下来,我们将对单人照中Delaunay三角形进行变形,使之与布莱德利脸上的每个三角形保持一直,使新的面孔更加适应这张自拍照。在这个过程的每个三角形扭曲都是线性变换,因此可以使用超快速线性矩阵运算来移动每个三角形内的像素。
在下图中,我们扭曲了单人照中由界标点3、14和24组成的Delaunay三角形,以使其正好适合布莱德利的脸,并且这三个点与布莱德利的3、14和24界标点精确匹配:
在C#中执行Delaunay三角剖分和变形的代码如下:
代码语言:javascript复制// calculate Delaunay triangles
var triangles = Utility.GetDelaunayTriangles(bradleyHull);
// get transformations to warp the new face onto Bradley's face
var warps = Utility.GetWarps(markHull, bradleyHull, triangles);
// apply the warps to the new face to prep it for insertion into the main image
var warpedImg = Utility.ApplyWarps(newImage, image.Width, image.Height, warps);
// the remaining code goes here...
我们使用一个便捷类Utility,该类包含有GetDelaunayTriangles方法用于计算三角形,GetWarps方法用于计算每个三角形的翘曲,以及ApplyWarps方法使单人照脸部与布莱德利的脸部凸包相匹配。
现在,单人照中的脸已用warpedImg表示,并且以及充分变形匹配布莱德利:
颜色转换
单人照与布拉德利的凸包点
我们还有一件事需要处理,单人照中人物的肤色与布拉德利的肤色并不相同。因此,如果我只是在自拍照中将图像放在其顶部,我们将在图像边缘看到剧烈的颜色变化:
为了解决这一问题,我们将使用OpenCV中的一个函数SeamlessClone,该函数可以将一个图像无缝地融合到另一个图像中,并消除任何颜色差异。
这是在C#中进行无缝克隆的方法:
代码语言:javascript复制// prepare a mask for the warped image
var mask = new Mat(image.Height, image.Width, MatType.CV_8UC3);
mask.SetTo(0);
Cv2.FillConvexPoly(mask, bradleyHull, new Scalar(255, 255, 255), LineTypes.Link8);
// find the center of the warped face
var r = Cv2.BoundingRect(bradleyHull);
var center = new OpenCvSharp.Point(r.Left r.Width / 2, r.Top r.Height / 2);
// blend the warped face into the main image
var selfie = BitmapConverter.ToMat(image);
var blend = new Mat(selfie.Size(), selfie.Type());
Cv2.SeamlessClone(warpedImg, selfie, mask, center, blend, SeamlessCloneMethods.NormalClone);
// return the modified main image
return BitmapConverter.ToBitmap(blend);
使用SeamlessClone方法需要我们完成以下两件事:
• 首先需要一个mask来告诉它要混合哪些像素。我们在获取布拉德利面部凸包时使用FillConvexPoly方法即可计算所需的mask。
• 中心点处应该完全是单人照的肤色100%,距离中心点越远的像素将获得越接近的布拉德利肤色。我们通过调用BoundingRect获得布拉德利脸的边界框,然后取该框的中心来估计中心的位置。
然后,我调用SeamlessClone进行克隆并将结果存储在blend变量中,最终结果如下所示:
其他
看到这里小伙伴们可能在想为什么在这个过程中需要使用凸包,而不是直接不使用所有界标点来计算三角形?
原因实际上很简单,我们比较一下布拉德利的自拍与单人照。不难发现一个人在笑而另一个人没有?如果我们直接使用所有界标点,该程序将尝试把整个脸都进行变形,以便于和布拉德利的嘴唇,鼻子和眼睛完全匹配。这会使单人照中的人的嘴唇张开,以使单人照中的人物微笑并露出牙齿。
但结果似乎并不太好。
如果只使用凸包壳点,该程序可以使单人照中人物的下巴变形,以匹配布拉德利的下颌线。但是它无法处理该人物的眼睛,鼻子和嘴巴。这意味着表情等在新图像中保持不变,看起来也更加自然。
最后,我们将使用Instagram滤镜来进一步消除色差:
总结
通过以上方式对面部进行一定的变形即可完成一幅图像的人脸插入工作,是不是很简单呢!