Reject same-year post-mosaic building labels
This commit is contained in:
@@ -134,6 +134,38 @@ def test_normalizer_rejects_features_created_after_dated_imagery(tmp_path: Path)
|
|||||||
assert audit["decision_counts"] == {"accepted": 1, "created_after_imagery_period": 1}
|
assert audit["decision_counts"] == {"accepted": 1, "created_after_imagery_period": 1}
|
||||||
|
|
||||||
|
|
||||||
|
def test_normalizer_rejects_same_year_feature_after_annual_mosaic_start(tmp_path: Path) -> None:
|
||||||
|
raster_path = tmp_path / "image.tif"
|
||||||
|
with rasterio.open(
|
||||||
|
raster_path, "w", driver="GTiff", width=100, height=100, count=3,
|
||||||
|
dtype="uint8", crs="EPSG:4326", transform=from_origin(4.0, 51.0, 0.001, 0.001),
|
||||||
|
) as dataset:
|
||||||
|
dataset.write(np.zeros((3, 100, 100), dtype="uint8"))
|
||||||
|
geometry = {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[[4.01, 50.99], [4.02, 50.99], [4.02, 50.98], [4.01, 50.98], [4.01, 50.99]]],
|
||||||
|
}
|
||||||
|
reference_path = tmp_path / "reference.geojson"
|
||||||
|
reference_path.write_text(json.dumps({
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [{"type": "Feature", "properties": {"BEGINDATUM": "2025-08-18"}, "geometry": geometry}],
|
||||||
|
}), encoding="utf-8")
|
||||||
|
|
||||||
|
normalized, audit = module.normalize(
|
||||||
|
reference_path=reference_path,
|
||||||
|
raster_path=raster_path,
|
||||||
|
source_name="grb",
|
||||||
|
min_label_px=3,
|
||||||
|
imagery_observed_at="2025-01-01T00:00:00Z",
|
||||||
|
imagery_valid_to="2025-12-31T23:59:59Z",
|
||||||
|
reference_observed_at="2026-07-01T00:00:00Z",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert normalized["features"] == []
|
||||||
|
assert audit["decision_counts"] == {"created_after_imagery_period": 1}
|
||||||
|
assert audit["imagery_feature_creation_cutoff"] == "2025-01-01T00:00:00+00:00"
|
||||||
|
|
||||||
|
|
||||||
def test_normalizer_merges_only_touching_visible_roof_instances(tmp_path: Path) -> None:
|
def test_normalizer_merges_only_touching_visible_roof_instances(tmp_path: Path) -> None:
|
||||||
raster_path = tmp_path / "image.tif"
|
raster_path = tmp_path / "image.tif"
|
||||||
with rasterio.open(
|
with rasterio.open(
|
||||||
|
|||||||
@@ -159,9 +159,16 @@ def normalize(
|
|||||||
decisions: list[dict[str, Any]] = []
|
decisions: list[dict[str, Any]] = []
|
||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
counts: Counter[str] = Counter()
|
counts: Counter[str] = Counter()
|
||||||
imagery_cutoff = (
|
imagery_dates = [
|
||||||
datetime.fromisoformat(imagery_valid_to.replace("Z", "+00:00")) if imagery_valid_to else None
|
datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||||
)
|
for value in (imagery_observed_at, imagery_valid_to)
|
||||||
|
if value
|
||||||
|
]
|
||||||
|
# Annual mosaics expose a validity interval but not the flight date for
|
||||||
|
# each pixel. Using the interval end admits buildings created later in the
|
||||||
|
# same year that are visibly absent from the mosaic. The earliest governed
|
||||||
|
# date is therefore the only conservative training-label cutoff.
|
||||||
|
imagery_cutoff = min(imagery_dates) if imagery_dates else None
|
||||||
with rasterio.open(raster_path) as raster:
|
with rasterio.open(raster_path) as raster:
|
||||||
if raster.crs is None:
|
if raster.crs is None:
|
||||||
raise SystemExit("Raster CRS is required")
|
raise SystemExit("Raster CRS is required")
|
||||||
@@ -260,6 +267,8 @@ def normalize(
|
|||||||
"min_label_px": min_label_px,
|
"min_label_px": min_label_px,
|
||||||
"imagery_observed_at": imagery_observed_at,
|
"imagery_observed_at": imagery_observed_at,
|
||||||
"imagery_valid_to": imagery_valid_to,
|
"imagery_valid_to": imagery_valid_to,
|
||||||
|
"imagery_feature_creation_cutoff": imagery_cutoff.isoformat() if imagery_cutoff else None,
|
||||||
|
"imagery_feature_creation_cutoff_policy": "earliest_governed_imagery_date",
|
||||||
"reference_observed_at": reference_observed_at,
|
"reference_observed_at": reference_observed_at,
|
||||||
"temporal_mismatch_days": temporal_mismatch_days,
|
"temporal_mismatch_days": temporal_mismatch_days,
|
||||||
"temporal_alignment_status": temporal_alignment_status,
|
"temporal_alignment_status": temporal_alignment_status,
|
||||||
|
|||||||
Reference in New Issue
Block a user