Detection QA reports a precision/recall curve, average precision and a calibration sweep; segmentation QA reported a single operating point. Both rank their outputs by confidence, so the same view applies, and the asymmetry meant the two panels answered different questions about comparable runs — an inconsistency introduced when detection gained the curve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
229 lines
7.3 KiB
Python
229 lines
7.3 KiB
Python
from __future__ import annotations
|
|
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.db.session import get_db
|
|
from app.schemas import (
|
|
AnalysisQaResponse,
|
|
Envelope,
|
|
GeoJsonFeatureCollection,
|
|
JobRead,
|
|
SegmentationListResponse,
|
|
SegmentationModelsResponse,
|
|
SegmentationQaRequest,
|
|
SegmentationRead,
|
|
SegmentationRunListResponse,
|
|
SegmentationRunRead,
|
|
SegmentationRunRequest,
|
|
SegmentationRunResponse,
|
|
)
|
|
from app.services.model_registry_service import ModelRegistryService
|
|
from app.services.detection_service import DetectionService
|
|
from app.services.segmentation_service import SegmentationService
|
|
from app.utils.response import envelope
|
|
|
|
router = APIRouter(prefix="/segmentation", tags=["segmentation"])
|
|
|
|
|
|
@router.get("/models", response_model=Envelope[SegmentationModelsResponse])
|
|
def list_segmentation_models() -> dict:
|
|
return envelope({"models": [model.model_dump() for model in ModelRegistryService.list_model_capabilities(task_type="segmentation")]})
|
|
|
|
|
|
@router.post("/run", response_model=Envelope[SegmentationRunResponse])
|
|
def run_segmentation(payload: SegmentationRunRequest, db: Session = Depends(get_db)) -> dict:
|
|
result = SegmentationService.run_segmentation(
|
|
db=db,
|
|
project_id=payload.project_id,
|
|
dataset_id=payload.dataset_id,
|
|
model_id=payload.model_id,
|
|
confidence_threshold=payload.confidence_threshold,
|
|
class_filter=payload.class_filter,
|
|
tile_manifest_path=payload.tile_manifest_path,
|
|
parameters_json=payload.parameters_json,
|
|
)
|
|
return envelope(result.model_dump())
|
|
|
|
|
|
@router.post("/run-async", response_model=Envelope[JobRead])
|
|
def queue_segmentation(payload: SegmentationRunRequest, db: Session = Depends(get_db)) -> dict:
|
|
"""Queue a segmentation run for the background worker.
|
|
|
|
Configured segmentation walks the same tile manifest as detection and is
|
|
just as unsuited to running inside the request. Poll ``GET /jobs/{id}``.
|
|
"""
|
|
|
|
job = SegmentationService.enqueue_segmentation(
|
|
db=db,
|
|
project_id=payload.project_id,
|
|
dataset_id=payload.dataset_id,
|
|
model_id=payload.model_id,
|
|
confidence_threshold=payload.confidence_threshold,
|
|
class_filter=payload.class_filter,
|
|
tile_manifest_path=payload.tile_manifest_path,
|
|
parameters_json=payload.parameters_json,
|
|
)
|
|
return envelope(JobRead.model_validate(job).model_dump(mode="json"))
|
|
|
|
|
|
@router.get("/runs", response_model=Envelope[SegmentationRunListResponse])
|
|
def list_segmentation_runs(
|
|
project_id: UUID | None = None,
|
|
dataset_id: UUID | None = None,
|
|
db: Session = Depends(get_db),
|
|
) -> dict:
|
|
return envelope(SegmentationService.list_runs(db, project_id=project_id, dataset_id=dataset_id).model_dump())
|
|
|
|
|
|
@router.get("/runs/{analysis_run_id}", response_model=Envelope[SegmentationRunRead])
|
|
def get_segmentation_run(analysis_run_id: UUID, db: Session = Depends(get_db)) -> dict:
|
|
return envelope(SegmentationService.get_run(db, analysis_run_id).model_dump())
|
|
|
|
|
|
@router.get(
|
|
"/runs/{analysis_run_id}/segmentations",
|
|
response_model=Envelope[SegmentationListResponse],
|
|
)
|
|
def list_segmentation_run_outputs(
|
|
analysis_run_id: UUID,
|
|
dataset_id: UUID | None = None,
|
|
class_name: str | None = None,
|
|
min_confidence: float | None = None,
|
|
limit: int = Query(
|
|
default=DetectionService.DEFAULT_RESULT_LIMIT,
|
|
ge=0,
|
|
le=50_000,
|
|
description="Maximum results to return; 0 returns everything. Highest confidence first.",
|
|
),
|
|
offset: int = Query(default=0, ge=0),
|
|
db: Session = Depends(get_db),
|
|
) -> dict:
|
|
return envelope(
|
|
SegmentationService.list_segmentations(
|
|
db,
|
|
limit=limit,
|
|
offset=offset,
|
|
analysis_run_id=analysis_run_id,
|
|
dataset_id=dataset_id,
|
|
class_name=class_name,
|
|
min_confidence=min_confidence,
|
|
).model_dump()
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/datasets/{dataset_id}/segmentations",
|
|
response_model=Envelope[SegmentationListResponse],
|
|
)
|
|
def list_dataset_segmentations(
|
|
dataset_id: UUID,
|
|
analysis_run_id: UUID | None = None,
|
|
class_name: str | None = None,
|
|
min_confidence: float | None = None,
|
|
limit: int = Query(
|
|
default=DetectionService.DEFAULT_RESULT_LIMIT,
|
|
ge=0,
|
|
le=50_000,
|
|
description="Maximum results to return; 0 returns everything. Highest confidence first.",
|
|
),
|
|
offset: int = Query(default=0, ge=0),
|
|
db: Session = Depends(get_db),
|
|
) -> dict:
|
|
return envelope(
|
|
SegmentationService.list_segmentations(
|
|
db,
|
|
limit=limit,
|
|
offset=offset,
|
|
analysis_run_id=analysis_run_id,
|
|
dataset_id=dataset_id,
|
|
class_name=class_name,
|
|
min_confidence=min_confidence,
|
|
).model_dump()
|
|
)
|
|
|
|
|
|
@router.get("/segmentations/{segmentation_id}", response_model=Envelope[SegmentationRead])
|
|
def get_segmentation(segmentation_id: UUID, db: Session = Depends(get_db)) -> dict:
|
|
return envelope(SegmentationService.get_segmentation(db, segmentation_id).model_dump())
|
|
|
|
|
|
@router.get(
|
|
"/runs/{analysis_run_id}/geojson",
|
|
response_model=Envelope[GeoJsonFeatureCollection],
|
|
)
|
|
def get_segmentation_run_geojson(
|
|
analysis_run_id: UUID,
|
|
class_name: str | None = None,
|
|
min_confidence: float | None = None,
|
|
limit: int = Query(
|
|
default=DetectionService.DEFAULT_RESULT_LIMIT,
|
|
ge=0,
|
|
le=50_000,
|
|
description="Maximum results to return; 0 returns everything. Highest confidence first.",
|
|
),
|
|
db: Session = Depends(get_db),
|
|
) -> dict:
|
|
return envelope(
|
|
SegmentationService.segmentations_to_geojson(
|
|
db,
|
|
limit=limit,
|
|
analysis_run_id=analysis_run_id,
|
|
class_name=class_name,
|
|
min_confidence=min_confidence,
|
|
)
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/datasets/{dataset_id}/geojson",
|
|
response_model=Envelope[GeoJsonFeatureCollection],
|
|
)
|
|
def get_dataset_segmentation_geojson(
|
|
dataset_id: UUID,
|
|
analysis_run_id: UUID | None = None,
|
|
class_name: str | None = None,
|
|
min_confidence: float | None = None,
|
|
limit: int = Query(
|
|
default=DetectionService.DEFAULT_RESULT_LIMIT,
|
|
ge=0,
|
|
le=50_000,
|
|
description="Maximum results to return; 0 returns everything. Highest confidence first.",
|
|
),
|
|
db: Session = Depends(get_db),
|
|
) -> dict:
|
|
return envelope(
|
|
SegmentationService.segmentations_to_geojson(
|
|
db,
|
|
limit=limit,
|
|
analysis_run_id=analysis_run_id,
|
|
dataset_id=dataset_id,
|
|
class_name=class_name,
|
|
min_confidence=min_confidence,
|
|
)
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/runs/{analysis_run_id}/qa/reference",
|
|
response_model=Envelope[AnalysisQaResponse],
|
|
)
|
|
def compare_segmentation_run_with_reference(
|
|
analysis_run_id: UUID,
|
|
payload: SegmentationQaRequest,
|
|
db: Session = Depends(get_db),
|
|
) -> dict:
|
|
return envelope(
|
|
SegmentationService.compare_segmentations_with_reference(
|
|
db=db,
|
|
analysis_run_id=analysis_run_id,
|
|
reference_dataset_id=payload.reference_dataset_id,
|
|
iou_threshold=payload.iou_threshold,
|
|
class_name=payload.class_name,
|
|
min_confidence=payload.min_confidence,
|
|
calibration_thresholds=payload.calibration_thresholds,
|
|
)
|
|
)
|