Buckets:
| """Turn a raw video clip into a grayscale mouth-ROI array of shape (T, H, W). | |
| Mouth localization tries, in order: | |
| 1. MediaPipe legacy Solutions API (``mp.solutions.face_mesh``, mediapipe <= ~0.10.20) | |
| 2. MediaPipe Tasks API (``FaceLandmarker``, modern mediapipe; downloads its | |
| ~4 MB model file to models/ on first use) | |
| 3. Fixed center crop of the lower-middle of the frame (no mediapipe at all). | |
| The fallback (3) is acceptable for LRW, whose clips are already face-centered, | |
| but landmark cropping matters for your own recordings. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import urllib.request | |
| from typing import List, Optional | |
| import cv2 | |
| import numpy as np | |
| # Mouth landmark indices — identical topology in legacy FaceMesh and the | |
| # Tasks-API FaceLandmarker (478-point mesh). | |
| _MOUTH_IDX = [61, 291, 0, 17, 40, 270, 39, 269, 37, 267, 84, 314, 91, 321] | |
| _MODEL_URL = ("https://storage.googleapis.com/mediapipe-models/face_landmarker/" | |
| "face_landmarker/float16/1/face_landmarker.task") | |
| _MODEL_PATH = os.environ.get( | |
| "MP_FACE_MODEL", | |
| os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), | |
| "models", "face_landmarker.task")) | |
| # Unified detector: callable(frame_bgr) -> [(x, y) normalized] or None. | |
| _detector = None | |
| _detector_failed = False | |
| def _init_detector() -> None: | |
| global _detector, _detector_failed | |
| if _detector is not None or _detector_failed: | |
| return | |
| try: | |
| import mediapipe as mp | |
| if hasattr(mp, "solutions"): # ---- legacy Solutions API ---- | |
| mesh = mp.solutions.face_mesh.FaceMesh( | |
| static_image_mode=False, max_num_faces=1, | |
| refine_landmarks=True, min_detection_confidence=0.5, | |
| min_tracking_confidence=0.5) | |
| def detect(frame_bgr: np.ndarray): | |
| res = mesh.process(cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)) | |
| if not res.multi_face_landmarks: | |
| return None | |
| lm = res.multi_face_landmarks[0].landmark | |
| return [(lm[i].x, lm[i].y) for i in _MOUTH_IDX] | |
| else: # ---- modern Tasks API ---- | |
| from mediapipe.tasks import python as mp_tasks | |
| from mediapipe.tasks.python import vision | |
| if not os.path.isfile(_MODEL_PATH): | |
| os.makedirs(os.path.dirname(_MODEL_PATH), exist_ok=True) | |
| print(f"[preprocess] downloading FaceLandmarker model " | |
| f"-> {_MODEL_PATH}") | |
| urllib.request.urlretrieve(_MODEL_URL, _MODEL_PATH) | |
| landmarker = vision.FaceLandmarker.create_from_options( | |
| vision.FaceLandmarkerOptions( | |
| base_options=mp_tasks.BaseOptions( | |
| model_asset_path=_MODEL_PATH), | |
| running_mode=vision.RunningMode.IMAGE, num_faces=1)) | |
| def detect(frame_bgr: np.ndarray): | |
| img = mp.Image(image_format=mp.ImageFormat.SRGB, | |
| data=cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)) | |
| res = landmarker.detect(img) | |
| if not res.face_landmarks: | |
| return None | |
| lm = res.face_landmarks[0] | |
| return [(lm[i].x, lm[i].y) for i in _MOUTH_IDX] | |
| _detector = detect | |
| except Exception as e: # pragma: no cover - environment dependent | |
| print(f"[preprocess] mediapipe unavailable " | |
| f"({type(e).__name__}: {e}) — using center-crop fallback") | |
| _detector_failed = True | |
| def _read_frames(video_path: str) -> List[np.ndarray]: | |
| cap = cv2.VideoCapture(video_path) | |
| frames = [] | |
| while True: | |
| ok, frame = cap.read() | |
| if not ok: | |
| break | |
| frames.append(frame) # BGR | |
| cap.release() | |
| if not frames: | |
| raise RuntimeError(f"Could not read any frames from {video_path}") | |
| return frames | |
| def _mouth_box(frame_bgr: np.ndarray, pad: float = 0.6): | |
| _init_detector() | |
| if _detector is None: | |
| return None | |
| pts = _detector(frame_bgr) | |
| if pts is None: | |
| return None | |
| h, w = frame_bgr.shape[:2] | |
| xs = [x * w for x, _ in pts] | |
| ys = [y * h for _, y in pts] | |
| cx, cy = np.mean(xs), np.mean(ys) | |
| half = max(max(xs) - min(xs), max(ys) - min(ys)) * (0.5 + pad) | |
| return cx, cy, half | |
| def _crop(frame_bgr: np.ndarray, box, out_size: int) -> np.ndarray: | |
| h, w = frame_bgr.shape[:2] | |
| if box is None: # center-crop fallback (lower-middle of frame) | |
| side = min(h, w) // 2 | |
| cx, cy = w // 2, int(h * 0.62) | |
| else: | |
| cx, cy, half = box | |
| side = int(half) | |
| x0 = int(np.clip(cx - side, 0, w - 1)) | |
| y0 = int(np.clip(cy - side, 0, h - 1)) | |
| x1 = int(np.clip(cx + side, 1, w)) | |
| y1 = int(np.clip(cy + side, 1, h)) | |
| roi = frame_bgr[y0:y1, x0:x1] | |
| if roi.size == 0: | |
| roi = frame_bgr | |
| gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) | |
| return cv2.resize(gray, (out_size, out_size), interpolation=cv2.INTER_AREA) | |
| def _resample_time(frames: List[np.ndarray], n: int) -> List[np.ndarray]: | |
| if len(frames) == n: | |
| return frames | |
| idx = np.linspace(0, len(frames) - 1, n).round().astype(int) | |
| return [frames[i] for i in idx] | |
| def extract_mouth_clip(video_path: str, out_size: int = 96, | |
| num_frames: Optional[int] = 29) -> np.ndarray: | |
| """Return a float32 array (T, out_size, out_size) with values in [0, 1].""" | |
| frames = _read_frames(video_path) | |
| # Detect the mouth box once per clip (first frame) for temporal stability; | |
| # fall back per-frame if the first-frame detection fails. | |
| box = _mouth_box(frames[0]) | |
| crops = [] | |
| for f in frames: | |
| b = box if box is not None else _mouth_box(f) | |
| crops.append(_crop(f, b, out_size)) | |
| if num_frames is not None: | |
| crops = _resample_time(crops, num_frames) | |
| clip = np.stack(crops).astype(np.float32) / 255.0 | |
| return clip | |
Xet Storage Details
- Size:
- 5.95 kB
- Xet hash:
- 16a9b5a535749acd98da8a9bd259f885299eb304eff6c9c1a8b5b5036764c9ba
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.