| |
| """Pack Full_TriVis DWPose skeletons into flat memmaps for T2M-GPT training. |
| |
| Reads `Full_TriVis/split_lab_front.csv` (columns: split, npz_path, Sentence, |
| Sign_sentence, ...), loads each clip's DWPose `.npz` (all_xy [T,128,2], |
| all_score [T,128]) and concatenates every clip of a split into one contiguous |
| float16 array so that training can random-access frames without touching 24k |
| individual files on the shared volume. |
| |
| Per split it writes into --out-dir: |
| {split}_xy.npy float16 [total_frames, 256] xy flattened, NaN -> 0 |
| {split}_valid.npy uint8 [total_frames, 128] 1 = keypoint usable |
| {split}_index.json per-clip offsets + text fields |
| And from the *train* split only: mean.npy / std.npy [256] (valid-only stats). |
| |
| No body normalization is applied -- coordinates stay DWPose frame-normalized |
| [0,1] so absolute hand motion is preserved; the only transform is the global |
| per-dim standardization, which is invertible from mean/std. |
| """ |
| import argparse |
| import csv |
| import json |
| import os |
|
|
| import numpy as np |
| from tqdm import tqdm |
|
|
| from dataset.layout import Layout, PRESETS |
|
|
| FULL_NKP = 128 |
|
|
|
|
| def read_rows(csv_path): |
| with open(csv_path, newline="", encoding="utf-8") as f: |
| return list(csv.DictReader(f)) |
|
|
|
|
| def clip_len(path): |
| with np.load(path) as d: |
| return int(d["all_xy"].shape[0]) |
|
|
|
|
| def load_clip(path, score_thr, layout): |
| with np.load(path) as d: |
| xy = d["all_xy"].astype(np.float32) |
| sc = d["all_score"].astype(np.float32) |
| valid = np.isfinite(xy).all(-1) & (sc > score_thr) |
| xy = np.nan_to_num(xy, nan=0.0, posinf=0.0, neginf=0.0) |
| |
| keep = layout.keep |
| xy, valid = xy[:, keep, :], valid[:, keep] |
| return xy.reshape(len(xy), layout.dim), valid.astype(np.uint8) |
|
|
|
|
| def gloss_to_text(s): |
| """'1 | nam | mua | may ?' -> '1 nam mua may ?' (drop the gloss separator).""" |
| return " ".join(t.strip() for t in str(s).split("|") if t.strip()) |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--csv", default="../Full_TriVis/split_lab_front.csv") |
| ap.add_argument("--root", default="..", help="repo root that npz_path is relative to") |
| ap.add_argument("--out-dir", default="./dataset/VSL") |
| ap.add_argument("--score-thr", type=float, default=0.3) |
| ap.add_argument("--min-frames", type=int, default=64, help="drop clips shorter than this") |
| ap.add_argument("--limit", type=int, default=0, help="debug: only N clips per split") |
| ap.add_argument("--layout", default="full", choices=list(PRESETS), |
| help="keypoint set: 'upper' drops knees+ankles (never detected here)") |
| args = ap.parse_args() |
|
|
| os.makedirs(args.out_dir, exist_ok=True) |
| layout = Layout.preset(args.layout) |
| layout.save(args.out_dir) |
| print(layout) |
| NKP, DIM = layout.n_kpts, layout.dim |
| rows = read_rows(args.csv) |
| by_split = {} |
| for r in rows: |
| by_split.setdefault(r["split"], []).append(r) |
| print({k: len(v) for k, v in by_split.items()}) |
|
|
| for split, srows in sorted(by_split.items()): |
| if args.limit: |
| srows = srows[: args.limit] |
|
|
| |
| keep, lengths = [], [] |
| for r in tqdm(srows, desc=f"{split}: scan"): |
| p = os.path.join(args.root, r["npz_path"]) |
| try: |
| T = clip_len(p) |
| except Exception as e: |
| print(f" skip {r['npz_path']}: {e}") |
| continue |
| if T < args.min_frames: |
| continue |
| keep.append(r) |
| lengths.append(T) |
| total = int(sum(lengths)) |
| print(f"{split}: {len(keep)} clips, {total} frames") |
|
|
| xy_mm = np.lib.format.open_memmap( |
| os.path.join(args.out_dir, f"{split}_xy.npy"), |
| mode="w+", dtype=np.float16, shape=(total, DIM)) |
| vd_mm = np.lib.format.open_memmap( |
| os.path.join(args.out_dir, f"{split}_valid.npy"), |
| mode="w+", dtype=np.uint8, shape=(total, NKP)) |
|
|
| index, off = [], 0 |
| |
| s1 = np.zeros(DIM, np.float64) |
| s2 = np.zeros(DIM, np.float64) |
| cnt = np.zeros(DIM, np.float64) |
|
|
| for r, T in zip(tqdm(keep, desc=f"{split}: pack"), lengths): |
| p = os.path.join(args.root, r["npz_path"]) |
| xy, vd = load_clip(p, args.score_thr, layout) |
| assert len(xy) == T, (len(xy), T) |
| xy_mm[off:off + T] = xy.astype(np.float16) |
| vd_mm[off:off + T] = vd |
| if split == "train": |
| m = np.repeat(vd, 2, axis=1).astype(np.float64) |
| x64 = xy.astype(np.float64) |
| s1 += (x64 * m).sum(0) |
| s2 += (x64 * x64 * m).sum(0) |
| cnt += m.sum(0) |
| index.append({ |
| "name": os.path.splitext(os.path.basename(r["npz_path"]))[0], |
| "npz_path": r["npz_path"], |
| "start": off, |
| "length": T, |
| "gloss": gloss_to_text(r["Sign_sentence"]), |
| "sentence": r["Sentence"], |
| "category": r.get("Category", ""), |
| }) |
| off += T |
|
|
| xy_mm.flush() |
| vd_mm.flush() |
| del xy_mm, vd_mm |
| with open(os.path.join(args.out_dir, f"{split}_index.json"), "w", encoding="utf-8") as f: |
| json.dump(index, f, ensure_ascii=False) |
|
|
| if split == "train": |
| cnt = np.maximum(cnt, 1.0) |
| mean = s1 / cnt |
| var = np.maximum(s2 / cnt - mean ** 2, 0.0) |
| std = np.sqrt(var) |
| |
| |
| |
| |
| std[cnt < 100] = 1.0 |
| std = np.maximum(std, 1e-2) |
| np.save(os.path.join(args.out_dir, "mean.npy"), mean.astype(np.float32)) |
| np.save(os.path.join(args.out_dir, "std.npy"), std.astype(np.float32)) |
| print("mean/std saved; std range", float(std.min()), float(std.max())) |
|
|
| L = np.array(lengths) |
| print(f"{split}: len min {L.min()} median {int(np.median(L))} p95 " |
| f"{int(np.percentile(L, 95))} max {L.max()}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|