Upgrade async GPU analysis and workbench UX
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user