Skip to content

COCOeval

Run COCO evaluation to compute AP/AR metrics.

from hotcoco import COCO, COCOeval

coco_gt = COCO("instances_val2017.json")
coco_dt = coco_gt.load_res("detections.json")

ev = COCOeval(coco_gt, coco_dt, "bbox")
ev.evaluate()
ev.accumulate()
ev.summarize()
use hotcoco::{COCO, COCOeval};
use hotcoco::params::IouType;
use std::path::Path;

let coco_gt = COCO::new(Path::new("instances_val2017.json"))?;
let coco_dt = coco_gt.load_res(Path::new("detections.json"))?;

let mut ev = COCOeval::new(coco_gt, coco_dt, IouType::Bbox);
ev.evaluate();
ev.accumulate();
ev.summarize();

Constructor

COCOeval(
    coco_gt: COCO,
    coco_dt: COCO,
    iou_type: str,
    *,
    lvis_style: bool = False,
    oid_style: bool = False,
    hierarchy: Hierarchy | None = None,
)
Parameter Type Default Description
coco_gt COCO Ground truth COCO object
coco_dt COCO Detections COCO object (from load_res)
iou_type str "bbox", "segm", "keypoints", or "obb"
lvis_style bool False Enable LVIS federated evaluation mode
oid_style bool False Enable Open Images evaluation mode (IoU=0.5, group-of matching)
hierarchy Hierarchy | None None Category hierarchy for GT expansion in OID mode

pycocotools keyword spellings

pycocotools spells the constructor keywords cocoGt, cocoDt, and iouType, and consumers pass them that way (torchmetrics' pycocotools backend calls COCOeval(gt, dt, iouType=...)). Both spellings are accepted; mixing the two spellings of one argument is an error.

// Standard COCO
COCOeval::new(coco_gt: COCO, coco_dt: COCO, iou_type: IouType) -> Self

// LVIS federated
COCOeval::new_lvis(coco_gt: COCO, coco_dt: COCO, iou_type: IouType) -> Self

// Open Images
COCOeval::new_oid(coco_gt: COCO, coco_dt: COCO, hierarchy: Option<Hierarchy>) -> Self
Parameter Type Description
coco_gt COCO Ground truth COCO object
coco_dt COCO Detections COCO object (from load_res)
iou_type IouType IouType::Bbox, IouType::Segm, IouType::Keypoints, or IouType::Obb
hierarchy Option<Hierarchy> Category hierarchy for GT expansion; None to skip expansion

The same classes are also reachable under hotcoco.detection (from hotcoco.detection import COCOeval), an explicit namespace for the detection metric family. Both spellings return the same objects.


Properties

params

params: Params

Evaluation parameters. Modify before calling evaluate().

pub params: Params

See Params for every configurable field and an example of changing them.


stats

stats: np.ndarray | list[float]

The 12 summary metrics (10 for keypoints) as a float64 numpy array, populated after summarize(). An empty list before summarize() is called — both states match pycocotools.

ev.summarize()
print(f"AP: {ev.stats[0]:.3f}")
print(f"AP50: {ev.stats[1]:.3f}")
fn stats(&self) -> Option<&[f64]>
ev.summarize();
if let Some(stats) = ev.stats() {
    println!("AP: {:.3}", stats[0]);
    println!("AP50: {:.3}", stats[1]);
}

eval_imgs

Per-image, per-category, per-area-range evaluation results, populated after evaluate(). Entries are None where an image has neither ground truth nor detections for that category, as in pycocotools.

eval_imgs: list[dict | None]
fn eval_imgs(&self) -> &[Option<EvalImg>]

Each entry is a dict whose keys match pycocotools' evalImgsimage_id and category_id in snake_case, everything else camelCase (there are no snake_case variants of the camelCase keys):

Key Description
image_id Image ID
category_id Category ID
aRng Area range [min, max] this entry was evaluated under
maxDet Max-detections cap applied
dtIds Detection annotation IDs, score-descending
gtIds Ground-truth annotation IDs
dtMatches Per IoU threshold: matched GT id per detection (0 = unmatched)
gtMatches Per IoU threshold: matched DT id per ground truth (0 = unmatched)
dtScores Detection confidence scores
dtIgnore Per IoU threshold: whether each detection was ignored
gtIgnore Whether each GT was ignored (crowd or out of area range)
dtMatched / gtMatched hotcoco extension: per-threshold boolean match flags
gtInDenominator hotcoco extension: whether each GT counts in the recall denominator (differs from gtIgnore for Open Images group-of boxes)

For a worked example, see Per-image evaluation results.


eval

Accumulated precision/recall arrays, populated after accumulate().

eval: dict | None

Contains "precision", "recall", and "scores" arrays.

fn accumulated(&self) -> Option<&AccumulatedEval>

Access elements with precision_idx(t, r, k, a, m) and recall_idx(t, k, a, m).

precision and scores have shape [T x R x K x A x M]; recall has shape [T x K x A x M]:

Dimension Name Default size Description
T IoU thresholds 10 [0.50, 0.55, ..., 0.95]
R Recall thresholds 101 [0.00, 0.01, ..., 1.00]
K Categories varies Number of evaluated categories
A Area ranges 4 [all, small, medium, large]
M Max detections 3 [1, 10, 100]

A value of -1 means no data — for example, no ground truth annotations for that category and area combination.

For worked examples, see Precision and recall arrays and Extracting per-category AP.


Methods

Call order

get_results(), print_results(), summary_lines(), report(), results(), and save_results() read the summary, so call them after summarize() (or run()). report(), results(), and save_results() raise RuntimeError otherwise; get_results() returns an empty dict.

metric_keys(), metric_defs(), provenance(), is_benchmark_standard(), and reference_deviations() read only the configuration and work before evaluate().

evaluate

evaluate() -> None

Run per-image evaluation. Matches detections to ground truth annotations using greedy matching sorted by confidence. Must be called before accumulate().

Populates eval_imgs.


accumulate

accumulate() -> None

Accumulate per-image results into precision/recall curves using interpolated precision at 101 recall thresholds.

Populates eval.


summarize

summarize() -> None

Compute and print the standard COCO metrics. Populates stats.

The table goes through Python's sys.stdout, so contextlib.redirect_stdout captures or silences it, and it shows in a notebook cell. To skip printing altogether, call summary_lines instead.

Non-default parameters

summarize() uses a fixed display format that assumes default iou_thrs, max_dets, and area_rng_lbl. If you've changed any of these, a UserWarning is emitted (catchable with warnings.catch_warnings, visible in Jupyter) and some metrics might show -1.000 (for example, AP50 when iou_thrs doesn't include 0.50). The stats array always has 12 entries (10 for keypoints) regardless of your parameters. -1.000 always means "not computed for this configuration" — an unknown area label or max-dets value degrades to -1.0 rather than silently substituting the "all" slice.

Prints 12 lines for bbox/segm (10 for keypoints):

 Average Precision  (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.382
 Average Precision  (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = 0.584
 ...

run

run() -> None

Run the full pipeline in one call: evaluate()accumulate()summarize(). Primarily used with LVIS pipelines (Detectron2, MMDetection) that expect a single run() call.


metric_keys

metric_keys() -> list[str]

Return metric names in canonical display order for the current evaluation mode. This is the authoritative ordering — the same list that drives summarize() and get_results().

ev = COCOeval(gt, dt, "bbox")
ev.metric_keys()
# ['AP', 'AP50', 'AP75', 'APs', 'APm', 'APl', 'AR1', 'AR10', 'AR100', 'ARs', 'ARm', 'ARl']

metric_defs

metric_defs() -> list[dict]

Return the metric catalog as structured data, one dict per metric in metric_keys() order: name (str), ap (bool — AP vs AR), iou_thr (float or None for the full 0.50:0.05:0.95 sweep), area (str), max_det (int), and freq_group ("rare" / "common" / "frequent" or None; LVIS only).

ev.metric_defs()[1]
# {'name': 'AP50', 'ap': True, 'iou_thr': 0.5, 'area': 'all', 'max_det': 100, 'freq_group': None}

This exists so renderers read a metric's axes instead of parsing them back out of its name — "AR10" is ambiguous between a detection cap of 10 and an IoU of 0.10, and only the catalog knows which. hotcoco's own PDF report and dashboard consume it.


get_results

get_results(prefix: str | None = None, per_class: bool = False) -> dict[str, float]

Return the summary metrics as a dict.

Parameter Type Default Description
prefix str | None None If given, each key is prefixed as "{prefix}/{metric}".
per_class bool False If True, include per-category AP values keyed as "AP/{cat_name}" (or "{prefix}/AP/{cat_name}" with a prefix).

Standard bbox/segm keys: AP, AP50, AP75, APs, APm, APl, AR1, AR10, AR100, ARs, ARm, ARl.

Keypoint keys: AP, AP50, AP75, APm, APl, AR, AR50, AR75, ARm, ARl.

LVIS keys: see The 13 LVIS metrics.

ev.run()
ev.get_results()   # {"AP": 0.578, "AP50": 0.861, ...}

For prefixed and per-class keys and how they feed an experiment tracker, see Logging metrics.


print_results() -> None

Print a formatted results table to stdout. For LVIS, matches the lvis-api print_results() style.


summary_lines

summary_lines() -> list[str]

The same lines summarize() prints, returned instead of written to stdout — one string per metric, already formatted. Use it to route the summary into a logger, a report, or a test assertion.


virtual_cat_names

virtual_cat_names: list[str]   # property

Category names added by Open Images hierarchy expansion — ancestor categories that exist in the hierarchy but not in the dataset's own taxonomy. Empty when not in OID mode, when no hierarchy expansion occurred, or before evaluate(). Use it to distinguish expanded ancestor categories from the model's native classes:

ev = COCOeval(gt, dt, "bbox", oid_style=True, hierarchy=h)
ev.evaluate()
ev.virtual_cat_names   # for example, ['Carnivore', 'Mammal'] — no parentheses; it's a property

slice_by

slice_by(slices: dict[str, list[int]] | Callable[[dict], str]) -> dict[str, Any]

Re-accumulate metrics for named subsets of images without recomputing IoU, and return one metrics dict per slice. Pass either an explicit {name: [image_ids]} mapping or a function that takes an image dict and returns a slice name.

ev.run()
by_light = ev.slice_by({"day": day_ids, "night": night_ids})
by_light["night"]["AP"]

Requires evaluate() to have run. Matching is done once and reused for every slice, so slicing a dozen ways costs barely more than slicing one way. See sliced evaluation.


report

report() -> dict

Return a full evaluation report.

This is the shape every hotcoco metric family reports in, so code that renders a detection report renders a panoptic or tracking one unchanged.

Returns a dict with:

Key Type Description
"task" str "detection".
"provenance" str "parity_verified" or "extension" — see Provenance.
"metrics" dict[str, float] Summary metrics keyed by name.
"per_class" dict[str, dict[str, float]] {class_name: {metric: value}}.
"per_group" dict[str, dict[str, float]] LVIS frequency buckets in LVIS mode; empty otherwise.
"curves" dict[str, list[float]] One aggregate precision-recall curve per IoU threshold ("pr@0.50", "pr@0.55", ...), averaged over categories at area="all" and the largest max_dets, plus the shared "rec_thrs" x-axis.
"params" dict The evaluation parameters used.
ev.run()
report = ev.report()

report["metrics"]["AP"]              # 0.377
report["per_class"]["person"]["AP"]  # 0.521

Provenance values

Value Meaning
"parity_verified" Comparable to a published leaderboard
"extension" A real metric or configuration, but not a leaderboard number

What each value covers and why is in Check provenance before you publish a number. For plotting curves, see Plotting precision-recall curves.


provenance

provenance() -> str

Return the same value as report()["provenance"], read from the configuration alone. See Checking before you evaluate.


is_benchmark_standard

is_benchmark_standard() -> bool

Return True exactly when provenance() is "parity_verified". Any other value — including a variant added in a future release — reads as False, so a renderer that checks this predicate caveats unknown provenance rather than passing it through. See Checking before you evaluate.


reference_deviations

reference_deviations() -> list[str]

Return one human-readable sentence per way this run departs from the reference configuration, empty exactly when provenance() is "parity_verified". See Checking before you evaluate.


results

results(per_class: bool = False) -> dict

Return evaluation results as a serializable dict.

Parameter Type Default Description
per_class bool False If True, include per-category AP values under the "per_class" key.

Returns a dict with:

Key Type Description
"hotcoco_version" str hotcoco version that produced these results.
"provenance" str Same value as report()["provenance"].
"params" dict Evaluation parameters: iou_type, eval_mode, iou_thresholds, recall_thresholds, area_ranges, max_dets, use_cats, kpt_oks_sigmas, and reference_deviations — enough for a saved run to explain its own provenance.
"metrics" dict[str, float] Summary metrics keyed by name (same keys as get_results()).
"per_class" dict[str, float] | absent Per-category AP values keyed by category name. Only present if per_class=True.
ev.run()
ev.results(per_class=True)   # {"hotcoco_version": ..., "metrics": {...}, "per_class": {...}}

Serialized, the dict looks like this:

{
  "hotcoco_version": "1.0.0",
  "provenance": "parity_verified",
  "params": {
    "iou_type": "bbox",
    "eval_mode": "coco",
    "iou_thresholds": [0.5, 0.55, ...],
    "recall_thresholds": [0.0, 0.01, ...],
    "area_ranges": {"all": [0, 10000000000.0], "small": [0, 1024.0], ...},
    "max_dets": [1, 10, 100],
    "use_cats": true,
    "kpt_oks_sigmas": [0.026, 0.025, ...],
    "reference_deviations": []
  },
  "metrics": {
    "AP": 0.378, "AP50": 0.584, "AP75": 0.412, ...
  },
  "per_class": {
    "person": 0.58, "car": 0.41, ...
  }
}

For a worked example, see Saving results to JSON.


save_results

save_results(path: str, per_class: bool = False) -> None

Save evaluation results to a JSON file. Raises IOError if the file cannot be written.

Parameter Type Default Description
path str Output file path.
per_class bool False If True, include per-category AP values.
ev.run()
ev.save_results("results.json", per_class=True)

The file holds the dict returned by results().


confusion_matrix

confusion_matrix(
    iou_thr: float = 0.5,
    max_det: int | None = None,
    min_score: float | None = None,
) -> dict

Compute a per-category confusion matrix. Unlike evaluate(), this method compares all detections in an image against all ground truth boxes regardless of category, enabling cross-category confusion analysis.

This method is standalone — no evaluate() call is needed first.

Parameters:

Parameter Type Default Description
iou_thr float 0.5 IoU threshold for a DT↔GT match
max_det int \| None last params.max_dets value Max detections per image by score
min_score float \| None None Discard detections below this confidence before max_det truncation

Returns a dict with:

Key Type Description
"matrix" np.ndarray[uint64] shape (K+1, K+1) Raw confusion counts. Rows = GT category, cols = predicted. Index K is background. Unsigned — cast before subtracting counts (for example, matrix.astype(np.int64)) to avoid underflow.
"normalized" np.ndarray[float64] shape (K+1, K+1) Row-normalized version (rows sum to 1.0; zero rows stay zero).
"cat_ids" list[int] Category IDs for rows/cols 0..K-1.
"cat_names" list[str] Category names for rows/cols 0..K-1, in the same order as cat_ids.
"num_cats" int Number of categories K.
"iou_thr" float IoU threshold used.
cm = ev.confusion_matrix(iou_thr=0.5, max_det=100)
cm["matrix"]   # np.ndarray uint64, shape (K+1, K+1)

For the cell layout and a full walkthrough, see Confusion matrix in the diagnostics guide.


tide_errors

tide_errors(
    pos_thr: float = 0.5,
    bg_thr: float = 0.1,
) -> dict

Decompose detection errors into six TIDE error types (Bolya et al., ECCV 2020) and compute ΔAP — the AP gain from eliminating each error type.

Requires evaluate() to have been called first.

Parameters:

Parameter Type Default Description
pos_thr float 0.5 IoU threshold for TP/FP classification
bg_thr float 0.1 Background IoU threshold for Loc/Both/Bkg discrimination

Returns a dict with:

Key Type Description
"delta_ap" dict[str, float] ΔAP for each error type. Keys: "Cls", "Loc", "Both", "Dupe", "Bkg", "Miss", plus tidecv's special oracles "FP" (suppress every false positive) and "FN" (drop every missed GT from the denominator; a superset of "Miss").
"counts" dict[str, int] Count of each error type. Keys: "Cls", "Loc", "Both", "Dupe", "Bkg", "Miss".
"ap_base" float Baseline mean AP at pos_thr.
"pos_thr" float IoU threshold used.
"bg_thr" float Background threshold used.
result = ev.tide_errors(pos_thr=0.5, bg_thr=0.1)
result["delta_ap"]   # {"Cls": ..., "Loc": ..., "Both": ..., "Dupe": ..., "Bkg": ..., "Miss": ..., "FP": ..., "FN": ...}

For the error taxonomy and a full walkthrough, see TIDE error analysis in the diagnostics guide.


calibration

calibration(
    n_bins: int = 10,
    iou_threshold: float = 0.5,
) -> dict

Compute confidence calibration metrics — how well confidence scores predict actual detection accuracy.

Requires evaluate() to have been called first. Bins all non-ignored detections by confidence score and compares the mean confidence in each bin to the fraction of true positives.

Parameters:

Parameter Type Default Description
n_bins int 10 Number of equal-width confidence bins in [0, 1].
iou_threshold float 0.5 IoU threshold for TP/FP classification. Must match one of params.iouThrs.

Returns a dict with:

Key Type Description
"ece" float Expected Calibration Error — weighted mean of per-bin |accuracy - confidence|.
"mce" float Maximum Calibration Error — worst per-bin gap.
"bins" list[dict] Per-bin breakdown. Each dict has bin_lower, bin_upper, avg_confidence, avg_accuracy, count.
"per_category" dict[str, float] Per-category ECE, keyed by category name.
"iou_threshold" float IoU threshold used.
"n_bins" int Number of bins.
"num_detections" int Total non-ignored detections analyzed.
cal = ev.calibration(n_bins=10, iou_threshold=0.5)
cal["ece"], cal["mce"]   # (0.0412, 0.1873)

For interpretation and a full walkthrough, see Confidence calibration in the diagnostics guide.


f_scores

f_scores(beta: float = 1.0) -> dict[str, float]

Compute F-beta scores after accumulate() (or run()).

For each (IoU threshold, category), finds the confidence operating point that maximizes F-beta, then averages across categories — analogous to how mAP averages precision. Returns three metrics mirroring AP/AP50/AP75.

Parameter Type Default Description
beta float 1.0 Trade-off weight. beta=1 → F1 (equal weight). beta<1 → weights precision. beta>1 → weights recall.

Returns a dict with three keys:

Key Description
"F1" Mean max-F1 across IoU 0.50:0.05:0.95, all categories
"F1_50" Max-F1 at IoU=0.50
"F1_75" Max-F1 at IoU=0.75

Key names reflect beta, formatted with no trailing zeros: "F0.5", "F0.5_50", "F0.5_75" for beta=0.5; "F2", "F2_50", "F2_75" for beta=2.0.

Returns an empty dict if accumulate() has not been called.

ev.run()
ev.f_scores()   # {"F1": 0.523, "F1_50": 0.712, "F1_75": 0.581}

For a full walkthrough, see F-scores in the diagnostics guide.


image_diagnostics

image_diagnostics(
    iou_thr: float = 0.5,
    score_thr: float = 0.5,
) -> dict

Per-image diagnostics: annotation TP/FP/FN index, per-image F1 and AP scores, error profiles, and label error candidates.

Requires evaluate() to have been called first.

Parameters:

Parameter Type Default Description
iou_thr float 0.5 IoU threshold for TP/FP classification (snapped to nearest in params.iouThrs).
score_thr float 0.5 Minimum detection confidence to consider for label error detection.

Returns a dict with:

Key Type Description
"dt_status" dict[int, str] Detection annotation ID → "tp" or "fp".
"gt_status" dict[int, str] GT annotation ID → "matched" or "fn".
"dt_match" dict[int, int] TP detection → matched GT annotation ID.
"gt_match" dict[int, int] Matched GT → the detection that matched it.
"img_summary" dict[int, dict] Per-image summary; see the image summary table that follows.
"label_errors" list[dict] Suspected label errors, sorted by detection score descending; see the label error table that follows.
"iou_thr" float Actual IoU threshold used (snapped).
"score_thr" float Score threshold used for label error detection.

Each image summary dict contains:

Key Type Description
"tp" int True positive count.
"fp" int False positive count.
"fn" int False negative count.
"f1" float F1 score: 2*tp / (2*tp + fp + fn). 1.0 for empty images.
"ap" float AP at the selected IoU threshold (101-point interpolation).
"error_profile" str One of "perfect", "fp_heavy", "fn_heavy", "mixed".

Each label error dict contains:

Key Type Description
"image_id" int Image containing the suspected error.
"dt_id" int Detection annotation ID.
"dt_score" float Detection confidence.
"dt_category" str Detection category name.
"dt_category_id" int Detection category ID.
"gt_id" int \| None Overlapping GT annotation ID (None for missing_annotation).
"gt_category" str \| None GT category name.
"gt_category_id" int \| None GT category ID.
"iou" float Bbox IoU between detection and GT (0.0 for missing_annotation).
"type" str "wrong_label" or "missing_annotation".
diag = ev.image_diagnostics(iou_thr=0.5, score_thr=0.5)
diag["img_summary"][42]   # {"tp": 3, "fp": 1, "fn": 0, "f1": 0.857, "ap": 0.75, "error_profile": "fp_heavy"}

For a full walkthrough, see Per-image diagnostics and label error detection in the diagnostics guide.


LVIS evaluation

These three names are drop-in replacements for the lvis-api package, so Detectron2 and MMDetection pipelines run unchanged. For the federated protocol, see LVIS evaluation; for redirecting an existing from lvis import ... pipeline, see LVIS-based pipelines.

LVISeval

hotcoco.LVISeval(gt: COCO, dt: COCO, iou_type: str = "segm") -> COCOeval

Returns a COCOeval configured for federated evaluation — equivalent to COCOeval(gt, dt, iou_type, lvis_style=True). Supports run(), print_results(), and get_results(), which is what those pipelines call. The default iou_type is "segm", matching lvis-api.

LVISEval (capital E) is an alias: that is the spelling lvis-api exports and the one from lvis import LVISEval expects.

ev = LVISeval(lvis_gt, lvis_dt, "segm")
ev.run()
ev.get_results()   # {"AP": ..., "APr": ..., "APc": ..., "APf": ..., "AR@300": ..., ...}

The metric set is listed in The 13 LVIS metrics.

LVIS

An alias for COCO, provided because lvis-api names its dataset class LVIS. Loading is identical.

LVISResults

hotcoco.LVISResults(lvis_gt: COCO, results, max_dets: int = 300) -> COCO

Returns a COCO detections object, equivalent to lvis_gt.load_res(results). max_dets is accepted for API compatibility but not applied here — the 300-detection cap is a Params setting that LVISeval already configures.


Module-level functions

compare

hotcoco.compare(
    eval_a: COCOeval,
    eval_b: COCOeval,
    n_bootstrap: int = 0,
    seed: int = 42,
    confidence: float = 0.95,
) -> dict

Pairwise model comparison. Both evaluators must have had evaluate() called and use the same eval_mode, iou_type, and evaluation grid — mismatched iou_thrs, rec_thrs, max_dets, or area ranges raise ValueError rather than summarizing one run under the other's catalog. Accumulation and summarization are performed internally on the shared image set.

Parameter Type Default Description
eval_a COCOeval Baseline model evaluation.
eval_b COCOeval Improved model evaluation.
n_bootstrap int 0 Bootstrap samples for CIs (0 = disabled).
seed int 42 Random seed for reproducibility.
confidence float 0.95 Confidence level, for example 0.95 for a 95% CI.

Returns a dict with:

Key Type Description
metric_keys list[str] Metric names in canonical display order.
metrics_a dict[str, float] Summary metrics for model A.
metrics_b dict[str, float] Summary metrics for model B.
deltas dict[str, float] Per-metric delta (B − A).
ci dict or None Bootstrap CIs per metric (lower, upper, confidence, prob_positive, std_err). None if n_bootstrap=0.
per_category list[dict] Per-category AP comparison, sorted by delta ascending. Each entry has cat_id, cat_name, ap_a, ap_b, delta.
n_bootstrap int Number of bootstrap samples used.
num_images int Number of shared images.
result = hotcoco.compare(ev_a, ev_b, n_bootstrap=1000)
result["deltas"]["AP"], result["ci"]["AP"]   # (0.033, {"lower": 0.01, "upper": 0.05, ...})

For bootstrap interpretation and a full walkthrough, see Model comparison in the diagnostics guide.