Detection and segmentation run listings returned every run a project had ever produced. Runs accumulate with every analysis while the panel only ever draws the recent ones, so the response grew without bound for no benefit. Both take limit and offset now and report total, limit, offset and truncated, matching the result listings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
235 lines
7.5 KiB
Python
235 lines
7.5 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,
|
|
limit: int = Query(default=DetectionService.DEFAULT_RUN_LIST_LIMIT, ge=0, le=5_000),
|
|
offset: int = Query(default=0, ge=0),
|
|
db: Session = Depends(get_db),
|
|
) -> dict:
|
|
return envelope(
|
|
SegmentationService.list_runs(
|
|
db, project_id=project_id, dataset_id=dataset_id, limit=limit, offset=offset
|
|
).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,
|
|
)
|
|
)
|