460 lines
17 KiB
TypeScript
460 lines
17 KiB
TypeScript
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
import { segmentationApi } from '../services/api'
|
|
import type {
|
|
DatasetCreateResponse,
|
|
JobRead,
|
|
QualityCheckRead,
|
|
SegmentationModelCapability,
|
|
SegmentationQaResult,
|
|
SegmentationRead,
|
|
SegmentationRunRead,
|
|
SegmentationRunResponse,
|
|
} from '../types'
|
|
import { formatError } from '../lib/formatError'
|
|
import {
|
|
analysisRunIdFromSegmentationJob,
|
|
completedSegmentationResponse,
|
|
SegmentationJobError,
|
|
waitForSegmentationJob,
|
|
} from '../services/segmentationJob'
|
|
|
|
interface SegmentationWorkflowOptions {
|
|
selectedProjectId: string | null
|
|
rasterDatasets: DatasetCreateResponse[]
|
|
qaIouThreshold: number
|
|
loadProjectData: (projectId: string) => Promise<unknown>
|
|
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,
|
|
qaIouThreshold,
|
|
loadProjectData,
|
|
loadQualityChecks,
|
|
}: SegmentationWorkflowOptions) {
|
|
const [segmentationModels, setSegmentationModels] = useState<SegmentationModelCapability[]>([])
|
|
const [loadingSegmentationModels, setLoadingSegmentationModels] = useState(false)
|
|
const [segmentationModelError, setSegmentationModelError] = useState<string | null>(null)
|
|
const [selectedSegmentationDatasetId, setSelectedSegmentationDatasetId] = useState('')
|
|
const [selectedSegmentationModelId, setSelectedSegmentationModelId] = useState('segmentation-placeholder')
|
|
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)
|
|
const [loadingSegmentationResults, setLoadingSegmentationResults] = useState(false)
|
|
const [segmentationReferenceDatasetId, setSegmentationReferenceDatasetId] = useState('')
|
|
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,
|
|
[segmentationModels, selectedSegmentationModelId],
|
|
)
|
|
|
|
const loadSegmentationModels = async () => {
|
|
setLoadingSegmentationModels(true)
|
|
setSegmentationModelError(null)
|
|
try {
|
|
const response = await segmentationApi.listModels()
|
|
setSegmentationModels(response.models)
|
|
const selectionStillAvailable = response.models.some((model) => model.model_id === selectedSegmentationModelId)
|
|
const selectionConfigured = response.models.some(
|
|
(model) => model.model_id === selectedSegmentationModelId && model.configured,
|
|
)
|
|
if ((!selectionStillAvailable || !selectionConfigured) && response.models.length > 0) {
|
|
const configuredModel = response.models.find(
|
|
(model) => model.configured && model.model_id !== 'fixture-segmenter',
|
|
)
|
|
setSelectedSegmentationModelId(
|
|
configuredModel?.model_id ?? (selectionStillAvailable ? selectedSegmentationModelId : response.models[0].model_id),
|
|
)
|
|
}
|
|
} catch (error) {
|
|
setSegmentationModelError(formatError(error, 'De segmentatiemodellen konden niet worden geladen'))
|
|
} finally {
|
|
setLoadingSegmentationModels(false)
|
|
}
|
|
}
|
|
|
|
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)
|
|
setSelectedSegmentationRunId((current) => (
|
|
response.items.some((run) => run.id === current) ? current : response.items[0]?.id ?? ''
|
|
))
|
|
} catch (error) {
|
|
if (
|
|
segmentationRunsRequestSequence.current === sequence
|
|
&& selectedProjectIdRef.current === projectId
|
|
) {
|
|
setSegmentationRunError(formatError(error, 'De segmentatieruns konden niet worden geladen'))
|
|
}
|
|
}
|
|
}
|
|
|
|
const loadSegmentationResults = async (analysisRunId = selectedSegmentationRunId) => {
|
|
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: requestProjectId,
|
|
class_name: segmentationClassFilter || null,
|
|
min_confidence: segmentationMinConfidenceFilter > 0 ? segmentationMinConfidenceFilter : null,
|
|
}
|
|
const [segmentationsResponse, geoJsonResponse] = await Promise.all([
|
|
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) {
|
|
if (
|
|
segmentationResultsRequestSequence.current === sequence
|
|
&& selectedProjectIdRef.current === requestProjectId
|
|
) {
|
|
setSegmentationRunError(formatError(error, 'De segmentatieresultaten konden niet worden geladen'))
|
|
}
|
|
} finally {
|
|
if (
|
|
segmentationResultsRequestSequence.current === sequence
|
|
&& selectedProjectIdRef.current === requestProjectId
|
|
) {
|
|
setLoadingSegmentationResults(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
const runSegmentation = async () => {
|
|
if (!selectedProjectId) {
|
|
setSegmentationRunError('Kies eerst een werkruimte')
|
|
return
|
|
}
|
|
const datasetId = selectedSegmentationDatasetId || rasterDatasets[0]?.id
|
|
if (!datasetId) {
|
|
setSegmentationRunError('Kies eerst een rasterbron')
|
|
return
|
|
}
|
|
if (!selectedSegmentationModel?.configured) {
|
|
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 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(projectId)
|
|
assertExecutionCurrent()
|
|
await loadSegmentationResults(result.analysis_run_id)
|
|
assertExecutionCurrent()
|
|
await loadProjectData(projectId)
|
|
} catch (error) {
|
|
if (
|
|
!isAbortError(error)
|
|
&& segmentationExecutionSequence.current === executionSequence
|
|
&& selectedProjectIdRef.current === projectId
|
|
) {
|
|
setSegmentationRunError(formatError(error, 'De segmentatie is mislukt'))
|
|
}
|
|
} finally {
|
|
if (activeSegmentationControllerRef.current === controller) {
|
|
activeSegmentationControllerRef.current = null
|
|
}
|
|
if (
|
|
segmentationExecutionSequence.current === executionSequence
|
|
&& selectedProjectIdRef.current === projectId
|
|
) {
|
|
setRunningSegmentation(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
const runSegmentationQa = async () => {
|
|
if (!selectedSegmentationRunId) {
|
|
setSegmentationQaError('Kies eerst een segmentatierun')
|
|
return
|
|
}
|
|
if (!segmentationReferenceDatasetId) {
|
|
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(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(projectId)
|
|
} catch (error) {
|
|
if (
|
|
segmentationQaRequestSequence.current === sequence
|
|
&& selectedProjectIdRef.current === projectId
|
|
) {
|
|
setSegmentationQaError(formatError(error, 'De segmentatiecontrole is mislukt'))
|
|
}
|
|
} finally {
|
|
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 {
|
|
segmentationModels,
|
|
loadingSegmentationModels,
|
|
segmentationModelError,
|
|
selectedSegmentationDatasetId,
|
|
selectedSegmentationModelId,
|
|
selectedSegmentationModel,
|
|
segmentationTileManifestPath,
|
|
segmentationConfidenceThreshold,
|
|
runningSegmentation,
|
|
segmentationJob,
|
|
segmentationRunResult,
|
|
segmentationRunError,
|
|
segmentationRuns,
|
|
selectedSegmentationRunId,
|
|
segmentationItems,
|
|
segmentationTotal,
|
|
segmentationTruncated,
|
|
segmentationGeoJson,
|
|
segmentationClassFilter,
|
|
segmentationMinConfidenceFilter,
|
|
loadingSegmentationResults,
|
|
segmentationReferenceDatasetId,
|
|
segmentationQaResult,
|
|
segmentationQaError,
|
|
runningSegmentationQa,
|
|
loadSegmentationModels,
|
|
loadSegmentationRuns,
|
|
loadSegmentationResults,
|
|
runSegmentation,
|
|
runSegmentationQa,
|
|
resetSegmentationForProject,
|
|
setSelectedSegmentationDatasetId,
|
|
setSelectedSegmentationModelId,
|
|
setSegmentationTileManifestPath,
|
|
setSegmentationConfidenceThreshold,
|
|
setSelectedSegmentationRunId,
|
|
setSegmentationClassFilter,
|
|
setSegmentationMinConfidenceFilter,
|
|
setSegmentationReferenceDatasetId,
|
|
}
|
|
}
|