calibrate a confidence threshold from one inference pass

Threshold calibration ran the model over every tile once per threshold — three
GPU passes to compare 0.50, 0.25 and 0.15 on a hundred-tile raster. The answer
is already in a single run at the lowest value: detections above a higher cut
are a subset of it, and duplicate suppression walks candidates in descending
confidence, so a lower-confidence box can never displace a higher-confidence
one. The kept set above any cut is identical whichever threshold the run used,
which is what makes one pass sufficient rather than merely cheaper.

QA now takes calibration_thresholds and reads each operating point off the same
precision/recall walk it already performs, marking the F1-optimal cut. The lab
runs inference once and fills its table from the sweep.

The contract test asserted the per-threshold loop by name, pinning the waste it
was meant to describe. It now states what calibration owes an operator: a row
per requested threshold, from one run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jens
2026-08-22 19:37:19 +02:00
co-authored by Claude Opus 5
parent 8a26007281
commit 2cd2c49389
11 changed files with 303 additions and 52 deletions
@@ -648,8 +648,11 @@ export function DetectionLab({
</thead>
<tbody>
{detectionCalibrationRows.map((row) => (
<tr key={row.threshold}>
<td>{row.threshold.toFixed(2)}</td>
<tr key={row.threshold} className={row.best_f1 ? 'calibration-row-best' : undefined}>
<td>
{row.threshold.toFixed(2)}
{row.best_f1 ? <span className="status-badge"> beste F1</span> : null}
</td>
<td>{row.status}</td>
<td>{row.detection_count ?? 'n.v.t.'}</td>
<td>{formatNullableNumber(row.precision ?? null, 3)}</td>