Files
geointel/backend/tests/test_detection_georeferencing_crs_strictness.py
T
JensandClaude Opus 5 08188005bd correct the tiled inference chain and move runs off the request thread
Tile handling produced results that were wrong before any model quality
question arose:

- orthophoto tiles reached the model through PIL convert("RGB"), which
  truncates the high byte of a 16-bit product and treats a 4-band RGB+NIR
  tile's infrared channel as colour. Tiles are now read with rasterio, the
  visible bands are chosen explicitly, and values are percentile-stretched
  across all three bands together so hue is preserved;
- an object wider than the tile overlap was truncated by both tiles into two
  boxes that barely intersect, so IoU suppression kept both: two false
  positives and one missed footprint per seam building. Suppression now also
  compares overlap against the smaller box, and boxes cut by an interior tile
  edge are dropped in favour of the neighbouring tile's complete view;
- georeferencing fell back to an assumed EPSG:4326 when a manifest carried no
  CRS, producing geometry that renders plausibly in the wrong place. QA
  already refused such a tile; inference now fails closed too.

Segmentation QA scored candidates against every reference feature in the
dataset, so every building outside the inferred tiles counted as a false
negative. It now applies the same persisted tile coverage that detection QA
has always used, including the indexed ST_Intersects prefilter.

Duplicate suppression uses an STRtree instead of the O(n^2) scan, tiles are
predicted in batches of YOLO_BATCH_SIZE (a setting that existed but was never
read), and detection/segmentation runs can be queued through /run-async for a
polling background worker rather than holding an HTTP worker thread for
minutes of GPU work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:32:44 +02:00

74 lines
2.6 KiB
Python

"""Inference must refuse to guess a CRS.
Detection QA rejects a tile without explicit CRS metadata, but the inference
side silently assumed EPSG:4326. That produced geometry that renders as a
plausible polygon in the wrong place, which is worse than a clear failure:
"fail closed" is the stated rule for the runtime.
"""
from __future__ import annotations
import pytest
from app.core.errors import AppError
from app.services.detection_georeferencing import (
pixel_bbox_to_epsg4326_polygon,
pixel_points_to_epsg4326_polygon,
)
from app.services.detection_service import DetectionService
TILE_WITHOUT_CRS = {
"bounds": [4.0, 51.0, 5.0, 52.0],
"pixel_window": [0, 0, 100, 100],
}
def test_manifest_without_crs_is_rejected() -> None:
with pytest.raises(AppError) as exc_info:
DetectionService._require_manifest_crs({"tiles": [{"bounds": [0, 0, 1, 1]}]})
assert exc_info.value.code == "DETECTION_TILE_MANIFEST_INVALID"
def test_manifest_crs_is_read_from_any_of_the_documented_keys() -> None:
assert DetectionService._require_manifest_crs({"crs": "EPSG:31370"}) == "EPSG:31370"
assert DetectionService._require_manifest_crs({"source_crs": "EPSG:31370"}) == "EPSG:31370"
assert DetectionService._require_manifest_crs({"dataset_crs": "EPSG:3812"}) == "EPSG:3812"
def test_bbox_georeferencing_requires_an_explicit_crs() -> None:
with pytest.raises(AppError) as exc_info:
pixel_bbox_to_epsg4326_polygon(bbox=[0.0, 0.0, 10.0, 10.0], tile=TILE_WITHOUT_CRS)
assert exc_info.value.code == "DETECTION_TILE_CRS_REQUIRED"
def test_mask_georeferencing_requires_an_explicit_crs() -> None:
with pytest.raises(AppError) as exc_info:
pixel_points_to_epsg4326_polygon(
points=[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]], tile=TILE_WITHOUT_CRS
)
assert exc_info.value.code == "DETECTION_TILE_CRS_REQUIRED"
def test_explicit_crs_on_the_tile_is_used() -> None:
tile = {**TILE_WITHOUT_CRS, "crs": "EPSG:4326"}
polygon = pixel_bbox_to_epsg4326_polygon(bbox=[0.0, 0.0, 50.0, 50.0], tile=tile)
assert polygon.bounds == pytest.approx((4.0, 51.5, 4.5, 52.0))
def test_projected_bounds_are_reprojected_as_a_whole_rectangle() -> None:
# Lambert 72 around Mol. All four corners must be transformed, otherwise a
# rotated footprint is understated.
bounds = DetectionService._bounds_to_epsg4326([200000.0, 200000.0, 201000.0, 201000.0], "EPSG:31370")
assert bounds is not None
min_x, min_y, max_x, max_y = bounds
assert 4.0 < min_x < 6.0
assert 50.0 < min_y < 52.0
assert max_x > min_x and max_y > min_y