Expand YOLO training AOIs safely
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user