diff --git a/CHANGELOG.md b/CHANGELOG.md index 1781dc56..078f7036 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ # Changelog +## Sprint 195 Guided raster-to-detection workflow (2026-07-14) + +- Replaced the Detection Lab's manual manifest-path prerequisite with one guided action that creates canonical 512 px raster tiles with 64 px overlap, reuses an existing manifest, validates raster size and the local YOLO runtime, runs persisted detection and loads the persisted GeoJSON result on the existing MapLibre map. +- Added direct, explicit georeferenced GeoTIFF upload in Detection Lab through the existing dataset upload boundary; no browser-side provider fetch, model download or alternate persistence path was introduced. +- Kept manual manifest execution, model assets, preflight and calibration available under technical/management disclosures while making persisted detection QA a primary user step. +- Added clear preparation progress, understandable Dutch QA diagnostics, result-to-map navigation and focused regression coverage. +- Did not change API contracts, database migrations, model dependencies or backend inference behavior. + ## Sprint 194 Regional official time-series synchronization (2026-07-14) - Generalized the proven Statbel population operator from a hardcoded Mol import to an approved geographic scope while keeping Mol as the backwards-compatible default. diff --git a/backend/tests/test_sprint192_regional_map_state.py b/backend/tests/test_sprint192_regional_map_state.py index f602361f..55820269 100644 --- a/backend/tests/test_sprint192_regional_map_state.py +++ b/backend/tests/test_sprint192_regional_map_state.py @@ -28,9 +28,10 @@ def test_cancelled_selection_requests_cannot_restore_stale_results() -> None: assert "if (requestSequence.current !== sequence)" in themes_hook -def test_viewport_status_names_the_active_reference_layer() -> None: +def test_viewport_status_uses_end_user_map_language() -> None: hook = read("frontend/src/hooks/useViewportVectorLayer.ts") assert "load buildings from PostGIS" not in hook - assert "selectedDataset?.reference_layer_name?.trim() || 'features'" in hook - assert "to load ${layerLabel} from PostGIS" in hook + assert "Zichtbare kaartobjecten laden..." in hook + assert "kaartobjecten getoond" in hook + assert "PostGIS" not in hook diff --git a/backend/tests/test_sprint195_guided_detection_workflow.py b/backend/tests/test_sprint195_guided_detection_workflow.py new file mode 100644 index 00000000..25325c7b --- /dev/null +++ b/backend/tests/test_sprint195_guided_detection_workflow.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def read(path: str) -> str: + return (ROOT / path).read_text(encoding="utf-8") + + +def test_guided_detection_reuses_canonical_raster_and_detection_apis() -> None: + hook = read("frontend/src/hooks/useDetectionWorkflow.ts") + + assert "prepareAndRunDetection" in hook + assert "datasetsApi.rasterTile" in hook + assert "datasetsApi.rasterInspect" in hook + assert "rasterTileCount" in hook + assert "expectedTileCount > maxTiles" in hook + assert "tile_size: 512" in hook + assert "overlap: 64" in hook + assert "detectionApi.getYoloPreflight" in hook + assert "await executeDetection(selectedProjectId, datasetId, manifestPath)" in hook + assert "await loadDetectionResults(result.analysis_run_id)" in hook + assert "model_id: selectedDetectionModelId" in hook + assert "model_asset_id: selectedModelAssetId || null" in hook + + +def test_guided_detection_upload_uses_existing_dataset_persistence_boundary() -> None: + hook = read("frontend/src/hooks/useDetectionWorkflow.ts") + + assert "uploadDetectionRaster" in hook + assert "datasetsApi.upload" in hook + assert "datasetType: 'raster'" in hook + assert "datasetRole: 'source'" in hook + assert "sourceName: 'manual'" in hook + assert "explicit_user_upload" in hook + + +def test_detection_lab_hides_manifest_plumbing_and_exposes_map_first_result_flow() -> None: + lab = read("frontend/src/components/detection/DetectionLab.tsx") + app = read("frontend/src/App.tsx") + + assert "Gebouwen zoeken en op kaart tonen" in lab + assert 'aria-label="Luchtbeeld toevoegen"' in lab + assert 'aria-label="Technische tegelinstellingen"' in lab + assert "Worden automatisch voorbereid" in lab + assert "Toon op kaart" in lab + assert "onPrepareAndRunDetection={runGuidedDetection}" in app + assert "setMapContentMode('analysis')" in app + assert "setMapLayerVisible(true)" in app + assert "setActiveWorkspace('map')" in app + + +def test_detection_qa_remains_persisted_and_primary_not_parallel() -> None: + lab = read("frontend/src/components/detection/DetectionLab.tsx") + hook = read("frontend/src/hooks/useDetectionWorkflow.ts") + + assert 'aria-label="Kwaliteitscontrole gebouwdetectie"' in lab + assert "als kwaliteitscontrole in de database bewaard" in lab + assert "detectionApi.compareWithReference" in hook + assert "await loadQualityChecks(selectedProjectId)" in hook diff --git a/backend/tests/test_sprint96_useful_default_context.py b/backend/tests/test_sprint96_useful_default_context.py index 804864cd..e4bf54f2 100644 --- a/backend/tests/test_sprint96_useful_default_context.py +++ b/backend/tests/test_sprint96_useful_default_context.py @@ -23,7 +23,9 @@ def test_ai_labs_explain_missing_raster_input_before_disabled_runs() -> None: ) assert "Geen luchtbeeld beschikbaar in deze werkruimte." in detection_lab - assert "Voeg onder Bronnen een gegeorefereerde GeoTIFF toe." in detection_lab + assert "Voeg hieronder een gegeorefereerde GeoTIFF toe." in detection_lab + assert 'aria-label="Luchtbeeld toevoegen"' in detection_lab + assert "onUploadRaster" in detection_lab assert "rasterDatasets.length === 0" in detection_lab assert "No raster datasets available for segmentation." in segmentation_lab assert "Upload or select a raster dataset in Data before running segmentation." in segmentation_lab diff --git a/docs/AI_PIPELINES.md b/docs/AI_PIPELINES.md index c10e769c..ff18445a 100644 --- a/docs/AI_PIPELINES.md +++ b/docs/AI_PIPELINES.md @@ -49,6 +49,8 @@ Sprint 8B adds an import-safe real YOLO adapter path: - YOLO class labels are normalized to lowercase for persisted detection records and filtering, while the original model label remains available in detection provenance. - Detection runs remain synchronous behind the existing job and analysis-run persistence boundary for Sprint 8B. +The guided Detection Lab action does not introduce another inference pipeline. It creates a tile manifest through the existing raster service, validates that manifest and the selected local asset through YOLO preflight, then invokes the same configured detection service. Persisted `Detection` geometry remains the authoritative map output; QA continues to compare those rows against persisted reference `vector_features` and stores `QualityCheck`/`Metric` records. + ### Sprint 13 YOLO operational preflight Sprint 13 adds a local preflight command for configured YOLO operation: diff --git a/docs/API_CONTRACTS.md b/docs/API_CONTRACTS.md index c6a67e4e..36ee8b0d 100644 --- a/docs/API_CONTRACTS.md +++ b/docs/API_CONTRACTS.md @@ -610,6 +610,19 @@ Response: Sprint 8 implements Detection Lab foundation only. YOLO/PyTorch real inference is not enabled, no model is downloaded, and fixture detections require explicit fixture mode. +### Guided browser orchestration + +The current frontend offers one guided building-analysis action, but does not add a parallel backend workflow endpoint. It deliberately composes the canonical contracts in this order: + +1. optional explicit `POST /api/v1/projects/{project_id}/datasets/upload` for a georeferenced GeoTIFF; +2. `POST /api/v1/projects/{project_id}/datasets/{dataset_id}/raster/tile` with 512 px tiles and 64 px overlap; +3. `GET /api/v1/detection/yolo/preflight` with the returned manifest and selected local model asset; +4. `POST /api/v1/detection/run` only after successful preflight; +5. persisted run, Detection list and Detection GeoJSON reads; +6. optional persisted reference QA through the existing detection QA endpoint. + +The strict `POST /api/v1/detection/run` contract still requires `tile_manifest_path` for configured YOLO. The frontend does not create fake tiles, bypass tile limits, fetch external imagery or download model weights. + ### GET `/api/v1/detection/models` Returns object-detection model capability descriptors. diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 8dfe522a..19ba1e9f 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -1,3 +1,20 @@ +## Sprint 195 Guided raster-to-detection workflow (2026-07-14) + +Changed: +- Connected the existing Dataset upload, `raster/tile`, YOLO preflight, configured detection, persisted result listing and MapLibre GeoJSON overlay into one guided frontend action. +- Added explicit GeoTIFF input inside Detection Lab so users no longer need to navigate to dataset management before starting image analysis. +- Uses fixed safe tiling defaults of 512 px with 64 px overlap, reuses the current manifest and estimates the tile count from canonical raster inspection before writing tiles; the backend remains authoritative for tile-limit, manifest, dependency and local-model validation. +- Moved manifest paths, direct-manifest execution and calibration under management disclosures while returning persisted reference QA to the main result flow. +- Kept every run on the existing `Dataset -> Job -> AnalysisRun -> Detection -> QualityCheck/Metric` chain. No migration, API route, external fetch, model download or synthetic inference was added. + +Validated locally: +- `bash scripts/run_readiness_check.sh` passed with `575 passed`, backend compile, one Alembic head (`202607140001`), frontend typecheck/build and live-smoke syntax validation. +- `python -m alembic upgrade head --sql` generated the complete 26,972-byte PostgreSQL/PostGIS migration plan successfully. +- Focused guided-flow, stale-map-state and direct-upload contracts passed. The Windows workstation has no Docker CLI; live container/PostGIS validation therefore remains part of the Tower deployment pass. + +Next: +- Provision one existing real Mol operator orthophoto and matching official GRB reference into the regional workbench through the canonical upload API, then execute the guided action and verify the persisted map/QA result in the browser. + ## Sprint 194 Regional official time series and full-Area performance (2026-07-14) Changed: diff --git a/frontend/README.md b/frontend/README.md index ee3fce74..fade3d6b 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -85,6 +85,18 @@ Detection Lab and Segmentation Lab now share the same AI workspace hierarchy: mo AI Lab run controls explicitly explain when no raster dataset is available, instead of only showing disabled detection/segmentation run buttons. +Detection Lab now provides one guided operational path for configured building detection: + +1. choose an existing raster or explicitly upload a georeferenced GeoTIFF; +2. create canonical 512 px tiles with 64 px overlap through the existing raster API; +3. run the read-only YOLO preflight for the selected local model asset; +4. execute the existing persisted detection endpoint; +5. load the persisted Detection rows and GeoJSON and open them on the existing MapLibre map. + +The browser never manufactures manifest content, detections or QA metrics. Manual manifest paths and direct-manifest execution remain available only under technical tile settings. Detection QA remains the existing persisted reference comparison and is shown as a primary review step. + +Before creating tiles, the guided action inspects raster dimensions and estimates the number of 512/64 tiles against the backend-reported `YOLO_MAX_TILES`. Oversized imagery is stopped before tile files are written and must first be clipped to the intended work area. Repeated runs reuse the currently linked manifest. + ## Scope implemented - API client layer (`src/services/api`) - Project and area list/create flows diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 89ddc934..720db380 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -270,11 +270,14 @@ function App(): JSX.Element { runningDetectionCalibration, detectionCalibrationRows, detectionCalibrationError, + detectionWorkflowStage, loadDetectionModels, loadYoloPreflight, loadDetectionRuns, loadDetectionResults, runDetection, + uploadDetectionRaster, + prepareAndRunDetection, runDetectionQa, runDetectionCalibration, applyDetectionOperatorProfile, @@ -578,6 +581,22 @@ function App(): JSX.Element { setMapLayerVisible(true) setActiveWorkspace('map') } + const runGuidedDetection = async () => { + const completed = await prepareAndRunDetection() + if (completed) { + setMapContentMode('analysis') + setMapLayerVisible(true) + setActiveWorkspace('map') + } + } + const openDetectionResultsOnMap = () => { + if (!detectionGeoJson) { + return + } + setMapContentMode('analysis') + setMapLayerVisible(true) + setActiveWorkspace('map') + } const openDatasetExport = (dataset: DatasetCreateResponse) => { if (selectedProjectId) { loadDatasetDetails(selectedProjectId, dataset) @@ -1048,6 +1067,7 @@ function App(): JSX.Element { runningDetectionCalibration={runningDetectionCalibration} detectionCalibrationRows={detectionCalibrationRows} detectionCalibrationError={detectionCalibrationError} + detectionWorkflowStage={detectionWorkflowStage} selectedDetectionRunId={selectedDetectionRunId} detectionItems={detectionItems} detectionClassFilter={detectionClassFilter} @@ -1071,6 +1091,9 @@ function App(): JSX.Element { onSetConfidenceThreshold={setDetectionConfidenceThreshold} onSetTileManifestPath={setDetectionTileManifestPath} onRunDetection={runDetection} + onUploadRaster={uploadDetectionRaster} + onPrepareAndRunDetection={runGuidedDetection} + onOpenResultsOnMap={openDetectionResultsOnMap} onLoadRuns={() => loadDetectionRuns()} onSelectRun={setSelectedDetectionRunId} onSetClassFilter={setDetectionClassFilter} diff --git a/frontend/src/components/detection/DetectionLab.tsx b/frontend/src/components/detection/DetectionLab.tsx index 10fbfc15..32b3007e 100644 --- a/frontend/src/components/detection/DetectionLab.tsx +++ b/frontend/src/components/detection/DetectionLab.tsx @@ -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 + onPrepareAndRunDetection: () => Promise + 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(null) + const rasterFileInputRef = useRef(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 (
@@ -491,16 +520,16 @@ export function DetectionLab({

Nieuwe beeldanalyse

Wat is nog nodig?

-

De analyse start zodra een luchtbeeld en de bijbehorende beeldtegels beschikbaar zijn.

+

Kies een luchtbeeld en model. GeoIntel maakt de beeldtegels en laadt het resultaat daarna automatisch op de kaart.

- - {detectionRunReady ? 'Klaar om te starten' : 'Nog niet startklaar'} + + {guidedDetectionReady ? 'Klaar om te starten' : 'Nog niet startklaar'}
@@ -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'}
@@ -540,20 +571,57 @@ export function DetectionLab({
-
+
Analyse - {detectionRunReady ? 'Klaar om gebouwen te zoeken' : detectionRunBlockedReason} + {guidedDetectionReady ? 'Klaar om gebouwen te zoeken' : guidedDetectionBlockedReason}
{rasterDatasets.length === 0 ? (
Geen luchtbeeld beschikbaar in deze werkruimte. -

Voeg onder Bronnen een gegeorefereerde GeoTIFF toe. Daarna kan GeoIntel er beeldtegels en een detectierun van maken.

+

Voeg hieronder een gegeorefereerde GeoTIFF toe. GeoIntel controleert de projectie en bewaart het bronbestand als dataset.

) : null} +
+
+ Eigen luchtbeeld toevoegen +

Gebruik een GeoTIFF met geldige CRS en georeferentie. Een bestaand luchtbeeld kan meteen in de keuzelijst worden gebruikt.

+
+ + +
- {selectedDetectionModelId === 'yolo-configured' ? ( - - ) : null} - {selectedDetectionModelId === 'yolo-configured' && detectionTileManifestPath.trim() ? ( -
- Gekoppelde beeldtegels -

{detectionTileManifestPath}

- De technische controle wordt vernieuwd wanneer het model of tegelbestand wijzigt. -
- ) : null} - + +
+ + Technische tegelinstellingen + {detectionHasTileManifest ? 'manifest beschikbaar' : 'automatisch'} + +
+

De normale actie gebruikt automatisch 512 px-tegels met 64 px overlap. Alleen beheerders hoeven hier een bestaand manifest te koppelen.

+ {selectedDetectionModelId === 'yolo-configured' ? ( + + ) : null} + {selectedDetectionModelId === 'yolo-configured' && detectionTileManifestPath.trim() ? ( +
+ Gekoppelde beeldtegels +

{detectionTileManifestPath}

+ De technische controle wordt vernieuwd wanneer het model of tegelbestand wijzigt. +
+ ) : null} + +
+
@@ -761,9 +848,14 @@ export function DetectionLab({

Gevonden objecten

Bekijk eerder bewaarde analyses en filter op type of zekerheid.

- +
+ + +
)} + +

Kwaliteitscontrole gebouwdetectie

+

Vergelijk de gevonden gebouwen met een bewaarde officiƫle referentielaag. De uitkomst wordt als kwaliteitscontrole in de database bewaard.