Add guided detection calibration runner
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user