feat: guide raster building analysis workflow
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,6 +657,23 @@ export function DetectionLab({
|
||||
) : null}
|
||||
</label>
|
||||
</div>
|
||||
<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
|
||||
@@ -607,10 +692,12 @@ export function DetectionLab({
|
||||
<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
|
||||
<button className="secondary-action" type="button" onClick={onRunDetection} disabled={runningDetection || !detectionRunReady}>
|
||||
Bestaande beeldtegels analyseren
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ai-lab-state-stack">
|
||||
@@ -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>
|
||||
<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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { detectionApi } from '../services/api'
|
||||
import { datasetsApi, detectionApi } from '../services/api'
|
||||
import type {
|
||||
DatasetCreateResponse,
|
||||
DetectionModelCapability,
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
DetectionRead,
|
||||
DetectionRunRead,
|
||||
DetectionRunResponse,
|
||||
JobRead,
|
||||
ModelAssetRead,
|
||||
QualityCheckRead,
|
||||
YoloPreflightResponse,
|
||||
@@ -26,6 +27,17 @@ interface DetectionOperatorProfileSelection {
|
||||
confidenceThreshold: number
|
||||
}
|
||||
|
||||
export type DetectionWorkflowStage =
|
||||
| 'idle'
|
||||
| 'uploading'
|
||||
| 'ready'
|
||||
| 'tiling'
|
||||
| 'validating'
|
||||
| 'detecting'
|
||||
| 'loading'
|
||||
| 'complete'
|
||||
| 'failed'
|
||||
|
||||
export interface DetectionCalibrationRunRow {
|
||||
threshold: number
|
||||
status: 'queued' | 'running' | 'success' | 'failed'
|
||||
@@ -59,6 +71,21 @@ function parseCalibrationThresholds(value: string): number[] {
|
||||
return thresholds
|
||||
}
|
||||
|
||||
function tileManifestPathFromJob(job: JobRead): string | null {
|
||||
const manifestPath = job.result_json?.manifest_path
|
||||
return typeof manifestPath === 'string' && manifestPath.trim().length > 0 ? manifestPath.trim() : null
|
||||
}
|
||||
|
||||
function rasterTileCount(metadata: Record<string, unknown>, tileSize: number, overlap: number): number | null {
|
||||
const width = metadata.width
|
||||
const height = metadata.height
|
||||
if (typeof width !== 'number' || typeof height !== 'number' || width <= 0 || height <= 0) {
|
||||
return null
|
||||
}
|
||||
const step = tileSize - overlap
|
||||
return Math.ceil(width / step) * Math.ceil(height / step)
|
||||
}
|
||||
|
||||
export function useDetectionWorkflow({
|
||||
selectedProjectId,
|
||||
rasterDatasets,
|
||||
@@ -97,6 +124,7 @@ export function useDetectionWorkflow({
|
||||
const [runningDetectionCalibration, setRunningDetectionCalibration] = useState(false)
|
||||
const [detectionCalibrationRows, setDetectionCalibrationRows] = useState<DetectionCalibrationRunRow[]>([])
|
||||
const [detectionCalibrationError, setDetectionCalibrationError] = useState<string | null>(null)
|
||||
const [detectionWorkflowStage, setDetectionWorkflowStage] = useState<DetectionWorkflowStage>('idle')
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedDetectionDatasetId && rasterDatasets.length > 0) {
|
||||
@@ -201,6 +229,25 @@ export function useDetectionWorkflow({
|
||||
}
|
||||
}
|
||||
|
||||
const executeDetection = async (projectId: string, datasetId: string, manifestPath: string | null) => {
|
||||
const result = await detectionApi.run({
|
||||
project_id: projectId,
|
||||
dataset_id: datasetId,
|
||||
model_id: selectedDetectionModelId,
|
||||
model_asset_id: selectedModelAssetId || null,
|
||||
confidence_threshold: detectionConfidenceThreshold,
|
||||
tile_manifest_path: manifestPath,
|
||||
parameters_json: {},
|
||||
})
|
||||
setDetectionRunResult(result)
|
||||
setSelectedDetectionRunId(result.analysis_run_id)
|
||||
setDetectionWorkflowStage('loading')
|
||||
await loadDetectionRuns(projectId)
|
||||
await loadDetectionResults(result.analysis_run_id)
|
||||
await loadProjectData(projectId)
|
||||
return result
|
||||
}
|
||||
|
||||
const runDetection = async () => {
|
||||
if (!selectedProjectId) {
|
||||
setDetectionRunError('Select a project first')
|
||||
@@ -214,23 +261,122 @@ export function useDetectionWorkflow({
|
||||
setDetectionRunError(null)
|
||||
setDetectionRunResult(null)
|
||||
setRunningDetection(true)
|
||||
setDetectionWorkflowStage('detecting')
|
||||
try {
|
||||
const result = await detectionApi.run({
|
||||
project_id: selectedProjectId,
|
||||
dataset_id: datasetId,
|
||||
model_id: selectedDetectionModelId,
|
||||
model_asset_id: selectedModelAssetId || null,
|
||||
confidence_threshold: detectionConfidenceThreshold,
|
||||
tile_manifest_path: detectionTileManifestPath.trim() || null,
|
||||
parameters_json: {},
|
||||
})
|
||||
setDetectionRunResult(result)
|
||||
setSelectedDetectionRunId(result.analysis_run_id)
|
||||
await loadDetectionRuns(selectedProjectId)
|
||||
await loadDetectionResults(result.analysis_run_id)
|
||||
await loadProjectData(selectedProjectId)
|
||||
await executeDetection(selectedProjectId, datasetId, detectionTileManifestPath.trim() || null)
|
||||
setDetectionWorkflowStage('complete')
|
||||
} catch (error) {
|
||||
setDetectionRunError(formatError(error, 'Detection run failed'))
|
||||
setDetectionWorkflowStage('failed')
|
||||
} finally {
|
||||
setRunningDetection(false)
|
||||
}
|
||||
}
|
||||
|
||||
const uploadDetectionRaster = async (file: File): Promise<boolean> => {
|
||||
if (!selectedProjectId) {
|
||||
setDetectionRunError('De regionale werkruimte is nog niet geladen')
|
||||
return false
|
||||
}
|
||||
setDetectionRunError(null)
|
||||
setDetectionWorkflowStage('uploading')
|
||||
try {
|
||||
const dataset = await datasetsApi.upload(selectedProjectId, {
|
||||
file,
|
||||
datasetType: 'raster',
|
||||
source: 'user_upload',
|
||||
datasetRole: 'source',
|
||||
sourceName: 'manual',
|
||||
sourceMetadataJson: JSON.stringify({ purpose: 'building_detection' }),
|
||||
provenanceMetadataJson: JSON.stringify({ original_filename: file.name, acquisition: 'explicit_user_upload' }),
|
||||
})
|
||||
setSelectedDetectionDatasetId(dataset.id)
|
||||
setDetectionTileManifestPath('')
|
||||
setDetectionRunResult(null)
|
||||
setDetectionWorkflowStage('ready')
|
||||
await loadProjectData(selectedProjectId)
|
||||
return true
|
||||
} catch (error) {
|
||||
setDetectionRunError(formatError(error, 'Het luchtbeeld kon niet worden toegevoegd'))
|
||||
setDetectionWorkflowStage('failed')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const prepareAndRunDetection = async (): Promise<boolean> => {
|
||||
if (!selectedProjectId) {
|
||||
setDetectionRunError('De regionale werkruimte is nog niet geladen')
|
||||
return false
|
||||
}
|
||||
const datasetId = selectedDetectionDatasetId || rasterDatasets[0]?.id
|
||||
if (!datasetId) {
|
||||
setDetectionRunError('Kies of voeg eerst een gegeorefereerd luchtbeeld toe')
|
||||
return false
|
||||
}
|
||||
const selectedModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId)
|
||||
if (!selectedModel?.configured || selectedDetectionModelId === 'manual-fixture-detector') {
|
||||
setDetectionRunError(selectedModel?.limitation_message ?? 'Het gekozen analysemodel is niet beschikbaar')
|
||||
return false
|
||||
}
|
||||
if (selectedDetectionModelId === 'yolo-configured' && modelAssets.length > 0 && !selectedModelAssetId) {
|
||||
setDetectionRunError('Kies eerst een lokaal modelbestand')
|
||||
return false
|
||||
}
|
||||
|
||||
setDetectionRunError(null)
|
||||
setDetectionRunResult(null)
|
||||
setRunningDetection(true)
|
||||
try {
|
||||
let manifestPath = detectionTileManifestPath.trim()
|
||||
if (!manifestPath) {
|
||||
setDetectionWorkflowStage('tiling')
|
||||
const inspection = await datasetsApi.rasterInspect(selectedProjectId, datasetId)
|
||||
const expectedTileCount = rasterTileCount(inspection.metadata, 512, 64)
|
||||
const maxTiles = yoloPreflight?.max_tiles ?? 256
|
||||
if (expectedTileCount === null) {
|
||||
throw new Error('De afmetingen van het luchtbeeld konden niet veilig worden bepaald')
|
||||
}
|
||||
if (expectedTileCount > maxTiles) {
|
||||
throw new Error(
|
||||
`Dit luchtbeeld zou ${expectedTileCount} beeldtegels maken; het veilige maximum is ${maxTiles}. Knip het beeld eerst tot het gewenste werkgebied.`,
|
||||
)
|
||||
}
|
||||
const tileJob = await datasetsApi.rasterTile(selectedProjectId, datasetId, {
|
||||
tile_size: 512,
|
||||
overlap: 64,
|
||||
})
|
||||
manifestPath = tileManifestPathFromJob(tileJob) ?? ''
|
||||
if (!manifestPath) {
|
||||
throw new Error(tileJob.error_message || 'De tegelvoorbereiding leverde geen geldig manifest op')
|
||||
}
|
||||
setDetectionTileManifestPath(manifestPath)
|
||||
}
|
||||
|
||||
setDetectionWorkflowStage('validating')
|
||||
const preflight = await detectionApi.getYoloPreflight({
|
||||
tile_manifest_path: manifestPath,
|
||||
model_asset_id: selectedModelAssetId || null,
|
||||
})
|
||||
setYoloPreflight(preflight)
|
||||
setYoloPreflightError(null)
|
||||
if (
|
||||
!preflight.checks.manifest_valid ||
|
||||
!preflight.checks.tile_paths_exist ||
|
||||
!preflight.checks.tile_limit_ok ||
|
||||
!preflight.checks.dependencies_available ||
|
||||
!preflight.checks.model_file_exists
|
||||
) {
|
||||
throw new Error(preflight.message || 'De beeldtegels of modelruntime zijn niet startklaar')
|
||||
}
|
||||
|
||||
setDetectionWorkflowStage('detecting')
|
||||
await executeDetection(selectedProjectId, datasetId, manifestPath)
|
||||
setDetectionWorkflowStage('complete')
|
||||
return true
|
||||
} catch (error) {
|
||||
setDetectionRunError(formatError(error, 'De beeldanalyse is mislukt'))
|
||||
setDetectionWorkflowStage('failed')
|
||||
return false
|
||||
} finally {
|
||||
setRunningDetection(false)
|
||||
}
|
||||
@@ -371,6 +517,7 @@ export function useDetectionWorkflow({
|
||||
setDetectionRunResult(null)
|
||||
setDetectionCalibrationRows([])
|
||||
setDetectionCalibrationError(null)
|
||||
setDetectionWorkflowStage('idle')
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -405,11 +552,14 @@ export function useDetectionWorkflow({
|
||||
runningDetectionCalibration,
|
||||
detectionCalibrationRows,
|
||||
detectionCalibrationError,
|
||||
detectionWorkflowStage,
|
||||
loadDetectionModels,
|
||||
loadYoloPreflight,
|
||||
loadDetectionRuns,
|
||||
loadDetectionResults,
|
||||
runDetection,
|
||||
uploadDetectionRaster,
|
||||
prepareAndRunDetection,
|
||||
runDetectionQa,
|
||||
runDetectionCalibration,
|
||||
applyDetectionOperatorProfile,
|
||||
|
||||
@@ -3977,6 +3977,125 @@ button.entity-card {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.guided-raster-input {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(14rem, 1fr) minmax(14rem, 0.9fr) auto;
|
||||
gap: 0.75rem;
|
||||
align-items: end;
|
||||
border: 1px solid #d8e3de;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
background: #f8fbf9;
|
||||
}
|
||||
|
||||
.guided-raster-input > div,
|
||||
.file-picker-field {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.guided-raster-input strong,
|
||||
.file-picker-field span {
|
||||
color: var(--text);
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.guided-raster-input p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.guided-raster-input input[type="file"] {
|
||||
min-height: 2.45rem;
|
||||
padding: 0.42rem;
|
||||
background: #ffffff;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.guided-detection-progress {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.guided-detection-step {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
border: 1px solid #dce4e0;
|
||||
border-radius: 7px;
|
||||
padding: 0.52rem 0.6rem;
|
||||
background: #ffffff;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.guided-detection-step span {
|
||||
display: grid;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
flex: 0 0 1.25rem;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: #edf2ef;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.guided-detection-step strong {
|
||||
min-width: 0;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.guided-detection-step-active {
|
||||
border-color: #8fb9ad;
|
||||
background: #f2faf7;
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.guided-detection-step-complete {
|
||||
border-color: #b8dcc9;
|
||||
background: #f8fff9;
|
||||
color: #235f43;
|
||||
}
|
||||
|
||||
.guided-detection-step-active span,
|
||||
.guided-detection-step-complete span {
|
||||
background: #dcefe6;
|
||||
color: #235f43;
|
||||
}
|
||||
|
||||
.guided-detection-action {
|
||||
min-height: 2.75rem;
|
||||
padding-inline: 1.15rem;
|
||||
}
|
||||
|
||||
.technical-manifest-surface {
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.guided-raster-input {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.guided-detection-progress {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.guided-detection-progress {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.raster-readiness-item span,
|
||||
.raster-manifest-handoff span {
|
||||
color: var(--muted);
|
||||
|
||||
Reference in New Issue
Block a user