Raise configured YOLO max detections
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-09 10:14:07 +02:00
parent 9bef905da8
commit 25f098bde0
16 changed files with 82 additions and 1 deletions
+1
View File
@@ -15,6 +15,7 @@ YOLO_CONFIG_DIR=./storage/ultralytics
YOLO_DEVICE=cpu YOLO_DEVICE=cpu
YOLO_IMAGE_SIZE=640 YOLO_IMAGE_SIZE=640
YOLO_MAX_TILES=100 YOLO_MAX_TILES=100
YOLO_MAX_DETECTIONS=1000
YOLO_BATCH_SIZE=1 YOLO_BATCH_SIZE=1
ENABLE_GRB_WFS=false ENABLE_GRB_WFS=false
GRB_WFS_URL= GRB_WFS_URL=
+8
View File
@@ -7,6 +7,14 @@
# Changelog # Changelog
## Sprint 148 YOLO max-detection cap hardening (2026-07-09)
- Added `YOLO_MAX_DETECTIONS` with default `1000` and forward it to Ultralytics as `max_det`.
- Wired the setting through `.env.example`, Docker Compose, Unraid env examples and the Dockerman run script.
- Documented why dense building AOIs should not inherit the Ultralytics default cap of 300 detections before persisted QA/QC.
- Added regression coverage for adapter forwarding and Docker/Unraid runtime exposure.
- No model was activated, no detections were faked, and no API route or migration changed.
## Sprint 147 AOI512 YOLOv8s scale-match candidate gate (2026-07-09) ## Sprint 147 AOI512 YOLOv8s scale-match candidate gate (2026-07-09)
- Built and audited an AOI-scale YOLO dataset at `512px` tile size to test whether the previous `160px` training scale was the main quality blocker. - Built and audited an AOI-scale YOLO dataset at `512px` tile size to test whether the previous `160px` training scale was the main quality blocker.
+5
View File
@@ -359,6 +359,7 @@ YOLO_CONFIG_DIR=/app/storage/ultralytics
YOLO_DEVICE=cpu YOLO_DEVICE=cpu
YOLO_IMAGE_SIZE=640 YOLO_IMAGE_SIZE=640
YOLO_MAX_TILES=100 YOLO_MAX_TILES=100
YOLO_MAX_DETECTIONS=1000
YOLO_BATCH_SIZE=1 YOLO_BATCH_SIZE=1
``` ```
@@ -384,6 +385,10 @@ python scripts/yolo_preflight.py --model-path /absolute/path/to/local-model.pt -
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. 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.
The same read-only status is available through the API and Detection Lab UI: The same read-only status is available through the API and Detection Lab UI:
```bash ```bash
+1
View File
@@ -31,6 +31,7 @@ class Settings(BaseSettings):
yolo_device: str = Field(default="cpu", validation_alias="YOLO_DEVICE") yolo_device: str = Field(default="cpu", validation_alias="YOLO_DEVICE")
yolo_image_size: int = Field(default=640, validation_alias="YOLO_IMAGE_SIZE") yolo_image_size: int = Field(default=640, validation_alias="YOLO_IMAGE_SIZE")
yolo_max_tiles: int = Field(default=100, validation_alias="YOLO_MAX_TILES") yolo_max_tiles: int = Field(default=100, validation_alias="YOLO_MAX_TILES")
yolo_max_detections: int = Field(default=1000, validation_alias="YOLO_MAX_DETECTIONS")
yolo_batch_size: int = Field(default=1, validation_alias="YOLO_BATCH_SIZE") yolo_batch_size: int = Field(default=1, validation_alias="YOLO_BATCH_SIZE")
cors_origins: list[str] | str = Field( cors_origins: list[str] | str = Field(
default=["http://localhost:5173", "http://127.0.0.1:5173"], default=["http://localhost:5173", "http://127.0.0.1:5173"],
+1
View File
@@ -72,6 +72,7 @@ class YoloDetectionAdapter:
conf=float(confidence_threshold), conf=float(confidence_threshold),
imgsz=int(self.settings.yolo_image_size), imgsz=int(self.settings.yolo_image_size),
device=self.settings.yolo_device, device=self.settings.yolo_device,
max_det=int(self.settings.yolo_max_detections),
verbose=False, verbose=False,
) )
except AppError: except AppError:
@@ -93,6 +93,7 @@ def test_env_example_uses_runtime_env_names_read_by_backend_and_frontend() -> No
assert "YOLO_MODEL_PATH=" in env_example assert "YOLO_MODEL_PATH=" in env_example
assert "YOLO_CONFIG_DIR=./storage/ultralytics" in env_example assert "YOLO_CONFIG_DIR=./storage/ultralytics" in env_example
assert "YOLO_MAX_TILES=100" in env_example assert "YOLO_MAX_TILES=100" in env_example
assert "YOLO_MAX_DETECTIONS=1000" in env_example
assert "ENABLE_YOLO" not in env_example assert "ENABLE_YOLO" not in env_example
assert "ENABLE_SAM" not in env_example assert "ENABLE_SAM" not in env_example
assert "VITE_API_BASE_URL=" in env_example assert "VITE_API_BASE_URL=" in env_example
@@ -245,4 +246,5 @@ def test_unraid_deploy_passes_ai_build_arg_and_yolo_runtime_env() -> None:
assert '-e YOLO_MODELS_DIR="$YOLO_MODELS_DIR"' in run_script assert '-e YOLO_MODELS_DIR="$YOLO_MODELS_DIR"' in run_script
assert '-e YOLO_MODEL_PATH="$YOLO_MODEL_PATH"' in run_script assert '-e YOLO_MODEL_PATH="$YOLO_MODEL_PATH"' in run_script
assert '-e YOLO_MAX_TILES="$YOLO_MAX_TILES"' in run_script assert '-e YOLO_MAX_TILES="$YOLO_MAX_TILES"' in run_script
assert '-e YOLO_MAX_DETECTIONS="$YOLO_MAX_DETECTIONS"' in run_script
assert "-v \"${GEOINTEL_MODELS_PATH}:/app/models\"" in run_script assert "-v \"${GEOINTEL_MODELS_PATH}:/app/models\"" in run_script
+16 -1
View File
@@ -93,7 +93,7 @@ class RecordingPredictModel:
def __init__(self) -> None: def __init__(self) -> None:
self.seen_sources: list[dict] = [] self.seen_sources: list[dict] = []
def predict(self, *, source, conf, imgsz, device, verbose): def predict(self, *, source, conf, imgsz, device, verbose, max_det):
from PIL import Image from PIL import Image
with Image.open(source) as image: with Image.open(source) as image:
@@ -106,6 +106,7 @@ class RecordingPredictModel:
"imgsz": imgsz, "imgsz": imgsz,
"device": device, "device": device,
"verbose": verbose, "verbose": verbose,
"max_det": max_det,
} }
) )
return [] return []
@@ -399,6 +400,20 @@ def test_yolo_adapter_converts_single_band_tiles_to_rgb_before_prediction(tmp_pa
assert model.seen_sources[0]["imgsz"] == 64 assert model.seen_sources[0]["imgsz"] == 64
assert model.seen_sources[0]["device"] == "cpu" assert model.seen_sources[0]["device"] == "cpu"
assert model.seen_sources[0]["verbose"] is False assert model.seen_sources[0]["verbose"] is False
assert model.seen_sources[0]["max_det"] == 1000
def test_yolo_adapter_uses_configured_max_detections(tmp_path: Path) -> None:
Image = pytest.importorskip("PIL.Image")
tile_path = tmp_path / "rgb_tile.png"
Image.new("RGB", (16, 16), (10, 20, 30)).save(tile_path)
model = RecordingPredictModel()
settings = _settings(tmp_path, yolo_max_detections=1500)
detections = YoloDetectionAdapter(settings).predict_tile(model, tile_path, confidence_threshold=0.25)
assert detections == []
assert model.seen_sources[0]["max_det"] == 1500
def test_yolo_adapter_wraps_prediction_runtime_errors(tmp_path: Path) -> None: def test_yolo_adapter_wraps_prediction_runtime_errors(tmp_path: Path) -> None:
+1
View File
@@ -36,4 +36,5 @@ YOLO_CONFIG_DIR=/app/storage/ultralytics
YOLO_DEVICE=cpu YOLO_DEVICE=cpu
YOLO_IMAGE_SIZE=640 YOLO_IMAGE_SIZE=640
YOLO_MAX_TILES=100 YOLO_MAX_TILES=100
YOLO_MAX_DETECTIONS=1000
YOLO_BATCH_SIZE=1 YOLO_BATCH_SIZE=1
+2
View File
@@ -30,6 +30,7 @@ YOLO_CONFIG_DIR="${YOLO_CONFIG_DIR:-/app/storage/ultralytics}"
YOLO_DEVICE="${YOLO_DEVICE:-cpu}" YOLO_DEVICE="${YOLO_DEVICE:-cpu}"
YOLO_IMAGE_SIZE="${YOLO_IMAGE_SIZE:-640}" YOLO_IMAGE_SIZE="${YOLO_IMAGE_SIZE:-640}"
YOLO_MAX_TILES="${YOLO_MAX_TILES:-100}" YOLO_MAX_TILES="${YOLO_MAX_TILES:-100}"
YOLO_MAX_DETECTIONS="${YOLO_MAX_DETECTIONS:-1000}"
YOLO_BATCH_SIZE="${YOLO_BATCH_SIZE:-1}" YOLO_BATCH_SIZE="${YOLO_BATCH_SIZE:-1}"
install_dockerman_metadata() { install_dockerman_metadata() {
@@ -90,6 +91,7 @@ docker run -d \
-e YOLO_DEVICE="$YOLO_DEVICE" \ -e YOLO_DEVICE="$YOLO_DEVICE" \
-e YOLO_IMAGE_SIZE="$YOLO_IMAGE_SIZE" \ -e YOLO_IMAGE_SIZE="$YOLO_IMAGE_SIZE" \
-e YOLO_MAX_TILES="$YOLO_MAX_TILES" \ -e YOLO_MAX_TILES="$YOLO_MAX_TILES" \
-e YOLO_MAX_DETECTIONS="$YOLO_MAX_DETECTIONS" \
-e YOLO_BATCH_SIZE="$YOLO_BATCH_SIZE" \ -e YOLO_BATCH_SIZE="$YOLO_BATCH_SIZE" \
-v "${GEOINTEL_POSTGIS_DATA_PATH}:/var/lib/postgresql/data" \ -v "${GEOINTEL_POSTGIS_DATA_PATH}:/var/lib/postgresql/data" \
-v "${GEOINTEL_STORAGE_PATH}:/app/storage" \ -v "${GEOINTEL_STORAGE_PATH}:/app/storage" \
+1
View File
@@ -28,6 +28,7 @@ services:
YOLO_DEVICE: ${YOLO_DEVICE:-cpu} YOLO_DEVICE: ${YOLO_DEVICE:-cpu}
YOLO_IMAGE_SIZE: ${YOLO_IMAGE_SIZE:-640} YOLO_IMAGE_SIZE: ${YOLO_IMAGE_SIZE:-640}
YOLO_MAX_TILES: ${YOLO_MAX_TILES:-100} YOLO_MAX_TILES: ${YOLO_MAX_TILES:-100}
YOLO_MAX_DETECTIONS: ${YOLO_MAX_DETECTIONS:-1000}
YOLO_BATCH_SIZE: ${YOLO_BATCH_SIZE:-1} YOLO_BATCH_SIZE: ${YOLO_BATCH_SIZE:-1}
ports: ports:
- "${GEOINTEL_FRONTEND_PORT:-1202}:80" - "${GEOINTEL_FRONTEND_PORT:-1202}:80"
+1
View File
@@ -33,6 +33,7 @@ services:
YOLO_DEVICE: ${YOLO_DEVICE:-cpu} YOLO_DEVICE: ${YOLO_DEVICE:-cpu}
YOLO_IMAGE_SIZE: ${YOLO_IMAGE_SIZE:-640} YOLO_IMAGE_SIZE: ${YOLO_IMAGE_SIZE:-640}
YOLO_MAX_TILES: ${YOLO_MAX_TILES:-100} YOLO_MAX_TILES: ${YOLO_MAX_TILES:-100}
YOLO_MAX_DETECTIONS: ${YOLO_MAX_DETECTIONS:-1000}
YOLO_BATCH_SIZE: ${YOLO_BATCH_SIZE:-1} YOLO_BATCH_SIZE: ${YOLO_BATCH_SIZE:-1}
ports: ports:
- "${GEOINTEL_BACKEND_PORT:-8000}:8000" - "${GEOINTEL_BACKEND_PORT:-8000}:8000"
+8
View File
@@ -109,8 +109,16 @@ Environment variables:
- `YOLO_DEVICE` - `YOLO_DEVICE`
- `YOLO_IMAGE_SIZE` - `YOLO_IMAGE_SIZE`
- `YOLO_MAX_TILES` - `YOLO_MAX_TILES`
- `YOLO_MAX_DETECTIONS`
- `YOLO_BATCH_SIZE` - `YOLO_BATCH_SIZE`
`YOLO_MAX_DETECTIONS` is forwarded to Ultralytics as `max_det` for each
prediction call. GeoIntel defaults it to `1000` because building-rich AOIs can
contain far more than the Ultralytics default of 300 candidate boxes; keeping
the upstream default would cap recall before QA/QC begins. Operators may lower
the value for small rasters or raise it for dense urban tiles after reviewing
runtime and false-positive behavior.
### Local model asset catalog ### Local model asset catalog
GeoIntel can list local runtime model files mounted into the backend model GeoIntel can list local runtime model files mounted into the backend model
+3
View File
@@ -782,6 +782,9 @@ Validation errors:
- `DETECTION_TILE_MANIFEST_NOT_FOUND` when the provided manifest path does not exist. - `DETECTION_TILE_MANIFEST_NOT_FOUND` when the provided manifest path does not exist.
- `DETECTION_TILE_MANIFEST_INVALID` when the manifest cannot be parsed or lacks tile metadata. - `DETECTION_TILE_MANIFEST_INVALID` when the manifest cannot be parsed or lacks tile metadata.
- `DETECTION_TILE_LIMIT_EXCEEDED` when the manifest exceeds `YOLO_MAX_TILES`. - `DETECTION_TILE_LIMIT_EXCEEDED` when the manifest exceeds `YOLO_MAX_TILES`.
- Configured YOLO inference forwards `YOLO_MAX_DETECTIONS` to Ultralytics
`max_det` and defaults to `1000` so dense building AOIs are not silently
limited by the upstream default of 300 detections before persisted QA/QC.
- `DETECTION_DEPENDENCY_UNAVAILABLE` when YOLO dependencies are not installed. - `DETECTION_DEPENDENCY_UNAVAILABLE` when YOLO dependencies are not installed.
- `DETECTION_MODEL_LOAD_FAILED` when the local model file exists but cannot be loaded. - `DETECTION_MODEL_LOAD_FAILED` when the local model file exists but cannot be loaded.
+29
View File
@@ -1,3 +1,32 @@
## Sprint 148 YOLO max-detection cap hardening (2026-07-09)
Changed:
- Added backend setting `YOLO_MAX_DETECTIONS` / `Settings.yolo_max_detections`.
- `YoloDetectionAdapter` now forwards the value to Ultralytics as `max_det`.
- Default is `1000` instead of relying on Ultralytics' upstream default of 300.
- Added Docker/Unraid/runtime wiring:
- `.env.example`
- `docker-compose.yml`
- `docker-compose.unraid.yml`
- `deploy/unraid/geointel.env.example`
- `deploy/unraid/run-dockerman-container.sh`
- Updated backend/API/AI environment documentation.
Why:
- Real Kempen building AOIs often contain more than 300 reference buildings.
- The previous configured-YOLO path could saturate at 300 detections before QA/QC, capping recall independently of model quality.
- This does not activate a model and does not fake detections; it removes an inference runtime cap so persisted QA/QC can measure candidate models honestly.
Tested:
- Red step: targeted YOLO adapter tests failed because `max_det` was not passed to the model.
- `python -m pytest backend\tests\test_sprint8b_yolo_foundation.py::test_yolo_adapter_converts_single_band_tiles_to_rgb_before_prediction backend\tests\test_sprint8b_yolo_foundation.py::test_yolo_adapter_uses_configured_max_detections -q` (`2 passed`)
- Red step: Docker runtime config tests failed before `.env.example` and Unraid runner exposed `YOLO_MAX_DETECTIONS`.
- `python -m pytest backend\tests\test_docker_runtime_config.py::test_env_example_uses_runtime_env_names_read_by_backend_and_frontend backend\tests\test_docker_runtime_config.py::test_unraid_deploy_passes_ai_build_arg_and_yolo_runtime_env backend\tests\test_sprint8b_yolo_foundation.py -q` (`16 passed`)
Open:
- Rebuild/redeploy the Tower all-in-one image before rerunning live calibration so the container uses `YOLO_MAX_DETECTIONS=1000`.
- After deploy, rerun at least one high-density AOI calibration to confirm detection counts are no longer capped at 300.
## Sprint 147 AOI512 YOLOv8s scale-match candidate gate (2026-07-09) ## Sprint 147 AOI512 YOLOv8s scale-match candidate gate (2026-07-09)
Changed: Changed:
+1
View File
@@ -21,6 +21,7 @@ YOLO_ENABLED=false
YOLO_MODEL_PATH= YOLO_MODEL_PATH=
YOLO_MODEL_VERSION= YOLO_MODEL_VERSION=
YOLO_MAX_TILES=100 YOLO_MAX_TILES=100
YOLO_MAX_DETECTIONS=1000
ENABLE_GRB_WFS=false ENABLE_GRB_WFS=false
GRB_WFS_URL= GRB_WFS_URL=
OSM_OVERPASS_URL=https://overpass-api.de/api/interpreter OSM_OVERPASS_URL=https://overpass-api.de/api/interpreter
+2
View File
@@ -110,9 +110,11 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Benchmark an external remote-sensing YOLOv8l building candidate as an explicit local model asset. - [x] Benchmark an external remote-sensing YOLOv8l building candidate as an explicit local model asset.
- [x] Train and gate the `uniquehardneg160e50` YOLOv8s candidate through 7 positive AOIs and 9 hard-negative/background samples. - [x] Train and gate the `uniquehardneg160e50` YOLOv8s candidate through 7 positive AOIs and 9 hard-negative/background samples.
- [x] Train and gate an AOI-scale `aoi512e80` YOLOv8s candidate to test the 160px training-scale hypothesis. - [x] Train and gate an AOI-scale `aoi512e80` YOLOv8s candidate to test the 160px training-scale hypothesis.
- [x] Raise configured-YOLO `max_det` through `YOLO_MAX_DETECTIONS` so dense AOIs are not capped at 300 detections before QA/QC.
- [ ] Find or train a materially stronger aerial/Kempen building model candidate; `geointel-building-yolov8n-expanded160e50-pt` is the best current dense-AOI candidate but still too weak and too noisy for a V1 default. - [ ] Find or train a materially stronger aerial/Kempen building model candidate; `geointel-building-yolov8n-expanded160e50-pt` is the best current dense-AOI candidate but still too weak and too noisy for a V1 default.
- [ ] Train a higher-capacity local aerial-building detector with stronger positive recall while preserving the hard-negative false-positive gate. - [ ] Train a higher-capacity local aerial-building detector with stronger positive recall while preserving the hard-negative false-positive gate.
- [ ] Add more diverse positive AOIs and revisit geometry-to-box label strategy before the next default-model training attempt. - [ ] Add more diverse positive AOIs and revisit geometry-to-box label strategy before the next default-model training attempt.
- [ ] Rerun live dense-AOI calibration after redeploy with `YOLO_MAX_DETECTIONS=1000` to measure uncapped recall and false-positive pressure.
## Sprint 8 status ## Sprint 8 status