M3Face / utils /plot_landmark.py
m3face's picture
Adding files
332190f
import os
import PIL
import cv2
import pickle
import argparse
import numpy as np
import face_alignment
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.path import Path
def parse_args():
parser = argparse.ArgumentParser(description="Plot facial landmarks from an image.")
parser.add_argument(
"--image_path",
type=str,
default=None,
help="Path to the image file."
)
parser.add_argument("--size", type=int, default=512)
parser.add_argument("--crop", action="store_true", help="Crop around the face image.")
parser.add_argument(
"--output_dir",
type=str,
default="output/landmarks/",
help="Folder to save landmark images."
)
args = parser.parse_args()
return args
def get_patch(landmarks, color='lime', closed=False):
contour = landmarks
ops = [Path.MOVETO] + [Path.LINETO]*(len(contour)-1)
facecolor = (0, 0, 0, 0) # Transparent fill color, if open
if closed:
contour.append(contour[0])
ops.append(Path.CLOSEPOLY)
facecolor = color
path = Path(contour, ops)
return patches.PathPatch(path, facecolor=facecolor, edgecolor=color, lw=4)
def bbox_from_landmarks(landmarks):
landmarks_x, landmarks_y = zip(*landmarks)
x_min, x_max = min(landmarks_x), max(landmarks_x)
y_min, y_max = min(landmarks_y), max(landmarks_y)
width = x_max - x_min
height = y_max - y_min
# Give it a little room; I think it works anyway
x_min -= 25
y_min -= 25
width += 50
height += 50
bbox = (x_min, y_min, width, height)
return bbox
def plot_landmarks(landmarks, crop=False, size=512):
if crop:
(x_min, y_min, width, height) = bbox_from_landmarks(landmarks)
# print(x_min, y_min, width, height)
landmarks_np = np.array(landmarks)
landmarks_np[:, 0] = (landmarks_np[:, 0] - x_min) * size / width
landmarks_np[:, 1] = (landmarks_np[:, 1] - y_min) * size / height
landmarks = landmarks_np.tolist()
# Precisely control output image size
dpi = 72
fig, ax = plt.subplots(1, figsize=[size/dpi, size/dpi], tight_layout={'pad':0})
fig.set_dpi(dpi)
black = np.zeros((size, size, 3))
ax.imshow(black)
face_patch = get_patch(landmarks[0:17])
l_eyebrow = get_patch(landmarks[17:22], color='yellow')
r_eyebrow = get_patch(landmarks[22:27], color='yellow')
nose_v = get_patch(landmarks[27:31], color='orange')
nose_h = get_patch(landmarks[31:36], color='orange')
l_eye = get_patch(landmarks[36:42], color='magenta', closed=True)
r_eye = get_patch(landmarks[42:48], color='magenta', closed=True)
outer_lips = get_patch(landmarks[48:60], color='cyan', closed=True)
inner_lips = get_patch(landmarks[60:68], color='blue', closed=True)
ax.add_patch(face_patch)
ax.add_patch(l_eyebrow)
ax.add_patch(r_eyebrow)
ax.add_patch(nose_v)
ax.add_patch(nose_h)
ax.add_patch(l_eye)
ax.add_patch(r_eye)
ax.add_patch(outer_lips)
ax.add_patch(inner_lips)
plt.axis('off')
fig.canvas.draw()
buffer, (width, height) = fig.canvas.print_to_buffer()
assert width == height
assert width == size
buffer = np.frombuffer(buffer, np.uint8).reshape((height, width, 4))
buffer = buffer[:, :, 0:3]
plt.close(fig)
return PIL.Image.fromarray(buffer)
def get_landmarks(image):
fa = face_alignment.FaceAlignment(face_alignment.LandmarksType.TWO_D, flip_input=False, face_detector='sfd')
faces = fa.get_landmarks_from_image(image)
if faces is None or len(faces) == 0:
return None
landmarks = faces[0]
return landmarks
def save_landmarks(args):
os.makedirs(args.output_dir, exist_ok=True)
image_name = os.path.basename(args.image_path)
image = cv2.imread(args.image_path)
image = cv2.resize(image, (args.size, args.size))
landmarks = get_landmarks(image)
if landmarks is None:
print(f'No faces found in {image_name}')
return
filename = f'{args.output_dir}/{image_name}'
if args.crop:
landmarks_cropped_image = plot_landmarks(landmarks.tolist(), crop=True, size=args.size)
landmarks_cropped_image.save(filename)
else:
landmarks_image = plot_landmarks(landmarks.tolist(), size=args.size)
landmarks_image.save(filename)
print(f'Landmark saved in {filename}')
if __name__ == '__main__':
args = parse_args()
save_landmarks(args)