feat: scope detection QA to inference coverage
This commit is contained in:
@@ -6,7 +6,7 @@ from uuid import uuid4
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from geoalchemy2.shape import from_shape
|
||||
from shapely.geometry import box
|
||||
from shapely.geometry import Polygon, box
|
||||
|
||||
from app.main import app
|
||||
from app.db.session import get_db
|
||||
@@ -274,3 +274,150 @@ def test_detection_qa_no_match_case_persists_zero_scores() -> None:
|
||||
assert result["precision"] == 0.0
|
||||
assert result["recall"] == 0.0
|
||||
assert result["f1_score"] == 0.0
|
||||
|
||||
|
||||
def _coverage_manifest(tmp_path, dataset_id, bounds=(-1.0, -1.0, 3.0, 3.0)):
|
||||
manifest_path = tmp_path / "manifest.json"
|
||||
manifest_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"source_dataset_id": str(dataset_id),
|
||||
"crs": "EPSG:4326",
|
||||
"tiles": [
|
||||
{
|
||||
"index": 0,
|
||||
"path": "tile_0000.tif",
|
||||
"bounds": list(bounds),
|
||||
"crs": "EPSG:4326",
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return manifest_path
|
||||
|
||||
|
||||
def test_detection_qa_excludes_references_outside_persisted_tile_coverage(tmp_path) -> None:
|
||||
project_id = uuid4()
|
||||
dataset_id = uuid4()
|
||||
reference_dataset_id = uuid4()
|
||||
analysis_run_id = uuid4()
|
||||
manifest_path = _coverage_manifest(tmp_path, dataset_id, bounds=(0.0, 0.0, 1.0, 1.0))
|
||||
detection = _detection(project_id, dataset_id, analysis_run_id, geom=box(0.1, 0.1, 0.9, 0.9))
|
||||
reference_dataset = Dataset(
|
||||
id=reference_dataset_id,
|
||||
project_id=project_id,
|
||||
name="reference.geojson",
|
||||
dataset_type="vector",
|
||||
source="manual",
|
||||
dataset_role="reference",
|
||||
)
|
||||
inside_reference = VectorFeature(
|
||||
id=uuid4(),
|
||||
dataset_id=reference_dataset_id,
|
||||
feature_class="building",
|
||||
geometry=from_shape(box(0.1, 0.1, 0.9, 0.9), srid=4326),
|
||||
)
|
||||
outside_reference = VectorFeature(
|
||||
id=uuid4(),
|
||||
dataset_id=reference_dataset_id,
|
||||
feature_class="building",
|
||||
geometry=from_shape(box(10.0, 10.0, 11.0, 11.0), srid=4326),
|
||||
)
|
||||
db = FakeSession(
|
||||
objects={
|
||||
(AnalysisRun, analysis_run_id): AnalysisRun(
|
||||
id=analysis_run_id,
|
||||
project_id=project_id,
|
||||
dataset_id=dataset_id,
|
||||
analysis_type="detection",
|
||||
status="success",
|
||||
model_name="yolo-configured",
|
||||
parameters_json={
|
||||
"model_id": "yolo-configured",
|
||||
"tile_manifest_path": str(manifest_path),
|
||||
},
|
||||
),
|
||||
(Dataset, reference_dataset_id): reference_dataset,
|
||||
},
|
||||
query_rows={Detection: [detection], VectorFeature: [inside_reference, outside_reference]},
|
||||
)
|
||||
|
||||
result = DetectionService.compare_detections_with_reference(
|
||||
db=db,
|
||||
analysis_run_id=analysis_run_id,
|
||||
reference_dataset_id=reference_dataset_id,
|
||||
iou_threshold=0.5,
|
||||
)
|
||||
|
||||
quality_check = next(item for item in db.added if isinstance(item, QualityCheck))
|
||||
assert result["matches"] == 1
|
||||
assert result["false_negatives"] == 0
|
||||
assert result["reference_feature_count_raw"] == 2
|
||||
assert result["reference_feature_count"] == 1
|
||||
assert result["coverage"]["applied"] is True
|
||||
assert result["coverage"]["reference_excluded_outside_count"] == 1
|
||||
assert quality_check.parameters_json["coverage_policy"] == "persisted_tile_manifest_union"
|
||||
assert quality_check.findings_json["coverage"] == result["coverage"]
|
||||
|
||||
|
||||
def test_detection_qa_reports_box_to_footprint_diagnostic_without_changing_strict_metrics(tmp_path) -> None:
|
||||
project_id = uuid4()
|
||||
dataset_id = uuid4()
|
||||
reference_dataset_id = uuid4()
|
||||
analysis_run_id = uuid4()
|
||||
manifest_path = _coverage_manifest(tmp_path, dataset_id)
|
||||
detection = _detection(project_id, dataset_id, analysis_run_id, geom=box(0.0, 0.0, 2.0, 2.0))
|
||||
l_shaped_footprint = Polygon(
|
||||
[(0.0, 0.0), (2.0, 0.0), (2.0, 0.4), (0.4, 0.4), (0.4, 2.0), (0.0, 2.0), (0.0, 0.0)]
|
||||
)
|
||||
reference_dataset = Dataset(
|
||||
id=reference_dataset_id,
|
||||
project_id=project_id,
|
||||
name="reference.geojson",
|
||||
dataset_type="vector",
|
||||
source="manual",
|
||||
dataset_role="reference",
|
||||
)
|
||||
reference_feature = VectorFeature(
|
||||
id=uuid4(),
|
||||
dataset_id=reference_dataset_id,
|
||||
feature_class="building",
|
||||
geometry=from_shape(l_shaped_footprint, srid=4326),
|
||||
)
|
||||
db = FakeSession(
|
||||
objects={
|
||||
(AnalysisRun, analysis_run_id): AnalysisRun(
|
||||
id=analysis_run_id,
|
||||
project_id=project_id,
|
||||
dataset_id=dataset_id,
|
||||
analysis_type="detection",
|
||||
status="success",
|
||||
model_name="yolo-configured",
|
||||
parameters_json={
|
||||
"model_id": "yolo-configured",
|
||||
"tile_manifest_path": str(manifest_path),
|
||||
},
|
||||
),
|
||||
(Dataset, reference_dataset_id): reference_dataset,
|
||||
},
|
||||
query_rows={Detection: [detection], VectorFeature: [reference_feature]},
|
||||
)
|
||||
|
||||
result = DetectionService.compare_detections_with_reference(
|
||||
db=db,
|
||||
analysis_run_id=analysis_run_id,
|
||||
reference_dataset_id=reference_dataset_id,
|
||||
iou_threshold=0.5,
|
||||
)
|
||||
|
||||
diagnostics = result["box_to_footprint_diagnostics"]
|
||||
quality_check = next(item for item in db.added if isinstance(item, QualityCheck))
|
||||
assert result["matches"] == 0
|
||||
assert result["false_positives"] == 1
|
||||
assert result["false_negatives"] == 1
|
||||
assert diagnostics["diagnostic_only"] is True
|
||||
assert diagnostics["envelope_matches"] == 1
|
||||
assert diagnostics["possible_box_to_footprint_mismatch_count"] == 1
|
||||
assert quality_check.findings_json["box_to_footprint_diagnostics"] == diagnostics
|
||||
|
||||
Reference in New Issue
Block a user