Upgrade async GPU analysis and workbench UX

This commit is contained in:
Jens
2026-08-23 21:50:11 +02:00
parent 4040cbca7b
commit b996986d20
59 changed files with 3999 additions and 274 deletions
+246 -29
View File
@@ -1,7 +1,8 @@
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { segmentationApi } from '../services/api'
import type {
DatasetCreateResponse,
JobRead,
QualityCheckRead,
SegmentationModelCapability,
SegmentationQaResult,
@@ -10,6 +11,12 @@ import type {
SegmentationRunResponse,
} from '../types'
import { formatError } from '../lib/formatError'
import {
analysisRunIdFromSegmentationJob,
completedSegmentationResponse,
SegmentationJobError,
waitForSegmentationJob,
} from '../services/segmentationJob'
interface SegmentationWorkflowOptions {
selectedProjectId: string | null
@@ -19,6 +26,16 @@ interface SegmentationWorkflowOptions {
loadQualityChecks: (projectId?: string | null) => Promise<QualityCheckRead[] | void>
}
function isAbortError(error: unknown): boolean {
return error instanceof Error && error.name === 'AbortError'
}
function abortedError(): Error {
const error = new Error('Het volgen van de segmentatietaak is gestopt')
error.name = 'AbortError'
return error
}
export function useSegmentationWorkflow({
selectedProjectId,
rasterDatasets,
@@ -34,11 +51,14 @@ export function useSegmentationWorkflow({
const [segmentationTileManifestPath, setSegmentationTileManifestPath] = useState('')
const [segmentationConfidenceThreshold, setSegmentationConfidenceThreshold] = useState(0.5)
const [runningSegmentation, setRunningSegmentation] = useState(false)
const [segmentationJob, setSegmentationJob] = useState<JobRead | null>(null)
const [segmentationRunResult, setSegmentationRunResult] = useState<SegmentationRunResponse | null>(null)
const [segmentationRunError, setSegmentationRunError] = useState<string | null>(null)
const [segmentationRuns, setSegmentationRuns] = useState<SegmentationRunRead[]>([])
const [selectedSegmentationRunId, setSelectedSegmentationRunId] = useState('')
const [segmentationItems, setSegmentationItems] = useState<SegmentationRead[]>([])
const [segmentationTotal, setSegmentationTotal] = useState(0)
const [segmentationTruncated, setSegmentationTruncated] = useState(false)
const [segmentationGeoJson, setSegmentationGeoJson] = useState<GeoJSON.FeatureCollection | null>(null)
const [segmentationClassFilter, setSegmentationClassFilter] = useState('')
const [segmentationMinConfidenceFilter, setSegmentationMinConfidenceFilter] = useState(0)
@@ -47,6 +67,41 @@ export function useSegmentationWorkflow({
const [segmentationQaResult, setSegmentationQaResult] = useState<SegmentationQaResult | null>(null)
const [segmentationQaError, setSegmentationQaError] = useState<string | null>(null)
const [runningSegmentationQa, setRunningSegmentationQa] = useState(false)
const activeSegmentationControllerRef = useRef<AbortController | null>(null)
const selectedProjectIdRef = useRef(selectedProjectId)
const segmentationExecutionSequence = useRef(0)
const segmentationRunsRequestSequence = useRef(0)
const segmentationResultsRequestSequence = useRef(0)
const segmentationQaRequestSequence = useRef(0)
selectedProjectIdRef.current = selectedProjectId
useEffect(() => {
activeSegmentationControllerRef.current?.abort()
activeSegmentationControllerRef.current = null
segmentationExecutionSequence.current += 1
segmentationRunsRequestSequence.current += 1
segmentationResultsRequestSequence.current += 1
segmentationQaRequestSequence.current += 1
setSelectedSegmentationDatasetId('')
setSegmentationRuns([])
setSelectedSegmentationRunId('')
setSegmentationItems([])
setSegmentationTotal(0)
setSegmentationTruncated(false)
setSegmentationGeoJson(null)
setSegmentationRunResult(null)
setSegmentationRunError(null)
setSegmentationJob(null)
setRunningSegmentation(false)
setLoadingSegmentationResults(false)
setSegmentationTileManifestPath('')
setSegmentationQaResult(null)
setSegmentationQaError(null)
setRunningSegmentationQa(false)
return () => {
activeSegmentationControllerRef.current?.abort()
}
}, [selectedProjectId])
const selectedSegmentationModel = useMemo(
() => segmentationModels.find((model) => model.model_id === selectedSegmentationModelId) ?? null,
@@ -79,32 +134,54 @@ export function useSegmentationWorkflow({
}
const loadSegmentationRuns = async (projectId = selectedProjectId) => {
const sequence = segmentationRunsRequestSequence.current + 1
segmentationRunsRequestSequence.current = sequence
if (!projectId) {
setSegmentationRuns([])
setSelectedSegmentationRunId('')
return
}
try {
const response = await segmentationApi.listRuns({ project_id: projectId })
if (
segmentationRunsRequestSequence.current !== sequence
|| selectedProjectIdRef.current !== projectId
) return
setSegmentationRuns(response.items)
if (!selectedSegmentationRunId && response.items.length > 0) {
setSelectedSegmentationRunId(response.items[0].id)
}
setSelectedSegmentationRunId((current) => (
response.items.some((run) => run.id === current) ? current : response.items[0]?.id ?? ''
))
} catch (error) {
setSegmentationRunError(formatError(error, 'De segmentatieruns konden niet worden geladen'))
if (
segmentationRunsRequestSequence.current === sequence
&& selectedProjectIdRef.current === projectId
) {
setSegmentationRunError(formatError(error, 'De segmentatieruns konden niet worden geladen'))
}
}
}
const loadSegmentationResults = async (analysisRunId = selectedSegmentationRunId) => {
if (!analysisRunId) {
const sequence = segmentationResultsRequestSequence.current + 1
segmentationResultsRequestSequence.current = sequence
const requestProjectId = selectedProjectIdRef.current
if (!analysisRunId || !requestProjectId) {
setSegmentationItems([])
setSegmentationTotal(0)
setSegmentationTruncated(false)
setSegmentationGeoJson(null)
setLoadingSegmentationResults(false)
return
}
setLoadingSegmentationResults(true)
setSegmentationRunError(null)
setSegmentationItems([])
setSegmentationTotal(0)
setSegmentationTruncated(false)
setSegmentationGeoJson(null)
try {
const params = {
project_id: selectedProjectId ?? '',
project_id: requestProjectId,
class_name: segmentationClassFilter || null,
min_confidence: segmentationMinConfidenceFilter > 0 ? segmentationMinConfidenceFilter : null,
}
@@ -112,12 +189,33 @@ export function useSegmentationWorkflow({
segmentationApi.listSegmentations(analysisRunId, params),
segmentationApi.getRunGeoJson(analysisRunId, params),
])
if (
segmentationResultsRequestSequence.current !== sequence
|| selectedProjectIdRef.current !== requestProjectId
) return
if (segmentationsResponse.items.some((item) => (
item.project_id !== requestProjectId || item.analysis_run_id !== analysisRunId
))) {
throw new Error('De server retourneerde segmentaties uit een andere werkruimte of analyserun')
}
setSegmentationItems(segmentationsResponse.items)
setSegmentationTotal(segmentationsResponse.total)
setSegmentationTruncated(Boolean(segmentationsResponse.truncated))
setSegmentationGeoJson(geoJsonResponse)
} catch (error) {
setSegmentationRunError(formatError(error, 'De segmentatieresultaten konden niet worden geladen'))
if (
segmentationResultsRequestSequence.current === sequence
&& selectedProjectIdRef.current === requestProjectId
) {
setSegmentationRunError(formatError(error, 'De segmentatieresultaten konden niet worden geladen'))
}
} finally {
setLoadingSegmentationResults(false)
if (
segmentationResultsRequestSequence.current === sequence
&& selectedProjectIdRef.current === requestProjectId
) {
setLoadingSegmentationResults(false)
}
}
}
@@ -135,31 +233,109 @@ export function useSegmentationWorkflow({
setSegmentationRunError('Het gekozen segmentatiemodel is niet geconfigureerd')
return
}
if (selectedSegmentationModelId === 'fixture-segmenter') {
setSegmentationRunError('Het fixturemodel is uitsluitend beschikbaar voor expliciete geautomatiseerde tests')
return
}
if (!segmentationTileManifestPath.trim()) {
setSegmentationRunError('Koppel eerst het beeldtegelmanifest van het gekozen rasterbestand')
return
}
if (
(activeSegmentationControllerRef.current && !activeSegmentationControllerRef.current.signal.aborted)
|| segmentationJob?.status === 'queued'
|| segmentationJob?.status === 'running'
) {
setSegmentationRunError('Er wordt al een GPU-segmentatietaak verwerkt. Wacht tot die taak klaar is.')
return
}
const projectId = selectedProjectId
const parameters: Record<string, unknown> = {}
const request = {
project_id: projectId,
dataset_id: datasetId,
model_id: selectedSegmentationModelId,
confidence_threshold: segmentationConfidenceThreshold,
tile_manifest_path: segmentationTileManifestPath.trim() || null,
parameters_json: parameters,
}
const controller = new AbortController()
const executionSequence = segmentationExecutionSequence.current + 1
segmentationExecutionSequence.current = executionSequence
activeSegmentationControllerRef.current = controller
const assertExecutionCurrent = () => {
if (
controller.signal.aborted
|| segmentationExecutionSequence.current !== executionSequence
|| selectedProjectIdRef.current !== projectId
) {
throw abortedError()
}
}
setSegmentationRunError(null)
setSegmentationRunResult(null)
setRunningSegmentation(true)
setSegmentationJob(null)
try {
const parameters =
selectedSegmentationModelId === 'fixture-segmenter'
? { fixture_mode: true, fixture_segmentations: [] }
: {}
const result = await segmentationApi.run({
project_id: selectedProjectId,
dataset_id: datasetId,
model_id: selectedSegmentationModelId,
confidence_threshold: segmentationConfidenceThreshold,
tile_manifest_path: segmentationTileManifestPath.trim() || null,
parameters_json: parameters,
const queuedJob = await segmentationApi.runAsync(request)
assertExecutionCurrent()
setSegmentationJob(queuedJob)
const completedJob = await waitForSegmentationJob({
projectId,
initialJob: queuedJob,
signal: controller.signal,
onStatus: (job) => {
if (
segmentationExecutionSequence.current === executionSequence
&& selectedProjectIdRef.current === projectId
) {
setSegmentationJob(job)
}
},
})
assertExecutionCurrent()
const explicitAnalysisRunId = analysisRunIdFromSegmentationJob(completedJob)
const run = explicitAnalysisRunId
? await segmentationApi.getRun(explicitAnalysisRunId, projectId)
: (await segmentationApi.listRuns({ project_id: projectId, dataset_id: datasetId })).items
.find((candidate) => candidate.job_id === completedJob.id)
assertExecutionCurrent()
if (!run) {
throw new SegmentationJobError(
'De GPU-taak is voltooid, maar de bijbehorende bewaarde segmentatierun ontbreekt.',
'SEGMENTATION_RUN_RESULT_NOT_FOUND',
completedJob.id,
)
}
const result = completedSegmentationResponse(request, completedJob, run)
setSegmentationRunError(null)
setSegmentationRunResult(result)
setSelectedSegmentationRunId(result.analysis_run_id)
await loadSegmentationRuns(selectedProjectId)
await loadSegmentationRuns(projectId)
assertExecutionCurrent()
await loadSegmentationResults(result.analysis_run_id)
await loadProjectData(selectedProjectId)
assertExecutionCurrent()
await loadProjectData(projectId)
} catch (error) {
setSegmentationRunError(formatError(error, 'Segmentation run failed'))
if (
!isAbortError(error)
&& segmentationExecutionSequence.current === executionSequence
&& selectedProjectIdRef.current === projectId
) {
setSegmentationRunError(formatError(error, 'De segmentatie is mislukt'))
}
} finally {
setRunningSegmentation(false)
if (activeSegmentationControllerRef.current === controller) {
activeSegmentationControllerRef.current = null
}
if (
segmentationExecutionSequence.current === executionSequence
&& selectedProjectIdRef.current === projectId
) {
setRunningSegmentation(false)
}
}
}
@@ -172,33 +348,71 @@ export function useSegmentationWorkflow({
setSegmentationQaError('Kies eerst een referentiebron')
return
}
const projectId = selectedProjectIdRef.current
if (!projectId) {
setSegmentationQaError('Kies eerst een werkruimte')
return
}
const analysisRunId = selectedSegmentationRunId
const referenceDatasetId = segmentationReferenceDatasetId
const sequence = segmentationQaRequestSequence.current + 1
segmentationQaRequestSequence.current = sequence
setSegmentationQaError(null)
setSegmentationQaResult(null)
setRunningSegmentationQa(true)
try {
const result = await segmentationApi.compareWithReference(selectedSegmentationRunId, selectedProjectId!, {
reference_dataset_id: segmentationReferenceDatasetId,
const result = await segmentationApi.compareWithReference(analysisRunId, projectId, {
reference_dataset_id: referenceDatasetId,
iou_threshold: qaIouThreshold,
class_name: segmentationClassFilter || null,
min_confidence: segmentationMinConfidenceFilter > 0 ? segmentationMinConfidenceFilter : null,
})
if (
segmentationQaRequestSequence.current !== sequence
|| selectedProjectIdRef.current !== projectId
) return
setSegmentationQaResult(result)
await loadQualityChecks(selectedProjectId)
await loadQualityChecks(projectId)
} catch (error) {
setSegmentationQaError(formatError(error, 'Segmentation QA failed'))
if (
segmentationQaRequestSequence.current === sequence
&& selectedProjectIdRef.current === projectId
) {
setSegmentationQaError(formatError(error, 'De segmentatiecontrole is mislukt'))
}
} finally {
setRunningSegmentationQa(false)
if (
segmentationQaRequestSequence.current === sequence
&& selectedProjectIdRef.current === projectId
) {
setRunningSegmentationQa(false)
}
}
}
const resetSegmentationForProject = () => {
activeSegmentationControllerRef.current?.abort()
activeSegmentationControllerRef.current = null
segmentationExecutionSequence.current += 1
segmentationRunsRequestSequence.current += 1
segmentationResultsRequestSequence.current += 1
segmentationQaRequestSequence.current += 1
setSelectedSegmentationDatasetId('')
setSegmentationRuns([])
setSelectedSegmentationRunId('')
setSegmentationItems([])
setSegmentationTotal(0)
setSegmentationTruncated(false)
setSegmentationGeoJson(null)
setSegmentationRunResult(null)
setSegmentationRunError(null)
setSegmentationJob(null)
setRunningSegmentation(false)
setLoadingSegmentationResults(false)
setSegmentationTileManifestPath('')
setSegmentationQaResult(null)
setSegmentationQaError(null)
setRunningSegmentationQa(false)
}
return {
@@ -211,11 +425,14 @@ export function useSegmentationWorkflow({
segmentationTileManifestPath,
segmentationConfidenceThreshold,
runningSegmentation,
segmentationJob,
segmentationRunResult,
segmentationRunError,
segmentationRuns,
selectedSegmentationRunId,
segmentationItems,
segmentationTotal,
segmentationTruncated,
segmentationGeoJson,
segmentationClassFilter,
segmentationMinConfidenceFilter,