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.

segm needs image sizes

With iou_type="segm", every image holding a polygon or box annotation the evaluation covers — in params.img_ids and, with use_cats on, params.cat_ids — must carry height and width, or evaluate() raises ValueError naming the images. A polygon drawn onto an image of unknown size is an empty mask, which would score as AP 0 with nothing to say why; pycocotools raises KeyError at the same point. RLE masks carry their own size and are not affected. Box evaluation never reads the fields.

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

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

// Open Images
COCOeval::new_oid(coco_gt: impl Into<Arc<COCO>>, coco_dt: impl Into<Arc<COCO>>, hierarchy: Option<Hierarchy>) -> Self

Pass a COCO to hand the dataset over, or an Arc<COCO> to share one ground truth across several evaluators. The evaluator holds both datasets behind Arc and exposes them through coco_gt() and coco_dt().

Parameter Type Description
coco_gt COCO or Arc<COCO> Ground truth COCO object
coco_dt COCO or Arc<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' evalImgs — image_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.


ious

pycocotools' ious attribute: the IoU (or OKS) matrix of every image and category evaluate() ran over, keyed by (image_id, category_id). Empty before evaluate(), and fixed at that call as pycocotools' is: params edited afterwards for accumulate() do not move it.

ious: dict[tuple[int, int], np.ndarray | list]

A value is a (D, G) float64 array of the cell's detections, in score order and cut at the last maxDets, against its ground truths, or [] where either side is empty, as in pycocotools. With useCats off the category key is -1. The dict is built on each access. One divergence: for keypoints with useCats off, pycocotools' computeOks ignores useCats and lists [] in every cell; hotcoco gives the pooled OKS matrix.

fn iou_matrices(&self) -> Vec<((u64, u64), Option<IouMatrix>)>

Every cell of the grid in key order, None where either side is empty; the pooled cell under use_cats = false is keyed by u64::MAX. IouMatrix offers rows(), cols(), row(i), values(), and into_values().


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. A library that embeds hotcoco and wants neither the table nor the non-default-parameter warnings should use it, and read what the warnings would have said from reference_deviations.

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 ReferenceDeviationWarning 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.

It behaves as those three calls do: the table goes through sys.stdout, and the same ReferenceDeviationWarnings as summarize are emitted.


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.

Like summarize, it prints through sys.stdout, so contextlib.redirect_stdout, pytest's capsys, and notebook cells capture it. Called before summarize(), it prints nothing and emits a CallOrderWarning. In Rust, print_results_lines() returns the same lines without printing.


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.

It populates stats as summarize() does and prints nothing, but it also skips the ReferenceDeviationWarnings that summarize() emits. That makes it the quiet path for code that embeds hotcoco, and it means nothing tells you the run is not comparable to the reference: call reference_deviations, or read provenance from report, when that matters.


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, snapped to the nearest in params.iouThrs. Raises RuntimeError if that threshold was not in the grid when evaluate() ran.
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, or it raises ValueError, and have been in it when evaluate() ran, or it raises RuntimeError.

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). Raises RuntimeError if that threshold was not in the grid when evaluate() ran.
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 | None = 300) -> COCO

Returns a COCO detections object: lvis_gt.load_res(results) capped the way lvis-api's LVISResults caps it — each image's max_dets highest-scoring detections across every category, ties in results-file order. max_dets=-1 or None keeps every detection, and an integral float such as 300.0 is that integer. The cap is COCO.cap_detections_per_image(max_dets), which you can call directly (None keeps every detection).

LVISeval evaluates an LVISResults result as is, as lvis-api's LVISEval takes an LVISResults object unchanged, so max_dets=1000 or -1 holds through evaluation. Detections loaded with plain load_res() get lvis-api's default instead: LVISeval caps them at 300 the same way when constructed, as lvis-api caps a results path or list it loads itself. The 300 does not follow params.max_dets, which lvis-api's LVISEval does not pass to LVISResults either; StreamingEval with lvis_style=True applies the same 300.


StreamingEval

Evaluate detector batches as they come and get a COCOeval back. Each update() call matches its images right away; finalize() assembles every image seen into an evaluator ready for accumulate(), summarize(), and report(). The results are identical to a batch COCOeval over the same annotations, however the images were batched. For the training loop this is built for, see Streaming evaluation.

StreamingEval(
    categories: list[dict],
    iou_type: str = "bbox",
    lvis_style: bool = False,
    params: Params | None = None,
)
Parameter Type Default Description
categories list[dict] — Every category the run will see, as COCO category dicts. A category with no annotations keeps a -1.0 slot instead of vanishing from the per-class results. The list cannot grow later: update() raises KeyError for an annotation in a category that is not on it.
iou_type str "bbox" "bbox", "segm", "keypoints", or "obb"
lvis_style bool False LVIS federated evaluation; sets max_dets to [300] when params is not given
params Params | None None Evaluation parameters, frozen for the run. img_ids, when set, skips images outside it; cat_ids, when empty, is filled from categories.

Raises ValueError for Open Images: hierarchy expansion needs the whole ground truth before the first image is evaluated.

StreamingEval::new(params: Params, eval_mode: EvalMode, categories: Vec<Category>) -> Result<Self>

Returns an error for EvalMode::OpenImages.

StreamingEval::unknown_category_ids(anns) -> Vec<u64> returns the ids in anns that categories does not list, sorted and without duplicates — the same check update() makes, for validating data before streaming it. update() returns them as Error::UnknownCategoryIds. It is empty when params.use_cats is false.

StreamingEval::merge(&mut self, other), to_bytes(&self), and from_bytes(&[u8]) match the Python methods in the following sections, with errors as Error::InvalidInput. The Rust merge takes other by value and moves its cells over; StreamingEval is Clone for a caller that needs to keep it.

update

se.update(
    images: list[dict],
    gt_anns: list[dict],
    dt_anns: list[dict] | ndarray,
    *,
    segmentation: list[dict | list[list[float]]] | None = None,
) -> None

Match a batch of images' ground truth against their detections now. images are COCO image dicts with at least id — plus neg_category_ids and not_exhaustive_category_ids in LVIS mode, where they apply. A batch of one is fine; passing the detector's whole batch amortizes the per-call setup. gt_anns are annotation dicts in the shape COCO(dict) accepts; id is assigned and a missing area is derived (the mask's pixel count, or the box's w × h for an annotation without a mask), so targets as a data loader yields them — image_id, category_id, bbox, iscrowd — are enough. For segm, every image holding a polygon or box annotation in an evaluated category needs height and width, or the call raises ValueError naming the images. dt_anns are raw predictions in the shape load_res() accepts — image_id, category_id, bbox (or segmentation/keypoints), and score — and are loaded the same way: ids assigned, area derived, iscrowd cleared. Within an image, detections with tied scores rank in the order given, as they do in a results file, so keep a batch's predictions in the order the detector emitted them. An image seen again in a later call replaces its earlier result.

dt_anns can also be the numpy array load_res() accepts, shape (N, 7) with columns [image_id, x, y, w, h, score, category_id], which skips building a dict per detection. An (N, 6) array has no category column and puts every row in category 1, as load_res() does. An id that is NaN, negative, or not a whole number raises ValueError. For segm, segmentation is a list of N RLE or polygon entries, one per row; it goes only with an array, since a dict carries its own. Every row has a box, so each detection's area is the box's w × h, as load_res() gives a result that has a bbox, and that area decides the small, medium, and large buckets. For mask area, as torchmetrics computes it, pass dicts that carry segmentation and no bbox.

Loading and matching run without the GIL; converting the dicts does not. The guide has what update() costs.

A ground truth or detection whose category_id is not in categories raises KeyError naming every such id, as update_anns does for an unknown annotation id. Without it the label would drop out of every metric, because the category list is fixed when the StreamingEval is built. The batch is rejected whole: the evaluator is as it was before the call, so a caller that catches the error and carries on gets honest numbers. A batch COCOeval does not check, and drops such an annotation silently. Only the categories listed count: params.cat_ids narrows what is evaluated without making the rest unknown, and with use_cats false no category is checked, because every annotation pools into one.

A NaN score raises the same ValueError as load_res(). Calling this after finalize() raises RuntimeError.

merge

StreamingEval.merge(evaluators: Iterable[StreamingEval]) -> StreamingEval

A new StreamingEval holding every image the given evaluators have seen, as if one evaluator had received all their update() calls. This is how a run split across processes comes back together: each rank streams its shard of the images, one rank merges them all, and finalize() gives exactly what one stream over every image gives. No matching is redone. evaluators is any iterable, read one at a time, so a generator over from_bytes keeps one shard in memory beside the merged result rather than every rank's at once. An image seen by more than one evaluator keeps the result of the last, the rule update() applies to an image seen again, so an image that DistributedSampler repeats to even out the shards counts once. The evaluators passed in are left unchanged and usable.

Every evaluator must be built the same way: the same categories, iou_type, lvis_style, and params, img_ids included — leave img_ids empty on every rank rather than setting it to each rank's shard. Categories are compared by id, name, and LVIS frequency, in any order. Otherwise merge raises ValueError naming the first evaluator that differs and the field it differs in. An empty iterable raises ValueError, a single StreamingEval instead of an iterable of them raises TypeError, and a finalized evaluator raises RuntimeError.

to_bytes and from_bytes

se.to_bytes() -> bytes
StreamingEval.from_bytes(data: bytes) -> StreamingEval

Save the state and restore it, in this process or another: for example, all_gather each rank's to_bytes(), then from_bytes() and merge() them on one rank. Only what finalize() reads is saved, so an image replaced by a later update() costs nothing, and the size follows the images seen. The restored evaluator finalizes to the same numbers, keeps accepting update(), and merges like any other.

pickle, copy.copy, and copy.deepcopy use the same state, so a metric object that holds a StreamingEval can be checkpointed or copied. The bytes carry a format version, and from_bytes raises ValueError for bytes that are truncated, damaged, or from a version it does not read. Treat them as hotcoco's own format, not an interchange format: a release that changes it says so in the changelog.

finalize

se.finalize() -> COCOeval

Assemble every image seen so far into a COCOeval. Consumes the StreamingEval: update() or finalize() afterwards raises RuntimeError.

The returned evaluator holds the per-cell match results and the category list, not the annotations, which is what keeps finalize() cheap. It supports accumulate(), summarize(), report(), results(), get_results(), slice_by(), and compare(). Its eval_imgs is empty, and the analyses that rebuild full per-image records from the datasets — confusion_matrix(), tide_errors(), calibration(), f_scores(), and image_diagnostics() — see no cells. Build a batch COCOeval for those.


Module-level functions

__version__

hotcoco.__version__ -> str

The version of the compiled extension that is loaded, for example "1.1.0". For an installed wheel it equals importlib.metadata.version("hotcoco"). Use it to gate a workaround on a release or to put in a bug report.


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, evaluation grid, and category axis — mismatched iou_thrs, rec_thrs, max_dets, area ranges, use_cats, or set of cat_ids raise ValueError rather than summarizing one run under the other's catalog. The same categories in another order compare fine: the per-category table pairs by id. 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.