Files
depth_edges_annotate_worlduav/src/augmentor/dataset.py
pikaliov 13ff079891 Refactor output to directory-based layout + migration script
Replace prefix-based naming (crop_12_4_depth.png) with directory-based
layout where modality is determined by folder (depth/crop_12_4.png).

New structure:
  output_root/{modality}/{rel_parent}/{stem}.png    (vis)
  output_root/npy/{modality}/{rel_parent}/{stem}.npy (intermediate)
  output_root/safetensors/{rel_parent}/{stem}.safetensors (training)

- Rewrite io_utils.py save functions: (output_root, rel_parent, stem)
- Update ImageRecord: output_root + rel_parent instead of output_dir
- Add path helpers: npy_path(), vis_path(), safetensors_path()
- Add scripts/migrate_layout.py for converting existing datasets
- Update all tests (143 passing)
- Update README with new layout docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 17:11:01 +03:00

213 lines
6.2 KiB
Python

"""Dataset discovery, completion filtering, and PyTorch Dataset for augmentation."""
from __future__ import annotations
import logging
from pathlib import Path
from typing import Any, NamedTuple
import torch
from PIL import Image
from torch.utils.data import Dataset
from torchvision import transforms
from src.augmentor.io_utils import npy_path, vis_path, safetensors_path
logger = logging.getLogger(__name__)
EXTENSIONS = {".png", ".jpg", ".jpeg", ".bmp"}
EXCLUDE_NAMES = {"merge.tif"}
EXCLUDE_DIRS = {"Index", "__MACOSX", "charts", "__pycache__"}
# Incomplete World-UAV Country scenes (16 of 171): no DB crops, no positive.json.
INCOMPLETE_SCENES: set[str] = {
"CastleHill", "Dalry", "Haymarket", "NewTown", "Stockbridge",
"CamdenTown", "CoventGarden", "Fitzrovia", "Mayfair", "SoHo",
"Ancoats", "Castlefield", "Deansgate", "NorthernQuarter", "Piccadilly",
"JewelleryQuarter",
}
class ImageRecord(NamedTuple):
"""Lightweight descriptor for a single dataset image."""
abs_path: Path
rel_path: str
stem: str
output_root: Path
rel_parent: str
def is_query_record(record: ImageRecord) -> bool:
"""Return True if the record belongs to a query (drone) image."""
return "query" in Path(record.rel_path).parts
def split_by_view(
records: list[ImageRecord],
) -> tuple[list[ImageRecord], list[ImageRecord]]:
"""Split records into (db_records, query_records)."""
db: list[ImageRecord] = []
query: list[ImageRecord] = []
for r in records:
if is_query_record(r):
query.append(r)
else:
db.append(r)
return db, query
# ---------------------------------------------------------------------------
# Discovery
# ---------------------------------------------------------------------------
def discover_images(
root: Path,
subset: str | None = None,
source: str | None = None,
) -> list[ImageRecord]:
"""Recursively find images under *root*, preserving relative paths.
Args:
root: Dataset root directory.
subset: Limit to a World-UAV subset (Country, Terrain, Rot).
source: Filter by source — 'query' (drone) or 'db' (satellite).
Returns:
Sorted list of ImageRecord.
"""
search_root = root / subset if subset else root
if not search_root.exists():
logger.warning("Search root does not exist: %s", search_root)
return []
records: list[ImageRecord] = []
n_skipped_incomplete = 0
for p in sorted(search_root.rglob("*")):
if not p.is_file():
continue
if p.name in EXCLUDE_NAMES:
continue
if p.suffix.lower() not in EXTENSIONS:
continue
if any(d in p.parts for d in EXCLUDE_DIRS):
continue
if any(scene in p.parts for scene in INCOMPLETE_SCENES):
n_skipped_incomplete += 1
continue
if source is not None:
rel_parts = p.relative_to(root).parts
if source == "query" and "DB" in rel_parts:
continue
if source == "db" and "query" in rel_parts:
continue
rel = p.relative_to(root)
records.append(ImageRecord(
abs_path=p,
rel_path=str(rel),
stem=p.stem,
output_root=Path(),
rel_parent=str(rel.parent),
))
if n_skipped_incomplete > 0:
logger.info(
"Skipped %d images from %d incomplete scenes.",
n_skipped_incomplete, len(INCOMPLETE_SCENES),
)
return records
def attach_output_dirs(
records: list[ImageRecord],
output_root: Path,
) -> list[ImageRecord]:
"""Set output_root for each record."""
return [r._replace(output_root=output_root) for r in records]
# Modality name for each stage (used for folder names).
STAGE_MODALITY: dict[str, str] = {
"depth": "depth",
"edges": "edge",
"segmentation": "segm",
"chmv2": "chm",
}
def filter_completed(
records: list[ImageRecord],
stage: str,
) -> tuple[list[ImageRecord], int]:
"""Return (pending_records, n_skipped) for a given stage."""
if stage == "consolidate":
return filter_consolidated(records)
modality = STAGE_MODALITY.get(stage)
if modality is None:
return records, 0
pending: list[ImageRecord] = []
skipped = 0
for r in records:
np_p = npy_path(r.output_root, modality, r.rel_parent, r.stem)
vis_p = vis_path(r.output_root, modality, r.rel_parent, r.stem)
if np_p.exists() or vis_p.exists():
skipped += 1
else:
pending.append(r)
return pending, skipped
def filter_consolidated(
records: list[ImageRecord],
) -> tuple[list[ImageRecord], int]:
"""Return (pending, skipped) for safetensors consolidation stage."""
pending: list[ImageRecord] = []
skipped = 0
for r in records:
st = safetensors_path(r.output_root, r.rel_parent, r.stem)
if st.exists():
skipped += 1
else:
pending.append(r)
return pending, skipped
# ---------------------------------------------------------------------------
# PyTorch Dataset
# ---------------------------------------------------------------------------
class AugmentDataset(Dataset):
"""Loads RGB images at image_size x image_size for the augmentation pipeline.
Args:
records: List of ImageRecord to load.
image_size: Target spatial resolution (default 256).
"""
def __init__(self, records: list[ImageRecord], image_size: int = 256) -> None:
self.records = records
self.resize = transforms.Resize(
(image_size, image_size),
interpolation=transforms.InterpolationMode.BILINEAR,
)
self.to_tensor = transforms.ToTensor()
def __len__(self) -> int:
return len(self.records)
def __getitem__(self, idx: int) -> dict[str, Any]:
r = self.records[idx]
img = Image.open(r.abs_path).convert("RGB")
tensor = self.to_tensor(self.resize(img))
return {
"image_raw": tensor,
"rel_path": r.rel_path,
"stem": r.stem,
"output_root": str(r.output_root),
"rel_parent": r.rel_parent,
}