Skip to content

Detection Metrics

polars-cv provides a comprehensive suite of detection metrics built on top of Polars lazy expressions and the polars-cv matching primitives. All curve computations use native Polars operations.

Architecture

The metrics system follows a three-layer architecture:

Input Data → Matcher → DetectionTable → Metric Function → MetricResult
  1. Matchers convert raw data into a canonical DetectionTable.
  2. Metric functions compute curves and scalar metrics from the table.
  3. Result objects carry computed curves with convenience methods.

DetectionTable

The DetectionTable is the canonical intermediate representation. It wraps two aligned lazy frames:

  • detections — one row per detection with image_id, class_id, score, is_tp, gt_idx, iou, det_idx.
  • image_metadata — one row per (image, class) with n_gts, weight, gt_label.

Matchers

PreMatchedAdapter

For data that already has per-detection TP/FP assignments:

from polars_cv.metrics import PreMatchedAdapter, precision_recall_curve

adapter = PreMatchedAdapter()
table = adapter.match(
    data,
    pred_col="confidence",
    gt_col="is_tp",
    image_id_col="image_id",
)
result = precision_recall_curve(table)

ContourMatcher

For heatmap + binary mask inputs (used by FROC/LROC workflows):

from polars_cv.metrics import ContourMatcher, froc_curve

matcher = ContourMatcher(iou_threshold=0.5, extraction_threshold=0.1)
table = matcher.match(data, pred_col="heatmap", gt_col="gt_mask")
result = froc_curve(table)

BBoxMatcher

For bounding-box detection inputs:

from polars_cv.metrics import BBoxMatcher, precision_recall_curve

matcher = BBoxMatcher(iou_threshold=0.5)
table = matcher.match(
    data,
    pred_col="pred_bboxes",
    gt_col="gt_bboxes",
    score_col="pred_scores",
)
result = precision_recall_curve(table)

Available Metrics

Precision-Recall

from polars_cv.metrics import (
    precision_recall_curve,
    average_precision,
    mean_average_precision,
    precision_at_threshold,
    recall_at_threshold,
    f1_at_threshold,
)

pr = precision_recall_curve(table)
ap = average_precision(table)
map_val = mean_average_precision(table, iou_thresholds=[0.5, 0.55, 0.6, ..., 0.95])

FROC

from polars_cv.metrics import froc_curve

result = froc_curve(table)
print(result.auc(fp_range=(0, 8)))
print(result.sensitivity_at_fp(1.0))
print(result.summary_table())

LROC

from polars_cv.metrics import lroc_curve

result = lroc_curve(table)
print(result.auc())
print(result.sensitivity_at_fpf(0.5))

Confusion Matrix

from polars_cv.metrics import confusion_at_threshold

counts = confusion_at_threshold(table, threshold=0.5)
# {'tp': 10, 'fp': 3, 'fn': 2}

Bootstrap Confidence Intervals

# Sequential (works with any metric)
ci = result.bootstrap_ci(n_bootstrap=1000, seed=42)

# Vectorized (faster, for PR AUC)
from polars_cv.metrics import bootstrap_pr_auc
ci = bootstrap_pr_auc(table, n_bootstrap=1000, seed=42)

IoU Re-thresholding

The DetectionTable stores raw IoU values from matching, enabling re-thresholding without re-running the matching step:

# Compute mAP across COCO IoU thresholds
map_val = mean_average_precision(
    table,
    iou_thresholds=[0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95],
)

Class-Aware Metrics

Pass a class_col to the matcher, then compute per-class or averaged metrics:

table = adapter.match(data, ..., class_col="category")
ap_cat = average_precision(table, class_id="cat")
map_val = mean_average_precision(table)  # averages across all classes