Hôm nay, Ngô Tôn .IT sẽ cùng các bạn tìm hiểu cách biến mặt này thành mặt khác bằng OpenCV – Face Morphing.
Để thực hiện bài này, các bạn nên tìm hiểu trước về 2 bài toán này đó là: Facial Landmark Detection và Delaunay Triangulation.
Morphing là gì?
Morphing là một kỹ thuật xử lý hình ảnh được sử dụng để biến đổi trạng thái từ một hình ảnh này sang hình ảnh khác.
Ý tưởng đằng sau Image Morphing khá đơn giản. Đưa ra hai hình ảnh I và J, chúng ta muốn tạo một hình ảnh ở giữa M bằng cách trộn các hình ảnh I và J. Sự pha trộn giữa ảnh I và J được kiểm soát bởi một tham số α nằm trong khoảng từ 0 đến 1. Khi α = 0 thì ảnh M sẽ giống ảnh I và khi α = 1 ảnh M sẽ giống ảnh J. Như vậy, chúng ta có thể áp dụng cho từng pixel (x, y):
M(x, y) = (1 – α)I(x, y) + αJ(x, y)
Vì vậy, để biến hình ảnh I thành hình ảnh J, trước tiên chúng ta cần thiết lập sự tương ứng pixel giữa hai hình ảnh. Nói cách khác, đối với mỗi pixel (xi, yi) trong hình ảnh I, chúng ta cần tìm pixel (xj, yj) tương ứng trong hình ảnh J. Giả sử chúng ta đã tìm thấy những điểm tương ứng này, chúng ta có thể kết hợp các hình ảnh theo hai bước:
- Đầu tiên, chúng ta cần tính toán vị trí (xm, ym) của pixel trong hình ảnh được biến đổi. Nó được đưa ra bởi phương trình sau (1):
- xm = (1 – α) xi + α xj
- ym = (1 – α) yi + α yj
- Sau đó, chúng ta cần tìm cường độ của pixel tại (xm, ym) bằng công thức sau (2):
- M(xm, ym) = (1 – α)I(xi, yi) + αJ(xj, yj)
Rất dễ dàng để tìm thấy một vài điểm tương ứng. Để biến hình hai đối tượng khác nhau, chẳng hạn như mặt mèo và mặt người, chúng ta có thể nhấp vào một vài điểm trên hai hình ảnh để thiết lập sự tương ứng và nội suy kết quả cho các pixel còn lại. Tiếp theo chúng ta sẽ xem Face Morphing được thực hiện chi tiết như thế nào, nhưng kỹ thuật tương tự có thể được áp dụng cho hai đối tượng bất kỳ.
Face Morphing
Biến đổi hai khuôn mặt (Face Morphing) có thể được thực hiện theo các bước sau. Để đơn giản, chúng ta sẽ cho rằng các hình ảnh có cùng kích thước, nhưng nó không phải là điều cần thiết.
1. Tìm điểm tương ứng bằng cách sử dụng tính năng phát hiện đặc điểm khuôn mặt (Facial Feature Detection)
Hãy bắt đầu bằng cách đạt được điểm tương ứng.
Đầu tiên, chúng ta có thể nhận được rất nhiều điểm bằng cách tự động (hoặc thủ công) bằng Facial Landmark Detection. Mình đã sử dụng dlib để phát hiện 68 điểm tương ứng. Các bạn cài đặt thư viện dlib và tải về file shape_predictor_68_face_landmarks.dat
Tiếp theo, chúng ta thêm bốn điểm nữa (một điểm ở tai bên phải, một điểm ở cổ và hai điểm ở vai).
Cuối cùng, thêm các góc của hình ảnh và nửa điểm giữa các góc đó làm điểm tương ứng.
face_landmark_detection.py
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
import sys import os import dlib import glob import numpy as np from skimage import io import cv2 def doCropping(theImage1,theImage2): if(isinstance(theImage1,str)): img1=cv2.imread(theImage1) else: img1=cv2.imdecode(np.fromstring(theImage1.read(), np.uint8),1) if(isinstance(theImage2,str)): img2=cv2.imread(theImage2) else: img2=cv2.imdecode(np.fromstring(theImage2.read(), np.uint8),1) size1=img1.shape size2=img2.shape diff0=(size1[0]-size2[0])//2 diff1=(size1[1]-size2[1])//2 avg0=(size1[0]+size2[0])//2 avg1=(size1[1]+size2[1])//2 if(size1[0]==size2[0] and size1[1]==size2[1]): return [img1,img2] elif(size1[0]<=size2[0] and size1[1]<=size2[1]): scale0=size1[0]/size2[0] scale1=size1[1]/size2[1] if(scale0>scale1): res=cv2.resize(img2,None,scale0,scale0,interpolation=cv2.INTER_AREA) else: res=cv2.resize(img2,None,scale1,scale1,interpolation=cv2.INTER_AREA) return doCroppingHelp(img1,res) elif(size1[0]>=size2[0] and size1[1]>=size2[1]): scale0=size2[0]/size1[0] scale1=size2[1]/size1[1] if(scale0>scale1): res=cv2.resize(img1,None,scale0,scale0,interpolation=cv2.INTER_AREA) else: res=cv2.resize(img1,None,scale1,scale1,interpolation=cv2.INTER_AREA) return doCroppingHelp(res,img2) elif(size1[0]>=size2[0] and size1[1]<=size2[1]): return [img1[diff0:avg0,:],img2[:,-diff1:avg1]] else: return [img1[:,diff1:avg1],img2[-diff0:avg0,:]] def doCroppingHelp(img1,img2): size1=img1.size size2=img2.size diff0=(size1[0]-size2[0])//2 diff1=(size1[1]-size2[1])//2 avg0=(size1[0]+size2[0])//2 avg1=(size1[1]+size2[1])//2 if(size1[0]==size2[0] and size1[1]==size2[1]): return [img1,img2] elif(size1[0]<=size2[0] and size1[1]<=size2[1]): return [img1,img2[-diff0:avg0,-diff1:avg1]] elif(size1[0]>=size2[0] and size1[1]>=size2[1]): return [img1[diff0:avg0,diff1:avg1],img2] elif(size1[0]>=size2[0] and size1[1]<=size2[1]): return [img1[diff0:avg0,:],img2[:,-diff1:avg1]] else: return [img1[:,diff1:avg1],img2[-diff0:avg0,:]] def makeCorrespondence(thePredictor,theImage1,theImage2): # Detect the points of face. predictor_path = thePredictor detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor(predictor_path) # Setting up some initial values. array = np.zeros((68,2)) size=(0,0) imgList=doCropping(theImage1,theImage2) list1=[] list2=[] j=1 for img in imgList: size=(img.shape[0],img.shape[1]) if(j==1): currList=list1 else: currList=list2 # Ask the detector to find the bounding boxes of each face. The 1 in the # second argument indicates that we should upsample the image 1 time. This # will make everything bigger and allow us to detect more faces. # Also give error if face is not found. dets = detector(img, 1) if(len(dets)==0): if(isinstance(f,str)): return [[0,f],0,0,0,0,0] else: return [[0,"No. "+str(j)],0,0,0,0,0] j=j+1 for k, d in enumerate(dets): # Get the landmarks/parts for the face in box d. shape = predictor(img, d) for i in range(0,68): currList.append((int(shape.part(i).x),int(shape.part(i).y))) array[i][0]+=shape.part(i).x array[i][1]+=shape.part(i).y currList.append((1,1)) currList.append((size[1]-1,1)) currList.append(((size[1]-1)//2,1)) currList.append((1,size[0]-1)) currList.append((1,(size[0]-1)//2)) currList.append(((size[1]-1)//2,size[0]-1)) currList.append((size[1]-1,size[0]-1)) currList.append(((size[1]-1)//2,(size[0]-1)//2)) narray=array/2 narray=np.append(narray,[[1,1]],axis=0) narray=np.append(narray,[[size[1]-1,1]],axis=0) narray=np.append(narray,[[(size[1]-1)//2,1]],axis=0) narray=np.append(narray,[[1,size[0]-1]],axis=0) narray=np.append(narray,[[1,(size[0]-1)//2]],axis=0) narray=np.append(narray,[[(size[1]-1)//2,size[0]-1]],axis=0) narray=np.append(narray,[[size[1]-1,size[0]-1]],axis=0) narray=np.append(narray,[[(size[1]-1)//2,(size[0]-1)//2]],axis=0) return [size,imgList[0],imgList[1],list1,list2,narray] |
2. Tam giác phân Delaunay (Delaunay Triangulation)
Từ bước trước, chúng ta có hai bộ 80 điểm – một bộ cho mỗi hình ảnh. Chúng ta có thể tính giá trị trung bình của các điểm tương ứng trong hai tập hợp và thu được một tập hợp duy nhất là 80 điểm. Trên tập hợp các điểm trung bình này, chúng ta thực hiện Delaunay Triangulation. Kết quả của tam giác Delaunay là một danh sách các tam giác được biểu diễn bằng các chỉ số của các điểm trong mảng 80 điểm. Trong trường hợp cụ thể này, phép tam giác tạo ra 149 tam giác nối 80 điểm.
Phương thức tam giác được lưu trữ dưới dạng một mảng ba cột.
Triangulation Points |
39 41 38 |
36 31 29 |
38 37 21 |
… |
Nó cho thấy rằng các điểm 39, 41 và 38 tạo thành một tam giác, v.v.
delaunay.py
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 53 54 55 |
import cv2 import numpy as np import random def makeDelaunay(theSize1,theSize0,theList): # Check if a point is inside a rectangle def rect_contains(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 # Write the delaunay triangles into a file def draw_delaunay(subdiv,dictionary1) : list4=[] triangleList = subdiv.getTriangleList(); r = (0, 0, theSize1,theSize0) for t in triangleList : pt1 = (int(t[0]), int(t[1])) pt2 = (int(t[2]), int(t[3])) pt3 = (int(t[4]), int(t[5])) if rect_contains(r, pt1) and rect_contains(r, pt2) and rect_contains(r, pt3) : list4.append((dictionary1[pt1],dictionary1[pt2],dictionary1[pt3])) dictionary1={} return list4 # Make a rectangle. rect = (0, 0, theSize1,theSize0) # Create an instance of Subdiv2D. subdiv = cv2.Subdiv2D(rect); # Make a points list and a searchable dictionary. theList=theList.tolist() points=[(int(x[0]),int(x[1])) for x in theList] dictionary={x[0]:x[1] for x in list(zip(points,range(76)))} # Insert points into subdiv for p in points : subdiv.insert(p) # Make a delaunay triangulation list. list4=draw_delaunay(subdiv,dictionary); # Return the list. return list4 |
3. Nắn ảnh và pha trộn (Warping images and alpha blending)
Bây giờ, chúng ta có thể kết hợp hai hình ảnh một cách thông minh. Như đã đề cập trước đây, lượng pha trộn sẽ được kiểm soát bởi một tham số α.
Tạo một hình thái bằng cách sử dụng các bước sau.
- Tìm vị trí của các điểm đối tượng trong hình ảnh được biến đổi: Trong hình ảnh được biến đổi, chúng ta có thể tìm vị trí của tất cả 80 điểm bằng cách sử dụng phương trình (1).
- Tính các phép biến đổi affine: Vậy ta có tập hợp 80 điểm trong hình 1, một tập hợp khác gồm 80 điểm trong hình 2 và tập hợp thứ ba gồm 80 điểm trong hình được biến đổi. Chúng ta cũng biết tam giác xác định trên những điểm này. Chọn một tam giác trong hình 1 và tam giác tương ứng trong hình được biến đổi và tính phép biến đổi affine ánh xạ ba góc của tam giác trong hình 1 với ba góc của tam giác tương ứng trong hình được biến đổi. Trong OpenCV, điều này có thể được thực hiện bằng cách sử dụng getAffineTransform. Tính một phép biến đổi affine cho mọi cặp 149 tam giác. Cuối cùng, lặp lại quy trình của hình ảnh 2 và hình ảnh đã biến hình.
- Nắn các hình tam giác: Đối với mỗi hình tam giác trong hình ảnh 1, sử dụng phép biến đổi affine được tính ở bước trước để biến đổi tất cả các pixel bên trong hình tam giác thành hình ảnh được biến đổi. Lặp lại điều này cho tất cả các hình tam giác trong hình 1 để có được phiên bản cong vênh của hình 1. Tương tự, có được phiên bản cong cho hình 2. Trong OpenCV, điều này đạt được bằng cách sử dụng hàm warpAffine. Tuy nhiên, warpAffine có hình ảnh chứ không phải hình tam giác. Bí quyết là tính toán hộp giới hạn cho hình tam giác, làm cong tất cả các pixel bên trong hộp giới hạn bằng cách sử dụng warpAffine, sau đó che các pixel bên ngoài hình tam giác. Mặt nạ hình tam giác được tạo bằng fillConvexPoly. Đảm bảo sử dụng blendMode BORDER_REFLECT_101 trong khi sử dụng warpAffine. Nó che giấu các đường nối tốt hơn.
- Nắn ảnh pha trộn alpha: Trong bước trước, chúng ta đã thu được phiên bản chỉnh của hình ảnh 1 và hình ảnh 2. Hai hình ảnh này có thể được trộn alpha bằng cách sử dụng phương trình (2) và đây là hình ảnh biến đổi cuối cùng.
face_morph.py
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
import numpy as np import cv2 import sys import os import math from subprocess import Popen, PIPE from PIL import Image # 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 morphTriangle(img1, img2, img, t1, t2, t, alpha) : # Find bounding rectangle for each triangle r1 = cv2.boundingRect(np.float32([t1])) r2 = cv2.boundingRect(np.float32([t2])) r = cv2.boundingRect(np.float32([t])) # Offset points by left top corner of the respective rectangles t1Rect = [] t2Rect = [] tRect = [] for i in range(0, 3): tRect.append(((t[i][0] - r[0]),(t[i][1] - r[1]))) t1Rect.append(((t1[i][0] - r1[0]),(t1[i][1] - r1[1]))) t2Rect.append(((t2[i][0] - r2[0]),(t2[i][1] - r2[1]))) # Get mask by filling triangle mask = np.zeros((r[3], r[2], 3), dtype = np.float32) cv2.fillConvexPoly(mask, np.int32(tRect), (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]] img2Rect = img2[r2[1]:r2[1] + r2[3], r2[0]:r2[0] + r2[2]] size = (r[2], r[3]) warpImage1 = applyAffineTransform(img1Rect, t1Rect, tRect, size) warpImage2 = applyAffineTransform(img2Rect, t2Rect, tRect, size) # Alpha blend rectangular patches imgRect = (1.0 - alpha) * warpImage1 + alpha * warpImage2 # Copy triangular region of the rectangular patch to the output image img[r[1]:r[1]+r[3], r[0]:r[0]+r[2]] = img[r[1]:r[1]+r[3], r[0]:r[0]+r[2]] * ( 1 - mask ) + imgRect * mask def makeMorphs(theDuration,theFrameRate,theImage1,theImage2,theList1,theList2,theList4,size,theResult): totalImages=int(theDuration*theFrameRate) p = Popen(['ffmpeg', '-y', '-f', 'image2pipe', '-r', str(theFrameRate),'-s',str(size[1])+'x'+str(size[0]), '-i', '-', '-c:v', 'libx264', '-crf', '25','-vf','scale=trunc(iw/2)*2:trunc(ih/2)*2','-pix_fmt','yuv420p', theResult], stdin=PIPE) for j in range(0,totalImages): # Read images img1 = theImage1 img2 = theImage2 # Convert Mat to float data type img1 = np.float32(img1) img2 = np.float32(img2) # Read array of corresponding points points1 = theList1 points2 = theList2 points = []; alpha=j/(totalImages-1) # Compute weighted average point coordinates for i in range(0, len(points1)): x = ( 1 - alpha ) * points1[i][0] + alpha * points2[i][0] y = ( 1 - alpha ) * points1[i][1] + alpha * points2[i][1] points.append((x,y)) # Allocate space for final output imgMorph = np.zeros(img1.shape, dtype = img1.dtype) # Read triangles from delaunay_output.txt for i in range(len(theList4)): x = int(theList4[i][0]) y = int(theList4[i][1]) z = int(theList4[i][2]) t1 = [points1[x], points1[y], points1[z]] t2 = [points2[x], points2[y], points2[z]] t = [ points[x], points[y], points[z] ] # Morph one triangle at a time. morphTriangle(img1, img2, imgMorph, t1, t2, t, alpha) temp_res=cv2.cvtColor(np.uint8(imgMorph),cv2.COLOR_BGR2RGB) res=Image.fromarray(temp_res) res.save(p.stdin,'JPEG') p.stdin.close() p.wait() |
Vậy là xong, chúng ta cùng xem kết quả nhé
Leave a Reply