Add operator YOLO label QA contact sheets
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-11 21:34:22 +02:00
parent 8cb435361d
commit fbccf8322e
7 changed files with 682 additions and 0 deletions
@@ -61,6 +61,10 @@ def test_all_in_one_dockerfile_copies_operator_scripts_for_runtime_use() -> None
assert "COPY scripts/prepare_operator_real_data_samples.py /app/scripts/prepare_operator_real_data_samples.py" in dockerfile
assert "COPY scripts/export_operator_yolo_tile_dataset.py /app/scripts/export_operator_yolo_tile_dataset.py" in dockerfile
assert "COPY scripts/audit_operator_yolo_dataset_quality.py /app/scripts/audit_operator_yolo_dataset_quality.py" in dockerfile
assert (
"COPY scripts/render_operator_yolo_label_qa_contact_sheets.py "
"/app/scripts/render_operator_yolo_label_qa_contact_sheets.py"
) in dockerfile
assert "COPY scripts/train_operator_yolo_detector.sh /app/scripts/train_operator_yolo_detector.sh" in dockerfile
@@ -0,0 +1,162 @@
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
def test_operator_yolo_label_qa_contact_sheets_render_visual_artifacts(tmp_path: Path) -> None:
script_path = ROOT / "scripts" / "render_operator_yolo_label_qa_contact_sheets.py"
assert script_path.exists()
dataset_dir = tmp_path / "yolo-dataset"
image_train = dataset_dir / "images" / "train"
image_val = dataset_dir / "images" / "val"
labels_train = dataset_dir / "labels" / "train"
labels_val = dataset_dir / "labels" / "val"
image_train.mkdir(parents=True)
image_val.mkdir(parents=True)
labels_train.mkdir(parents=True)
labels_val.mkdir(parents=True)
for path, color in (
(image_train / "dense_000.png", (120, 130, 140)),
(image_train / "invalid_000.png", (80, 100, 120)),
(image_val / "missing_000.png", (90, 120, 90)),
(image_val / "negative_000.png", (50, 50, 55)),
):
Image.new("RGB", (64, 64), color=color).save(path)
(labels_train / "dense_000.txt").write_text(
"0 0.500000 0.500000 0.500000 0.500000\n"
"0 0.250000 0.250000 0.250000 0.250000\n",
encoding="utf-8",
)
(labels_train / "invalid_000.txt").write_text(
"0 0.500000 0.500000 0.300000 0.300000\n"
"not-a-valid-yolo-row\n",
encoding="utf-8",
)
(labels_val / "negative_000.txt").write_text("", encoding="utf-8")
missing_label_path = labels_val / "missing_000.txt"
summary_path = dataset_dir / "yolo_tile_dataset_summary.json"
summary_path.write_text(
json.dumps(
{
"status": "ok",
"dataset_yaml": str(dataset_dir / "dataset.yaml"),
"output_dir": str(dataset_dir),
"class_names": ["building"],
"tile_size": 64,
"stride": 64,
"tiles": [
{
"sample_slug": "dense",
"sample_role": "reference",
"background_category": "reference_aoi",
"split": "train",
"tile_index": 0,
"kept": True,
"image_path": str(image_train / "dense_000.png"),
"label_path": str(labels_train / "dense_000.txt"),
"label_count": 2,
"is_negative": False,
},
{
"sample_slug": "invalid",
"sample_role": "reference",
"background_category": "reference_aoi",
"split": "train",
"tile_index": 1,
"kept": True,
"image_path": str(image_train / "invalid_000.png"),
"label_path": str(labels_train / "invalid_000.txt"),
"label_count": 1,
"is_negative": False,
},
{
"sample_slug": "missing",
"sample_role": "background_candidate",
"background_category": "sparse_building_context",
"split": "val",
"tile_index": 2,
"kept": True,
"image_path": str(image_val / "missing_000.png"),
"label_path": str(missing_label_path),
"label_count": 1,
"is_negative": False,
},
{
"sample_slug": "negative",
"sample_role": "background_candidate",
"background_category": "pure_empty_negative",
"split": "val",
"tile_index": 3,
"kept": True,
"image_path": str(image_val / "negative_000.png"),
"label_path": str(labels_val / "negative_000.txt"),
"label_count": 0,
"is_negative": True,
},
],
}
),
encoding="utf-8",
)
output_dir = tmp_path / "label-qa"
result = subprocess.run(
[
sys.executable,
str(script_path),
"--summary-path",
str(summary_path),
"--output-dir",
str(output_dir),
"--max-tiles",
"4",
"--columns",
"2",
"--thumb-size",
"128",
],
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
assert "Operator YOLO label QA contact sheets rendered" in result.stdout
report = json.loads((output_dir / "operator_yolo_label_qa_summary.json").read_text(encoding="utf-8"))
assert report["status"] == "ok"
assert report["selected_tile_count"] == 4
assert report["rendered_tile_count"] == 4
assert report["missing_label_file_count"] == 1
assert report["invalid_label_count"] == 1
assert [tile["sample_slug"] for tile in report["selected_tiles"]] == [
"dense",
"invalid",
"missing",
"negative",
]
sheet_path = output_dir / report["contact_sheets"][0]["path"]
assert sheet_path.exists()
sheet = Image.open(sheet_path).convert("RGB")
assert sheet.size[0] >= 256
assert sheet.size[1] >= 256
assert len(sheet.getcolors(maxcolors=1000000) or []) > 4
markdown = (output_dir / "operator_yolo_label_qa_contact_sheet.md").read_text(encoding="utf-8")
assert "Operator YOLO Label QA Contact Sheets" in markdown
assert "missing label files: 1" in markdown
assert "invalid label rows: 1" in markdown
assert "contact_sheet_001.png" in markdown