feat: guide raster building analysis workflow
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type {
|
||||
DatasetCreateResponse,
|
||||
DetectionModelCapability,
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
QualityCheckRead,
|
||||
YoloPreflightResponse,
|
||||
} from '../../types'
|
||||
import type { DetectionCalibrationRunRow } from '../../hooks/useDetectionWorkflow'
|
||||
import type { DetectionCalibrationRunRow, DetectionWorkflowStage } from '../../hooks/useDetectionWorkflow'
|
||||
import { DETECTION_OPERATOR_PROFILES, type DetectionOperatorProfile } from './detectionProfiles'
|
||||
|
||||
const DETECTION_PAGE_SIZE_OPTIONS = [25, 50, 100] as const
|
||||
@@ -59,6 +59,7 @@ interface DetectionLabProps {
|
||||
runningDetectionCalibration: boolean
|
||||
detectionCalibrationRows: DetectionCalibrationRunRow[]
|
||||
detectionCalibrationError: string | null
|
||||
detectionWorkflowStage: DetectionWorkflowStage
|
||||
selectedDetectionRunId: string
|
||||
detectionItems: DetectionRead[]
|
||||
detectionClassFilter: string
|
||||
@@ -82,6 +83,9 @@ interface DetectionLabProps {
|
||||
onSetConfidenceThreshold: (value: number) => void
|
||||
onSetTileManifestPath: (value: string) => void
|
||||
onRunDetection: () => void
|
||||
onUploadRaster: (file: File) => Promise<boolean>
|
||||
onPrepareAndRunDetection: () => Promise<void>
|
||||
onOpenResultsOnMap: () => void
|
||||
onLoadRuns: () => void
|
||||
onSelectRun: (runId: string) => void
|
||||
onSetClassFilter: (value: string) => void
|
||||
@@ -115,6 +119,7 @@ export function DetectionLab({
|
||||
runningDetectionCalibration,
|
||||
detectionCalibrationRows,
|
||||
detectionCalibrationError,
|
||||
detectionWorkflowStage,
|
||||
selectedDetectionRunId,
|
||||
detectionItems,
|
||||
detectionClassFilter,
|
||||
@@ -138,6 +143,9 @@ export function DetectionLab({
|
||||
onSetConfidenceThreshold,
|
||||
onSetTileManifestPath,
|
||||
onRunDetection,
|
||||
onUploadRaster,
|
||||
onPrepareAndRunDetection,
|
||||
onOpenResultsOnMap,
|
||||
onLoadRuns,
|
||||
onSelectRun,
|
||||
onSetClassFilter,
|
||||
@@ -173,6 +181,8 @@ export function DetectionLab({
|
||||
const lowestFalsePositivePressureCandidate = bestLowestCalibrationRow(calibrationRows, 'falsePositives')
|
||||
const [detectionResultPage, setDetectionResultPage] = useState(1)
|
||||
const [detectionPageSize, setDetectionPageSize] = useState(DEFAULT_DETECTION_PAGE_SIZE)
|
||||
const [pendingRasterFile, setPendingRasterFile] = useState<File | null>(null)
|
||||
const rasterFileInputRef = useRef<HTMLInputElement>(null)
|
||||
const detectionPageCount = Math.max(1, Math.ceil(detectionItems.length / detectionPageSize))
|
||||
const currentDetectionPage = Math.min(detectionResultPage, detectionPageCount)
|
||||
const detectionPageStart = (currentDetectionPage - 1) * detectionPageSize
|
||||
@@ -192,6 +202,12 @@ export function DetectionLab({
|
||||
detectionModelUiRunnable &&
|
||||
detectionHasExplicitModelAsset &&
|
||||
detectionHasTileManifest
|
||||
const guidedDetectionReady =
|
||||
Boolean(selectedProjectId) &&
|
||||
detectionHasDataset &&
|
||||
detectionHasModel &&
|
||||
detectionModelUiRunnable &&
|
||||
detectionHasExplicitModelAsset
|
||||
const detectionRunBlockedReason = !selectedProjectId
|
||||
? 'De regionale werkruimte is nog niet geladen'
|
||||
: !detectionHasDataset
|
||||
@@ -208,6 +224,19 @@ export function DetectionLab({
|
||||
? 'Maak eerst beeldtegels voor het gekozen luchtbeeld'
|
||||
: null
|
||||
const calibrationRunReady = detectionRunReady && detectionReferenceDatasetId.length > 0 && calibrationThresholdText.trim().length > 0
|
||||
const guidedDetectionBlockedReason = !selectedProjectId
|
||||
? 'De regionale werkruimte is nog niet geladen'
|
||||
: !detectionHasDataset
|
||||
? 'Kies of voeg een gegeorefereerd luchtbeeld toe'
|
||||
: !detectionHasModel
|
||||
? 'Kies een analysemodel'
|
||||
: selectedDetectionModelId === 'manual-fixture-detector'
|
||||
? 'Het fixturemodel is alleen bedoeld voor expliciete tests en demo\'s'
|
||||
: !detectionModelReady
|
||||
? selectedDetectionModel?.limitation_message ?? 'Het gekozen model is niet geconfigureerd'
|
||||
: !detectionHasExplicitModelAsset
|
||||
? 'Kies een lokaal modelbestand onder beheer'
|
||||
: null
|
||||
|
||||
return (
|
||||
<section className="workspace-panel ai-lab-shell detection-lab-shell">
|
||||
@@ -491,16 +520,16 @@ export function DetectionLab({
|
||||
<div className="ai-lab-run-surface" aria-label="Detection run controls">
|
||||
<h3>Nieuwe beeldanalyse</h3>
|
||||
<div
|
||||
className={detectionRunReady ? 'lab-readiness-panel lab-readiness-panel-ready' : 'lab-readiness-panel'}
|
||||
className={guidedDetectionReady ? 'lab-readiness-panel lab-readiness-panel-ready' : 'lab-readiness-panel'}
|
||||
aria-label="Detection run readiness"
|
||||
>
|
||||
<div className="ai-lab-section-header">
|
||||
<div>
|
||||
<h3>Wat is nog nodig?</h3>
|
||||
<p>De analyse start zodra een luchtbeeld en de bijbehorende beeldtegels beschikbaar zijn.</p>
|
||||
<p>Kies een luchtbeeld en model. GeoIntel maakt de beeldtegels en laadt het resultaat daarna automatisch op de kaart.</p>
|
||||
</div>
|
||||
<span className={detectionRunReady ? 'status-badge status-badge-ready' : 'status-badge'}>
|
||||
{detectionRunReady ? 'Klaar om te starten' : 'Nog niet startklaar'}
|
||||
<span className={guidedDetectionReady ? 'status-badge status-badge-ready' : 'status-badge'}>
|
||||
{guidedDetectionReady ? 'Klaar om te starten' : 'Nog niet startklaar'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="lab-readiness-grid">
|
||||
@@ -522,7 +551,9 @@ export function DetectionLab({
|
||||
{detectionRequiresTileManifest
|
||||
? detectionHasTileManifest
|
||||
? 'Beschikbaar'
|
||||
: 'Maak eerst tegels vanuit het luchtbeeld'
|
||||
: detectionHasDataset
|
||||
? 'Worden automatisch voorbereid'
|
||||
: 'Wachten op een luchtbeeld'
|
||||
: 'Niet vereist'}
|
||||
</strong>
|
||||
</div>
|
||||
@@ -540,20 +571,57 @@ export function DetectionLab({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={detectionRunReady ? 'lab-action-guardrail lab-action-guardrail-ready' : 'lab-action-guardrail'}>
|
||||
<div className={guidedDetectionReady ? 'lab-action-guardrail lab-action-guardrail-ready' : 'lab-action-guardrail'}>
|
||||
<span>Analyse</span>
|
||||
<strong>{detectionRunReady ? 'Klaar om gebouwen te zoeken' : detectionRunBlockedReason}</strong>
|
||||
<strong>{guidedDetectionReady ? 'Klaar om gebouwen te zoeken' : guidedDetectionBlockedReason}</strong>
|
||||
</div>
|
||||
{rasterDatasets.length === 0 ? (
|
||||
<div className="result-state result-state-empty">
|
||||
<strong>Geen luchtbeeld beschikbaar in deze werkruimte.</strong>
|
||||
<p>Voeg onder Bronnen een gegeorefereerde GeoTIFF toe. Daarna kan GeoIntel er beeldtegels en een detectierun van maken.</p>
|
||||
<p>Voeg hieronder een gegeorefereerde GeoTIFF toe. GeoIntel controleert de projectie en bewaart het bronbestand als dataset.</p>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="guided-raster-input" aria-label="Luchtbeeld toevoegen">
|
||||
<div>
|
||||
<strong>Eigen luchtbeeld toevoegen</strong>
|
||||
<p>Gebruik een GeoTIFF met geldige CRS en georeferentie. Een bestaand luchtbeeld kan meteen in de keuzelijst worden gebruikt.</p>
|
||||
</div>
|
||||
<label className="file-picker-field">
|
||||
<span>GeoTIFF-bestand</span>
|
||||
<input
|
||||
ref={rasterFileInputRef}
|
||||
type="file"
|
||||
accept=".tif,.tiff,image/tiff,application/geotiff"
|
||||
onChange={(event) => setPendingRasterFile(event.target.files?.[0] ?? null)}
|
||||
disabled={runningDetection}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className="secondary-action"
|
||||
type="button"
|
||||
disabled={!pendingRasterFile || runningDetection}
|
||||
onClick={async () => {
|
||||
if (pendingRasterFile && await onUploadRaster(pendingRasterFile)) {
|
||||
setPendingRasterFile(null)
|
||||
if (rasterFileInputRef.current) {
|
||||
rasterFileInputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{detectionWorkflowStage === 'uploading' ? 'Luchtbeeld toevoegen...' : 'Luchtbeeld toevoegen'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="lab-form-grid">
|
||||
<label>
|
||||
Luchtbeeld
|
||||
<select value={selectedDetectionDatasetId} onChange={(event) => onSelectDataset(event.target.value)}>
|
||||
<select
|
||||
value={selectedDetectionDatasetId}
|
||||
onChange={(event) => {
|
||||
onSelectDataset(event.target.value)
|
||||
onSetTileManifestPath('')
|
||||
}}
|
||||
>
|
||||
<option value="">Kies een luchtbeeld</option>
|
||||
{rasterDatasets.map((dataset) => (
|
||||
<option key={dataset.id} value={dataset.id}>
|
||||
@@ -589,27 +657,46 @@ export function DetectionLab({
|
||||
) : null}
|
||||
</label>
|
||||
</div>
|
||||
{selectedDetectionModelId === 'yolo-configured' ? (
|
||||
<label>
|
||||
Beeldtegelbestand
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Pad naar de aangemaakte beeldtegels"
|
||||
value={detectionTileManifestPath}
|
||||
onChange={(event) => onSetTileManifestPath(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
{selectedDetectionModelId === 'yolo-configured' && detectionTileManifestPath.trim() ? (
|
||||
<div className="linked-manifest-card">
|
||||
<strong>Gekoppelde beeldtegels</strong>
|
||||
<p>{detectionTileManifestPath}</p>
|
||||
<span>De technische controle wordt vernieuwd wanneer het model of tegelbestand wijzigt.</span>
|
||||
</div>
|
||||
) : null}
|
||||
<button className="primary-action" type="button" onClick={onRunDetection} disabled={runningDetection || !detectionRunReady}>
|
||||
Zoek gebouwen
|
||||
<div className="guided-detection-progress" aria-live="polite">
|
||||
<DetectionWorkflowStep label="1. Luchtbeeld" complete={detectionHasDataset} active={detectionWorkflowStage === 'uploading'} />
|
||||
<DetectionWorkflowStep label="2. Beeldtegels" complete={detectionHasTileManifest || detectionWorkflowStage === 'complete'} active={detectionWorkflowStage === 'tiling'} />
|
||||
<DetectionWorkflowStep label="3. Modelcontrole" complete={detectionWorkflowStage === 'detecting' || detectionWorkflowStage === 'loading' || detectionWorkflowStage === 'complete'} active={detectionWorkflowStage === 'validating'} />
|
||||
<DetectionWorkflowStep label="4. Resultaat" complete={detectionWorkflowStage === 'complete'} active={detectionWorkflowStage === 'detecting' || detectionWorkflowStage === 'loading'} />
|
||||
</div>
|
||||
<button className="primary-action guided-detection-action" type="button" onClick={onPrepareAndRunDetection} disabled={runningDetection || !guidedDetectionReady}>
|
||||
{detectionWorkflowActionLabel(detectionWorkflowStage)}
|
||||
</button>
|
||||
|
||||
<details className="ai-lab-model-surface technical-manifest-surface" aria-label="Technische tegelinstellingen">
|
||||
<summary>
|
||||
<span>Technische tegelinstellingen</span>
|
||||
<strong>{detectionHasTileManifest ? 'manifest beschikbaar' : 'automatisch'}</strong>
|
||||
</summary>
|
||||
<div className="ai-lab-disclosure-body">
|
||||
<p className="muted">De normale actie gebruikt automatisch 512 px-tegels met 64 px overlap. Alleen beheerders hoeven hier een bestaand manifest te koppelen.</p>
|
||||
{selectedDetectionModelId === 'yolo-configured' ? (
|
||||
<label>
|
||||
Beeldtegelbestand
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Pad naar de aangemaakte beeldtegels"
|
||||
value={detectionTileManifestPath}
|
||||
onChange={(event) => onSetTileManifestPath(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
{selectedDetectionModelId === 'yolo-configured' && detectionTileManifestPath.trim() ? (
|
||||
<div className="linked-manifest-card">
|
||||
<strong>Gekoppelde beeldtegels</strong>
|
||||
<p>{detectionTileManifestPath}</p>
|
||||
<span>De technische controle wordt vernieuwd wanneer het model of tegelbestand wijzigt.</span>
|
||||
</div>
|
||||
) : null}
|
||||
<button className="secondary-action" type="button" onClick={onRunDetection} disabled={runningDetection || !detectionRunReady}>
|
||||
Bestaande beeldtegels analyseren
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -761,9 +848,14 @@ export function DetectionLab({
|
||||
<h3>Gevonden objecten</h3>
|
||||
<p className="muted">Bekijk eerder bewaarde analyses en filter op type of zekerheid.</p>
|
||||
</div>
|
||||
<button className="secondary-action" type="button" onClick={onLoadRuns} disabled={!selectedProjectId}>
|
||||
Analyses vernieuwen
|
||||
</button>
|
||||
<div className="panel-action-row">
|
||||
<button className="secondary-action" type="button" onClick={onLoadRuns} disabled={!selectedProjectId}>
|
||||
Analyses vernieuwen
|
||||
</button>
|
||||
<button className="primary-action" type="button" onClick={onOpenResultsOnMap} disabled={detectionItems.length === 0}>
|
||||
Toon op kaart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lab-form-grid">
|
||||
<label>
|
||||
@@ -954,9 +1046,12 @@ export function DetectionLab({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div className="ai-lab-qa-surface" aria-label="Kwaliteitscontrole gebouwdetectie">
|
||||
<h3>Kwaliteitscontrole gebouwdetectie</h3>
|
||||
<p className="muted">Vergelijk de gevonden gebouwen met een bewaarde officiële referentielaag. De uitkomst wordt als kwaliteitscontrole in de database bewaard.</p>
|
||||
<label>
|
||||
Referentielaag
|
||||
<select value={detectionReferenceDatasetId} onChange={(event) => onSelectReferenceDataset(event.target.value)}>
|
||||
@@ -989,36 +1084,34 @@ export function DetectionLab({
|
||||
<p>Fout negatief: {detectionQaResult.false_negatives}</p>
|
||||
{detectionQaResult.coverage ? (
|
||||
<div className="detection-qa-diagnostic">
|
||||
<span>Inference coverage</span>
|
||||
<span>Gecontroleerd beeldbereik</span>
|
||||
<strong>
|
||||
{detectionQaResult.coverage.applied
|
||||
? `${detectionQaResult.coverage.reference_evaluated_count} of ${detectionQaResult.coverage.reference_raw_count} reference features evaluated`
|
||||
: 'No tile manifest coverage applied'}
|
||||
? `${detectionQaResult.coverage.reference_evaluated_count} van ${detectionQaResult.coverage.reference_raw_count} referentieobjecten gecontroleerd`
|
||||
: 'De volledige referentielaag is gecontroleerd'}
|
||||
</strong>
|
||||
<p>
|
||||
{detectionQaResult.coverage.applied
|
||||
? `${detectionQaResult.coverage.reference_excluded_outside_count} outside coverage, ${detectionQaResult.coverage.reference_clipped_boundary_count} clipped at the boundary, ${detectionQaResult.coverage.tile_count} ${detectionQaResult.coverage.tile_count === 1 ? 'tile' : 'tiles'}.`
|
||||
: 'This run uses the complete selected reference population.'}
|
||||
? `${detectionQaResult.coverage.reference_excluded_outside_count} buiten beeldbereik, ${detectionQaResult.coverage.reference_clipped_boundary_count} aan de rand begrensd, ${detectionQaResult.coverage.tile_count} beeldtegels.`
|
||||
: 'Deze controle gebruikt alle objecten uit de gekozen referentielaag.'}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
{detectionQaResult.box_to_footprint_diagnostics ? (
|
||||
<div className="detection-qa-diagnostic detection-qa-diagnostic-caution">
|
||||
<span>Box-to-footprint diagnostic only</span>
|
||||
<span>Aanvullende vormdiagnose</span>
|
||||
<strong>
|
||||
{detectionQaResult.box_to_footprint_diagnostics.envelope_matches} envelope matches versus{' '}
|
||||
{detectionQaResult.box_to_footprint_diagnostics.strict_matches} canonical matches
|
||||
{detectionQaResult.box_to_footprint_diagnostics.envelope_matches} rechthoekmatches tegenover{' '}
|
||||
{detectionQaResult.box_to_footprint_diagnostics.strict_matches} strikte vormmatches
|
||||
</strong>
|
||||
<p>
|
||||
{detectionQaResult.box_to_footprint_diagnostics.possible_box_to_footprint_mismatch_count} possible matching artifacts. Canonical precision, recall and F1 above remain footprint-IoU based.
|
||||
{detectionQaResult.box_to_footprint_diagnostics.possible_box_to_footprint_mismatch_count} mogelijke vormafwijkingen. Precisie, recall en F1 hierboven blijven gebaseerd op de strikte geometrische overlap.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1060,6 +1153,37 @@ function formatModelAssetSize(sizeBytes: number): string {
|
||||
return `${sizeBytes} B`
|
||||
}
|
||||
|
||||
function DetectionWorkflowStep({
|
||||
label,
|
||||
complete,
|
||||
active,
|
||||
}: {
|
||||
label: string
|
||||
complete: boolean
|
||||
active: boolean
|
||||
}): JSX.Element {
|
||||
const className = active
|
||||
? 'guided-detection-step guided-detection-step-active'
|
||||
: complete
|
||||
? 'guided-detection-step guided-detection-step-complete'
|
||||
: 'guided-detection-step'
|
||||
return (
|
||||
<div className={className}>
|
||||
<span aria-hidden="true">{complete ? 'OK' : active ? '...' : '-'}</span>
|
||||
<strong>{label}</strong>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function detectionWorkflowActionLabel(stage: DetectionWorkflowStage): string {
|
||||
if (stage === 'tiling') return 'Beeldtegels voorbereiden...'
|
||||
if (stage === 'validating') return 'Model en beeld controleren...'
|
||||
if (stage === 'detecting') return 'Gebouwen zoeken...'
|
||||
if (stage === 'loading') return 'Resultaat op kaart laden...'
|
||||
if (stage === 'complete') return 'Analyse opnieuw uitvoeren'
|
||||
return 'Gebouwen zoeken en op kaart tonen'
|
||||
}
|
||||
|
||||
function buildCalibrationRows(detectionRuns: DetectionRunRead[], qualityChecks: QualityCheckRead[]): CalibrationRow[] {
|
||||
const runById = new Map(detectionRuns.map((run) => [run.id, run]))
|
||||
return qualityChecks
|
||||
|
||||
Reference in New Issue
Block a user