Constrain visible roof grouping to compact clusters
This commit is contained in:
@@ -179,3 +179,25 @@ def test_normalizer_merges_only_touching_visible_roof_instances(tmp_path: Path)
|
|||||||
assert sorted(item["properties"]["source_feature_count"] for item in normalized["features"]) == [1, 2]
|
assert sorted(item["properties"]["source_feature_count"] for item in normalized["features"]) == [1, 2]
|
||||||
assert audit["accepted_source_feature_count"] == 3
|
assert audit["accepted_source_feature_count"] == 3
|
||||||
assert audit["accepted_feature_count"] == 2
|
assert audit["accepted_feature_count"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_visible_roof_merge_retains_large_touching_chains() -> None:
|
||||||
|
features = []
|
||||||
|
for index in range(13):
|
||||||
|
left = float(index)
|
||||||
|
features.append(
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"id": str(index),
|
||||||
|
"properties": {"source_feature_id": str(index)},
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[[left, 0], [left + 1, 0], [left + 1, 1], [left, 1], [left, 0]]],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
merged = module.merge_touching_roof_instances(features, "grb")
|
||||||
|
assert len(merged) == 13
|
||||||
|
assert {item["properties"]["label_semantics"] for item in merged} == {
|
||||||
|
"native_instance_complex_touch_group"
|
||||||
|
}
|
||||||
|
|||||||
@@ -102,3 +102,10 @@ and visible-instance counts. This mode is not the default: the Belgium v8
|
|||||||
experiment showed that unconditional touching-footprint dissolve can merge
|
experiment showed that unconditional touching-footprint dissolve can merge
|
||||||
whole urban blocks and therefore must pass the same independent gates before
|
whole urban blocks and therefore must pass the same independent gates before
|
||||||
it can replace native instances.
|
it can replace native instances.
|
||||||
|
|
||||||
|
The compact-roof variant therefore merges a connected group only when it has
|
||||||
|
at most 12 source footprints and fills at least 55% of its axis-aligned
|
||||||
|
envelope. Larger or irregular connected groups retain their native instances
|
||||||
|
and are marked `native_instance_complex_touch_group`. These fixed criteria
|
||||||
|
prevent administrative row-house chains from becoming one ambiguous detector
|
||||||
|
box while keeping the experiment deterministic and auditable.
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ def _polygonal(geometry: Any) -> Any | None:
|
|||||||
|
|
||||||
|
|
||||||
def merge_touching_roof_instances(features: list[dict[str, Any]], source_name: str) -> list[dict[str, Any]]:
|
def merge_touching_roof_instances(features: list[dict[str, Any]], source_name: str) -> list[dict[str, Any]]:
|
||||||
"""Dissolve only touching/overlapping footprints into imagery-visible roof instances."""
|
"""Dissolve only compact touching groups into imagery-visible roof instances."""
|
||||||
if not features:
|
if not features:
|
||||||
return []
|
return []
|
||||||
source_geometries = [(feature, shape(feature["geometry"])) for feature in features]
|
source_geometries = [(feature, shape(feature["geometry"])) for feature in features]
|
||||||
@@ -105,6 +105,17 @@ def merge_touching_roof_instances(features: list[dict[str, Any]], source_name: s
|
|||||||
if geometry.intersects(component)
|
if geometry.intersects(component)
|
||||||
]
|
]
|
||||||
source_ids = sorted(str(feature["properties"]["source_feature_id"]) for feature in contributors)
|
source_ids = sorted(str(feature["properties"]["source_feature_id"]) for feature in contributors)
|
||||||
|
envelope_area = component.envelope.area
|
||||||
|
fill_ratio = component.area / envelope_area if envelope_area else 0.0
|
||||||
|
# Large connected blocks and irregular chains are administratively
|
||||||
|
# adjacent but not one reliably box-shaped roof target. Preserve their
|
||||||
|
# native instances instead of creating a giant ambiguous detector box.
|
||||||
|
if len(contributors) > 12 or fill_ratio < 0.55:
|
||||||
|
for feature in contributors:
|
||||||
|
retained = json.loads(json.dumps(feature))
|
||||||
|
retained["properties"]["label_semantics"] = "native_instance_complex_touch_group"
|
||||||
|
merged.append(retained)
|
||||||
|
continue
|
||||||
properties = dict(contributors[0]["properties"])
|
properties = dict(contributors[0]["properties"])
|
||||||
properties.update(
|
properties.update(
|
||||||
{
|
{
|
||||||
@@ -112,6 +123,7 @@ def merge_touching_roof_instances(features: list[dict[str, Any]], source_name: s
|
|||||||
"source_feature_ids": source_ids,
|
"source_feature_ids": source_ids,
|
||||||
"source_feature_count": len(source_ids),
|
"source_feature_count": len(source_ids),
|
||||||
"source_feature_id": source_ids[0],
|
"source_feature_id": source_ids[0],
|
||||||
|
"roof_group_fill_ratio": fill_ratio,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
digest = hashlib.sha256(component.normalize().wkb).hexdigest()[:24]
|
digest = hashlib.sha256(component.normalize().wkb).hexdigest()[:24]
|
||||||
|
|||||||
Reference in New Issue
Block a user