Files
hao-ai-lab-FastVideo/data_pipeline/segment_tracks.py
T
shaoxiongduan 92cc893cfc trackwan: 14B teacher pipeline, layer-wise track LR, and overfit tooling
Training:
- finetune.py: optional track/base param groups (WANTRACK_TRACK_GROUP) so the
  track pathway can take a different LR (WANTRACK_TRACK_LR_MULT); patch_embedding
  gradient masked to the track slot [:, 36:]. Raises if nothing matches.
- track_warmup.py callback: hold the pretrained DiT at lr 0 while the track
  pathway warms up, then ramp it back in.

14B configs + held-alloc launchers:
- finetune_wantrack_{synth_sparse_fixed,synth_sparse_random,openvid_sparse_merged_bias,
  synth_stage2}_14b + openvid_14b_{warmup,lwlr} + 1.3B stage2 configs.
- run_wan14b_held.sh (held alloc + auto-restart + rank cleanup, per-stage WANTRACK
  env), acquire_nodes.sh, chain_stepAB.sh, run_stepB_seed.sh, run_stepC_merge.sh,
  watch_gate.sh, run_synth_stage2_slurm.sh.

Init / diagnostics:
- convert_trackwan_init_v2.py: --pe-src to lift patch_embedding[:, 36:] and
  --track-src to lift track_encoder together (co-adapted merge); sharded-source support.
- check_gate_growth.py, gradient_flow_analysis.py, gradient_flow_summarize.py.

Data pipeline: segment_tracks.py sharding; decode/segment/tracks slurm scripts;
precompute_null_text, two_track_val, cfg_ablation, lwlr_experiment.

Inference: app_multi_trace.py updates.
2026-07-23 03:52:56 +00:00

167 lines
8.0 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Stage 0d: segment frame-0 (SAM2.1-b+ by default; --model to switch) and label each
CoTracker grid point by object.
Adds an ``object_ids`` array ([N] int, -1 = background/none) to each tracks ``.npz``,
so the trainer's object-coverage sampling can guarantee >=1 track per object. A grid
point is assigned the SMALLEST mask that contains it (most specific object).
Run on a GPU node (FastSAM is light). Idempotent (skips npz that already have object_ids)::
srun --jobid=<job> --overlap --ntasks=1 env CUDA_VISIBLE_DEVICES=0 PYTHONPATH=$PWD \
.venv/bin/python data_pipeline/segment_tracks.py --data-dir <dataset root>
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import numpy as np
def read_frame0(path: str) -> np.ndarray:
try:
from decord import VideoReader, cpu
return VideoReader(path, ctx=cpu(0))[0].asnumpy() # HxWx3 uint8
except Exception: # noqa: BLE001
import av
c = av.open(path)
for f in c.decode(video=0):
return f.to_ndarray(format="rgb24")
raise RuntimeError(f"could not read {path}")
def object_ids_for_points(masks: np.ndarray, pts_xy: np.ndarray, H: int, W: int) -> np.ndarray:
"""masks [M,H,W] bool, pts_xy [N,2] px -> object id per point (-1 none), smallest mask wins."""
N = pts_xy.shape[0]
oid = np.full(N, -1, np.int64)
if masks.size == 0:
return oid
areas = masks.reshape(masks.shape[0], -1).sum(1) # [M]
order = np.argsort(areas) # smallest first -> assign, larger won't overwrite
xi = np.clip(pts_xy[:, 0].round().astype(int), 0, W - 1)
yi = np.clip(pts_xy[:, 1].round().astype(int), 0, H - 1)
assigned = np.zeros(N, bool)
for m in order:
inside = masks[m][yi, xi] & (~assigned)
oid[inside] = int(m)
assigned |= inside
return oid
def lowrank_track_weights(tracks: np.ndarray, rank: int = 3, pct: float = 97.0) -> np.ndarray:
"""Per-point sampling weight in [0,1] = percentile-normalized low-rank motion residual.
Stack per-point displacement [N, 2T], subtract the mean trajectory + top-`rank` shared
SVD modes (camera / dominant scene motion), and take the residual norm. A point is heavy
iff it moves *uniquely* relative to all other points (independent object motion), not just
a lot -- so on egocentric/moving-camera clips the head-motion background is down-weighted.
"""
T, N, _ = tracks.shape
D = (tracks - tracks[0:1]).transpose(1, 0, 2).reshape(N, 2 * T).astype(np.float64)
Dc = D - D.mean(0, keepdims=True)
U, S, Vt = np.linalg.svd(Dc, full_matrices=False)
r = int(min(rank, S.shape[0]))
resid = np.sqrt(((Dc - (U[:, :r] * S[:r]) @ Vt[:r])**2).sum(1))
lo, hi = float(resid.min()), float(np.percentile(resid, pct))
w = np.zeros_like(resid) if hi <= lo else np.clip((resid - lo) / (hi - lo), 0.0, 1.0)
return w.astype(np.float32)
def main() -> None:
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--data-dir", type=Path, required=True)
p.add_argument("--videos-subdir", type=str, default="videos")
p.add_argument("--tracks-subdir", type=str, default="tracks")
p.add_argument("--manifest", type=str, default="videos2caption.json")
p.add_argument("--model", type=str, default="sam2.1_b.pt",
help="ultralytics weight; FastSAM-*.pt use the FastSAM loader, everything else "
"(sam2.1_b.pt, sam2.1_l.pt, sam_b.pt, mobile_sam.pt, ...) use the SAM loader")
p.add_argument("--weights-dir", type=str, default="/mnt/lustre/vlm-s4duan/models/seg",
help="dir holding cached weights; falls back to auto-download if missing")
p.add_argument("--device", type=str, default="cuda")
p.add_argument("--imgsz", type=int, default=1024)
p.add_argument("--conf", type=float, default=0.4)
p.add_argument("--iou", type=float, default=0.9)
p.add_argument("--min-area-frac",
type=float,
default=0.0,
help="drop masks smaller than this fraction of the frame (fights over-segmentation)")
p.add_argument("--max-masks", type=int, default=0, help="keep only the N largest masks (0 = keep all)")
p.add_argument("--limit", type=int, default=None)
p.add_argument("--shard", type=int, default=0, help="this shard index (0-based)")
p.add_argument("--num-shards", type=int, default=1, help="total shards for parallel processing")
args = p.parse_args()
# FastSAM-*.pt -> FastSAM loader; everything else (SAM/SAM2/SAM2.1/MobileSAM) -> SAM loader.
# Both expose the same no-prompt "everything" call used below.
from ultralytics import FastSAM, SAM
wp = Path(args.weights_dir) / args.model
weight = str(wp) if wp.exists() else args.model
model = (FastSAM if Path(args.model).name.lower().startswith("fastsam") else SAM)(weight)
manifest_path = args.data_dir / args.manifest
items = json.loads(manifest_path.read_text()) if manifest_path.exists() else []
if args.limit:
items = items[:args.limit]
if args.num_shards > 1:
items = items[args.shard::args.num_shards]
print(f"[seg] shard {args.shard}/{args.num_shards}: processing {len(items)} items", flush=True)
n_ok = 0
for k, item in enumerate(items, 1):
vpath = args.data_dir / args.videos_subdir / item["path"]
npz_path = Path(item.get("points_path") or (args.data_dir / args.tracks_subdir / f"{vpath.stem}.npz"))
if not npz_path.exists():
print(f"[seg] [{k}/{len(items)}] {vpath.name}: no npz, skip", flush=True)
continue
d = dict(np.load(npz_path))
if "object_ids" in d and "track_weights" in d:
n_ok += 1
continue
frame0 = read_frame0(str(vpath))
H, W = frame0.shape[0], frame0.shape[1]
res = model(frame0,
device=args.device,
retina_masks=True,
imgsz=args.imgsz,
conf=args.conf,
iou=args.iou,
verbose=False)
masks = np.zeros((0, H, W), bool)
if res and res[0].masks is not None:
masks = res[0].masks.data.cpu().numpy().astype(bool) # [M,h,w]
if masks.shape[1:] != (H, W): # resize masks to frame res if needed
import cv2
masks = np.stack([cv2.resize(m.astype(np.uint8), (W, H),
interpolation=cv2.INTER_NEAREST).astype(bool) for m in masks]) \
if masks.shape[0] else np.zeros((0, H, W), bool)
# drop tiny masks / cap count to fight FastSAM over-segmentation
if masks.shape[0] and (args.min_area_frac > 0 or args.max_masks):
areas = masks.reshape(masks.shape[0], -1).sum(1).astype(np.float64)
if args.min_area_frac > 0:
keep = (areas / float(H * W)) >= args.min_area_frac
masks, areas = masks[keep], areas[keep]
if args.max_masks and masks.shape[0] > args.max_masks:
masks = masks[np.argsort(-areas)[:args.max_masks]]
tracks = d["tracks"].astype(np.float32) # [T,N,2] px (orig res)
oid = object_ids_for_points(masks, tracks[0], H, W) # frame-0 positions
d["object_ids"] = oid.astype(np.int64)
d["n_objects"] = np.int64(masks.shape[0])
d["track_weights"] = lowrank_track_weights(tracks) # [N] low-rank informativeness in [0,1]
tmp = npz_path.with_suffix(".tmp.npz")
np.savez(tmp, **d)
tmp.replace(npz_path)
n_ok += 1
cov = int((oid >= 0).sum())
print(
f"[seg] [{k}/{len(items)}] {vpath.name}: {masks.shape[0]} objs, "
f"{cov}/{oid.shape[0]} grid pts labeled, "
f"w[mean={d['track_weights'].mean():.3f} >0.5={(d['track_weights'] > 0.5).mean():.2f}]",
flush=True)
print(f"[seg] done; {n_ok}/{len(items)} npz have object_ids", flush=True)
if __name__ == "__main__":
main()