Tools,

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.

For PDFs from Python, see pytesseract PDF to text, and for tables, PDF table to Markdown.

What I did not test

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.