|
|
|
from __future__ import annotations |
|
|
|
import io |
|
import re |
|
from copy import copy |
|
from os import path |
|
from typing import Any, Dict, List, Tuple |
|
from urllib.request import urlopen |
|
|
|
import face_recognition |
|
import numpy as np |
|
from PIL import Image, ImageDraw, ImageOps |
|
|
|
PILImage = Image.Image |
|
FaceLandmarks = List[Dict[str, List[Tuple[Any, ...]]]] |
|
|
|
|
|
class FaceIsNotDetected(Exception): |
|
"""[summary] |
|
|
|
Args: |
|
Exception ([type]): [description] |
|
""" |
|
|
|
pass |
|
|
|
|
|
class FaceSym: |
|
"""[summary]""" |
|
|
|
SimImages = Tuple[PILImage, PILImage, PILImage, PILImage, PILImage, PILImage] |
|
|
|
def __init__(self, img_location: str) -> None: |
|
"""[summary] |
|
|
|
Args: |
|
img_location (str): [description] |
|
|
|
Raises: |
|
ValueError: [description] |
|
""" |
|
self.f_img: np.ndarray[Any, Any] |
|
self.image_location = img_location |
|
if self.__is_valid_url(img_location): |
|
self.__load_from_url(img_location) |
|
elif path.isfile(img_location): |
|
self.__load_from_local(img_location) |
|
else: |
|
raise ValueError( |
|
f"{repr(img_location)} is not a valid location of an image." |
|
) |
|
|
|
self.f_img_PIL = Image.fromarray(self.f_img) |
|
self.image_size: tuple[int, int] = self.f_img_PIL.size |
|
self.face_locations = face_recognition.face_locations(self.f_img) |
|
self.face_landmarks = face_recognition.face_landmarks(self.f_img) |
|
self.mid_face_locations = self.__get_mid_face_locations(self.face_landmarks) |
|
self.face_count = len(self.face_locations) |
|
|
|
def get_cropped_face_images(self,) -> list[PILImage]: |
|
"""[summary] |
|
|
|
Returns: |
|
List[PILImage]: [description] |
|
""" |
|
images = [] |
|
for face_location in self.face_locations: |
|
top, right, bottom, left = face_location |
|
cropped_face_img = self.f_img[top:bottom, left:right] |
|
pil_img = Image.fromarray(cropped_face_img) |
|
|
|
images.append(pil_img) |
|
|
|
return images |
|
|
|
def get_face_box_drawed_image(self) -> PILImage: |
|
"""[summary] |
|
|
|
Returns: |
|
PILImage: [description] |
|
""" |
|
pil = copy(self.f_img_PIL) |
|
draw = ImageDraw.Draw(pil) |
|
for idx, (top, right, bottom, left) in enumerate(self.face_locations): |
|
name = str(f"{idx:02d}") |
|
mid_face = self.mid_face_locations[idx] |
|
|
|
draw.rectangle(((left, top), (right, bottom)), outline=(0, 0, 255)) |
|
|
|
_, text_height = draw.textsize(name) |
|
draw.rectangle( |
|
((left, bottom - text_height - 10), (right, bottom)), |
|
fill=(0, 0, 255), |
|
outline=(0, 0, 255), |
|
) |
|
draw.text((left + 6, bottom - text_height - 5), name, fill=(255, 255, 255)) |
|
|
|
draw.line( |
|
((mid_face[0], -10), mid_face, (mid_face[0], self.image_size[0])), |
|
fill=(255, 255, 0), |
|
width=10, |
|
) |
|
del draw |
|
return pil |
|
|
|
def get_full_image( |
|
self, is_pil: bool = False |
|
) -> np.ndarray[Any, Any] | PILImage: |
|
"""[summary] |
|
|
|
Args: |
|
is_pil (bool, optional): [description]. Defaults to False. |
|
|
|
Returns: |
|
Union[np.ndarray, PILImage]: [description] |
|
""" |
|
|
|
if is_pil: |
|
return self.f_img_PIL |
|
else: |
|
return self.f_img |
|
|
|
def get_symmetrized_images(self, idx: int = 0) -> SimImages: |
|
"""[summary] |
|
|
|
Args: |
|
idx (int, optional): [description]. Defaults to 0. |
|
|
|
Returns: |
|
SimImages: [description] |
|
""" |
|
|
|
def get_concat_h(im1: PILImage, im2: PILImage) -> PILImage: |
|
dst = Image.new("RGB", (im1.width + im2.width, im1.height)) |
|
dst.paste(im1, (0, 0)) |
|
dst.paste(im2, (im1.width, 0)) |
|
return dst |
|
|
|
face_count = len(self.mid_face_locations) |
|
if face_count < 1: |
|
raise FaceIsNotDetected |
|
elif face_count <= idx: |
|
raise IndexError(f"0 <= idx <= {face_count - 1}") |
|
else: |
|
mid_face = self.mid_face_locations[idx] |
|
|
|
cropped_left_img = self.f_img[0 : self.image_size[1], 0 : int(mid_face[0])] |
|
cropped_right_img = self.f_img[ |
|
0 : self.image_size[1], int(mid_face[0]) : self.image_size[0] |
|
] |
|
|
|
pil_img_left = Image.fromarray(cropped_left_img) |
|
pil_img_left_mirrored = ImageOps.mirror(pil_img_left) |
|
pil_img_left_inner = get_concat_h(pil_img_left, pil_img_left_mirrored) |
|
pil_img_left_outer = get_concat_h(pil_img_left_mirrored, pil_img_left) |
|
|
|
pil_img_right = Image.fromarray(cropped_right_img) |
|
pil_img_right_mirrored = ImageOps.mirror(pil_img_right) |
|
pil_img_right_inner = get_concat_h(pil_img_right_mirrored, pil_img_right) |
|
pil_img_right_outer = get_concat_h(pil_img_right, pil_img_right_mirrored) |
|
|
|
return ( |
|
pil_img_left, |
|
pil_img_left_inner, |
|
pil_img_left_outer, |
|
pil_img_right, |
|
pil_img_right_inner, |
|
pil_img_right_outer, |
|
) |
|
|
|
def __load_from_url(self, url: str) -> None: |
|
"""[summary] |
|
|
|
Args: |
|
url (str): [description] |
|
|
|
Raises: |
|
ValueError: [description] |
|
""" |
|
if not self.__is_valid_url(url): |
|
raise ValueError(f"{repr(url)} is not valid url") |
|
else: |
|
img_data = io.BytesIO(urlopen(url).read()) |
|
self.f_img = face_recognition.load_image_file(img_data) |
|
|
|
def __load_from_local(self, path_: str) -> None: |
|
if path.isfile(path_): |
|
self.f_img = face_recognition.load_image_file(path_) |
|
|
|
@staticmethod |
|
def __is_valid_url(url: str) -> bool: |
|
"""[summary] |
|
|
|
Args: |
|
url (str): [description] |
|
|
|
Returns: |
|
bool: [description] |
|
|
|
Note: |
|
Copyright (c) Django Software Foundation and individual |
|
contributors. All rights reserved. |
|
""" |
|
regex = re.compile( |
|
r"^(?:http|ftp)s?://" |
|
|
|
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|" |
|
r"[A-Z0-9-]{2,}\.?)|" |
|
r"localhost|" |
|
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" |
|
r"(?::\d+)?" |
|
r"(?:/?|[/?]\S+)$", |
|
re.IGNORECASE, |
|
) |
|
return re.match(regex, url) is not None |
|
|
|
@staticmethod |
|
def __get_mid_face_locations( |
|
face_landmarks: FaceLandmarks, |
|
) -> list[tuple[int, int]]: |
|
"""[summary] |
|
|
|
Args: |
|
face_landmarks (FaceLandmarks): [description] |
|
|
|
Returns: |
|
List[Tuple[int, int]]: [description] |
|
""" |
|
|
|
def mean(lst: list[int]) -> int: |
|
return int(sum(lst) / len(lst)) |
|
|
|
mid_faces = [] |
|
for face_landmark in face_landmarks: |
|
if not ("left_eye" in face_landmark and "right_eye" in face_landmark): |
|
raise ValueError("eye locations was missing.") |
|
l_e_xs = [i[0] for i in face_landmark["left_eye"]] |
|
l_e_ys = [i[1] for i in face_landmark["left_eye"]] |
|
r_e_xs = [i[0] for i in face_landmark["right_eye"]] |
|
r_e_ys = [i[1] for i in face_landmark["right_eye"]] |
|
mid_face = ( |
|
(mean(l_e_xs) + mean(r_e_xs)) // 2, |
|
(mean(l_e_ys) + mean(r_e_ys)) // 2, |
|
) |
|
mid_faces.append(mid_face) |
|
return mid_faces |
|
|
|
|
|
def main() -> None: |
|
"""[summary]""" |
|
data = list( |
|
map( |
|
lambda x: "https://pbs.twimg.com/media/%s?format=jpg" % x, |
|
[ |
|
"E7okHDEVUAE1O6i", |
|
"E7jaibgUcAUWvg-", |
|
"E7jahEbUcAMNLdU", |
|
"E7Jqli9VEAEStvs", |
|
"E7Jqk-aUcAcfg3o", |
|
"E7EhGi2XoAsMrO5", |
|
"E5dhLccUYAUD5Yx", |
|
"E5TOAqUVUAMckXT", |
|
"E4vK6e0VgAAksnK", |
|
"E4Va7u4VkAAKde3", |
|
"E4A0ksEUYAIpynP", |
|
"E3xXzcyUYAIX1dC", |
|
"E2zkvONVcAQEE_S", |
|
"E1cBsxDUcAIe_LZ", |
|
"E1W4HTRVUAgYkmo", |
|
"E1HbVAeVIAId5yP", |
|
"E09INVFUcAYpcWo", |
|
"E0oh0hmUUAAfJV9", |
|
], |
|
) |
|
) |
|
success, fail = 0, 0 |
|
for idx, link in enumerate(data): |
|
print(f"[{idx:02d}]", link, end="") |
|
f = FaceSym(link) |
|
if f.face_count != 0: |
|
print("=>Detected") |
|
f.get_symmetrized_images() |
|
success += 1 |
|
else: |
|
print("=>Not Detected") |
|
fail += 1 |
|
|
|
else: |
|
print(f"DATA: {len(data)}", f"OK: {success}", f"NG: {fail}") |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |