feat: guide raster building analysis workflow
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { detectionApi } from '../services/api'
|
||||
import { datasetsApi, detectionApi } from '../services/api'
|
||||
import type {
|
||||
DatasetCreateResponse,
|
||||
DetectionModelCapability,
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
DetectionRead,
|
||||
DetectionRunRead,
|
||||
DetectionRunResponse,
|
||||
JobRead,
|
||||
ModelAssetRead,
|
||||
QualityCheckRead,
|
||||
YoloPreflightResponse,
|
||||
@@ -26,6 +27,17 @@ interface DetectionOperatorProfileSelection {
|
||||
confidenceThreshold: number
|
||||
}
|
||||
|
||||
export type DetectionWorkflowStage =
|
||||
| 'idle'
|
||||
| 'uploading'
|
||||
| 'ready'
|
||||
| 'tiling'
|
||||
| 'validating'
|
||||
| 'detecting'
|
||||
| 'loading'
|
||||
| 'complete'
|
||||
| 'failed'
|
||||
|
||||
export interface DetectionCalibrationRunRow {
|
||||
threshold: number
|
||||
status: 'queued' | 'running' | 'success' | 'failed'
|
||||
@@ -59,6 +71,21 @@ function parseCalibrationThresholds(value: string): number[] {
|
||||
return thresholds
|
||||
}
|
||||
|
||||
function tileManifestPathFromJob(job: JobRead): string | null {
|
||||
const manifestPath = job.result_json?.manifest_path
|
||||
return typeof manifestPath === 'string' && manifestPath.trim().length > 0 ? manifestPath.trim() : null
|
||||
}
|
||||
|
||||
function rasterTileCount(metadata: Record<string, unknown>, tileSize: number, overlap: number): number | null {
|
||||
const width = metadata.width
|
||||
const height = metadata.height
|
||||
if (typeof width !== 'number' || typeof height !== 'number' || width <= 0 || height <= 0) {
|
||||
return null
|
||||
}
|
||||
const step = tileSize - overlap
|
||||
return Math.ceil(width / step) * Math.ceil(height / step)
|
||||
}
|
||||
|
||||
export function useDetectionWorkflow({
|
||||
selectedProjectId,
|
||||
rasterDatasets,
|
||||
@@ -97,6 +124,7 @@ export function useDetectionWorkflow({
|
||||
const [runningDetectionCalibration, setRunningDetectionCalibration] = useState(false)
|
||||
const [detectionCalibrationRows, setDetectionCalibrationRows] = useState<DetectionCalibrationRunRow[]>([])
|
||||
const [detectionCalibrationError, setDetectionCalibrationError] = useState<string | null>(null)
|
||||
const [detectionWorkflowStage, setDetectionWorkflowStage] = useState<DetectionWorkflowStage>('idle')
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedDetectionDatasetId && rasterDatasets.length > 0) {
|
||||
@@ -201,6 +229,25 @@ export function useDetectionWorkflow({
|
||||
}
|
||||
}
|
||||
|
||||
const executeDetection = async (projectId: string, datasetId: string, manifestPath: string | null) => {
|
||||
const result = await detectionApi.run({
|
||||
project_id: projectId,
|
||||
dataset_id: datasetId,
|
||||
model_id: selectedDetectionModelId,
|
||||
model_asset_id: selectedModelAssetId || null,
|
||||
confidence_threshold: detectionConfidenceThreshold,
|
||||
tile_manifest_path: manifestPath,
|
||||
parameters_json: {},
|
||||
})
|
||||
setDetectionRunResult(result)
|
||||
setSelectedDetectionRunId(result.analysis_run_id)
|
||||
setDetectionWorkflowStage('loading')
|
||||
await loadDetectionRuns(projectId)
|
||||
await loadDetectionResults(result.analysis_run_id)
|
||||
await loadProjectData(projectId)
|
||||
return result
|
||||
}
|
||||
|
||||
const runDetection = async () => {
|
||||
if (!selectedProjectId) {
|
||||
setDetectionRunError('Select a project first')
|
||||
@@ -214,23 +261,122 @@ export function useDetectionWorkflow({
|
||||
setDetectionRunError(null)
|
||||
setDetectionRunResult(null)
|
||||
setRunningDetection(true)
|
||||
setDetectionWorkflowStage('detecting')
|
||||
try {
|
||||
const result = await detectionApi.run({
|
||||
project_id: selectedProjectId,
|
||||
dataset_id: datasetId,
|
||||
model_id: selectedDetectionModelId,
|
||||
model_asset_id: selectedModelAssetId || null,
|
||||
confidence_threshold: detectionConfidenceThreshold,
|
||||
tile_manifest_path: detectionTileManifestPath.trim() || null,
|
||||
parameters_json: {},
|
||||
})
|
||||
setDetectionRunResult(result)
|
||||
setSelectedDetectionRunId(result.analysis_run_id)
|
||||
await loadDetectionRuns(selectedProjectId)
|
||||
await loadDetectionResults(result.analysis_run_id)
|
||||
await loadProjectData(selectedProjectId)
|
||||
await executeDetection(selectedProjectId, datasetId, detectionTileManifestPath.trim() || null)
|
||||
setDetectionWorkflowStage('complete')
|
||||
} catch (error) {
|
||||
setDetectionRunError(formatError(error, 'Detection run failed'))
|
||||
setDetectionWorkflowStage('failed')
|
||||
} finally {
|
||||
setRunningDetection(false)
|
||||
}
|
||||
}
|
||||
|
||||
const uploadDetectionRaster = async (file: File): Promise<boolean> => {
|
||||
if (!selectedProjectId) {
|
||||
setDetectionRunError('De regionale werkruimte is nog niet geladen')
|
||||
return false
|
||||
}
|
||||
setDetectionRunError(null)
|
||||
setDetectionWorkflowStage('uploading')
|
||||
try {
|
||||
const dataset = await datasetsApi.upload(selectedProjectId, {
|
||||
file,
|
||||
datasetType: 'raster',
|
||||
source: 'user_upload',
|
||||
datasetRole: 'source',
|
||||
sourceName: 'manual',
|
||||
sourceMetadataJson: JSON.stringify({ purpose: 'building_detection' }),
|
||||
provenanceMetadataJson: JSON.stringify({ original_filename: file.name, acquisition: 'explicit_user_upload' }),
|
||||
})
|
||||
setSelectedDetectionDatasetId(dataset.id)
|
||||
setDetectionTileManifestPath('')
|
||||
setDetectionRunResult(null)
|
||||
setDetectionWorkflowStage('ready')
|
||||
await loadProjectData(selectedProjectId)
|
||||
return true
|
||||
} catch (error) {
|
||||
setDetectionRunError(formatError(error, 'Het luchtbeeld kon niet worden toegevoegd'))
|
||||
setDetectionWorkflowStage('failed')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const prepareAndRunDetection = async (): Promise<boolean> => {
|
||||
if (!selectedProjectId) {
|
||||
setDetectionRunError('De regionale werkruimte is nog niet geladen')
|
||||
return false
|
||||
}
|
||||
const datasetId = selectedDetectionDatasetId || rasterDatasets[0]?.id
|
||||
if (!datasetId) {
|
||||
setDetectionRunError('Kies of voeg eerst een gegeorefereerd luchtbeeld toe')
|
||||
return false
|
||||
}
|
||||
const selectedModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId)
|
||||
if (!selectedModel?.configured || selectedDetectionModelId === 'manual-fixture-detector') {
|
||||
setDetectionRunError(selectedModel?.limitation_message ?? 'Het gekozen analysemodel is niet beschikbaar')
|
||||
return false
|
||||
}
|
||||
if (selectedDetectionModelId === 'yolo-configured' && modelAssets.length > 0 && !selectedModelAssetId) {
|
||||
setDetectionRunError('Kies eerst een lokaal modelbestand')
|
||||
return false
|
||||
}
|
||||
|
||||
setDetectionRunError(null)
|
||||
setDetectionRunResult(null)
|
||||
setRunningDetection(true)
|
||||
try {
|
||||
let manifestPath = detectionTileManifestPath.trim()
|
||||
if (!manifestPath) {
|
||||
setDetectionWorkflowStage('tiling')
|
||||
const inspection = await datasetsApi.rasterInspect(selectedProjectId, datasetId)
|
||||
const expectedTileCount = rasterTileCount(inspection.metadata, 512, 64)
|
||||
const maxTiles = yoloPreflight?.max_tiles ?? 256
|
||||
if (expectedTileCount === null) {
|
||||
throw new Error('De afmetingen van het luchtbeeld konden niet veilig worden bepaald')
|
||||
}
|
||||
if (expectedTileCount > maxTiles) {
|
||||
throw new Error(
|
||||
`Dit luchtbeeld zou ${expectedTileCount} beeldtegels maken; het veilige maximum is ${maxTiles}. Knip het beeld eerst tot het gewenste werkgebied.`,
|
||||
)
|
||||
}
|
||||
const tileJob = await datasetsApi.rasterTile(selectedProjectId, datasetId, {
|
||||
tile_size: 512,
|
||||
overlap: 64,
|
||||
})
|
||||
manifestPath = tileManifestPathFromJob(tileJob) ?? ''
|
||||
if (!manifestPath) {
|
||||
throw new Error(tileJob.error_message || 'De tegelvoorbereiding leverde geen geldig manifest op')
|
||||
}
|
||||
setDetectionTileManifestPath(manifestPath)
|
||||
}
|
||||
|
||||
setDetectionWorkflowStage('validating')
|
||||
const preflight = await detectionApi.getYoloPreflight({
|
||||
tile_manifest_path: manifestPath,
|
||||
model_asset_id: selectedModelAssetId || null,
|
||||
})
|
||||
setYoloPreflight(preflight)
|
||||
setYoloPreflightError(null)
|
||||
if (
|
||||
!preflight.checks.manifest_valid ||
|
||||
!preflight.checks.tile_paths_exist ||
|
||||
!preflight.checks.tile_limit_ok ||
|
||||
!preflight.checks.dependencies_available ||
|
||||
!preflight.checks.model_file_exists
|
||||
) {
|
||||
throw new Error(preflight.message || 'De beeldtegels of modelruntime zijn niet startklaar')
|
||||
}
|
||||
|
||||
setDetectionWorkflowStage('detecting')
|
||||
await executeDetection(selectedProjectId, datasetId, manifestPath)
|
||||
setDetectionWorkflowStage('complete')
|
||||
return true
|
||||
} catch (error) {
|
||||
setDetectionRunError(formatError(error, 'De beeldanalyse is mislukt'))
|
||||
setDetectionWorkflowStage('failed')
|
||||
return false
|
||||
} finally {
|
||||
setRunningDetection(false)
|
||||
}
|
||||
@@ -371,6 +517,7 @@ export function useDetectionWorkflow({
|
||||
setDetectionRunResult(null)
|
||||
setDetectionCalibrationRows([])
|
||||
setDetectionCalibrationError(null)
|
||||
setDetectionWorkflowStage('idle')
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -405,11 +552,14 @@ export function useDetectionWorkflow({
|
||||
runningDetectionCalibration,
|
||||
detectionCalibrationRows,
|
||||
detectionCalibrationError,
|
||||
detectionWorkflowStage,
|
||||
loadDetectionModels,
|
||||
loadYoloPreflight,
|
||||
loadDetectionRuns,
|
||||
loadDetectionResults,
|
||||
runDetection,
|
||||
uploadDetectionRaster,
|
||||
prepareAndRunDetection,
|
||||
runDetectionQa,
|
||||
runDetectionCalibration,
|
||||
applyDetectionOperatorProfile,
|
||||
|
||||
Reference in New Issue
Block a user