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.

160 lines
4.6 KiB
Python

import argparse
import csv
import os
import shutil
from pathlib import Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Split a flat image folder by source-video prefixes from CSV."
)
parser.add_argument(
"--flat-root",
type=Path,
required=True,
help="Flat folder with extracted images.",
)
parser.add_argument(
"--split-csv",
type=Path,
required=True,
help="CSV with recommended_split and flat_prefix columns.",
)
parser.add_argument(
"--output-root",
type=Path,
required=True,
help="Output folder that will contain train/valid/test.",
)
parser.add_argument(
"--image-ext",
default="jpg",
help="Image extension in the flat folder, default: jpg.",
)
parser.add_argument(
"--mode",
choices=("hardlink", "copy"),
default="hardlink",
help="Write mode for split files. hardlink avoids doubling disk usage.",
)
parser.add_argument(
"--overwrite",
action="store_true",
help="Remove existing output_root before writing.",
)
return parser.parse_args()
def load_split_rows(csv_path: Path) -> list[dict]:
with csv_path.open("r", encoding="utf-8-sig", newline="") as f:
rows = list(csv.DictReader(f))
required = {"recommended_split", "flat_prefix"}
if not rows:
raise ValueError(f"No rows found in {csv_path}")
missing = required - set(rows[0].keys())
if missing:
raise ValueError(f"Missing required CSV columns: {sorted(missing)}")
return rows
def ensure_clean_dir(path: Path, overwrite: bool) -> None:
if path.exists():
if not overwrite:
raise FileExistsError(
f"Output path already exists: {path}. Use --overwrite to replace it."
)
shutil.rmtree(path)
path.mkdir(parents=True, exist_ok=True)
def link_or_copy(src: Path, dst: Path, mode: str) -> str:
if mode == "hardlink":
try:
os.link(src, dst)
return "hardlink"
except OSError:
shutil.copy2(src, dst)
return "copy_fallback"
shutil.copy2(src, dst)
return "copy"
def main() -> None:
args = parse_args()
rows = load_split_rows(args.split_csv)
ext = args.image_ext.lower().lstrip(".")
ensure_clean_dir(args.output_root, args.overwrite)
manifest_path = args.output_root / "_split_manifest.csv"
split_name_map = {
"train": "train",
"val": "valid",
"valid": "valid",
"test": "test",
}
total_written = 0
total_missing = 0
with manifest_path.open("w", encoding="utf-8-sig", newline="") as mf:
writer = csv.DictWriter(
mf,
fieldnames=[
"recommended_split",
"output_split",
"group_name",
"file_name",
"flat_prefix",
"written_count",
"missing",
"write_mode",
],
)
writer.writeheader()
for row in rows:
split_raw = row["recommended_split"].strip().lower()
output_split = split_name_map.get(split_raw)
if output_split is None:
raise ValueError(f"Unsupported split value: {row['recommended_split']}")
prefix = row["flat_prefix"].strip()
pattern = f"{prefix}__f*.{ext}"
matches = sorted(args.flat_root.glob(pattern))
out_dir = args.output_root / output_split
out_dir.mkdir(parents=True, exist_ok=True)
write_mode = ""
for src in matches:
dst = out_dir / src.name
write_mode = link_or_copy(src, dst, args.mode)
missing = 0 if matches else 1
total_missing += missing
total_written += len(matches)
writer.writerow(
{
"recommended_split": row["recommended_split"],
"output_split": output_split,
"group_name": row.get("group_name", ""),
"file_name": row.get("file_name", ""),
"flat_prefix": prefix,
"written_count": len(matches),
"missing": missing,
"write_mode": write_mode,
}
)
print(f"output_root={args.output_root}")
print(f"manifest={manifest_path}")
print(f"total_written={total_written}")
print(f"missing_prefixes={total_missing}")
if __name__ == "__main__":
main()