个人项目-人脸合成

一个使用 python 语言,将多个人脸的特征融合为一个人脸的小型个人项目。

本文大量参考了该文档:Average Face : OpenCV ( C++ / Python ) Tutorial | LearnOpenCV #

我们需要:六张人脸图像,以及六份人脸图像对应的脸部特征坐标文件。

面部特征读取: readPoints

这部分内容并不难,我们创建列表 point 存储每个图像特征的点坐标,创建列表 pointsarray 存储对应的六个 point 列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
def readPoints(path) :
pointsArray = []
for filePath in sorted(os.listdir(path)):
# 找到图片对应的坐标文件
if filePath.endswith(".txt"):
points = []
with open(os.path.join(path, filePath)) as file :
# txt文件中,每行是一个坐标的x、y值
for line in file :
x, y = line.split()
points.append((int(x), int(y)))
pointsArray.append(points)
return pointsArray

图像读取:readImage

这部分内容很简单,使用 opencv 中的 imread 函数从相应路径path中读取图片,并对图片进行归一化,存储在 imagesArray 数组中。

1
2
3
4
5
6
7
8
9
10
def readImages(path) :
imagesArray = []

for filePath in sorted(os.listdir(path)):
if filePath.endswith(".jpg"):
img = cv2.imread(os.path.join(path,filePath))
img = np.float32(img)/255.0
imagesArray.append(img)

return imagesArray

坐标变化:similarTransform

我们输入的这六张原始人脸图像输入大小可以是不同的。我们在这里采用了将面部归一化方法,我们规格化人脸图像,将所有人脸图像变换到同一个坐标系(即下文的输出坐标系)中。

因此,我们在这里设置了 similarTransform 函数,计算得到一个相似变换矩阵,这是将输入点集转换到输出点集所需一个的仿射变换参数。

我们在这里选择输出坐标系的方法是:将面部大小扭曲为 600×600 图像,使得左眼的左眼角位于像素位置 ( 180, 200 ),右眼的右眼角位于像素位置 ( 420, 200 )。

我们将该坐标系称为输出坐标系,将原始图像的坐标称为输入坐标系。

我们根据提供的输入点与输出点,计算了输入点、输出点的新坐标(xinyinxoutyout)。新坐标相比于原坐标,逆时针旋转了 60°。

我们通过计算相似变换,即 estimateRigidTransform 函数,将点从输入坐标系变换到输出坐标系。`

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def similarTransform(inPoints, outPoints) :
sin60 = math.sin(60*math.pi/180)
cos60 = math.cos(60*math.pi/180)

inPts = np.copy(inPoints).tolist()
outPts = np.copy(outPoints).tolist()

# 根据原输入点和输出点,获取新输入点和输出点坐标。
xin = cos60*(inPts[0][0] - inPts[1][0]) - sin60*(inPts[0][1] - inPts[1][1]) + inPts[1][0]
yin = sin60*(inPts[0][0] - inPts[1][0]) + cos60*(inPts[0][1] - inPts[1][1]) + inPts[1][1]
print(xin ,yin)
inPts.append([np.int(xin), np.int(yin)])

xout = cos60*(outPts[0][0] - outPts[1][0]) - sin60*(outPts[0][1] - outPts[1][1]) + outPts[1][0]
yout = sin60*(outPts[0][0] - outPts[1][0]) + cos60*(outPts[0][1] - outPts[1][1]) + outPts[1][1]
print(xout ,yout)

outPts.append([np.int(xout), np.int(yout)])

# 通过输入点和输出点获得transform
tform = cv2.estimateAffinePartial2D(np.array([inPts]), np.array([outPts]))

return tform[0]

通过这个函数,我们可以将所有面部图像调整到统一大小,同时两个眼角是对齐的。

三角剖分:DelaunayTriangles

我们下面需要计算三角剖分。

三角剖分是人脸合成中应用非常广的一个方法。

我们将人脸分成多个目标区域,这里三角剖分目的网格化图像脸部区域,方便寻找特征点。

Delaunay

opencv 库中提供了进行三角剖分的函数 cv2.Subdiv2D,可以直接使用。

同时,我们需要保证每个三角剖分得到的三角形必须在图像里面(即三个点都在图像里),因此还需要判断。

这里判断用的是自定义的函数 rectContains(rect, point),判断点 point 是否在 rect 对应的矩形中。

如果对应的三个三角形的点都在 rect 函数中,那么我们就可以把点集中的点与三角形中的点进行比较。

就把这个三角形添加到 ind 列表中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# rect [0] and [1] 右下 ,[2] and [3] 左上
# 判断 point 是否在 rect 中
def rectContains(rect, point) :
# 左下角的点
if point[0] < rect[0] :
return False
elif point[1] < rect[1] :
return False
# 右上角的点
elif point[0] > rect[2] :
return False
elif point[1] > rect[3] :
return False
return True

# 计算三角剖分
def calculateDelaunayTriangles(rect, points):
# Create subdiv to make delanauy triangle
subdiv = cv2.Subdiv2D(rect)

# Insert points into subdiv
for p in points:
subdiv.insert((p[0], p[1]))

# List of triangles. Each triangle is a list of 3 points ( 6 numbers )
triangleList = subdiv.getTriangleList()
# Find the indices of triangles in the points array

delaunayTri = []

for t in triangleList:
pt = []
pt.append((t[0], t[1]))
pt.append((t[2], t[3]))
pt.append((t[4], t[5]))

pt1 = (t[0], t[1])
pt2 = (t[2], t[3])
pt3 = (t[4], t[5])

# 保证 Delaunay 的三角形在矩形内
if rectContains(rect, pt1) and rectContains(rect, pt2) and rectContains(rect, pt3):
ind = []
for j in range(0, 3):
for k in range(0, len(points)):
# 遍历 points ,找到与三角形顶点坐标接近(差值小于1.0)的点的索引。
if(abs(pt[j][0] - points[k][0]) < 1.0 and abs(pt[j][1] - points[k][1]) < 1.0):
ind.append(k)
if len(ind) == 3:
delaunayTri.append((ind[0], ind[1], ind[2]))

return delaunayTri

三角剖分的仿射变换:warpTriangle

仿射变换是线性变换、平移变换这两种简单变换的叠加,包括缩放(Scale)、平移 (transform)、旋转(rotate)、反射(reflection)等一系列操作。

在python中,我们可以使用 getAffineTransform(A,B) 函数生成从 A 到 B 对应的仿射变换矩阵。之后便可以应用仿射变换,将原有图像仿射变换至另一个图像。

具体代码如下所示,其中src是原图像, srcTri是原图像定义的三角形, dstTri是目标图像定义的三角形。

1
2
3
4
5
6
7
8
9
def applyAffineTransform(src, srcTri, dstTri, size) :

# 计算得到仿射变换矩阵warpMat
warpMat = cv2.getAffineTransform( np.float32(srcTri), np.float32(dstTri) )

# 对原图像 src 应用仿射变换,得到目标图像 dst
dst = cv2.warpAffine( src, warpMat, (size[0], size[1]), None, flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101 )

return dst

之后,我们便可以应用我们得到的仿射变换矩阵,将上面三角剖分得到的三角形仿射变换到新的人脸上。

注意,这里的仿射变换与上面的similarTransform不同。similarTransform针对的是整个人脸图像,而这里的warpTriangle是针对图像三角剖分得到的三角形,应用场景是不同的。

具体代码如下,其中img1为原图像,img2为输出得到的图像,t1为源图像 img1 中三角形的三个顶点坐标。t2为目标图像 img2 中三角形的三个顶点坐标。这两个三角形在各自的图像中有相同的形状和大小,但位置和姿态可能不同。我们目的就是将 img1t1 所在的区域通过仿射变换映射到 img2t2 所在的区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def warpTriangle(img1, img2, t1, t2):
# 找到包含图形的三角形t1、t2的最小边界矩形。
r1 = cv2.boundingRect(np.float32([t1]))
r2 = cv2.boundingRect(np.float32([t2]))

t1Rect = []
t2Rect = []
t2RectInt = []

# 减去边界矩形的坐标,将三角形的顶点转换到相对于矩形坐标系的偏移坐标。
for i in range(0, 3):
t1Rect.append(((t1[i][0] - r1[0]), (t1[i][1] - r1[1])))
t2Rect.append(((t2[i][0] - r2[0]), (t2[i][1] - r2[1])))
t2RectInt.append(((t2[i][0] - r2[0]), (t2[i][1] - r2[1])))

# 创建一个大小与 r2 一样的空白掩码,并填充三角形区域,形成掩码
mask = np.zeros((r2[3], r2[2], 3), dtype=np.float32)
cv2.fillConvexPoly(mask, np.int32(t2RectInt), (1.0, 1.0, 1.0), 16, 0)

# 从源图像提取 r1 定义的矩形区域,形成 img1Rect。
img1Rect = img1[r1[1]:r1[1] + r1[3], r1[0]:r1[0] + r1[2]]

# 确定目标矩阵大小
size = (r2[2], r2[3])

# 直接应用变换
img2Rect = applyAffineTransform(img1Rect, t1Rect, t2Rect, size)

# 乘掩码,来保证仅保留三角形区域。
img2Rect = img2Rect * mask

# 将目标图像对应区域 img2 中的三角形区域清零,后将变换后的矩形区域 img2Rect 加到目标图像中对应的区域,完成三角形区域的复制。
img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] = img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] * ((1.0, 1.0, 1.0) - mask)
img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] = img2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]] + img2Rect

实际应用:

在实际应用中,我们需要准备的内容为六张人脸图像,以及这六份人脸图像对应的脸部特征坐标文件。

设置脸部图像宽度w、高度h均为 600。

接下来设置输出图像的输出坐标系,在这个过程中,依照similarTransform中提到的,我们设置脸部的焦点为图像顶部的高度的三分之一,眼角为(0.3wh / 3)和(0.7wh / 3)。

1
eyecornerDst = [( int(0.3 * w ), int(h / 3)), (int(0.7 * w ), int(h / 3)) ]

接下来我们设置了两个变量,分别是boundaryPtspointsAvg

boundaryPts定义图像边界上的八个点,确保在Delaunay三角剖分过程中,所有区域都被覆盖。这八个点分别代表着图像边界中的左上角,上中央,右上角,右中央,右下角,下中央,左下角,左中央这八个点。

1
boundaryPts = np.array([(0,0), (w/2,0), (w-1,0), (w-1,h/2), ( w-1, h-1 ), ( w/2, h-1 ), (0, h-1), (0,h/2) ])

pointsAvg 则会存储输入图像特征点的平均位置。初始值设置为0,列表长度为特征点数量加上边界点数量。

1
pointsAvg = np.array([(0,0)]* ( len(allPoints[0]) + len(boundaryPts) ), np.float32())

下面对原始输入图像进行仿射变换和对齐。我们应用上面的similarTransform函数,对于每张输入图片,都计算从原始坐标系到目标坐标系的仿射变换矩阵并实现变换。对于原图片上的特征点,也通过该方法进行变换,同时把boundaryPts上定义的八个边界点也附加到特征点上面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
n = len(allPoints[0]) # 特征点数目
numImages = len(images) # 输入图片数目

for i in range(0, numImages):
# 获取图像的特征点
points1 = allPoints[i]

# 两个眼部坐标,分别是36号和45号。
eyecornerSrc = [ allPoints[i][36], allPoints[i][45] ]
# 计算从输入图像眼角到目标眼角的相似性变换矩阵。
tform = similarTransform(eyecornerSrc, eyecornerDst)
# 应用图像仿射变换
img = cv2.warpAffine(images[i], tform, (w,h))
# 为了适应 transform 函数要求输入数据具有特定的形状,进行reshape操作。
# transform 需要输入数据是三维数组,其中每个特征点都包含在一个独立的二维数组中。
points2 = np.reshape(np.array(points1), (68,1,2))
# 原特征点也应用仿射变换矩阵进行变换
points = cv2.transform(points2, tform)
# 变回来
points = np.float32(np.reshape(points, (68, 2)))

# 添加边界点,应用在三角剖分中。
points = np.append(points, boundaryPts, axis=0)

# 计算特征点的平均位置
pointsAvg = pointsAvg + points / numImages

pointsNorm.append(points)
imagesNorm.append(img)

最后,我们根据特征点计算得到所有的三角剖分后,应用 calculateDelaunayTriangles 函数得到所有的特征点,然后我们就可以遍历dt中的三角形,然后对三角剖分实现仿射变换 warpTriangle 来将每个三角形实现变换,并添加到空白图像img中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 图片左下右上
rect = (0, 0, w, h)
# 。通过平均特征点,计算 Delaunay 三角剖分,生成三角形。
dt = calculateDelaunayTriangles(rect, np.array(pointsAvg))

# 输出图片设置
output = np.zeros((h,w,3), np.float32())
# Warp input images to average image landmarks
for i in range(0, len(imagesNorm)) :
img = np.zeros((h,w,3), np.float32())
# Transform triangles one by one
# 遍历 Delaunay 三角形
for j in range(0, len(dt)) :
tin = []
tout = []
for k in range(0, 3) :
# 第 i 个图像第 j 个三角剖分的第 k 个坐标
pIn = pointsNorm[i][dt[j][k]]
pIn = constrainPoint(pIn, w, h)
# 平均特征点中的顶点坐标
pOut = pointsAvg[dt[j][k]]
pOut = constrainPoint(pOut, w, h)

tin.append(pIn)
tout.append(pOut)
# 对输入图像的三角形区域进行变形并绘制到输出图像上
warpTriangle(imagesNorm[i], img, tin, tout)

# 将变形后的图像叠加到输出图像上
output = output + img


output = output / numImages

# 展示结果。
cv2.imshow('image', output)
cv2.waitKey(0)

根据文档给出的输入图片与特征值文本,得到的图像如下图所示:

最终合成图像

当然,由于我们事先已经对原始的图像进行了关键点标注并生成了对应的txt文件,这个方法不能应用到现实生活中,还需要进一步完善。

  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

请我喝杯咖啡吧~

支付宝
微信