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
126 lines
4.3 KiB
Python
126 lines
4.3 KiB
Python
"""Detections that straddle a tile seam must not become two half buildings.
|
|
|
|
Tiling uses a fixed overlap. An object wider than that overlap is truncated by
|
|
both tiles, so the two boxes barely intersect and plain IoU suppression keeps
|
|
them both: two false positives plus one missed footprint for every seam
|
|
building. The suppressor therefore also compares overlap against the smaller
|
|
box, and truncated boxes that sit against an interior tile edge are dropped in
|
|
favour of the neighbouring tile's complete view.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from shapely.geometry import box
|
|
|
|
from app.services.detection_service import DetectionService
|
|
|
|
|
|
def _candidate(name: str, geometry, confidence: float, *, tile_index: int = 0, tile_bounds=None):
|
|
return {
|
|
"class_name": "building",
|
|
"confidence": confidence,
|
|
"geometry": geometry,
|
|
"bbox": [0.0, 0.0, 1.0, 1.0],
|
|
"source_tile_path": f"/tiles/tile_{tile_index:04d}.tif",
|
|
"properties": {"tile_index": tile_index, "name": name},
|
|
"tile_bounds": tile_bounds,
|
|
}
|
|
|
|
|
|
def test_identical_overlapping_predictions_are_still_suppressed() -> None:
|
|
kept = DetectionService._suppress_duplicate_candidates(
|
|
[
|
|
_candidate("a", box(0.0, 0.0, 1.0, 1.0), 0.7),
|
|
_candidate("b", box(0.02, 0.02, 1.02, 1.02), 0.9),
|
|
],
|
|
iou_threshold=0.5,
|
|
)
|
|
|
|
assert [item["properties"]["name"] for item in kept] == ["b"]
|
|
|
|
|
|
def test_a_box_contained_in_a_larger_one_is_suppressed() -> None:
|
|
"""A truncated seam half sits inside the complete box from the next tile."""
|
|
|
|
complete = box(0.0, 0.0, 10.0, 10.0)
|
|
truncated_half = box(0.0, 0.0, 4.0, 10.0) # IoU with ``complete`` is 0.4
|
|
|
|
kept = DetectionService._suppress_duplicate_candidates(
|
|
[
|
|
_candidate("complete", complete, 0.88),
|
|
_candidate("truncated", truncated_half, 0.61),
|
|
],
|
|
iou_threshold=0.5,
|
|
)
|
|
|
|
assert [item["properties"]["name"] for item in kept] == ["complete"]
|
|
|
|
|
|
def test_genuinely_adjacent_buildings_are_both_kept() -> None:
|
|
"""Terraced houses touch but do not contain one another."""
|
|
|
|
kept = DetectionService._suppress_duplicate_candidates(
|
|
[
|
|
_candidate("left", box(0.0, 0.0, 10.0, 10.0), 0.9),
|
|
_candidate("right", box(10.0, 0.0, 20.0, 10.0), 0.85),
|
|
],
|
|
iou_threshold=0.5,
|
|
)
|
|
|
|
assert sorted(item["properties"]["name"] for item in kept) == ["left", "right"]
|
|
|
|
|
|
def test_different_classes_are_never_merged() -> None:
|
|
first = _candidate("a", box(0.0, 0.0, 10.0, 10.0), 0.9)
|
|
second = _candidate("b", box(0.0, 0.0, 10.0, 10.0), 0.8)
|
|
second["class_name"] = "solar_panel"
|
|
|
|
kept = DetectionService._suppress_duplicate_candidates([first, second], iou_threshold=0.5)
|
|
|
|
assert len(kept) == 2
|
|
|
|
|
|
def test_boxes_clipped_by_an_interior_tile_edge_are_dropped() -> None:
|
|
"""The overlapping neighbour tile still sees the whole object."""
|
|
|
|
tile = box(0.0, 0.0, 10.0, 10.0)
|
|
raster = box(0.0, 0.0, 30.0, 10.0)
|
|
|
|
candidates = [
|
|
# Sits against the tile's right edge: truncated by the tile, not real.
|
|
_candidate("edge", box(9.0, 2.0, 10.0, 4.0), 0.8, tile_bounds=tile.bounds),
|
|
# Comfortably inside the tile.
|
|
_candidate("interior", box(2.0, 2.0, 4.0, 4.0), 0.8, tile_bounds=tile.bounds),
|
|
]
|
|
|
|
kept = DetectionService._drop_tile_edge_truncations(
|
|
candidates, raster_bounds=raster.bounds, tolerance=0.001
|
|
)
|
|
|
|
assert [item["properties"]["name"] for item in kept] == ["interior"]
|
|
|
|
|
|
def test_boxes_against_the_raster_edge_are_kept() -> None:
|
|
"""No neighbouring tile exists there, so the box is all the evidence there is."""
|
|
|
|
tile = box(0.0, 0.0, 10.0, 10.0)
|
|
raster = box(0.0, 0.0, 10.0, 10.0)
|
|
|
|
candidates = [_candidate("edge", box(9.0, 2.0, 10.0, 4.0), 0.8, tile_bounds=tile.bounds)]
|
|
|
|
kept = DetectionService._drop_tile_edge_truncations(
|
|
candidates, raster_bounds=raster.bounds, tolerance=0.001
|
|
)
|
|
|
|
assert [item["properties"]["name"] for item in kept] == ["edge"]
|
|
|
|
|
|
def test_edge_filter_keeps_candidates_without_tile_bounds() -> None:
|
|
candidates = [_candidate("unknown", box(2.0, 2.0, 4.0, 4.0), 0.8, tile_bounds=None)]
|
|
|
|
kept = DetectionService._drop_tile_edge_truncations(
|
|
candidates, raster_bounds=(0.0, 0.0, 30.0, 10.0), tolerance=0.001
|
|
)
|
|
|
|
assert len(kept) == 1
|