You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

136 lines
4.6 KiB
Python

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()