Created
August 4, 2021 23:41
-
-
Save cunhafh/edb2e1ca74d0d67197c546dc91325d65 to your computer and use it in GitHub Desktop.
Iris detection and color change
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import cv2 | |
import dlib | |
import faceBlendCommon as face | |
import numpy as np | |
# Load Image | |
im = cv2.imread("cv2/images/girl.jpg") | |
# Detect face landmarks | |
PREDICTOR_PATH = r"C:\Users\felipe.cunha\Documents\venv\cv2\week1-pyton\data\models\shape_predictor_68_face_landmarks.dat" | |
faceDetector = dlib.get_frontal_face_detector() | |
landmarkDetector = dlib.shape_predictor(PREDICTOR_PATH) | |
landmarks = face.getLandmarks(faceDetector, landmarkDetector, im) | |
def createEyeMask(eyeLandmarks, im): | |
leftEyePoints = eyeLandmarks | |
eyeMask = np.zeros_like(im) | |
cv2.fillConvexPoly(eyeMask, np.int32(leftEyePoints), (255, 255, 255)) | |
eyeMask = np.uint8(eyeMask) | |
return eyeMask | |
def findIris(eyeMask, im, thresh): | |
r = im[:,:,2] | |
_, binaryIm = cv2.threshold(r, thresh, 255, cv2.THRESH_BINARY_INV) | |
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (4,4)) | |
morph = cv2.dilate(binaryIm, kernel, 1) | |
morph = cv2.merge((morph, morph, morph)) | |
morph = morph.astype(float)/255 | |
eyeMask = eyeMask.astype(float)/255 | |
iris = cv2.multiply(eyeMask, morph) | |
return iris | |
def findCentroid(iris): | |
M = cv2.moments(iris[:,:,0]) | |
cX = int(M["m10"] / M["m00"]) | |
cY = int(M["m01"] / M["m00"]) | |
centroid = (cX,cY) | |
return centroid | |
def createIrisMask(iris, centroid): | |
cnts, _ = cv2.findContours(np.uint8(iris[:,:,0]), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
flag = 10000 | |
final_cnt = None | |
for cnt in cnts: | |
(x,y),radius = cv2.minEnclosingCircle(cnt) | |
distance = abs(centroid[0]-x)+abs(centroid[1]-y) | |
if distance < flag : | |
flag = distance | |
final_cnt = cnt | |
else: | |
continue | |
(x,y),radius = cv2.minEnclosingCircle(final_cnt) | |
center = (int(x),int(y)) | |
radius = int(radius) - 2 | |
irisMask = np.zeros_like(iris) | |
inverseIrisMask = np.ones_like(iris)*255 | |
cv2.circle(irisMask,center,radius,(255, 255, 255),-1) | |
cv2.circle(inverseIrisMask,center,radius,(0, 0, 0),-1) | |
irisMask = cv2.GaussianBlur(irisMask, (5,5), cv2.BORDER_DEFAULT) | |
inverseIrisMask = cv2.GaussianBlur(inverseIrisMask, (5,5), cv2.BORDER_DEFAULT) | |
return irisMask, inverseIrisMask | |
def changeEyeColor(im, irisMask, inverseIrisMask): | |
imCopy = cv2.applyColorMap(im, cv2.COLORMAP_TWILIGHT_SHIFTED) | |
imCopy = imCopy.astype(float)/255 | |
irisMask = irisMask.astype(float)/255 | |
inverseIrisMask = inverseIrisMask.astype(float)/255 | |
im = im.astype(float)/255 | |
faceWithoutEye = cv2.multiply(inverseIrisMask, im) | |
newIris = cv2.multiply(irisMask, imCopy) | |
result = faceWithoutEye + newIris | |
return result | |
def float642Uint8(im): | |
im2Convert = im.astype(np.float64) / np.amax(im) | |
im2Convert = 255 * im2Convert | |
convertedIm = im2Convert.astype(np.uint8) | |
return convertedIm | |
# Create eye mask using eye landmarks from facial landmark detection | |
leftEyeMask = createEyeMask(landmarks[36:42], im) | |
rightEyeMask = createEyeMask(landmarks[43:49], im) | |
# Find the iris by thresholding the red channel of the image within the boundaries of the eye mask | |
leftIris = findIris(leftEyeMask, im, 40) | |
rightIris = findIris(rightEyeMask, im, 50) | |
# Find the centroid of the binary image of the eye | |
leftIrisCentroid = findCentroid(leftIris) | |
rightIrisCentroid = findCentroid(rightIris) | |
# Generate the iris mask and its inverse mask | |
leftIrisMask, leftInverseIrisMask = createIrisMask(leftIris, leftIrisCentroid) | |
rightIrisMask, rightInverseIrisMask = createIrisMask(rightIris, rightIrisCentroid) | |
# Change the eye color and merge it to the original image | |
coloredEyesLady = changeEyeColor(im, rightIrisMask, rightInverseIrisMask) | |
coloredEyesLady = float642Uint8(coloredEyesLady) | |
coloredEyesLady = changeEyeColor(coloredEyesLady, leftIrisMask, leftInverseIrisMask) | |
# Present results | |
cv2.imshow("", coloredEyesLady) | |
cv2.waitKey(0) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
faceBlendCommon is a file
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(60math.pi/180)
c60 = math.cos(60math.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.estimateRigidTransform(np.array([inPts]), np.array([outPts]), False)
return tform
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
eyecornerSrc = [pointsIn[36], pointsIn[45]]
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
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]))
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()
]
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 = []
return imOut