diff --git a/src/bodyparts/body.py b/src/bodyparts/body.py new file mode 100644 index 0000000..73fa355 --- /dev/null +++ b/src/bodyparts/body.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pygame as pg + +from utils import landmark2vec + +if TYPE_CHECKING: + from environment import Environment + +LANDMARKS = [152] + + +class Body: + def __init__(self, env: Environment) -> None: + self.env = env + self.texture = pg.image.load("assets/body.png").convert_alpha() + + def draw(self, screen) -> None: + + # compute position + if self.env.results.multi_face_landmarks: + for face_landmarks in self.env.results.multi_face_landmarks: + + body_pos = landmark2vec(face_landmarks.landmark[LANDMARKS[0]], screen) + + texture_scaled = pg.transform.scale( + self.texture, + (300, 200), + ) + + texture_pos = body_pos - pg.Vector2( + 150, + 25, + ) + + # draw texture + screen.blit(texture_scaled, texture_pos) diff --git a/src/bodyparts/crown.py b/src/bodyparts/crown.py new file mode 100644 index 0000000..de79e87 --- /dev/null +++ b/src/bodyparts/crown.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pygame as pg + +from utils import landmark2vec + +if TYPE_CHECKING: + from environment import Environment + +LANDMARKS = [10] + + +class Crown: + def __init__(self, env: Environment) -> None: + self.env = env + self.texture = pg.image.load("assets/crown.png").convert_alpha() + + def draw(self, screen) -> None: + + # compute position + if self.env.results.multi_face_landmarks: + for face_landmarks in self.env.results.multi_face_landmarks: + + # head_top = landmark2vec(face_landmarks.landmark[LANDMARKS[1]], screen) + # head_bottom = landmark2vec(face_landmarks.landmark[LANDMARKS[3]], screen) + + # angle = (head_bottom - head_top).angle_to(pg.Vector2(0, 1)) + # height = (head_bottom - head_top).length() + # ratio = self.texture.get_width() / self.texture.get_height() + + crown_pos = landmark2vec(face_landmarks.landmark[LANDMARKS[0]], screen) + + texture_scaled = pg.transform.scale( + self.texture, + (100, 100), + ) + + texture_pos = crown_pos - pg.Vector2( + texture_scaled.get_width() / 2, + texture_scaled.get_height() / 2, + ) + + # draw texture + screen.blit(texture_scaled, texture_pos) diff --git a/src/bodyparts/ear.py b/src/bodyparts/ear.py new file mode 100644 index 0000000..8856387 --- /dev/null +++ b/src/bodyparts/ear.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pygame as pg + +from utils import landmark2vec + +if TYPE_CHECKING: + from environment import Environment + +LANDMARKS = [103, 332] + + +class Ear: + def __init__(self, side: bool, env: Environment) -> None: + self.env = env + self.side = side + self.texture = ( + pg.image.load("assets/earL.png").convert_alpha() + if side + else pg.image.load("assets/earR.png").convert_alpha() + ) + + def draw(self, screen) -> None: + + # compute position + if self.env.results.multi_face_landmarks: + for face_landmarks in self.env.results.multi_face_landmarks: + + texture_scaled = pg.transform.scale( + self.texture, + (100, 250), + ) + + if self.side: # right moustache + moustache_pos = landmark2vec(face_landmarks.landmark[LANDMARKS[0]], screen) + + texture_pos = moustache_pos - pg.Vector2( + 3 * texture_scaled.get_width() / 4, + 0.8 * texture_scaled.get_height(), + ) + + else: # left moustache + moustache_pos = landmark2vec(face_landmarks.landmark[LANDMARKS[1]], screen) + + texture_pos = moustache_pos - pg.Vector2( + texture_scaled.get_width() / 4, + 0.8 * texture_scaled.get_height(), + ) + + # draw texture + screen.blit(texture_scaled, texture_pos) diff --git a/src/bodyparts/eye.py b/src/bodyparts/eye.py new file mode 100644 index 0000000..ba2d221 --- /dev/null +++ b/src/bodyparts/eye.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pygame as pg + +from utils import blitRotate, landmark2vec + +if TYPE_CHECKING: + from environment import Environment + +LANDMARK_LEFT_EYELID = [386, 374] +LANDMARK_RIGHT_EYELID = [159, 145] +LANDMARK_LEFT_IRIS = [473, 474, 475, 476, 477] +LANDMARK_RIGHT_IRIS = [468, 469, 470, 471, 472] + + +class Eye: + """The eye class is responsible of drawing an eye.""" + + def __init__(self, side: bool, env: Environment) -> None: + """Initialize the eye.""" + self.env = env + self.side = side + self.texture = ( + pg.image.load("assets/eyeL.png").convert_alpha() + if side + else pg.image.load("assets/eyeR.png").convert_alpha() + ) + + def draw(self, screen) -> None: + """Draw the eye.""" + # compute position + if self.env.results.multi_face_landmarks: + for face_landmarks in self.env.results.multi_face_landmarks: + if self.side: # right eye + iris_pos = landmark2vec(face_landmarks.landmark[LANDMARK_RIGHT_IRIS[0]], screen) + + iris_top = landmark2vec(face_landmarks.landmark[LANDMARK_RIGHT_IRIS[2]], screen) + iris_bottom = landmark2vec(face_landmarks.landmark[LANDMARK_RIGHT_IRIS[4]], screen) + + eyelid_top = landmark2vec(face_landmarks.landmark[LANDMARK_RIGHT_EYELID[0]], screen) + eyelid_bottom = landmark2vec(face_landmarks.landmark[LANDMARK_RIGHT_EYELID[1]], screen) + + else: # left eye + iris_pos = landmark2vec(face_landmarks.landmark[LANDMARK_LEFT_IRIS[0]], screen) + + iris_top = landmark2vec(face_landmarks.landmark[LANDMARK_LEFT_IRIS[2]], screen) + iris_bottom = landmark2vec(face_landmarks.landmark[LANDMARK_LEFT_IRIS[4]], screen) + + eyelid_top = landmark2vec(face_landmarks.landmark[LANDMARK_LEFT_EYELID[0]], screen) + eyelid_bottom = landmark2vec(face_landmarks.landmark[LANDMARK_LEFT_EYELID[1]], screen) + + angle = (iris_bottom - iris_top).angle_to(pg.Vector2(0, 1)) + width = (iris_bottom - iris_top).length() + height = (eyelid_bottom - eyelid_top).length() * 5 + + texture_scaled = pg.transform.scale( + self.texture, + (width, height), + ) + + texture_pos = iris_pos - pg.Vector2( + texture_scaled.get_width() / 2, + texture_scaled.get_height() / 2, + ) + + # draw texture + blitRotate(screen, texture_scaled, texture_pos, angle) diff --git a/src/bodyparts/head.py b/src/bodyparts/head.py new file mode 100644 index 0000000..b57d49e --- /dev/null +++ b/src/bodyparts/head.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pygame as pg + +from utils import blitRotate, landmark2vec + +if TYPE_CHECKING: + from environment import Environment + +LANDMARKS = [234, 10, 454, 152] + + +class Head: + def __init__(self, env: Environment) -> None: + self.env = env + self.texture = pg.image.load("assets/head.png").convert_alpha() + + def draw(self, screen) -> None: + + # compute position + if self.env.results.multi_face_landmarks: + for face_landmarks in self.env.results.multi_face_landmarks: + + head_top = landmark2vec(face_landmarks.landmark[LANDMARKS[1]], screen) + head_bottom = landmark2vec(face_landmarks.landmark[LANDMARKS[3]], screen) + + angle = (head_bottom - head_top).angle_to(pg.Vector2(0, 1)) + height = (head_bottom - head_top).length() + ratio = self.texture.get_width() / self.texture.get_height() + + head_pos = (head_bottom + head_top) / 2 + + texture_scaled = pg.transform.scale( + self.texture, + (height * ratio, height), + ) + + texture_pos = head_pos - pg.Vector2( + texture_scaled.get_width() / 2, + texture_scaled.get_height() / 2, + ) + + # draw texture + blitRotate(screen, texture_scaled, texture_pos, angle) diff --git a/src/bodyparts/moustache.py b/src/bodyparts/moustache.py new file mode 100644 index 0000000..7f249e8 --- /dev/null +++ b/src/bodyparts/moustache.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pygame as pg + +from utils import landmark2vec + +if TYPE_CHECKING: + from environment import Environment + +LANDMARKS = [138, 367] + + +class Moustache: + def __init__(self, side: bool, env: Environment) -> None: + self.env = env + self.side = side + self.texture = ( + pg.image.load("assets/moustacheL.png").convert_alpha() + if side + else pg.image.load("assets/moustacheR.png").convert_alpha() + ) + + def draw(self, screen) -> None: + + # compute position + if self.env.results.multi_face_landmarks: + for face_landmarks in self.env.results.multi_face_landmarks: + + texture_scaled = pg.transform.scale( + self.texture, + (200, 100), + ) + + if self.side: # right moustache + moustache_pos = landmark2vec(face_landmarks.landmark[LANDMARKS[0]], screen) + + texture_pos = moustache_pos - pg.Vector2( + texture_scaled.get_width(), + texture_scaled.get_height() / 2, + ) + + else: # left moustache + moustache_pos = landmark2vec(face_landmarks.landmark[LANDMARKS[1]], screen) + + texture_pos = moustache_pos - pg.Vector2( + 0, + texture_scaled.get_height() / 2, + ) + + # draw texture + screen.blit(texture_scaled, texture_pos) diff --git a/src/bodyparts/mouth.py b/src/bodyparts/mouth.py new file mode 100644 index 0000000..487c127 --- /dev/null +++ b/src/bodyparts/mouth.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pygame as pg + +from utils import blitRotate, landmark2vec + +if TYPE_CHECKING: + from environment import Environment + +LANDMARKS = [78, 13, 308, 14] + + +class Mouth: + def __init__(self, env: Environment) -> None: + self.env = env + self.texture = pg.image.load("assets/mouth.png").convert_alpha() + + def draw(self, screen) -> None: + + # compute position + if self.env.results.multi_face_landmarks: + for face_landmarks in self.env.results.multi_face_landmarks: + + mouth_top = landmark2vec(face_landmarks.landmark[LANDMARKS[1]], screen) + mouth_bottom = landmark2vec(face_landmarks.landmark[LANDMARKS[3]], screen) + + mouth_left = landmark2vec(face_landmarks.landmark[LANDMARKS[0]], screen) + mouth_right = landmark2vec(face_landmarks.landmark[LANDMARKS[2]], screen) + + angle = (mouth_bottom - mouth_top).angle_to(pg.Vector2(0, 1)) + height = (mouth_bottom - mouth_top).length() + width = (mouth_right - mouth_left).length() + + head_pos = (mouth_bottom + mouth_top) / 2 + + texture_scaled = pg.transform.scale( + self.texture, + (width, height), + ) + + texture_pos = head_pos - pg.Vector2( + texture_scaled.get_width() / 2, + texture_scaled.get_height() / 2, + ) + + # draw texture + blitRotate(screen, texture_scaled, texture_pos, angle) diff --git a/src/environment.py b/src/environment.py index 16cb381..86c4abf 100644 --- a/src/environment.py +++ b/src/environment.py @@ -4,7 +4,13 @@ import cv2 import mediapipe as mp import pygame as pg -from parts.eye import Eye +from bodyparts.body import Body +from bodyparts.crown import Crown +from bodyparts.ear import Ear +from bodyparts.eye import Eye +from bodyparts.head import Head +from bodyparts.moustache import Moustache +from bodyparts.mouth import Mouth class Environment: @@ -42,8 +48,16 @@ class Environment: # create body parts self.body_parts = { + "body": Body(self), + "left_ear": Ear(False, self), + "right_ear": Ear(True, self), + "head": Head(self), + "left_moustache": Moustache(False, self), + "right_moustache": Moustache(True, self), "left_eye": Eye(False, self), "right_eye": Eye(True, self), + "crown": Crown(self), + "mouth": Mouth(self), } def start(self) -> None: @@ -104,26 +118,16 @@ class Environment: if self.results.multi_face_landmarks: for face_landmarks in self.results.multi_face_landmarks: self.mp_drawing.draw_landmarks( - image=self.frame, - landmark_list=face_landmarks, - connections=self.mp_face_mesh.FACEMESH_TESSELATION, - landmark_drawing_spec=None, - connection_drawing_spec=self.mp_drawing_styles.get_default_face_mesh_tesselation_style(), - ) - self.mp_drawing.draw_landmarks( - image=self.frame, - landmark_list=face_landmarks, - connections=self.mp_face_mesh.FACEMESH_CONTOURS, - landmark_drawing_spec=None, - connection_drawing_spec=self.mp_drawing_styles.get_default_face_mesh_contours_style(), - ) - self.mp_drawing.draw_landmarks( - image=self.frame, - landmark_list=face_landmarks, - connections=self.mp_face_mesh.FACEMESH_IRISES, - landmark_drawing_spec=None, - connection_drawing_spec=self.mp_drawing_styles.get_default_face_mesh_iris_connections_style(), + self.frame, + face_landmarks, + landmark_drawing_spec=self.mp_drawing_styles.DrawingSpec((0, 0, 255), 0, 0), ) + # self.mp_drawing.draw_landmarks( + # self.frame, + # face_landmarks, + # [(469, 470), (470, 471)], + # None, + # ) # flip the image horizontally for a selfie-view display cv2.imshow("MediaPipe Face Mesh", cv2.flip(self.frame, 1)) @@ -135,4 +139,6 @@ class Environment: for part in self.body_parts.values(): part.draw(self.screen) + self.screen.blit(pg.transform.flip(self.screen, True, False), (0, 0)) + pg.display.flip() diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..cff2548 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,22 @@ +import numpy as np +import pygame as pg +from mediapipe.python.solutions.drawing_utils import _normalized_to_pixel_coordinates + + +def blitRotate(surf, image, topleft, angle): + """Rotate an image while drawing it.""" + rotated_image = pg.transform.rotate(image, angle) + new_rect = rotated_image.get_rect(center=image.get_rect(topleft=topleft).center) + surf.blit(rotated_image, new_rect.topleft) + + +def landmark2vec(landmark, screen): + """Convert a landmark to a pygame Vector2.""" + return pg.Vector2( + _normalized_to_pixel_coordinates( + np.clip(landmark.x, 0, 1), + np.clip(landmark.y, 0, 1), + screen.get_width(), + screen.get_height(), + ) # type: ignore + )