Commit ·
dfafbb7
0
Parent(s):
uplauded utils & webcam.py
Browse files
utils.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import numpy as np
|
| 3 |
+
import mediapipe as mp
|
| 4 |
+
from mediapipe.tasks import python
|
| 5 |
+
from mediapipe.tasks.python import vision
|
| 6 |
+
import cv2
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# landmarks of features from mediapipe
|
| 10 |
+
face_points={
|
| 11 |
+
"BLUSH_LEFT": [50],
|
| 12 |
+
"BLUSH_RIGHT": [280],
|
| 13 |
+
"LEFT_EYE": [33, 246, 161, 160, 159, 158, 157, 173, 133, 155, 154, 153, 145, 144, 163, 7, 33],
|
| 14 |
+
"RIGHT_EYE": [362, 298, 384, 385, 386, 387, 388, 466, 263, 249, 390, 373, 374, 380, 381, 382, 362],
|
| 15 |
+
"EYELINER_LEFT": [243, 112, 26, 22, 23, 24, 110, 25, 226, 130, 33, 7, 163, 144, 145, 153, 154, 155, 133, 243],
|
| 16 |
+
"EYELINER_RIGHT": [463, 362, 382, 381, 380, 374, 373, 390, 249, 263, 359, 446, 255, 339, 254, 253, 252, 256, 341, 463],
|
| 17 |
+
"EYESHADOW_LEFT": [226, 247, 30, 29, 27, 28, 56, 190, 243, 173, 157, 158, 159, 160, 161, 246, 33, 130, 226],
|
| 18 |
+
"EYESHADOW_RIGHT": [463, 414, 286, 258, 257, 259, 260, 467, 446, 359, 263, 466, 388, 387, 386, 385, 384, 398, 362, 463],
|
| 19 |
+
"FACE": [152, 148, 176, 149, 150, 136, 172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109, 10, 338, 297, 332, 284, 251, 389, 454, 323, 401, 361, 435, 288, 397, 365, 379, 378, 400, 377, 152],
|
| 20 |
+
"LIP_UPPER": [61, 185, 40, 39, 37, 0, 267, 269, 270, 409, 291, 308, 415, 310, 312, 13, 82, 81, 80, 191, 78],
|
| 21 |
+
"LIP_LOWER": [61, 146, 91, 181, 84, 17, 314, 405, 321, 375, 291, 308, 324, 402, 317, 14, 87, 178, 88, 95, 78, 61],
|
| 22 |
+
"EYEBROW_LEFT": [55, 107, 66, 105, 63, 70, 46, 53, 52, 65, 55],
|
| 23 |
+
"EYEBROW_RIGHT": [285, 336, 296, 334, 293, 300, 276, 283, 295, 285]
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
# initialize mediapipe face landmarker (new Tasks API)
|
| 27 |
+
# requires face_landmarker.task model file in the project directory
|
| 28 |
+
_base_options = python.BaseOptions(model_asset_path='face_landmarker.task')
|
| 29 |
+
_options = vision.FaceLandmarkerOptions(
|
| 30 |
+
base_options=_base_options,
|
| 31 |
+
num_faces=1,
|
| 32 |
+
)
|
| 33 |
+
_face_landmarker = vision.FaceLandmarker.create_from_options(_options)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _normalized_to_pixel_coordinates(normalized_x, normalized_y, image_width, image_height):
|
| 37 |
+
x_px = min(math.floor(normalized_x * image_width), image_width - 1)
|
| 38 |
+
y_px = min(math.floor(normalized_y * image_height), image_height - 1)
|
| 39 |
+
return (x_px, y_px)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# to display image in cv2 window
|
| 43 |
+
def show_image(image: np.array, msg: str = "Loaded Image"):
|
| 44 |
+
"""
|
| 45 |
+
image : image as np array
|
| 46 |
+
msg : cv2 window name
|
| 47 |
+
"""
|
| 48 |
+
image_copy = image.copy()
|
| 49 |
+
cv2.imshow(msg, image_copy)
|
| 50 |
+
cv2.waitKey(0)
|
| 51 |
+
cv2.destroyAllWindows()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def read_landmarks(image: np.array):
|
| 55 |
+
"""
|
| 56 |
+
image : image as np.array
|
| 57 |
+
"""
|
| 58 |
+
landmark_cordinates = {}
|
| 59 |
+
# convert BGR to RGB, then wrap in mediapipe Image
|
| 60 |
+
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
| 61 |
+
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb_image)
|
| 62 |
+
|
| 63 |
+
# detect facial landmarks (returns normalized points 0 to 477)
|
| 64 |
+
results = _face_landmarker.detect(mp_image)
|
| 65 |
+
face_landmarks = results.face_landmarks[0]
|
| 66 |
+
|
| 67 |
+
# convert normalized points w.r.to image dimensions
|
| 68 |
+
for idx, landmark in enumerate(face_landmarks):
|
| 69 |
+
landmark_px = _normalized_to_pixel_coordinates(
|
| 70 |
+
landmark.x, landmark.y, image.shape[1], image.shape[0]
|
| 71 |
+
)
|
| 72 |
+
# create a map of facial landmarks to (x,y) coordinates
|
| 73 |
+
if landmark_px:
|
| 74 |
+
landmark_cordinates[idx] = landmark_px
|
| 75 |
+
return landmark_cordinates
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# based on input facial features create make w.r.to colors
|
| 79 |
+
def add_mask(
|
| 80 |
+
mask: np.array, idx_to_coordinates: dict, face_connections: list, colors: list
|
| 81 |
+
):
|
| 82 |
+
"""
|
| 83 |
+
mask: image filled with 0's
|
| 84 |
+
idx_to_coordinates : dict with (x,y) cordinates for each face landmarks
|
| 85 |
+
face_connections : list of (x,y) cordinates for each facial features
|
| 86 |
+
colors : list of [B,G,R] color for each features
|
| 87 |
+
"""
|
| 88 |
+
for i, connection in enumerate(face_connections):
|
| 89 |
+
# extract (x,y) w.r.to image for each cordinates
|
| 90 |
+
points = np.array([idx_to_coordinates[idx] for idx in connection])
|
| 91 |
+
# make a shape of feature in the mask and add color
|
| 92 |
+
cv2.fillPoly(mask, [points], colors[i])
|
| 93 |
+
|
| 94 |
+
# smoothening of image
|
| 95 |
+
mask = cv2.GaussianBlur(mask, (7, 7), 4)
|
| 96 |
+
return mask
|
webcam.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pyvirtualcam
|
| 4 |
+
from utils import read_landmarks, add_mask, face_points
|
| 5 |
+
|
| 6 |
+
face_elements = ["LIP_LOWER", "LIP_UPPER", "EYEBROW_LEFT", "EYEBROW_RIGHT",
|
| 7 |
+
"EYELINER_LEFT", "EYELINER_RIGHT", "EYESHADOW_LEFT", "EYESHADOW_RIGHT"]
|
| 8 |
+
colors_map = {
|
| 9 |
+
# upper lip and lower lips
|
| 10 |
+
"LIP_UPPER": [0, 0, 255], # Red in BGR
|
| 11 |
+
"LIP_LOWER": [0, 0, 255], # Red in BGR
|
| 12 |
+
# eyeliner
|
| 13 |
+
"EYELINER_LEFT": [139, 0, 0], # Dark Blue in BGR
|
| 14 |
+
"EYELINER_RIGHT": [139, 0, 0], # Dark Blue in BGR
|
| 15 |
+
# eye shadow
|
| 16 |
+
"EYESHADOW_LEFT": [0, 100, 0], # Dark Green in BGR
|
| 17 |
+
"EYESHADOW_RIGHT": [0, 100, 0], # Dark Green in BGR
|
| 18 |
+
# eye brow
|
| 19 |
+
"EYEBROW_LEFT": [19, 69, 139], # Dark Brown in BGR
|
| 20 |
+
"EYEBROW_RIGHT": [19, 69, 139], # Dark Brown in BGR
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
face_connections = [face_points[idx] for idx in face_elements]
|
| 24 |
+
colors = [colors_map[idx] for idx in face_elements]
|
| 25 |
+
|
| 26 |
+
cap = cv2.VideoCapture(0)
|
| 27 |
+
if not cap.isOpened():
|
| 28 |
+
raise RuntimeError("Could not open webcam. Make sure Zoom is not already using it.")
|
| 29 |
+
|
| 30 |
+
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 31 |
+
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 32 |
+
fps = cap.get(cv2.CAP_PROP_FPS) or 30
|
| 33 |
+
|
| 34 |
+
print(f"Webcam opened: {width}x{height} @ {fps}fps")
|
| 35 |
+
|
| 36 |
+
with pyvirtualcam.Camera(width=width, height=height, fps=fps) as cam:
|
| 37 |
+
print(f"Virtual camera running: {cam.device}")
|
| 38 |
+
print("Press Q in the preview window to quit.")
|
| 39 |
+
while True:
|
| 40 |
+
ret, frame = cap.read()
|
| 41 |
+
if not ret:
|
| 42 |
+
continue # skip bad frames, don't exit
|
| 43 |
+
# frame = cv2.flip(frame, 1) # mirror horizontally
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
landmark_coordinates = read_landmarks(frame)
|
| 47 |
+
mask = np.zeros_like(frame)
|
| 48 |
+
mask = add_mask(mask, idx_to_coordinates=landmark_coordinates,
|
| 49 |
+
face_connections=face_connections, colors=colors)
|
| 50 |
+
output = cv2.addWeighted(frame, 1.0, mask, 0.2, 1.0)
|
| 51 |
+
except (IndexError, KeyError):
|
| 52 |
+
output = frame # no face detected, pass through plain video
|
| 53 |
+
|
| 54 |
+
# show local preview so you can confirm filter is working
|
| 55 |
+
cv2.imshow("Virtual Makeup Preview (Q to quit)", output)
|
| 56 |
+
if cv2.waitKey(1) & 0xFF == ord('q'):
|
| 57 |
+
break
|
| 58 |
+
|
| 59 |
+
# pyvirtualcam expects RGB
|
| 60 |
+
cam.send(cv2.cvtColor(output, cv2.COLOR_BGR2RGB))
|
| 61 |
+
cam.sleep_until_next_frame()
|
| 62 |
+
|
| 63 |
+
cap.release()
|
| 64 |
+
cv2.destroyAllWindows()
|