#!/usr/bin/env python3
"""
holistic_mp4.py
Process an MP4 with MediaPipe Holistic:
- Saves annotated video
- Exports CSV of face/pose/hand landmarks per frame
Usage:
python holistic_mp4.py /path/to/input.mp4
python holistic_mp4.py /path/to/input.mp4 --out-video out.mp4 --out-csv out.csv --show
"""
import argparse
import csv
import os
import sys
from pathlib import Path
import cv2
import mediapipe as mp
mp_holistic = mp.solutions.holistic
mp_drawing = mp.solutions.drawing_utils
mp_styles = mp.solutions.drawing_styles
def parse_args():
p = argparse.ArgumentParser(description="Run MediaPipe Holistic on an MP4 and export annotated video + CSV landmarks.")
p.add_argument("input", help="Input .mp4 file")
p.add_argument("--out-video", help="Output annotated MP4 path (default: _annotated.mp4)")
p.add_argument("--out-csv", help="Output CSV path for landmarks (default: _landmarks.csv)")
p.add_argument("--model-complexity", type=int, default=1, choices=[0, 1, 2], help="Holistic model complexity")
p.add_argument("--no-smooth", action="store_true", help="Disable smoothing (smoothing is ON by default)")
p.add_argument("--refine-face", action="store_true", help="Refine face landmarks (iris, lips).")
p.add_argument("--show", action="store_true", help="Show preview window while processing")
return p.parse_args()
def open_video_writer(cap, out_path):
# Properties from input
fps = cap.get(cv2.CAP_PROP_FPS)
if fps is None or fps <= 0:
fps = 30.0 # sensible fallback
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# Writer
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
writer = cv2.VideoWriter(out_path, fourcc, float(fps), (width, height))
if not writer.isOpened():
raise RuntimeError(f"Failed to open VideoWriter at {out_path}")
return writer, fps, (width, height)
def write_landmarks_to_csv(writer, frame_idx, ts_ms, kind, landmarks, world_landmarks=None, handedness=None):
"""
landmarks: NormalizedLandmarkList (x,y,z, visibility?) -> face/hand have no visibility; pose has visibility.
world_landmarks: LandmarkList in meters (optional, pose_world_landmarks available).
handedness: "Left"|"Right"|None (we label hand sets by field name; not a confidence score here)
"""
if not landmarks:
return
# index by position; world coords may be absent or differ in length
wl = world_landmarks.landmark if world_landmarks and getattr(world_landmarks, "landmark", None) else None
for i, lm in enumerate(landmarks.landmark):
world_x = world_y = world_z = ""
if wl and i < len(wl):
world_x, world_y, world_z = wl[i].x, wl[i].y, wl[i].z
# Some landmark types (pose) include visibility; others (face/hands) don't
vis = getattr(lm, "visibility", "")
writer.writerow([
frame_idx,
int(ts_ms),
kind, # e.g., face, pose, left_hand, right_hand
i,
lm.x, lm.y, lm.z,
vis,
"", # presence not provided in Holistic landmarks
world_x, world_y, world_z,
handedness or ""
])
def main():
args = parse_args()
in_path = Path(args.input)
if not in_path.exists():
print(f"Input not found: {in_path}", file=sys.stderr)
sys.exit(1)
out_video = Path(args.out_video) if args.out_video else in_path.with_name(in_path.stem + "_annotated.mp4")
out_csv = Path(args.out_csv) if args.out_csv else in_path.with_name(in_path.stem + "_landmarks.csv")
cap = cv2.VideoCapture(str(in_path))
if not cap.isOpened():
print(f"Could not open video: {in_path}", file=sys.stderr)
sys.exit(1)
writer, fps, (w, h) = open_video_writer(cap, str(out_video))
# Prepare CSV
out_csv.parent.mkdir(parents=True, exist_ok=True)
csv_file = open(out_csv, "w", newline="", encoding="utf-8")
csv_writer = csv.writer(csv_file)
csv_writer.writerow([
"frame", "timestamp_ms", "type", "landmark_index",
"x", "y", "z", "visibility", "presence",
"world_x", "world_y", "world_z", "handedness"
])
# Holistic configuration
holistic = mp_holistic.Holistic(
static_image_mode=False,
model_complexity=args.model_complexity,
smooth_landmarks=(not args.no_smooth),
refine_face_landmarks=args.refine_face,
enable_segmentation=False
)
try:
frame_idx = 0
print(f"Processing: {in_path.name} -> {out_video.name}, {out_csv.name}")
while True:
ok, frame_bgr = cap.read()
if not ok:
break
# Timestamp (ms) based on frame index and fps
ts_ms = (frame_idx / fps) * 1000.0
# Convert to RGB for MediaPipe
image_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
image_rgb.flags.writeable = False
results = holistic.process(image_rgb)
image_rgb.flags.writeable = True
# Draw on a BGR copy for output
out_frame = frame_bgr
# Face
if results.face_landmarks:
mp_drawing.draw_landmarks(
out_frame,
results.face_landmarks,
mp_holistic.FACEMESH_TESSELATION,
landmark_drawing_spec=None,
connection_drawing_spec=mp_styles.get_default_face_mesh_tesselation_style(),
)
write_landmarks_to_csv(csv_writer, frame_idx, ts_ms, "face", results.face_landmarks)
# Pose
if results.pose_landmarks:
mp_drawing.draw_landmarks(
out_frame,
results.pose_landmarks,
mp_holistic.POSE_CONNECTIONS,
landmark_drawing_spec=mp_styles.get_default_pose_landmarks_style()
)
write_landmarks_to_csv(
csv_writer, frame_idx, ts_ms, "pose",
results.pose_landmarks,
world_landmarks=getattr(results, "pose_world_landmarks", None)
)
# Left hand
if results.left_hand_landmarks:
mp_drawing.draw_landmarks(
out_frame,
results.left_hand_landmarks,
mp_holistic.HAND_CONNECTIONS,
landmark_drawing_spec=mp_styles.get_default_hand_landmarks_style()
)
write_landmarks_to_csv(csv_writer, frame_idx, ts_ms, "left_hand", results.left_hand_landmarks, handedness="Left")
# Right hand
if results.right_hand_landmarks:
mp_drawing.draw_landmarks(
out_frame,
results.right_hand_landmarks,
mp_holistic.HAND_CONNECTIONS,
landmark_drawing_spec=mp_styles.get_default_hand_landmarks_style()
)
write_landmarks_to_csv(csv_writer, frame_idx, ts_ms, "right_hand", results.right_hand_landmarks, handedness="Right")
# Write frame
writer.write(out_frame)
# Optional preview
if args.show:
cv2.imshow("Holistic (annotated)", out_frame)
if cv2.waitKey(1) & 0xFF == 27: # ESC
break
# Lightweight progress
if frame_idx % 120 == 0:
print(f" frame {frame_idx}", end="\r", flush=True)
frame_idx += 1
print(f"\nDone.\n Video: {out_video}\n CSV: {out_csv}")
finally:
holistic.close()
writer.release()
cap.release()
csv_file.close()
if args.show:
cv2.destroyAllWindows()
if __name__ == "__main__":
main()