fishxinyu/CameraClone-Dataset / processing /build_camera_dataset.py
fishxinyu's picture
download
raw
28.8 kB
#!/usr/bin/env python3
"""
Build LTX-2/Camera-Dataset from the Kwai CameraClone-Dataset.
Produces reference/target video pairs that share a camera trajectory but show
different scenes, in the flat `<name>.mp4` / `<name>_camera.mp4` + dataset.json
layout used by LTX-2/Depth-Dataset, Pose-Dataset and Inpainting-Dataset.
Pairing comes from the source dataset's own structure: within one `traj_<group>_<cam>`
directory every video was rendered with the same camera trajectory at a different
3D location, so (video_path, ref_video_path) from a CSV row is a valid pair by
construction. See README.md for the measurements behind the design.
Stages (all resumable; artifacts cached in this directory):
index parse the 1.3 GB CSV into a compact path index
select caption-cluster the candidates and pick 2400 scene-diverse pairs
measure estimate camera motion for the selected targets
trim cap near-static trajectories, cut to the final 2000
encode normalise both videos of every pair to 1280x720 / 73 frames / h264
emit write dataset.json and the file -> original path mapping CSV
verify check counts, formats, pair correctness and diversity
"""
import argparse
import collections
import csv
import json
import os
import random
import re
import subprocess
import sys
import time
import numpy as np
import pandas as pd
SRC_ROOT = "/mnt/dataset/xinyuy/datasets/CameraClone-Dataset"
SRC_CSV = f"{SRC_ROOT}/CamCloneDataset.csv"
WORK = f"{SRC_ROOT}/processing"
LTX = "/mnt/dataset/xinyuy/datasets/LTX-2"
# The val split excludes every trajectory group and every scene location used by train,
# so the two sets share no camera trajectory and no 3D location — zero overlap.
SPLITS = {
"train": dict(out_dir=f"{LTX}/Camera-Dataset", sfx="",
n_final=2000, n_candidates=2400, exclude=None),
"val": dict(out_dir=f"{LTX}/Camera-Dataset-Val", sfx="_val",
n_final=50, n_candidates=70, exclude="train"),
}
STATIC_CAP = 0.08 # max share of near-static trajectories in the final set
N_CLUSTERS = 60 # caption clusters -> scene diversity
SEED = 0
# Native mode remuxes the source video stream untouched: HEVC / yuvj420p / 15 fps /
# 77 frames at the batch's own resolution. Set NATIVE = False to re-encode to
# OUT_W x OUT_H at OUT_FRAMES (h264) instead.
NATIVE = True
NATIVE_FRAMES = 77
NATIVE_CODEC, NATIVE_PIX_FMT = "hevc", "yuvj420p"
NATIVE_DIMS = {"0316": (1344, 768), "0317": (1344, 768)}
NATIVE_DIMS_DEFAULT = (1008, 576)
OUT_W, OUT_H, OUT_FRAMES = 1280, 720, 73 # 73 = 8*9+1, LTX latent-safe under the native 77
ENCODE_JOBS, ENCODE_THREADS = 32, 3
MEASURE_WORKERS = 64
# motion thresholds, in units of the 320px-wide analysis frame
T_PAN, T_TILT, T_ROLL, T_ZOOM = 30.0, 30.0, 8.0, 15.0
T_LINEAR, T_ARC = 0.85, 0.40
PATH_RE = re.compile(r"^\./data/(\d+)/traj_(\d+)_(\d+)/scene(\d+)_(\d+)\.mp4$")
# index and candidates (with captions + clusters) are split-independent and shared.
INDEX_PQ = f"{WORK}/index.parquet"
CAND_PQ = f"{WORK}/candidates.parquet"
# Per-split; bound by configure().
SPLIT = OUT_DIR = SEL_CSV = MOTION_CSV = FINAL_CSV = MAPPING_CSV = None
N_FINAL = N_CANDIDATES = EXCLUDE_CSV = None
def configure(split):
"""Bind the per-split output paths and sizes."""
global SPLIT, OUT_DIR, SEL_CSV, MOTION_CSV, FINAL_CSV, MAPPING_CSV
global N_FINAL, N_CANDIDATES, EXCLUDE_CSV
cfg = SPLITS[split]
SPLIT, OUT_DIR = split, cfg["out_dir"]
N_FINAL, N_CANDIDATES = cfg["n_final"], cfg["n_candidates"]
sfx = cfg["sfx"]
SEL_CSV = f"{WORK}/selected_candidates{sfx}.csv"
MOTION_CSV = f"{WORK}/motion_labels{sfx}.csv"
FINAL_CSV = f"{WORK}/final_pairs{sfx}.csv"
MAPPING_CSV = f"{WORK}/camera_dataset_mapping{sfx}.csv"
EXCLUDE_CSV = (f"{WORK}/final_pairs{SPLITS[cfg['exclude']]['sfx']}.csv"
if cfg["exclude"] else None)
def log(msg):
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
def native_dims(date):
"""Source resolution of a render batch. Both videos of a pair come from the same
trajectory dir, hence the same batch, so a pair is always internally consistent."""
return NATIVE_DIMS.get(str(date), NATIVE_DIMS_DEFAULT)
# --------------------------------------------------------------------------- index
def stage_index():
"""Stream the source CSV into a compact index. Captions are left on disk."""
log(f"indexing {SRC_CSV}")
rows = []
bad = 0
with open(SRC_CSV) as f:
f.readline() # header
for i, line in enumerate(f):
# video_path and ref_video_path are unquoted and contain no commas;
# the file has no embedded newlines (verified: lines == rows + 1).
a, b, _ = line.split(",", 2)
ma, mb = PATH_RE.match(a), PATH_RE.match(b)
if not ma or not mb:
bad += 1
continue
date, grp, cam, tsid, _ = ma.groups()
rows.append((i, date, int(grp), int(cam), int(tsid), int(mb.group(4))))
df = pd.DataFrame(rows, columns=["row", "date", "grp", "cam", "tsid", "rsid"])
df.to_parquet(INDEX_PQ, index=False)
log(f"indexed {len(df):,} rows ({bad} unparseable) -> {INDEX_PQ}")
log(f" {df.groupby(['date', 'grp']).ngroups:,} trajectory groups, "
f"{df.groupby(['date', 'tsid']).ngroups:,} target locations")
# --------------------------------------------------------------------------- select
def _fetch_captions(rownos):
"""Second pass over the CSV, pulling captions only for the rows we need."""
want = set(rownos)
out = {}
csv.field_size_limit(10 ** 7)
with open(SRC_CSV, newline="") as f:
for i, rec in enumerate(csv.reader(f)):
if i == 0:
cap_col = rec.index("caption")
continue
j = i - 1
if j in want:
out[j] = rec[cap_col]
if len(out) == len(want):
break
return out
def stage_select():
rng = random.Random(SEED)
df = pd.read_parquet(INDEX_PQ)
log(f"loaded index: {len(df):,} rows")
if os.path.exists(CAND_PQ):
cand = pd.read_parquet(CAND_PQ)
log(f"reusing cached candidate pool: {len(cand):,} candidates with captions + clusters")
else:
# One candidate per (group, target location): each group's 4 locations are
# different scenes, so this gives up to 4 caption choices per trajectory group.
log("building candidate pool (one row per group x target location)")
cand = (df.sample(frac=1.0, random_state=SEED)
.drop_duplicates(subset=["date", "grp", "tsid"], keep="first")
.reset_index(drop=True))
log(f" {len(cand):,} candidates over {cand.groupby(['date', 'grp']).ngroups:,} groups")
log("fetching captions for candidates (second CSV pass)")
caps = _fetch_captions(cand["row"].tolist())
cand["caption"] = cand["row"].map(caps)
missing = cand["caption"].isna().sum()
if missing:
log(f" dropping {missing} candidates with no caption")
cand = cand[cand["caption"].notna()].reset_index(drop=True)
log(f"clustering captions into {N_CLUSTERS} scene clusters (TF-IDF + MiniBatchKMeans)")
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import MiniBatchKMeans
vec = TfidfVectorizer(stop_words="english", ngram_range=(1, 2),
max_features=50000, min_df=5, max_df=0.5)
X = vec.fit_transform(cand["caption"])
km = MiniBatchKMeans(n_clusters=N_CLUSTERS, random_state=SEED,
n_init=5, batch_size=4096)
cand["cluster"] = km.fit_predict(X)
cand.to_parquet(CAND_PQ, index=False)
log(f" clusters sized {cand['cluster'].value_counts().min()}"
f"..{cand['cluster'].value_counts().max()}")
# Proportional per-date quota, so no render batch dominates.
groups_per_date = df.groupby("date")["grp"].nunique()
total_groups = groups_per_date.sum()
quota = {d: int(round(N_CANDIDATES * n / total_groups))
for d, n in groups_per_date.items()}
log(f" per-date quota: {quota}")
# Round-robin over caption clusters, always drawing from the least-used one.
pools = collections.defaultdict(list)
for rec in cand.sample(frac=1.0, random_state=SEED).itertuples(index=False):
pools[rec.cluster].append(rec)
cursor = collections.defaultdict(int)
taken = collections.Counter()
used_groups, used_tloc, used_rloc = set(), set(), set()
date_count = collections.Counter()
selected = []
# Pre-seeding the "used" sets with the other split's picks is what makes the splits
# disjoint: no shared trajectory group, no shared target or reference location.
if EXCLUDE_CSV:
if not os.path.exists(EXCLUDE_CSV):
sys.exit(f"split '{SPLIT}' excludes {EXCLUDE_CSV}, which does not exist yet")
prev = pd.read_csv(EXCLUDE_CSV, dtype={"date": str})
for r in prev.itertuples(index=False):
used_groups.add((r.date, r.grp))
used_tloc.add((r.date, r.tsid))
used_rloc.add((r.date, r.rsid))
log(f" holding out {len(used_groups):,} trajectory groups and "
f"{len(used_tloc | used_rloc):,} locations used by {os.path.basename(EXCLUDE_CSV)}")
while len(selected) < N_CANDIDATES:
progressed = False
for cl in sorted(pools, key=lambda c: (taken[c], rng.random())):
pool, i = pools[cl], cursor[cl]
while i < len(pool):
r = pool[i]
i += 1
if ((r.date, r.grp) in used_groups
or (r.date, r.tsid) in used_tloc
or (r.date, r.rsid) in used_rloc
or date_count[r.date] >= quota.get(r.date, 0)):
continue
used_groups.add((r.date, r.grp))
used_tloc.add((r.date, r.tsid))
used_rloc.add((r.date, r.rsid))
date_count[r.date] += 1
taken[cl] += 1
selected.append(r)
progressed = True
break
cursor[cl] = i
if progressed:
break
if not progressed:
log(f" pool exhausted at {len(selected)} candidates")
break
sel = pd.DataFrame(selected)
sel["target_src"] = sel.apply(
lambda r: f"data/{r.date}/traj_{r.grp}_{r.cam:02d}/scene{r.tsid}_{r.cam:02d}.mp4", axis=1)
sel["ref_src"] = sel.apply(
lambda r: f"data/{r.date}/traj_{r.grp}_{r.cam:02d}/scene{r.rsid}_{r.cam:02d}.mp4", axis=1)
sel["name"] = sel.apply(
lambda r: f"{r.date}_g{r.grp}_c{r.cam:02d}_s{r.tsid}", axis=1)
sel.to_csv(SEL_CSV, index=False)
log(f"selected {len(sel):,} candidates -> {SEL_CSV}")
log(f" dates: {dict(sel['date'].value_counts().sort_index())}")
log(f" clusters used: {sel['cluster'].nunique()}/{N_CLUSTERS}, "
f"max per cluster {sel['cluster'].value_counts().max()}")
# --------------------------------------------------------------------------- measure
def measure_trajectory(path, step=4, width=320):
"""Cumulative 2D camera motion via ORB + RANSAC similarity fit.
RANSAC rejects the animated character, so the fit tracks the background —
i.e. the camera. Returns [pan, tilt, roll_deg, 100*log_scale, straightness],
where straightness = |net displacement| / path length separates linear moves
(~1.0) from arcs and splines.
"""
import cv2
cv2.setNumThreads(1)
cap = cv2.VideoCapture(path)
frames, i = [], 0
while True:
ok, fr = cap.read()
if not ok:
break
if i % step == 0:
h0, w0 = fr.shape[:2]
fr = cv2.resize(fr, (width, int(width * h0 / w0)))
frames.append(cv2.cvtColor(fr, cv2.COLOR_BGR2GRAY))
i += 1
cap.release()
if len(frames) < 3:
return None
orb = cv2.ORB_create(1500)
kd = [orb.detectAndCompute(f, None) for f in frames]
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
pan = tilt = roll = lsc = 0.0
path_len = 0.0
n = 0
for (k1, d1), (k2, d2) in zip(kd[:-1], kd[1:]):
if d1 is None or d2 is None or len(k1) < 8 or len(k2) < 8:
continue
m = bf.match(d1, d2)
if len(m) < 8:
continue
src = np.float32([k1[x.queryIdx].pt for x in m]).reshape(-1, 1, 2)
dst = np.float32([k2[x.trainIdx].pt for x in m]).reshape(-1, 1, 2)
M, _ = cv2.estimateAffinePartial2D(src, dst, method=cv2.RANSAC,
ransacReprojThreshold=2.0)
if M is None:
continue
a, b = M[0, 0], M[1, 0]
dx, dy = M[0, 2], M[1, 2]
pan += dx
tilt += dy
roll += np.degrees(np.arctan2(b, a))
lsc += np.log(max(np.hypot(a, b), 1e-6))
path_len += np.hypot(dx, dy)
n += 1
if n == 0:
return None
net = np.hypot(pan, tilt)
straight = net / path_len if path_len > 1e-6 else 0.0
return [pan, tilt, roll, lsc * 100, straight]
def _measure_one(args):
name, path = args
try:
return name, measure_trajectory(path)
except Exception:
return name, None
def classify(pan, tilt, roll, zoom, straight):
comps = []
if abs(pan) > T_PAN:
comps.append("pan_right" if pan < 0 else "pan_left")
if abs(tilt) > T_TILT:
comps.append("tilt_down" if tilt < 0 else "tilt_up")
if abs(roll) > T_ROLL:
comps.append("roll_cw" if roll < 0 else "roll_ccw")
if abs(zoom) > T_ZOOM:
comps.append("zoom_out" if zoom < 0 else "zoom_in")
n_axes = len(comps)
if not comps:
comps = ["static"]
shape = "linear" if straight >= T_LINEAR else ("arc" if straight >= T_ARC else "complex")
return "+".join(sorted(comps)), n_axes, shape
def stage_measure():
from concurrent.futures import ProcessPoolExecutor
sel = pd.read_csv(SEL_CSV, dtype={"date": str})
done = {}
if os.path.exists(MOTION_CSV):
prev = pd.read_csv(MOTION_CSV)
done = set(prev["name"])
log(f"resuming: {len(done)} already measured")
todo = [(r.name_, f"{SRC_ROOT}/{r.target_src}")
for r in sel.rename(columns={"name": "name_"}).itertuples(index=False)
if r.name_ not in done]
log(f"measuring camera motion for {len(todo):,} targets on {MEASURE_WORKERS} workers")
recs = []
t0 = time.time()
with ProcessPoolExecutor(MEASURE_WORKERS) as ex:
for k, (name, sig) in enumerate(ex.map(_measure_one, todo, chunksize=4), 1):
if sig is not None:
comps, n_axes, shape = classify(*sig)
recs.append(dict(name=name, pan=sig[0], tilt=sig[1], roll=sig[2],
zoom=sig[3], straightness=sig[4],
motion_components=comps, n_axes=n_axes, path_shape=shape))
if k % 400 == 0:
log(f" {k}/{len(todo)} ({time.time() - t0:.0f}s)")
new = pd.DataFrame(recs)
if done:
new = pd.concat([prev, new], ignore_index=True)
new.to_csv(MOTION_CSV, index=False)
log(f"measured {len(new):,} trajectories in {time.time() - t0:.0f}s -> {MOTION_CSV}")
log(f" static share: {(new['motion_components'] == 'static').mean():.1%}")
# --------------------------------------------------------------------------- trim
def stage_trim():
rng = random.Random(SEED)
sel = pd.read_csv(SEL_CSV, dtype={"date": str})
mot = pd.read_csv(MOTION_CSV)
df = sel.merge(mot, on="name", how="inner")
log(f"{len(df):,} candidates with motion labels")
is_static = df["motion_components"] == "static"
statics = df[is_static].sample(frac=1.0, random_state=SEED)
moving = df[~is_static].sample(frac=1.0, random_state=SEED)
log(f" near-static: {len(statics):,} ({len(statics) / len(df):.1%}), moving: {len(moving):,}")
keep_static = min(len(statics), int(N_FINAL * STATIC_CAP))
keep_moving = min(len(moving), N_FINAL - keep_static)
keep_static = min(len(statics), N_FINAL - keep_moving) # backfill if moving is short
final = pd.concat([statics.head(keep_static), moving.head(keep_moving)])
final = final.sample(frac=1.0, random_state=SEED).reset_index(drop=True)
final.to_csv(FINAL_CSV, index=False)
log(f"final set: {len(final):,} pairs "
f"({keep_static} static capped at {STATIC_CAP:.0%}, {keep_moving} moving)")
comps = collections.Counter()
for c in final["motion_components"]:
for part in c.split("+"):
comps[part] += 1
log(" motion components: " + ", ".join(
f"{k} {100 * v / len(final):.1f}%" for k, v in comps.most_common()))
log(f" path shape: " + ", ".join(
f"{k} {100 * v / len(final):.1f}%" for k, v in final['path_shape'].value_counts().items()))
log(f" clusters used: {final['cluster'].nunique()}/{N_CLUSTERS}")
# --------------------------------------------------------------------------- encode
def _encode_one(job):
src, dst = job
if NATIVE:
# Remux only — the video stream is copied bit-for-bit, so this is lossless.
# ffmpeg still parses the source, so a corrupt input fails loudly here.
cmd = ["ffmpeg", "-v", "error", "-y", "-i", src, "-c:v", "copy", "-an", dst]
else:
cmd = ["ffmpeg", "-v", "error", "-y", "-threads", str(ENCODE_THREADS), "-i", src,
"-vf", f"scale={OUT_W}:-2,crop={OUT_W}:{OUT_H}",
"-frames:v", str(OUT_FRAMES),
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
"-pix_fmt", "yuv420p", "-an", dst]
p = subprocess.run(cmd, capture_output=True, text=True)
return dst, p.returncode, p.stderr.strip()[:300]
def stage_encode():
from concurrent.futures import ThreadPoolExecutor
os.makedirs(OUT_DIR, exist_ok=True)
final = pd.read_csv(FINAL_CSV, dtype={"date": str})
jobs = []
for r in final.itertuples(index=False):
jobs.append((f"{SRC_ROOT}/{r.target_src}", f"{OUT_DIR}/{r.name}.mp4"))
jobs.append((f"{SRC_ROOT}/{r.ref_src}", f"{OUT_DIR}/{r.name}_camera.mp4"))
jobs = [(s, d) for s, d in jobs if not os.path.exists(d) or os.path.getsize(d) == 0]
if NATIVE:
log(f"remuxing {len(jobs):,} clips -> native {NATIVE_CODEC}, {NATIVE_FRAMES} frames, "
f"source resolution ({ENCODE_JOBS} jobs)")
else:
log(f"encoding {len(jobs):,} clips -> {OUT_W}x{OUT_H}, {OUT_FRAMES} frames, h264 "
f"({ENCODE_JOBS} jobs x {ENCODE_THREADS} threads)")
fails, t0 = [], time.time()
with ThreadPoolExecutor(ENCODE_JOBS) as ex:
for k, (dst, rc, err) in enumerate(ex.map(_encode_one, jobs), 1):
if rc != 0:
fails.append((dst, err))
if k % 500 == 0:
log(f" {k}/{len(jobs)} ({time.time() - t0:.0f}s)")
log(f"encoded in {time.time() - t0:.0f}s, {len(fails)} failures")
for d, e in fails[:10]:
log(f" FAIL {d}: {e}")
if fails:
sys.exit(1)
# --------------------------------------------------------------------------- emit
def stage_emit():
final = pd.read_csv(FINAL_CSV, dtype={"date": str})
src_dims = {r.name: native_dims(r.date) for r in final.itertuples(index=False)}
entries, rows = [], []
for r in final.itertuples(index=False):
media = f"{r.name}.mp4"
ref = f"{r.name}_camera.mp4"
entries.append({"caption": r.caption, "media_path": media, "reference_path": ref})
w, h = src_dims[r.name]
rows.append(dict(
media_path=media, reference_path=ref,
target_src_path=f"./{r.target_src}", reference_src_path=f"./{r.ref_src}",
date=r.date, traj_group=r.grp, cam_index=r.cam,
target_scene_id=r.tsid, ref_scene_id=r.rsid,
src_width=w, src_height=h,
motion_components=r.motion_components, n_axes=r.n_axes, path_shape=r.path_shape,
pan=round(r.pan, 2), tilt=round(r.tilt, 2), roll=round(r.roll, 2),
zoom=round(r.zoom, 2), straightness=round(r.straightness, 3),
caption_cluster=r.cluster, caption=r.caption))
with open(f"{OUT_DIR}/dataset.json", "w") as f:
json.dump(entries, f, indent=2, ensure_ascii=False)
pd.DataFrame(rows).to_csv(MAPPING_CSV, index=False)
log(f"wrote {OUT_DIR}/dataset.json ({len(entries):,} entries)")
log(f"wrote {MAPPING_CSV} ({len(rows):,} rows)")
# --------------------------------------------------------------------------- verify
def _probe(path):
out = subprocess.run(
["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
"stream=width,height,nb_frames,codec_name,pix_fmt", "-of", "csv=p=0", path],
capture_output=True, text=True).stdout.strip()
return out
def stage_verify():
from concurrent.futures import ThreadPoolExecutor
ok = True
def check(label, cond, detail=""):
nonlocal ok
print(f" [{'PASS' if cond else 'FAIL'}] {label}{(' — ' + detail) if detail else ''}")
if not cond:
ok = False
ds = json.load(open(f"{OUT_DIR}/dataset.json"))
mp = pd.read_csv(MAPPING_CSV, dtype={"date": str})
listing = os.listdir(OUT_DIR)
print("\n1. counts")
check(f"{len(listing)} entries in {os.path.basename(OUT_DIR)}",
len(listing) == 2 * N_FINAL + 1, f"expected {2 * N_FINAL + 1}")
check(f"{len(ds)} dataset.json records", len(ds) == N_FINAL)
check("mapping CSV rows match", len(mp) == N_FINAL)
names = [e["media_path"] for e in ds] + [e["reference_path"] for e in ds]
check("no duplicate filenames", len(set(names)) == len(names))
check("all referenced files exist",
all(os.path.exists(f"{OUT_DIR}/{n}") for n in names))
print("\n2. schema parity with Depth-Dataset")
ref_ds = json.load(open("/mnt/dataset/xinyuy/datasets/LTX-2/Depth-Dataset/dataset.json"))
check("dataset.json keys identical",
set(ds[0].keys()) == set(ref_ds[0].keys()), str(sorted(ds[0].keys())))
check("all captions non-empty",
all(isinstance(e["caption"], str) and e["caption"].strip() for e in ds))
print(f"\n3. video format (ffprobe, all {len(names)} clips)")
with ThreadPoolExecutor(48) as ex:
probes = dict(zip(names, ex.map(lambda n: _probe(f"{OUT_DIR}/{n}"), names)))
def expected(name):
# ffprobe csv order is codec_name,width,height,pix_fmt,nb_frames
if NATIVE:
w, h = native_dims(name.split("_")[0])
return f"{NATIVE_CODEC},{w},{h},{NATIVE_PIX_FMT},{NATIVE_FRAMES}"
return f"h264,{OUT_W},{OUT_H},yuv420p,{OUT_FRAMES}"
label = (f"all clips are native {NATIVE_CODEC}/{NATIVE_PIX_FMT}/{NATIVE_FRAMES}f at their "
f"batch resolution" if NATIVE else f"all clips are {expected(names[0])}")
bad = [n for n, p in probes.items() if p != expected(n)]
check(label, not bad,
f"{len(bad)} mismatched, e.g. {bad[:3]} -> {[probes[b] for b in bad[:3]]}" if bad else "")
if NATIVE:
dims = collections.Counter(",".join(p.split(",")[1:3]) for p in probes.values())
print(" resolutions: " + ", ".join(f"{k.replace(',', 'x')} {v}" for k, v in dims.most_common()))
desync = [e["media_path"] for e in ds
if probes[e["media_path"]].split(",")[-1] != probes[e["reference_path"]].split(",")[-1]]
check("target/reference frame counts match (motion sync)", not desync,
f"{len(desync)} desynced" if desync else "")
print("\n4. pair correctness")
same_traj = mp.apply(lambda r: os.path.dirname(r.target_src_path)
== os.path.dirname(r.reference_src_path), axis=1)
check("every pair shares a trajectory dir (same camera motion)", same_traj.all(),
f"{(~same_traj).sum()} violations")
diff_scene = (mp["target_scene_id"] != mp["ref_scene_id"])
check("every pair uses different scenes", diff_scene.all(),
f"{(~diff_scene).sum()} violations")
check("source files still exist",
all(os.path.exists(f"{SRC_ROOT}/{p[2:]}") for p in mp["target_src_path"].head(200)))
print("\n5. diversity")
check(f"{mp.groupby(['date', 'traj_group']).ngroups} distinct trajectory groups",
mp.groupby(["date", "traj_group"]).ngroups == N_FINAL)
check(f"{mp.groupby(['date', 'target_scene_id']).ngroups} distinct target locations",
mp.groupby(["date", "target_scene_id"]).ngroups == N_FINAL)
other = [s for s in SPLITS if s != SPLIT]
for o in other:
om = f"{WORK}/camera_dataset_mapping{SPLITS[o]['sfx']}.csv"
if not os.path.exists(om):
continue
om = pd.read_csv(om, dtype={"date": str})
g_self = set(zip(mp["date"], mp["traj_group"]))
g_other = set(zip(om["date"], om["traj_group"]))
loc_self = set(zip(mp["date"], mp["target_scene_id"])) | set(zip(mp["date"], mp["ref_scene_id"]))
loc_other = set(zip(om["date"], om["target_scene_id"])) | set(zip(om["date"], om["ref_scene_id"]))
f_self = set(mp["media_path"]) | set(mp["reference_path"])
f_other = set(om["media_path"]) | set(om["reference_path"])
check(f"no trajectory group shared with '{o}' split", not (g_self & g_other),
f"{len(g_self & g_other)} shared")
check(f"no scene location shared with '{o}' split", not (loc_self & loc_other),
f"{len(loc_self & loc_other)} shared")
check(f"no video shared with '{o}' split", not (f_self & f_other),
f"{len(f_self & f_other)} shared")
comps = collections.Counter()
for c in mp["motion_components"]:
for part in c.split("+"):
comps[part] += 1
print(" motion components: " + ", ".join(
f"{k} {100 * v / len(mp):.1f}%" for k, v in comps.most_common()))
print(" path shape: " + ", ".join(
f"{k} {100 * v / len(mp):.1f}%" for k, v in mp["path_shape"].value_counts().items()))
print(" date batches: " + ", ".join(
f"{k} {v}" for k, v in sorted(mp["date"].value_counts().items())))
print(f" caption clusters: {mp['caption_cluster'].nunique()}/{N_CLUSTERS} used, "
f"largest {mp['caption_cluster'].value_counts().max()} "
f"({100 * mp['caption_cluster'].value_counts().max() / len(mp):.1f}%)")
print("\n6. trajectory-match spot check (30 random pairs re-measured)")
rng = random.Random(SEED)
sample = rng.sample(range(len(mp)), 30)
agree = 0
checked = 0
for i in sample:
r = mp.iloc[i]
a = measure_trajectory(f"{OUT_DIR}/{r.media_path}")
b = measure_trajectory(f"{OUT_DIR}/{r.reference_path}")
if a is None or b is None:
continue
checked += 1
dom = int(np.argmax(np.abs(a[:4])))
if abs(a[dom]) < 5 or np.sign(a[dom]) == np.sign(b[dom]):
agree += 1
check(f"dominant-axis sign agreement {agree}/{checked}", checked and agree / checked >= 0.8)
total = sum(os.path.getsize(f"{OUT_DIR}/{n}") for n in names)
print(f"\n dataset size: {total / 1e9:.1f} GB")
print(f"\n{'ALL CHECKS PASSED' if ok else 'SOME CHECKS FAILED'}")
return 0 if ok else 1
# --------------------------------------------------------------------------- main
STAGES = {
"index": stage_index, "select": stage_select, "measure": stage_measure,
"trim": stage_trim, "encode": stage_encode, "emit": stage_emit, "verify": stage_verify,
}
if __name__ == "__main__":
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--stage", default="all",
choices=list(STAGES) + ["all"], help="stage to run (default: all)")
ap.add_argument("--split", default="train", choices=list(SPLITS),
help="which dataset to build (default: train). 'val' holds out "
"everything 'train' used, so build train first.")
args = ap.parse_args()
os.makedirs(WORK, exist_ok=True)
configure(args.split)
random.seed(SEED)
np.random.seed(SEED)
log(f"split={args.split} -> {OUT_DIR} ({N_FINAL} pairs)")
order = list(STAGES) if args.stage == "all" else [args.stage]
if args.split != "train" and "index" in order:
order.remove("index") # the index is shared and split-independent
rc = 0
for s in order:
log(f"===== stage: {s} =====")
rc = STAGES[s]() or 0
sys.exit(rc)

Xet Storage Details

Size:
28.8 kB
·
Xet hash:
043ede35d2e96cc5cb7f2d7c08c6e91c96e0425c0886c6b5d633ce211c7b5d72

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.