174 lines
5.9 KiB
Python
174 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
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 "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 "--negative-keep-ratio" in result.stdout
|
|
assert "--min-label-visible-ratio" in result.stdout
|
|
assert "--background-negative-repeat" in result.stdout
|
|
|
|
|
|
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"
|