Harden operator tile background metadata
This commit is contained in:
@@ -147,3 +147,27 @@ def test_background_negative_repeat_only_applies_to_training_background_tiles()
|
|||||||
split="train",
|
split="train",
|
||||||
background_negative_repeat=4,
|
background_negative_repeat=4,
|
||||||
) == 1
|
) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_background_category_is_derived_for_legacy_operator_manifests() -> None:
|
||||||
|
module = load_tile_exporter()
|
||||||
|
|
||||||
|
pure_empty_sample = {
|
||||||
|
"sample_slug": "postel_bos",
|
||||||
|
"sample_role": "background_candidate",
|
||||||
|
"reference_feature_count": 0,
|
||||||
|
}
|
||||||
|
sparse_context_sample = {
|
||||||
|
"sample_slug": "kasterlee_bos",
|
||||||
|
"sample_role": "background_candidate",
|
||||||
|
"reference_feature_count": 104,
|
||||||
|
}
|
||||||
|
reference_sample = {
|
||||||
|
"sample_slug": "geel",
|
||||||
|
"sample_role": "reference",
|
||||||
|
"reference_feature_count": 2500,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert module.background_category_for_sample(pure_empty_sample) == "pure_empty_negative"
|
||||||
|
assert module.background_category_for_sample(sparse_context_sample) == "sparse_building_context"
|
||||||
|
assert module.background_category_for_sample(reference_sample) == "reference_aoi"
|
||||||
|
|||||||
@@ -20,6 +20,17 @@ def load_sample_preparer():
|
|||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def load_tile_exporter():
|
||||||
|
script_path = ROOT / "scripts" / "export_operator_yolo_tile_dataset.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("operator_tile_exporter_s156", script_path)
|
||||||
|
assert spec is not None
|
||||||
|
assert spec.loader is not None
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[spec.name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
def test_background_samples_are_classified_by_actual_reference_density() -> None:
|
def test_background_samples_are_classified_by_actual_reference_density() -> None:
|
||||||
module = load_sample_preparer()
|
module = load_sample_preparer()
|
||||||
|
|
||||||
@@ -93,7 +104,21 @@ def test_hard_negative_matrix_can_filter_background_categories() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_yolo_tile_export_preserves_background_category_provenance() -> None:
|
def test_yolo_tile_export_preserves_background_category_provenance() -> None:
|
||||||
|
module = load_tile_exporter()
|
||||||
script = (ROOT / "scripts" / "export_operator_yolo_tile_dataset.py").read_text(encoding="utf-8")
|
script = (ROOT / "scripts" / "export_operator_yolo_tile_dataset.py").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "background_category = str(sample.get(\"background_category\") or \"reference_aoi\")" in script
|
assert module.background_category_for_sample(
|
||||||
|
{
|
||||||
|
"sample_slug": "postel_bos",
|
||||||
|
"sample_role": "background_candidate",
|
||||||
|
"reference_feature_count": 0,
|
||||||
|
}
|
||||||
|
) == "pure_empty_negative"
|
||||||
|
assert module.background_category_for_sample(
|
||||||
|
{
|
||||||
|
"sample_slug": "kasterlee_bos",
|
||||||
|
"sample_role": "background_candidate",
|
||||||
|
"reference_feature_count": 104,
|
||||||
|
}
|
||||||
|
) == "sparse_building_context"
|
||||||
assert "\"background_category\": background_category" in script
|
assert "\"background_category\": background_category" in script
|
||||||
|
|||||||
@@ -367,9 +367,40 @@ negative tiles, and records `yolo_tile_dataset_summary.json` with
|
|||||||
`--min-label-visible-ratio` drops labels where only a small clipped fragment of
|
`--min-label-visible-ratio` drops labels where only a small clipped fragment of
|
||||||
the original building bbox is visible inside the tile; this reduces noisy
|
the original building bbox is visible inside the tile; this reduces noisy
|
||||||
tile-edge labels in overlapping-tile datasets. Use `0` for legacy behavior.
|
tile-edge labels in overlapping-tile datasets. Use `0` for legacy behavior.
|
||||||
|
For legacy operator manifests that predate explicit `background_category`, the
|
||||||
|
exporter derives the same categories as the split-background evaluator:
|
||||||
|
background samples with `reference_feature_count == 0` become
|
||||||
|
`pure_empty_negative`, and background samples with one or more reference
|
||||||
|
features become `sparse_building_context`.
|
||||||
It remains operator tooling only: no provider fetch, no API mutation and no
|
It remains operator tooling only: no provider fetch, no API mutation and no
|
||||||
automatic model training.
|
automatic model training.
|
||||||
|
|
||||||
|
For the current AOI1024 baseline, prefer the stricter clean-label profile before
|
||||||
|
spending another training run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it geointel python3 /app/scripts/export_operator_yolo_tile_dataset.py \
|
||||||
|
--manifest-path /app/storage/operator-data/operator-samples-1024/operator_samples_manifest.json \
|
||||||
|
--output-dir /app/storage/operator-data/yolo-building-aoi1024-cleanpx12vis035 \
|
||||||
|
--tile-size 512 \
|
||||||
|
--stride 256 \
|
||||||
|
--negative-keep-ratio 1.0 \
|
||||||
|
--min-label-px 12 \
|
||||||
|
--min-label-visible-ratio 0.35 \
|
||||||
|
--val-samples turnhout,retie,westerlo,arendonk_heide \
|
||||||
|
--force
|
||||||
|
```
|
||||||
|
|
||||||
|
Then audit with stricter small-box gates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it geointel python3 /app/scripts/audit_operator_yolo_dataset_quality.py \
|
||||||
|
--summary-path /app/storage/operator-data/yolo-building-aoi1024-cleanpx12vis035/yolo_tile_dataset_summary.json \
|
||||||
|
--output-dir /app/artifacts/operator-yolo-dataset-audit/aoi1024-cleanpx12vis035 \
|
||||||
|
--max-small-box-share 0.25 \
|
||||||
|
--min-median-box-area 0.001
|
||||||
|
```
|
||||||
|
|
||||||
Audit the generated tile dataset before spending another long training run:
|
Audit the generated tile dataset before spending another long training run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ from typing import Any, Iterable
|
|||||||
|
|
||||||
DEFAULT_MANIFEST_PATH = Path("/app/storage/operator-data/operator_samples_manifest.json")
|
DEFAULT_MANIFEST_PATH = Path("/app/storage/operator-data/operator_samples_manifest.json")
|
||||||
DEFAULT_OUTPUT_DIR = Path("/app/storage/operator-data/yolo-building-tile-dataset")
|
DEFAULT_OUTPUT_DIR = Path("/app/storage/operator-data/yolo-building-tile-dataset")
|
||||||
|
REFERENCE_AOI_CATEGORY = "reference_aoi"
|
||||||
|
PURE_EMPTY_BACKGROUND_CATEGORY = "pure_empty_negative"
|
||||||
|
SPARSE_BACKGROUND_CATEGORY = "sparse_building_context"
|
||||||
rasterio: Any = None
|
rasterio: Any = None
|
||||||
Window: Any = None
|
Window: Any = None
|
||||||
Transformer: Any = None
|
Transformer: Any = None
|
||||||
@@ -165,6 +168,18 @@ def background_negative_repeat_count(
|
|||||||
return max(1, background_negative_repeat)
|
return max(1, background_negative_repeat)
|
||||||
|
|
||||||
|
|
||||||
|
def background_category_for_sample(sample: dict[str, Any]) -> str:
|
||||||
|
explicit_category = str(sample.get("background_category") or "").strip()
|
||||||
|
if explicit_category:
|
||||||
|
return explicit_category
|
||||||
|
if str(sample.get("sample_role") or "reference") != "background_candidate":
|
||||||
|
return REFERENCE_AOI_CATEGORY
|
||||||
|
reference_feature_count = int(sample.get("reference_feature_count") or 0)
|
||||||
|
if reference_feature_count <= 0:
|
||||||
|
return PURE_EMPTY_BACKGROUND_CATEGORY
|
||||||
|
return SPARSE_BACKGROUND_CATEGORY
|
||||||
|
|
||||||
|
|
||||||
def resolve_manifest_path(raw: str, manifest_path: Path) -> Path:
|
def resolve_manifest_path(raw: str, manifest_path: Path) -> Path:
|
||||||
path = Path(raw)
|
path = Path(raw)
|
||||||
if path.exists():
|
if path.exists():
|
||||||
@@ -315,7 +330,7 @@ def export_sample_tiles(
|
|||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
sample_slug = str(sample["sample_slug"])
|
sample_slug = str(sample["sample_slug"])
|
||||||
sample_role = str(sample.get("sample_role") or "reference")
|
sample_role = str(sample.get("sample_role") or "reference")
|
||||||
background_category = str(sample.get("background_category") or "reference_aoi")
|
background_category = background_category_for_sample(sample)
|
||||||
split = "val" if sample_slug.lower() in val_slugs else "train"
|
split = "val" if sample_slug.lower() in val_slugs else "train"
|
||||||
raster_path = resolve_manifest_path(str(sample["raster_path"]), manifest_path)
|
raster_path = resolve_manifest_path(str(sample["raster_path"]), manifest_path)
|
||||||
reference_path = resolve_manifest_path(str(sample["reference_path"]), manifest_path)
|
reference_path = resolve_manifest_path(str(sample["reference_path"]), manifest_path)
|
||||||
|
|||||||
Reference in New Issue
Block a user