Benchmarks
Headline
Section titled “Headline”Measured against upstream EasyOCR over a mixed corpus (documents, tables, rotated scans, scene text, receipts) spanning five recognizer groups, on CPU, both engines at their native multi-threaded default.
The table below is generated from the committed benchmarks/published/latest.json by
task python:publish, and CI fails if the two drift apart — it is measured output, not prose.
| Engine | Throughput (img/s) | Peak RSS (MB) | Mean CER | Mean token-F1 |
|---|---|---|---|---|
| EasyOCR (warm/batch) | 0.12 | 4,963.9 | 0.569 | 0.337 |
| sceptre (warm/batch) | 0.23 (~1.9×) | 4,653.7 (~1.4× lower, per-image median) | 0.569 | 0.358 |
| sceptre (cold CLI run) | 0.21 (~1.8×) | 4,653.7 | 0.569 | 0.358 |
Measured with sceptre 0.6.0 (ort/cpu, ONNX Runtime 1.28.0) against EasyOCR 1.7.2 (torch 2.13.0+cu130) on
runner-medium (Linux/x86_64), over 40 of 43 corpus entries. Regenerate with task python:benchmark then task python:publish.
“Warm/batch” is a fresh subprocess per language group that loads its model/Reader once and
processes every image in that group — the fairest like-for-like comparison. “Cold CLI run” is
sceptre’s per-invocation cost: a fresh process per image, paying model load every time, which still
beats EasyOCR’s warm, already-loaded reader.
Quality (CER, WER, token-F1) is averaged over the labeled subset of the corpus only, scored against ground truth. The accuracy columns are the parity check: sceptre reads the corpus the same way EasyOCR does. Peak RSS is dominated by the CRAFT detector both engines share, so its ratio narrows toward parity as page size grows — throughput is the more stable difference between the two.
Methodology
Section titled “Methodology”Both engines are measured identically: each runs as a fresh subprocess per language group under
/usr/bin/time, so peak RSS is a like-for-like whole-process figure. EasyOCR’s figure legitimately
includes the Python + torch runtime — that is its real cost, not an artifact of measurement.
Timings use --repeats 2 (median wall time, max peak RSS observed).
Three images in the corpus (alpha, test, Hadley_Crater) are capability gaps — formats sceptre
cannot decode that EasyOCR can — reported separately and excluded from the speed/quality aggregates
so a fast-failing decode never inflates sceptre’s numbers. See
Image formats.
Full methodology, including the RSS-measurement rationale, threading choice, and the quality signal, is recorded in ADR 0021.
Reproducing the numbers
Section titled “Reproducing the numbers”task python:benchmarkre-measures and writes benchmark-results/comparison.{json,md}, which is gitignored and
machine-local. Useful flags:
| Flag | Purpose |
|---|---|
--group labeled --limit 3 --repeats 1 |
A fast inner-loop run over a small subset. |
--baseline <prior.json> |
See per-image deltas against a previous run. |
--assert |
Enforce the regression gate (see below). |
Measuring and publishing are separate steps. To turn a run into the numbers shown above:
task python:publishdistils the report into the committed benchmarks/published/latest.json — headline figures plus
the provenance block naming the sceptre, ONNX Runtime, EasyOCR and torch versions behind them — and
regenerates the marked table regions in the README and on this page. A report with no provenance
block is refused rather than published. task python:publish:check re-renders without writing and
fails if the committed tables have drifted from the artifact; CI runs it on every pull request, and
it needs neither torch nor a built binary. See
ADR 0030.
cargo bench covers internal hot paths (criterion microbenchmarks); the head-to-head harness above
measures end-to-end OCR against EasyOCR. Parity fixtures backing the quality signal live under
crates/sceptre/tests/data/golden/.