/detection/runs/{id}/detections and its GeoJSON sibling returned every
persisted detection, as did the segmentation equivalents. A regional run holds
tens of thousands, and these are the endpoints the results table and the map
overlay call after every run.
They now take limit and offset, default to 2.000, and report total, limit,
offset and truncated so the complete population stays visible while what is
transferred does not. The GeoJSON responses carry the same window in a
geointel_result_window foreign member.
Rows are ordered by confidence, so a capped overlay draws the strongest
detections rather than an arbitrary slice, and the lab says how many of how
many are being shown rather than silently presenting a page as the whole run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
228 lines
7.3 KiB
Python
228 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,
|
|
)
|
|
)
|