310 lines
10 KiB
Python
310 lines
10 KiB
Python
# Copyright 2017 BIG VISION LLC ALL RIGHTS RESERVED
|
|
#
|
|
# This code is made available to the students of
|
|
# the online course titled "Computer Vision for Faces"
|
|
# by Satya Mallick for personal non-commercial use.
|
|
#
|
|
# Sharing this code is strictly prohibited without written
|
|
# permission from Big Vision LLC.
|
|
#
|
|
# For licensing and other inquiries, please email
|
|
# spmallick@bigvisionllc.com
|
|
#
|
|
import cv2
|
|
import dlib
|
|
import numpy as np
|
|
import math
|
|
|
|
# Returns 8 points on the boundary of a rectangle
|
|
def getEightBoundaryPoints(h, w):
|
|
boundaryPts = []
|
|
boundaryPts.append((0,0))
|
|
boundaryPts.append((w/2, 0))
|
|
boundaryPts.append((w-1,0))
|
|
boundaryPts.append((w-1, h/2))
|
|
boundaryPts.append((w-1, h-1))
|
|
boundaryPts.append((w/2, h-1))
|
|
boundaryPts.append((0, h-1))
|
|
boundaryPts.append((0, h/2))
|
|
return np.array(boundaryPts, dtype=np.float)
|
|
|
|
|
|
# Constrains points to be inside boundary
|
|
def constrainPoint(p, w, h):
|
|
p = (min(max(p[0], 0), w - 1), min(max(p[1], 0), h - 1))
|
|
return p
|
|
|
|
# convert Dlib shape detector object to list of tuples
|
|
def dlibLandmarksToPoints(shape):
|
|
points = []
|
|
for p in shape.parts():
|
|
pt = (p.x, p.y)
|
|
points.append(pt)
|
|
return points
|
|
|
|
# Compute similarity transform given two sets of two points.
|
|
# OpenCV requires 3 pairs of corresponding points.
|
|
# We are faking the third one.
|
|
def similarityTransform(inPoints, outPoints):
|
|
s60 = math.sin(60*math.pi/180)
|
|
c60 = math.cos(60*math.pi/180)
|
|
|
|
inPts = np.copy(inPoints).tolist()
|
|
outPts = np.copy(outPoints).tolist()
|
|
|
|
# The third point is calculated so that the three points make an equilateral triangle
|
|
xin = c60*(inPts[0][0] - inPts[1][0]) - s60*(inPts[0][1] - inPts[1][1]) + inPts[1][0]
|
|
yin = s60*(inPts[0][0] - inPts[1][0]) + c60*(inPts[0][1] - inPts[1][1]) + inPts[1][1]
|
|
|
|
inPts.append([np.int(xin), np.int(yin)])
|
|
|
|
xout = c60*(outPts[0][0] - outPts[1][0]) - s60*(outPts[0][1] - outPts[1][1]) + outPts[1][0]
|
|
yout = s60*(outPts[0][0] - outPts[1][0]) + c60*(outPts[0][1] - outPts[1][1]) + outPts[1][1]
|
|
|
|
outPts.append([np.int(xout), np.int(yout)])
|
|
# Now we can use estimateRigidTransform for calculating the similarity transform.
|
|
tform = cv2.estimateAffinePartial2D(np.array([inPts]), np.array([outPts]))
|
|
return tform[0]
|
|
|
|
# Normalizes a facial image to a standard size given by outSize.
|
|
# Normalization is done based on Dlib's landmark points passed as pointsIn
|
|
# After normalization, left corner of the left eye is at (0.3 * w, h/3 )
|
|
# and right corner of the right eye is at ( 0.7 * w, h / 3) where w and h
|
|
# are the width and height of outSize.
|
|
def normalizeImagesAndLandmarks(outSize, imIn, pointsIn):
|
|
h, w = outSize
|
|
|
|
# Corners of the eye in input image
|
|
if len(pointsIn) == 68:
|
|
eyecornerSrc = [pointsIn[36], pointsIn[45]]
|
|
elif len(pointsIn) == 5:
|
|
eyecornerSrc = [pointsIn[2], pointsIn[0]]
|
|
|
|
# Corners of the eye in normalized image
|
|
eyecornerDst = [(np.int(0.3 * w), np.int(h/3)),
|
|
(np.int(0.7 * w), np.int(h/3))]
|
|
|
|
# Calculate similarity transform
|
|
tform = similarityTransform(eyecornerSrc, eyecornerDst)
|
|
imOut = np.zeros(imIn.shape, dtype=imIn.dtype)
|
|
|
|
# Apply similarity transform to input image
|
|
imOut = cv2.warpAffine(imIn, tform, (w, h))
|
|
|
|
# reshape pointsIn from numLandmarks x 2 to numLandmarks x 1 x 2
|
|
points2 = np.reshape(pointsIn, (pointsIn.shape[0], 1, pointsIn.shape[1]))
|
|
|
|
# Apply similarity transform to landmarks
|
|
pointsOut = cv2.transform(points2, tform)
|
|
|
|
# reshape pointsOut to numLandmarks x 2
|
|
pointsOut = np.reshape(pointsOut, (pointsIn.shape[0], pointsIn.shape[1]))
|
|
|
|
return imOut, pointsOut
|
|
|
|
def alignFace(imIn, faceRect, landmarkDetector, outSize):
|
|
# Corners of the eye in input image
|
|
(w, h) = outSize
|
|
landmarks = landmarkDetector(cv2.cvtColor(imIn, cv2.COLOR_BGR2RGB), faceRect)
|
|
pointsIn = np.array(dlibLandmarksToPoints(landmarks))
|
|
|
|
eyecornerSrc = [pointsIn[2], pointsIn[0]]
|
|
|
|
# Corners of the eye in normalized image
|
|
eyecornerDst = [(np.int(0.2 * w), np.int(h/3)),
|
|
(np.int(0.8 * w), np.int(h/3))]
|
|
|
|
# Calculate similarity transform
|
|
tform = similarityTransform(eyecornerSrc, eyecornerDst)
|
|
|
|
imIn = np.float32(imIn)/255.0
|
|
imOut = np.zeros(imIn.shape, dtype=imIn.dtype)
|
|
|
|
# Apply similarity transform to input image
|
|
imOut = cv2.warpAffine(imIn, tform, outSize)
|
|
|
|
imOut = np.uint8(imOut*255)
|
|
|
|
return imOut
|
|
|
|
|
|
# find the point closest to an array of points
|
|
# pointsArray is a Nx2 and point is 1x2 ndarray
|
|
def findIndex(pointsArray, point):
|
|
dist = np.linalg.norm(pointsArray-point, axis=1)
|
|
minIndex = np.argmin(dist)
|
|
return minIndex
|
|
|
|
|
|
# Check if a point is inside a rectangle
|
|
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
|
|
|
|
|
|
# Calculate Delaunay triangles for set of points
|
|
# Returns the vector of indices of 3 points for each triangle
|
|
def calculateDelaunayTriangles(rect, points):
|
|
|
|
# Create an instance of Subdiv2D
|
|
subdiv = cv2.Subdiv2D(rect)
|
|
|
|
# Insert points into subdiv
|
|
for p in points:
|
|
subdiv.insert((p[0], p[1]))
|
|
|
|
# Get Delaunay triangulation
|
|
triangleList = subdiv.getTriangleList()
|
|
|
|
# Find the indices of triangles in the points array
|
|
delaunayTri = []
|
|
|
|
for t in triangleList:
|
|
# The triangle returned by getTriangleList is
|
|
# a list of 6 coordinates of the 3 points in
|
|
# x1, y1, x2, y2, x3, y3 format.
|
|
# Store triangle as a list of three points
|
|
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])
|
|
|
|
if rectContains(rect, pt1) and rectContains(rect, pt2) and rectContains(rect, pt3):
|
|
# Variable to store a triangle as indices from list of points
|
|
ind = []
|
|
# Find the index of each vertex in the points list
|
|
for j in range(0, 3):
|
|
for k in range(0, len(points)):
|
|
if(abs(pt[j][0] - points[k][0]) < 1.0 and abs(pt[j][1] - points[k][1]) < 1.0):
|
|
ind.append(k)
|
|
# Store triangulation as a list of indices
|
|
if len(ind) == 3:
|
|
delaunayTri.append((ind[0], ind[1], ind[2]))
|
|
|
|
return delaunayTri
|
|
|
|
# Apply affine transform calculated using srcTri and dstTri to src and
|
|
# output an image of size.
|
|
def applyAffineTransform(src, srcTri, dstTri, size):
|
|
|
|
# Given a pair of triangles, find the affine transform.
|
|
warpMat = cv2.getAffineTransform(np.float32(srcTri), np.float32(dstTri))
|
|
|
|
# Apply the Affine Transform just found to the src image
|
|
dst = cv2.warpAffine(src, warpMat, (size[0], size[1]), None,
|
|
flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101)
|
|
|
|
return dst
|
|
|
|
# Warps and alpha blends triangular regions from img1 and img2 to img
|
|
def warpTriangle(img1, img2, t1, t2):
|
|
# Find bounding rectangle for each triangle
|
|
r1 = cv2.boundingRect(np.float32([t1]))
|
|
r2 = cv2.boundingRect(np.float32([t2]))
|
|
|
|
# Offset points by left top corner of the respective rectangles
|
|
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])))
|
|
|
|
# Get mask by filling triangle
|
|
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)
|
|
|
|
# Apply warpImage to small rectangular patches
|
|
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
|
|
|
|
# Copy triangular region of the rectangular patch to the output image
|
|
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
|
|
|
|
# detect facial landmarks in image
|
|
def getLandmarks(faceDetector, landmarkDetector, im, FACE_DOWNSAMPLE_RATIO = 1):
|
|
points = []
|
|
imSmall = cv2.resize(im,None,
|
|
fx=1.0/FACE_DOWNSAMPLE_RATIO,
|
|
fy=1.0/FACE_DOWNSAMPLE_RATIO,
|
|
interpolation = cv2.INTER_LINEAR)
|
|
|
|
faceRects = faceDetector(imSmall, 0)
|
|
|
|
if len(faceRects) > 0:
|
|
maxArea = 0
|
|
maxRect = None
|
|
# TODO: test on images with multiple faces
|
|
for face in faceRects:
|
|
if face.area() > maxArea:
|
|
maxArea = face.area()
|
|
maxRect = [face.left(),
|
|
face.top(),
|
|
face.right(),
|
|
face.bottom()
|
|
]
|
|
|
|
rect = dlib.rectangle(*maxRect)
|
|
scaledRect = dlib.rectangle(int(rect.left()*FACE_DOWNSAMPLE_RATIO),
|
|
int(rect.top()*FACE_DOWNSAMPLE_RATIO),
|
|
int(rect.right()*FACE_DOWNSAMPLE_RATIO),
|
|
int(rect.bottom()*FACE_DOWNSAMPLE_RATIO))
|
|
|
|
landmarks = landmarkDetector(im, scaledRect)
|
|
points = dlibLandmarksToPoints(landmarks)
|
|
return points
|
|
|
|
# Warps an image in a piecewise affine manner.
|
|
# The warp is defined by the movement of landmark points specified by pointsIn
|
|
# to a new location specified by pointsOut. The triangulation beween points is specified
|
|
# by their indices in delaunayTri.
|
|
def warpImage(imIn, pointsIn, pointsOut, delaunayTri):
|
|
h, w, ch = imIn.shape
|
|
# Output image
|
|
imOut = np.zeros(imIn.shape, dtype=imIn.dtype)
|
|
|
|
# Warp each input triangle to output triangle.
|
|
# The triangulation is specified by delaunayTri
|
|
for j in range(0, len(delaunayTri)):
|
|
# Input and output points corresponding to jth triangle
|
|
tin = []
|
|
tout = []
|
|
|
|
for k in range(0, 3):
|
|
# Extract a vertex of input triangle
|
|
pIn = pointsIn[delaunayTri[j][k]]
|
|
# Make sure the vertex is inside the image.
|
|
pIn = constrainPoint(pIn, w, h)
|
|
|
|
# Extract a vertex of the output triangle
|
|
pOut = pointsOut[delaunayTri[j][k]]
|
|
# Make sure the vertex is inside the image.
|
|
pOut = constrainPoint(pOut, w, h)
|
|
|
|
# Push the input vertex into input triangle
|
|
tin.append(pIn)
|
|
# Push the output vertex into output triangle
|
|
tout.append(pOut)
|
|
|
|
# Warp pixels inside input triangle to output triangle.
|
|
warpTriangle(imIn, imOut, tin, tout)
|
|
return imOut
|