File size: 9,171 Bytes
d646dd7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
#!/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()