Face Module
Face Detection Module
Author: Nathan Forsyth
Face
Class for displaying and retrieving data for faces recognized by the FaceDetector module.
Attributes:
| Name | Type | Description |
|---|---|---|
landmarks |
list[tuple[int, int, int]]
|
List of landmarks using 3D pixel coordinates. |
normalized_landmarks |
list[tuple[float, float, float]]
|
List of the original normalized landmark tuples provided by MediaPipe. |
box |
BoundingBox
|
Bounding box of the face. |
center |
tuple[int, int]
|
2D pixel coordinates of the bounding box center. |
flip |
bool
|
When True, left/right feature shortcuts are swapped for mirrored images. |
left_eye_open |
bool
|
Whether the left eye is detected as open. |
right_eye_open |
bool
|
Whether the right eye is detected as open. |
mouth_open |
bool
|
Whether the mouth is detected as open. |
left_iris_center |
tuple[int, int] | None
|
2D pixel center of the left iris. |
right_iris_center |
tuple[int, int] | None
|
2D pixel center of the right iris. |
iris_tracking |
dict[str, tuple[float, float] | None]
|
Dictionary of iris positions inside each eye as normalized (x, y) tuples. |
image |
ndarray
|
Image used by the FaceDetector. Used as the default image to display the drawn face. |
connection_style |
ConnectionStyle
|
Style settings for face connection lines. |
landmark_style |
LandmarkStyle
|
Style settings for face landmarks. |
Source code in src/pumpkinpipe/face.py
class Face:
"""
Class for displaying and retrieving data for faces recognized by the FaceDetector module.
:ivar landmarks: List of landmarks using 3D pixel coordinates.
:ivar normalized_landmarks: List of the original normalized landmark tuples provided by MediaPipe.
:ivar box: Bounding box of the face.
:ivar center: 2D pixel coordinates of the bounding box center.
:ivar flip: When True, left/right feature shortcuts are swapped for mirrored images.
:ivar left_eye_open: Whether the left eye is detected as open.
:ivar right_eye_open: Whether the right eye is detected as open.
:ivar mouth_open: Whether the mouth is detected as open.
:ivar left_iris_center: 2D pixel center of the left iris.
:ivar right_iris_center: 2D pixel center of the right iris.
:ivar iris_tracking: Dictionary of iris positions inside each eye as normalized (x, y) tuples.
:ivar image: Image used by the FaceDetector. Used as the default image to display the drawn face.
:ivar connection_style: Style settings for face connection lines.
:ivar landmark_style: Style settings for face landmarks.
"""
_DEFAULT_CONNECTIONS = FaceLandmarksConnections.FACE_LANDMARKS_CONTOURS
_LEFT_EYE_OPEN_IDS = (362, 385, 387, 263, 373, 380)
_RIGHT_EYE_OPEN_IDS = (33, 160, 158, 133, 153, 144)
_MOUTH_OPEN_IDS = (13, 14, 61, 291)
_EYE_OPEN_THRESHOLD = 0.2
_MOUTH_OPEN_THRESHOLD = 0.15
def __init__(
self,
landmarks: list[tuple[int, int, int]],
normalized_landmarks: list[tuple[float, float, float]],
box: BoundingBox,
image: np.ndarray,
flip: bool = True,
):
"""
Initialize the face.
:param landmarks: The (x, y, z) pixel coordinates of landmarks.
:param normalized_landmarks: The normalized landmark tuples provided by MediaPipe.
:param box: The bounding box of the face.
:param image: The image that was used to detect the face.
:param flip: When True, swap left/right feature shortcuts for mirrored images.
"""
self.landmarks: list[tuple[int, int, int]] = landmarks
self.normalized_landmarks: list[tuple[float, float, float]] = normalized_landmarks
self.original_landmarks: list[tuple[float, float, float]] = normalized_landmarks
self.flip: bool = flip
self.box: BoundingBox = box
self.center: tuple[int, int] = self.box.center
self.image: np.ndarray = image
self.left_eye, self.right_eye = self._side_feature_landmarks(
FaceLandmarksConnections.FACE_LANDMARKS_LEFT_EYE,
FaceLandmarksConnections.FACE_LANDMARKS_RIGHT_EYE,
)
self.left_eyebrow, self.right_eyebrow = self._side_feature_landmarks(
FaceLandmarksConnections.FACE_LANDMARKS_LEFT_EYEBROW,
FaceLandmarksConnections.FACE_LANDMARKS_RIGHT_EYEBROW,
)
self.left_iris, self.right_iris = self._side_feature_landmarks(
FaceLandmarksConnections.FACE_LANDMARKS_LEFT_IRIS,
FaceLandmarksConnections.FACE_LANDMARKS_RIGHT_IRIS,
)
left_eye_open_ids, right_eye_open_ids = self._side_ids(
Face._LEFT_EYE_OPEN_IDS,
Face._RIGHT_EYE_OPEN_IDS,
)
self.left_eye_open: bool = self._eye_is_open(left_eye_open_ids)
self.right_eye_open: bool = self._eye_is_open(right_eye_open_ids)
self.mouth_open: bool = self._mouth_is_open()
self.left_iris_center: tuple[int, int] | None = self._center_of(self.left_iris)
self.right_iris_center: tuple[int, int] | None = self._center_of(self.right_iris)
self.iris_tracking: dict[str, tuple[float, float] | None] = {
"left": self._iris_position(self.left_iris_center, self.left_eye),
"right": self._iris_position(self.right_iris_center, self.right_eye),
}
self.connection_style: ConnectionStyle = ConnectionStyle()
self.landmark_style: LandmarkStyle = LandmarkStyle(fill=(0, 255, 255), radius=1)
def _side_ids(self, left_ids, right_ids):
if self.flip:
return right_ids, left_ids
return left_ids, right_ids
def _side_feature_landmarks(self, left_connections, right_connections):
if self.flip:
left_connections, right_connections = right_connections, left_connections
return (
self._landmarks_for_connections(left_connections),
self._landmarks_for_connections(right_connections),
)
def _landmarks_for_connections(self, connections) -> list[tuple[int, int, int]]:
landmark_ids = sorted({connection.start for connection in connections} | {connection.end for connection in connections})
return [self.landmarks[index] for index in landmark_ids if index < len(self.landmarks)]
def _eye_is_open(self, ids: tuple[int, int, int, int, int, int]) -> bool:
if max(ids) >= len(self.landmarks):
return False
p1, p2, p3, p4, p5, p6 = [self.landmarks[index] for index in ids]
vertical_distance = math.dist(p2[:2], p6[:2]) + math.dist(p3[:2], p5[:2])
horizontal_distance = 2 * math.dist(p1[:2], p4[:2])
if horizontal_distance == 0:
return False
return vertical_distance / horizontal_distance > Face._EYE_OPEN_THRESHOLD
def _mouth_is_open(self) -> bool:
if max(Face._MOUTH_OPEN_IDS) >= len(self.landmarks):
return False
upper_lip = self.landmarks[13]
lower_lip = self.landmarks[14]
left_corner = self.landmarks[61]
right_corner = self.landmarks[291]
mouth_width = math.dist(left_corner[:2], right_corner[:2])
if mouth_width == 0:
return False
return math.dist(upper_lip[:2], lower_lip[:2]) / mouth_width > Face._MOUTH_OPEN_THRESHOLD
def _center_of(self, landmarks: list[tuple[int, int, int]]) -> tuple[int, int] | None:
if not landmarks:
return None
x = sum(point[0] for point in landmarks) // len(landmarks)
y = sum(point[1] for point in landmarks) // len(landmarks)
return x, y
def _iris_position(
self,
iris_center: tuple[int, int] | None,
eye_landmarks: list[tuple[int, int, int]],
) -> tuple[float, float] | None:
if iris_center is None or not eye_landmarks:
return None
min_x = min(point[0] for point in eye_landmarks)
max_x = max(point[0] for point in eye_landmarks)
min_y = min(point[1] for point in eye_landmarks)
max_y = max(point[1] for point in eye_landmarks)
width = max_x - min_x
height = max_y - min_y
if width == 0 or height == 0:
return None
x, y = iris_center
return (x - min_x) / width, (y - min_y) / height
def draw(
self,
image: np.ndarray | None = None,
connections=None,
landmarks: bool = True,
):
"""
Draws the face landmarks and connections on the specified image.
:param image: The target image for drawing. If None, it will draw on the face's image.
:param connections: MediaPipe face connection set to draw. Defaults to face contours.
:param landmarks: When True, draw landmark points.
"""
if image is None:
image = self.image
if connections is None:
connections = Face._DEFAULT_CONNECTIONS
for connection in connections:
start_x, start_y, _ = self.landmarks[connection.start]
end_x, end_y, _ = self.landmarks[connection.end]
cv2.line(
image,
(start_x, start_y),
(end_x, end_y),
self.connection_style.stroke,
self.connection_style.thickness,
)
if landmarks:
for x, y, _ in self.landmarks:
cv2.circle(image, (x, y), self.landmark_style.radius, self.landmark_style.fill, -1)
def debug(
self,
image: np.ndarray | None = None,
contours: bool = True,
bounding_box: bool = True,
center: bool = True,
landmark_count: bool = True,
face_status: bool = True,
iris_tracking: bool = True,
):
"""
Draws requested debug information. Defaults to all debug information.
:param image: The image to draw on. If None, the face will draw on its own image.
:param contours: When True, draw face contours and landmark points.
:param bounding_box: When True, draw the outer bounding box of the face.
:param center: When True, draw and label the bounding box center.
:param landmark_count: When True, display the number of landmarks found.
:param face_status: When True, display eye and mouth open/closed status.
:param iris_tracking: When True, draw iris centers and display iris tracking values.
"""
if image is None:
image = self.image
debug_font = cv2.FONT_HERSHEY_PLAIN
debug_text_size = 1
debug_thickness = 1
if contours:
self.draw(image)
if bounding_box:
self.box.draw_corners(image, length=20, thickness=5, stroke=(0, 0, 0))
self.box.draw_corners(image, length=19, thickness=4, stroke=(255, 255, 255))
if landmark_count:
stack_text(
image,
[f"Face landmarks: {len(self.landmarks)}"],
self.box.origin,
debug_font,
debug_text_size,
debug_thickness,
(255, 255, 255),
HAlign.RIGHT,
VAlign.TOP,
)
debug_lines = []
if face_status:
debug_lines.extend(
[
f"Left eye: {'Open' if self.left_eye_open else 'Closed'}",
f"Right eye: {'Open' if self.right_eye_open else 'Closed'}",
f"Mouth: {'Open' if self.mouth_open else 'Closed'}",
]
)
if iris_tracking:
for label, tracking in self.iris_tracking.items():
if tracking is None:
debug_lines.append(f"{label.title()} iris: n/a")
else:
x, y = tracking
debug_lines.append(f"{label.title()} iris: ({x:.2f}, {y:.2f})")
for iris_center in (self.left_iris_center, self.right_iris_center):
if iris_center is not None:
cv2.circle(image, iris_center, 4, (255, 0, 255), -1)
cv2.circle(image, iris_center, 4, (0, 0, 0), 1)
if debug_lines:
stack_text(
image,
debug_lines,
self.box.opposite,
debug_font,
debug_text_size,
debug_thickness,
(255, 255, 255),
HAlign.RIGHT,
VAlign.TOP,
)
if center:
cv2.circle(image, self.center, 7, (0, 255, 0), -1)
cv2.circle(image, self.center, 7, (0, 0, 0), 2)
stack_text(
image,
[f"Center: ({self.center[0]}, {self.center[1]})"],
self.box.origin,
debug_font,
debug_text_size * 1.5,
debug_thickness * 2,
(0, 255, 0),
HAlign.LEFT,
VAlign.BOTTOM,
0,
)
def set_connection_style(
self,
stroke: tuple[int, int, int] | list[int] | None = None,
thickness: float | None = None,
):
"""
Modifies the style of the face connections when drawn.
:param stroke: The BGR color of the connections.
:param thickness: The thickness of the connector lines in pixels.
"""
if stroke is not None:
self.connection_style.stroke = stroke
if thickness is not None:
self.connection_style.thickness = int(thickness)
def set_landmarks_style(
self,
fill: tuple[int, int, int] | list[int] | None = None,
stroke: tuple[int, int, int] | list[int] | None = None,
radius: float | None = None,
thickness: float | None = None,
):
"""
Modifies the style of the face landmarks when drawn.
:param fill: The BGR color of the landmarks.
:param stroke: The BGR color of the landmark outlines. Stored for API consistency.
:param radius: Radius of the landmarks.
:param thickness: Thickness of the landmark outline. Stored for API consistency.
"""
if fill is not None:
self.landmark_style.fill = fill
if stroke is not None:
self.landmark_style.stroke = stroke
if radius is not None:
self.landmark_style.radius = int(radius)
if thickness is not None:
self.landmark_style.thickness = int(thickness)
__init__
__init__(landmarks: list[tuple[int, int, int]], normalized_landmarks: list[tuple[float, float, float]], box: BoundingBox, image: ndarray, flip: bool = True)
Initialize the face.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
landmarks
|
list[tuple[int, int, int]]
|
The (x, y, z) pixel coordinates of landmarks. |
required |
normalized_landmarks
|
list[tuple[float, float, float]]
|
The normalized landmark tuples provided by MediaPipe. |
required |
box
|
BoundingBox
|
The bounding box of the face. |
required |
image
|
ndarray
|
The image that was used to detect the face. |
required |
flip
|
bool
|
When True, swap left/right feature shortcuts for mirrored images. |
True
|
Source code in src/pumpkinpipe/face.py
def __init__(
self,
landmarks: list[tuple[int, int, int]],
normalized_landmarks: list[tuple[float, float, float]],
box: BoundingBox,
image: np.ndarray,
flip: bool = True,
):
"""
Initialize the face.
:param landmarks: The (x, y, z) pixel coordinates of landmarks.
:param normalized_landmarks: The normalized landmark tuples provided by MediaPipe.
:param box: The bounding box of the face.
:param image: The image that was used to detect the face.
:param flip: When True, swap left/right feature shortcuts for mirrored images.
"""
self.landmarks: list[tuple[int, int, int]] = landmarks
self.normalized_landmarks: list[tuple[float, float, float]] = normalized_landmarks
self.original_landmarks: list[tuple[float, float, float]] = normalized_landmarks
self.flip: bool = flip
self.box: BoundingBox = box
self.center: tuple[int, int] = self.box.center
self.image: np.ndarray = image
self.left_eye, self.right_eye = self._side_feature_landmarks(
FaceLandmarksConnections.FACE_LANDMARKS_LEFT_EYE,
FaceLandmarksConnections.FACE_LANDMARKS_RIGHT_EYE,
)
self.left_eyebrow, self.right_eyebrow = self._side_feature_landmarks(
FaceLandmarksConnections.FACE_LANDMARKS_LEFT_EYEBROW,
FaceLandmarksConnections.FACE_LANDMARKS_RIGHT_EYEBROW,
)
self.left_iris, self.right_iris = self._side_feature_landmarks(
FaceLandmarksConnections.FACE_LANDMARKS_LEFT_IRIS,
FaceLandmarksConnections.FACE_LANDMARKS_RIGHT_IRIS,
)
left_eye_open_ids, right_eye_open_ids = self._side_ids(
Face._LEFT_EYE_OPEN_IDS,
Face._RIGHT_EYE_OPEN_IDS,
)
self.left_eye_open: bool = self._eye_is_open(left_eye_open_ids)
self.right_eye_open: bool = self._eye_is_open(right_eye_open_ids)
self.mouth_open: bool = self._mouth_is_open()
self.left_iris_center: tuple[int, int] | None = self._center_of(self.left_iris)
self.right_iris_center: tuple[int, int] | None = self._center_of(self.right_iris)
self.iris_tracking: dict[str, tuple[float, float] | None] = {
"left": self._iris_position(self.left_iris_center, self.left_eye),
"right": self._iris_position(self.right_iris_center, self.right_eye),
}
self.connection_style: ConnectionStyle = ConnectionStyle()
self.landmark_style: LandmarkStyle = LandmarkStyle(fill=(0, 255, 255), radius=1)
debug
debug(image: ndarray | None = None, contours: bool = True, bounding_box: bool = True, center: bool = True, landmark_count: bool = True, face_status: bool = True, iris_tracking: bool = True)
Draws requested debug information. Defaults to all debug information.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
ndarray | None
|
The image to draw on. If None, the face will draw on its own image. |
None
|
contours
|
bool
|
When True, draw face contours and landmark points. |
True
|
bounding_box
|
bool
|
When True, draw the outer bounding box of the face. |
True
|
center
|
bool
|
When True, draw and label the bounding box center. |
True
|
landmark_count
|
bool
|
When True, display the number of landmarks found. |
True
|
face_status
|
bool
|
When True, display eye and mouth open/closed status. |
True
|
iris_tracking
|
bool
|
When True, draw iris centers and display iris tracking values. |
True
|
Source code in src/pumpkinpipe/face.py
def debug(
self,
image: np.ndarray | None = None,
contours: bool = True,
bounding_box: bool = True,
center: bool = True,
landmark_count: bool = True,
face_status: bool = True,
iris_tracking: bool = True,
):
"""
Draws requested debug information. Defaults to all debug information.
:param image: The image to draw on. If None, the face will draw on its own image.
:param contours: When True, draw face contours and landmark points.
:param bounding_box: When True, draw the outer bounding box of the face.
:param center: When True, draw and label the bounding box center.
:param landmark_count: When True, display the number of landmarks found.
:param face_status: When True, display eye and mouth open/closed status.
:param iris_tracking: When True, draw iris centers and display iris tracking values.
"""
if image is None:
image = self.image
debug_font = cv2.FONT_HERSHEY_PLAIN
debug_text_size = 1
debug_thickness = 1
if contours:
self.draw(image)
if bounding_box:
self.box.draw_corners(image, length=20, thickness=5, stroke=(0, 0, 0))
self.box.draw_corners(image, length=19, thickness=4, stroke=(255, 255, 255))
if landmark_count:
stack_text(
image,
[f"Face landmarks: {len(self.landmarks)}"],
self.box.origin,
debug_font,
debug_text_size,
debug_thickness,
(255, 255, 255),
HAlign.RIGHT,
VAlign.TOP,
)
debug_lines = []
if face_status:
debug_lines.extend(
[
f"Left eye: {'Open' if self.left_eye_open else 'Closed'}",
f"Right eye: {'Open' if self.right_eye_open else 'Closed'}",
f"Mouth: {'Open' if self.mouth_open else 'Closed'}",
]
)
if iris_tracking:
for label, tracking in self.iris_tracking.items():
if tracking is None:
debug_lines.append(f"{label.title()} iris: n/a")
else:
x, y = tracking
debug_lines.append(f"{label.title()} iris: ({x:.2f}, {y:.2f})")
for iris_center in (self.left_iris_center, self.right_iris_center):
if iris_center is not None:
cv2.circle(image, iris_center, 4, (255, 0, 255), -1)
cv2.circle(image, iris_center, 4, (0, 0, 0), 1)
if debug_lines:
stack_text(
image,
debug_lines,
self.box.opposite,
debug_font,
debug_text_size,
debug_thickness,
(255, 255, 255),
HAlign.RIGHT,
VAlign.TOP,
)
if center:
cv2.circle(image, self.center, 7, (0, 255, 0), -1)
cv2.circle(image, self.center, 7, (0, 0, 0), 2)
stack_text(
image,
[f"Center: ({self.center[0]}, {self.center[1]})"],
self.box.origin,
debug_font,
debug_text_size * 1.5,
debug_thickness * 2,
(0, 255, 0),
HAlign.LEFT,
VAlign.BOTTOM,
0,
)
draw
draw(image: ndarray | None = None, connections=None, landmarks: bool = True)
Draws the face landmarks and connections on the specified image.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
ndarray | None
|
The target image for drawing. If None, it will draw on the face's image. |
None
|
connections
|
MediaPipe face connection set to draw. Defaults to face contours. |
None
|
|
landmarks
|
bool
|
When True, draw landmark points. |
True
|
Source code in src/pumpkinpipe/face.py
def draw(
self,
image: np.ndarray | None = None,
connections=None,
landmarks: bool = True,
):
"""
Draws the face landmarks and connections on the specified image.
:param image: The target image for drawing. If None, it will draw on the face's image.
:param connections: MediaPipe face connection set to draw. Defaults to face contours.
:param landmarks: When True, draw landmark points.
"""
if image is None:
image = self.image
if connections is None:
connections = Face._DEFAULT_CONNECTIONS
for connection in connections:
start_x, start_y, _ = self.landmarks[connection.start]
end_x, end_y, _ = self.landmarks[connection.end]
cv2.line(
image,
(start_x, start_y),
(end_x, end_y),
self.connection_style.stroke,
self.connection_style.thickness,
)
if landmarks:
for x, y, _ in self.landmarks:
cv2.circle(image, (x, y), self.landmark_style.radius, self.landmark_style.fill, -1)
set_connection_style
set_connection_style(stroke: tuple[int, int, int] | list[int] | None = None, thickness: float | None = None)
Modifies the style of the face connections when drawn.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stroke
|
tuple[int, int, int] | list[int] | None
|
The BGR color of the connections. |
None
|
thickness
|
float | None
|
The thickness of the connector lines in pixels. |
None
|
Source code in src/pumpkinpipe/face.py
def set_connection_style(
self,
stroke: tuple[int, int, int] | list[int] | None = None,
thickness: float | None = None,
):
"""
Modifies the style of the face connections when drawn.
:param stroke: The BGR color of the connections.
:param thickness: The thickness of the connector lines in pixels.
"""
if stroke is not None:
self.connection_style.stroke = stroke
if thickness is not None:
self.connection_style.thickness = int(thickness)
set_landmarks_style
set_landmarks_style(fill: tuple[int, int, int] | list[int] | None = None, stroke: tuple[int, int, int] | list[int] | None = None, radius: float | None = None, thickness: float | None = None)
Modifies the style of the face landmarks when drawn.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fill
|
tuple[int, int, int] | list[int] | None
|
The BGR color of the landmarks. |
None
|
stroke
|
tuple[int, int, int] | list[int] | None
|
The BGR color of the landmark outlines. Stored for API consistency. |
None
|
radius
|
float | None
|
Radius of the landmarks. |
None
|
thickness
|
float | None
|
Thickness of the landmark outline. Stored for API consistency. |
None
|
Source code in src/pumpkinpipe/face.py
def set_landmarks_style(
self,
fill: tuple[int, int, int] | list[int] | None = None,
stroke: tuple[int, int, int] | list[int] | None = None,
radius: float | None = None,
thickness: float | None = None,
):
"""
Modifies the style of the face landmarks when drawn.
:param fill: The BGR color of the landmarks.
:param stroke: The BGR color of the landmark outlines. Stored for API consistency.
:param radius: Radius of the landmarks.
:param thickness: Thickness of the landmark outline. Stored for API consistency.
"""
if fill is not None:
self.landmark_style.fill = fill
if stroke is not None:
self.landmark_style.stroke = stroke
if radius is not None:
self.landmark_style.radius = int(radius)
if thickness is not None:
self.landmark_style.thickness = int(thickness)
FaceDetector
Class for setting up MediaPipe and finding face data.
Attributes:
| Name | Type | Description |
|---|---|---|
timestamp_ms |
Variable for tracking time between frames. |
|
frame_rate |
Desired frame rate. Set to 30. |
|
landmarker |
Pipeline to detect face landmarks. |
Source code in src/pumpkinpipe/face.py
class FaceDetector:
"""
Class for setting up MediaPipe and finding face data.
:ivar timestamp_ms: Variable for tracking time between frames.
:ivar frame_rate: Desired frame rate. Set to 30.
:ivar landmarker: Pipeline to detect face landmarks.
"""
def __init__(self, number_of_faces: int = 1):
"""
Initialize the face detector.
:param number_of_faces: The maximum number of faces for the detector to try and process.
"""
with get_model_path("face_landmarker.task") as model_path:
options = vision.FaceLandmarkerOptions(
base_options=BaseOptions(model_asset_path=model_path),
running_mode=vision.RunningMode.VIDEO,
num_faces=number_of_faces,
output_face_blendshapes=False,
output_facial_transformation_matrixes=False,
)
self.landmarker = vision.FaceLandmarker.create_from_options(options)
self.timestamp_ms = 0
self.frame_rate = 30
def find_faces(self, image: np.ndarray, flip: bool = True) -> list[Face]:
"""
Detect faces and add them to a list.
:param image: The image to detect faces in.
:param flip: When True, swap left/right feature shortcuts for mirrored images.
:return: A list of detected faces.
"""
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
height, width, _ = image.shape
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=image_rgb)
result = self.landmarker.detect_for_video(mp_image, self.timestamp_ms)
self.timestamp_ms += int(1000 / self.frame_rate)
if not result.face_landmarks:
return []
faces: list[Face] = []
for face_lms in result.face_landmarks:
pixel_landmarks: list[tuple[int, int, int]] = []
normalized_landmarks: list[tuple[float, float, float]] = []
x_list: list[int] = []
y_list: list[int] = []
for lm in face_lms:
x = int(lm.x * width)
y = int(lm.y * height)
z = int(lm.z * width)
normalized_landmarks.append((lm.x, lm.y, lm.z))
pixel_landmarks.append((x, y, z))
x_list.append(x)
y_list.append(y)
bounding_box = BoundingBox(
(min(x_list) - 10, min(y_list) - 10),
(max(x_list) + 10, max(y_list) + 10),
)
faces.append(Face(pixel_landmarks, normalized_landmarks, bounding_box, image, flip))
return faces
__init__
__init__(number_of_faces: int = 1)
Initialize the face detector.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
number_of_faces
|
int
|
The maximum number of faces for the detector to try and process. |
1
|
Source code in src/pumpkinpipe/face.py
def __init__(self, number_of_faces: int = 1):
"""
Initialize the face detector.
:param number_of_faces: The maximum number of faces for the detector to try and process.
"""
with get_model_path("face_landmarker.task") as model_path:
options = vision.FaceLandmarkerOptions(
base_options=BaseOptions(model_asset_path=model_path),
running_mode=vision.RunningMode.VIDEO,
num_faces=number_of_faces,
output_face_blendshapes=False,
output_facial_transformation_matrixes=False,
)
self.landmarker = vision.FaceLandmarker.create_from_options(options)
self.timestamp_ms = 0
self.frame_rate = 30
find_faces
find_faces(image: ndarray, flip: bool = True) -> list[Face]
Detect faces and add them to a list.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
ndarray
|
The image to detect faces in. |
required |
flip
|
bool
|
When True, swap left/right feature shortcuts for mirrored images. |
True
|
Returns:
| Type | Description |
|---|---|
list[Face]
|
A list of detected faces. |
Source code in src/pumpkinpipe/face.py
def find_faces(self, image: np.ndarray, flip: bool = True) -> list[Face]:
"""
Detect faces and add them to a list.
:param image: The image to detect faces in.
:param flip: When True, swap left/right feature shortcuts for mirrored images.
:return: A list of detected faces.
"""
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
height, width, _ = image.shape
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=image_rgb)
result = self.landmarker.detect_for_video(mp_image, self.timestamp_ms)
self.timestamp_ms += int(1000 / self.frame_rate)
if not result.face_landmarks:
return []
faces: list[Face] = []
for face_lms in result.face_landmarks:
pixel_landmarks: list[tuple[int, int, int]] = []
normalized_landmarks: list[tuple[float, float, float]] = []
x_list: list[int] = []
y_list: list[int] = []
for lm in face_lms:
x = int(lm.x * width)
y = int(lm.y * height)
z = int(lm.z * width)
normalized_landmarks.append((lm.x, lm.y, lm.z))
pixel_landmarks.append((x, y, z))
x_list.append(x)
y_list.append(y)
bounding_box = BoundingBox(
(min(x_list) - 10, min(y_list) - 10),
(max(x_list) + 10, max(y_list) + 10),
)
faces.append(Face(pixel_landmarks, normalized_landmarks, bounding_box, image, flip))
return faces
main
main()
Test script for the Face Detection module.
Source code in src/pumpkinpipe/face.py
def main():
"""
Test script for the Face Detection module.
"""
cap = cv2.VideoCapture(0)
face_detector = FaceDetector()
while True:
success, img = cap.read()
if not success:
break
img = cv2.flip(img, 1)
faces = face_detector.find_faces(img)
for face in faces:
face.debug()
cv2.imshow("Image", img)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
if cv2.getWindowProperty("Image", cv2.WND_PROP_VISIBLE) < 1:
break
cap.release()
cv2.destroyAllWindows()