Initial public release
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def load_tile_exporter():
|
||||
script_path = ROOT / "scripts" / "export_operator_yolo_tile_dataset.py"
|
||||
spec = importlib.util.spec_from_file_location("operator_tile_exporter", 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_operator_yolo_tile_dataset_export_script_contract() -> None:
|
||||
script_path = ROOT / "scripts" / "export_operator_yolo_tile_dataset.py"
|
||||
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert script_path.exists()
|
||||
script = script_path.read_text(encoding="utf-8")
|
||||
|
||||
assert "py_compile scripts/export_operator_yolo_tile_dataset.py" in readiness
|
||||
assert "operator_samples_manifest.json" in script
|
||||
assert "yolo-building-tile-dataset" in script
|
||||
assert "dataset.yaml" in script
|
||||
assert "images/train" in script
|
||||
assert "labels/train" in script
|
||||
assert "images/val" in script
|
||||
assert "labels/val" in script
|
||||
assert "tile_size" in script
|
||||
assert "stride" in script
|
||||
assert "negative_keep_ratio" in script
|
||||
assert "min_label_visible_ratio" in script
|
||||
assert "drop_low_variance_negatives" in script
|
||||
assert "skipped_low_variance_negative_tile_count" in script
|
||||
assert "positive_tile_count" in script
|
||||
assert "negative_tile_count" in script
|
||||
assert "skipped_negative_tile_count" in script
|
||||
assert "source_name" in script
|
||||
assert "reference_layer_name" in script
|
||||
assert "Window" in script
|
||||
assert "Transformer" in script
|
||||
assert "fixture_mode" not in script
|
||||
assert "will_download_models" not in script
|
||||
|
||||
|
||||
def test_operator_yolo_tile_dataset_export_help_does_not_require_gis_dependencies() -> None:
|
||||
script_path = ROOT / "scripts" / "export_operator_yolo_tile_dataset.py"
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script_path), "--help"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "Export operator real-data samples to a tile-level YOLO detection dataset" in result.stdout
|
||||
assert "--tile-size" in result.stdout
|
||||
assert "--stride" in result.stdout
|
||||
assert "--samples" in result.stdout
|
||||
assert "--negative-keep-ratio" in result.stdout
|
||||
assert "--min-label-visible-ratio" in result.stdout
|
||||
assert "--background-negative-repeat" in result.stdout
|
||||
assert "--drop-low-variance-negatives" in result.stdout
|
||||
assert "--blank-range-threshold" in result.stdout
|
||||
assert "--class-name" in result.stdout
|
||||
assert "--reference-source" in result.stdout
|
||||
assert "--reference-layer" in result.stdout
|
||||
|
||||
|
||||
def test_default_validation_split_is_explicit_and_rejects_holdout_leakage() -> None:
|
||||
module = load_tile_exporter()
|
||||
samples = [
|
||||
{"sample_slug": "geel", "recommended_split": "train"},
|
||||
{"sample_slug": "turnhout", "recommended_split": "val"},
|
||||
{"sample_slug": "retie", "recommended_split": "val"},
|
||||
{"sample_slug": "westerlo", "recommended_split": "val"},
|
||||
{"sample_slug": "arendonk_heide", "recommended_split": "val"},
|
||||
{"sample_slug": "vosselaar_center", "recommended_split": "val"},
|
||||
{"sample_slug": "grobbendonk_center", "recommended_split": "val"},
|
||||
]
|
||||
|
||||
assert module.DEFAULT_VALIDATION_SAMPLE_SLUGS == frozenset(
|
||||
{
|
||||
"turnhout",
|
||||
"retie",
|
||||
"westerlo",
|
||||
"arendonk_heide",
|
||||
"vosselaar_center",
|
||||
"grobbendonk_center",
|
||||
}
|
||||
)
|
||||
assert module.validate_validation_split(
|
||||
samples,
|
||||
set(module.DEFAULT_VALIDATION_SAMPLE_SLUGS),
|
||||
) == set(module.DEFAULT_VALIDATION_SAMPLE_SLUGS)
|
||||
|
||||
with pytest.raises(SystemExit, match="recommended validation holdouts"):
|
||||
module.validate_validation_split(samples, {"turnhout"})
|
||||
with pytest.raises(SystemExit, match="unknown samples"):
|
||||
module.validate_validation_split(samples, {"turnhout", "missing"})
|
||||
|
||||
|
||||
def test_manifest_sample_selection_keeps_external_holdouts_out_of_targeted_dataset() -> None:
|
||||
module = load_tile_exporter()
|
||||
samples = [
|
||||
{"sample_slug": "geel", "recommended_split": "train"},
|
||||
{"sample_slug": "beerse_center", "recommended_split": "train"},
|
||||
{"sample_slug": "vosselaar_center", "recommended_split": "val"},
|
||||
{"sample_slug": "turnhout", "recommended_split": "val"},
|
||||
{"sample_slug": "retie", "recommended_split": "val"},
|
||||
{"sample_slug": "westerlo", "recommended_split": "val"},
|
||||
]
|
||||
|
||||
selected, excluded = module.select_manifest_samples(
|
||||
samples,
|
||||
{"geel", "beerse_center", "vosselaar_center"},
|
||||
)
|
||||
|
||||
assert [sample["sample_slug"] for sample in selected] == [
|
||||
"geel",
|
||||
"beerse_center",
|
||||
"vosselaar_center",
|
||||
]
|
||||
assert excluded == ["retie", "turnhout", "westerlo"]
|
||||
assert module.validate_validation_split(selected, {"vosselaar_center"}) == {
|
||||
"vosselaar_center"
|
||||
}
|
||||
with pytest.raises(SystemExit, match="unknown samples"):
|
||||
module.select_manifest_samples(samples, {"geel", "missing"})
|
||||
|
||||
|
||||
def test_train_only_shard_requires_explicit_opt_in() -> None:
|
||||
module = load_tile_exporter()
|
||||
samples = [{"sample_slug": "new-train-aoi", "recommended_split": "train"}]
|
||||
|
||||
with pytest.raises(SystemExit, match="must include at least one"):
|
||||
module.validate_validation_split(samples, set())
|
||||
assert module.validate_validation_split(samples, set(), allow_empty=True) == set()
|
||||
|
||||
|
||||
def test_validation_coverage_reports_holdouts_without_retained_tiles() -> None:
|
||||
module = load_tile_exporter()
|
||||
coverage = module.validation_sample_coverage(
|
||||
[
|
||||
{"sample_slug": "turnhout", "split": "val", "kept": True},
|
||||
{"sample_slug": "retie", "split": "val", "kept": True},
|
||||
{"sample_slug": "geel", "split": "train", "kept": True},
|
||||
],
|
||||
{"turnhout", "retie", "arendonk_heide"},
|
||||
)
|
||||
|
||||
assert coverage == {
|
||||
"retained_validation_sample_slugs": ["retie", "turnhout"],
|
||||
"empty_validation_sample_slugs": ["arendonk_heide"],
|
||||
}
|
||||
|
||||
|
||||
def test_iter_tile_windows_covers_edges_without_duplicates() -> None:
|
||||
module = load_tile_exporter()
|
||||
|
||||
windows = list(module.iter_tile_windows(width=512, height=512, tile_size=192, stride=96))
|
||||
|
||||
assert len(windows) == 25
|
||||
assert windows[0].row_off == 0
|
||||
assert windows[0].col_off == 0
|
||||
assert windows[-1].row_off == 320
|
||||
assert windows[-1].col_off == 320
|
||||
assert len({(window.row_off, window.col_off) for window in windows}) == len(windows)
|
||||
assert all(window.width == 192 for window in windows)
|
||||
assert all(window.height == 192 for window in windows)
|
||||
|
||||
|
||||
def test_negative_tile_keep_is_deterministic_and_ratio_bound() -> None:
|
||||
module = load_tile_exporter()
|
||||
|
||||
first = [module.keep_negative_tile("geel", index, 0.25) for index in range(50)]
|
||||
second = [module.keep_negative_tile("geel", index, 0.25) for index in range(50)]
|
||||
all_kept = [module.keep_negative_tile("geel", index, 1.0) for index in range(10)]
|
||||
none_kept = [module.keep_negative_tile("geel", index, 0.0) for index in range(10)]
|
||||
|
||||
assert first == second
|
||||
assert 1 <= sum(first) <= 25
|
||||
assert all(all_kept)
|
||||
assert not any(none_kept)
|
||||
|
||||
|
||||
def test_labels_for_tile_can_drop_tiny_visible_box_fragments() -> None:
|
||||
module = load_tile_exporter()
|
||||
tile = module.TileWindow(row_off=0, col_off=0, height=100, width=100)
|
||||
mostly_outside_box = module.PixelBox(min_col=90, min_row=10, max_col=190, max_row=90)
|
||||
|
||||
labels_without_gate = module.labels_for_tile(
|
||||
tile,
|
||||
[mostly_outside_box],
|
||||
min_label_px=4,
|
||||
min_visible_ratio=0.0,
|
||||
)
|
||||
labels_with_gate = module.labels_for_tile(
|
||||
tile,
|
||||
[mostly_outside_box],
|
||||
min_label_px=4,
|
||||
min_visible_ratio=0.25,
|
||||
)
|
||||
|
||||
assert labels_without_gate == ["0 0.95000000 0.50000000 0.10000000 0.80000000"]
|
||||
assert labels_with_gate == []
|
||||
|
||||
|
||||
def test_background_negative_repeat_only_applies_to_training_background_tiles() -> None:
|
||||
module = load_tile_exporter()
|
||||
|
||||
assert module.background_negative_repeat_count(
|
||||
is_negative=True,
|
||||
sample_role="background_candidate",
|
||||
split="train",
|
||||
background_negative_repeat=4,
|
||||
) == 4
|
||||
assert module.background_negative_repeat_count(
|
||||
is_negative=True,
|
||||
sample_role="background_candidate",
|
||||
split="val",
|
||||
background_negative_repeat=4,
|
||||
) == 1
|
||||
assert module.background_negative_repeat_count(
|
||||
is_negative=False,
|
||||
sample_role="background_candidate",
|
||||
split="train",
|
||||
background_negative_repeat=4,
|
||||
) == 1
|
||||
assert module.background_negative_repeat_count(
|
||||
is_negative=True,
|
||||
sample_role="reference",
|
||||
split="train",
|
||||
background_negative_repeat=4,
|
||||
) == 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"
|
||||
|
||||
|
||||
def test_export_can_skip_low_variance_negative_tiles(tmp_path: Path, monkeypatch) -> None:
|
||||
module = load_tile_exporter()
|
||||
raster_path = tmp_path / "sample.tif"
|
||||
reference_path = tmp_path / "reference.geojson"
|
||||
raster_path.write_bytes(b"fake-raster")
|
||||
reference_path.write_text('{"type": "FeatureCollection", "features": []}', encoding="utf-8")
|
||||
|
||||
class FakeDataset:
|
||||
width = 256
|
||||
height = 128
|
||||
crs = "EPSG:31370"
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, traceback):
|
||||
return False
|
||||
|
||||
class FakeRasterio:
|
||||
@staticmethod
|
||||
def open(path):
|
||||
assert Path(path) == raster_path
|
||||
return FakeDataset()
|
||||
|
||||
class FakeImageObject:
|
||||
def __init__(self, array):
|
||||
self.array = array
|
||||
|
||||
def save(self, path):
|
||||
Path(path).write_bytes(b"png")
|
||||
|
||||
class FakeImage:
|
||||
@staticmethod
|
||||
def fromarray(array):
|
||||
return FakeImageObject(array)
|
||||
|
||||
def fake_image_array_from_raster_window(dataset, tile_window):
|
||||
if tile_window.col_off == 0:
|
||||
return np.full((128, 128, 3), 255, dtype=np.uint8)
|
||||
image = np.zeros((128, 128, 3), dtype=np.uint8)
|
||||
image[:, 64:, :] = 80
|
||||
return image
|
||||
|
||||
monkeypatch.setattr(module, "rasterio", FakeRasterio)
|
||||
monkeypatch.setattr(module, "Image", FakeImage)
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"load_reference_pixel_boxes",
|
||||
lambda reference_path, dataset, min_label_px, **kwargs: [],
|
||||
)
|
||||
monkeypatch.setattr(module, "image_array_from_raster_window", fake_image_array_from_raster_window)
|
||||
|
||||
records = module.export_sample_tiles(
|
||||
sample={
|
||||
"sample_slug": "blank_negative",
|
||||
"sample_role": "background_candidate",
|
||||
"background_category": "pure_empty_negative",
|
||||
"raster_path": str(raster_path),
|
||||
"reference_path": str(reference_path),
|
||||
},
|
||||
manifest_path=tmp_path / "operator_samples_manifest.json",
|
||||
output_dir=tmp_path / "dataset",
|
||||
val_slugs=set(),
|
||||
tile_size=128,
|
||||
stride=128,
|
||||
negative_keep_ratio=1.0,
|
||||
min_label_px=4,
|
||||
min_label_visible_ratio=0.0,
|
||||
background_negative_repeat=1,
|
||||
drop_low_variance_negatives=True,
|
||||
blank_range_threshold=3,
|
||||
reference_source="grb",
|
||||
reference_layer="buildings",
|
||||
)
|
||||
|
||||
skipped = [record for record in records if not record["kept"]]
|
||||
kept = [record for record in records if record["kept"]]
|
||||
|
||||
assert len(skipped) == 1
|
||||
assert skipped[0]["skip_reason"] == "low_visual_variance_negative"
|
||||
assert skipped[0]["low_visual_variance"] is True
|
||||
assert skipped[0]["is_negative"] is True
|
||||
assert skipped[0]["tile_index"] == 0
|
||||
assert len(kept) == 1
|
||||
assert kept[0]["tile_index"] == 1
|
||||
assert kept[0]["low_visual_variance"] is False
|
||||
assert Path(kept[0]["image_path"]).exists()
|
||||
Reference in New Issue
Block a user