Record unique hard-negative YOLO candidate gate
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-09 03:54:48 +02:00
parent afd2cba7c8
commit 4e9fe6ccb1
6 changed files with 202 additions and 7 deletions
+17
View File
@@ -603,6 +603,23 @@ and maximum background detections per sample. It is evidence/report tooling
only: it does not run inference, mutate application data, download models or
change the active YOLO configuration.
If a legacy positive evidence portfolio records `model_asset_id` at portfolio
level but does not include per-run tile size/overlap, pass explicit tile
defaults instead of letting the report guess:
```bash
python scripts/build_detection_model_promotion_report.py \
--positive-portfolio /mnt/user/appdata/geointel/artifacts/detection-calibration-portfolio/uniquehardneg160e50-positive/calibration_evidence_portfolio.json \
--hard-negative-summary /mnt/user/appdata/geointel/artifacts/detection-hard-negatives/uniquehardneg160e50/hard_negative_matrix_summary.json \
--output-dir /mnt/user/appdata/geointel/artifacts/detection-model-promotion/uniquehardneg160e50-positive-vs-hard-negative \
--min-positive-samples 7 \
--min-background-samples 9 \
--min-mean-f1 0.25 \
--max-background-detections-per-sample 0 \
--default-positive-tile-size 640 \
--default-positive-tile-overlap 64
```
Clean old offline demo export artifacts without touching uploaded source data:
```bash
@@ -47,6 +47,18 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--min-background-samples", type=int, default=3)
parser.add_argument("--min-mean-f1", type=float, default=0.25)
parser.add_argument("--max-background-detections-per-sample", type=int, default=0)
parser.add_argument(
"--default-positive-tile-size",
type=int,
default=None,
help="Tile size to use for positive portfolio runs that do not record tile_size.",
)
parser.add_argument(
"--default-positive-tile-overlap",
type=int,
default=None,
help="Tile overlap to use for positive portfolio runs that do not record tile_overlap.",
)
return parser.parse_args()
@@ -56,15 +68,23 @@ def read_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8-sig"))
def as_candidate_key(item: dict[str, Any]) -> CandidateKey | None:
model_asset_id = item.get("model_asset_id") or item.get("model_request")
def as_candidate_key(
item: dict[str, Any],
*,
fallback_model_asset_id: str | None = None,
fallback_tile_size: int | None = None,
fallback_tile_overlap: int | None = None,
) -> CandidateKey | None:
model_asset_id = item.get("model_asset_id") or item.get("model_request") or fallback_model_asset_id
if not model_asset_id:
return None
try:
return CandidateKey(
model_asset_id=str(model_asset_id),
tile_size=int(item.get("tile_size")),
tile_overlap=int(item.get("tile_overlap")),
tile_size=int(item.get("tile_size") if item.get("tile_size") is not None else fallback_tile_size),
tile_overlap=int(
item.get("tile_overlap") if item.get("tile_overlap") is not None else fallback_tile_overlap
),
threshold=float(item.get("threshold")),
)
except (TypeError, ValueError):
@@ -81,12 +101,23 @@ def numeric(item: dict[str, Any], key: str) -> float | None:
return None
def collect_positive_runs(portfolio: dict[str, Any]) -> dict[CandidateKey, list[dict[str, Any]]]:
def collect_positive_runs(
portfolio: dict[str, Any],
*,
default_tile_size: int | None = None,
default_tile_overlap: int | None = None,
) -> dict[CandidateKey, list[dict[str, Any]]]:
best_by_candidate_sample: dict[tuple[CandidateKey, str], dict[str, Any]] = {}
portfolio_model_asset_id = portfolio.get("model_asset_id")
for sample in portfolio.get("samples") or []:
sample_slug = str(sample.get("sample_slug") or "unknown")
for run in sample.get("runs") or []:
key = as_candidate_key(run)
key = as_candidate_key(
run,
fallback_model_asset_id=str(portfolio_model_asset_id) if portfolio_model_asset_id else None,
fallback_tile_size=default_tile_size,
fallback_tile_overlap=default_tile_overlap,
)
f1 = numeric(run, "f1_score")
if key is None or f1 is None:
continue
@@ -136,7 +167,11 @@ def build_decisions(args: argparse.Namespace) -> dict[str, Any]:
portfolio_path = Path(args.positive_portfolio)
positive_portfolio = read_json(portfolio_path)
background_paths = [Path(path) for path in args.hard_negative_summary]
positive = collect_positive_runs(positive_portfolio)
positive = collect_positive_runs(
positive_portfolio,
default_tile_size=args.default_positive_tile_size,
default_tile_overlap=args.default_positive_tile_overlap,
)
background = collect_background_runs(background_paths)
decisions = []