import argparse import csv import shutil import zipfile from pathlib import Path def parse_args(): parser = argparse.ArgumentParser( description="Package extracted frame folders into per-video ZIP archives for CVAT." ) parser.add_argument("--frames-root", required=True, help="Root with extracted frames, e.g. D:\\MAI_CVAT_frames_s15\\frames_all") parser.add_argument("--output-root", required=True, help="Where ZIP archives and manifests will be written") parser.add_argument("--group-filter", action="append", default=[], help="Optional substring filter for group names") parser.add_argument("--max-files-per-zip", type=int, default=0, help="Optional limit; skip items above this count when > 0") parser.add_argument("--overwrite", action="store_true") return parser.parse_args() def main(): args = parse_args() frames_root = Path(args.frames_root).expanduser().resolve() output_root = Path(args.output_root).expanduser().resolve() if not frames_root.exists(): raise SystemExit(f"Frames root not found: {frames_root}") filters = [s.strip().lower() for s in args.group_filter if s.strip()] zip_root = output_root / "zips" zip_root.mkdir(parents=True, exist_ok=True) rows = [] video_dirs = list(find_video_dirs(frames_root)) for video_dir in video_dirs: rel_parts = video_dir.relative_to(frames_root).parts if len(rel_parts) < 5: continue split, role, group_name, video_name, interval_name = rel_parts[:5] if filters and not any(token in group_name.lower() for token in filters): continue files = sorted( [ p for p in video_dir.iterdir() if p.is_file() and p.suffix.lower() in {".jpg", ".jpeg", ".png"} ] ) file_count = len(files) if file_count == 0: continue if args.max_files_per_zip > 0 and file_count > args.max_files_per_zip: continue safe_group = sanitize(group_name) safe_video = sanitize(video_name) safe_interval = sanitize(interval_name) zip_name = f"{safe_group}__{safe_video}__{safe_interval}.zip" zip_path = zip_root / zip_name if zip_path.exists(): if args.overwrite: zip_path.unlink() else: rows.append(build_row(split, role, group_name, video_name, interval_name, file_count, zip_path, video_dir)) continue with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_STORED) as zf: for file_path in files: zf.write(file_path, arcname=file_path.name) rows.append(build_row(split, role, group_name, video_name, interval_name, file_count, zip_path, video_dir)) manifest_path = output_root / "cvat_task_packages.csv" write_csv( manifest_path, rows, [ "split", "role", "group_name", "video_name", "interval_name", "frame_count", "zip_path", "source_dir", ], ) print(f"Wrote {len(rows)} package rows to {manifest_path}") print(f"ZIP archives are in {zip_root}") def find_video_dirs(frames_root: Path): for split_dir in sorted(p for p in frames_root.iterdir() if p.is_dir()): for role_dir in sorted(p for p in split_dir.iterdir() if p.is_dir()): for group_dir in sorted(p for p in role_dir.iterdir() if p.is_dir()): for video_dir in sorted(p for p in group_dir.iterdir() if p.is_dir()): for interval_dir in sorted(p for p in video_dir.iterdir() if p.is_dir()): yield interval_dir def sanitize(value: str): out = [] for ch in value: if ch in '<>:"/\\|?*': out.append("_") else: out.append(ch) return "".join(out).strip().rstrip(".") def build_row(split, role, group_name, video_name, interval_name, file_count, zip_path: Path, source_dir: Path): return { "split": split, "role": role, "group_name": group_name, "video_name": video_name, "interval_name": interval_name, "frame_count": file_count, "zip_path": str(zip_path), "source_dir": str(source_dir), } def write_csv(path: Path, rows, fieldnames): path.parent.mkdir(parents=True, exist_ok=True) with path.open("w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() for row in rows: writer.writerow(row) if __name__ == "__main__": main()