Initial public release
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s

This commit is contained in:
Jens
2026-08-31 21:56:53 +02:00
commit faeb58ef6d
1386 changed files with 263203 additions and 0 deletions
@@ -0,0 +1,838 @@
"""Prepare explicit real operator samples for GeoIntel detection QA.
This script downloads small orthophoto and GRB building reference pairs from
Digitaal Vlaanderen for documented Kempen AOIs. It is an operator/runtime
helper, not an application provider integration: no GeoIntel API route calls it
and no production data is fetched silently by the app.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from dataclasses import dataclass, replace
from pathlib import Path
from typing import Any
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from training_dataset_eligibility import TRAINING_ELIGIBILITY_POLICY_VERSION # noqa: E402
WMS_URL = "https://geo.api.vlaanderen.be/omwrgbmrvl/wms"
GRB_GBG_URL = "https://geo.api.vlaanderen.be/GRB/ogc/features/v1/collections/GBG/items"
DEFAULT_OUTPUT_DIR = Path("/app/storage/operator-data")
DEFAULT_GRB_PAGE_LIMIT = 1000
DEFAULT_GRB_MAX_FEATURES = 100000
REFERENCE_AOI_CATEGORY = "reference_aoi"
PURE_EMPTY_BACKGROUND_CATEGORY = "pure_empty_negative"
SPARSE_BACKGROUND_CATEGORY = "sparse_building_context"
TRAINING_EXPANSION_SAMPLE_SLUGS = frozenset(
{"olen_center", "lille_center", "oud_turnhout_center", "kasterlee_center"}
)
SMALL_BUILDING_TRAINING_SAMPLE_SLUGS = frozenset(
{"beerse_center", "rijkevorsel_center", "hoogstraten_center", "vorselaar_center"}
)
REVIEWED_ACCURACY_EXPANSION_SAMPLE_SLUGS = frozenset(
{
"arendonk_center",
"dessel_center",
"meerhout_center",
"laakdal_center",
"nijlen_center",
"hulshout_center",
}
)
SMALL_BUILDING_VALIDATION_SAMPLE_SLUGS = frozenset(
{"vosselaar_center", "grobbendonk_center"}
)
MOL_OPERATIONAL_SAMPLE_SLUGS = (
"mol",
"mol_achterbos",
"mol_gompel",
"mol_donk",
"mol_postel",
)
MOL_OPERATIONAL_VALIDATION_SAMPLE_SLUGS = frozenset(MOL_OPERATIONAL_SAMPLE_SLUGS[1:])
MOL_BACKGROUND_CONTROL_SAMPLE_SLUGS = ("postel_bos",)
DEFAULT_VALIDATION_SAMPLE_SLUGS = frozenset(
{
"turnhout",
"retie",
"westerlo",
"arendonk_heide",
*SMALL_BUILDING_VALIDATION_SAMPLE_SLUGS,
*MOL_OPERATIONAL_VALIDATION_SAMPLE_SLUGS,
}
)
requests: Any = None
rasterio: Any = None
Transformer: Any = None
MemoryFile: Any = None
from_bounds: Any = None
@dataclass(frozen=True)
class OperatorSample:
slug: str
display_name: str
center_lon: float
center_lat: float
half_size_m: float = 250.0
width: int = 512
height: int = 512
sample_role: str = "reference"
allow_empty_reference: bool = False
municipality: str | None = None
operational_zone: str = "regional_reference"
SAMPLES: dict[str, OperatorSample] = {
"mol": OperatorSample(
slug="mol",
display_name="Mol center",
center_lon=5.1167,
center_lat=51.1919,
municipality="Mol",
operational_zone="center",
),
"mol_achterbos": OperatorSample(
slug="mol_achterbos",
display_name="Mol Achterbos residential",
center_lon=5.0979785,
center_lat=51.2008032,
municipality="Mol",
operational_zone="residential",
),
"mol_gompel": OperatorSample(
slug="mol_gompel",
display_name="Mol Gompel mixed settlement",
center_lon=5.1502009,
center_lat=51.1927937,
municipality="Mol",
operational_zone="mixed_settlement",
),
"mol_donk": OperatorSample(
slug="mol_donk",
display_name="Mol Donk canal and industrial context",
center_lon=5.1126881,
center_lat=51.2179802,
municipality="Mol",
operational_zone="canal_industrial",
),
"mol_postel": OperatorSample(
slug="mol_postel",
display_name="Mol Postel rural village",
center_lon=5.1897863,
center_lat=51.2874865,
municipality="Mol",
operational_zone="rural_village",
),
"geel": OperatorSample(
slug="geel",
display_name="Geel center",
center_lon=4.991,
center_lat=51.162,
),
"turnhout": OperatorSample(
slug="turnhout",
display_name="Turnhout center",
center_lon=4.9488,
center_lat=51.3225,
),
"herentals": OperatorSample(
slug="herentals",
display_name="Herentals center",
center_lon=4.8339,
center_lat=51.1766,
half_size_m=220.0,
),
"balen": OperatorSample(
slug="balen",
display_name="Balen center",
center_lon=5.1703,
center_lat=51.1688,
half_size_m=220.0,
),
"retie": OperatorSample(
slug="retie",
display_name="Retie center",
center_lon=5.0827,
center_lat=51.2665,
half_size_m=220.0,
),
"westerlo": OperatorSample(
slug="westerlo",
display_name="Westerlo center",
center_lon=4.9158,
center_lat=51.0909,
half_size_m=220.0,
),
"olen_center": OperatorSample(
slug="olen_center",
display_name="Olen center training expansion",
center_lon=4.8597257,
center_lat=51.1438611,
),
"lille_center": OperatorSample(
slug="lille_center",
display_name="Lille center training expansion",
center_lon=4.8242404,
center_lat=51.2382180,
),
"oud_turnhout_center": OperatorSample(
slug="oud_turnhout_center",
display_name="Oud-Turnhout center training expansion",
center_lon=4.9817086,
center_lat=51.3178319,
),
"kasterlee_center": OperatorSample(
slug="kasterlee_center",
display_name="Kasterlee center training expansion",
center_lon=4.9678120,
center_lat=51.2407915,
),
"beerse_center": OperatorSample(
slug="beerse_center",
display_name="Beerse center small-building training expansion",
center_lon=4.8534,
center_lat=51.3192,
),
"rijkevorsel_center": OperatorSample(
slug="rijkevorsel_center",
display_name="Rijkevorsel center small-building training expansion",
center_lon=4.7604,
center_lat=51.3487,
),
"hoogstraten_center": OperatorSample(
slug="hoogstraten_center",
display_name="Hoogstraten center small-building training expansion",
center_lon=4.7609,
center_lat=51.4002,
),
"vorselaar_center": OperatorSample(
slug="vorselaar_center",
display_name="Vorselaar center small-building training expansion",
center_lon=4.7731,
center_lat=51.2020,
),
"arendonk_center": OperatorSample(
slug="arendonk_center",
display_name="Arendonk center reviewed accuracy expansion",
center_lon=5.0864557,
center_lat=51.3202315,
municipality="Arendonk",
operational_zone="reviewed_accuracy_training",
),
"dessel_center": OperatorSample(
slug="dessel_center",
display_name="Dessel center reviewed accuracy expansion",
center_lon=5.1128221,
center_lat=51.2390765,
municipality="Dessel",
operational_zone="reviewed_accuracy_training",
),
"meerhout_center": OperatorSample(
slug="meerhout_center",
display_name="Meerhout center reviewed accuracy expansion",
center_lon=5.0772388,
center_lat=51.1317433,
municipality="Meerhout",
operational_zone="reviewed_accuracy_training",
),
"laakdal_center": OperatorSample(
slug="laakdal_center",
display_name="Laakdal center reviewed accuracy expansion",
center_lon=4.9552253,
center_lat=51.0801317,
municipality="Laakdal",
operational_zone="reviewed_accuracy_training",
),
"nijlen_center": OperatorSample(
slug="nijlen_center",
display_name="Nijlen center reviewed accuracy expansion",
center_lon=4.6702859,
center_lat=51.1610023,
municipality="Nijlen",
operational_zone="reviewed_accuracy_training",
),
"hulshout_center": OperatorSample(
slug="hulshout_center",
display_name="Hulshout center reviewed accuracy expansion",
center_lon=4.7885461,
center_lat=51.0753923,
municipality="Hulshout",
operational_zone="reviewed_accuracy_training",
),
"vosselaar_center": OperatorSample(
slug="vosselaar_center",
display_name="Vosselaar center small-building validation",
center_lon=4.8899,
center_lat=51.3095,
),
"grobbendonk_center": OperatorSample(
slug="grobbendonk_center",
display_name="Grobbendonk center small-building validation",
center_lon=4.7358,
center_lat=51.1907,
),
"postel_bos": OperatorSample(
slug="postel_bos",
display_name="Postel forest background candidate",
center_lon=5.16,
center_lat=51.305,
half_size_m=260.0,
sample_role="background_candidate",
allow_empty_reference=True,
municipality="Mol",
operational_zone="forest_background",
),
"lommel_heide": OperatorSample(
slug="lommel_heide",
display_name="Lommel forest background candidate",
center_lon=5.287,
center_lat=51.249,
half_size_m=260.0,
sample_role="background_candidate",
allow_empty_reference=True,
),
"kasterlee_bos": OperatorSample(
slug="kasterlee_bos",
display_name="Kasterlee forest background candidate",
center_lon=4.965,
center_lat=51.273,
half_size_m=260.0,
sample_role="background_candidate",
allow_empty_reference=True,
),
"dessel_heide": OperatorSample(
slug="dessel_heide",
display_name="Dessel heath background candidate",
center_lon=5.092,
center_lat=51.235,
half_size_m=260.0,
sample_role="background_candidate",
allow_empty_reference=True,
),
"ravels_bos": OperatorSample(
slug="ravels_bos",
display_name="Ravels forest background candidate",
center_lon=4.977,
center_lat=51.384,
half_size_m=260.0,
sample_role="background_candidate",
allow_empty_reference=True,
),
"meerhout_bos": OperatorSample(
slug="meerhout_bos",
display_name="Meerhout forest background candidate",
center_lon=5.069,
center_lat=51.115,
half_size_m=260.0,
sample_role="background_candidate",
allow_empty_reference=True,
),
"geel_bel": OperatorSample(
slug="geel_bel",
display_name="Geel-Bel rural background candidate",
center_lon=5.046,
center_lat=51.137,
half_size_m=260.0,
sample_role="background_candidate",
allow_empty_reference=True,
),
"arendonk_heide": OperatorSample(
slug="arendonk_heide",
display_name="Arendonk heath background candidate",
center_lon=5.238,
center_lat=51.334,
half_size_m=260.0,
sample_role="background_candidate",
allow_empty_reference=True,
),
"herenthout_bos": OperatorSample(
slug="herenthout_bos",
display_name="Herenthout forest background candidate",
center_lon=4.781,
center_lat=51.143,
half_size_m=260.0,
sample_role="background_candidate",
allow_empty_reference=True,
),
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Prepare real Digitaal Vlaanderen orthophoto/GRB building samples for GeoIntel operator QA.",
)
parser.add_argument(
"--output-dir",
type=Path,
default=Path(os.environ.get("OPERATOR_DATA_DIR", DEFAULT_OUTPUT_DIR)),
help="Directory for generated GeoTIFF, GeoJSON and manifest files.",
)
parser.add_argument(
"--samples",
default=",".join(SAMPLES),
help="Comma/space separated sample slugs to prepare. Defaults to all documented samples.",
)
parser.add_argument(
"--force",
action="store_true",
help="Refetch and overwrite sample files. By default existing raster/reference pairs are reused.",
)
parser.add_argument(
"--manifest-name",
default="operator_samples_manifest.json",
help="Manifest filename written inside output-dir.",
)
parser.add_argument(
"--width",
type=int,
default=int(os.environ.get("OPERATOR_SAMPLE_WIDTH", "512")),
help="Orthophoto WMS output width in pixels. Use larger values for operator training datasets.",
)
parser.add_argument(
"--height",
type=int,
default=int(os.environ.get("OPERATOR_SAMPLE_HEIGHT", "512")),
help="Orthophoto WMS output height in pixels. Use larger values for operator training datasets.",
)
parser.add_argument(
"--half-size-scale",
type=float,
default=float(os.environ.get("OPERATOR_SAMPLE_HALF_SIZE_SCALE", "1")),
help="Multiplier applied to each documented AOI half-size in meters.",
)
parser.add_argument(
"--reference-page-limit",
type=int,
default=int(os.environ.get("OPERATOR_GRB_PAGE_LIMIT", str(DEFAULT_GRB_PAGE_LIMIT))),
help="GRB OGC API Features page size for reference buildings.",
)
parser.add_argument(
"--reference-max-features",
type=int,
default=int(os.environ.get("OPERATOR_GRB_MAX_FEATURES", str(DEFAULT_GRB_MAX_FEATURES))),
help="Safety cap for paged GRB reference features per sample.",
)
return parser.parse_args()
def ensure_gis_dependencies() -> None:
global MemoryFile, Transformer, from_bounds, rasterio, requests
try:
import requests as requests_module
import rasterio as rasterio_module
from pyproj import Transformer as transformer_class
from rasterio.io import MemoryFile as memory_file_class
from rasterio.transform import from_bounds as from_bounds_function
except Exception as exc: # pragma: no cover - exercised only in runtime envs.
raise SystemExit(
"prepare_operator_real_data_samples.py requires requests, rasterio and pyproj. "
"Run it inside the GeoIntel all-in-one container or an equivalent GIS Python environment."
) from exc
requests = requests_module
rasterio = rasterio_module
Transformer = transformer_class
MemoryFile = memory_file_class
from_bounds = from_bounds_function
def selected_samples(raw: str) -> list[OperatorSample]:
slugs = [value.strip().lower() for value in raw.replace(",", " ").split() if value.strip()]
if not slugs:
raise SystemExit("--samples must include at least one sample slug")
unknown = [slug for slug in slugs if slug not in SAMPLES]
if unknown:
raise SystemExit(f"Unknown sample slug(s): {', '.join(unknown)}. Known: {', '.join(SAMPLES)}")
return [SAMPLES[slug] for slug in slugs]
def background_category_for_sample(sample: OperatorSample, reference_feature_count: int) -> str:
if sample.sample_role != "background_candidate":
return REFERENCE_AOI_CATEGORY
return PURE_EMPTY_BACKGROUND_CATEGORY if reference_feature_count <= 0 else SPARSE_BACKGROUND_CATEGORY
def recommended_split_for_sample(sample: OperatorSample) -> str:
return "val" if sample.slug in DEFAULT_VALIDATION_SAMPLE_SLUGS else "train"
def apply_sample_overrides(
sample: OperatorSample,
*,
width: int,
height: int,
half_size_scale: float,
) -> OperatorSample:
if width <= 0 or height <= 0:
raise SystemExit("--width and --height must be positive integers")
if half_size_scale <= 0:
raise SystemExit("--half-size-scale must be greater than zero")
return replace(
sample,
width=width,
height=height,
half_size_m=sample.half_size_m * half_size_scale,
)
def sample_bounds(sample: OperatorSample) -> tuple[tuple[float, float, float, float], list[float]]:
lambert = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True)
wgs84 = Transformer.from_crs("EPSG:31370", "EPSG:4326", always_xy=True)
center_x, center_y = lambert.transform(sample.center_lon, sample.center_lat)
minx = center_x - sample.half_size_m
miny = center_y - sample.half_size_m
maxx = center_x + sample.half_size_m
maxy = center_y + sample.half_size_m
corners = [
wgs84.transform(x, y)
for x, y in ((minx, miny), (minx, maxy), (maxx, miny), (maxx, maxy))
]
lon_values = [point[0] for point in corners]
lat_values = [point[1] for point in corners]
return (minx, miny, maxx, maxy), [
min(lon_values),
min(lat_values),
max(lon_values),
max(lat_values),
]
def prepared_url(url: str, params: dict[str, str]) -> str:
return requests.Request("GET", url, params=params).prepare().url
def raster_summary(path: Path) -> dict[str, Any]:
with rasterio.open(path) as ds:
return {
"path": str(path),
"crs": str(ds.crs),
"bounds": list(ds.bounds),
"width": ds.width,
"height": ds.height,
"count": ds.count,
"dtypes": list(ds.dtypes),
}
def geojson_feature_count(path: Path) -> int:
payload = json.loads(path.read_text(encoding="utf-8-sig"))
return len(payload.get("features") or [])
def sample_artifact_paths(sample: OperatorSample, output_dir: Path) -> tuple[Path, Path]:
ortho_path = output_dir / f"{sample.slug}_orthophoto_wms_{sample.width}.tif"
reference_path = output_dir / f"{sample.slug}_grb_gbg_buildings.geojson"
return ortho_path, reference_path
def next_geojson_link(payload: dict[str, Any]) -> str | None:
for link in payload.get("links") or []:
if link.get("rel") == "next" and "geo+json" in str(link.get("type", "")).lower():
href = link.get("href")
if href:
return str(href)
for link in payload.get("links") or []:
if link.get("rel") == "next":
href = link.get("href")
if href:
return str(href)
return None
def merge_reference_page_features(
pages: list[dict[str, Any]],
*,
max_features: int,
) -> tuple[list[dict[str, Any]], bool]:
features: list[dict[str, Any]] = []
seen_feature_keys: set[str] = set()
truncated = False
for page in pages:
for feature in page.get("features") or []:
feature_key = str(feature.get("id") or json.dumps(feature.get("geometry"), sort_keys=True))
if feature_key in seen_feature_keys:
continue
if len(features) >= max_features:
truncated = True
break
seen_feature_keys.add(feature_key)
features.append(feature)
if truncated:
break
return features, truncated
def fetch_orthophoto(sample: OperatorSample, ortho_path: Path, lambert_bbox: tuple[float, float, float, float]) -> str:
minx, miny, maxx, maxy = lambert_bbox
wms_params = {
"SERVICE": "WMS",
"VERSION": "1.3.0",
"REQUEST": "GetMap",
"LAYERS": "Ortho",
"STYLES": "",
"FORMAT": "image/tiff",
"CRS": "EPSG:31370",
"BBOX": f"{minx},{miny},{maxx},{maxy}",
"WIDTH": str(sample.width),
"HEIGHT": str(sample.height),
}
response = requests.get(WMS_URL, params=wms_params, timeout=120)
response.raise_for_status()
content_type = response.headers.get("content-type", "")
if "image" not in content_type.lower() and "tiff" not in content_type.lower():
raise SystemExit(f"Orthophoto WMS did not return an image for {sample.slug}: {content_type}")
with MemoryFile(response.content) as memfile:
with memfile.open() as src:
image = src.read()
profile = src.profile.copy()
profile.update(
driver="GTiff",
width=src.width,
height=src.height,
count=src.count,
dtype=src.dtypes[0],
crs="EPSG:31370",
transform=from_bounds(minx, miny, maxx, maxy, src.width, src.height),
compress="deflate",
tiled=False,
)
with rasterio.open(ortho_path, "w", **profile) as dst:
dst.write(image)
dst.update_tags(
source="Digitaal Vlaanderen OMWRGBMRVL WMS Ortho layer",
source_url=prepared_url(WMS_URL, wms_params),
attribution="Bron: Orthofotomozaiek Vlaanderen, Digitaal Vlaanderen",
aoi=f"{sample.display_name} sample AOI for GeoIntel operator validation",
)
return prepared_url(WMS_URL, wms_params)
def fetch_reference(
sample: OperatorSample,
reference_path: Path,
geo_bbox: list[float],
*,
page_limit: int = DEFAULT_GRB_PAGE_LIMIT,
max_features: int = DEFAULT_GRB_MAX_FEATURES,
) -> tuple[str, int]:
if page_limit <= 0:
raise SystemExit("--reference-page-limit must be a positive integer")
if max_features <= 0:
raise SystemExit("--reference-max-features must be a positive integer")
ogc_params = {
"f": "application/geo+json",
"limit": str(page_limit),
"bbox": ",".join(f"{value:.8f}" for value in geo_bbox),
}
pages: list[dict[str, Any]] = []
page_urls = [prepared_url(GRB_GBG_URL, ogc_params)]
response = requests.get(GRB_GBG_URL, params=ogc_params, timeout=120)
seen_next_urls: set[str] = set()
stopped_at_feature_cap = False
while True:
response.raise_for_status()
page = response.json()
pages.append(page)
next_url = next_geojson_link(page)
if not next_url:
break
if next_url in seen_next_urls:
raise SystemExit(f"GRB GBG pagination loop detected for {sample.slug}: {next_url}")
if sum(len(current_page.get("features") or []) for current_page in pages) >= max_features:
stopped_at_feature_cap = True
break
seen_next_urls.add(next_url)
page_urls.append(next_url)
response = requests.get(next_url, params=None, timeout=120)
reference = pages[0] if pages else {"type": "FeatureCollection", "features": []}
features, truncated = merge_reference_page_features(pages, max_features=max_features)
truncated = truncated or stopped_at_feature_cap
if not features and not sample.allow_empty_reference:
raise SystemExit(f"GRB GBG returned no building features for {sample.slug} bbox {geo_bbox}")
reference["features"] = features
reference["name"] = f"GRB GBG buildings - {sample.display_name} sample AOI"
reference["source"] = "Digitaal Vlaanderen GRB OGC API Features collection GBG"
reference["source_url"] = prepared_url(GRB_GBG_URL, ogc_params)
reference["source_urls"] = page_urls
reference["attribution"] = "Bron: Grootschalig Referentie Bestand Vlaanderen, Digitaal Vlaanderen"
reference["bbox"] = geo_bbox
reference["sample_slug"] = sample.slug
reference["sample_role"] = sample.sample_role
reference["municipality"] = sample.municipality
reference["operational_zone"] = sample.operational_zone
reference["allow_empty_reference"] = sample.allow_empty_reference
reference["background_category"] = background_category_for_sample(sample, len(features))
reference["recommended_split"] = recommended_split_for_sample(sample)
reference["reference_page_limit"] = page_limit
reference["reference_max_features"] = max_features
reference["reference_pages_fetched"] = len(pages)
reference["reference_truncated"] = truncated
reference["numberReturned"] = len(features)
if "links" in reference:
reference["links"] = [link for link in reference.get("links") or [] if link.get("rel") != "next"]
for feature in features:
props = feature.setdefault("properties", {})
props.setdefault("source_name", "grb")
props.setdefault("reference_layer_name", "buildings")
props.setdefault("sample_slug", sample.slug)
props.setdefault("sample_role", sample.sample_role)
props.setdefault("municipality", sample.municipality)
props.setdefault("operational_zone", sample.operational_zone)
props.setdefault("background_category", background_category_for_sample(sample, len(features)))
props.setdefault("recommended_split", recommended_split_for_sample(sample))
reference_path.write_text(json.dumps(reference, ensure_ascii=False), encoding="utf-8")
return prepared_url(GRB_GBG_URL, ogc_params), len(features)
def prepare_sample(
sample: OperatorSample,
output_dir: Path,
force: bool,
*,
reference_page_limit: int = DEFAULT_GRB_PAGE_LIMIT,
reference_max_features: int = DEFAULT_GRB_MAX_FEATURES,
) -> dict[str, Any]:
ortho_path, reference_path = sample_artifact_paths(sample, output_dir)
lambert_bbox, geo_bbox = sample_bounds(sample)
skip_existing = ortho_path.exists() and reference_path.exists() and not force
source_urls: dict[str, str | None] = {"orthophoto": None, "reference": None}
if not skip_existing:
source_urls["orthophoto"] = fetch_orthophoto(sample, ortho_path, lambert_bbox)
source_urls["reference"], reference_feature_count = fetch_reference(
sample,
reference_path,
geo_bbox,
page_limit=reference_page_limit,
max_features=reference_max_features,
)
else:
reference_feature_count = geojson_feature_count(reference_path)
background_category = background_category_for_sample(sample, reference_feature_count)
return {
"sample_slug": sample.slug,
"display_name": sample.display_name,
"center_lon": sample.center_lon,
"center_lat": sample.center_lat,
"half_size_m": sample.half_size_m,
"width": sample.width,
"height": sample.height,
"sample_role": sample.sample_role,
"municipality": sample.municipality,
"operational_zone": sample.operational_zone,
"allow_empty_reference": sample.allow_empty_reference,
"background_category": background_category,
"recommended_split": recommended_split_for_sample(sample),
"raster_path": str(ortho_path),
"reference_path": str(reference_path),
"reference_feature_count": reference_feature_count,
"raster": raster_summary(ortho_path),
"wgs84_bbox": geo_bbox,
"epsg31370_bbox": list(lambert_bbox),
"skip_existing": skip_existing,
"source_urls": source_urls,
"reference_page_limit": reference_page_limit,
"reference_max_features": reference_max_features,
"attribution": {
"orthophoto": "Bron: Orthofotomozaiek Vlaanderen, Digitaal Vlaanderen",
"reference": "Bron: Grootschalig Referentie Bestand Vlaanderen, Digitaal Vlaanderen",
},
}
def write_readme(output_dir: Path, samples: list[dict[str, Any]]) -> None:
lines = [
"# GeoIntel operator real-data samples",
"",
"Generated for runtime validation, not committed to the GeoIntel repository.",
"",
"Sources:",
"- Orthophoto rasters: Digitaal Vlaanderen OMWRGBMRVL WMS `Ortho` layer.",
"- Reference buildings: Digitaal Vlaanderen GRB OGC API Features `GBG` collection.",
"- Attribution: Bron: Orthofotomozaiek Vlaanderen / Grootschalig Referentie Bestand Vlaanderen, Digitaal Vlaanderen.",
"",
"Samples:",
]
for sample in samples:
lines.append(
f"- `{sample['sample_slug']}`: `{Path(sample['raster_path']).name}` and "
f"`{Path(sample['reference_path']).name}`, "
f"{sample['reference_feature_count']} reference features, role `{sample['sample_role']}`, "
f"municipality `{sample['municipality'] or 'regional'}`, zone `{sample['operational_zone']}`, "
f"background category `{sample['background_category']}`, "
f"recommended split `{sample['recommended_split']}`."
)
lines.append("")
lines.append("Purpose: configured-YOLO detection + persisted QA/QC validation with operator-provided files.")
(output_dir / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
def main() -> int:
args = parse_args()
ensure_gis_dependencies()
output_dir: Path = args.output_dir
output_dir.mkdir(parents=True, exist_ok=True)
samples = [
prepare_sample(
apply_sample_overrides(
sample,
width=args.width,
height=args.height,
half_size_scale=args.half_size_scale,
),
output_dir,
force=args.force,
reference_page_limit=args.reference_page_limit,
reference_max_features=args.reference_max_features,
)
for sample in selected_samples(args.samples)
]
write_readme(output_dir, samples)
manifest = {
"schema_version": 2,
"description": "GeoIntel operator real-data samples for configured-YOLO QA validation.",
"training_eligibility": {
"policy_version": TRAINING_ELIGIBILITY_POLICY_VERSION,
"status": "not_eligible",
"reason": (
"Direct provider downloads are QA-only until re-ingested through the governed "
"dataset source registry, contract validator and provenance snapshot flow."
),
},
"output_dir": str(output_dir),
"sample_width": args.width,
"sample_height": args.height,
"half_size_scale": args.half_size_scale,
"reference_page_limit": args.reference_page_limit,
"reference_max_features": args.reference_max_features,
"default_validation_sample_slugs": sorted(DEFAULT_VALIDATION_SAMPLE_SLUGS),
"training_expansion_sample_slugs": sorted(TRAINING_EXPANSION_SAMPLE_SLUGS),
"reviewed_accuracy_expansion_sample_slugs": sorted(REVIEWED_ACCURACY_EXPANSION_SAMPLE_SLUGS),
"samples": samples,
}
manifest_path = output_dir / args.manifest_name
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8")
print(json.dumps({"status": "ok", "manifest_path": str(manifest_path), "samples": samples}, indent=2, ensure_ascii=False))
return 0
if __name__ == "__main__":
sys.exit(main())