Record unique hard-negative YOLO candidate gate
This commit is contained in:
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user