feat: draft avatar, need homography
This commit is contained in:
parent
6b751757dd
commit
51a42e0b4b
39
src/bodyparts/body.py
Normal file
39
src/bodyparts/body.py
Normal file
|
@ -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)
|
46
src/bodyparts/crown.py
Normal file
46
src/bodyparts/crown.py
Normal file
|
@ -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)
|
53
src/bodyparts/ear.py
Normal file
53
src/bodyparts/ear.py
Normal file
|
@ -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)
|
69
src/bodyparts/eye.py
Normal file
69
src/bodyparts/eye.py
Normal file
|
@ -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)
|
46
src/bodyparts/head.py
Normal file
46
src/bodyparts/head.py
Normal file
|
@ -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)
|
53
src/bodyparts/moustache.py
Normal file
53
src/bodyparts/moustache.py
Normal file
|
@ -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)
|
49
src/bodyparts/mouth.py
Normal file
49
src/bodyparts/mouth.py
Normal file
|
@ -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)
|
|
@ -4,7 +4,13 @@ import cv2
|
||||||
import mediapipe as mp
|
import mediapipe as mp
|
||||||
import pygame as pg
|
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:
|
class Environment:
|
||||||
|
@ -42,8 +48,16 @@ class Environment:
|
||||||
|
|
||||||
# create body parts
|
# create body parts
|
||||||
self.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),
|
"left_eye": Eye(False, self),
|
||||||
"right_eye": Eye(True, self),
|
"right_eye": Eye(True, self),
|
||||||
|
"crown": Crown(self),
|
||||||
|
"mouth": Mouth(self),
|
||||||
}
|
}
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
|
@ -104,26 +118,16 @@ class Environment:
|
||||||
if self.results.multi_face_landmarks:
|
if self.results.multi_face_landmarks:
|
||||||
for face_landmarks in self.results.multi_face_landmarks:
|
for face_landmarks in self.results.multi_face_landmarks:
|
||||||
self.mp_drawing.draw_landmarks(
|
self.mp_drawing.draw_landmarks(
|
||||||
image=self.frame,
|
self.frame,
|
||||||
landmark_list=face_landmarks,
|
face_landmarks,
|
||||||
connections=self.mp_face_mesh.FACEMESH_TESSELATION,
|
landmark_drawing_spec=self.mp_drawing_styles.DrawingSpec((0, 0, 255), 0, 0),
|
||||||
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.mp_drawing.draw_landmarks(
|
||||||
|
# self.frame,
|
||||||
|
# face_landmarks,
|
||||||
|
# [(469, 470), (470, 471)],
|
||||||
|
# None,
|
||||||
|
# )
|
||||||
|
|
||||||
# flip the image horizontally for a selfie-view display
|
# flip the image horizontally for a selfie-view display
|
||||||
cv2.imshow("MediaPipe Face Mesh", cv2.flip(self.frame, 1))
|
cv2.imshow("MediaPipe Face Mesh", cv2.flip(self.frame, 1))
|
||||||
|
@ -135,4 +139,6 @@ class Environment:
|
||||||
for part in self.body_parts.values():
|
for part in self.body_parts.values():
|
||||||
part.draw(self.screen)
|
part.draw(self.screen)
|
||||||
|
|
||||||
|
self.screen.blit(pg.transform.flip(self.screen, True, False), (0, 0))
|
||||||
|
|
||||||
pg.display.flip()
|
pg.display.flip()
|
||||||
|
|
22
src/utils.py
Normal file
22
src/utils.py
Normal file
|
@ -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
|
||||||
|
)
|
Loading…
Reference in a new issue