Expand YOLO training AOIs safely
This commit is contained in:
@@ -6,6 +6,7 @@ import subprocess
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
@@ -75,6 +76,30 @@ def test_operator_yolo_tile_dataset_export_help_does_not_require_gis_dependencie
|
||||
assert "--blank-range-threshold" in result.stdout
|
||||
|
||||
|
||||
def test_default_validation_split_is_explicit_and_rejects_holdout_leakage() -> None:
|
||||
module = load_tile_exporter()
|
||||
samples = [
|
||||
{"sample_slug": "geel", "recommended_split": "train"},
|
||||
{"sample_slug": "turnhout", "recommended_split": "val"},
|
||||
{"sample_slug": "retie", "recommended_split": "val"},
|
||||
{"sample_slug": "westerlo", "recommended_split": "val"},
|
||||
{"sample_slug": "arendonk_heide", "recommended_split": "val"},
|
||||
]
|
||||
|
||||
assert module.DEFAULT_VALIDATION_SAMPLE_SLUGS == frozenset(
|
||||
{"turnhout", "retie", "westerlo", "arendonk_heide"}
|
||||
)
|
||||
assert module.validate_validation_split(
|
||||
samples,
|
||||
set(module.DEFAULT_VALIDATION_SAMPLE_SLUGS),
|
||||
) == set(module.DEFAULT_VALIDATION_SAMPLE_SLUGS)
|
||||
|
||||
with pytest.raises(SystemExit, match="recommended validation holdouts"):
|
||||
module.validate_validation_split(samples, {"turnhout"})
|
||||
with pytest.raises(SystemExit, match="unknown samples"):
|
||||
module.validate_validation_split(samples, {"turnhout", "missing"})
|
||||
|
||||
|
||||
def test_iter_tile_windows_covers_edges_without_duplicates() -> None:
|
||||
module = load_tile_exporter()
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
@@ -43,6 +44,40 @@ def test_operator_sample_registry_includes_kempen_reference_and_background_candi
|
||||
assert all(module.SAMPLES[slug].sample_role == "background_candidate" for slug in expected_background_slugs)
|
||||
|
||||
|
||||
def test_operator_training_expansion_preserves_geographically_separate_holdouts() -> None:
|
||||
module = load_sample_preparer()
|
||||
|
||||
expected_expansion = {"olen_center", "lille_center", "oud_turnhout_center", "kasterlee_center"}
|
||||
expected_holdouts = {"turnhout", "retie", "westerlo", "arendonk_heide"}
|
||||
|
||||
assert module.TRAINING_EXPANSION_SAMPLE_SLUGS == frozenset(expected_expansion)
|
||||
assert module.DEFAULT_VALIDATION_SAMPLE_SLUGS == frozenset(expected_holdouts)
|
||||
assert all(module.SAMPLES[slug].sample_role == "reference" for slug in expected_expansion)
|
||||
assert all(not module.SAMPLES[slug].allow_empty_reference for slug in expected_expansion)
|
||||
assert all(module.recommended_split_for_sample(module.SAMPLES[slug]) == "train" for slug in expected_expansion)
|
||||
assert all(module.recommended_split_for_sample(module.SAMPLES[slug]) == "val" for slug in expected_holdouts)
|
||||
|
||||
def distance_m(left, right) -> float:
|
||||
radius_m = 6_371_008.8
|
||||
left_lat = math.radians(left.center_lat)
|
||||
right_lat = math.radians(right.center_lat)
|
||||
delta_lat = right_lat - left_lat
|
||||
delta_lon = math.radians(right.center_lon - left.center_lon)
|
||||
haversine = (
|
||||
math.sin(delta_lat / 2) ** 2
|
||||
+ math.cos(left_lat) * math.cos(right_lat) * math.sin(delta_lon / 2) ** 2
|
||||
)
|
||||
return 2 * radius_m * math.asin(math.sqrt(haversine))
|
||||
|
||||
reference_holdouts = expected_holdouts - {"arendonk_heide"}
|
||||
for expansion_slug in expected_expansion:
|
||||
expansion = module.SAMPLES[expansion_slug]
|
||||
assert min(
|
||||
distance_m(expansion, module.SAMPLES[holdout_slug])
|
||||
for holdout_slug in reference_holdouts
|
||||
) >= 2_000
|
||||
|
||||
|
||||
def test_operator_background_candidates_are_unique_enough_for_hard_negative_training() -> None:
|
||||
module = load_sample_preparer()
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ def test_prepare_sample_manifest_records_background_category_from_cached_referen
|
||||
prepared = module.prepare_sample(sample, tmp_path, force=False)
|
||||
|
||||
assert prepared["background_category"] == "sparse_building_context"
|
||||
assert prepared["recommended_split"] == "train"
|
||||
assert prepared["reference_feature_count"] == 1
|
||||
|
||||
|
||||
|
||||
@@ -201,3 +201,62 @@ def test_false_negative_audit_finds_persistent_reference_misses(tmp_path: Path)
|
||||
assert active["false_negative_area_m2"]["median"] > 0
|
||||
assert report["recommendations"]
|
||||
assert (output_dir / "detection_false_negative_audit.md").is_file()
|
||||
|
||||
|
||||
def test_false_negative_audit_rejects_mismatched_reference_populations(tmp_path: Path) -> None:
|
||||
script = ROOT / "scripts" / "audit_detection_false_negative_evidence.py"
|
||||
portfolio_args = []
|
||||
for label, source_ids in (("active", ("one", "two")), ("candidate", ("one",))):
|
||||
portfolio_dir = tmp_path / label
|
||||
evidence_dir = portfolio_dir / "samples" / "geel" / "evidence"
|
||||
evidence_dir.mkdir(parents=True)
|
||||
evidence_path = evidence_dir / "calibration_evidence.geojson"
|
||||
evidence_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
_evidence_feature(
|
||||
"false_negative",
|
||||
source_id,
|
||||
_polygon(5.0 + index * 0.001, 51.2, 0.0001),
|
||||
)
|
||||
for index, source_id in enumerate(source_ids)
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
portfolio_path = portfolio_dir / "calibration_evidence_portfolio.json"
|
||||
portfolio_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"model_asset_id": f"model-{label}",
|
||||
"samples": [
|
||||
{
|
||||
"sample_slug": "geel",
|
||||
"evidence_geojson_path": str(evidence_path),
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
portfolio_args.extend(("--portfolio", f"{label}={portfolio_path}"))
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(script),
|
||||
*portfolio_args,
|
||||
"--output-dir",
|
||||
str(tmp_path / "audit"),
|
||||
],
|
||||
cwd=ROOT,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "different reference populations" in result.stderr
|
||||
|
||||
Reference in New Issue
Block a user