Expand YOLO training AOIs safely
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-12 23:42:29 +02:00
parent 53cd38a5b2
commit 0f49c980ba
11 changed files with 310 additions and 7 deletions
+46 -3
View File
@@ -25,6 +25,10 @@ REFERENCE_AOI_CATEGORY = "reference_aoi"
PURE_EMPTY_BACKGROUND_CATEGORY = "pure_empty_negative"
SPARSE_BACKGROUND_CATEGORY = "sparse_building_context"
LOW_VARIANCE_NEGATIVE_SKIP_REASON = "low_visual_variance_negative"
DEFAULT_VALIDATION_SAMPLE_SLUGS = frozenset(
{"turnhout", "retie", "westerlo", "arendonk_heide"}
)
DEFAULT_VALIDATION_SAMPLES = ",".join(sorted(DEFAULT_VALIDATION_SAMPLE_SLUGS))
rasterio: Any = None
Window: Any = None
Transformer: Any = None
@@ -67,8 +71,11 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--stride", type=int, default=int(os.environ.get("OPERATOR_YOLO_TILE_STRIDE", "128")))
parser.add_argument(
"--val-samples",
default=os.environ.get("OPERATOR_YOLO_VAL_SAMPLES", "turnhout"),
help="Comma/space separated sample slugs assigned to validation. Defaults to turnhout.",
default=os.environ.get("OPERATOR_YOLO_VAL_SAMPLES", DEFAULT_VALIDATION_SAMPLES),
help=(
"Comma/space separated sample slugs assigned to validation. "
"Defaults to the documented Turnhout, Retie, Westerlo and Arendonk-heide holdouts."
),
)
parser.add_argument(
"--negative-keep-ratio",
@@ -145,6 +152,35 @@ def split_slugs(raw: str) -> set[str]:
return {value.strip().lower() for value in raw.replace(",", " ").split() if value.strip()}
def validate_validation_split(samples: list[dict[str, Any]], val_slugs: set[str]) -> set[str]:
sample_slugs = {
str(sample.get("sample_slug") or "").strip().lower()
for sample in samples
if str(sample.get("sample_slug") or "").strip()
}
if not val_slugs:
raise SystemExit("YOLO validation split must include at least one sample")
unknown = val_slugs - sample_slugs
if unknown:
raise SystemExit(
"YOLO validation split references unknown samples: " + ", ".join(sorted(unknown))
)
recommended_holdouts = {
str(sample.get("sample_slug") or "").strip().lower()
for sample in samples
if str(sample.get("recommended_split") or "").strip().lower() == "val"
}
missing_holdouts = recommended_holdouts - val_slugs
if missing_holdouts:
raise SystemExit(
"YOLO validation split omits recommended validation holdouts: "
+ ", ".join(sorted(missing_holdouts))
)
if not sample_slugs - val_slugs:
raise SystemExit("YOLO validation split leaves no training samples")
return val_slugs
def edge_starts(length: int, tile_size: int, stride: int) -> list[int]:
if tile_size <= 0:
raise ValueError("tile_size must be positive")
@@ -367,6 +403,7 @@ def export_sample_tiles(
sample_slug = str(sample["sample_slug"])
sample_role = str(sample.get("sample_role") or "reference")
background_category = background_category_for_sample(sample)
recommended_split = str(sample.get("recommended_split") or "")
split = "val" if sample_slug.lower() in val_slugs else "train"
raster_path = resolve_manifest_path(str(sample["raster_path"]), manifest_path)
reference_path = resolve_manifest_path(str(sample["reference_path"]), manifest_path)
@@ -390,6 +427,9 @@ def export_sample_tiles(
exported.append(
{
"sample_slug": sample_slug,
"sample_role": sample_role,
"background_category": background_category,
"recommended_split": recommended_split,
"split": split,
"tile_index": tile_index,
"kept": False,
@@ -410,6 +450,7 @@ def export_sample_tiles(
"sample_slug": sample_slug,
"sample_role": sample_role,
"background_category": background_category,
"recommended_split": recommended_split,
"split": split,
"tile_index": tile_index,
"kept": False,
@@ -446,6 +487,7 @@ def export_sample_tiles(
"sample_slug": sample_slug,
"sample_role": sample_role,
"background_category": background_category,
"recommended_split": recommended_split,
"split": split,
"tile_index": tile_index,
"repeat_index": repeat_index,
@@ -479,7 +521,7 @@ def main() -> int:
samples = manifest.get("samples") or []
if not samples:
raise SystemExit("Operator sample manifest contains no samples")
val_slugs = split_slugs(args.val_samples)
val_slugs = validate_validation_split(samples, split_slugs(args.val_samples))
exported_tiles: list[dict[str, Any]] = []
for sample in samples:
exported_tiles.extend(
@@ -527,6 +569,7 @@ def main() -> int:
"drop_low_variance_negatives": args.drop_low_variance_negatives,
"blank_range_threshold": args.blank_range_threshold,
"source_sample_count": len(samples),
"validation_sample_slugs": sorted(val_slugs),
"tile_count": len(kept_tiles),
"positive_tile_count": len(positive_tiles),
"negative_tile_count": len(negative_tiles),