Average Face

Average face是一項很有趣的電腦視覺技術,它可以將一批不同臉孔的圖片,計算出五官的平均後顯示出來,形成一張所謂的「大眾臉」。Average face有的人翻譯成平均臉,但個人覺得大眾臉似乎更貼切。

這個技術並不稀奇,早在2011年,一位南非的旅行家Mike就曾經把它旅遊各地所拍攝的人像合成在一起,稱為「The Faces Of Tomorrow」,例如,下圖左是在雪梨邦黛海灘(Bondi Beach, Sydney) 遊客的average face,右側則是香港大學。

This is what people look like at Bondi Beach, Sydney Hong Kong University

此外,你應該還記得前幾年曾流行過一時,由不同國家臉孔所平均出來的代表圖像,這些圖片發表自faceresearch.org:

這個faceresearch.org是由蘇格蘭亞伯丁大學(the University of Aberdeen)心理學研究室所所成立、專門以臉部為主題的研究網站,當中就包含了一個人臉平均的互動展示,您也可以玩看看:http://www.faceresearch.org/demos/average

為什麼要瞭解average face

透過average face技術,我們可以掌握如何:

  1. 偵測臉孔
  2. 取得臉部的landmarks
  3. 取得Delaunay_Triangulation或Voronoi_Diagrams
  4. 臉部不同區塊的合成

程式說明

下方的範例程式參考自Satya Mallick, PhD的網路課程「Computer Vision for Faces 2018」 ,如果您有興趣,可click此連結:https://www.learnopencv.com/cv4faces-best-project-award-fall-2018/。

取得landmarks

imagePaths為存放所有人臉的folder,下方程式一張張的取得每張人臉的landmarks,並放置於allPoints,圖片內容則放置於images。

  for imagePath in imagePaths:
    im = cv2.imread(imagePath)
    if im is None:
      print("image:{} not read properly".format(imagePath))
    else:
        points = fbc.getLandmarks(faceDetector, landmarkDetector, cv2.cvtColor(im, cv2.COLOR_BGR2RGB))
        if len(points) > 0:
          allPoints.append(points)

          im = np.float32(im)/255.0
          images.append(im)
        else:
          print("Couldn't detect face landmarks")

此外,除了人臉當中的68個landmarks,我們還需要加上圖片邊框上的8個點,這樣在最後合成人像時,可有比較完整的大頭照,而非僅有臉部五官區域。在原有的68 點landmarks再加入8個點的方式是:(boundaryPoints🡪8個新點)

New_landmarks = np.concatenate((landmark_points, boundaryPoints), axis=0)

New_landmarks = np.concatenate((landmark_points, boundaryPoints), axis=0)

# Dimensions of output image
  w = 600
  h = 600
boundaryPts = fbc.getEightBoundaryPoints(h, w)

臉孔坐標定位

normalizeImagesAndLandmarks()這個function,參考每張臉孔的landmarks,將各個臉部圖片依新的landmarks值定位於該坐標點,例如,範例中左眼的左上方的landmarks會固定於 (0.3 * w, h/3 ),右眼右上角則位於 ( 0.7 * w, h / 3) ,其中,w, h指的是最終輸出圖片的寬與高。

for i, img in enumerate(images):
    points = allPoints[i]
    points = np.array(points)

    img, points = fbc.normalizeImagesAndLandmarks((h, w), img, points)

    # Calculate average landmark locations
    pointsAvg = pointsAvg + (points / (1.0*numImages))

    # Append boundary points. Will be used in Delaunay Triangulation
    points = np.concatenate((points, boundaryPts), axis=0)

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

function:normalizeImagesAndLandmarks

def normalizeImagesAndLandmarks(outSize, imIn, pointsIn):
  h, w = outSize

  eyecornerSrc = [pointsIn[36], pointsIn[45]]
  eyecornerDst = [(np.int(0.3 * w), np.int(h/3)),
                  (np.int(0.7 * w), np.int(h/3))]

  tform = similarityTransform(eyecornerSrc, eyecornerDst)
  imOut = np.zeros(imIn.shape, dtype=imIn.dtype)

  imOut = cv2.warpAffine(imIn, tform, (w, h))
  points2 = np.reshape(pointsIn, (pointsIn.shape[0], 1, pointsIn.shape[1]))
  pointsOut = cv2.transform(points2, tform)
  pointsOut = np.reshape(pointsOut, (pointsIn.shape[0], pointsIn.shape[1]))

  return imOut, pointsOut

最終定位好的72個landmarks(含邊框8個)以及臉部圖片,放置於pointsNorm以及imagesNorm。

取得Delaunay三角剖分區域

calculateDelaunayTriangles function只需要輸出圖的shape以及定位點list,便可輸出其Delaunay triangulation areas。

dt = fbc.calculateDelaunayTriangles(rect, pointsAvg)

function: calculateDelaunayTriangles

def calculateDelaunayTriangles(rect, points):
  subdiv = cv2.Subdiv2D(rect)

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

  triangleList = subdiv.getTriangleList()
  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])

    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)):
          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

合成臉部圖片

output為輸出的最終圖片,而warpImage function在輸入了原圖、原landmarks、定位後的landmarks、以及上一步所取得的dt(Delaunay triangulation)資訊後,便能將原圖的各個Delaunay triangulation area變形為定位後的Delaunay triangulation area。最後,再將每張變形後的Delaunay triangulation area拼接回臉部圖形。

  output = np.zeros((h, w, 3), dtype=np.float)

  # Warp input images to average image landmarks
  for i in range(0, numImages):
    imWarp = fbc.warpImage(imagesNorm[i], pointsNorm[i], pointsAvg.tolist(), dt)
    output = output + imWarp

此function針對輸入delaunay point list進行變形

def warpImage(imIn, pointsIn, pointsOut, delaunayTri):
  h, w, ch = imIn.shape
  # Output image
  imOut = np.zeros(imIn.shape, dtype=imIn.dtype)

  for j in range(0, len(delaunayTri)):
    tin = []
    tout = []

    for k in range(0, 3):
      # Extract a vertex of input triangle
      pIn = pointsIn[delaunayTri[j][k]]
      pIn = constrainPoint(pIn, w, h)

      # Extract a vertex of the output triangle
      pOut = pointsOut[delaunayTri[j][k]]
      pOut = constrainPoint(pOut, w, h)

      tin.append(pIn)
      tout.append(pOut)

 

區塊變形指令使用的是cv2.estimateRigidTransform,但要注意,目前版本的OpenCV已不支援,須改用cv2.estimateAffinePartial2D來替代。

tform = cv2.estimateRigidTransform(np.array([inPts]), np.array([outPts]), False)

  • 改為

tform,_ = cv2.estimateAffinePartial2D(np.array([inPts]), np.array([outPts]))

輸入前後兩種point list,回傳的tform就是cv2.warpAffine的affine transformation參數。

imOut = cv2.warpAffine(imIn, tform, (w, h))

測試

最後,請大家猜猜這兩位是誰?

實際上並沒有這兩個人,而是分別由下面這些公司男女同仁所平均而成的臉部。

由於合成的臉部是經過校準及對稱的,因此,這種對齊後的五官組合起來通常讓人感覺端正美觀,也就是符合一般人所稱的俊男美女的特色。