Benchmarking OCR It's pipeline
OCR It reads a screen-captured
region with a bundled Tesseract.js build. This page measures that pipeline against
raw Tesseract (isolating what the extension's own image preprocessing adds) and
against RapidOCR, EasyOCR and PaddleOCR — three open-source engines from
different model families — on a small, controlled, synthetic corpus with known
ground truth. Reproducible with
npm run bench; see tools/bench/.
Method
Six short passages, styled like a book page, are rendered under four visual conditions and OCR'd by eight methods. Every method sees the exact same raw image per sample — the only variable is the OCR method itself.
Conditions
Methods compared
- OCR It — Tesseract.js against the same vendored language data the
extension ships, with the extension's real preprocessing (crop → ~2× upscale →
contrast-stretched greyscale) imported directly from
src/ocr/preprocess.js, not reimplemented. - Tesseract.js, raw — identical engine, model and recognize parameters, preprocessing switched off. Comparing this row to OCR It isolates what the preprocessing step is actually worth.
- Tesseract CLI, raw — the native
tesseractbinary, pointed at the same.traineddatafile viaTESSDATA_PREFIX, so this isolates native binary vs. WASM port rather than comparing different models. - RapidOCR, raw — PP-OCRv4 models over ONNX Runtime, a different engine and model family entirely.
- EasyOCR, raw — a CRAFT text detector and a CRNN recogniser on PyTorch. A third model family, and by far the heaviest: torch plus its weights is on the order of a gigabyte, against the ~10 MB the extension ships.
- PaddleOCR, raw — the official PaddlePaddle runtime with current PP-OCRv6 weights. Same lineage as RapidOCR, two model generations newer and not exported to ONNX, which turns out to matter more than anything else on this page.
- Qwen2.5-VL-3B-Instruct — not an OCR engine but a
vision-language model, 4-bit, on MLX, asked to transcribe the image verbatim.
Included because "just use a small VLM" is the suggestion this project gets
most often. Opt-in when reproducing: it needs ~3 GB of weights, so
npm run benchonly includes it withOCRIT_BENCH_VLM=1. - Gemini Nano — Chrome's built-in Prompt API, given the crop and asked for a verbatim transcription, running in an extension page in a real headless Chrome. The only on-device model Chrome exposes to an extension, and so the only alternative here the product could actually reach.
Engines run one at a time — each finishing all 24 samples before the next starts, and released before the next loads its model. Sample-major ordering reads more naturally, but it times every engine while every other engine's model is resident, and that is not a small effect: measured with a multi-gigabyte model held in memory alongside, EasyOCR appeared to take 657 ms a page rather than 335, and Gemini Nano 7.6 s rather than 1.7.
Metrics are Character Error Rate and Word Error Rate (Levenshtein edit distance against the known ground truth, whitespace-collapsed, case/punctuation kept — what those mean, and why both), plus wall-clock recognize time with each engine kept warm (a persistent worker/process, not reloaded per image — the CLI is the one exception, since that's inherent to invoking it as a binary).
Sample size. 24 images total (6 passages × 4 conditions). This is enough to see real, repeatable differences between these specific methods on this specific corpus — it is not enough to make general claims about either engine's accuracy on documents unlike this one (see Limitations).
Results
Overall
By condition
Findings
The preprocessing step is a trade-off, not a universal win
Comparing OCR It to Tesseract.js, raw — the only difference between those two rows is the extension's own crop → upscale → contrast-stretch step:
- On low-contrast text, preprocessing measurably helps — the contrast stretch is doing exactly what it's designed to do.
- On blurred text, preprocessing is slightly worse than doing nothing — upscaling a soft image with smoothing compounds the softness rather than fixing it. One blurred sample went from a clean read to misreading "carriage" as "carnage" once enhance was applied.
- On a plain, undegraded non-retina capture, it makes no measurable difference either way.
In other words: the README's "helps a lot on non-retina displays" claim holds specifically for the washed-out-contrast case it was written for, not as a blanket "always sharpens" claim — and it has a real, if small, cost against a genuinely soft source image.
Native binary vs. WASM: no accuracy difference, as expected
Tesseract CLI, raw and Tesseract.js, raw produce effectively identical text against the same trained data — the WASM port isn't leaving accuracy on the table. The CLI's per-call process-spawn overhead roughly cancels out the WASM runtime's own overhead, so wall-clock time lands in the same range for a single image; the real-world difference shows up over a long run, where Tesseract.js pays its setup cost once (a persistent worker) and the CLI pays it on every single page.
RapidOCR: confident, but the word boundaries collapse under blur
RapidOCR trails every Tesseract variant on both accuracy and speed across the board — though see PaddleOCR below before reading that as a verdict on its model family — and the gap is not subtle once the source is degraded: under the blurred condition its Word Error Rate is far higher than its Character Error Rate, because whole runs of words merge into one unbroken token — "thelampsswung", "wheelsfoundtheoldstoneseasily" — while it keeps reporting a high confidence score on what it did read. The characters it recognizes are largely right; the spaces between them are not, and nothing in its own confidence number flags that. For a tool whose whole job is handing off clean text (to an LLM, to search, to a transcript), that failure mode matters more than the raw character accuracy number suggests.
EasyOCR: the words are right, the order and the punctuation are not
EasyOCR has the highest Character Error Rate here, but the lowest Word Error Rate of the two non-Tesseract engines — the reverse of RapidOCR's shape, and the reason is visible in the output. It reads the words themselves well, then loses the parts around them: the em dash comes back as "L", a full stop becomes ":", and under blur its detector emits boxes in an order that is not reading order, so "The lamps along the quay were lit" is returned as "The lamps the quay" were lit … along with the missing word appended after the end of the line.
It is also the slowest by a wide margin, and the only engine whose cost tracks input size: the retina samples are four times the pixels of the others and take roughly five times as long (~1.6 s versus ~0.3 s per image), where both Tesseract and RapidOCR normalise internally and barely move. Not a warm-up artefact — the harness discards one pass per engine before timing, and all six retina samples land in the same range.
For this extension's job the trade is not close. EasyOCR needs PyTorch and downloaded weights, on the order of a gigabyte against the ~10 MB Tesseract build the extension ships, and it is neither more accurate nor faster on screen-rendered text.
PaddleOCR: the model family was never the problem, the vintage was
RapidOCR runs PP-OCRv4 exported to ONNX. PaddleOCR here runs PP-OCRv6 on the runtime it was built for. Same lineage, and the gap between them is larger than the gap between any two other rows on this page: 4.7% CER against 0.3%, and under blur, 14.0% against 0.4%. Whatever is wrong with the RapidOCR row is about which weights and which export, not about Paddle models being unsuited to body text.
On accuracy PaddleOCR is the only non-Tesseract engine that competes here — a handful of characters behind across 24 samples, mostly an em dash returned as an en dash — and it is the only one that does not degrade on the blurred condition at all.
It costs about twelve times as long per page. ~1.4 s against ~115 ms, and that is before considering that the runtime is a full deep-learning framework which cannot be bundled into a browser extension in any case. For a tool that runs offline inside a page, on a rectangle the user picked, at one hotkey per page, this is the wrong trade — but it is a real answer to "is Tesseract still the right choice", and on harder material than this corpus it could easily be the right one.
Chrome's own model is accurate, and still not the right tool
Gemini Nano, through the Prompt API, reads this corpus at 0.3% CER and 1.6% WER — level with PaddleOCR, and far better than the standalone vision-language model below. Its worst sample is not a misread at all: an em dash returned as an en dash and a curly apostrophe straightened. It handles the blurred condition without flinching.
It costs about 1.7 s a page against 109 ms, so roughly sixteen times the extension's pipeline — around eight and a half minutes for a 300-page book against thirty-five seconds. That is the good case, not the bad one: it is a small model doing a task it is genuinely good at.
The reasons not to ship it are not about accuracy.
- It is Chrome-only. The extension is one source tree building for two browsers; this exists in one of them.
- The requirements exclude people. 22 GB of free disk, and either more than 4 GB of VRAM or 16 GB of RAM with four cores. Below that the API reports unavailable, so the Tesseract path has to exist anyway — and once it does, it is doing the work for everyone on a modest machine.
- Several gigabytes get downloaded — by Chrome rather than by the extension, but the user still pays for it, and Chrome will not start that download without a user gesture.
- No confidence signal. Like every model here that generates text rather than recognising it, it reports nothing per word, so a page it quietly paraphrased looks exactly like one it read correctly.
Chrome's actual OCR engine is not reachable from an extension
Chrome does ship a real OCR engine — "Screen AI", internally gOCR, a ~33 MB library plus TFLite models, downloaded as a component and used for PDF accessibility and Reading Mode. It is a far better fit on paper than a language model: purpose-built, small, fast.
There is no extension API for it. No chrome.screenAI,
no OCR namespace, nothing in the extension API surface at all. The interface
that exists — OpticalCharacterRecognizer::PerformOCR — is
browser-process C++ in chrome/browser/screen_ai, callable from
Chromium code and nowhere else. The component is not even installed by default:
Chrome fetches it on demand and drops it thirty days after last use.
It can be reached by loading the library directly through an FFI and calling its exported C functions, which is what clv-locro does from Python — copying the library and models out of a local Chrome install and driving them through ctypes against a reverse-engineered interface. That is a legitimate route for a desktop tool. It is not one an extension has, and the library is a proprietary Google component rather than something redistributable.
The vision-language model is slower, less accurate, and wrong in a worse way
Qwen2.5-VL-3B reads this corpus at 5.2% CER against 0.1% for Tesseract, taking 2.2 s a page against 117 ms. On a 300-page book that is eleven minutes with the GPU pinned, against about thirty-five seconds.
The speed is the smaller problem. An OCR engine that fails produces visible nonsense; this fails by writing fluent, plausible, wrong text.
truth: "the water held them steady until morning"
model: "the water helded them steady until morning" ← invented a word
truth: "The lamps along the quay were lit one by one"
model: "The lamps along the quay were one by one" ← dropped a word, still grammatical
On the highest-resolution inputs it degenerates outright. Two of the six retina samples collapsed into repetition — "THE THE KEEPER'S'S LOG … six days running running … more more more than than than than use use" — which is why its retina row (15.3% CER) is worse than its 1× rows (1.9%). More pixels made it worse, the opposite of every other engine here.
Greedy decoding invites that, so it is worth asking whether a repetition penalty fixes it. It does not; it trades a loud failure for a quiet one:
| Decoding | What came back | CER |
|---|---|---|
| Greedy | Repetition loop | 52.8% |
| Repetition penalty 1.1 | "the light turneded, anyway." — the rest of the sentence silently dropped | 26.0% |
| Repetition penalty 1.2 | Identical truncation | 26.0% |
And unlike every other engine on this page, a VLM reports no per-word confidence at all — which is why it is absent from the confidence table below. There is no signal to flag the page it quietly shortened.
What this does not show. This corpus is clean, screen-rendered text, which is the case classical OCR was built to win and the case a VLM's language prior helps least. Nobody recommending these models is talking about crisp rendered prose; they are talking about photographs, scans, handwriting and messy layouts, none of which are tested here. Extracting a title from a poster is also a handful of output tokens, where a strong prior is an asset — transcribing a page is several hundred, where the same prior invents words. Read this as "for capturing rendered text off a screen, the 10 MB engine wins", not as a verdict on vision models for OCR.
Three questions the table above cannot answer
Every Tesseract variant reads the corpus above at roughly 0.1% CER, which is useful for comparing engines and useless for tuning one: there is no headroom left to measure a change against. So these three run on a deliberately harsher render of the same passages — 9–13px type, washed-out contrast, defocus — where the differences are visible.
The upscale earns its place; the contrast stretch does not
Upscaling before recognition is worth about 4.5 points of character error. The contrast stretch that follows it is within noise of doing nothing — it is not hurting, but it is not what makes the pipeline work either.
Binarising first makes things worse, and that is the useful result. Otsu and Sauvola are the textbook preprocessing steps for OCR, and both lose to handing Tesseract a greyscale image: it binarises internally, with the layout context to do it better than a threshold applied before it can see anything. Doing that work up front discards information it wanted.
2× upscaling is the sweet spot, and the extension already caps there
enhanceFactor() in src/ocr/preprocess.js normalises a
crop toward ~2 device pixels per CSS pixel and refuses to go past 3×. That cap
turns out to be right for reasons beyond cost: 3× and 4× are less
accurate than 2×, as well as slower. Interpolation invents no new detail, and
past a point the smooth gradients it does invent cost more than the extra size
buys.
The strongest number here is the 1× row. On the smallest type, going from no upscale to 2× is the difference between an unusable read and a clean one — which is the whole argument for the preprocessing step, and also the argument for zooming a reader up before pinning a capture region rather than after.
Two columns already work — on Auto, and only on Auto
Tesseract's layout analysis finds the gutter on its own and returns the left column complete, then the right. Nothing needs building for the common case.
The failure is a settings trap. Pick Single column or Single block — the modes whose names sound like what a book page is — and the two columns are interleaved line by line into text that is worthless, while confidence stays around 95. Auto is not merely the best default here; it is the only whole-crop mode that works at all on this layout.
Pinning one region per column also reads perfectly, at the cost of a second capture per page. That is the fallback for layouts Tesseract cannot segment — a figure straddling the gutter, or a two-page spread.
PSM 1 scores the same as PSM 3 because it silently falls back to it: PSM 1 adds
orientation and script detection, which needs osd.traineddata, and
the extension does not vendor it.
Reading the numbers
Character Error Rate is the number of single-character insertions, deletions and substitutions needed to turn the OCR output back into the correct text, divided by the length of the correct text. 0% is a perfect read; it can exceed 100%, because an engine that invents a paragraph can need more edits than the reference has characters.
Word Error Rate is the identical calculation over whitespace-separated words instead of characters. A word counts as wrong if anything inside it is wrong, so WER is always the harsher number — on ordinary prose, two to six times the CER.
Why the report carries both
Because the ratio between them says how an engine failed, which neither number says alone. The same sentence, corrupted five ways:
A missing space costs the same single character edit as a typo, but destroys two words instead of one. That is the RapidOCR row exactly: 4.7% CER against 28.8% WER, a six-fold ratio, because it merges "the lamps swung" into "thelampsswung". Almost every character is right and the text is still unusable.
Reordering is the mirror image — moving one word wrecks a long run of characters while costing only a couple of word tokens. That is EasyOCR's 8.7% CER against 19.5% WER, a much tighter ratio, and it is what a detector emitting boxes out of reading order looks like in the numbers.
Confidence is the engine's opinion, not a measurement
Every engine but the Tesseract CLI reports a confidence score. It is the engine's own estimate of its own output — for Tesseract, the mean of its per-word scores — computed without any knowledge of the correct answer. Nothing validates it.
RapidOCR reports the second-highest confidence on this page while producing the worst word accuracy on it. EasyOCR is the least confident of the alternatives and the more word-accurate of the two. Across engines the ranking is close to meaningless, because each score means something different internally.
Within a single engine it is worth something as a relative signal: pages that normally land at 93–95 and then come back at 60 are worth a look. What it cannot do is certify a page is correct. The two-column result above is the clean demonstration — interleaved, worthless text at 52.9% CER, still reporting around 95. That is why the extension shows a thumbnail of exactly what was cropped next to every page, and why the confidence figure beside it is the lesser of the two signals.
Limitations
- Synthetic corpus, one language, one font, book-page-style prose only — real scans, photographs, and other layouts (tables, multi-column, handwriting) aren't represented here.
- Three open-source alternative engines, all tuned as much for scene text and layout detection as for dense body text — which is the workload this corpus is made of, and not the one they were built to win. On photographed, skewed or handwritten material they would likely close the gap or win outright, and PaddleOCR's showing here is a reminder of how much a model generation moves the numbers: any of these could look different a version from now.
- 24 samples. Treat the specific percentages as indicative, not precise population statistics.
- The vision-language row is one model, at one quantisation (4-bit), with one prompt, decoded greedily. A larger or less quantised model, or a prompt tuned for the task, would score differently — though not by enough to close a 19-fold speed gap.
Reproducing this
brew install tesseract
python3 -m venv tools/bench/.venv
tools/bench/.venv/bin/pip install -r tools/bench/requirements.txt
npm install
npm run bench
Regenerates docs/bench/results.json, which this page reads directly.
See tools/bench/
for the full harness.