document the analysis contracts and correct the job-queue claim
The README's architecture diagram and stack list promised Redis + RQ. There is no such thing in this codebase: redis_url is a settings field nothing reads, rq is not a dependency, and the only worker is an asyncio poller over a Postgres job table. Describe what exists. API_CONTRACTS and KNOWN_LIMITATIONS gain the rules a reader needs to interpret a result: deterministic confidence-ranked matching, the precision/recall curve, the three flood-hazard cell populations, the sub-cell selection fallback, the whole-feature object count next to clipped area metrics, and the required manifest CRS. The new limitations are stated as limits rather than buried: raster analysis has no sub-cell precision, and the object count is not fractional. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -131,7 +131,13 @@ YOLO_IMAGE_SIZE=640
|
|||||||
YOLO_MAX_TILES=100
|
YOLO_MAX_TILES=100
|
||||||
YOLO_MAX_DETECTIONS=1000
|
YOLO_MAX_DETECTIONS=1000
|
||||||
YOLO_DUPLICATE_IOU_THRESHOLD=0.5
|
YOLO_DUPLICATE_IOU_THRESHOLD=0.5
|
||||||
|
# Tiles per model.predict call. 1 leaves most of a modern GPU idle on a run of
|
||||||
|
# a hundred tiles; raise it until VRAM is the limit.
|
||||||
YOLO_BATCH_SIZE=1
|
YOLO_BATCH_SIZE=1
|
||||||
|
# Drop boxes truncated by an interior tile edge. Because tiles overlap, the
|
||||||
|
# neighbouring tile saw the same object whole, so the truncated half is a
|
||||||
|
# duplicate and a shape error at once. Boxes on the outer raster edge are kept.
|
||||||
|
YOLO_SUPPRESS_TILE_EDGE_DETECTIONS=true
|
||||||
|
|
||||||
# Local segmentation models. GeoIntel never downloads model weights
|
# Local segmentation models. GeoIntel never downloads model weights
|
||||||
# automatically; point these to existing local files to enable inference.
|
# automatically; point these to existing local files to enable inference.
|
||||||
@@ -176,3 +182,7 @@ GEOINTEL_CORS_ORIGINS=https://geointel.itworx.tech,http://geointel.itworx.tech,h
|
|||||||
GEOINTEL_MAX_UPLOAD_MB=500
|
GEOINTEL_MAX_UPLOAD_MB=500
|
||||||
GEOINTEL_AOI_WORKER_ENABLED=false
|
GEOINTEL_AOI_WORKER_ENABLED=false
|
||||||
GEOINTEL_AOI_WORKER_POLL_SECONDS=2
|
GEOINTEL_AOI_WORKER_POLL_SECONDS=2
|
||||||
|
# Executes queued detection.run / segmentation.run jobs from POST
|
||||||
|
# /detection/run-async, so tiled GPU inference never blocks an HTTP request.
|
||||||
|
GEOINTEL_ANALYSIS_WORKER_ENABLED=false
|
||||||
|
GEOINTEL_ANALYSIS_WORKER_POLL_SECONDS=2
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ verbergen.
|
|||||||
| Tijd | Immutable DatasetVersions en vergelijking van ruimtelijke snapshots |
|
| Tijd | Immutable DatasetVersions en vergelijking van ruimtelijke snapshots |
|
||||||
| QA/QC | Precision, recall, F1, IoU, false-positive/-negative evidence en reviews |
|
| QA/QC | Precision, recall, F1, IoU, false-positive/-negative evidence en reviews |
|
||||||
| Export | Reproduceerbare GeoJSON- en artefactexports met provenance |
|
| Export | Reproduceerbare GeoJSON- en artefactexports met provenance |
|
||||||
| Runtime | DockerMan-native Unraid-container met PostGIS, Redis, backend en frontend |
|
| Runtime | DockerMan-native Unraid-container met PostGIS, backend en frontend |
|
||||||
|
|
||||||
## Architectuur
|
## Architectuur
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ verbergen.
|
|||||||
flowchart LR
|
flowchart LR
|
||||||
UI["React + MapLibre workbench"] --> API["FastAPI contracts"]
|
UI["React + MapLibre workbench"] --> API["FastAPI contracts"]
|
||||||
API --> GIS["GeoPandas / Shapely / Rasterio"]
|
API --> GIS["GeoPandas / Shapely / Rasterio"]
|
||||||
API --> JOBS["Redis + RQ"]
|
API --> JOBS["Job-queue in PostgreSQL"]
|
||||||
JOBS --> AI["PyTorch / YOLO / SAM"]
|
JOBS --> AI["PyTorch / YOLO / SAM"]
|
||||||
GIS --> DB[("PostgreSQL + PostGIS")]
|
GIS --> DB[("PostgreSQL + PostGIS")]
|
||||||
AI --> DB
|
AI --> DB
|
||||||
@@ -118,12 +118,12 @@ PostGIS en de persistentieregels volgen
|
|||||||
- **Spatial:** GeoPandas, Shapely, Rasterio, PyProj en GDAL
|
- **Spatial:** GeoPandas, Shapely, Rasterio, PyProj en GDAL
|
||||||
- **AI:** PyTorch, Ultralytics YOLO en SAM-compatibele segmentatie
|
- **AI:** PyTorch, Ultralytics YOLO en SAM-compatibele segmentatie
|
||||||
- **Data:** PostgreSQL/PostGIS, lokale artefactopslag en immutable versions
|
- **Data:** PostgreSQL/PostGIS, lokale artefactopslag en immutable versions
|
||||||
- **Jobs:** Redis en RQ
|
- **Jobs:** job-tabel in PostgreSQL met achtergrondworkers in het API-proces
|
||||||
- **Deployment:** één DockerMan-native Unraid-container
|
- **Deployment:** één DockerMan-native Unraid-container
|
||||||
|
|
||||||
## Lokaal starten
|
## Lokaal starten
|
||||||
|
|
||||||
Vereisten: Python 3.11+, Node 20.19+ of 22.12+, PostgreSQL/PostGIS en Redis.
|
Vereisten: Python 3.11+, Node 20.19+ of 22.12+ en PostgreSQL/PostGIS.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m pip install -e backend/.[dev]
|
python -m pip install -e backend/.[dev]
|
||||||
|
|||||||
+110
-7
@@ -617,13 +617,39 @@ is not a temporal observation and receives no fabricated `observed_at` value.
|
|||||||
|
|
||||||
### POST `/api/v1/projects/{project_id}/datasets/{dataset_id}/raster/flood-hazard/select`
|
### POST `/api/v1/projects/{project_id}/datasets/{dataset_id}/raster/flood-hazard/select`
|
||||||
|
|
||||||
Returns mapped positive-depth area in hectares, share of the selection, mean,
|
Returns mapped positive-depth area in hectares, share of the *modelled* area,
|
||||||
P90 and maximum modeled local depth and `modelled_max_depth_area_integral_m3`.
|
mean, P90 and maximum modeled local depth and
|
||||||
Every result identifies mechanism, climate context, probability class and
|
`modelled_max_depth_area_integral_m3`. Every result identifies mechanism,
|
||||||
return period. The integral sums local modeled maximum depth times cell area;
|
climate context, probability class and return period. The integral sums local
|
||||||
it is explicitly not concurrent flood storage, permanent waterbody content,
|
modeled maximum depth times cell area; it is explicitly not concurrent flood
|
||||||
current water level or bathymetry. These unsupported metrics remain listed in
|
storage, permanent waterbody content, current water level or bathymetry. These
|
||||||
the response.
|
unsupported metrics remain listed in the response.
|
||||||
|
|
||||||
|
Three cell populations are reported separately, because conflating them turns
|
||||||
|
missing data into a claim of safety:
|
||||||
|
|
||||||
|
- `selected_cell_count` — cells inside the drawn selection;
|
||||||
|
- `valid_cell_count` / `no_data_cell_count` — the split between cells the VMM
|
||||||
|
raster models and cells it does not;
|
||||||
|
- `inundated_cell_count` — modelled cells with a positive depth.
|
||||||
|
|
||||||
|
`inundated_fraction` and `modelled_inundated_share_pct` are shares of the
|
||||||
|
modelled cells, not of the drawn selection. `inundated_fraction` is `null` when
|
||||||
|
nothing was modelled at all. `data_coverage_ratio`, `model_coverage_pct`,
|
||||||
|
`modelled_area_ha` and `selection_area_ha` make the difference between the
|
||||||
|
drawn area and the analysed area explicit, and `coverage_warning` states it in
|
||||||
|
words. A selection reaching past the modelled extent previously reported a
|
||||||
|
diluted risk share for the whole rectangle.
|
||||||
|
|
||||||
|
**Selections finer than one source cell.** `geometry_mask` selects a cell when
|
||||||
|
its centre falls inside the geometry, so a rectangle smaller than a cell — or
|
||||||
|
one landing between four centres — selected nothing and the analysis returned
|
||||||
|
zeros indistinguishable from "nothing here". Every raster selection now falls
|
||||||
|
back to the cells the geometry touches and reports that in
|
||||||
|
`cell_selection_warning` (`coverage_warning` for flood hazard), because the
|
||||||
|
result then covers more ground than was drawn. This applies to terrain,
|
||||||
|
bathymetry, thematic raster and flood hazard, in both the single-dataset and
|
||||||
|
the partitioned paths.
|
||||||
|
|
||||||
### POST `/api/v1/projects/{project_id}/datasets/raster/flood-hazard/select`
|
### POST `/api/v1/projects/{project_id}/datasets/raster/flood-hazard/select`
|
||||||
|
|
||||||
@@ -1056,6 +1082,21 @@ Return vector bounds and feature count.
|
|||||||
|
|
||||||
### POST `/api/v1/projects/{project_id}/datasets/{dataset_id}/vector/select`
|
### POST `/api/v1/projects/{project_id}/datasets/{dataset_id}/vector/select`
|
||||||
|
|
||||||
|
Selection metrics describe two different populations and now say so.
|
||||||
|
`intersection_area` and `intersection_length` clip each feature to the
|
||||||
|
selection, while the object count treats any feature touching the selection as
|
||||||
|
whole — which is what an operator expects from "objecten", but overstates the
|
||||||
|
count along every edge. The response therefore adds
|
||||||
|
`fully_covered_feature_count`, `partially_covered_feature_count` and
|
||||||
|
`selection_edge_warning`, and marks the count metric as an estimate whenever
|
||||||
|
the selection cuts features. Area and length metrics stay exact and do not
|
||||||
|
inherit that caveat.
|
||||||
|
|
||||||
|
Partitioned selection (`/datasets/vector/partitions/select`) de-duplicates on
|
||||||
|
`source_feature_id` across municipal partitions for the returned geometry as
|
||||||
|
well as for the count. A feature on a shared boundary was previously counted
|
||||||
|
once but drawn once per partition.
|
||||||
|
|
||||||
Read-only spatial selection over persisted `vector_features`.
|
Read-only spatial selection over persisted `vector_features`.
|
||||||
|
|
||||||
Request:
|
Request:
|
||||||
@@ -1619,6 +1660,44 @@ reproducibility. Clients must not submit arbitrary model paths.
|
|||||||
|
|
||||||
GeoIntel does not download model weights automatically. Configured YOLO runs read existing tile files from the manifest, convert YOLO pixel-space boxes to EPSG:4326 detection polygons and persist detections as first-class records.
|
GeoIntel does not download model weights automatically. Configured YOLO runs read existing tile files from the manifest, convert YOLO pixel-space boxes to EPSG:4326 detection polygons and persist detections as first-class records.
|
||||||
|
|
||||||
|
The manifest must carry explicit CRS metadata (`crs`, `source_crs` or
|
||||||
|
`dataset_crs`). A manifest without it fails with
|
||||||
|
`DETECTION_TILE_MANIFEST_INVALID` rather than being georeferenced against an
|
||||||
|
assumed EPSG:4326, which would place detections plausibly but wrongly.
|
||||||
|
|
||||||
|
Tiles are read with rasterio: the visible RGB bands are selected explicitly and
|
||||||
|
percentile-stretched to 8-bit, so 16-bit and 4-band (RGB + NIR) orthophotos
|
||||||
|
reach the model as the kind of image it was trained on. Tiles are predicted in
|
||||||
|
batches of `YOLO_BATCH_SIZE`.
|
||||||
|
|
||||||
|
Post-processing removes two artefacts of tiled inference:
|
||||||
|
|
||||||
|
- boxes truncated by an *interior* tile edge are dropped, because the
|
||||||
|
overlapping neighbouring tile observed the same object completely
|
||||||
|
(`YOLO_SUPPRESS_TILE_EDGE_DETECTIONS`, default on). Boxes against the outer
|
||||||
|
raster edge are kept;
|
||||||
|
- duplicates are suppressed on IoU *and* on intersection-over-smaller-area, so
|
||||||
|
an object wider than the tile overlap does not survive as two partial boxes.
|
||||||
|
|
||||||
|
`result_json` reports `raw_detection_count`, `suppressed_detection_count`,
|
||||||
|
`tile_edge_truncated_count`, `duplicate_iou_threshold` and
|
||||||
|
`containment_suppression_threshold`.
|
||||||
|
|
||||||
|
### POST `/api/v1/detection/run-async`
|
||||||
|
|
||||||
|
Same request body as `POST /api/v1/detection/run`, but queues the run instead of
|
||||||
|
executing it inside the request, and returns a `JobRead`. Tiled GPU inference
|
||||||
|
over up to `YOLO_MAX_TILES` tiles takes minutes; performing it in the request
|
||||||
|
holds a worker thread and times the client out. Cheap validation (project,
|
||||||
|
dataset, dataset type) still happens synchronously, so an invalid request is
|
||||||
|
rejected immediately rather than by a job that fails minutes later.
|
||||||
|
|
||||||
|
Queued jobs are executed by the background analysis worker
|
||||||
|
(`GEOINTEL_ANALYSIS_WORKER_ENABLED`, poll interval
|
||||||
|
`GEOINTEL_ANALYSIS_WORKER_POLL_SECONDS`), which claims a job before dispatching
|
||||||
|
it so the same run is never started twice. Poll `GET /api/v1/jobs/{id}` for
|
||||||
|
progress. `POST /api/v1/segmentation/run-async` behaves identically.
|
||||||
|
|
||||||
Unavailable model response:
|
Unavailable model response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -1759,6 +1838,30 @@ candidate geometry versus the persisted reference footprint. Envelope results
|
|||||||
are explicitly `diagnostic_only` and are persisted in
|
are explicitly `diagnostic_only` and are persisted in
|
||||||
`quality_checks.findings_json`; they never replace or inflate canonical metrics.
|
`quality_checks.findings_json`; they never replace or inflate canonical metrics.
|
||||||
|
|
||||||
|
`box_to_footprint_diagnostics.candidate_geometry_mode` reports whether the
|
||||||
|
candidates are `axis_aligned_boxes` or `footprint_polygons`. For a box detector
|
||||||
|
the strict footprint IoU has a ceiling below 1 on rotated or non-rectangular
|
||||||
|
buildings, and the response says so in `warnings` and in `interpretation`.
|
||||||
|
|
||||||
|
Matching is deterministic. Candidates are ranked by confidence, highest first,
|
||||||
|
with feature identity as tiebreaker, before the greedy IoU assignment. Database
|
||||||
|
row order is not usable for this: every detection in a run shares one
|
||||||
|
transaction timestamp, so ordering by `created_at` left the assignment — and
|
||||||
|
therefore the score and the false-positive evidence shown to a reviewer —
|
||||||
|
undefined between identical runs.
|
||||||
|
|
||||||
|
The response also returns `precision_recall_curve`: precision, recall and F1 at
|
||||||
|
every confidence value present in the run, plus `average_precision`, `best_f1`
|
||||||
|
and `best_f1_threshold`. A single F1 describes one operating point and cannot
|
||||||
|
compare two models whose calibration differs; the curve can. `average_precision`,
|
||||||
|
`best_f1` and `best_f1_threshold` are persisted as `metrics` rows alongside the
|
||||||
|
existing ones.
|
||||||
|
|
||||||
|
Segmentation QA (`POST /api/v1/segmentation/runs/{analysis_run_id}/qa/reference`)
|
||||||
|
applies the same tile-coverage clipping and returns the same `coverage` block.
|
||||||
|
Without it, every reference feature outside the inferred tiles counted as a
|
||||||
|
false negative and recall was understated by an arbitrary amount.
|
||||||
|
|
||||||
Configured-YOLO QA fails closed with `DETECTION_QA_COVERAGE_UNAVAILABLE` when
|
Configured-YOLO QA fails closed with `DETECTION_QA_COVERAGE_UNAVAILABLE` when
|
||||||
manifest provenance is absent, `DETECTION_QA_COVERAGE_MISMATCH` when it belongs
|
manifest provenance is absent, `DETECTION_QA_COVERAGE_MISMATCH` when it belongs
|
||||||
to another raster, `DETECTION_QA_COVERAGE_INVALID` when bounds/CRS are invalid,
|
to another raster, `DETECTION_QA_COVERAGE_INVALID` when bounds/CRS are invalid,
|
||||||
|
|||||||
@@ -43,6 +43,16 @@ runtime source of truth.
|
|||||||
underlying named Area itself is wholly valid.
|
underlying named Area itself is wholly valid.
|
||||||
- Cross-region and land/sea selections stay split by legal/source zone.
|
- Cross-region and land/sea selections stay split by legal/source zone.
|
||||||
Semantically incompatible metrics are not merged.
|
Semantically incompatible metrics are not merged.
|
||||||
|
- Raster analysis is cell-based. A selection smaller than one source cell is
|
||||||
|
answered over the cells it touches, so the analysed area is larger than the
|
||||||
|
drawn area; the response says so rather than returning zeros. Sub-cell
|
||||||
|
precision is not available from a raster source.
|
||||||
|
- The object count in a vector selection counts whole features that touch the
|
||||||
|
selection, while area and length metrics clip to it. The response reports how
|
||||||
|
many features the edge cuts; it does not attempt a fractional object count.
|
||||||
|
- Flood-hazard percentages are shares of the modelled area. Where the VMM model
|
||||||
|
does not cover the selection, that is reported as missing coverage and never
|
||||||
|
as an absence of risk.
|
||||||
|
|
||||||
## Historical analysis
|
## Historical analysis
|
||||||
|
|
||||||
@@ -68,12 +78,28 @@ runtime source of truth.
|
|||||||
`YOLO_SEG_MODEL_PATH`/`SAM_MODEL_PATH` to existing local weights and enables
|
`YOLO_SEG_MODEL_PATH`/`SAM_MODEL_PATH` to existing local weights and enables
|
||||||
them explicitly. GeoIntel never downloads segmentation weights automatically;
|
them explicitly. GeoIntel never downloads segmentation weights automatically;
|
||||||
fixture segmentation remains explicit-only.
|
fixture segmentation remains explicit-only.
|
||||||
|
- Detection QA reports both a strict footprint IoU and an envelope diagnostic.
|
||||||
|
For an axis-aligned box detector the strict figure has a ceiling below 1 on
|
||||||
|
rotated or non-rectangular buildings; the response states which geometry mode
|
||||||
|
applies rather than silently attributing that gap to detection quality.
|
||||||
|
- Segmentation inference still predicts one tile per call. Batching is
|
||||||
|
implemented for detection only, because the SAM adapter is prompt-driven and
|
||||||
|
does not share the detector's batch semantics.
|
||||||
|
- Tile-edge truncated boxes are dropped in favour of the overlapping
|
||||||
|
neighbour's complete view. On a manifest generated with `overlap=0` there is
|
||||||
|
no such neighbour, so an object on a seam is lost rather than duplicated.
|
||||||
|
Generate tiles with an overlap larger than the biggest expected object.
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
|
|
||||||
- Long AI/GIS work still uses the existing synchronous job abstraction rather
|
- Detection and segmentation runs can be queued through
|
||||||
than a distributed durable queue. Interrupted synchronous work is marked
|
`POST /detection/run-async` and executed by the in-process analysis worker, so
|
||||||
failed on restart and must be retried explicitly.
|
tiled GPU inference no longer blocks an HTTP request. This is a job table in
|
||||||
|
PostgreSQL polled by the API process, not a distributed durable queue: there
|
||||||
|
is no cross-host distribution and no automatic retry. The synchronous
|
||||||
|
`POST /detection/run` remains available for small runs. Interrupted work is
|
||||||
|
marked failed on restart and must be retried explicitly.
|
||||||
|
- Other long GIS work still uses the synchronous job abstraction.
|
||||||
- GeoIntel remains a controlled single-operator product. The optional guest
|
- GeoIntel remains a controlled single-operator product. The optional guest
|
||||||
demo adds bounded presentation access only; multi-user authorization and
|
demo adds bounded presentation access only; multi-user authorization and
|
||||||
tenant isolation remain outside the product scope.
|
tenant isolation remain outside the product scope.
|
||||||
|
|||||||
Reference in New Issue
Block a user