feat: add map-driven orthophoto analysis
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-15 02:01:03 +02:00
parent 845c4696e7
commit daccd3869a
29 changed files with 1300 additions and 26 deletions
+56 -25
View File
@@ -229,12 +229,18 @@ export function useDetectionWorkflow({
}
}
const executeDetection = async (projectId: string, datasetId: string, manifestPath: string | null) => {
const executeDetection = async (
projectId: string,
datasetId: string,
manifestPath: string | null,
modelId = selectedDetectionModelId,
modelAssetId = selectedModelAssetId,
) => {
const result = await detectionApi.run({
project_id: projectId,
dataset_id: datasetId,
model_id: selectedDetectionModelId,
model_asset_id: selectedModelAssetId || null,
model_id: modelId,
model_asset_id: modelAssetId || null,
confidence_threshold: detectionConfidenceThreshold,
tile_manifest_path: manifestPath,
parameters_json: {},
@@ -303,24 +309,31 @@ export function useDetectionWorkflow({
}
}
const prepareAndRunDetection = async (): Promise<boolean> => {
const prepareAndRunDetection = async (
datasetIdOverride?: string,
modelIdOverride?: string,
): Promise<DetectionRunResponse | null> => {
if (!selectedProjectId) {
setDetectionRunError('De regionale werkruimte is nog niet geladen')
return false
return null
}
const datasetId = selectedDetectionDatasetId || rasterDatasets[0]?.id
const datasetId = datasetIdOverride || selectedDetectionDatasetId || rasterDatasets[0]?.id
if (!datasetId) {
setDetectionRunError('Kies of voeg eerst een gegeorefereerd luchtbeeld toe')
return false
return null
}
const selectedModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId)
if (!selectedModel?.configured || selectedDetectionModelId === 'manual-fixture-detector') {
const effectiveModelId = modelIdOverride || selectedDetectionModelId
const effectiveModelAssetId = effectiveModelId === 'yolo-configured'
? modelAssets.find((asset) => asset.active)?.model_asset_id ?? selectedModelAssetId
: selectedModelAssetId
const selectedModel = detectionModels.find((model) => model.model_id === effectiveModelId)
if (!selectedModel?.configured || effectiveModelId === 'manual-fixture-detector') {
setDetectionRunError(selectedModel?.limitation_message ?? 'Het gekozen analysemodel is niet beschikbaar')
return false
return null
}
if (selectedDetectionModelId === 'yolo-configured' && modelAssets.length > 0 && !selectedModelAssetId) {
if (effectiveModelId === 'yolo-configured' && modelAssets.length > 0 && !effectiveModelAssetId) {
setDetectionRunError('Kies eerst een lokaal modelbestand')
return false
return null
}
setDetectionRunError(null)
@@ -355,7 +368,7 @@ export function useDetectionWorkflow({
setDetectionWorkflowStage('validating')
const preflight = await detectionApi.getYoloPreflight({
tile_manifest_path: manifestPath,
model_asset_id: selectedModelAssetId || null,
model_asset_id: effectiveModelAssetId || null,
})
setYoloPreflight(preflight)
setYoloPreflightError(null)
@@ -370,46 +383,63 @@ export function useDetectionWorkflow({
}
setDetectionWorkflowStage('detecting')
await executeDetection(selectedProjectId, datasetId, manifestPath)
const result = await executeDetection(
selectedProjectId,
datasetId,
manifestPath,
effectiveModelId,
effectiveModelAssetId,
)
setDetectionWorkflowStage('complete')
return true
return result
} catch (error) {
setDetectionRunError(formatError(error, 'De beeldanalyse is mislukt'))
setDetectionWorkflowStage('failed')
return false
return null
} finally {
setRunningDetection(false)
}
}
const runDetectionQa = async () => {
if (!selectedDetectionRunId) {
const compareDetectionRunWithReference = async (
analysisRunId: string,
referenceDatasetId: string,
useCurrentFilters = true,
): Promise<DetectionQaResult | null> => {
if (!analysisRunId) {
setDetectionQaError('Select a detection run')
return
return null
}
if (!detectionReferenceDatasetId) {
if (!referenceDatasetId) {
setDetectionQaError('Select a reference dataset')
return
return null
}
setSelectedDetectionRunId(analysisRunId)
setDetectionReferenceDatasetId(referenceDatasetId)
setDetectionQaError(null)
setDetectionQaResult(null)
setRunningDetectionQa(true)
try {
const result = await detectionApi.compareWithReference(selectedDetectionRunId, {
reference_dataset_id: detectionReferenceDatasetId,
const result = await detectionApi.compareWithReference(analysisRunId, {
reference_dataset_id: referenceDatasetId,
iou_threshold: qaIouThreshold,
class_name: detectionClassFilter || null,
min_confidence: detectionMinConfidenceFilter > 0 ? detectionMinConfidenceFilter : null,
class_name: useCurrentFilters ? detectionClassFilter || null : null,
min_confidence: useCurrentFilters && detectionMinConfidenceFilter > 0 ? detectionMinConfidenceFilter : null,
})
setDetectionQaResult(result)
await loadQualityChecks(selectedProjectId)
return result
} catch (error) {
setDetectionQaError(formatError(error, 'Detection QA failed'))
return null
} finally {
setRunningDetectionQa(false)
}
}
const runDetectionQa = async (): Promise<DetectionQaResult | null> =>
compareDetectionRunWithReference(selectedDetectionRunId, detectionReferenceDatasetId)
const runDetectionCalibration = async () => {
if (!selectedProjectId) {
setDetectionCalibrationError('Select a project before calibration')
@@ -560,6 +590,7 @@ export function useDetectionWorkflow({
runDetection,
uploadDetectionRaster,
prepareAndRunDetection,
compareDetectionRunWithReference,
runDetectionQa,
runDetectionCalibration,
applyDetectionOperatorProfile,
@@ -0,0 +1,121 @@
import { useState } from 'react'
import { datasetsApi } from '../services/api'
import type {
DatasetCreateResponse,
DetectionQaResult,
DetectionRunResponse,
OrthophotoAcquisitionResult,
VectorSelectionBBox,
} from '../types'
import { formatError } from '../lib/formatError'
export type MapOrthophotoAnalysisStage =
| 'idle'
| 'acquiring'
| 'detecting'
| 'validating'
| 'complete'
| 'failed'
interface MapOrthophotoAnalysisOptions {
selectedProjectId: string | null
selectedAreaId: string
datasets: DatasetCreateResponse[]
loadProjectData: (projectId: string) => Promise<unknown>
prepareAndRunDetection: (datasetId?: string) => Promise<DetectionRunResponse | null>
compareDetectionRunWithReference: (
analysisRunId: string,
referenceDatasetId: string,
) => Promise<DetectionQaResult | null>
onAnalysisReady: () => void
}
function findBuildingReference(datasets: DatasetCreateResponse[]): DatasetCreateResponse | null {
return datasets.find(
(dataset) =>
dataset.status === 'ready' &&
dataset.dataset_role === 'reference' &&
dataset.source_name === 'grb' &&
dataset.reference_layer_name === 'buildings',
) ?? null
}
export function useMapOrthophotoAnalysis({
selectedProjectId,
selectedAreaId,
datasets,
loadProjectData,
prepareAndRunDetection,
compareDetectionRunWithReference,
onAnalysisReady,
}: MapOrthophotoAnalysisOptions) {
const [stage, setStage] = useState<MapOrthophotoAnalysisStage>('idle')
const [status, setStatus] = useState('')
const [error, setError] = useState<string | null>(null)
const [lastResult, setLastResult] = useState<OrthophotoAcquisitionResult | null>(null)
const run = async (bbox: VectorSelectionBBox): Promise<boolean> => {
if (!selectedProjectId) {
setError('De regionale werkruimte is nog niet geladen.')
setStage('failed')
return false
}
setError(null)
setLastResult(null)
setStage('acquiring')
setStatus('1/3 Officieel luchtbeeld voor de rechthoek ophalen...')
try {
const job = await datasetsApi.acquireOrthophoto(selectedProjectId, {
bbox,
area_id: selectedAreaId || undefined,
})
const acquisition = job.result_json as unknown as OrthophotoAcquisitionResult | null
const datasetId = job.output_dataset_id || acquisition?.output_dataset_id
if (job.status !== 'success' || !datasetId || !acquisition) {
throw new Error(job.error_message || 'Het officiële luchtbeeld werd niet als dataset bewaard.')
}
setLastResult(acquisition)
await loadProjectData(selectedProjectId)
setStage('detecting')
setStatus('2/3 Lokaal AI-model herkent gebouwen...')
const detection = await prepareAndRunDetection(datasetId)
if (!detection) {
throw new Error('De beeldanalyse stopte. Open Beeldanalyse voor de technische oorzaak.')
}
const reference = findBuildingReference(datasets)
if (reference) {
setStage('validating')
setStatus('3/3 Resultaat vergelijken met officiële GRB-gebouwen...')
const quality = await compareDetectionRunWithReference(detection.analysis_run_id, reference.id)
setStatus(
quality
? `Analyse klaar: ${detection.detection_count.toLocaleString('nl-BE')} gebouwen herkend en gecontroleerd.`
: `Analyse klaar: ${detection.detection_count.toLocaleString('nl-BE')} gebouwen herkend; kwaliteitscontrole kon niet afronden.`,
)
} else {
setStatus(
`Analyse klaar: ${detection.detection_count.toLocaleString('nl-BE')} gebouwen herkend. De GRB-referentielaag ontbreekt voor automatische controle.`,
)
}
setStage('complete')
onAnalysisReady()
return true
} catch (caught) {
setError(formatError(caught, 'De kaartgestuurde beeldanalyse is mislukt'))
setStatus('Analyse gestopt.')
setStage('failed')
return false
}
}
return {
stage,
status,
error,
lastResult,
running: stage === 'acquiring' || stage === 'detecting' || stage === 'validating',
run,
}
}