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.
|
- 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.
|
- 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`.
|
- 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.
|
- 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.
|
- 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 result.returncode == 0
|
||||||
assert "Prepare real Digitaal Vlaanderen" in result.stdout
|
assert "Prepare real Digitaal Vlaanderen" in result.stdout
|
||||||
assert "--samples" 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:
|
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}
|
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:
|
def test_background_candidate_can_write_empty_reference_geojson(tmp_path: Path, monkeypatch) -> None:
|
||||||
module = load_sample_preparer()
|
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.
|
- 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`.
|
- 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.
|
- `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.
|
- Updated operator script documentation.
|
||||||
|
|
||||||
Why:
|
Why:
|
||||||
@@ -18,9 +19,14 @@ Tested:
|
|||||||
- `python -m pytest backend\tests\test_sprint130_operator_yolo_tile_dataset.py -q` (`6 passed`)
|
- `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`.
|
- 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`)
|
- `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:
|
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)
|
## 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] 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] 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_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.
|
- [ ] 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.
|
||||||
- [ ] 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.
|
- [ ] Build the next candidate gate around better positive AOI coverage, label strategy and hard-negative retention.
|
||||||
|
|
||||||
## Sprint 8 status
|
## 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.
|
records Digitaal Vlaanderen attribution and reuses existing files by default.
|
||||||
Use `--force` only when the local runtime artifacts should be regenerated.
|
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
|
The real-data smoke is intentionally mutating and refuses to run without
|
||||||
operator-supplied files. Current V1 upload support expects a georeferenced
|
operator-supplied files. Current V1 upload support expects a georeferenced
|
||||||
`.tif`, `.tiff` or `.geotiff` raster and a `.geojson` or `.json` reference
|
`.tif`, `.tiff` or `.geotiff` raster and a `.geojson` or `.json` reference
|
||||||
@@ -316,13 +334,13 @@ with overlapping raster windows:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker exec -it geointel python3 /app/scripts/export_operator_yolo_tile_dataset.py \
|
docker exec -it geointel python3 /app/scripts/export_operator_yolo_tile_dataset.py \
|
||||||
--manifest-path /app/storage/operator-data/operator_samples_manifest.json \
|
--manifest-path /app/storage/operator-data/operator-samples-1024/operator_samples_manifest.json \
|
||||||
--output-dir /app/storage/operator-data/yolo-building-tile-expanded160 \
|
--output-dir /app/storage/operator-data/yolo-building-aoi1024-visible025 \
|
||||||
--tile-size 160 \
|
--tile-size 512 \
|
||||||
--stride 80 \
|
--stride 256 \
|
||||||
--negative-keep-ratio 1.0 \
|
--negative-keep-ratio 1.0 \
|
||||||
--min-label-visible-ratio 0.25 \
|
--min-label-visible-ratio 0.25 \
|
||||||
--val-samples turnhout,retie,kasterlee_bos \
|
--val-samples turnhout,retie,westerlo,arendonk_heide \
|
||||||
--force
|
--force
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import argparse
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, replace
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -196,6 +196,24 @@ def parse_args() -> argparse.Namespace:
|
|||||||
default="operator_samples_manifest.json",
|
default="operator_samples_manifest.json",
|
||||||
help="Manifest filename written inside output-dir.",
|
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()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@@ -229,6 +247,25 @@ def selected_samples(raw: str) -> list[OperatorSample]:
|
|||||||
return [SAMPLES[slug] for slug in slugs]
|
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]]:
|
def sample_bounds(sample: OperatorSample) -> tuple[tuple[float, float, float, float], list[float]]:
|
||||||
lambert = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True)
|
lambert = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True)
|
||||||
wgs84 = Transformer.from_crs("EPSG:31370", "EPSG:4326", 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 [])
|
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:
|
def fetch_orthophoto(sample: OperatorSample, ortho_path: Path, lambert_bbox: tuple[float, float, float, float]) -> str:
|
||||||
minx, miny, maxx, maxy = lambert_bbox
|
minx, miny, maxx, maxy = lambert_bbox
|
||||||
wms_params = {
|
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]:
|
def prepare_sample(sample: OperatorSample, output_dir: Path, force: bool) -> dict[str, Any]:
|
||||||
ortho_path = output_dir / f"{sample.slug}_orthophoto_wms_512.tif"
|
ortho_path, reference_path = sample_artifact_paths(sample, output_dir)
|
||||||
reference_path = output_dir / f"{sample.slug}_grb_gbg_buildings.geojson"
|
|
||||||
lambert_bbox, geo_bbox = sample_bounds(sample)
|
lambert_bbox, geo_bbox = sample_bounds(sample)
|
||||||
skip_existing = ortho_path.exists() and reference_path.exists() and not force
|
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_lon": sample.center_lon,
|
||||||
"center_lat": sample.center_lat,
|
"center_lat": sample.center_lat,
|
||||||
"half_size_m": sample.half_size_m,
|
"half_size_m": sample.half_size_m,
|
||||||
|
"width": sample.width,
|
||||||
|
"height": sample.height,
|
||||||
"sample_role": sample.sample_role,
|
"sample_role": sample.sample_role,
|
||||||
"allow_empty_reference": sample.allow_empty_reference,
|
"allow_empty_reference": sample.allow_empty_reference,
|
||||||
"raster_path": str(ortho_path),
|
"raster_path": str(ortho_path),
|
||||||
@@ -416,13 +460,28 @@ def main() -> int:
|
|||||||
ensure_gis_dependencies()
|
ensure_gis_dependencies()
|
||||||
output_dir: Path = args.output_dir
|
output_dir: Path = args.output_dir
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
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)
|
write_readme(output_dir, samples)
|
||||||
|
|
||||||
manifest = {
|
manifest = {
|
||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"description": "GeoIntel operator real-data samples for configured-YOLO QA validation.",
|
"description": "GeoIntel operator real-data samples for configured-YOLO QA validation.",
|
||||||
"output_dir": str(output_dir),
|
"output_dir": str(output_dir),
|
||||||
|
"sample_width": args.width,
|
||||||
|
"sample_height": args.height,
|
||||||
|
"half_size_scale": args.half_size_scale,
|
||||||
"samples": samples,
|
"samples": samples,
|
||||||
}
|
}
|
||||||
manifest_path = output_dir / args.manifest_name
|
manifest_path = output_dir / args.manifest_name
|
||||||
|
|||||||
Reference in New Issue
Block a user