Spaces:
Running
Running
Download collector/scripts/process_hands.py from Stouching-Robotics/DataFlowing: direct link, hf CLI and curl.
- Browser
- Download file 9.17 kB
-
https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/collector/scripts/process_hands.py
- Command line
-
hf download hf://spaces/Stouching-Robotics/DataFlowing/collector/scripts/process_hands.py
-
curl -L -o process_hands.py https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/collector/scripts/process_hands.py
9.17 kB
| #!/usr/bin/env python3 | |
| """ | |
| 手部关键点提取 CLI —— 对已录制会话离线处理。 | |
| 用法: | |
| python scripts/process_hands.py kpts /path/to/session --mode bare | |
| python scripts/process_hands.py label /path/to/session | |
| python scripts/process_hands.py all /path/to/session --mode bare | |
| """ | |
| import argparse | |
| import os | |
| import sys | |
| import subprocess | |
| import shutil | |
| import numpy as np | |
| import cv2 | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| from core.hand_tracking import process_session, label_session, load_hand_kpts, draw_kpts_overlay | |
| def _progress_bar(cur: int, total: int, width: int = 40): | |
| ratio = min(cur / max(total, 1), 1.0) | |
| filled = int(width * ratio) | |
| bar = "█" * filled + "░" * (width - filled) | |
| sys.stderr.write(f"\r [{bar}] {cur}/{total} ({ratio*100:.0f}%)") | |
| sys.stderr.flush() | |
| def cmd_kpts(args): | |
| """提取手部关键点(2D + 可选 3D)。""" | |
| print(f"提取手部关键点: {args.session}") | |
| print(f" 模式: {args.mode}, 设备: {args.device}") | |
| sys.stderr.write("\n") | |
| result = process_session( | |
| session_path=args.session, | |
| mode=args.mode, | |
| det_device=args.device, | |
| pose_device=args.device, | |
| progress_cb=_progress_bar, | |
| status_cb=lambda msg: print(f" [{msg}]"), | |
| ) | |
| sys.stderr.write("\n") | |
| if result["success"]: | |
| print(f"✅ 完成: {result['frames']} 帧, " | |
| f"{result['elapsed']:.1f}s, {result['fps']:.1f} fps") | |
| else: | |
| print(f"❌ 失败: {result['error']}") | |
| sys.exit(1) | |
| def cmd_label(args): | |
| """手势自动标注。""" | |
| print(f"手势标注: {args.session}") | |
| result = label_session( | |
| session_path=args.session, | |
| progress_cb=_progress_bar, | |
| ) | |
| sys.stderr.write("\n") | |
| if result["success"]: | |
| print(f"✅ 完成: {result['frames']} 帧, {result['elapsed']:.1f}s") | |
| else: | |
| print(f"❌ 失败: {result['error']}") | |
| sys.exit(1) | |
| def cmd_all(args): | |
| """先提取关键点,再标注。""" | |
| cmd_kpts(args) | |
| print() | |
| cmd_label(args) | |
| def cmd_show(args): | |
| """显示已有关键点信息。""" | |
| kpts = load_hand_kpts(args.session) | |
| if kpts: | |
| n = len(kpts) | |
| sample = kpts[min(kpts.keys())] | |
| print(f"手部关键点数据: {n} 帧") | |
| print(f" 维度: {len(sample['hand_data'])}") | |
| print(f" 最多手数: {sample['num_hands']}") | |
| else: | |
| print("❌ 未找到手部关键点数据") | |
| sys.exit(1) | |
| def _find_video(session_path: str) -> str: | |
| """在 session 目录下找第一个可用的 RGB 视频文件。""" | |
| from core.helpers import detect_session_format, egodata_video_path, egodata_metadata_path | |
| import json | |
| vdir = os.path.join(session_path, "videos") | |
| if not os.path.isdir(vdir): | |
| return "" | |
| fmt = detect_session_format(session_path) | |
| # 先尝试从 metadata 获取相机列表 | |
| if fmt == "egodata": | |
| meta_path = egodata_metadata_path(session_path) | |
| if os.path.isfile(meta_path): | |
| with open(meta_path) as f: | |
| cameras = json.load(f).get("cameras", {}) | |
| for cam_name in cameras: | |
| if cameras[cam_name].get("type") == "depth": | |
| continue | |
| for vp in [egodata_video_path(session_path, cam_name), | |
| os.path.join(vdir, f"{cam_name}.mp4")]: | |
| if os.path.isfile(vp): | |
| return vp | |
| # 直接扫描 videos/ 目录 | |
| for entry in sorted(os.listdir(vdir)): | |
| if "depth" in entry.lower(): | |
| continue | |
| full = os.path.join(vdir, entry) | |
| if os.path.isfile(full) and entry.endswith(".mp4"): | |
| return full | |
| # 子目录结构 | |
| sub_dir = os.path.join(full, "chunk-0000") | |
| if os.path.isdir(sub_dir): | |
| for fname in os.listdir(sub_dir): | |
| if fname.endswith(".mp4"): | |
| return os.path.join(sub_dir, fname) | |
| return "" | |
| def cmd_render(args): | |
| """将手部关键点叠加到视频上,导出可视化视频。""" | |
| import time | |
| from core.helpers import keypoints_video_dir | |
| session_path = args.session | |
| kpts = load_hand_kpts(session_path) | |
| if not kpts: | |
| print("❌ 未找到手部关键点数据,请先运行 kpts 命令") | |
| sys.exit(1) | |
| video_path = _find_video(session_path) | |
| if not video_path: | |
| print("❌ 未找到可播放的视频文件") | |
| sys.exit(1) | |
| cam_name = os.path.splitext(os.path.basename(video_path))[0] | |
| out_dir = keypoints_video_dir(session_path) | |
| os.makedirs(out_dir, exist_ok=True) | |
| out_path = os.path.join(out_dir, f"{cam_name}_hand_kpts.mp4") | |
| cap = cv2.VideoCapture(video_path) | |
| total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) | |
| fps = cap.get(cv2.CAP_PROP_FPS) | |
| if fps <= 0: | |
| fps = 30.0 | |
| ret, first = cap.read() | |
| if not ret: | |
| print("❌ 无法读取视频帧") | |
| sys.exit(1) | |
| h, w = first.shape[:2] | |
| cap.set(cv2.CAP_PROP_POS_FRAMES, 0) | |
| # ffmpeg 优先,cv2 回退 | |
| writer = None | |
| ffmpeg_proc = None | |
| if shutil.which("ffmpeg"): | |
| ffmpeg_cmd = [ | |
| "ffmpeg", "-y", "-f", "rawvideo", "-vcodec", "rawvideo", | |
| "-s", f"{w}x{h}", "-pix_fmt", "bgr24", "-r", str(fps), | |
| "-i", "-", "-c:v", "libx264", "-preset", "fast", "-crf", "23", | |
| "-pix_fmt", "yuv420p", out_path, | |
| ] | |
| try: | |
| ffmpeg_proc = subprocess.Popen( | |
| ffmpeg_cmd, stdin=subprocess.PIPE, | |
| stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | |
| except Exception: | |
| ffmpeg_proc = None | |
| if not ffmpeg_proc: | |
| for codec in ["avc1", "mp4v"]: | |
| fourcc = cv2.VideoWriter_fourcc(*codec) | |
| writer = cv2.VideoWriter(out_path, fourcc, fps, (w, h)) | |
| if writer.isOpened(): | |
| break | |
| print(f"输入视频: {video_path} ({total} 帧, {fps} fps)") | |
| print(f"手部关键点: {len(kpts)} 帧") | |
| print(f"输出: {out_path}") | |
| t0 = time.perf_counter() | |
| fi = 0 | |
| while True: | |
| ret, frame = cap.read() | |
| if not ret: | |
| break | |
| kpt = kpts.get(fi) | |
| if kpt is not None and np.any(kpt["hand_data"] > 0): | |
| frame = draw_kpts_overlay(frame, kpt["hand_data"], kpt["track_ids"]) | |
| if ffmpeg_proc: | |
| try: | |
| ffmpeg_proc.stdin.write(frame.tobytes()) | |
| except BrokenPipeError: | |
| break | |
| else: | |
| writer.write(frame) | |
| fi += 1 | |
| if fi % 50 == 0: | |
| _progress_bar(fi, total) | |
| cap.release() | |
| if ffmpeg_proc: | |
| ffmpeg_proc.stdin.close() | |
| ffmpeg_proc.wait() | |
| else: | |
| writer.release() | |
| elapsed = time.perf_counter() - t0 | |
| _progress_bar(total, total) | |
| sys.stderr.write("\n") | |
| size_mb = os.path.getsize(out_path) / (1024 * 1024) | |
| print(f"✅ 完成: {out_path}") | |
| print(f" {fi} 帧, {elapsed:.1f}s ({fi / elapsed:.1f} fps), {size_mb:.1f} MB") | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="手部关键点提取与标注", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| 示例: | |
| python scripts/process_hands.py kpts data/recordings/Test005/episode_000001 --mode bare | |
| python scripts/process_hands.py render data/recordings/Test005/episode_000001 | |
| python scripts/process_hands.py all data/recordings/Test005/episode_000001 --mode bare | |
| python scripts/process_hands.py show data/recordings/Test005/episode_000001 | |
| """, | |
| ) | |
| sub = parser.add_subparsers(dest="command", help="子命令") | |
| p_kpts = sub.add_parser("kpts", help="提取手部关键点") | |
| p_kpts.add_argument("session", help="会话目录路径") | |
| p_kpts.add_argument("--mode", default="glove", | |
| choices=["glove", "bare"], help="追踪模式") | |
| p_kpts.add_argument("--device", default="cuda", help="推理设备") | |
| p_label = sub.add_parser("label", help="手势自动标注") | |
| p_label.add_argument("session", help="会话目录路径") | |
| p_all = sub.add_parser("all", help="提取 + 标注") | |
| p_all.add_argument("session", help="会话目录路径") | |
| p_all.add_argument("--mode", default="glove", | |
| choices=["glove", "bare"], help="追踪模式") | |
| p_all.add_argument("--device", default="cuda", help="推理设备") | |
| p_show = sub.add_parser("show", help="查看已有数据") | |
| p_show.add_argument("session", help="会话目录路径") | |
| p_render = sub.add_parser("render", help="导出关键点可视化视频") | |
| p_render.add_argument("session", help="会话目录路径") | |
| args = parser.parse_args() | |
| if args.command == "kpts": | |
| cmd_kpts(args) | |
| elif args.command == "label": | |
| cmd_label(args) | |
| elif args.command == "all": | |
| cmd_all(args) | |
| elif args.command == "show": | |
| cmd_show(args) | |
| elif args.command == "render": | |
| cmd_render(args) | |
| else: | |
| parser.print_help() | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| main() | |