Add guided detection calibration runner
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-08 13:11:02 +02:00
parent 0e182ad69d
commit c0bebd609b
8 changed files with 358 additions and 0 deletions
+137
View File
@@ -21,6 +21,39 @@ interface DetectionWorkflowOptions {
loadQualityChecks: (projectId?: string | null) => Promise<QualityCheckRead[] | void>
}
export interface DetectionCalibrationRunRow {
threshold: number
status: 'queued' | 'running' | 'success' | 'failed'
analysis_run_id?: string | null
job_id?: string | null
quality_check_id?: string | null
detection_count?: number | null
precision?: number | null
recall?: number | null
f1_score?: number | null
false_positives?: number | null
false_negatives?: number | null
message?: string | null
}
function parseCalibrationThresholds(value: string): number[] {
const tokens = value
.split(/[\s,;]+/)
.map((token) => token.trim())
.filter(Boolean)
const thresholds: number[] = []
for (const token of tokens) {
const threshold = Number(token)
if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
return []
}
if (!thresholds.includes(threshold)) {
thresholds.push(threshold)
}
}
return thresholds
}
export function useDetectionWorkflow({
selectedProjectId,
rasterDatasets,
@@ -55,6 +88,10 @@ export function useDetectionWorkflow({
const [yoloPreflight, setYoloPreflight] = useState<YoloPreflightResponse | null>(null)
const [loadingYoloPreflight, setLoadingYoloPreflight] = useState(false)
const [yoloPreflightError, setYoloPreflightError] = useState<string | null>(null)
const [calibrationThresholdText, setCalibrationThresholdText] = useState('0.50 0.25 0.15')
const [runningDetectionCalibration, setRunningDetectionCalibration] = useState(false)
const [detectionCalibrationRows, setDetectionCalibrationRows] = useState<DetectionCalibrationRunRow[]>([])
const [detectionCalibrationError, setDetectionCalibrationError] = useState<string | null>(null)
const loadDetectionModels = async () => {
setLoadingDetectionModels(true)
@@ -204,6 +241,98 @@ export function useDetectionWorkflow({
}
}
const runDetectionCalibration = async () => {
if (!selectedProjectId) {
setDetectionCalibrationError('Select a project before calibration')
return
}
const datasetId = selectedDetectionDatasetId || rasterDatasets[0]?.id
if (!datasetId) {
setDetectionCalibrationError('Select a raster dataset before calibration')
return
}
if (!detectionReferenceDatasetId) {
setDetectionCalibrationError('Select a reference dataset before calibration')
return
}
const selectedModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId)
if (!selectedModel?.configured || selectedDetectionModelId === 'manual-fixture-detector') {
setDetectionCalibrationError('Select a configured non-fixture detection model before calibration')
return
}
if (selectedDetectionModelId === 'yolo-configured' && !detectionTileManifestPath.trim()) {
setDetectionCalibrationError('Configured YOLO calibration requires a tile manifest')
return
}
if (selectedDetectionModelId === 'yolo-configured' && modelAssets.length > 0 && !selectedModelAssetId) {
setDetectionCalibrationError('Select a local model asset before calibration')
return
}
const thresholds = parseCalibrationThresholds(calibrationThresholdText)
if (thresholds.length === 0) {
setDetectionCalibrationError('Provide at least one valid threshold between 0 and 1')
return
}
setDetectionCalibrationError(null)
setDetectionCalibrationRows(thresholds.map((threshold) => ({ threshold, status: 'queued' })))
setRunningDetectionCalibration(true)
try {
for (const threshold of thresholds) {
setDetectionCalibrationRows((rows) =>
rows.map((row) => row.threshold === threshold ? { ...row, status: 'running', message: 'Running detection' } : row),
)
try {
const result = await detectionApi.run({
project_id: selectedProjectId,
dataset_id: datasetId,
model_id: selectedDetectionModelId,
model_asset_id: selectedModelAssetId || null,
confidence_threshold: threshold,
tile_manifest_path: detectionTileManifestPath.trim() || null,
parameters_json: { calibration: true, calibration_thresholds: thresholds },
})
setSelectedDetectionRunId(result.analysis_run_id)
const qa = await detectionApi.compareWithReference(result.analysis_run_id, {
reference_dataset_id: detectionReferenceDatasetId,
iou_threshold: qaIouThreshold,
class_name: detectionClassFilter || null,
min_confidence: null,
})
setDetectionCalibrationRows((rows) =>
rows.map((row) => row.threshold === threshold
? {
...row,
status: 'success',
analysis_run_id: result.analysis_run_id,
job_id: result.job_id,
quality_check_id: qa.quality_check_id,
detection_count: result.detection_count,
precision: qa.precision ?? null,
recall: qa.recall ?? null,
f1_score: qa.f1_score ?? null,
false_positives: qa.false_positives,
false_negatives: qa.false_negatives,
message: result.message,
}
: row),
)
} catch (error) {
const message = formatError(error, `Calibration threshold ${threshold} failed`)
setDetectionCalibrationRows((rows) =>
rows.map((row) => row.threshold === threshold ? { ...row, status: 'failed', message } : row),
)
setDetectionCalibrationError(message)
break
}
}
await loadDetectionRuns(selectedProjectId)
await loadQualityChecks(selectedProjectId)
await loadProjectData(selectedProjectId)
} finally {
setRunningDetectionCalibration(false)
}
}
const resetDetectionForProject = () => {
setSelectedDetectionDatasetId('')
setDetectionRuns([])
@@ -211,6 +340,8 @@ export function useDetectionWorkflow({
setDetectionItems([])
setDetectionGeoJson(null)
setDetectionRunResult(null)
setDetectionCalibrationRows([])
setDetectionCalibrationError(null)
}
return {
@@ -241,12 +372,17 @@ export function useDetectionWorkflow({
yoloPreflight,
loadingYoloPreflight,
yoloPreflightError,
calibrationThresholdText,
runningDetectionCalibration,
detectionCalibrationRows,
detectionCalibrationError,
loadDetectionModels,
loadYoloPreflight,
loadDetectionRuns,
loadDetectionResults,
runDetection,
runDetectionQa,
runDetectionCalibration,
resetDetectionForProject,
setSelectedDetectionDatasetId,
setSelectedDetectionModelId,
@@ -257,5 +393,6 @@ export function useDetectionWorkflow({
setDetectionClassFilter,
setDetectionMinConfidenceFilter,
setDetectionReferenceDatasetId,
setCalibrationThresholdText,
}
}