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