#!/usr/bin/env python3 """Migrate World-UAV-aug from flat prefix layout to directory-based layout. Old layout (prefix-based): World-UAV-aug/Rot/scene/DB/img/crop_12_4_depth.png World-UAV-aug/Rot/scene/DB/img/crop_12_4_edge.png World-UAV-aug/Rot/scene/DB/img/crop_12_4_segm.png World-UAV-aug/Rot/scene/DB/img/crop_12_4_chm.png World-UAV-aug/Rot/scene/DB/img/crop_12_4_depth.npy ... New layout (directory-based): World-UAV-aug/depth/Rot/scene/DB/img/crop_12_4.png World-UAV-aug/edge/Rot/scene/DB/img/crop_12_4.png World-UAV-aug/segm/Rot/scene/DB/img/crop_12_4.png World-UAV-aug/chm/Rot/scene/DB/img/crop_12_4.png World-UAV-aug/npy/depth/Rot/scene/DB/img/crop_12_4.npy ... Usage: python scripts/migrate_layout.py /mnt/data1tb/cvgl_datasets/World-UAV-aug python scripts/migrate_layout.py /mnt/data1tb/cvgl_datasets/World-UAV-aug --dry-run """ from __future__ import annotations import argparse import os import shutil from pathlib import Path # Suffix → (modality, extension) _SUFFIX_MAP = { "_depth.png": ("depth", ".png"), "_depth.npy": ("depth", ".npy"), "_edge.png": ("edge", ".png"), "_edge.npy": ("edge", ".npy"), "_segm.png": ("segm", ".png"), "_segm.npy": ("segm", ".npy"), "_chm.png": ("chm", ".png"), "_chm.npy": ("chm", ".npy"), } # Top-level dirs to skip (they belong to the new layout). _SKIP_DIRS = {"depth", "edge", "segm", "chm", "npy", "safetensors"} def find_old_files(root: Path) -> list[tuple[Path, str, str, str]]: """Find all files with old prefix-based naming. Returns list of (old_path, rel_parent, stem, suffix_key). """ results = [] for p in sorted(root.rglob("*")): if not p.is_file(): continue # Skip files already in new-layout dirs. try: rel = p.relative_to(root) except ValueError: continue if rel.parts and rel.parts[0] in _SKIP_DIRS: continue name = p.name for suffix_key, (modality, ext) in _SUFFIX_MAP.items(): if name.endswith(suffix_key): stem = name[: -len(suffix_key)] rel_parent = str(p.parent.relative_to(root)) results.append((p, rel_parent, stem, suffix_key)) break return results def compute_new_path( root: Path, rel_parent: str, stem: str, suffix_key: str, ) -> Path: """Compute the new path for a file.""" modality, ext = _SUFFIX_MAP[suffix_key] if ext == ".npy": return root / "npy" / modality / rel_parent / f"{stem}{ext}" else: return root / modality / rel_parent / f"{stem}{ext}" def migrate(root: Path, dry_run: bool = False) -> None: files = find_old_files(root) if not files: print(f"No old-layout files found in {root}") return print(f"Found {len(files)} files to migrate in {root}") moved = 0 for old_path, rel_parent, stem, suffix_key in files: new_path = compute_new_path(root, rel_parent, stem, suffix_key) if dry_run: print(f" {old_path.relative_to(root)} → {new_path.relative_to(root)}") else: new_path.parent.mkdir(parents=True, exist_ok=True) shutil.move(str(old_path), str(new_path)) moved += 1 if dry_run: print(f"\nDry run: {len(files)} files would be moved.") else: print(f"Moved {moved} files.") # Clean up empty directories left behind. if not dry_run: _cleanup_empty_dirs(root) def _cleanup_empty_dirs(root: Path) -> None: """Remove empty directories (bottom-up).""" removed = 0 for dirpath, dirnames, filenames in os.walk(str(root), topdown=False): d = Path(dirpath) if d == root: continue # Skip new-layout dirs. try: rel = d.relative_to(root) except ValueError: continue if rel.parts and rel.parts[0] in _SKIP_DIRS: continue if not any(d.iterdir()): d.rmdir() removed += 1 if removed: print(f"Cleaned up {removed} empty directories.") def main() -> None: parser = argparse.ArgumentParser( description="Migrate World-UAV-aug from prefix to directory layout", ) parser.add_argument("root", type=Path, help="Path to World-UAV-aug directory") parser.add_argument("--dry-run", action="store_true", help="Show what would be moved without doing it") args = parser.parse_args() if not args.root.is_dir(): print(f"Error: {args.root} is not a directory") return migrate(args.root, dry_run=args.dry_run) if __name__ == "__main__": main()