Skip to main content

Detection: evaluating a model

For computer vision and ML engineers who ship models to devices, where quantization, input size and model size trade off against accuracy. This page follows the detector example.

Screenshots coming

The example project is examples/detection-eval. Its screenshots show as placeholders until they're captured.

The problem​

One mAP number hides where a model got worse. Quantizing a detector to int8 makes it several times smaller, and the average moves less than what you lost on one kind of scene, or on the objects far away. You need metrics per class and per object size, on slices of your data, next to the reference model's, and the images where it went wrong.

The example​

An off-the-shelf detector (torchvision's SSDlite, BSD-licensed) runs on CPU with ONNX Runtime, in fp32 and in int8, at several input sizes. It's evaluated on four slices of images: indoor, outdoor, warehouse, and small objects (people and cars pasted far away on empty scenes). Each run measures COCO-style AP, AP per object size and per class, recall on small objects, model size and latency.

The photos are public domain, and no detection dataset is: their labels come from a much larger model (Faster R-CNN), not from people. The detector is scored against what a better model sees.

The int8 model is scored with its reference arithmetic: quantization in float, as its graph says, rather than with ONNX Runtime's int8 kernels, which round differently from one CPU to another. Its metrics don't move from one CI runner to the next, which is what a regression test needs.

Its story:

  1. The first int8 model is 3.5 times smaller, which is what the robot's accelerator needs. It loses 35% of its mAP on the indoor slice, and some of the small objects.
  2. Per-channel weights and percentile calibration bring the indoor slice back.
  3. qa optimize searches the quantization and post-processing settings; the Analysis view shows which ones matter. With them, the int8 model finds small objects as well as the last fp32 release.
  4. The tuned model ships as a milestone, the next reference.

What you see​

Metrics Diff per slice: int8 against fp32, the model 72% smaller everywhere, mAP and AP on small objects red on the indoor slice.

The Metrics Diff of the int8 runs against the fp32 release, slice by slice: indoor mAP -35% and AP small -34% in orange, AP small -7% on small-objects and -15% outdoor, warehouse better, and the model size -72% in green on every slice.
The Metrics Diff shows each test's change: the int8 model is smaller everywhere, and much worse indoors.

Precision-recall curves per class, int8 over the fp32 reference drawn dashed.

Precision-recall curves of the indoor slice, one per class: the int8 model's solid, the fp32 release's dashed in the same color. The legend gives each curve's AP50: person drops from 0.19 to 0.06.
Plots of the reference are drawn over the new ones, dashed: here the int8 model's precision-recall curves over fp32's.

Detections on the images: the int8 model misses people that fp32 finds (in red).

A hall with people and cars pasted in, new and reference side by side: the int8 model (left) finds one of the people, the fp32 release (right) three. What each missed is outlined in red: two more people on the left; the faded person and the three cars on both.
Each run draws its detections, ground truth in red where it missed: the int8 model misses two people fp32 finds.

An HTML report per run, with AP per class and object size, and the images where the most objects were missed.

The int8 run's HTML report on small-objects: mAP 0.338, AP50 0.507, AP small 0.341, latency and model size; AP per class for person and car, AP and recall by object size, latency, then the images with the most missed objects, boxes drawn.
Your code can write an HTML report: it shows in the run's card, here with the images where the model missed the most objects.

Automated tuning: qa optimize tries quantization settings; the Analysis view shows their convergence, each parameter's effect, and the trade-offs.

The Analysis of the tune-int8 experiment: the best parameters found at iteration 10 of 20 (percentile 99.999, nothing kept in fp32, score 0.2, NMS 0.4) with their mAP, AP small and model size; parallel coordinates of the 20 iterations; the convergence of the objective.
The Analysis of an automated tuning: the best parameters, every iteration in parallel coordinates, and how the search converged.

A milestone for the release: the tuned model's commit is saved with a label and notes, shared with the team, and becomes the reference the next changes are compared with.

The milestone dialog of the kd-1.3 release commit, opened from the crown next to its hash: Shared on, the label kd-1.3, the notes "int8, per-channel, recalibrated", and the Delete and Save buttons.
A commit's star makes it a milestone: give it a label and notes, and share it with everyone on the project.

The configuration​

:image in a path gives a slider over the slice's images, synced across runs. The plots set their traces' colors, so with side_by_side: false the web app draws the reference dashed and faded on the same axes.

qaboard.yaml
inputs:
# relative to this folder: each slice of the test set is a folder with its images and labels.json
database:
linux: inputs
globs: labels.json
use_parent_folder: true
batches: qa/batches.yaml

outputs:
metrics: qa/metrics.yaml
# Run cards wide enough for the photos (new and reference about 415 px wide each) and the plots (840 px by default)
style:
width: 840px
visualizations:
- name: Detections
path: "images/:image.jpg" # 1..N, a synced slider over the slice's images
- name: Precision-recall
path: pr.plotly.json # a curve per class at IoU 0.5: the reference's are dashed
side_by_side: false
- name: Recall by object size
path: size_recall.plotly.json
side_by_side: false
- name: Report
path: report.html # per-class AP, AP by size, latency, the images with the most missed objects
- name: Confusions
path: confusion.plotly.json # labeled class x detected class, and the background
default_hidden: true
- name: Detections (JSON)
path: detections.json
default_hidden: true

# Experimental: may change without notice
gate:
reference: main
batches: [ci]
rules:
- {metric: map, min: 0.15} # median over the batch's runs
- {metric: ap_small, max_regression: 10%}
- {metric: map, per_test_max_regression: 25%} # no slice and variant much worse than in the reference
max_failed_runs: 0

The variants are configurations (fp32, int8, size-320...), so each run shows them as tags. To compare int8 with fp32 on the same commit, select the commit as the reference too (Also Select as Reference), then filter each bar's runs by configuration. See Picking a reference.

Run this example​

You need Python 3.10 or later and uv. The first run downloads the photos (about 20 MB) and the model's weights, and exports the model to ONNX with PyTorch for CPU, which uv installs in its cache (about 200 MB).

cd examples/detection-eval
pip install qaboard -r requirements.txt
qa --config int8 --config size-320 run --input slices/small-objects
qa batch ci # 4 slices, fp32 and int8: about 30 s

See the example's README for the other batches and the tuning.

Read more​