Spaces:
Sleeping
Sleeping
File size: 6,722 Bytes
78738de | 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 | """Bước 1 P0 (BRIEF 11/08/2026): cắt frame từng cú từ video broadcast.
Đọc ``datasets/bb9_pilot/shots.json`` (Danh chọn tay 10 cú full-table), cắt
frame bằng OpenCV theo [start, end] của mỗi cú, GIỮ TIMESTAMP THẬT của từng
frame (``CAP_PROP_POS_MSEC`` — PTS container, không phải số frame × 1/fps):
backend §6 sau này ăn RoPE theo timestamp thật để chịu VFR YouTube, nên lưu
ngay từ P0 khỏi phải trích lại (BRIEF bối cảnh #7).
Frame ra ``datasets/bb9_pilot/shot_XX/frames/*.jpg`` + ``frames_meta.csv``
(frame_file, frame_idx_video, t_video_s) + ``meta.json`` per shot. Thư mục
``datasets/`` nằm ngoài git (repo public, bản quyền video) — chỉ commit
script + shots.json KHÔNG commit frame.
``shots.json`` (Danh đưa timestamp, format tự quyết P0):
{"video": "pFV7sdBJ42M.mp4", # tuỳ chọn, mặc định dò *.mp4
"shots": [{"id": 1, "start": "1:23:45.5", "end": "1:23:58",
"note": "ghi chu tuy y"}, ...]}
start/end nhận giây (số) hoặc chuỗi "HH:MM:SS(.f)" / "MM:SS(.f)".
Chạy (venv CV — chỉ cần cv2/numpy, nhưng nếp P0 dồn hết vào cv-env):
D:\\Khoa luan\\poolcoach-cv-env\\Scripts\\python.exe \
scripts/broadcast/extract_shots.py
"""
from __future__ import annotations
import argparse
import csv
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2] # poolcoach-rl/
sys.path.insert(0, str(ROOT / "src"))
import cv2 # noqa: E402
PILOT_ROOT = ROOT / "datasets" / "bb9_pilot"
# Cảnh báo VFR: dt giữa 2 frame lệch quá 25% so với 1/fps danh định thì đếm.
DT_JITTER_TOL = 0.25
def parse_ts(v) -> float:
"""Giây (float) hoặc "HH:MM:SS(.f)" / "MM:SS(.f)" -> giây."""
if isinstance(v, (int, float)):
return float(v)
parts = str(v).strip().split(":")
if not 1 <= len(parts) <= 3 or any(p == "" for p in parts):
raise ValueError(f"Timestamp khong hop le: {v!r}")
sec = 0.0
for p in parts:
sec = sec * 60.0 + float(p)
return sec
def extract_shot(cap: cv2.VideoCapture, shot: dict, out_dir: Path,
fps_nominal: float, jpg_quality: int) -> dict:
"""Cắt 1 cú: seek tới start, đọc tới end, lưu jpg + timestamp thật."""
t0, t1 = parse_ts(shot["start"]), parse_ts(shot["end"])
if t1 <= t0:
raise ValueError(f"Shot {shot['id']}: end ({t1}) <= start ({t0}).")
frames_dir = out_dir / "frames"
frames_dir.mkdir(parents=True, exist_ok=True)
cap.set(cv2.CAP_PROP_POS_MSEC, t0 * 1000.0)
rows: list[tuple[str, int, float]] = []
n_jitter = 0
prev_t = None
while True:
ok = cap.grab()
if not ok:
break
t = cap.get(cv2.CAP_PROP_POS_MSEC) / 1000.0
# Seek theo msec có thể rơi về keyframe trước start — bỏ qua tới t0.
if t < t0 - 1e-6:
continue
if t > t1 + 1e-6:
break
ok, frame = cap.retrieve()
if not ok:
break
idx_video = int(cap.get(cv2.CAP_PROP_POS_FRAMES)) - 1
name = f"{len(rows):05d}.jpg"
cv2.imwrite(str(frames_dir / name), frame,
[cv2.IMWRITE_JPEG_QUALITY, jpg_quality])
rows.append((name, idx_video, round(t, 6)))
if prev_t is not None and fps_nominal > 0:
dt_nom = 1.0 / fps_nominal
if abs((t - prev_t) - dt_nom) > DT_JITTER_TOL * dt_nom:
n_jitter += 1
prev_t = t
if not rows:
raise RuntimeError(
f"Shot {shot['id']}: khong doc duoc frame nao trong "
f"[{t0}, {t1}]s — kiem lai timestamp/video.")
with open(out_dir / "frames_meta.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["frame_file", "frame_idx_video", "t_video_s"])
w.writerows(rows)
meta = {
"shot_id": shot["id"],
"note": shot.get("note", ""),
"start_requested_s": t0,
"end_requested_s": t1,
"n_frames": len(rows),
"t_first_s": rows[0][2],
"t_last_s": rows[-1][2],
"fps_nominal": fps_nominal,
"n_dt_jitter": n_jitter,
"extracted_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
}
with open(out_dir / "meta.json", "w", encoding="utf-8") as f:
json.dump(meta, f, indent=2, ensure_ascii=False)
return meta
def main() -> None:
ap = argparse.ArgumentParser(description="Cat frame tung cu theo shots.json (P0)")
ap.add_argument("--root", type=Path, default=PILOT_ROOT)
ap.add_argument("--shots", type=Path, default=None,
help="mac dinh <root>/shots.json")
ap.add_argument("--video", type=Path, default=None,
help="mac dinh: 'video' trong shots.json, roi *.mp4 duy nhat trong root")
ap.add_argument("--only", type=int, nargs="*", default=None,
help="chi cat cac shot id nay")
ap.add_argument("--jpg-quality", type=int, default=95)
args = ap.parse_args()
shots_path = args.shots or args.root / "shots.json"
spec = json.loads(shots_path.read_text(encoding="utf-8"))
shots = spec["shots"]
if args.only:
shots = [s for s in shots if s["id"] in set(args.only)]
if not shots:
sys.exit("[ERROR] Khong co shot nao de cat.")
video = args.video
if video is None and spec.get("video"):
video = args.root / spec["video"]
if video is None:
mp4s = sorted(args.root.glob("*.mp4"))
if len(mp4s) != 1:
sys.exit(f"[ERROR] Can --video: thay {len(mp4s)} file *.mp4 trong {args.root}")
video = mp4s[0]
cap = cv2.VideoCapture(str(video))
if not cap.isOpened():
sys.exit(f"[ERROR] Khong mo duoc video: {video}")
fps_nominal = cap.get(cv2.CAP_PROP_FPS)
print(f"video: {video.name} fps_nominal={fps_nominal:.3f}")
summaries = []
for shot in shots:
out_dir = args.root / f"shot_{shot['id']:02d}"
m = extract_shot(cap, shot, out_dir, fps_nominal, args.jpg_quality)
summaries.append(m)
print(f"shot_{shot['id']:02d}: {m['n_frames']} frames "
f"t=[{m['t_first_s']:.3f}, {m['t_last_s']:.3f}]s "
f"jitter={m['n_dt_jitter']}")
cap.release()
total = sum(m["n_frames"] for m in summaries)
jit = sum(m["n_dt_jitter"] for m in summaries)
print(f"\n[DONE] {len(summaries)} shots, {total} frames, dt-jitter {jit} frame-gaps")
if jit:
print("[WARN] Co dt jitter — video VFR that, timestamp da luu la PTS container.")
if __name__ == "__main__":
main()
|