HTML demos for face, hand, gesture, and posture tracking using MediaPipe. Includes Python CLI tools for processing video files. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
152 lines
6.3 KiB
Python
Executable File
152 lines
6.3 KiB
Python
Executable File
import cv2
|
|
import mediapipe as mp
|
|
from mediapipe.tasks import python
|
|
from mediapipe.tasks.python import vision
|
|
import numpy as np
|
|
from mediapipe.framework.formats import landmark_pb2
|
|
import argparse
|
|
import os
|
|
import csv
|
|
|
|
# --- NEW: Helper function to create the landmark-to-feature map ---
|
|
def create_landmark_map():
|
|
"""Creates a mapping from landmark index to facial feature name."""
|
|
landmark_map = {}
|
|
|
|
# Define the connection groups from MediaPipe's face_mesh solutions
|
|
connection_groups = {
|
|
'lips': mp.solutions.face_mesh.FACEMESH_LIPS,
|
|
'left_eye': mp.solutions.face_mesh.FACEMESH_LEFT_EYE,
|
|
'right_eye': mp.solutions.face_mesh.FACEMESH_RIGHT_EYE,
|
|
'left_eyebrow': mp.solutions.face_mesh.FACEMESH_LEFT_EYEBROW,
|
|
'right_eyebrow': mp.solutions.face_mesh.FACEMESH_RIGHT_EYEBROW,
|
|
'face_oval': mp.solutions.face_mesh.FACEMESH_FACE_OVAL,
|
|
'left_iris': mp.solutions.face_mesh.FACEMESH_LEFT_IRIS,
|
|
'right_iris': mp.solutions.face_mesh.FACEMESH_RIGHT_IRIS,
|
|
}
|
|
|
|
# Populate the map by iterating through the connection groups
|
|
for part_name, connections in connection_groups.items():
|
|
for connection in connections:
|
|
landmark_map[connection[0]] = part_name
|
|
landmark_map[connection[1]] = part_name
|
|
|
|
return landmark_map
|
|
|
|
# --- Helper Function to Draw Landmarks ---
|
|
def draw_landmarks_on_image(rgb_image, detection_result):
|
|
"""Draws face landmarks on a single image frame."""
|
|
face_landmarks_list = detection_result.face_landmarks
|
|
annotated_image = np.copy(rgb_image)
|
|
|
|
# Loop through the detected faces to visualize.
|
|
for face_landmarks in face_landmarks_list:
|
|
face_landmarks_proto = landmark_pb2.NormalizedLandmarkList()
|
|
face_landmarks_proto.landmark.extend([
|
|
landmark_pb2.NormalizedLandmark(x=landmark.x, y=landmark.y, z=landmark.z) for landmark in face_landmarks
|
|
])
|
|
|
|
mp.solutions.drawing_utils.draw_landmarks(
|
|
image=annotated_image,
|
|
landmark_list=face_landmarks_proto,
|
|
connections=mp.solutions.face_mesh.FACEMESH_TESSELATION,
|
|
landmark_drawing_spec=None,
|
|
connection_drawing_spec=mp.solutions.drawing_styles
|
|
.get_default_face_mesh_tesselation_style())
|
|
mp.solutions.drawing_utils.draw_landmarks(
|
|
image=annotated_image,
|
|
landmark_list=face_landmarks_proto,
|
|
connections=mp.solutions.face_mesh.FACEMESH_CONTOURS,
|
|
landmark_drawing_spec=None,
|
|
connection_drawing_spec=mp.solutions.drawing_styles
|
|
.get_default_face_mesh_contours_style())
|
|
mp.solutions.drawing_utils.draw_landmarks(
|
|
image=annotated_image,
|
|
landmark_list=face_landmarks_proto,
|
|
connections=mp.solutions.face_mesh.FACEMESH_IRISES,
|
|
landmark_drawing_spec=None,
|
|
connection_drawing_spec=mp.solutions.drawing_styles
|
|
.get_default_face_mesh_iris_connections_style())
|
|
|
|
return annotated_image
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Process a video to detect and draw face landmarks.')
|
|
parser.add_argument('input_video', help='The path to the input video file.')
|
|
args = parser.parse_args()
|
|
|
|
input_video_path = args.input_video
|
|
base_name, extension = os.path.splitext(input_video_path)
|
|
output_video_path = f"{base_name}_annotated{extension}"
|
|
output_csv_path = f"{base_name}_landmarks.csv"
|
|
|
|
# --- NEW: Create the landmark map ---
|
|
landmark_to_part_map = create_landmark_map()
|
|
|
|
# --- Configuration & Setup ---
|
|
model_path = 'face_landmarker.task'
|
|
base_options = python.BaseOptions(model_asset_path=model_path)
|
|
options = vision.FaceLandmarkerOptions(base_options=base_options,
|
|
output_face_blendshapes=True,
|
|
output_facial_transformation_matrixes=True,
|
|
num_faces=1)
|
|
detector = vision.FaceLandmarker.create_from_options(options)
|
|
|
|
# --- Video and CSV Setup ---
|
|
cap = cv2.VideoCapture(input_video_path)
|
|
if not cap.isOpened():
|
|
print(f"Error: Could not open video file {input_video_path}")
|
|
return
|
|
|
|
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
fps = int(cap.get(cv2.CAP_PROP_FPS))
|
|
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
|
out = cv2.VideoWriter(output_video_path, fourcc, fps, (frame_width, frame_height))
|
|
|
|
# Open CSV file for writing
|
|
with open(output_csv_path, 'w', newline='') as csvfile:
|
|
csv_writer = csv.writer(csvfile)
|
|
# NEW: Write the updated header row
|
|
csv_writer.writerow(['frame', 'face', 'landmark_index', 'face_part', 'x', 'y', 'z'])
|
|
|
|
print(f"Processing video: {input_video_path} 📹")
|
|
frame_number = 0
|
|
while(cap.isOpened()):
|
|
ret, frame = cap.read()
|
|
if not ret:
|
|
break
|
|
|
|
frame_number += 1
|
|
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb_frame)
|
|
detection_result = detector.detect(mp_image)
|
|
|
|
# Write landmark data to CSV
|
|
if detection_result.face_landmarks:
|
|
for face_index, face_landmarks in enumerate(detection_result.face_landmarks):
|
|
for landmark_index, landmark in enumerate(face_landmarks):
|
|
# NEW: Look up the face part name from the map
|
|
face_part = landmark_to_part_map.get(landmark_index, 'unknown')
|
|
# NEW: Write the new column to the CSV row
|
|
csv_writer.writerow([frame_number, face_index, landmark_index, face_part, landmark.x, landmark.y, landmark.z])
|
|
|
|
# Draw landmarks on the frame for the video
|
|
annotated_frame = draw_landmarks_on_image(rgb_frame, detection_result)
|
|
bgr_annotated_frame = cv2.cvtColor(annotated_frame, cv2.COLOR_RGB2BGR)
|
|
out.write(bgr_annotated_frame)
|
|
|
|
# Release everything when the job is finished
|
|
cap.release()
|
|
out.release()
|
|
cv2.destroyAllWindows()
|
|
|
|
print(f"\n✅ Processing complete.")
|
|
print(f"Annotated video saved to: {output_video_path}")
|
|
print(f"Landmarks CSV saved to: {output_csv_path}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|