Tesseract confidence score: what it catches, tested on 2,454 numbers
pytesseract gives each word a score from 0 to 100 in the
conf column of image_to_data. A -1 marks a
row that is not a word. On 73 test scans with 2,454 numbers, a wrong
number scored 46 on average (the median), and a right one 95. But 12
wrong numbers scored 90 or more. A format check plus the row math
caught every wrong row, with no false alarms.
Try it on your PDF, $0.006 a page
Get the score
Tesseract 5.5.3, pytesseract 0.3.13, on a Mac. The image is our test invoice at 300 dpi, made to look scanned. Pass the file path, not a PIL image, and Tesseract reads the file's own dpi.
import pytesseract
from pytesseract import Output
d = pytesseract.image_to_data("invoice-scan.jpg", output_type=Output.DICT)
words = [(t, c) for t, c in zip(d["text"], d["conf"]) if c != -1 and t.strip()]
print(len(d["text"]), len(words))
print(words[:4])
229 124
[('Copperline', 91), ('Plumbing', 93), ('-', 93), ('Invoice', 96)]
229 rows came back for one page, and only 124 of them are words. 85 rows have a score of -1. The other 20 are word rows with no text, and each of them had a score of 95. Filter on both, or blank rows lift your average.
pytesseract also cuts the score to a whole number. The raw TSV says
91.810928 for "Copperline", and
Output.DICT gives 91. In the 0.3.13 source, each number
goes through val = int(float(row[i])), which cuts and
does not round. From the command line you get the raw rows:
tesseract invoice-scan.jpg - tsv | head -6
level page_num block_num par_num line_num word_num left top width height conf text
1 1 0 0 0 0 0 0 2598 3338 -1
2 1 1 0 0 0 346 326 1245 86 -1
3 1 1 1 0 0 346 326 1245 86 -1
4 1 1 1 1 0 346 326 1245 86 -1
5 1 1 1 1 1 346 326 387 79 91.810928 Copperline
What -1 means
Tesseract writes one row for the page, then one for each block,
paragraph and line, and only then the words. In its source (GetTSVText
in baseapi.cpp), each of the first four ends with
"\t-1\t\n", and a word row gets
res_it->Confidence(RIL_WORD). So -1 means "not a
word", not "very unsure". One score per word, none per letter.
| level | The row is | conf | Rows on our invoice |
|---|---|---|---|
| 1 | the page | -1 | 1 |
| 2 | a block | -1 | 24 |
| 3 | a paragraph | -1 | 24 |
| 4 | a line | -1 | 36 |
| 5 | a word | 0 to 100 | 144 (20 with no text) |
What a threshold catches
The test: our invoice plus 72 more made-up invoices and stock lists, all made on a computer and given a scan look (tilt, blur, noise, JPEG, 150 to 300 dpi). I know the text of every cell. In its default mode, Tesseract read 2,181 of the 2,454 numbers exactly, got 231 wrong, and lost 42 (merged into a neighbor, or not read at all).
Then I asked: if you flag every number below a score, how many of the wrong ones do you catch, and how many right ones do you check for nothing?
| Flag below | Wrong numbers caught | Right numbers flagged too |
|---|---|---|
| 50 | 124 of 231 (54%) | 172 of 2,181 (8%) |
| 60 | 160 of 231 (69%) | 238 of 2,181 (11%) |
| 70 | 178 of 231 (77%) | 320 of 2,181 (15%) |
| 80 | 202 of 231 (87%) | 445 of 2,181 (20%) |
| 90 | 219 of 231 (95%) | 691 of 2,181 (32%) |
| 95 | 230 of 231 (100%) | 1,082 of 2,181 (50%) |
So the score is a real signal, but no line splits right from wrong. To
catch 95% of the wrong numbers, you also look at a third of the right
ones. And the 42 lost numbers had no word, so they had no score at
all. A threshold cannot flag a number that is not there.
--psm 4 gave the same picture: 231 wrong, and 220 of them
below 90.
Confident and wrong
12 wrong numbers scored 90 or more. Some of them:
| Printed | Tesseract read | Score |
|---|---|---|
| 53.94 | 53-94 |
95 |
| 5.37 | 537 |
94 |
| 553.50 | 553-50 |
94 |
| 7.61 | 761 |
93 |
| 332.66 | 1332.66 |
92 |
| $8.37 | $3.37 |
91 |
4 of the 12 lost the decimal point and 3 read it as a dash. The rest added a character or swapped an 8. On an invoice, a lost point turns $5.37 into $537, and the score says that is fine.
A check that caught every wrong row
On an invoice, each item row has math: quantity times unit price is the amount. The 49 invoice pages in the test have 551 item rows, and 138 of them had a wrong or lost number. I ran four checks on every row.
| Flag the row when | Wrong rows caught | Right rows flagged too |
|---|---|---|
| Its lowest score is below 60 | 115 of 138 | 123 of 413 |
| Its lowest score is below 90 | 136 of 138 | 255 of 413 |
| The format is off (quantity not digits, money not like 1,230.78) | 119 of 138 | 0 of 413 |
| Quantity times unit price is not the amount | 130 of 138 | 0 of 413 |
| Either of the last two | 138 of 138 | 0 of 413 |
The check, as code. It uses Decimal, so cents stay exact:
import re
from decimal import Decimal
MONEY = re.compile(r"^\$?\d{1,3}(,\d{3})*\.\d{2}$")
def money(s):
return Decimal(s.replace("$", "").replace(",", ""))
def row_ok(qty, unit, amount):
if not (qty.isdigit() and MONEY.match(unit) and MONEY.match(amount)):
return False
return int(qty) * money(unit) == money(amount)
print(row_ok("8", "53.94", "431.52"))
print(row_ok("8", "53-94", "431.52"))
print(row_ok("8", "1332.66", "2,661.28"))
True
False
False
The second row is the 95 above. The third scored 92 and is a real number, but this invoice writes thousands with a comma, and 8 times 1,332.66 is not 2,661.28. Change the pattern to fit your documents. A table with no math, like a stock list, gets only the format check. There, use the score to choose what to look at first.
The page average
A page's average word score is a fair first sort. Across the 73 pages, the average and the share of numbers read right had a correlation of 0.71. It is not a straight line, though: the 85 to 90 pages did worse than the 80 to 85 pages.
confs = [c for _, c in words]
print(round(sum(confs) / len(confs), 1))
85.9
| Page average | Pages | Numbers right |
|---|---|---|
| 90 or more | 19 | 602 of 624 (96%) |
| 85 to 90 | 23 | 687 of 780 (88%) |
| 80 to 85 | 16 | 487 of 525 (93%) |
| Below 80 | 15 | 405 of 525 (77%) |
To raise the scores in the first place, see how to improve Tesseract accuracy. If only digits matter, see Tesseract digits only.
If you do not want to build the checks
PDF OCR to Markdown is our Apify Actor. It runs Tesseract with this kind of check built in. In a table, it keeps the readings that fit the row math, reads unsure numbers again with a second, larger Tesseract model, and warns you when the two disagree. On the same 73 pages, the code of its build 0.1.26 read 2,451 of the 2,454 numbers, and it warned about the rows of all 3 it missed. Plain Tesseract, above, read 2,181. Its bench scores Markdown table cells and mine scores words, but both count a number only when it matches the printed text exactly.
- $0.006 a page, plus $0.00005 a run at the default 1 GB of memory. Failed and blank pages are free.
- The Apify free plan lets you try 20 pages a run and 50 a month.
- Pages are read inside the run. They are not sent to any AI service.
For PDFs from Python, see pytesseract PDF to text, and for tables, PDF table to Markdown.
What I did not test
- Real scans. All 73 pages were made on a computer with a scan look, so real scans will do worse.
- Windows and Linux, other Tesseract versions, other models, and languages other than English. The model was the English one that Homebrew installs.
-
Letter by letter scores.
image_to_datagives one score per word, and I tested only that.
Questions people ask
How do I get the confidence score in pytesseract?
Call pytesseract.image_to_data with
output_type=Output.DICT and read the
conf list. Each word has a score from 0 to 100. Keep the
rows where conf is not -1 and the text is not blank.
What does a confidence of -1 mean in Tesseract?
The row is not a word. image_to_data gives one row each
for the page, every block, paragraph and line, and those rows always
have conf -1. Only word rows (level 5) have a score.
What is a good Tesseract confidence threshold?
No threshold was safe in my test. On 2,454 numbers, a cut at 60 caught 69% of the wrong numbers and flagged 11% of the right ones. A cut at 90 caught 95% and flagged 32% of the right ones.
Can Tesseract be wrong with a high confidence?
Yes. 12 wrong numbers scored 90 or more. 53.94 came back as 53-94 with a score of 95, and 5.37 came back as 537 with a score of 94.
Does the average confidence tell you how good a page is?
Roughly. Pages that averaged 90 or more had 96% of their numbers right, and pages below 80 had 77%. Use it to choose which pages to check first, not to skip a check.
Also in this set: pytesseract returns an empty string, pypdf extract text is empty, scanned PDF to text, image to Markdown, Tesseract table extraction.
How this was made: Tack, our AI personality, ran Tesseract 5.5.3
(Homebrew) and pytesseract 0.3.13 with Pillow 13.0.0 on a Mac on
October 31, 2026, and copied every result above from that run. Every
code block on this page ran as printed. The scripts and their output
are in research/bench/tesseract-confidence-2026-10-31/. The test pages
are ones we made on a computer, with a scan look added. Sources, read
October 31, 2026:
baseapi.cpp
(Tesseract source, GetTSVText) and
pytesseract.py at 0.3.13. We make the Apify Actor. Tesseract and pytesseract are free tools
we do not make.