| import os |
| import json |
| import glob |
| import multiprocessing |
| import argparse |
| import os.path |
| import shutil |
| import cv2 |
| import mediapipe as mp |
| from tqdm.auto import tqdm |
| from joblib import Parallel, delayed |
| import numpy as np |
| import gc |
| import warnings |
| import hashlib |
| from contextlib import contextmanager |
| from typing import Optional |
| import joblib |
|
|
| from video_preprocess import PreprocessConfig, apply_darken_then_brighten |
|
|
|
|
| |
| |
| _EYEBROW_IDXS = [ |
| |
| 70, |
| 63, |
| 105, |
| 66, |
| 107, |
| 55, |
| 65, |
| 52, |
| 53, |
| 46, |
| |
| 336, |
| 296, |
| 334, |
| 293, |
| 300, |
| 276, |
| 283, |
| 282, |
| 295, |
| 285, |
| ] |
|
|
|
|
| @contextmanager |
| def tqdm_joblib(tqdm_object): |
| """Context manager to patch joblib so tqdm reports completed tasks (not just dispatched).""" |
|
|
| class TqdmBatchCompletionCallback(joblib.parallel.BatchCompletionCallBack): |
| def __call__(self, *args, **kwargs): |
| tqdm_object.update(n=self.batch_size) |
| return super().__call__(*args, **kwargs) |
|
|
| old_callback = joblib.parallel.BatchCompletionCallBack |
| joblib.parallel.BatchCompletionCallBack = TqdmBatchCompletionCallback |
| try: |
| yield tqdm_object |
| finally: |
| joblib.parallel.BatchCompletionCallBack = old_callback |
| tqdm_object.close() |
|
|
|
|
| def _label_from_path(path: str) -> str: |
| |
| label = path.split("/")[-2] |
| return "".join([i for i in label if i.isalpha()]).lower() |
|
|
|
|
| def _uid_from_path(path: str, label: str) -> str: |
| uid = os.path.splitext(os.path.basename(path))[0] |
| return "_".join([label, uid]) |
|
|
|
|
| def _try_open_video(path: str) -> cv2.VideoCapture: |
| cap = cv2.VideoCapture(path) |
| if cap.isOpened(): |
| return cap |
|
|
| |
| if hasattr(cv2, "CAP_FFMPEG"): |
| cap2 = cv2.VideoCapture(path, cv2.CAP_FFMPEG) |
| if cap2.isOpened(): |
| return cap2 |
|
|
| return cap |
|
|
|
|
| def _pose25_from_mp_pose(pose_landmarks): |
| if pose_landmarks is None: |
| return [np.nan] * 25, [np.nan] * 25 |
|
|
| lm = pose_landmarks.landmark |
|
|
| def get_xy(i: int): |
| return lm[i].x, lm[i].y |
|
|
| def avg_xy(a: int, b: int): |
| ax, ay = get_xy(a) |
| bx, by = get_xy(b) |
| return (ax + bx) / 2.0, (ay + by) / 2.0 |
|
|
| |
| |
| nose = get_xy(0) |
| |
| neck = avg_xy(11, 12) |
|
|
| rshoulder = get_xy(12) |
| relbow = get_xy(14) |
| rwrist = get_xy(16) |
|
|
| lshoulder = get_xy(11) |
| lelbow = get_xy(13) |
| lwrist = get_xy(15) |
|
|
| midhip = avg_xy(23, 24) |
|
|
| rhip = get_xy(24) |
| rknee = get_xy(26) |
| rankle = get_xy(28) |
|
|
| lhip = get_xy(23) |
| lknee = get_xy(25) |
| lankle = get_xy(27) |
|
|
| |
| reye = get_xy(5) |
| leye = get_xy(2) |
| rear = get_xy(8) |
| lear = get_xy(7) |
|
|
| |
| lbigtoe = get_xy(31) |
| lsmalltoe = get_xy(31) |
| lheel = get_xy(29) |
|
|
| rbigtoe = get_xy(32) |
| rsmalltoe = get_xy(32) |
| rheel = get_xy(30) |
|
|
| points = [ |
| nose, |
| neck, |
| rshoulder, |
| relbow, |
| rwrist, |
| lshoulder, |
| lelbow, |
| lwrist, |
| midhip, |
| rhip, |
| rknee, |
| rankle, |
| lhip, |
| lknee, |
| lankle, |
| reye, |
| leye, |
| rear, |
| lear, |
| lbigtoe, |
| lsmalltoe, |
| lheel, |
| rbigtoe, |
| rsmalltoe, |
| rheel, |
| ] |
|
|
| xs = [p[0] for p in points] |
| ys = [p[1] for p in points] |
| return xs, ys |
|
|
|
|
| def _hand21_from_landmarks(hand_landmarks): |
| if hand_landmarks is None: |
| return [np.nan] * 21, [np.nan] * 21 |
| xs, ys = [], [] |
| for landmark in hand_landmarks.landmark: |
| xs.append(landmark.x) |
| ys.append(landmark.y) |
| return xs, ys |
|
|
|
|
| def _face_from_landmarks(face_landmarks, face_mode: str): |
| if face_mode == "none": |
| return None, None |
|
|
| if face_mode == "full": |
| idxs = None |
| n = 468 |
| elif face_mode == "eyebrows": |
| idxs = _EYEBROW_IDXS |
| n = len(_EYEBROW_IDXS) |
| else: |
| raise ValueError(f"Unsupported face_mode: {face_mode}") |
|
|
| if face_landmarks is None: |
| return [np.nan] * n, [np.nan] * n |
|
|
| lm = face_landmarks.landmark |
| if idxs is None: |
| xs = [p.x for p in lm] |
| ys = [p.y for p in lm] |
| return xs, ys |
|
|
| xs, ys = [], [] |
| for i in idxs: |
| xs.append(lm[i].x) |
| ys.append(lm[i].y) |
| return xs, ys |
|
|
|
|
| def process_video( |
| path: str, |
| save_dir: str, |
| *, |
| use_holistic: bool = False, |
| face_mode: str = "none", |
| write_placeholders: bool = False, |
| preprocess_config: Optional[PreprocessConfig] = None, |
| export_darkened_dir: Optional[str] = None, |
| uid_suffix: str = "", |
| ): |
| if not hasattr(mp, "solutions"): |
| raise RuntimeError( |
| "mediapipe does not expose mp.solutions. " |
| "This usually means an incompatible mediapipe build. " |
| "Use Python 3.9/3.10 and reinstall mediapipe==0.10.14." |
| ) |
| if not os.path.isfile(path): |
| warnings.warn(path + " file not found") |
| return |
|
|
| label = _label_from_path(path) |
| uid = _uid_from_path(path, label) |
| if uid_suffix: |
| uid = f"{uid}{uid_suffix}" |
|
|
| cap = _try_open_video(path) |
| if not cap.isOpened(): |
| warnings.warn(f"OpenCV could not open video: {path}") |
| return |
|
|
| pose_points_x, pose_points_y = [], [] |
| hand1_points_x, hand1_points_y = [], [] |
| hand2_points_x, hand2_points_y = [], [] |
| face_points_x, face_points_y = [], [] |
|
|
| n_frames = 0 |
| rng = None |
| darken_factor = None |
| writer = None |
|
|
| if preprocess_config is not None: |
| seed = int(hashlib.md5(path.encode("utf-8")).hexdigest()[:8], 16) |
| rng = np.random.default_rng(seed) |
| if preprocess_config.apply_darken: |
| darken_factor = float( |
| rng.uniform(preprocess_config.darken_min, preprocess_config.darken_max) |
| ) |
|
|
| if export_darkened_dir: |
| os.makedirs(export_darkened_dir, exist_ok=True) |
| fps = cap.get(cv2.CAP_PROP_FPS) |
| if not fps or fps <= 0: |
| fps = 25 |
| width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) |
| height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) |
| out_path = os.path.join(export_darkened_dir, f"{uid}.mp4") |
| writer = cv2.VideoWriter( |
| out_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (width, height) |
| ) |
|
|
| if use_holistic: |
| holistic = mp.solutions.holistic.Holistic( |
| static_image_mode=False, |
| model_complexity=1, |
| smooth_landmarks=True, |
| enable_segmentation=False, |
| refine_face_landmarks=True, |
| min_detection_confidence=0.5, |
| min_tracking_confidence=0.5, |
| ) |
| try: |
| while cap.isOpened(): |
| ret, image = cap.read() |
| if not ret: |
| break |
|
|
| if preprocess_config is not None: |
| image = apply_darken_then_brighten( |
| image, |
| config=preprocess_config, |
| rng=rng, |
| darken_factor=darken_factor, |
| ) |
| if writer is not None: |
| writer.write(image) |
|
|
| image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) |
| results = holistic.process(image) |
|
|
| pose_x, pose_y = _pose25_from_mp_pose(results.pose_landmarks) |
| |
| h1x, h1y = _hand21_from_landmarks(results.left_hand_landmarks) |
| h2x, h2y = _hand21_from_landmarks(results.right_hand_landmarks) |
|
|
| fx, fy = _face_from_landmarks(results.face_landmarks, face_mode) |
|
|
| pose_points_x.append(pose_x) |
| pose_points_y.append(pose_y) |
| hand1_points_x.append(h1x) |
| hand1_points_y.append(h1y) |
| hand2_points_x.append(h2x) |
| hand2_points_y.append(h2y) |
|
|
| if face_mode != "none": |
| face_points_x.append(fx) |
| face_points_y.append(fy) |
|
|
| n_frames += 1 |
| finally: |
| holistic.close() |
| else: |
| |
| hands = mp.solutions.hands.Hands( |
| min_detection_confidence=0.5, min_tracking_confidence=0.5 |
| ) |
| pose = mp.solutions.pose.Pose( |
| static_image_mode=False, |
| model_complexity=1, |
| enable_segmentation=False, |
| min_detection_confidence=0.5, |
| min_tracking_confidence=0.5, |
| ) |
| try: |
| while cap.isOpened(): |
| ret, image = cap.read() |
| if not ret: |
| break |
| if preprocess_config is not None: |
| image = apply_darken_then_brighten( |
| image, |
| config=preprocess_config, |
| rng=rng, |
| darken_factor=darken_factor, |
| ) |
| if writer is not None: |
| writer.write(image) |
| image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) |
|
|
| hand_results = hands.process(image) |
| pose_results = pose.process(image) |
|
|
| |
| hand1 = ( |
| hand_results.multi_hand_landmarks[0] |
| if hand_results.multi_hand_landmarks |
| else None |
| ) |
| hand2 = ( |
| hand_results.multi_hand_landmarks[1] |
| if hand_results.multi_hand_landmarks |
| and len(hand_results.multi_hand_landmarks) > 1 |
| else None |
| ) |
| h1x, h1y = _hand21_from_landmarks(hand1) |
| h2x, h2y = _hand21_from_landmarks(hand2) |
|
|
| |
| pose_x, pose_y = _pose25_from_mp_pose(pose_results.pose_landmarks) |
|
|
| pose_points_x.append(pose_x) |
| pose_points_y.append(pose_y) |
| hand1_points_x.append(h1x) |
| hand1_points_y.append(h1y) |
| hand2_points_x.append(h2x) |
| hand2_points_y.append(h2y) |
|
|
| n_frames += 1 |
| finally: |
| hands.close() |
| pose.close() |
|
|
| cap.release() |
| if writer is not None: |
| writer.release() |
|
|
| if n_frames == 0 and not write_placeholders: |
| warnings.warn(f"No frames decoded (n_frames=0). Skipping: {path}") |
| return |
|
|
| |
| if n_frames == 0 and write_placeholders: |
| pose_points_x = [[np.nan] * 25] |
| pose_points_y = [[np.nan] * 25] |
| hand1_points_x = [[np.nan] * 21] |
| hand1_points_y = [[np.nan] * 21] |
| hand2_points_x = [[np.nan] * 21] |
| hand2_points_y = [[np.nan] * 21] |
| if face_mode != "none": |
| n_face = 468 if face_mode == "full" else len(_EYEBROW_IDXS) |
| face_points_x = [[np.nan] * n_face] |
| face_points_y = [[np.nan] * n_face] |
|
|
| save_data = { |
| "uid": uid, |
| "label": label, |
| "pose_x": pose_points_x, |
| "pose_y": pose_points_y, |
| "hand1_x": hand1_points_x, |
| "hand1_y": hand1_points_y, |
| "hand2_x": hand2_points_x, |
| "hand2_y": hand2_points_y, |
| "n_frames": n_frames, |
| } |
| if face_mode != "none": |
| save_data["face_x"] = face_points_x |
| save_data["face_y"] = face_points_y |
|
|
| with open(os.path.join(save_dir, f"{uid}.json"), "w") as f: |
| json.dump(save_data, f) |
|
|
| del save_data |
| gc.collect() |
|
|
|
|
| def _build_label_dir_map(include_dir: str) -> dict[str, str]: |
| """Maps lowercased directory name -> actual directory name.""" |
| mapping: dict[str, str] = {} |
| try: |
| for name in os.listdir(include_dir): |
| full = os.path.join(include_dir, name) |
| if os.path.isdir(full): |
| mapping[name.lower()] = name |
| cleaned = ''.join([c for c in name if c.isalpha()]).lower() |
| if cleaned and cleaned not in mapping: |
| mapping[cleaned] = name |
| except FileNotFoundError: |
| return mapping |
| return mapping |
|
|
|
|
| def _resolve_video_path(include_dir: str, rel_path: str, label_dir_map: dict[str, str]) -> str: |
| """Resolves train_test_paths entries against different dataset folder layouts. |
| |
| Supports: |
| 1) Official INCLUDE layout: <include_dir>/<Category>/<N. Label>/<file> |
| 2) Flat label layout: <include_dir>/<Label>/<file> |
| """ |
| direct = os.path.join(include_dir, rel_path) |
| if os.path.isfile(direct): |
| return direct |
|
|
| |
| parts = [p for p in rel_path.split('/') if p] |
| filename = os.path.basename(rel_path) |
| label = None |
| if len(parts) >= 2: |
| label_part = parts[-2] |
| label = ''.join([c for c in label_part if c.isalpha()]).lower() |
|
|
| if label: |
| dir_name = label_dir_map.get(label) |
| if dir_name: |
| cand = os.path.join(include_dir, dir_name, filename) |
| if os.path.isfile(cand): |
| return cand |
|
|
| |
| for variant in (label.capitalize(), label.title()): |
| dir_name = label_dir_map.get(variant.lower()) |
| if dir_name: |
| cand = os.path.join(include_dir, dir_name, filename) |
| if os.path.isfile(cand): |
| return cand |
|
|
| return direct |
|
|
|
|
| def load_file(path, include_dir): |
| with open(path, "r") as fp: |
| rel_paths = [ln.strip() for ln in fp.read().splitlines() if ln.strip()] |
|
|
| label_dir_map = _build_label_dir_map(include_dir) |
| return [_resolve_video_path(include_dir, rel, label_dir_map) for rel in rel_paths] |
|
|
|
|
| def scan_include_dir_videos(include_dir: str) -> list[str]: |
| """Scans include_dir for videos in a flat label-folder layout. |
| |
| Expected layout: |
| <include_dir>/<Label>/*.MOV (or mp4/avi/mkv) |
| """ |
| exts = ("*.MOV", "*.mov", "*.MP4", "*.mp4", "*.AVI", "*.avi", "*.MKV", "*.mkv") |
| videos: list[str] = [] |
| for ext in exts: |
| videos.extend(glob.glob(os.path.join(include_dir, "*", ext))) |
| |
| return sorted(set(videos)) |
|
|
|
|
| def split_paths( |
| paths: list[str], seed: int = 0, train_ratio: float = 0.8, val_ratio: float = 0.1 |
| ): |
| if not (0.0 < train_ratio < 1.0): |
| raise ValueError( |
| f"train_ratio must be in (0, 1), got {train_ratio}. " |
| "Example: --train_ratio 0.7" |
| ) |
| if not (0.0 <= val_ratio < 1.0): |
| raise ValueError( |
| f"val_ratio must be in [0, 1), got {val_ratio}. " |
| "Example: --val_ratio 0.15" |
| ) |
| if train_ratio + val_ratio >= 1.0: |
| raise ValueError( |
| f"train_ratio + val_ratio must be < 1.0, got {train_ratio + val_ratio:.4f}." |
| ) |
| if not paths: |
| return [], [], [] |
| rng = np.random.default_rng(seed) |
| idx = np.arange(len(paths)) |
| rng.shuffle(idx) |
| paths = [paths[i] for i in idx] |
|
|
| n = len(paths) |
| n_train = int(train_ratio * n) |
| n_val = int(val_ratio * n) |
|
|
| train = paths[:n_train] |
| val = paths[n_train : n_train + n_val] |
| test = paths[n_train + n_val :] |
| return train, val, test |
|
|
|
|
| def _hash_fraction(seed: int, path: str) -> float: |
| digest = hashlib.md5(f"{seed}|{path}".encode("utf-8")).hexdigest() |
| value = int(digest[:8], 16) |
| return value / float(2**32) |
|
|
|
|
| def select_paths_by_probability( |
| paths: list[str], *, probability: float, seed: int |
| ) -> list[str]: |
| if probability <= 0.0: |
| return [] |
| if probability >= 1.0: |
| return list(paths) |
| return [p for p in paths if _hash_fraction(seed, p) < probability] |
|
|
|
|
| def load_train_test_val_paths(include_dir: str, dataset: str): |
| train_paths = load_file(f"train_test_paths/{dataset}_train.txt", include_dir) |
| val_paths = load_file(f"train_test_paths/{dataset}_val.txt", include_dir) |
| test_paths = load_file(f"train_test_paths/{dataset}_test.txt", include_dir) |
| return train_paths, val_paths, test_paths |
|
|
|
|
| def save_keypoints( |
| *, |
| dataset: str, |
| file_paths: list[str], |
| mode: str, |
| save_root: str, |
| n_jobs: int, |
| use_holistic: bool, |
| face_mode: str, |
| limit: int, |
| no_parallel: bool, |
| write_placeholders: bool, |
| preprocess_config: Optional[PreprocessConfig], |
| export_darkened_dir: Optional[str], |
| uid_suffix: str = "", |
| ): |
| save_dir = os.path.join(save_root, f"{dataset}_{mode}_keypoints") |
| os.makedirs(save_dir, exist_ok=True) |
|
|
| if limit and limit > 0: |
| file_paths = file_paths[:limit] |
|
|
| existing = [p for p in file_paths if os.path.isfile(p)] |
| missing = len(file_paths) - len(existing) |
| if missing: |
| warnings.warn(f"{missing} {mode} videos missing on disk (skipping them)") |
|
|
| if no_parallel or n_jobs == 1: |
| for path in tqdm(existing, desc=f"processing {mode} videos"): |
| process_video( |
| path, |
| save_dir, |
| use_holistic=use_holistic, |
| face_mode=face_mode, |
| write_placeholders=write_placeholders, |
| preprocess_config=preprocess_config, |
| export_darkened_dir=export_darkened_dir, |
| uid_suffix=uid_suffix, |
| ) |
| return |
|
|
| if not existing: |
| return |
|
|
| with tqdm_joblib(tqdm(total=len(existing), desc=f"processing {mode} videos")): |
| Parallel(n_jobs=n_jobs, backend="multiprocessing")( |
| delayed(process_video)( |
| path, |
| save_dir, |
| use_holistic=use_holistic, |
| face_mode=face_mode, |
| write_placeholders=write_placeholders, |
| preprocess_config=preprocess_config, |
| export_darkened_dir=export_darkened_dir, |
| uid_suffix=uid_suffix, |
| ) |
| for path in existing |
| ) |
|
|
|
|
| def preflight_videos(video_paths: list[str], n: int = 10): |
| """Quickly checks existence + OpenCV decode for a small sample.""" |
| sample = video_paths[: max(0, n)] |
| exists = 0 |
| opened = 0 |
| first_read_ok = 0 |
|
|
| for path in sample: |
| if not os.path.isfile(path): |
| continue |
| exists += 1 |
| cap = _try_open_video(path) |
| if not cap.isOpened(): |
| cap.release() |
| continue |
| opened += 1 |
| ok, _frame = cap.read() |
| if ok: |
| first_read_ok += 1 |
| cap.release() |
|
|
| print('Preflight results:') |
| print(f' sample size: {len(sample)}') |
| print(f' exists on disk: {exists}') |
| print(f' OpenCV opened: {opened}') |
| print(f' first frame decoded: {first_read_ok}') |
|
|
|
|
| if __name__ == "__main__": |
| parser = argparse.ArgumentParser(description="Generate keypoints from MediaPipe") |
| parser.add_argument( |
| "--include_dir", |
| default="", |
| type=str, |
| required=True, |
| help="path to the location of INCLUDE/INCLUDE50 videos", |
| ) |
| parser.add_argument( |
| "--save_dir", |
| default="", |
| type=str, |
| required=True, |
| help="location to output json files", |
| ) |
| parser.add_argument( |
| "--dataset", default="include", type=str, help="options: include or include50" |
| ) |
| parser.add_argument( |
| "--video", |
| default=None, |
| type=str, |
| help="process a single video path (bypasses train_test_paths)", |
| ) |
| parser.add_argument( |
| "--use_holistic", |
| action="store_true", |
| help="use MediaPipe Holistic (pose+hands+face) instead of separate pose/hands", |
| ) |
| parser.add_argument( |
| "--face_mode", |
| default="none", |
| choices=["none", "eyebrows", "full"], |
| help="face landmarks to save when using holistic", |
| ) |
| parser.add_argument( |
| "--splits", |
| default="all", |
| choices=["train", "val", "test", "all"], |
| help="which splits to process", |
| ) |
| parser.add_argument( |
| "--limit", |
| default=0, |
| type=int, |
| help="process only the first N videos per split (useful for testing)", |
| ) |
| parser.add_argument( |
| "--jobs", |
| default=multiprocessing.cpu_count(), |
| type=int, |
| help="number of parallel workers", |
| ) |
| parser.add_argument( |
| "--no_parallel", |
| action="store_true", |
| help="disable multiprocessing (easier debugging)", |
| ) |
| parser.add_argument( |
| "--write_placeholders", |
| action="store_true", |
| help="write placeholder NaN JSONs even when videos can't be decoded", |
| ) |
| parser.add_argument( |
| "--apply_darken", |
| action="store_true", |
| help="apply darken then brighten preprocessing before MediaPipe", |
| ) |
| parser.add_argument( |
| "--apply_brighten", |
| action="store_true", |
| help="apply brighten preprocessing without darkening", |
| ) |
| parser.add_argument( |
| "--darken_min", |
| default=0.3, |
| type=float, |
| help="minimum brightness multiplier for darkening", |
| ) |
| parser.add_argument( |
| "--darken_max", |
| default=0.8, |
| type=float, |
| help="maximum brightness multiplier for darkening", |
| ) |
| parser.add_argument( |
| "--brighten_method", |
| default="clahe", |
| choices=["clahe", "gamma"], |
| help="brighten method used after darkening", |
| ) |
| parser.add_argument( |
| "--brighten_gamma_min", |
| default=1.2, |
| type=float, |
| help="minimum gamma for gamma brighten", |
| ) |
| parser.add_argument( |
| "--brighten_gamma_max", |
| default=1.8, |
| type=float, |
| help="maximum gamma for gamma brighten", |
| ) |
| parser.add_argument( |
| "--export_darkened_dir", |
| default=None, |
| type=str, |
| help="optional directory to save darkened+brightened videos", |
| ) |
|
|
| parser.add_argument( |
| "--preflight", |
| action="store_true", |
| help="only check that videos exist and can be decoded, then exit", |
| ) |
| parser.add_argument( |
| "--preflight_n", |
| default=10, |
| type=int, |
| help="number of videos to check in preflight", |
| ) |
|
|
| parser.add_argument( |
| "--scan", |
| action="store_true", |
| help="ignore train_test_paths and scan include_dir/<label>/* for videos, then make a random train/val/test split", |
| ) |
| parser.add_argument( |
| "--split_seed", |
| default=0, |
| type=int, |
| help="seed used when --scan creates train/val/test splits", |
| ) |
| parser.add_argument( |
| "--train_ratio", |
| default=0.8, |
| type=float, |
| help="train split ratio used with --scan", |
| ) |
| parser.add_argument( |
| "--val_ratio", |
| default=0.1, |
| type=float, |
| help="validation split ratio used with --scan", |
| ) |
| parser.add_argument( |
| "--clean_output", |
| action="store_true", |
| help="delete selected split output folders before generating keypoints", |
| ) |
| parser.add_argument( |
| "--train_dark_prob", |
| default=0.0, |
| type=float, |
| help="probability of creating an extra darkened sample for each train video", |
| ) |
| parser.add_argument( |
| "--train_dark_suffix", |
| default="__dark", |
| type=str, |
| help="suffix added to uid for train darkened variants", |
| ) |
| parser.add_argument( |
| "--train_dark_seed", |
| default=None, |
| type=int, |
| help="seed for selecting train videos for dark variants (defaults to --split_seed)", |
| ) |
|
|
| args = parser.parse_args() |
|
|
| if not (0.0 <= args.train_dark_prob <= 1.0): |
| raise ValueError( |
| f"--train_dark_prob must be in [0, 1], got {args.train_dark_prob}." |
| ) |
| if args.train_dark_prob > 0 and not args.train_dark_suffix: |
| raise ValueError("--train_dark_suffix must be non-empty when --train_dark_prob > 0.") |
| if "/" in args.train_dark_suffix or "\\" in args.train_dark_suffix: |
| raise ValueError("--train_dark_suffix must not contain path separators.") |
|
|
| preprocess_config = None |
| if args.apply_darken or args.apply_brighten: |
| preprocess_config = PreprocessConfig( |
| apply_darken=args.apply_darken, |
| apply_brighten=True, |
| darken_min=args.darken_min, |
| darken_max=args.darken_max, |
| brighten_method=args.brighten_method, |
| brighten_gamma_min=args.brighten_gamma_min, |
| brighten_gamma_max=args.brighten_gamma_max, |
| ) |
|
|
| if args.video is not None: |
| single_dir = os.path.join(args.save_dir, f"{args.dataset}_single_keypoints") |
| os.makedirs(single_dir, exist_ok=True) |
| process_video( |
| args.video, |
| single_dir, |
| use_holistic=args.use_holistic, |
| face_mode=args.face_mode, |
| write_placeholders=args.write_placeholders, |
| preprocess_config=preprocess_config, |
| export_darkened_dir=args.export_darkened_dir, |
| ) |
| print(f"Saved single-video keypoints to: {single_dir}") |
| raise SystemExit(0) |
|
|
| if args.scan: |
| all_paths = scan_include_dir_videos(args.include_dir) |
| train_paths, val_paths, test_paths = split_paths( |
| all_paths, |
| seed=args.split_seed, |
| train_ratio=args.train_ratio, |
| val_ratio=args.val_ratio, |
| ) |
| else: |
| train_paths, val_paths, test_paths = load_train_test_val_paths( |
| args.include_dir, args.dataset |
| ) |
|
|
| if args.preflight: |
| paths = [] |
| if args.splits in ("train", "all"): |
| paths.extend(train_paths) |
| if args.splits in ("val", "all"): |
| paths.extend(val_paths) |
| if args.splits in ("test", "all"): |
| paths.extend(test_paths) |
| preflight_videos(paths, n=args.preflight_n) |
| raise SystemExit(0) |
|
|
| selected_splits = ( |
| ["train", "val", "test"] if args.splits == "all" else [args.splits] |
| ) |
|
|
| if args.clean_output: |
| for split in selected_splits: |
| split_dir = os.path.join(args.save_dir, f"{args.dataset}_{split}_keypoints") |
| if os.path.isdir(split_dir): |
| shutil.rmtree(split_dir) |
| print(f"Removed existing output directory: {split_dir}") |
|
|
| if args.splits in ("val", "all"): |
| save_keypoints( |
| dataset=args.dataset, |
| file_paths=val_paths, |
| mode="val", |
| save_root=args.save_dir, |
| n_jobs=args.jobs, |
| use_holistic=args.use_holistic, |
| face_mode=args.face_mode, |
| limit=args.limit, |
| no_parallel=args.no_parallel, |
| write_placeholders=args.write_placeholders, |
| preprocess_config=preprocess_config, |
| export_darkened_dir=args.export_darkened_dir, |
| ) |
|
|
| if args.splits in ("test", "all"): |
| save_keypoints( |
| dataset=args.dataset, |
| file_paths=test_paths, |
| mode="test", |
| save_root=args.save_dir, |
| n_jobs=args.jobs, |
| use_holistic=args.use_holistic, |
| face_mode=args.face_mode, |
| limit=args.limit, |
| no_parallel=args.no_parallel, |
| write_placeholders=args.write_placeholders, |
| preprocess_config=preprocess_config, |
| export_darkened_dir=args.export_darkened_dir, |
| ) |
|
|
| if args.splits in ("train", "all"): |
| save_keypoints( |
| dataset=args.dataset, |
| file_paths=train_paths, |
| mode="train", |
| save_root=args.save_dir, |
| n_jobs=args.jobs, |
| use_holistic=args.use_holistic, |
| face_mode=args.face_mode, |
| limit=args.limit, |
| no_parallel=args.no_parallel, |
| write_placeholders=args.write_placeholders, |
| preprocess_config=preprocess_config, |
| export_darkened_dir=args.export_darkened_dir, |
| ) |
|
|
| if args.splits in ("train", "all") and args.train_dark_prob > 0: |
| train_dark_seed = ( |
| args.train_dark_seed if args.train_dark_seed is not None else args.split_seed |
| ) |
| train_candidates = train_paths[: args.limit] if args.limit and args.limit > 0 else train_paths |
| dark_train_paths = select_paths_by_probability( |
| train_candidates, probability=args.train_dark_prob, seed=train_dark_seed |
| ) |
| print( |
| f"Selected {len(dark_train_paths)}/{len(train_candidates)} train videos " |
| f"for dark variants (prob={args.train_dark_prob}, seed={train_dark_seed})." |
| ) |
| if dark_train_paths: |
| dark_preprocess_config = PreprocessConfig( |
| apply_darken=True, |
| apply_brighten=False, |
| darken_min=args.darken_min, |
| darken_max=args.darken_max, |
| brighten_method=args.brighten_method, |
| brighten_gamma_min=args.brighten_gamma_min, |
| brighten_gamma_max=args.brighten_gamma_max, |
| ) |
| save_keypoints( |
| dataset=args.dataset, |
| file_paths=dark_train_paths, |
| mode="train", |
| save_root=args.save_dir, |
| n_jobs=args.jobs, |
| use_holistic=args.use_holistic, |
| face_mode=args.face_mode, |
| limit=0, |
| no_parallel=args.no_parallel, |
| write_placeholders=args.write_placeholders, |
| preprocess_config=dark_preprocess_config, |
| export_darkened_dir=args.export_darkened_dir, |
| uid_suffix=args.train_dark_suffix, |
| ) |
|
|