Files
geointel/backend
Codex d64f07ad9c
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s
Normalize DHMV tile mosaic resolution
2026-07-15 19:01:11 +02:00
..
2026-07-15 19:01:11 +02:00
2026-07-15 18:54:31 +02:00
2026-06-16 23:36:32 +02:00
2026-06-16 23:36:32 +02:00
2026-06-16 23:36:32 +02:00
2026-07-05 00:40:08 +02:00
2026-07-15 18:13:05 +02:00

GeoIntel Backend (Sprint 3 foundation layer)

FastAPI backend for GeoIntel Kempen Foundation Sprints.

The map-first explorer uses the existing persisted vector selection endpoint. Its bounded GeoJSON preview reports feature_count, while total_feature_count reports the exact PostGIS intersection count before the 1,000-feature response cap. This keeps municipality-scale analysis honest without sending unbounded geometry to the browser.

Scope implemented

  • Project CRUD
  • Area CRUD with PostGIS geometry
  • Vector and raster dataset upload/registration
  • Deterministic local storage metadata capture
  • PostGIS migration and database foundation
  • Job foundation for async-ready GIS operations

Sprint 2 additions

  • Dataset typing and lifecycle support:
    • uploaded
    • validating
    • ready
    • failed
  • Vector metadata extraction:
    • feature count
    • geometry type summary
    • bounds
    • approximate area
    • CRS and CRS assumption
  • Raster metadata endpoint:
    • returns raster profile when rasterio is available
    • returns clear RASTER_PROCESSING_UNAVAILABLE error when dependency is missing
  • Deterministic storage metadata capture:
    • original filename
    • stored filename
    • MIME/content type
    • size bytes
    • checksum SHA-256

Sprint 3 additions

  • Lightweight job architecture:
    • jobs table and migrations
    • job create/list/read/status API
    • synchronous execution behind job abstraction
  • Vector operations foundation:
    • inspect
    • bbox
    • stats
    • clip by area
    • buffer
    • intersect
    • invalid geometry rejection with typed errors
  • Raster operation foundation:
    • inspect
    • metadata
    • preview readiness
    • clip by area (dependency-aware with unavailable fallback)
    • tile generation with manifest output
    • real preview image generation when dependencies are installed

Sprint 4 additions

  • Raster foundation is now implemented with real extraction and deterministic artifact outputs:
    • metadata returns width, height, band count, CRS, bounds, resolution, dtype, nodata, transform
    • preview endpoint generates and reuses PNG previews with width/height
    • clip operation persists a derived raster dataset with:
      • source_dataset_id
      • operation
      • operation_parameters
    • tile operation writes deterministic raster tiles under tiles/{project_id}/{source_dataset_id}/{tile_set_id}
    • tile manifest includes tile path, pixel window, bounds, transform, and count
  • Dependency behavior:
    • when rasterio is missing, raster processing returns RASTER_PROCESSING_UNAVAILABLE
    • preview endpoint additionally requires numpy/pillow and returns RASTER_PROCESSING_UNAVAILABLE when missing

Sprint 5 additions

  • Raster analytics hardening:
    • raster band statistics now include:
      • min, max, mean, std
      • nodata count and ratio
      • valid pixel count
      • dtype
      • optional histogram bins (default 16 bins)
    • raster reproject operation implemented (CRS transform + rasterio reprojection) using dependency-aware raster processing checks.
    • reproject failures are explicit (INVALID_PARAMETERS, INVALID_DATASET_CRS, RASTER_PROCESSING_UNAVAILABLE).
  • Raster clip and tile hardening:
    • clip validates area presence and CRS alignment constraints.
    • tile manifest records tile_set_id, tile_size, overlap, source_dataset_id, source_raster_id, bounds, parameters, count, tile paths, ai_inference, and tile_server.
  • Job result persistence for raster ops:
    • raster clip/reproject/tile job payloads persist derived dataset references when outputs are produced.

Sprint 6 additions

  • Added local spectral index operations:
    • NDVI endpoint: POST /raster/indices/ndvi
    • NDWI endpoint: POST /raster/indices/ndwi
    • NDBI endpoint: POST /raster/indices/ndbi
  • Spectral index input validation:
    • band parameters must be positive integers
    • band parameters must exist in source raster band count
  • Dependency-aware execution:
    • returns RASTER_PROCESSING_UNAVAILABLE when rasterio or numpy are unavailable
  • Real index output handling:
    • local windowed float32 GeoTIFF generation
    • NaN strategy for invalid pixels / division by zero
  • Provenance capture for derived index datasets:
    • source_dataset_id, operation, band_mapping, formula
    • output_dtype, nodata_strategy, value_range_note
    • output_dataset_id, created_at, path

Sprint 7B additions

  • Added provider registry skeleton for grb, osm, manual and fixture.
  • Added provider capability endpoints:
    • GET /api/v1/external/providers
    • GET /api/v1/external/providers/{provider_name}
    • GET /api/v1/external/providers/{provider_name}/layers
    • GET /api/v1/external/providers/{provider_name}/status
    • POST /api/v1/external/providers/{provider_name}/import
  • GRB and OSM imports return explicit not_configured responses; no live WFS or Overpass calls are made.
  • Manual and fixture providers describe existing upload/fixture flows only.
  • Added live PostGIS migration smoke script for environments with a real database:
bash scripts/live_migration_smoke.sh

Sprint 8 additions

  • Added Detection Lab foundation:
    • detections ORM model and Alembic migration with PostGIS geometry storage.
    • hardened analysis_runs for dataset/job/model/result metadata.
    • model registry capability service for yolo-placeholder and manual-fixture-detector.
    • detection service boundary for creating jobs, analysis runs and dependency-aware unavailable responses.
  • Added detection endpoints:
    • GET /api/v1/detection/models
    • GET /api/v1/detection/model-assets
    • POST /api/v1/detection/run
    • GET /api/v1/detection/runs/{analysis_run_id}
    • GET /api/v1/detection/runs/{analysis_run_id}/detections
  • YOLO/PyTorch real inference is not enabled in Sprint 8.
  • Fixture detector mode is test/demo-only and requires explicit fixture_mode=true.

Sprint 8B additions

  • Added optional configured YOLO integration foundation:
    • yolo-configured model registry capability.
    • import-safe adapter for local Ultralytics model files.
    • raster tile manifest validation and tile limit enforcement.
    • pixel bbox to EPSG:4326 detection polygon conversion.
    • persisted detections through the existing detection/job/analysis-run path.
  • YOLO dependencies are optional extras and are not required for backend startup.
  • GeoIntel does not download YOLO model weights automatically.

Sprint 8C additions

  • Added detection visualization/review API support:
    • list detection runs
    • list detections by run or dataset with class/confidence filters
    • get detection detail
    • return persisted detections as GeoJSON FeatureCollections
  • Added detection QA against reference vector datasets:
    • compares persisted detection geometries against persisted vector_features
    • persists quality_checks and metrics
    • returns precision, recall, F1, mean IoU and false positive/negative counts
    • configured-YOLO runs clip both QA populations to persisted tile-manifest coverage before matching and fail closed on missing/mismatched coverage provenance
    • persists a diagnostic-only candidate-box versus reference-envelope pass so box-to-footprint matching artifacts are visible without altering canonical footprint-IoU metrics
  • Segmentation, LiDAR, AI Copilot, Training Studio and Reports remain out of scope.

Sprint 9 additions

  • Added Segmentation Lab foundation:
    • segmentations ORM model and Alembic migration with PostGIS MultiPolygon geometry storage.
    • segmentation model registry capabilities for segmentation-placeholder, fixture-segmenter, yolo-seg-configured and sam-configured.
    • segmentation service boundary for creating jobs, analysis runs and unavailable model responses.
    • explicit fixture segmenter mode for tests/demo fixtures only.
  • Added segmentation endpoints:
    • GET /api/v1/segmentation/models
    • POST /api/v1/segmentation/run
    • GET /api/v1/segmentation/runs
    • GET /api/v1/segmentation/runs/{analysis_run_id}
    • GET /api/v1/segmentation/runs/{analysis_run_id}/segmentations
    • GET /api/v1/segmentation/runs/{analysis_run_id}/geojson
    • POST /api/v1/segmentation/runs/{analysis_run_id}/qa/reference
  • Real SAM and YOLO-seg inference are not enabled in Sprint 9.
  • Mask paths are provenance/debug artifacts; persisted PostGIS geometry is authoritative for QA, map display and GeoJSON.

Sprint 17 additions

  • Added export foundation backed by the existing exports table.
  • GeoJSON exports now persist export records and write JSON artifacts for:
    • vector datasets
    • detection analysis runs
    • segmentation analysis runs
  • Added project metadata JSON export for project, dataset and QA/QC summary state.
  • Added export read/list/content endpoints:
    • POST /api/v1/exports/geojson
    • POST /api/v1/exports/metadata
    • GET /api/v1/exports/projects/{project_id}/exports
    • GET /api/v1/exports/{export_id}
    • GET /api/v1/exports/{export_id}/content
  • Exported detection and segmentation GeoJSON is generated from persisted first-class geometry rows.
  • No new migrations, product lines, live providers or AI dependencies are introduced by this export pass.
  • Old offline demo export artifacts can be inspected with python scripts/cleanup_demo_artifacts.py and removed only with an explicit --apply. The script keeps the newest exports per demo project, refuses to delete files outside STORAGE_ROOT, and blocks apply runs above --max-delete until the cap is raised after a dry-run review. Use repeated --export-type values to target only specific artifact kinds. In Docker, use docker compose exec -T backend python scripts/cleanup_demo_artifacts.py.
  • Live cleanup validation is available with bash scripts/verify_demo_cleanup_dry_run.sh. It runs the same maintenance path without --apply and fails if the summary reports anything other than a dry-run with zero deleted exports/files.

Run locally

Prerequisites

  • Python 3.11+
  • PostgreSQL with PostGIS

Install dependencies

cd backend
python -m pip install -e .[dev]

Optional AI dependencies for configured local YOLO inference:

cd backend
python -m pip install -e .[ai]

Docker and Unraid builds keep AI dependencies disabled by default. To build an image with local PyTorch/Ultralytics support, set:

GEOINTEL_INSTALL_AI=true

The default remains false so normal GIS deployments do not install the large AI runtime. GeoIntel still requires an explicit local model path and never downloads weights automatically.

AI-enabled Docker images include the native OpenCV runtime libraries required by Ultralytics. Dependency availability is checked with real torch and ultralytics imports, so missing shared libraries are reported as dependency_unavailable instead of being treated as configured. Docker/Unraid runtimes set YOLO_CONFIG_DIR to a writable storage path so Ultralytics does not attempt to write settings under the root user config directory.

Configured YOLO requires:

YOLO_ENABLED=true
YOLO_MODELS_DIR=/absolute/path/to/models
YOLO_MODEL_PATH=/absolute/path/to/local-model.pt

Optional local model compatibility smoke:

python scripts/yolo_preflight.py --model-path /absolute/path/to/local-model.pt --tile-manifest-path /absolute/path/to/manifest.json --check-model-load --json

In Docker, run the same smoke through the backend container:

docker compose exec -T backend python scripts/yolo_preflight.py --model-path /absolute/path/to/local-model.pt --tile-manifest-path /absolute/path/to/manifest.json --check-model-load --json

In the all-in-one Unraid runtime, place model files under the configured models directory, mounted as /app/models by default:

GEOINTEL_MODELS_PATH=/mnt/user/appdata/geointel/models
YOLO_ENABLED=true
YOLO_MODELS_DIR=/app/models
YOLO_MODEL_PATH=/app/models/local-model.pt

The root helper can write those values safely after a local model is placed:

python scripts/configure_yolo_model.py \
  --models-dir /mnt/user/appdata/geointel/models \
  --env-file /mnt/user/appdata/geointel/.env \
  --apply

When a promotion report recommends an exact model/tile/threshold candidate, prefer the guarded activation helper. It validates the report, checks the local model asset and writes .env only when --apply is supplied:

python scripts/activate_promoted_yolo_candidate.py \
  --promotion-report /mnt/user/appdata/geointel/artifacts/detection-model-promotion/split-aware/aoi1024bg512r3e50-high-threshold-split-20260710T222934Z/detection_model_promotion_report.json \
  --candidate-key 'geointel-building-yolov8s-aoi1024bg512r3e50-pt|512|64|0.35' \
  --models-dir /mnt/user/appdata/geointel/models \
  --env-file /mnt/user/appdata/geointel/.env \
  --json

Add --apply only after reviewing the emitted env updates. The smoke and activation helpers load no model by default, run no inference and do not download weights. Restart or rebuild the runtime after applying because the active model is read from YOLO_MODEL_PATH.

Operator-only local training preparation is available when real public model candidates are too weak for the target imagery. It is not a browser feature and does not change API contracts:

docker exec -it geointel python3 /app/scripts/export_operator_yolo_dataset.py \
  --manifest-path /app/storage/operator-data/operator_samples_manifest.json \
  --output-dir /app/storage/operator-data/yolo-building-dataset \
  --val-samples turnhout \
  --force

In an AI-enabled runtime with an existing local base model:

docker exec \
  -e OPERATOR_YOLO_DATASET_DIR=/app/storage/operator-data/yolo-building-dataset \
  -e YOLO_BASE_MODEL_PATH=/app/models/yolov8n.pt \
  -e TRAIN_MODEL_OUTPUT_PATH=/app/models/geointel-building-detector.pt \
  -e TRAIN_EPOCHS=8 \
  -e TRAIN_IMGSZ=512 \
  -e TRAIN_BATCH=2 \
  -e TRAIN_WORKERS=0 \
  -e TRAIN_DEVICE=cpu \
  -e PYTHON_BIN=python3 \
  geointel bash /app/scripts/train_operator_yolo_detector.sh

The exporter creates a YOLO dataset.yaml plus image/label folders from the explicit operator sample manifest. The training wrapper writes training_summary.json and a local .pt artifact, which still must be validated through model preflight and the real-data QA matrix before use.

For a larger tile-level training set, use overlapping windows instead of one image per AOI:

docker exec -it geointel python3 /app/scripts/export_operator_yolo_tile_dataset.py \
  --manifest-path /app/storage/operator-data/operator_samples_manifest.json \
  --output-dir /app/storage/operator-data/yolo-building-tile-dataset \
  --tile-size 192 \
  --stride 96 \
  --negative-keep-ratio 0.5 \
  --val-samples turnhout \
  --force

Then point OPERATOR_YOLO_DATASET_DIR at /app/storage/operator-data/yolo-building-tile-dataset and keep the same training wrapper. Tile-level output remains operator tooling outside the V1 browser product.

Use --samples (or OPERATOR_YOLO_SAMPLES) when an experiment needs a deliberate manifest subset. The generated summary records the source manifest count plus selected and excluded sample slugs. Unknown samples and any selected manifest holdout that is omitted from --val-samples fail before files are written.

The backend also exposes a read-only model asset catalog for the mounted model directory:

curl http://localhost:1202/api/v1/detection/model-assets

The catalog lists local .pt, .onnx and .engine files with size, SHA-256 and active-model status. Detection runs may submit model_asset_id with model_id="yolo-configured" to use a cataloged local model for that run. The backend resolves the ID to a file inside YOLO_MODELS_DIR; browser clients do not send arbitrary model paths.

Configured YOLO inference uses raster tile artifacts from the existing tile manifest flow. Single-band or otherwise non-RGB tile images are converted to a temporary RGB prediction image before inference; georeferencing still comes from the persisted tile manifest transform/bounds metadata.

Optional tuning:

YOLO_MODEL_ID=yolo-configured
YOLO_MODEL_DISPLAY_NAME="Configured YOLO detector"
YOLO_MODEL_VERSION=local-v1
YOLO_MODELS_DIR=/app/models
YOLO_CONFIG_DIR=/app/storage/ultralytics
YOLO_DEVICE=cpu
YOLO_IMAGE_SIZE=640
YOLO_MAX_TILES=100
YOLO_MAX_DETECTIONS=1000
YOLO_DUPLICATE_IOU_THRESHOLD=0.5
YOLO_BATCH_SIZE=1

YOLO local preflight

Sprint 13 adds a local-only preflight for configured YOLO paths:

python scripts/yolo_preflight.py --model-path /absolute/path/to/local-model.pt --tile-manifest-path /absolute/path/to/manifest.json

Machine-readable output:

python scripts/yolo_preflight.py --model-path /absolute/path/to/local-model.pt --tile-manifest-path /absolute/path/to/manifest.json --json

To validate only local model/manifest paths on a machine without optional AI dependencies:

python scripts/yolo_preflight.py --model-path /absolute/path/to/local-model.pt --tile-manifest-path /absolute/path/to/manifest.json --assume-dependencies --json

The preflight checks configuration, dependency availability, local model file existence, tile manifest validity, tile count and referenced tile paths. JSON output also includes runtime diagnostics for the model directory, YOLO_CONFIG_DIR, installed torch/ultralytics versions and CUDA availability when dependency checks pass. It does not load a YOLO model, run inference or download weights.

YOLO_MAX_DETECTIONS is forwarded to Ultralytics as max_det. The default is 1000 because dense building AOIs can exceed the upstream default cap of 300 detections before QA/QC can measure recall honestly.

YOLO_DUPLICATE_IOU_THRESHOLD controls GeoIntel-side cross-tile duplicate suppression after YOLO pixel boxes are converted to EPSG:4326 polygons and before Detection rows are persisted. Candidates are sorted by confidence per class; lower-confidence same-class candidates with geometry IoU greater than or equal to the threshold are suppressed. The default is 0.5; set 0 to disable this post-processing for debugging.

The same read-only status is available through the API and Detection Lab UI:

curl http://localhost:1202/api/v1/detection/yolo/preflight

To validate the full configured-YOLO runtime path against Docker/Tower after a model is mounted and selected, run:

bash scripts/verify_model_asset_detection_workflow.sh http://192.168.10.150:1202

The smoke uses the existing demo raster to generate a tile manifest, selects a cataloged local model asset, verifies read-only preflight, submits the existing detection run endpoint and checks persisted AnalysisRun, Detection list and Detection GeoJSON output. It does not download weights or inject detector fixtures. A zero detection result is still a valid runtime smoke outcome on the synthetic demo raster.

To validate the configured building model on operator-provided GIS data, mount or copy a real georeferenced raster and a real reference-building GeoJSON onto the runtime host, then run:

REAL_RASTER_PATH=/mnt/user/appdata/geointel/data/orthophoto.tif \
REAL_REFERENCE_VECTOR_PATH=/mnt/user/appdata/geointel/data/reference-buildings.geojson \
bash scripts/verify_real_data_detection_qa_workflow.sh http://192.168.10.150:1202

This smoke refuses missing/unsupported inputs, uploads the raster and reference dataset through the normal dataset service, generates raster tiles, selects a local model asset, runs configured YOLO detection, compares persisted detections against persisted vector_features, persists QA/QC rows and exports the detection GeoJSON. It never seeds demo detections, enables fixture mode, fetches live providers or downloads model weights. Configured-YOLO model class labels are normalized to lowercase for filtering and persisted detections while the original model label is retained in detection provenance. Raster tile manifests generated for AI handoff include source CRS metadata so pixel-space model outputs can be transformed to WGS84 GeoJSON coordinates. Current V1 upload support is limited to GeoTIFF-style rasters and GeoJSON/JSON reference vectors.

When REAL_AREA_BBOX=minx,miny,maxx,maxy is supplied, the same workflow also persists an EPSG:4326 project Area before uploading data. REAL_AREA_NAME and REAL_PROJECT_REGION retain operator context. The multi-sample runner fills these values from manifest wgs84_bbox and municipality metadata, so generated projects are immediately usable in the map without an alternate persistence path or API contract.

To prepare the documented operator sample corpus inside the all-in-one runtime container, run:

docker exec -it geointel python3 /app/scripts/prepare_operator_real_data_samples.py

The helper writes GeoTIFF orthophotos, GRB GBG building GeoJSON files and operator_samples_manifest.json under /app/storage/operator-data. In addition to the established positive and background AOIs, the registry contains Beerse, Rijkevorsel, Hoogstraten and Vorselaar as focused small-building training AOIs. Vosselaar and Grobbendonk are independent validation AOIs and must not be exported into the training split. Background candidates can persist empty GRB FeatureCollections for negative-tile training; normal reference AOIs still fail when GRB returns no buildings. These are runtime artifacts only and are not committed to Git.

Mol additionally has operational holdouts for Achterbos, Gompel, Donk and Postel, with Mol center as the historical baseline and Postel-bos as a separate background control. Prepare and execute that pack with the documented prepare_operator_real_data_samples.py and run_mol_operational_validation.sh commands in scripts/README.md. The runner produces a coverage-aware operational decision report: canonical footprint-IoU metrics remain authoritative, reference-envelope matches remain diagnostic, and no report can activate or mutate a model asset.

For municipality-wide navigation, run /app/scripts/provision_mol_municipality_workspace.py inside the all-in-one container. It verifies the official Mol boundary (NIS 13025), pages and clips all GRB GBG buildings, records checksums/provenance under persistent operator storage and imports both datasets through the existing HTTP service boundary. The command is explicit and idempotent; it is never executed during backend startup. See scripts/README.md for exact usage and refresh controls.

The current recommended local building model is geointel-building-yolov8s-smallbld-minpx3-img640-ft30-pt with tile size 512, overlap 64 and confidence threshold 0.15. Its SHA256 is a9088b8491dfae36694b53e9e9406cb4e3511d334a5712fa34f75078a47759c1. The promotion evidence covers seven positive AOIs at QA match IoU 0.25 and three pure-empty background AOIs. The model improves recall and persisted false-negative counts, but has lower precision than the previous balanced model; operators must review and persist QA/QC rather than treating detections as ground truth.

The latest coverage-aligned rerun of this exact profile measured mean precision 0.6141, recall 0.6062 and F1 0.6069 over Mol Achterbos, Donk, Gompel and Postel plus Retie, Turnhout and Westerlo. The three pure-empty controls remained at zero detections. A reviewed six-AOI fine-tuning challenger reached mean F1 0.6248 but remained inactive because it produced two false detections in the Postel-bos empty control.

For model-quality calibration, run the confidence sweep wrapper:

REAL_RASTER_PATH=/mnt/user/appdata/geointel/storage/operator-data/geel_orthophoto_wms_512.tif \
REAL_REFERENCE_VECTOR_PATH=/mnt/user/appdata/geointel/storage/operator-data/geel_grb_gbg_buildings.geojson \
CALIBRATION_THRESHOLDS="0.50 0.35 0.25 0.15" \
bash scripts/run_detection_calibration_sweep.sh http://192.168.10.150:1202

The sweep creates one real persisted workflow run per threshold, fetches the persisted QualityCheck/Metric rows and writes a calibration_summary.json with persisted detection count, raw candidate count, suppressed duplicate count, duplicate IoU threshold, score, precision, recall, F1, mean IoU and false positive/negative counts. It is intended to tune confidence/IoU/model choices, not to add new inference behavior.

To compare local model assets and tile settings as well as thresholds, run the quality matrix wrapper:

REAL_RASTER_PATH=/mnt/user/appdata/geointel/storage/operator-data/geel_orthophoto_wms_512.tif \
REAL_REFERENCE_VECTOR_PATH=/mnt/user/appdata/geointel/storage/operator-data/geel_grb_gbg_buildings.geojson \
QUALITY_MODEL_ASSET_IDS="yolov8n-building-segmentation-pt yolov8n-pt" \
QUALITY_TILE_SIZES="512 640" \
QUALITY_TILE_OVERLAPS="64" \
QUALITY_THRESHOLDS="0.50 0.15" \
bash scripts/run_detection_quality_matrix.sh http://192.168.10.150:1202

The matrix creates one real persisted workflow run per combination and writes quality_matrix_summary.json with the selected model asset, tile size, tile overlap, threshold, detection count, QA score, precision, recall, F1, mean IoU and false-positive/false-negative counts. It ranks best_by_score, best_by_recall and best_by_precision. It does not download weights, create fake detections, fetch live providers or change backend API behavior.

To aggregate the same matrix over every prepared operator sample, run:

OPERATOR_SAMPLE_MANIFEST_PATH=storage/operator-data/operator_samples_manifest.json \
QUALITY_MODEL_ASSET_IDS="yolov8n-building-segmentation-pt yolov8n-pt" \
QUALITY_TILE_SIZES="512 640" \
QUALITY_TILE_OVERLAPS="64" \
QUALITY_THRESHOLDS="0.50 0.15" \
bash scripts/run_multi_sample_detection_quality_matrix.sh http://192.168.10.150:1202

The combined multi_sample_quality_summary.json reports per-sample and overall best configurations. It is an operator benchmarking command, not a backend API or provider import path.

Before promoting any local model as a default, also run the hard-negative matrix against the documented background candidates:

OPERATOR_SAMPLE_MANIFEST_PATH=storage/operator-data/operator_samples_manifest.json \
OPERATOR_BACKGROUND_SAMPLE_SLUGS="postel_bos lommel_heide kasterlee_bos" \
QUALITY_MODEL_ASSET_IDS="geointel-building-yolov8n-expanded160e50-pt geointel-building-yolov8n-tile30-pt yolov8s-building-segmentation-pt" \
QUALITY_TILE_SIZES="640" \
QUALITY_TILE_OVERLAPS="64" \
QUALITY_THRESHOLDS="0.25 0.15 0.05" \
bash scripts/run_operator_hard_negative_detection_matrix.sh http://192.168.10.150:1202

This path uploads only background rasters, runs configured-YOLO detection and counts detections as false-positive pressure. It does not upload reference vectors or run QA/QC, so it cannot produce fake precision/recall metrics for empty background AOIs.

To inspect the evidence behind a calibration run, export the persisted QA evidence bundle:

CALIBRATION_SUMMARY_PATH=/mnt/user/appdata/geointel/artifacts/detection-calibration/20260707T002103Z/calibration_summary.json \
bash scripts/export_detection_calibration_evidence.sh http://192.168.10.150:1202

The bundle writes combined QA evidence GeoJSON plus a standalone HTML/SVG review artifact that separates matched detections, matched references, false positives and false negatives by role. It reads existing persisted QualityCheck evidence only and does not rerun inference.

For source-image review of false negatives, run scripts/render_detection_false_negative_review_contact_sheets.py against a fixed-threshold evidence portfolio. It uses the selected run's persisted tile manifest, overlays candidate/reference context and explicitly exports reference features outside tile coverage. The command is read-only and never changes QualityCheck, Metric, Detection or model state.

Run backend

cd backend
python -m uvicorn app.main:app --reload

Run backend tests

cd backend
python -m pytest

For warning-sensitive release checks, the backend is expected to pass with Python deprecation warnings promoted to errors for the timestamp-heavy service paths:

cd backend
python -m pytest -W error::DeprecationWarning tests/test_geojson_dataset_service.py tests/test_qa_service.py tests/test_sprint7a_persistence_foundation.py tests/test_sprint8c_detection_visualization_qa.py tests/test_sprint9_segmentation_foundation.py tests/test_vector_operations_service.py

The repository readiness gate now applies the same warning policy to the full backend suite:

bash scripts/run_readiness_check.sh

That readiness gate also runs the API contract smoke check before backend/frontend compilation and tests.

Golden QA/QC benchmark

Sprint 12 includes a deterministic QA/QC regression benchmark using explicit fixture data:

python scripts/run_golden_qa_benchmark.py

Machine-readable output:

python scripts/run_golden_qa_benchmark.py --json

Shell wrapper used by release-readiness checks:

bash scripts/verify_golden_qa_benchmark.sh

The benchmark compares fixtures/golden/predicted_buildings.geojson against fixtures/golden/reference_buildings.geojson and fails on metric drift. Expected baseline:

  • precision: 0.5
  • recall: 0.5
  • F1: 0.5
  • mean IoU: 0.8339768339761133
  • false positives: 1
  • false negatives: 1

The command uses existing QA/QC service logic and verifies QualityCheck/Metric persistence through an in-memory test session. It does not require live providers, AI models, Docker or PostGIS.

scripts/run_readiness_check.sh runs this benchmark automatically, so any change that alters the golden QA/QC metric baseline must update the fixture and expected metrics deliberately.

Demo workflow seed

Sprint 15 adds an explicit offline demo workflow seed. It creates or returns a demo project, AOI, fixture reference buildings, fixture candidate buildings and a persisted QA/QC result. It does not fetch live GRB/OSM data and does not run AI inference.

API:

curl -X POST http://localhost:1202/api/v1/demo/workflow

CLI:

python scripts/seed_demo_workflow.py --json

In Docker Compose on a LAN host:

curl -X POST http://192.168.10.150:1202/api/v1/demo/workflow

QA/QC result listing

Persisted project quality checks and metric rows can be listed with:

curl http://localhost:1202/api/v1/projects/{project_id}/quality-checks

The frontend QA/QC Results panel uses this endpoint after loading the demo workflow or running QA.

Detection QA evidence can be reviewed without changing its persisted metrics:

curl "http://localhost:1202/api/v1/projects/{project_id}/quality-checks/{quality_check_id}/reviews?reviewed=false&limit=50"

curl -X POST "http://localhost:1202/api/v1/projects/{project_id}/quality-checks/{quality_check_id}/reviews" \
  -H "Content-Type: application/json" \
  -d '{"evidence_role":"false_positive","evidence_feature_id":"DETECTION_UUID","decision":"qa_alignment_mismatch","notes":"Box and footprint represent the same building."}'

The list is derived from persisted quality-check evidence and paginates at a maximum of 200 rows. The upsert verifies project ownership, quality-check type, role-specific decisions and persisted Detection/VectorFeature ownership. detection_reviews never mutates model output, reference geometry or canonical Metric rows. Evidence GeoJSON queries only stored evidence ids instead of a complete regional GRB dataset.

Export foundation

Persisted exports can be created from the existing workbench state:

curl -X POST http://localhost:1202/api/v1/exports/metadata \
  -H "Content-Type: application/json" \
  -d '{"project_id":"PROJECT_UUID"}'

Vector dataset GeoJSON export:

curl -X POST http://localhost:1202/api/v1/exports/geojson \
  -H "Content-Type: application/json" \
  -d '{"export_kind":"dataset","dataset_id":"DATASET_UUID"}'

Detection or segmentation run GeoJSON export:

curl -X POST http://localhost:1202/api/v1/exports/geojson \
  -H "Content-Type: application/json" \
  -d '{"export_kind":"detection_run","analysis_run_id":"ANALYSIS_RUN_UUID"}'

List and inspect exports:

curl http://localhost:1202/api/v1/exports/projects/PROJECT_UUID/exports
curl http://localhost:1202/api/v1/exports/EXPORT_UUID/content

Download an artifact as a browser/file response:

curl -OJ http://localhost:1202/api/v1/exports/EXPORT_UUID/download

Create a lightweight HTML project report artifact:

curl -X POST http://localhost:1202/api/v1/exports/report \
  -H "Content-Type: application/json" \
  -d '{"project_id":"PROJECT_UUID"}'

The report contains project, dataset, QA/QC summary and export history state only. It is not a PDF designer and does not add a separate reporting module.

After rebuilding a Docker/LAN deployment, verify the end-to-end demo and export flow through the browser-facing frontend proxy:

bash scripts/verify_demo_export_workflow.sh http://192.168.10.150:1202

The script seeds the explicit demo workflow, verifies persisted QA/QC results, creates metadata/report/vector GeoJSON exports, lists exports and downloads the JSON/GeoJSON/HTML artifacts.

Backend import smoke

cd backend
python -c "from app.main import app; print(app.title)"

Dockerized backend

docker compose up --build backend db

The Docker Compose stack does not require a root .env file for the default local runtime. The database service exposes a container-internal Postgres healthcheck, and the backend also runs docker_start.sh, which retries an actual SQL SELECT 1 connection before running python -m alembic upgrade head and starting Uvicorn.

PostGIS is not published on the host 5432 port by default. This avoids conflicts with existing Postgres/PostGIS services on NAS or server hosts. The backend connects over Docker networking with db:5432.

Backend and frontend Docker build contexts exclude dependency folders, build outputs and Python bytecode caches via .dockerignore.

The Docker Compose frontend is published at http://localhost:1202.

Compose healthchecks are enabled for all runtime services:

  • db uses pg_isready.
  • backend checks http://127.0.0.1:8000/health inside the container.
  • frontend checks http://127.0.0.1/health through nginx, which also verifies the frontend-to-backend proxy path.

The frontend waits for a healthy backend before starting. Check runtime state:

docker compose ps
docker compose logs --tail=80 backend
docker compose logs --tail=80 frontend

The backend Docker image installs the approved GIS runtime extra (.[gis]) so browser-facing Docker deployments can report raster/vector processing capabilities accurately:

  • rasterio
  • numpy
  • pillow
  • geopandas
  • pyogrio
  • GDAL/GEOS/PROJ system libraries

After rebuilding the backend image, verify the LAN/browser runtime from the repository root:

bash scripts/verify_gis_runtime.sh http://localhost:1202

On a NAS or server host, use the published LAN URL:

bash scripts/verify_gis_runtime.sh http://192.168.10.150:1202

The script calls /api/v1/system/capabilities through the frontend proxy and fails if postgis, rasterio or geopandas are not reported as available.

The backend Docker build also runs:

python scripts/gis_import_smoke.py

Inside the backend Docker build context this resolves to backend/scripts/gis_import_smoke.py. The root scripts/gis_import_smoke.py wrapper calls the same smoke locally. The smoke imports rasterio, geopandas and pyogrio; if one of those imports fails, the backend image build fails before deployment.

Live Docker/PostGIS migration smoke

Sprint 11 validates the real PostGIS runtime path with the existing database service. From the repository root:

docker compose config
docker compose up -d db
DATABASE_URL=postgresql+psycopg://geointel:geointel@localhost:5432/geointel bash scripts/live_migration_smoke.sh

The smoke script:

  • opens a backend SQLAlchemy connection and runs SELECT 1
  • runs alembic upgrade head
  • checks PostGIS_Version() after migrations have created the extension
  • verifies one Alembic head
  • verifies required migrated tables and GiST indexes exist

Expected local environment:

DATABASE_URL=postgresql+psycopg://geointel:geointel@localhost:5432/geointel

If the database is not reachable, confirm Docker Desktop is running and that port 5432 is not already occupied. To clean up the local database container without deleting the named volume:

docker compose stop db

To remove the local PostGIS volume as well, use only when you explicitly want a fresh database:

docker compose down -v

Key docs

  • docs/API_CONTRACTS.md
  • docs/DATABASE_IMPLEMENTATION_PLAN.md
  • docs/DEFINITION_OF_DONE.md
  • docs/40-build-launch/SPRINT_1_SCOPE_FREEZE.md

Raster dependency note

Raster metadata and raster operations depend on local GDAL/rasterio availability.

To enable raster processing locally:

python -m pip install rasterio

If rasterio is unavailable:

  • raster metadata responses return 503 with RASTER_PROCESSING_UNAVAILABLE
  • raster clip/tile endpoints return explicit unavailable responses

Export report artifact

POST /api/v1/exports/report creates the existing lightweight project_report_html artifact. The report is a self-contained HTML handoff view rendered from persisted project, dataset, QA/QC and export-history state. It includes readiness scorecards, dataset inventory, QA/QC evidence, artifact history, known limitations and print-friendly CSS.

This remains a simple HTML export. It does not add a PDF designer, report builder, live provider fetching or new analysis behavior.

Vector area selection

POST /api/v1/projects/{project_id}/datasets/{dataset_id}/vector/select runs a read-only EPSG:4326 bbox query against persisted PostGIS vector_features and returns a canonical-envelope GeoJSON FeatureCollection. It is intended for the Map workspace area-extract flow and does not create derived datasets or export records by itself.

The same bounded endpoint is the canonical large-layer map delivery path. The frontend requests at most 1,000 features for the current viewport and surfaces the response truncated flag; the backend does not provide or imply an unbounded municipality-wide map response.

Selection summaries expose a primary metric plus an additive metrics list. Known persisted themes are aggregated in EPSG:31370: building footprints, forest, water surfaces and parcels return hectares; roads and linear watercourses return kilometres; population keeps its configured inhabitant aggregation. Intersecting feature counts remain available as supporting evidence. Water volume is deliberately unavailable because the current GRB source has no reliable depth/bathymetry dimension; GeoIntel does not manufacture volume from 2D polygons.

POST /api/v1/projects/{project_id}/datasets/{dataset_id}/vector/select/derive uses the same persisted vector_features selection but writes the result as a new derived vector dataset. The created dataset uses source="operation:selection", source_name="map_selection" and derived_from_dataset_id for source provenance, stores a GeoJSON artifact and indexes its features back into vector_features for later QA/QC and analysis.

POST /api/v1/exports/geojson with export_kind="vector_selection" persists the same bbox-selected FeatureCollection as a normal export record with export_type="vector_selection_geojson". This creates a handoff artifact only; it does not create a derived dataset.

Geographic scope provisioning

The explicit operator command below provisions the official 28-municipality Vlaamse vervoerregio Kempen boundary foundation:

docker exec geointel python /app/scripts/provision_geographic_scope.py \
  --scope kempen-transport-region

It reads current VRBG/Refgem boundaries, validates every registered name and NIS code, unions the regional geometry and creates one project, one regional Area, 28 municipality Areas and two source datasets through the public API. It never writes directly to PostGIS and does not run on startup. The persisted scope limitation explicitly distinguishes the transport-policy region from a cultural or landscape definition of Kempen.

Use --fetch-only for a source/geometry/checksum audit. The scope pass does not fetch thematic GRB, population or land-use data; those remain separate, bounded operator jobs.

Provision the regional GRB building theme after the scope pass:

docker exec geointel python /app/scripts/provision_regional_grb_buildings.py \
  --scope kempen-transport-region

The operator retains 28 checksummed municipality partitions but exposes one normal regional reference dataset. StorageService copies the combined artifact without materializing it as upload bytes; DatasetService creates the Dataset and immutable DatasetVersion; VectorFeatureService validates and flushes partition features in bounded batches. The transaction must index the exact manifest feature count or it rolls back and removes the managed copy. No public API contract or provider readiness claim is changed by this operator-only path.

Provision the regional current road, water and parcel context through the same persistence boundary:

docker exec geointel python /app/scripts/provision_regional_grb_context.py \
  --scope kempen-transport-region --layers roads water parcels

The operator keeps one resumable municipality partition set per theme and creates one regional reference Dataset per theme. Polygon ownership uses maximum overlap area; line ownership uses maximum overlap length. It preserves source geometry dimensions and collection-qualified source IDs, copies the combined artifact through StorageService and indexes bounded batches through DatasetService/VectorFeatureService. It does not add API routes, direct SQL or interactive provider downloads.

Temporal Mol data and evolution

Dataset uploads accept temporal_series_key, observed_at, valid_from, valid_to, temporal_granularity and source_version. Every new source or derived dataset also writes dataset version 1 in the same transaction.

After the Mol municipality workspace is available, import the official source snapshots explicitly:

docker exec geointel python /app/scripts/provision_mol_population_history.py
docker exec geointel python /app/scripts/provision_mol_historical_landuse.py
docker exec geointel python /app/scripts/provision_official_landuse_timeseries.py

The first command imports Statbel sector population for 2021-2025. The second imports Digitaal Vlaanderen historical land use for 1778, 1873 and 1969. The third imports the Departement Omgeving 10 m forest class for 2013, 2016, 2019, 2022 and 2025. All commands are idempotent, use the normal API/DatasetService flow and retain fetched artifacts in persistent operator storage. They never run on app startup.

Historical land-use work can be bounded explicitly:

docker exec geointel python /app/scripts/provision_mol_historical_landuse.py --years 1778 1969 --themes forest water

GET /api/v1/projects/{project_id}/temporal/series discovers the series and POST /api/v1/projects/{project_id}/temporal/compare compares two snapshots inside one EPSG:4326 bbox. Partial statistical sectors are estimates; old map editions without stable identities do not produce invented object changes. Modern raster-derived forest polygons have the same identity limitation. Their area is measured in EPSG:31370 and is exact within the 10 m source representation, not a cadastral forest survey.

The same source-governed operators can synchronize the approved regional scope in one explicit pass:

docker exec geointel python /app/scripts/provision_regional_timeseries.py

This resolves the retained official boundary and imports five Statbel population snapshots plus five modern forest, water, built-function and transport-infrastructure snapshots into Kempen Regional Workbench. Mol and regional series keys remain separate and existing immutable datasets are reused. Complete statistical sectors use exact published totals; a rectangle cutting a sector remains an area-weighted estimate. Forest area is measured within the official 10 m representation. Use --fetch-only to validate source artifacts without database mutation. The regional forest path partitions WCS requests by official municipality to stay within upstream response limits, then builds one retained 10 m mosaic and one normal regional vector Dataset. A failed source request leaves completed partition artifacts reusable and never lowers source resolution silently.

Official operator datasets record that their geometries were clipped to the persisted Area. When that exact Area is selected, vector totals and aggregate metrics use the already clipped geometries directly rather than intersecting every row with the same detailed boundary again. This optimization is allowed only for matching Dataset/Area ids with explicit clipping metadata or a known clipping operator; drawn rectangles and ordinary uploads keep the normal exact PostGIS intersection path.

Local Ollama GIS assistant

The optional assistant is a read-only backend integration. It lists locally installed Ollama models, calculates the active Area/bbox metrics from persisted PostGIS features and sends only that compact JSON context to Ollama. It never downloads models, sends geometries or treats model prose as source data.

Configuration:

OLLAMA_ENABLED=true
OLLAMA_BASE_URL=http://host.docker.internal:11434
OLLAMA_DEFAULT_MODEL=qwen3.5:9b
OLLAMA_TIMEOUT_SECONDS=120
OLLAMA_MAX_OUTPUT_TOKENS=700
OLLAMA_CONTEXT_TOKENS=16384

The Unraid deployment adds host.docker.internal:host-gateway automatically. Verify the connection with GET /api/v1/assistant/status, inspect installed models with GET /api/v1/assistant/models and ask a grounded question through POST /api/v1/projects/{project_id}/assistant/query. A requested model must be present in Ollama /api/tags. Missing water depth/bathymetry remains explicit; the assistant cannot turn 2D water geometry into volume. GeoIntel rejects an answer when Ollama reports done_reason=length, so a visibly truncated sentence is never presented as a complete result.

Agricultural-use parcel history

Prepare all definitive 2008-2025 regional editions without database writes:

docker exec geointel python /app/scripts/provision_agricultural_parcel_history.py --fetch-only

Import the checked artifacts through the canonical Dataset upload route:

docker exec geointel python /app/scripts/provision_agricultural_parcel_history.py

Use --scope mol, --years 2008,2019,2025 or --force only as explicit operator choices. The default scope is the persisted 28-municipality Kempen transport region. Every annual source ZIP and crop code list remains under the storage volume. PostGIS computes exact hectares for drawn rectangles and persisted Areas; parcel identities are deliberately unavailable for lineage.

Buildings and Addresses Register snapshot

After the Mol Area and regional GRB buildings have been provisioned, prepare the official register evidence with:

docker exec geointel python /app/scripts/provision_buildings_addresses_register.py --fetch-only

Review the generated manifest and then persist through DatasetService:

docker exec geointel python /app/scripts/provision_buildings_addresses_register.py

The resulting building_registry Dataset uses ordinary EPSG:4326 vector_features; no register-specific table or direct operator database write exists. Exact PostGIS selection exposes footprint hectares, lifecycle counts, aggregate unit/address counts and GRB reconciliation counts. Raw address pages are checksummed storage evidence only. Address labels and house/box numbers are not copied into queryable properties.

Helpful repository scripts

  • bash scripts/backend_install.sh
  • bash scripts/backend_test.sh
  • bash scripts/backend_dev.sh
  • bash scripts/smoke_backend_import.sh

Bounded official orthophoto acquisition

GET /api/v1/projects/{project_id}/datasets/orthophoto/products lists the governed product allowlist. POST .../datasets/orthophoto/acquire accepts an explicit EPSG:4326 map rectangle plus product_key and stores the official Digitaal Vlaanderen WMS response as a canonical EPSG:31370 raster Dataset. The default safety envelope is 128-1,024 m per side, 1 m/pixel, 32 MiB and a 24-hour exact-request cache. It runs synchronously behind the existing Job abstraction and never during startup.

Available products cover the most recent winter image, annual winter mosaics for 2012-2025, three older winter periods, RGB 1979-1990 and panchromatic 1971. Historical products persist validity metadata and are deliberately excluded from configured-YOLO/current-GRB QA. GET .../datasets/{dataset_id}/raster/image is the constrained binary PNG endpoint used by the MapLibre image overlay.

Settings: ORTHOPHOTO_ENABLED, ORTHOPHOTO_WMS_URL, ORTHOPHOTO_WMS_LAYER, ORTHOPHOTO_RESOLUTION_M, ORTHOPHOTO_MIN_SIDE_M, ORTHOPHOTO_MAX_SIDE_M, ORTHOPHOTO_TIMEOUT_SECONDS, ORTHOPHOTO_MAX_RESPONSE_MB and ORTHOPHOTO_CACHE_TTL_HOURS. Keep the official HTTPS URL and 1 m profile unless a separately verified deployment/model profile requires a change.

Governed DHMV terrain acquisition

GET /api/v1/projects/{project_id}/datasets/dhmv/products exposes the fixed official DTM/DSM registry. POST .../datasets/dhmv/acquire requests only DHMVII_DTM_1m or DHMVII_DSM_1m from the production Digitaal Vlaanderen WCS. The default 5 m analysis copy keeps complete-Mol processing bounded while retaining native 1 m resolution, EPSG:31370, TAW, -9999 nodata and the 2013-2015 acquisition period in provenance.

Run the complete Mol operator after the regional workspace and Mol Area exist:

docker exec geointel python /app/scripts/provision_mol_dhmv.py

The operator acquires DTM and DSM, clips each raster to the exact persisted Area, validates checksums and calls the terrain selection endpoint as a smoke. Use --products dtm_1m, --resolution-m 5 or --force when explicitly needed. POST .../raster/terrain/select returns height in m TAW, relief in metres and slope in degrees. GET .../raster/terrain/image returns the constrained MapLibre PNG. Water depth, volume and drainage remain unavailable.

Settings: DHMV_ENABLED, DHMV_WCS_URL, DHMV_RESOLUTION_M, DHMV_MIN_SIDE_M, DHMV_MAX_SIDE_M, DHMV_MAX_PIXELS, DHMV_TIMEOUT_SECONDS and DHMV_MAX_RESPONSE_MB.

Waterinfo station histories

Run the explicit operator after the regional workspace and Mol Area exist:

docker exec geointel python /app/scripts/provision_waterinfo_station_history.py \
  --project-name "Kempen Regional Workbench" \
  --area-name "Gemeente Mol" \
  --from-year 2013 --to-year 2025

The command retains raw KiWIS JSON/checksums and imports only real annual observations through the canonical dataset upload API. Every station has its own temporal-series key. Water levels and discharges remain Point measurements; they are never averaged across stations or presented as municipal water volume. Use --fetch-only to prepare and audit artifacts without persistence.

BWK/Natura 2000 state 2025

Run the governed Mol operator after the regional workspace and Mol Area exist:

docker exec geointel python /app/scripts/provision_mol_bwk_natura2000.py

The command fetches the official INBO WFS, retains raw checksummed pages, clips in EPSG:31370 and imports through DatasetService. --fetch-only builds evidence without persistence. A conflicting checksum for an already persisted state-2025 Mol Dataset fails closed instead of creating a silent replacement. PostGIS selection summaries keep BWK value classes separate and label PHAB-derived habitat hectares as estimates.