"""Build a train/val dataset from large images and prediction rasters. This is a bootstrap utility. Masks created from previous predictions are pseudo-labels, not human-verified ground truth. """ from __future__ import annotations import argparse import random from pathlib import Path import numpy as np import rasterio from rasterio.windows import Window def find_pairs(source_dir: Path): images = [p for p in source_dir.glob("*.tif") if "_prediction" not in p.stem.lower()] pairs = [] for image in images: pred = None for candidate in source_dir.glob(f"{image.stem}_*/{image.stem}_prediction.tif"): pred = candidate break if pred: pairs.append((image, pred)) return pairs def ensure_layout(output_dir: Path): for split in ("train", "val"): (output_dir / split / "images").mkdir(parents=True, exist_ok=True) (output_dir / split / "masks").mkdir(parents=True, exist_ok=True) def write_tile(src, mask_src, window: Window, image_path: Path, mask_path: Path, foreground_threshold: int): image = src.read(window=window) mask = mask_src.read(1, window=window) if image.shape[1] != window.height or image.shape[2] != window.width: return False if mask.shape[0] != window.height or mask.shape[1] != window.width: return False image_meta = src.meta.copy() image_meta.update( { "height": int(window.height), "width": int(window.width), "transform": src.window_transform(window), "compress": "lzw", } ) mask_meta = mask_src.meta.copy() mask_meta.update( { "count": 1, "dtype": "uint8", "height": int(window.height), "width": int(window.width), "transform": mask_src.window_transform(window), "compress": "lzw", } ) binary_mask = (mask >= foreground_threshold).astype(np.uint8) * 255 with rasterio.open(image_path, "w", **image_meta) as dst: dst.write(image) with rasterio.open(mask_path, "w", **mask_meta) as dst: dst.write(binary_mask, 1) return True def build_dataset(source_dir: Path, output_dir: Path, tile_size: int, stride: int, val_ratio: float, foreground_threshold: int): pairs = find_pairs(source_dir) if not pairs: raise RuntimeError(f"No image/prediction pairs found under {source_dir}") ensure_layout(output_dir) rng = random.Random(42) written = {"train": 0, "val": 0} for pair_index, (image_path, pred_path) in enumerate(pairs): with rasterio.open(image_path) as src, rasterio.open(pred_path) as mask_src: windows = [] for y in range(0, src.height - tile_size + 1, stride): for x in range(0, src.width - tile_size + 1, stride): windows.append(Window(x, y, tile_size, tile_size)) rng.shuffle(windows) for tile_index, window in enumerate(windows): split = "val" if rng.random() < val_ratio else "train" name = f"pair{pair_index:02d}_{tile_index:06d}.tif" ok = write_tile( src, mask_src, window, output_dir / split / "images" / name, output_dir / split / "masks" / name, foreground_threshold, ) if ok: written[split] += 1 print(f"Wrote pseudo dataset to {output_dir}") print(f"train tiles: {written['train']}") print(f"val tiles: {written['val']}") def main(): parser = argparse.ArgumentParser() parser.add_argument("--source-dir", default="data") parser.add_argument("--output-dir", default="data_pseudo") parser.add_argument("--tile-size", type=int, default=256) parser.add_argument("--stride", type=int, default=256) parser.add_argument("--val-ratio", type=float, default=0.15) parser.add_argument("--foreground-threshold", type=int, default=1) args = parser.parse_args() build_dataset( Path(args.source_dir), Path(args.output_dir), args.tile_size, args.stride, args.val_ratio, args.foreground_threshold, ) if __name__ == "__main__": main()