TestingAI / faceSym.py
npc0's picture
Create faceSym.py
4767e9d
# based on eggplants/face-symmetrizer
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 # type: ignore[import]
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?://" # http:// or https://
# domain...
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|"
r"[A-Z0-9-]{2,}\.?)|"
r"localhost|" # localhost...
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip
r"(?::\d+)?" # optional port
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()