Support larger operator training samples
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
- Added `--min-label-visible-ratio` / `OPERATOR_YOLO_MIN_LABEL_VISIBLE_RATIO` to the operator YOLO tile dataset exporter.
|
||||
- The exporter can now drop clipped building labels where only a small share of the original object bbox is visible in an overlapping tile.
|
||||
- Tile dataset summaries and audit reports now retain/report `min_label_visible_ratio`.
|
||||
- Added operator-only `--width`, `--height` and `--half-size-scale` options to `prepare_operator_real_data_samples.py` so larger training AOIs can be prepared explicitly.
|
||||
- Updated operator documentation for the recommended next dataset pass.
|
||||
- No model was activated, no detections were faked, no provider fetching was introduced and no migration changed.
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ def test_prepare_operator_real_data_samples_help_does_not_require_gis_dependenci
|
||||
assert result.returncode == 0
|
||||
assert "Prepare real Digitaal Vlaanderen" in result.stdout
|
||||
assert "--samples" in result.stdout
|
||||
assert "--width" in result.stdout
|
||||
assert "--height" in result.stdout
|
||||
assert "--half-size-scale" in result.stdout
|
||||
|
||||
|
||||
def test_multi_sample_detection_quality_matrix_runs_existing_matrix_for_each_sample() -> None:
|
||||
|
||||
@@ -63,6 +63,27 @@ def test_operator_background_candidates_are_unique_enough_for_hard_negative_trai
|
||||
assert half_sizes == {260.0}
|
||||
|
||||
|
||||
def test_operator_sample_can_be_scaled_for_larger_training_aoi(tmp_path: Path) -> None:
|
||||
module = load_sample_preparer()
|
||||
|
||||
sample = module.OperatorSample(
|
||||
slug="geel",
|
||||
display_name="Geel",
|
||||
center_lon=5.0,
|
||||
center_lat=51.0,
|
||||
half_size_m=250.0,
|
||||
)
|
||||
|
||||
configured = module.apply_sample_overrides(sample, width=1024, height=1024, half_size_scale=2.0)
|
||||
ortho_path, reference_path = module.sample_artifact_paths(configured, tmp_path)
|
||||
|
||||
assert configured.width == 1024
|
||||
assert configured.height == 1024
|
||||
assert configured.half_size_m == 500.0
|
||||
assert ortho_path.name == "geel_orthophoto_wms_1024.tif"
|
||||
assert reference_path.name == "geel_grb_gbg_buildings.geojson"
|
||||
|
||||
|
||||
def test_background_candidate_can_write_empty_reference_geojson(tmp_path: Path, monkeypatch) -> None:
|
||||
module = load_sample_preparer()
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Changed:
|
||||
- Default remains `0` for legacy behavior; use `0.25` for the next overlap-heavy operator dataset experiment.
|
||||
- Tile dataset summaries include `min_label_visible_ratio`.
|
||||
- `scripts/audit_operator_yolo_dataset_quality.py` now reports `min_label_visible_ratio` in JSON and Markdown.
|
||||
- Added operator-only `--width`, `--height` and `--half-size-scale` options to `scripts/prepare_operator_real_data_samples.py`; generated raster names now include the requested width.
|
||||
- Updated operator script documentation.
|
||||
|
||||
Why:
|
||||
@@ -18,9 +19,14 @@ Tested:
|
||||
- `python -m pytest backend\tests\test_sprint130_operator_yolo_tile_dataset.py -q` (`6 passed`)
|
||||
- Red step: `python -m pytest backend\tests\test_sprint146_operator_yolo_dataset_quality_audit.py -q` failed because the audit report did not expose `min_label_visible_ratio`.
|
||||
- `python -m pytest backend\tests\test_sprint130_operator_yolo_tile_dataset.py backend\tests\test_sprint146_operator_yolo_dataset_quality_audit.py -q` (`7 passed`)
|
||||
- Red step: `python -m pytest backend\tests\test_sprint127_operator_sample_quality_matrix.py backend\tests\test_sprint131_operator_sample_expansion.py -q` failed because sample prep lacked larger-AOI options.
|
||||
- `python -m pytest backend\tests\test_sprint127_operator_sample_quality_matrix.py backend\tests\test_sprint131_operator_sample_expansion.py -q` (`8 passed`)
|
||||
- Full readiness: `bash scripts/run_readiness_check.sh` (`427 passed`, frontend typecheck/build passed).
|
||||
- Tower deploy: first AI rebuild failed with Docker storage full; after Docker build cache cleanup `/var/lib/docker` had 98G free and redeploy passed live migration smoke and browser runtime verification on `http://192.168.10.150:1202`.
|
||||
- Tower dataset audit: exported `/app/storage/operator-data/yolo-building-aoi512-visible025` with `min_label_visible_ratio=0.25`; audit returned `needs_attention` because the current 512x512 source rasters still produce only 16 tiles and median normalized box area remains below gate.
|
||||
|
||||
Next:
|
||||
- Run full readiness, deploy Tower, export a new visible-ratio-gated operator tile dataset, audit it, then decide whether it is good enough for another CPU training candidate.
|
||||
- Prepare a larger explicit operator sample manifest, for example `/app/storage/operator-data/operator-samples-1024` with `--width 1024 --height 1024 --half-size-scale 2`, then export/audit `yolo-building-aoi1024-visible025` before another CPU training candidate.
|
||||
|
||||
## Sprint 149 YOLO duplicate suppression evidence (2026-07-09)
|
||||
|
||||
|
||||
+2
-1
@@ -115,9 +115,10 @@ This file now starts with the current implementation status. Older preparation/b
|
||||
- [x] Add configured-YOLO cross-tile duplicate suppression and raw/suppressed calibration evidence fields.
|
||||
- [x] Rerun live dense-AOI calibration after redeploy with `YOLO_DUPLICATE_IOU_THRESHOLD=0.5`; Westerlo 0.25 improved to F1 `0.2537313432835821` and Turnhout 0.25 improved to F1 `0.14114114114114112`, but the candidate remains rejected.
|
||||
- [x] Add `OPERATOR_YOLO_MIN_LABEL_VISIBLE_RATIO` so the next overlapping-tile dataset can drop tiny clipped edge-fragment labels.
|
||||
- [x] Add operator-only larger-AOI sample prep flags so the next training dataset is not limited to one 512x512 tile per documented sample.
|
||||
- [ ] 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.
|
||||
- [ ] Export and audit a visible-ratio-gated tile dataset on Tower before the next default-model training attempt.
|
||||
- [ ] Prepare `/app/storage/operator-data/operator-samples-1024` on Tower, then export and audit `yolo-building-aoi1024-visible025` before the next default-model training attempt.
|
||||
- [ ] Build the next candidate gate around better positive AOI coverage, label strategy and hard-negative retention.
|
||||
|
||||
## Sprint 8 status
|
||||
|
||||
+23
-5
@@ -191,6 +191,24 @@ negative-tile training. The helper fetches only the explicit documented AOIs,
|
||||
records Digitaal Vlaanderen attribution and reuses existing files by default.
|
||||
Use `--force` only when the local runtime artifacts should be regenerated.
|
||||
|
||||
For model-training candidates, prepare a larger operator-only sample manifest so
|
||||
tile overlap can create meaningful context instead of one tile per source
|
||||
raster:
|
||||
|
||||
```bash
|
||||
docker exec -it geointel python3 /app/scripts/prepare_operator_real_data_samples.py \
|
||||
--output-dir /app/storage/operator-data/operator-samples-1024 \
|
||||
--manifest-name operator_samples_manifest.json \
|
||||
--width 1024 \
|
||||
--height 1024 \
|
||||
--half-size-scale 2 \
|
||||
--force
|
||||
```
|
||||
|
||||
This keeps the same documented AOI centers but requests larger WMS rasters and a
|
||||
larger GRB reference bbox. Use the generated manifest path for the next YOLO
|
||||
tile export. The default remains 512x512 for quick smoke runs.
|
||||
|
||||
The real-data smoke is intentionally mutating and refuses to run without
|
||||
operator-supplied files. Current V1 upload support expects a georeferenced
|
||||
`.tif`, `.tiff` or `.geotiff` raster and a `.geojson` or `.json` reference
|
||||
@@ -316,13 +334,13 @@ with overlapping raster windows:
|
||||
|
||||
```bash
|
||||
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-expanded160 \
|
||||
--tile-size 160 \
|
||||
--stride 80 \
|
||||
--manifest-path /app/storage/operator-data/operator-samples-1024/operator_samples_manifest.json \
|
||||
--output-dir /app/storage/operator-data/yolo-building-aoi1024-visible025 \
|
||||
--tile-size 512 \
|
||||
--stride 256 \
|
||||
--negative-keep-ratio 1.0 \
|
||||
--min-label-visible-ratio 0.25 \
|
||||
--val-samples turnhout,retie,kasterlee_bos \
|
||||
--val-samples turnhout,retie,westerlo,arendonk_heide \
|
||||
--force
|
||||
```
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -196,6 +196,24 @@ def parse_args() -> argparse.Namespace:
|
||||
default="operator_samples_manifest.json",
|
||||
help="Manifest filename written inside output-dir.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--width",
|
||||
type=int,
|
||||
default=int(os.environ.get("OPERATOR_SAMPLE_WIDTH", "512")),
|
||||
help="Orthophoto WMS output width in pixels. Use larger values for operator training datasets.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--height",
|
||||
type=int,
|
||||
default=int(os.environ.get("OPERATOR_SAMPLE_HEIGHT", "512")),
|
||||
help="Orthophoto WMS output height in pixels. Use larger values for operator training datasets.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--half-size-scale",
|
||||
type=float,
|
||||
default=float(os.environ.get("OPERATOR_SAMPLE_HALF_SIZE_SCALE", "1")),
|
||||
help="Multiplier applied to each documented AOI half-size in meters.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -229,6 +247,25 @@ def selected_samples(raw: str) -> list[OperatorSample]:
|
||||
return [SAMPLES[slug] for slug in slugs]
|
||||
|
||||
|
||||
def apply_sample_overrides(
|
||||
sample: OperatorSample,
|
||||
*,
|
||||
width: int,
|
||||
height: int,
|
||||
half_size_scale: float,
|
||||
) -> OperatorSample:
|
||||
if width <= 0 or height <= 0:
|
||||
raise SystemExit("--width and --height must be positive integers")
|
||||
if half_size_scale <= 0:
|
||||
raise SystemExit("--half-size-scale must be greater than zero")
|
||||
return replace(
|
||||
sample,
|
||||
width=width,
|
||||
height=height,
|
||||
half_size_m=sample.half_size_m * half_size_scale,
|
||||
)
|
||||
|
||||
|
||||
def sample_bounds(sample: OperatorSample) -> tuple[tuple[float, float, float, float], list[float]]:
|
||||
lambert = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True)
|
||||
wgs84 = Transformer.from_crs("EPSG:31370", "EPSG:4326", always_xy=True)
|
||||
@@ -273,6 +310,12 @@ def geojson_feature_count(path: Path) -> int:
|
||||
return len(payload.get("features") or [])
|
||||
|
||||
|
||||
def sample_artifact_paths(sample: OperatorSample, output_dir: Path) -> tuple[Path, Path]:
|
||||
ortho_path = output_dir / f"{sample.slug}_orthophoto_wms_{sample.width}.tif"
|
||||
reference_path = output_dir / f"{sample.slug}_grb_gbg_buildings.geojson"
|
||||
return ortho_path, reference_path
|
||||
|
||||
|
||||
def fetch_orthophoto(sample: OperatorSample, ortho_path: Path, lambert_bbox: tuple[float, float, float, float]) -> str:
|
||||
minx, miny, maxx, maxy = lambert_bbox
|
||||
wms_params = {
|
||||
@@ -352,8 +395,7 @@ def fetch_reference(sample: OperatorSample, reference_path: Path, geo_bbox: list
|
||||
|
||||
|
||||
def prepare_sample(sample: OperatorSample, output_dir: Path, force: bool) -> dict[str, Any]:
|
||||
ortho_path = output_dir / f"{sample.slug}_orthophoto_wms_512.tif"
|
||||
reference_path = output_dir / f"{sample.slug}_grb_gbg_buildings.geojson"
|
||||
ortho_path, reference_path = sample_artifact_paths(sample, output_dir)
|
||||
lambert_bbox, geo_bbox = sample_bounds(sample)
|
||||
skip_existing = ortho_path.exists() and reference_path.exists() and not force
|
||||
|
||||
@@ -370,6 +412,8 @@ def prepare_sample(sample: OperatorSample, output_dir: Path, force: bool) -> dic
|
||||
"center_lon": sample.center_lon,
|
||||
"center_lat": sample.center_lat,
|
||||
"half_size_m": sample.half_size_m,
|
||||
"width": sample.width,
|
||||
"height": sample.height,
|
||||
"sample_role": sample.sample_role,
|
||||
"allow_empty_reference": sample.allow_empty_reference,
|
||||
"raster_path": str(ortho_path),
|
||||
@@ -416,13 +460,28 @@ def main() -> int:
|
||||
ensure_gis_dependencies()
|
||||
output_dir: Path = args.output_dir
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
samples = [prepare_sample(sample, output_dir, force=args.force) for sample in selected_samples(args.samples)]
|
||||
samples = [
|
||||
prepare_sample(
|
||||
apply_sample_overrides(
|
||||
sample,
|
||||
width=args.width,
|
||||
height=args.height,
|
||||
half_size_scale=args.half_size_scale,
|
||||
),
|
||||
output_dir,
|
||||
force=args.force,
|
||||
)
|
||||
for sample in selected_samples(args.samples)
|
||||
]
|
||||
write_readme(output_dir, samples)
|
||||
|
||||
manifest = {
|
||||
"schema_version": 1,
|
||||
"description": "GeoIntel operator real-data samples for configured-YOLO QA validation.",
|
||||
"output_dir": str(output_dir),
|
||||
"sample_width": args.width,
|
||||
"sample_height": args.height,
|
||||
"half_size_scale": args.half_size_scale,
|
||||
"samples": samples,
|
||||
}
|
||||
manifest_path = output_dir / args.manifest_name
|
||||
|
||||
Reference in New Issue
Block a user