feat(scope): make Belgium and North Sea operational default
This commit is contained in:
@@ -19,10 +19,16 @@ from app.schemas.segmentation import (
|
||||
SegmentationRunRead,
|
||||
SegmentationRunResponse,
|
||||
)
|
||||
from app.services.detection_georeferencing import pixel_points_to_epsg4326_polygon
|
||||
from app.services.detection_service import DetectionService
|
||||
from app.services.model_registry_service import ModelRegistryService
|
||||
from app.services.qa_service import QaService
|
||||
from app.services.quality_service import QualityService
|
||||
from app.services.segmentation_adapter import FixtureSegmentationAdapter
|
||||
from app.services.segmentation_adapter import (
|
||||
FixtureSegmentationAdapter,
|
||||
SamSegmentationAdapter,
|
||||
YoloSegmentationAdapter,
|
||||
)
|
||||
|
||||
|
||||
class SegmentationService:
|
||||
@@ -41,6 +47,8 @@ class SegmentationService:
|
||||
tile_manifest_path: str | None = None,
|
||||
parameters_json: dict[str, Any] | None = None,
|
||||
settings: Settings | None = None,
|
||||
yolo_seg_adapter_class: type[YoloSegmentationAdapter] = YoloSegmentationAdapter,
|
||||
sam_adapter_class: type[SamSegmentationAdapter] = SamSegmentationAdapter,
|
||||
) -> SegmentationRunResponse:
|
||||
parameters = dict(parameters_json or {})
|
||||
resolved_settings = settings or get_settings()
|
||||
@@ -58,7 +66,13 @@ class SegmentationService:
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
model = ModelRegistryService.get_model_capability(model_id, task_type="segmentation")
|
||||
model = ModelRegistryService.get_model_capability(
|
||||
model_id,
|
||||
settings=resolved_settings,
|
||||
task_type="segmentation",
|
||||
yolo_seg_adapter_class=yolo_seg_adapter_class,
|
||||
sam_adapter_class=sam_adapter_class,
|
||||
)
|
||||
if model is None:
|
||||
raise AppError(code="SEGMENTATION_MODEL_NOT_FOUND", message="Segmentation model not found", status_code=404)
|
||||
if model.model_id == "fixture-segmenter" and parameters.get("fixture_mode") is not True:
|
||||
@@ -67,6 +81,13 @@ class SegmentationService:
|
||||
message="Fixture segmenter requires explicit fixture_mode=true",
|
||||
status_code=400,
|
||||
)
|
||||
configured_model_ids = {resolved_settings.yolo_seg_model_id, resolved_settings.sam_model_id}
|
||||
if model.model_id in configured_model_ids and model.configured and not tile_manifest_path:
|
||||
raise AppError(
|
||||
code="SEGMENTATION_TILE_MANIFEST_REQUIRED",
|
||||
message="Configured segmentation inference requires an existing raster tile manifest path",
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
run_parameters = {
|
||||
"model_id": model.model_id,
|
||||
@@ -100,19 +121,24 @@ class SegmentationService:
|
||||
)
|
||||
|
||||
if model.model_id == "fixture-segmenter":
|
||||
segmentations = SegmentationService._persist_fixture_segmentations(
|
||||
db=db,
|
||||
project_id=project_id,
|
||||
dataset_id=dataset_id,
|
||||
analysis_run=analysis_run,
|
||||
job=job,
|
||||
model_name=model.model_id,
|
||||
model_version=model.version,
|
||||
raw_segmentations=parameters.get("fixture_segmentations"),
|
||||
confidence_threshold=confidence_threshold,
|
||||
class_filter=class_filter or [],
|
||||
settings=resolved_settings,
|
||||
)
|
||||
try:
|
||||
segmentations = SegmentationService._persist_fixture_segmentations(
|
||||
db=db,
|
||||
project_id=project_id,
|
||||
dataset_id=dataset_id,
|
||||
analysis_run=analysis_run,
|
||||
job=job,
|
||||
model_name=model.model_id,
|
||||
model_version=model.version,
|
||||
raw_segmentations=parameters.get("fixture_segmentations"),
|
||||
confidence_threshold=confidence_threshold,
|
||||
class_filter=class_filter or [],
|
||||
settings=resolved_settings,
|
||||
)
|
||||
except Exception as exc:
|
||||
# A rejected fixture payload must never leave the run stuck in "running".
|
||||
SegmentationService._fail_run_after_exception(db, analysis_run, job, exc, fallback_code="SEGMENTATION_INTERNAL_ERROR")
|
||||
raise
|
||||
SegmentationService._mark_success(db, analysis_run, job, segmentation_count=len(segmentations))
|
||||
return SegmentationRunResponse(
|
||||
analysis_run_id=analysis_run.id,
|
||||
@@ -125,8 +151,80 @@ class SegmentationService:
|
||||
message="Fixture segmentations persisted.",
|
||||
)
|
||||
|
||||
if model.model_id in configured_model_ids:
|
||||
try:
|
||||
segmentations, postprocess_summary = SegmentationService._run_configured_segmentation(
|
||||
db=db,
|
||||
project_id=project_id,
|
||||
dataset_id=dataset_id,
|
||||
analysis_run=analysis_run,
|
||||
job=job,
|
||||
model_name=model.model_id,
|
||||
model_version=model.version,
|
||||
tile_manifest_path=tile_manifest_path,
|
||||
confidence_threshold=confidence_threshold,
|
||||
class_filter=class_filter or [],
|
||||
settings=resolved_settings,
|
||||
yolo_seg_adapter_class=yolo_seg_adapter_class,
|
||||
sam_adapter_class=sam_adapter_class,
|
||||
)
|
||||
except AppError as exc:
|
||||
SegmentationService._mark_failed(db, analysis_run, job, code=exc.code, message=exc.message)
|
||||
return SegmentationRunResponse(
|
||||
analysis_run_id=analysis_run.id,
|
||||
job_id=job.id,
|
||||
project_id=project_id,
|
||||
dataset_id=dataset_id,
|
||||
model_id=model.model_id,
|
||||
status="failed",
|
||||
segmentation_count=0,
|
||||
error_code=exc.code,
|
||||
message=exc.message,
|
||||
)
|
||||
except Exception as exc:
|
||||
# An unexpected inference error must never leave the run stuck in "running".
|
||||
SegmentationService._fail_run_after_exception(db, analysis_run, job, exc, fallback_code="SEGMENTATION_INTERNAL_ERROR")
|
||||
raise
|
||||
SegmentationService._mark_success(
|
||||
db,
|
||||
analysis_run,
|
||||
job,
|
||||
segmentation_count=len(segmentations),
|
||||
extra_result=postprocess_summary,
|
||||
)
|
||||
return SegmentationRunResponse(
|
||||
analysis_run_id=analysis_run.id,
|
||||
job_id=job.id,
|
||||
project_id=project_id,
|
||||
dataset_id=dataset_id,
|
||||
model_id=model.model_id,
|
||||
status="success",
|
||||
segmentation_count=len(segmentations),
|
||||
message="Configured segmentation inference persisted georeferenced masks.",
|
||||
)
|
||||
|
||||
SegmentationService._mark_failed(
|
||||
db,
|
||||
analysis_run,
|
||||
job,
|
||||
code="SEGMENTATION_MODEL_UNAVAILABLE",
|
||||
message="Segmentation model is unavailable",
|
||||
)
|
||||
raise AppError(code="SEGMENTATION_MODEL_UNAVAILABLE", message="Segmentation model is unavailable", status_code=503)
|
||||
|
||||
@staticmethod
|
||||
def _fail_run_after_exception(db, analysis_run: AnalysisRun, job: Job, exc: Exception, fallback_code: str) -> None:
|
||||
try:
|
||||
db.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
code = getattr(exc, "code", None) or fallback_code
|
||||
message = getattr(exc, "message", None) or "Unexpected internal error during analysis run"
|
||||
try:
|
||||
SegmentationService._mark_failed(db, analysis_run, job, code=str(code), message=str(message))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_run(db, analysis_run_id: uuid.UUID) -> SegmentationRunRead:
|
||||
run = db.get(AnalysisRun, analysis_run_id)
|
||||
@@ -378,8 +476,10 @@ class SegmentationService:
|
||||
db.refresh(job)
|
||||
|
||||
@staticmethod
|
||||
def _mark_success(db, analysis_run: AnalysisRun, job: Job, segmentation_count: int) -> None:
|
||||
def _mark_success(db, analysis_run: AnalysisRun, job: Job, segmentation_count: int, extra_result: dict[str, Any] | None = None) -> None:
|
||||
result = {"segmentation_count": segmentation_count}
|
||||
if extra_result:
|
||||
result.update(extra_result)
|
||||
analysis_run.status = "success"
|
||||
analysis_run.finished_at = SegmentationService._now()
|
||||
analysis_run.result_json = result
|
||||
@@ -392,6 +492,129 @@ class SegmentationService:
|
||||
db.refresh(analysis_run)
|
||||
db.refresh(job)
|
||||
|
||||
@staticmethod
|
||||
def _run_configured_segmentation(
|
||||
db,
|
||||
project_id: uuid.UUID,
|
||||
dataset_id: uuid.UUID,
|
||||
analysis_run: AnalysisRun,
|
||||
job: Job,
|
||||
model_name: str,
|
||||
model_version: str | None,
|
||||
tile_manifest_path: str | None,
|
||||
confidence_threshold: float,
|
||||
class_filter: list[str],
|
||||
settings: Settings,
|
||||
yolo_seg_adapter_class: type[YoloSegmentationAdapter],
|
||||
sam_adapter_class: type[SamSegmentationAdapter],
|
||||
) -> tuple[list[Segmentation], dict[str, Any]]:
|
||||
manifest = DetectionService._load_tile_manifest(tile_manifest_path, settings.yolo_max_tiles)
|
||||
if model_name == settings.sam_model_id:
|
||||
adapter = sam_adapter_class(settings)
|
||||
model_path = Path(settings.sam_model_path or "").expanduser()
|
||||
else:
|
||||
adapter = yolo_seg_adapter_class(settings)
|
||||
model_path = Path(settings.yolo_seg_model_path or "").expanduser()
|
||||
model = adapter.load_model(model_path)
|
||||
|
||||
allowed_classes = {DetectionService._canonical_class_name(value) for value in class_filter if DetectionService._canonical_class_name(value)}
|
||||
manifest_crs = manifest.get("crs") or manifest.get("source_crs") or manifest.get("dataset_crs") or "EPSG:4326"
|
||||
candidates: list[dict[str, Any]] = []
|
||||
for tile in manifest["tiles"]:
|
||||
tile_path = DetectionService._resolve_tile_path(tile, Path(tile_manifest_path or "").expanduser())
|
||||
for raw in adapter.predict_tile(model, tile_path, confidence_threshold):
|
||||
model_class_name = str(raw.get("class_name") or "").strip()
|
||||
class_name = DetectionService._canonical_class_name(model_class_name)
|
||||
confidence = raw.get("confidence")
|
||||
confidence = float(confidence) if confidence is not None else None
|
||||
if allowed_classes and class_name not in allowed_classes:
|
||||
continue
|
||||
if confidence is not None and confidence < confidence_threshold:
|
||||
continue
|
||||
points = raw.get("points")
|
||||
if not isinstance(points, list) or len(points) < 3:
|
||||
continue
|
||||
geometry = pixel_points_to_epsg4326_polygon(points=points, tile=tile, crs=tile.get("crs") or manifest_crs)
|
||||
properties = dict(raw.get("properties") or {})
|
||||
if model_class_name and model_class_name != class_name:
|
||||
properties.setdefault("model_class_name", model_class_name)
|
||||
candidates.append(
|
||||
{
|
||||
"class_name": class_name,
|
||||
"confidence": confidence if confidence is not None else 0.0,
|
||||
"reported_confidence": confidence,
|
||||
"geometry": geometry,
|
||||
"bbox": raw.get("bbox"),
|
||||
"source_tile_path": str(tile_path),
|
||||
"tile_index": tile.get("index"),
|
||||
"properties": {**properties, "tile_index": tile.get("index")},
|
||||
}
|
||||
)
|
||||
filtered_candidates = DetectionService._suppress_duplicate_candidates(
|
||||
candidates,
|
||||
iou_threshold=float(settings.segmentation_duplicate_iou_threshold),
|
||||
)
|
||||
persisted: list[Segmentation] = []
|
||||
for candidate in filtered_candidates:
|
||||
geometry = candidate["geometry"]
|
||||
if isinstance(geometry, Polygon):
|
||||
geometry = MultiPolygon([geometry])
|
||||
bbox = candidate.get("bbox")
|
||||
bbox_json = None
|
||||
if isinstance(bbox, list) and len(bbox) == 4:
|
||||
bbox_json = {
|
||||
"x_min": float(bbox[0]),
|
||||
"y_min": float(bbox[1]),
|
||||
"x_max": float(bbox[2]),
|
||||
"y_max": float(bbox[3]),
|
||||
}
|
||||
segmentation = Segmentation(
|
||||
id=uuid.uuid4(),
|
||||
project_id=project_id,
|
||||
dataset_id=dataset_id,
|
||||
analysis_run_id=analysis_run.id,
|
||||
job_id=job.id,
|
||||
model_name=model_name,
|
||||
model_version=model_version,
|
||||
class_name=candidate["class_name"],
|
||||
confidence=candidate["reported_confidence"],
|
||||
geometry=from_shape(geometry, srid=4326),
|
||||
bbox_json=bbox_json,
|
||||
area_m2=SegmentationService._geodesic_area_m2(geometry),
|
||||
mask_path=None,
|
||||
source_tile_path=candidate["source_tile_path"],
|
||||
tile_index=candidate["tile_index"] if isinstance(candidate["tile_index"], int) else None,
|
||||
properties_json=candidate["properties"],
|
||||
provenance_json={
|
||||
"inference": "local",
|
||||
"model_id": model_name,
|
||||
"tile_manifest_path": str(Path(tile_manifest_path or "").expanduser()),
|
||||
"tile_index": candidate["tile_index"],
|
||||
"device": settings.yolo_device,
|
||||
},
|
||||
)
|
||||
db.add(segmentation)
|
||||
persisted.append(segmentation)
|
||||
db.commit()
|
||||
for segmentation in persisted:
|
||||
db.refresh(segmentation)
|
||||
return persisted, {
|
||||
"raw_segmentation_count": len(candidates),
|
||||
"suppressed_segmentation_count": len(candidates) - len(filtered_candidates),
|
||||
"duplicate_iou_threshold": float(settings.segmentation_duplicate_iou_threshold),
|
||||
"tile_manifest_path": str(Path(tile_manifest_path or "").expanduser()),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _geodesic_area_m2(geometry: MultiPolygon | Polygon) -> float | None:
|
||||
try:
|
||||
from pyproj import Geod
|
||||
|
||||
area, _ = Geod(ellps="WGS84").geometry_area_perimeter(geometry)
|
||||
return abs(float(area))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _persist_fixture_segmentations(
|
||||
db,
|
||||
|
||||
Reference in New Issue
Block a user