How to Read OCR Confidence Scores in C# and Trust Your Tesseract Results

How to Read OCR Confidence Scores in C# and Trust Your Tesseract Results When you run OCR over a scanned invoice or a photographed receipt, the extracted text is only half the story. The other half is how much you can trust each piece of it. A street address read from a crisp PDF and the same address read from a blurry phone photo both come back as plain strings, but only one of them deserves to flow straight into your database without a second look. That trust signal is what makes OCR safe to a
How to Read OCR Confidence Scores in C# and Trust Your Tesseract Results
When you run OCR over a scanned invoice or a photographed receipt, the extracted text is only half the story. The other half is how much you can trust each piece of it. A street address read from a crisp PDF and the same address read from a blurry phone photo both come back as plain strings, but only one of them deserves to flow straight into your database without a second look.
That trust signal is what makes OCR safe to automate. Instead of accepting a whole page blindly, you can route high-confidence documents straight through and send the doubtful ones to a person. Confidence scores are how you draw that line.
Quick disclosure: we work on IronOCR at Iron Software, so the API here is its result's confidence data. The pattern (threshold, then human-review the doubtful ones) applies to any OCR engine that reports confidence, including raw Tesseract. We'll be honest about the limits too: a confident reading can still be a confident mistake, so the score is a routing heuristic, not a correctness guarantee.
Here's the fastest version. After reading an image, the overall confidence sits on the result as a double between 0 and 100:
// IronOcr namespace exposes the engine and result types
using IronOcr;
// Create the OCR engine
var ocr = new IronTesseract();
// Load the image directly as an OCR input
using var input = new OcrImageInput("invoice-scan.png");
// Recognize the document
OcrResult result = ocr.Read(input);
// Single number summarizing how confident the engine is overall (0-100)
Console.WriteLine($"Overall confidence: {result.Confidence:F1}%");
Enter fullscreen mode Exit fullscreen mode
If you want to follow along, install the package the same way you'd add any other dependency:
# Install the IronOCR NuGet package
Install-Package IronOcr
Enter fullscreen mode Exit fullscreen mode
What an OCR confidence score actually means
A confidence score estimates how certain the engine is about the characters it produced. It reflects how cleanly the recognizer matched glyph shapes against its trained model. Sharp edges, good contrast, and a standard font push the number up, while noise, skew, and odd typefaces push it down.
What it does not tell you is whether the text is semantically right. The engine can be 98% sure it read an 8 when the original ink was a 3 that happened to render with a closed loop. We've watched that exact failure slip through, so we treat the number as a way to prioritize attention, never as a final stamp on critical fields.
The same Confidence property exists at every level of the result, so you can ask about the whole page or a single word:
// IronOcr namespace exposes the engine and result types
using IronOcr;
// Create the OCR engine
var ocr = new IronTesseract();
// Load the statement image as an OCR input
using var input = new OcrImageInput("statement.png");
// Recognize the document
OcrResult result = ocr.Read(input);
// Confidence exists on the whole result...
Console.WriteLine($"Result: {result.Confidence:F1}%");
// ...and on each individual word
Console.WriteLine($"Word: {result.Words[0].Confidence:F1}%");
Enter fullscreen mode Exit fullscreen mode
The lower you go, the more granular the signal. A page might average 92% while one smudged word inside it sits at 41%. That contrast is exactly what makes per-element scores worth reading.
Triage a whole document with the overall score
The overall Confidence is the quickest first-pass filter we reach for. It's an averaged certainty across everything the engine recognized, so it tells you whether a document is broadly clean or broadly trouble. Here we loop a small batch and compare each against a cutoff:
// IronOcr namespace exposes the engine and result types
using IronOcr;
// Create the OCR engine once and reuse it across the batch
var ocr = new IronTesseract();
// The small batch of scans to triage
string[] documents = { "receipt-01.png", "receipt-02.png", "receipt-03.png" };
// Read each file and score it against a cutoff
foreach (string file in documents)
{
// Load the current file as an OCR input
using var input = new OcrImageInput(file);
// Recognize the document
OcrResult result = ocr.Read(input);
// Quick first-pass triage on the document as a whole
string verdict = result.Confidence >= 85 ? "auto-accept" : "needs review";
Console.WriteLine($"{file}: {result.Confidence:F1}% -> {verdict}");
}
Enter fullscreen mode Exit fullscreen mode
One gotcha worth flagging early: the average hides outliers. A page can post a healthy 88% overall while a single critical field, an account number or a total, sits far below that. When a field matters, you check it directly rather than trusting the page average. That's the next step.
List the low-confidence words
To find the tokens worth a human's eye, iterate result.Words and read each word's Confidence. Every entry also carries its recognized Text and positional data, so you can point a reviewer straight at the spot on the page:
// IronOcr namespace exposes the engine and result types
using IronOcr;
// Create the OCR engine
var ocr = new IronTesseract();
// Load the label image as an OCR input
using var input = new OcrImageInput("shipping-label.png");
// Recognize the document
OcrResult result = ocr.Read(input);
// Any word scoring below this needs a human's eye
const double wordThreshold = 75;
// Collect every word the engine was unsure about
var suspectWords = result.Words
.Where(word => word.Confidence < wordThreshold) // keep the weak ones
.OrderBy(word => word.Confidence) // weakest first
.ToList();
// Report how many words were flagged
Console.WriteLine($"Flagged {suspectWords.Count} low-confidence word(s):");
// Print each flagged word with its coordinates
foreach (var word in suspectWords)
{
// Location tells reviewers exactly where to look on the page
Console.WriteLine(
$" '{word.Text}' at ({word.X},{word.Y}) -> {word.Confidence:F1}%");
}
Enter fullscreen mode Exit fullscreen mode
We set a word-level threshold, filter the words below it, and sort the weakest to the top. This is the right granularity for structured-data extraction: a license, a passport, or an invoice has a handful of fields that truly matter, and checking those specific words beats accepting or rejecting the whole document on its average. The same iteration works on result.Lines or result.Paragraphs when your fields span multiple words, and only the collection changes.
Route documents for human review
Now you can put both signals together into a two-tier rule: one cutoff for the overall result and a stricter one for any single word. That catches both globally poor scans and locally damaged fields.
// IronOcr namespace exposes the engine and result types
using IronOcr;
// Create the OCR engine
var ocr = new IronTesseract();
// Load the application image as an OCR input
using var input = new OcrImageInput("loan-application.png");
// Recognize the document
OcrResult result = ocr.Read(input);
const double pageThreshold = 90; // whole-document floor
const double wordThreshold = 80; // stricter floor for any single token
// Does the page as a whole clear the floor?
bool pageIsClean = result.Confidence >= pageThreshold;
// Does every single word clear the stricter floor?
bool everyWordIsClean = result.Words.All(word => word.Confidence >= wordThreshold);
// Auto-process only when both checks pass
if (pageIsClean && everyWordIsClean)
{
Console.WriteLine("Auto-processing: all scores cleared the threshold.");
// push extracted fields into your system of record
}
else
{
// Route to a person and tell them why it was flagged
var weakest = result.Words.OrderBy(w => w.Confidence).First();
Console.WriteLine("Sent to review queue.");
Console.WriteLine($"Reason: weakest word '{weakest.Text}' at {weakest.Confidence:F1}%");
}
Enter fullscreen mode Exit fullscreen mode
Only documents that clear both floors get processed automatically; everything else goes to a person along with the reason it was flagged. We've found the thresholds are a business decision: a marketing mailing list tolerates a far lower bar than a medical record or a wire-transfer instruction.
A word of caution we keep coming back to: a passing score is not verified data on high-stakes fields. For figures that carry legal or financial weight, such as totals, account numbers, and dates of birth, keep a human in the loop regardless of the number. Tuning the cutoffs is empirical work. Start strict, run a representative sample through, and watch how many documents land in review versus how many errors slip past. Loosen or tighten until the false-accept rate matches what your use case can absorb.
When a score is low, fix the image first
When a document scores poorly, the fix is usually the image rather than the threshold. You can apply preprocessing filters to the input before reading, then re-check the confidence to confirm the cleanup helped. That turns the score into a feedback loop of measure, clean, then measure again:
// IronOcr namespace exposes the engine and result types
using IronOcr;
// Create the OCR engine
var ocr = new IronTesseract();
// First pass on the raw, noisy scan
using var rawInput = new OcrImageInput("faded-receipt.png");
// Record the baseline confidence before any cleanup
double before = ocr.Read(rawInput).Confidence;
// Second pass after cleaning the same image up
using var cleanInput = new OcrImageInput("faded-receipt.png");
cleanInput.Deskew(); // straighten tilted text
cleanInput.ToGrayScale(); // drop distracting color
cleanInput.Binarize(); // hard black-and-white separation
cleanInput.DeNoise(); // remove speckle and grain
// Record the confidence after preprocessing
double after = ocr.Read(cleanInput).Confidence;
// Compare the two to see whether the cleanup actually helped
Console.WriteLine($"Before: {before:F1}% After: {after:F1}% Gain: {after - before:F1} pts");
Enter fullscreen mode Exit fullscreen mode
One mistake we see often is stacking every available filter and hoping for the best. Aggressive binarization or denoising on an already-clean image can erode thin strokes and drag the score down. Add filters one at a time and keep only the ones that move confidence up.
If you'd like to put this on your own documents, IronOCR has a free trial you can run against your real scans and measure the scores you get back.
Where this leaves you
The throughline is short: confidence is a routing signal that decides where attention goes, and human review stays the backstop for anything that truly matters. Read the overall score to triage, drill into per-word scores to flag weak tokens, threshold both to route the doubtful documents, and preprocess to rescue borderline scans.
What thresholds have worked for you in production? If you've found a confidence cutoff that balances throughput against false accepts on a specific document type such as receipts, IDs, or forms, we'd like to hear the number and how you landed on it. And if you've been burned by a confident misread that sailed through automation, tell us where it broke.

