Files
geointel/frontend/src/components/segmentation/SegmentationLab.tsx
T

530 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
DatasetCreateResponse,
JobRead,
SegmentationModelCapability,
SegmentationQaResult,
SegmentationRead,
SegmentationRunRead,
SegmentationRunResponse,
} from '../../types'
import { ModelSelector } from '../models/ModelSelector'
import {
analysisModelAvailabilityMessage,
analysisModelDisplayName,
toAnalysisModelOption,
} from '../models/modelOptions'
import { getDatasetSelectionLabel } from '../../lib/datasetDisplay'
interface SegmentationLabProps {
managementLocked?: boolean
blockedInferenceDatasetCount?: number
segmentationModels: SegmentationModelCapability[]
loadingSegmentationModels: boolean
segmentationModelError: string | null
selectedSegmentationDatasetId: string
selectedSegmentationModelId: string
segmentationTileManifestPath: string
segmentationConfidenceThreshold: number
runningSegmentation: boolean
segmentationJob: JobRead | null
segmentationRunResult: SegmentationRunResponse | null
segmentationRunError: string | null
segmentationRuns: SegmentationRunRead[]
selectedSegmentationRunId: string
segmentationItems: SegmentationRead[]
segmentationTotal: number
segmentationTruncated: boolean
segmentationClassFilter: string
segmentationMinConfidenceFilter: number
loadingSegmentationResults: boolean
segmentationReferenceDatasetId: string
segmentationQaResult: SegmentationQaResult | null
segmentationQaError: string | null
runningSegmentationQa: boolean
selectedProjectId: string | null
rasterDatasets: DatasetCreateResponse[]
referenceDatasets: DatasetCreateResponse[]
selectedSegmentationModelConfigured: boolean
selectedSegmentationModelLimitation: string | null
onLoadModels: () => void
onSelectDataset: (datasetId: string) => void
onSelectModel: (modelId: string) => void
onSetTileManifestPath: (value: string) => void
onSetConfidenceThreshold: (value: number) => void
onRunSegmentation: () => void
onLoadRuns: () => void
onSelectRun: (runId: string) => void
onSetClassFilter: (value: string) => void
onSetMinConfidenceFilter: (value: number) => void
onLoadResults: () => void
onSelectReferenceDataset: (datasetId: string) => void
onRunQa: () => void
}
function segmentationRunLabel(run: SegmentationRunRead): string {
const timestamp = run.finished_at ?? run.created_at
const dateLabel = timestamp
? new Intl.DateTimeFormat('nl-BE', { dateStyle: 'short', timeStyle: 'short' }).format(new Date(timestamp))
: 'datum onbekend'
return `${persistedSegmentationModelLabel(run.model_name)} · ${analysisStatusLabel(run.status)} · ${dateLabel}`
}
function analysisStatusLabel(status: string): string {
if (status === 'completed') return 'afgerond'
if (status === 'success') return 'geslaagd'
if (status === 'failed') return 'mislukt'
if (status === 'running') return 'bezig'
if (status === 'queued') return 'in wachtrij'
return status.replace(/_/g, ' ')
}
function persistedSegmentationModelLabel(modelName: string | null | undefined): string {
if (!modelName) return 'Segmentatie'
if (modelName === 'fixture-segmenter') return 'Testsegmentatie'
if (modelName === 'sam-placeholder') return 'SAM-model niet geconfigureerd'
if (modelName === 'yolo-seg-placeholder') return 'YOLO-segmentatie niet geconfigureerd'
if (modelName === 'segmentation-placeholder') return 'Segmentatiemodel niet geconfigureerd'
if (modelName === 'yolo-seg-configured') return 'Lokaal YOLO-segmentatiemodel'
if (modelName === 'sam-configured') return 'Lokaal SAM-model'
return modelName
}
function segmentationClassLabel(className: string): string {
if (className.toLowerCase() === 'building') return 'Gebouw'
return className
}
export function SegmentationLab({
managementLocked = false,
blockedInferenceDatasetCount = 0,
segmentationModels,
loadingSegmentationModels,
segmentationModelError,
selectedSegmentationDatasetId,
selectedSegmentationModelId,
segmentationTileManifestPath,
segmentationConfidenceThreshold,
runningSegmentation,
segmentationJob,
segmentationRunResult,
segmentationRunError,
segmentationRuns,
selectedSegmentationRunId,
segmentationItems,
segmentationTotal,
segmentationTruncated,
segmentationClassFilter,
segmentationMinConfidenceFilter,
loadingSegmentationResults,
segmentationReferenceDatasetId,
segmentationQaResult,
segmentationQaError,
runningSegmentationQa,
selectedProjectId,
rasterDatasets,
referenceDatasets,
selectedSegmentationModelConfigured,
selectedSegmentationModelLimitation,
onLoadModels,
onSelectDataset,
onSelectModel,
onSetTileManifestPath,
onSetConfidenceThreshold,
onRunSegmentation,
onLoadRuns,
onSelectRun,
onSetClassFilter,
onSetMinConfidenceFilter,
onLoadResults,
onSelectReferenceDataset,
onRunQa,
}: SegmentationLabProps): JSX.Element {
const segmentationHasDataset = selectedSegmentationDatasetId.length > 0
const segmentationHasTileManifest = segmentationTileManifestPath.trim().length > 0
const segmentationModelUiRunnable =
selectedSegmentationModelConfigured && selectedSegmentationModelId !== 'fixture-segmenter'
const selectedSegmentationModel = segmentationModels.find(
(model) => model.model_id === selectedSegmentationModelId,
) ?? null
const selectedSegmentationModelAvailability = selectedSegmentationModel
? analysisModelAvailabilityMessage(selectedSegmentationModel)
: selectedSegmentationModelLimitation ?? 'Het gekozen segmentatiemodel is niet geconfigureerd'
const segmentationRunReady =
Boolean(selectedProjectId) && segmentationHasDataset && segmentationModelUiRunnable
const segmentationJobActive = segmentationJob?.status === 'queued' || segmentationJob?.status === 'running'
const segmentationRunBlockedReason = !selectedProjectId
? 'Kies eerst een werkruimte'
: !segmentationHasDataset
? 'Kies een rasterbestand'
: selectedSegmentationModelId === 'fixture-segmenter'
? 'Het fixturemodel is alleen bedoeld voor expliciete tests en demos'
: !selectedSegmentationModelConfigured
? selectedSegmentationModelAvailability
: null
return (
<section className="workspace-panel ai-lab-shell segmentation-lab-shell">
<div className="panel-title-row">
<div>
<p className="eyebrow">Vlakken herkennen in beeld</p>
<h2>Segmentatie</h2>
</div>
<button className="secondary-action" type="button" onClick={onLoadModels} disabled={loadingSegmentationModels}>
Status vernieuwen
</button>
</div>
<details className="ai-lab-model-surface" aria-label="Technische informatie over segmentatiemodellen">
<summary>
<span>Technische modelinformatie</span>
<strong>{segmentationModels.length} modellen</strong>
</summary>
<div className="ai-lab-disclosure-body">
<div className="ai-lab-state-stack">
{loadingSegmentationModels ? (
<div className="result-state result-state-loading">
<strong>Segmentatiemodellen worden gecontroleerd.</strong>
<p>GeoIntel leest de modelregistratie van de backend.</p>
</div>
) : null}
{segmentationModelError ? (
<div className="result-state result-state-error">
<strong>De modelregistratie voor segmentatie is niet bereikbaar.</strong>
<p>{segmentationModelError}</p>
</div>
) : null}
{segmentationModels.length === 0 && !loadingSegmentationModels ? (
<div className="result-state result-state-empty">
<strong>De backend meldt geen segmentatiemodellen.</strong>
<p>Vernieuw de status zodra de backend opnieuw bereikbaar is.</p>
</div>
) : null}
</div>
<ul className="model-list">
{segmentationModels.map((model) => (
<li className={model.configured ? 'model-card model-card-ready' : 'model-card'} key={model.model_id}>
<strong>{analysisModelDisplayName(model)}</strong>
<span className={model.configured ? 'status-badge status-badge-ready' : 'status-badge'}>{model.configured ? 'gereed' : 'niet geconfigureerd'}</span>
<p className="muted">Ondersteunde klassen: {model.supported_classes.join(', ') || 'niet opgegeven'}</p>
<p className="muted">{analysisModelAvailabilityMessage(model)}</p>
<details className="technical-inline-details">
<summary>Technische identificatie</summary>
<div className="entity-meta">
<span>Model-ID: {model.model_id}</span>
<span>Framework: {model.framework}</span>
<span>Taaktype: {model.task_type}</span>
</div>
</details>
</li>
))}
</ul>
</div>
</details>
<div className="lab-block">
<div className="ai-lab-run-surface" aria-label="Segmentatie starten">
<h3>Nieuwe segmentatie</h3>
<div
className={segmentationRunReady ? 'lab-readiness-panel lab-readiness-panel-ready' : 'lab-readiness-panel'}
aria-label="Startklaar voor segmentatie"
>
<div className="ai-lab-section-header">
<div>
<h3>Wat is nog nodig?</h3>
<p>GeoIntel controleert het raster en model voordat de verwerking start.</p>
</div>
<span className={segmentationRunReady ? 'status-badge status-badge-ready' : 'status-badge'}>
{segmentationRunReady ? 'Klaar om te starten' : 'Nog niet startklaar'}
</span>
</div>
<div className="lab-readiness-grid">
<div className={segmentationHasDataset ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>Rasterbestand</span>
<strong>{segmentationHasDataset ? 'Geselecteerd' : 'Kies een rasterbestand'}</strong>
</div>
<div className={segmentationModelUiRunnable ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>Analysemodel</span>
<strong>
{selectedSegmentationModelId === 'fixture-segmenter'
? 'Alleen beschikbaar voor geautomatiseerde tests'
: selectedSegmentationModelConfigured
? 'Het gekozen model is beschikbaar'
: selectedSegmentationModelAvailability}
</strong>
</div>
<div className={segmentationHasDataset ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>Beeldtegels</span>
<strong>{segmentationHasTileManifest ? 'Technisch manifest gekoppeld' : segmentationHasDataset ? 'Worden automatisch voorbereid' : 'Wachten op een rasterbestand'}</strong>
</div>
</div>
</div>
<div className={segmentationRunReady ? 'lab-action-guardrail lab-action-guardrail-ready' : 'lab-action-guardrail'}>
<span>Analyse</span>
<strong>{segmentationRunReady ? 'Klaar om segmentatie te starten' : segmentationRunBlockedReason}</strong>
</div>
{rasterDatasets.length === 0 ? (
<div className="result-state result-state-empty">
<strong>Geen rasterbestand beschikbaar voor segmentatie.</strong>
{blockedInferenceDatasetCount > 0 ? (
<p>{blockedInferenceDatasetCount} rasterbestand(en) zijn vooraf uitgesloten door fixture-, quarantaine-, validatie- of provenancegegevens. De volledige servergate blijft beslissend.</p>
) : null}
<p>{managementLocked
? 'Ga naar Kaart en maak een begrensde officiële bronselectie. De synthetische demo-context blijft bewust uitgesloten van geconfigureerde modelinference.'
: 'Voeg eerst een gevalideerd rasterbestand toe onder Bronnen of haal via Kaart een begrensde officiële bron op.'}</p>
</div>
) : null}
<div className="lab-form-grid">
<label>
Rasterbestand
<select
value={selectedSegmentationDatasetId}
onChange={(event) => {
onSelectDataset(event.target.value)
onSetTileManifestPath('')
}}
>
<option value="">Kies een rasterbestand</option>
{rasterDatasets.map((dataset) => (
<option key={dataset.id} value={dataset.id}>
{getDatasetSelectionLabel(dataset)}
</option>
))}
</select>
</label>
<ModelSelector
label="Analysemodel"
value={selectedSegmentationModelId}
options={segmentationModels.map(toAnalysisModelOption)}
onChange={onSelectModel}
loading={loadingSegmentationModels}
error={segmentationModelError}
onRefresh={onLoadModels}
/>
<label>
Minimale zekerheid
<input
type="number"
min="0"
max="1"
step="0.05"
value={segmentationConfidenceThreshold}
onChange={(event) => onSetConfidenceThreshold(Number(event.target.value))}
/>
</label>
</div>
{!managementLocked ? <details className="technical-inline-details">
<summary>Technische beeldtegelinstelling</summary>
<label>
Beeldtegelmanifest
<input
type="text"
placeholder="Pad naar het beeldtegelmanifest"
value={segmentationTileManifestPath}
onChange={(event) => onSetTileManifestPath(event.target.value)}
/>
</label>
</details> : null}
<button
className="primary-action"
type="button"
onClick={onRunSegmentation}
disabled={runningSegmentation || segmentationJobActive || !segmentationRunReady}
>
{segmentationJob?.status === 'queued'
? 'Wachten op NVIDIA GPU…'
: runningSegmentation
? segmentationHasTileManifest ? 'GPU-segmentatie wordt verwerkt…' : 'Beeldtegels voorbereiden…'
: 'Segmentatie starten'}
</button>
</div>
</div>
<div className="ai-lab-state-stack">
{segmentationJobActive ? (
<div className="result-state result-state-loading" role="status" aria-live="polite">
<strong>{segmentationJob?.status === 'queued' ? 'GPU-taak staat in de wachtrij.' : 'GPU-segmentatie wordt uitgevoerd.'}</strong>
<p>
{segmentationJob?.status === 'queued'
? 'De server start de taak zodra de NVIDIA-worker beschikbaar is.'
: 'GeoIntel volgt de servertaak en toont na voltooiing alleen de werkelijk bewaarde polygonen.'}
</p>
<span className="muted">Taak-ID: {segmentationJob?.id}</span>
</div>
) : null}
{!selectedSegmentationModelConfigured ? (
<div className="result-state result-state-empty">
<strong>Het segmentatiemodel is nog niet gereed.</strong>
<p>{selectedSegmentationModelAvailability}</p>
</div>
) : null}
{segmentationRunError ? (
<div className="result-state result-state-error" role="alert">
<strong>De segmentatie is mislukt.</strong>
<p>{segmentationRunError}</p>
</div>
) : null}
{segmentationRunResult ? (
<div className={segmentationRunResult.segmentation_count === 0 ? 'result-state result-state-warning' : 'result-summary-card'} role="status">
<p>Status: {analysisStatusLabel(segmentationRunResult.status)}</p>
<p>{segmentationRunResult.message}</p>
<p>Herkende vlakken: {segmentationRunResult.segmentation_count}</p>
{segmentationRunResult.error_code ? <p className="error">Code: {segmentationRunResult.error_code}</p> : null}
<details className="technical-inline-details">
<summary>Technische verwerking</summary>
<div className="entity-meta">
<span>Analyserun-ID: {segmentationRunResult.analysis_run_id}</span>
<span>Taak-ID: {segmentationRunResult.job_id}</span>
</div>
</details>
</div>
) : null}
</div>
<div className="ai-lab-results-surface" aria-label="Segmentatieresultaten">
<div className="panel-title-row">
<div>
<h3>Herkende vlakken</h3>
<p className="muted">Bekijk bewaarde resultaten en filter op klasse of zekerheid.</p>
</div>
<button className="secondary-action" type="button" onClick={onLoadRuns} disabled={!selectedProjectId}>
Analyses vernieuwen
</button>
</div>
<div className="lab-form-grid">
<label>
Analyse
<select value={selectedSegmentationRunId} onChange={(event) => onSelectRun(event.target.value)}>
<option value="">Kies een bewaarde analyse</option>
{segmentationRuns.map((run) => (
<option key={run.id} value={run.id}>
{segmentationRunLabel(run)}
</option>
))}
</select>
</label>
<label>
Klasse
<input
type="text"
placeholder="Filter op klasse"
value={segmentationClassFilter}
onChange={(event) => onSetClassFilter(event.target.value)}
/>
</label>
<label>
Minimale zekerheid
<input
type="number"
min="0"
max="1"
step="0.05"
value={segmentationMinConfidenceFilter}
onChange={(event) => onSetMinConfidenceFilter(Number(event.target.value))}
/>
</label>
</div>
<button className="primary-action" type="button" onClick={onLoadResults} disabled={!selectedSegmentationRunId || loadingSegmentationResults}>
Resultaten laden
</button>
{loadingSegmentationResults ? (
<div className="result-state result-state-loading">
<strong>Segmentatieresultaten worden geladen.</strong>
<p>GeoIntel leest de bewaarde polygonen van deze analyse.</p>
</div>
) : null}
<div className="ai-lab-state-stack">
{loadingSegmentationResults || segmentationRunError ? null : !selectedSegmentationRunId ? (
<div className="result-state result-state-empty">
<strong>Kies eerst een bewaarde analyse.</strong>
<p>Daarna toont GeoIntel uitsluitend de polygonen van die analyserun.</p>
</div>
) : segmentationTotal === 0 ? (
<div className="result-state result-state-empty">
<strong>Geen bewaarde vlakken binnen deze filters.</strong>
<p>Dit bewijst niet dat het gebied geen relevante objecten bevat.</p>
</div>
) : (
<div className={segmentationTruncated ? 'result-state result-state-warning' : 'result-state result-state-ready'}>
<strong>
{segmentationTruncated
? `${segmentationItems.length} van ${segmentationTotal} vlakken geladen`
: `${segmentationTotal} vlakken geladen`}
</strong>
<p>
{segmentationTruncated
? 'De kaart en tabel tonen een begrensde pagina. Gebruik filters om het resultaat gericht te verfijnen.'
: 'Deze resultaten zijn bewaard in de database.'}
</p>
</div>
)}
</div>
{segmentationItems.length > 0 ? (
<div className="table-scroll">
<table>
<thead>
<tr>
<th>Klasse</th>
<th>Zekerheid</th>
<th>Oppervlakte m²</th>
<th>Model</th>
<th>Herkomst</th>
</tr>
</thead>
<tbody>
{segmentationItems.map((segmentation) => (
<tr key={segmentation.id}>
<td>{segmentationClassLabel(segmentation.class_name)}</td>
<td>{segmentation.confidence?.toFixed(2) ?? 'n.v.t.'}</td>
<td>{segmentation.area_m2?.toFixed(2) ?? 'n.v.t.'}</td>
<td>{persistedSegmentationModelLabel(segmentation.model_name)}</td>
<td>{segmentation.tile_index == null ? 'n.v.t.' : 'Luchtbeeldtegel'}</td>
</tr>
))}
</tbody>
</table>
</div>
) : null}
</div>
<div className="ai-lab-qa-surface" aria-label="Kwaliteitscontrole voor segmentatie">
<h3>Kwaliteitscontrole segmentatie</h3>
<label>
Referentielaag
<select value={segmentationReferenceDatasetId} onChange={(event) => onSelectReferenceDataset(event.target.value)}>
<option value="">Kies een referentielaag</option>
{referenceDatasets.map((dataset) => (
<option key={dataset.id} value={dataset.id}>
{getDatasetSelectionLabel(dataset)}
</option>
))}
</select>
</label>
<button className="primary-action" type="button" onClick={onRunQa} disabled={runningSegmentationQa || !selectedSegmentationRunId || !segmentationReferenceDatasetId}>
Vergelijk met referentielaag
</button>
{segmentationQaError ? (
<div className="result-state result-state-error">
<strong>De kwaliteitscontrole is mislukt.</strong>
<p>{segmentationQaError}</p>
</div>
) : null}
{segmentationQaResult ? (
<div className="result-summary-card">
<p>Status: {segmentationQaResult.status === 'completed' ? 'afgerond' : segmentationQaResult.status}</p>
<p>Precisie: {segmentationQaResult.precision?.toFixed(3) ?? 'n.v.t.'}</p>
<p>Herkenningsgraad: {segmentationQaResult.recall?.toFixed(3) ?? 'n.v.t.'}</p>
<p>F1: {segmentationQaResult.f1_score?.toFixed(3) ?? 'n.v.t.'}</p>
<p>Gemiddelde overlap: {segmentationQaResult.mean_iou?.toFixed(3) ?? 'n.v.t.'}</p>
<p>Onterecht gevonden: {segmentationQaResult.false_positives}</p>
<p>Gemist: {segmentationQaResult.false_negatives}</p>
<details className="technical-inline-details">
<summary>Technische referentie</summary>
<span>Kwaliteitscontrole-ID: {segmentationQaResult.quality_check_id}</span>
</details>
</div>
) : null}
</div>
</section>
)
}