npc0 commited on
Commit
4767e9d
1 Parent(s): 5a35d53

Create faceSym.py

Browse files
Files changed (1) hide show
  1. faceSym.py +294 -0
faceSym.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # based on eggplants/face-symmetrizer
2
+ from __future__ import annotations
3
+
4
+ import io
5
+ import re
6
+ from copy import copy
7
+ from os import path
8
+ from typing import Any, Dict, List, Tuple
9
+ from urllib.request import urlopen
10
+
11
+ import face_recognition # type: ignore[import]
12
+ import numpy as np
13
+ from PIL import Image, ImageDraw, ImageOps
14
+
15
+ PILImage = Image.Image
16
+ FaceLandmarks = List[Dict[str, List[Tuple[Any, ...]]]]
17
+
18
+
19
+ class FaceIsNotDetected(Exception):
20
+ """[summary]
21
+
22
+ Args:
23
+ Exception ([type]): [description]
24
+ """
25
+
26
+ pass
27
+
28
+
29
+ class FaceSym:
30
+ """[summary]"""
31
+
32
+ SimImages = Tuple[PILImage, PILImage, PILImage, PILImage, PILImage, PILImage]
33
+
34
+ def __init__(self, img_location: str) -> None:
35
+ """[summary]
36
+
37
+ Args:
38
+ img_location (str): [description]
39
+
40
+ Raises:
41
+ ValueError: [description]
42
+ """
43
+ self.f_img: np.ndarray[Any, Any]
44
+ self.image_location = img_location
45
+ if self.__is_valid_url(img_location):
46
+ self.__load_from_url(img_location)
47
+ elif path.isfile(img_location):
48
+ self.__load_from_local(img_location)
49
+ else:
50
+ raise ValueError(
51
+ f"{repr(img_location)} is not a valid location of an image."
52
+ )
53
+
54
+ self.f_img_PIL = Image.fromarray(self.f_img)
55
+ self.image_size: tuple[int, int] = self.f_img_PIL.size
56
+ self.face_locations = face_recognition.face_locations(self.f_img)
57
+ self.face_landmarks = face_recognition.face_landmarks(self.f_img)
58
+ self.mid_face_locations = self.__get_mid_face_locations(self.face_landmarks)
59
+ self.face_count = len(self.face_locations)
60
+
61
+ def get_cropped_face_images(self,) -> list[PILImage]:
62
+ """[summary]
63
+
64
+ Returns:
65
+ List[PILImage]: [description]
66
+ """
67
+ images = []
68
+ for face_location in self.face_locations:
69
+ top, right, bottom, left = face_location
70
+ cropped_face_img = self.f_img[top:bottom, left:right]
71
+ pil_img = Image.fromarray(cropped_face_img)
72
+
73
+ images.append(pil_img)
74
+
75
+ return images
76
+
77
+ def get_face_box_drawed_image(self) -> PILImage:
78
+ """[summary]
79
+
80
+ Returns:
81
+ PILImage: [description]
82
+ """
83
+ pil = copy(self.f_img_PIL)
84
+ draw = ImageDraw.Draw(pil)
85
+ for idx, (top, right, bottom, left) in enumerate(self.face_locations):
86
+ name = str(f"{idx:02d}")
87
+ mid_face = self.mid_face_locations[idx]
88
+
89
+ draw.rectangle(((left, top), (right, bottom)), outline=(0, 0, 255))
90
+
91
+ _, text_height = draw.textsize(name)
92
+ draw.rectangle(
93
+ ((left, bottom - text_height - 10), (right, bottom)),
94
+ fill=(0, 0, 255),
95
+ outline=(0, 0, 255),
96
+ )
97
+ draw.text((left + 6, bottom - text_height - 5), name, fill=(255, 255, 255))
98
+
99
+ draw.line(
100
+ ((mid_face[0], -10), mid_face, (mid_face[0], self.image_size[0])),
101
+ fill=(255, 255, 0),
102
+ width=10,
103
+ )
104
+ del draw
105
+ return pil
106
+
107
+ def get_full_image(
108
+ self, is_pil: bool = False
109
+ ) -> np.ndarray[Any, Any] | PILImage:
110
+ """[summary]
111
+
112
+ Args:
113
+ is_pil (bool, optional): [description]. Defaults to False.
114
+
115
+ Returns:
116
+ Union[np.ndarray, PILImage]: [description]
117
+ """
118
+
119
+ if is_pil:
120
+ return self.f_img_PIL
121
+ else:
122
+ return self.f_img
123
+
124
+ def get_symmetrized_images(self, idx: int = 0) -> SimImages:
125
+ """[summary]
126
+
127
+ Args:
128
+ idx (int, optional): [description]. Defaults to 0.
129
+
130
+ Returns:
131
+ SimImages: [description]
132
+ """
133
+
134
+ def get_concat_h(im1: PILImage, im2: PILImage) -> PILImage:
135
+ dst = Image.new("RGB", (im1.width + im2.width, im1.height))
136
+ dst.paste(im1, (0, 0))
137
+ dst.paste(im2, (im1.width, 0))
138
+ return dst
139
+
140
+ face_count = len(self.mid_face_locations)
141
+ if face_count < 1:
142
+ raise FaceIsNotDetected
143
+ elif face_count <= idx:
144
+ raise IndexError(f"0 <= idx <= {face_count - 1}")
145
+ else:
146
+ mid_face = self.mid_face_locations[idx]
147
+
148
+ cropped_left_img = self.f_img[0 : self.image_size[1], 0 : int(mid_face[0])]
149
+ cropped_right_img = self.f_img[
150
+ 0 : self.image_size[1], int(mid_face[0]) : self.image_size[0]
151
+ ]
152
+
153
+ pil_img_left = Image.fromarray(cropped_left_img)
154
+ pil_img_left_mirrored = ImageOps.mirror(pil_img_left)
155
+ pil_img_left_inner = get_concat_h(pil_img_left, pil_img_left_mirrored)
156
+ pil_img_left_outer = get_concat_h(pil_img_left_mirrored, pil_img_left)
157
+
158
+ pil_img_right = Image.fromarray(cropped_right_img)
159
+ pil_img_right_mirrored = ImageOps.mirror(pil_img_right)
160
+ pil_img_right_inner = get_concat_h(pil_img_right_mirrored, pil_img_right)
161
+ pil_img_right_outer = get_concat_h(pil_img_right, pil_img_right_mirrored)
162
+
163
+ return (
164
+ pil_img_left,
165
+ pil_img_left_inner,
166
+ pil_img_left_outer,
167
+ pil_img_right,
168
+ pil_img_right_inner,
169
+ pil_img_right_outer,
170
+ )
171
+
172
+ def __load_from_url(self, url: str) -> None:
173
+ """[summary]
174
+
175
+ Args:
176
+ url (str): [description]
177
+
178
+ Raises:
179
+ ValueError: [description]
180
+ """
181
+ if not self.__is_valid_url(url):
182
+ raise ValueError(f"{repr(url)} is not valid url")
183
+ else:
184
+ img_data = io.BytesIO(urlopen(url).read())
185
+ self.f_img = face_recognition.load_image_file(img_data)
186
+
187
+ def __load_from_local(self, path_: str) -> None:
188
+ if path.isfile(path_):
189
+ self.f_img = face_recognition.load_image_file(path_)
190
+
191
+ @staticmethod
192
+ def __is_valid_url(url: str) -> bool:
193
+ """[summary]
194
+
195
+ Args:
196
+ url (str): [description]
197
+
198
+ Returns:
199
+ bool: [description]
200
+
201
+ Note:
202
+ Copyright (c) Django Software Foundation and individual
203
+ contributors. All rights reserved.
204
+ """
205
+ regex = re.compile(
206
+ r"^(?:http|ftp)s?://" # http:// or https://
207
+ # domain...
208
+ r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|"
209
+ r"[A-Z0-9-]{2,}\.?)|"
210
+ r"localhost|" # localhost...
211
+ r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip
212
+ r"(?::\d+)?" # optional port
213
+ r"(?:/?|[/?]\S+)$",
214
+ re.IGNORECASE,
215
+ )
216
+ return re.match(regex, url) is not None
217
+
218
+ @staticmethod
219
+ def __get_mid_face_locations(
220
+ face_landmarks: FaceLandmarks,
221
+ ) -> list[tuple[int, int]]:
222
+ """[summary]
223
+
224
+ Args:
225
+ face_landmarks (FaceLandmarks): [description]
226
+
227
+ Returns:
228
+ List[Tuple[int, int]]: [description]
229
+ """
230
+
231
+ def mean(lst: list[int]) -> int:
232
+ return int(sum(lst) / len(lst))
233
+
234
+ mid_faces = []
235
+ for face_landmark in face_landmarks:
236
+ if not ("left_eye" in face_landmark and "right_eye" in face_landmark):
237
+ raise ValueError("eye locations was missing.")
238
+ l_e_xs = [i[0] for i in face_landmark["left_eye"]]
239
+ l_e_ys = [i[1] for i in face_landmark["left_eye"]]
240
+ r_e_xs = [i[0] for i in face_landmark["right_eye"]]
241
+ r_e_ys = [i[1] for i in face_landmark["right_eye"]]
242
+ mid_face = (
243
+ (mean(l_e_xs) + mean(r_e_xs)) // 2,
244
+ (mean(l_e_ys) + mean(r_e_ys)) // 2,
245
+ )
246
+ mid_faces.append(mid_face)
247
+ return mid_faces
248
+
249
+
250
+ def main() -> None:
251
+ """[summary]"""
252
+ data = list(
253
+ map(
254
+ lambda x: "https://pbs.twimg.com/media/%s?format=jpg" % x,
255
+ [
256
+ "E7okHDEVUAE1O6i",
257
+ "E7jaibgUcAUWvg-",
258
+ "E7jahEbUcAMNLdU",
259
+ "E7Jqli9VEAEStvs",
260
+ "E7Jqk-aUcAcfg3o",
261
+ "E7EhGi2XoAsMrO5",
262
+ "E5dhLccUYAUD5Yx",
263
+ "E5TOAqUVUAMckXT",
264
+ "E4vK6e0VgAAksnK",
265
+ "E4Va7u4VkAAKde3",
266
+ "E4A0ksEUYAIpynP",
267
+ "E3xXzcyUYAIX1dC",
268
+ "E2zkvONVcAQEE_S",
269
+ "E1cBsxDUcAIe_LZ",
270
+ "E1W4HTRVUAgYkmo",
271
+ "E1HbVAeVIAId5yP",
272
+ "E09INVFUcAYpcWo",
273
+ "E0oh0hmUUAAfJV9",
274
+ ],
275
+ )
276
+ )
277
+ success, fail = 0, 0
278
+ for idx, link in enumerate(data):
279
+ print(f"[{idx:02d}]", link, end="")
280
+ f = FaceSym(link)
281
+ if f.face_count != 0:
282
+ print("=>Detected")
283
+ f.get_symmetrized_images()
284
+ success += 1
285
+ else:
286
+ print("=>Not Detected")
287
+ fail += 1
288
+
289
+ else:
290
+ print(f"DATA: {len(data)}", f"OK: {success}", f"NG: {fail}")
291
+
292
+
293
+ if __name__ == "__main__":
294
+ main()