feat: draft avatar, need homography

This commit is contained in:
Laureηt 2022-11-28 21:57:50 +01:00
parent 6b751757dd
commit 51a42e0b4b
No known key found for this signature in database
GPG key ID: D88C6B294FD40994
9 changed files with 403 additions and 20 deletions

39
src/bodyparts/body.py Normal file
View 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
View 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
View 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
View 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
View 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)

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

View file

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

22
src/utils.py Normal file
View 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
)