audit YOLO geometry and overlapping validation rows
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Jens
2026-08-09 19:36:15 +02:00
parent fc18e72c7f
commit 736e773fb8
13 changed files with 789 additions and 16 deletions
+53 -4
View File
@@ -45,13 +45,50 @@ def validation_images(dataset_yaml: Path) -> list[Path]:
images = sorted(
path
for path in directory.iterdir()
if path.is_file() and path.suffix.casefold() in {".jpg", ".jpeg", ".png", ".tif", ".tiff"}
if path.is_file()
and path.suffix.casefold() in {".jpg", ".jpeg", ".png", ".tif", ".tiff"}
)
if not images:
raise ValueError("validation image directory is empty")
return images
def dataset_overlap_evidence(dataset_yaml: Path) -> dict[str, Any]:
summary_path = dataset_yaml.parent / "yolo_tile_dataset_summary.json"
if not summary_path.is_file():
return {
"status": "unavailable",
"summary_path": str(summary_path),
"validation_rows_independent": None,
}
payload = json.loads(summary_path.read_text(encoding="utf-8"))
tile_size = payload.get("tile_size")
stride = payload.get("stride")
if not isinstance(tile_size, int) or not isinstance(stride, int) or stride <= 0:
return {
"status": "invalid",
"summary_path": str(summary_path),
"summary_sha256": sha256_file(summary_path),
"validation_rows_independent": None,
}
overlap_pixels = max(tile_size - stride, 0)
return {
"status": "overlapping" if overlap_pixels else "non_overlapping",
"summary_path": str(summary_path),
"summary_sha256": sha256_file(summary_path),
"tile_size": tile_size,
"stride": stride,
"overlap_pixels": overlap_pixels,
"validation_rows_independent": overlap_pixels == 0,
"interpretation": (
"Tile metrics can repeat the same source object and are valid for "
"candidate ranking only, not independent object-level uncertainty."
if overlap_pixels
else "Tile rows do not overlap according to the dataset summary."
),
}
def metric_value(metrics: Any, attribute: str) -> float:
value = getattr(metrics.box, attribute)
return float(value)
@@ -68,7 +105,9 @@ def background_detection_count(
) -> tuple[int, int]:
selected = [path for path in images if path.stem.casefold().startswith(prefixes)]
if not selected:
raise ValueError("no validation images match the declared pure-background prefixes")
raise ValueError(
"no validation images match the declared pure-background prefixes"
)
count = 0
for start in range(0, len(selected), 16):
results = model.predict(
@@ -102,6 +141,7 @@ def main() -> int:
parser.error("--background-confidence must be between zero and one")
dataset_yaml = args.dataset_yaml.expanduser().resolve(strict=True)
images = validation_images(dataset_yaml)
overlap_evidence = dataset_overlap_evidence(dataset_yaml)
prefixes = tuple(value.casefold() for value in args.background_prefix)
import torch
@@ -161,7 +201,13 @@ def main() -> int:
}
)
except Exception as exc: # preserve the complete attempted matrix
row.update({"status": "error", "error_type": type(exc).__name__, "error": str(exc)[:1000]})
row.update(
{
"status": "error",
"error_type": type(exc).__name__,
"error": str(exc)[:1000],
}
)
finally:
del model
if torch.cuda.is_available():
@@ -187,6 +233,7 @@ def main() -> int:
"claim_boundary": "Non-protected validation ranking only; no test, challenge or promotion claim.",
"dataset_yaml": str(dataset_yaml),
"dataset_yaml_sha256": sha256_file(dataset_yaml),
"dataset_overlap_evidence": overlap_evidence,
"validation_image_count": len(images),
"pure_background_prefixes": list(prefixes),
"pure_background_confidence": args.background_confidence,
@@ -198,7 +245,9 @@ def main() -> int:
"attempts": rows,
}
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
args.output.write_text(
json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
return 0 if successful else 2