Skip to main content
Lesson 8 of 16 · Document AI: OCR and Document Intelligence

Optimizing OCR Accuracy

Lesson 8 of 16 Updated November 3, 2026 Developer
On this page
  1. Input Quality Determines Output Quality
  2. Resolution and Pre-processing
  3. Adapting Configuration to Document Type
  4. Processing Volumes
  5. Verify Before Exploiting
  6. Operational Summary
  7. Key Takeaways

Update November 2, 2026 — OCR. OCR 4.0 (mistral-ocr-4-0) was retired on September 30, 2026. Use OCR 4.1 (mistral-ocr-4-1), which has been generally available since August 31 at the same price, or the alias mistral-ocr-latest, which has pointed to it since July 16. OCR 4.1 adds confidence scores at the block level. Addendum November 3, 2026: A local PDF is transmitted in base64 as {"type": "document_url", "document_url": "data:application/pdf;base64,…"}; the document_base64 type from older examples exists neither in the documentation nor in the mistralai SDK, and those examples would have failed. Sources: OCR Processor · Document QnA.

Input Quality Determines Output Quality

The Mistral team put it bluntly during their Document AI webinar: it’s “garbage in, garbage out.” No matter how powerful the model, it cannot recover information the scan failed to capture. When an extraction project underperforms, the culprit is almost always upstream of the API—a faded photocopy, a re-scanned fax, or a photo taken at an angle in a poorly lit warehouse. This lesson covers the levers that actually impact accuracy.

Resolution and Pre-processing

The first lever is scan resolution. The targets depend on what the document contains:

  • Printed documents: 200 DPI minimum, 300 DPI recommended
  • Handwriting: 300 DPI minimum
  • Complex tables: 300 DPI recommended
  • Historical documents: 400 DPI or higher

Below these values, fine lines disappear, and no software processing can bring them back. Going higher mostly just increases file size. If you control the scanning chain, setting up the scanner correctly will save you far more trouble than any coding trick.

When the document arrives already digitized, image pre-processing can recover some ground. The function below chains four classic operations—grayscale conversion, contrast enhancement, sharpening, and binarization—before returning the image encoded in base64, ready for the OCR call:

from PIL import Image, ImageEnhance, ImageFilter
import io
import base64

def pre_process_image(image_path: str) -> str:
    """Pre-processes an image to improve OCR extraction."""
    img = Image.open(image_path)

    # 1. Convert to grayscale
    img = img.convert("L")

    # 2. Increase contrast
    enhancer = ImageEnhance.Contrast(img)
    img = enhancer.enhance(1.5)

    # 3. Increase sharpness
    img = img.filter(ImageFilter.SHARPEN)

    # 4. Binarization (thresholding)
    img = img.point(lambda x: 0 if x < 128 else 255, "1")

    # Convert to base64
    buffer = io.BytesIO()
    img.save(buffer, format="PNG")
    return base64.b64encode(buffer.getvalue()).decode("utf-8")

Test this processing on a sample before applying it to an entire batch. Binarization, in particular, is brutal: on an already sharp document, it adds nothing, and on a pencil document, it can erase the text entirely.

Orientation is the other correction you shouldn’t neglect. Scans often arrive rotated by 90 or 180 degrees, and the orientation information is usually stored in the image’s EXIF metadata:

from PIL import Image

def correct_orientation(image_path: str) -> Image.Image:
    """Corrects the orientation of a scanned image via EXIF metadata."""
    img = Image.open(image_path)

    # Use EXIF data to correct orientation
    try:
        exif = img._getexif()
        if exif:
            orientation = exif.get(274)  # EXIF orientation tag
            rotations = {3: 180, 6: 270, 8: 90}
            if orientation in rotations:
                img = img.rotate(rotations[orientation], expand=True)
    except (AttributeError, KeyError):
        pass

    return img

The broad try block is justified here: many images have no EXIF data, and missing orientation data shouldn’t interrupt batch processing.

Adapting Configuration to Document Type

For common printed documents—invoices, contracts, reports—the optimal configuration boils down to explicitly requesting table extraction rather than relying on default settings:

# Optimal configuration for printed documents
ocr_response = client.ocr.process(
    model="mistral-ocr-latest",
    document={
        "type": "document_url",
        "document_url": f"data:application/pdf;base64,{document_base64}"
    },
    table_format="markdown"  # Explicit table extraction
)

Handwriting requires more upstream care. Scan at 300 DPI minimum, increase the contrast between ink and paper, but skip the aggressive binarization from the previous script: it erases fine ballpoint pen strokes, and you’ll lose more information than you gain. The model itself handles handwriting well, including multilingual text, which wasn’t a given with previous OCR generations.

Then there are severely degraded scans—archives, crumpled documents, copies of copies. The strategy changes: start by reducing noise with a median filter before boosting contrast, then increase brightness if the whole thing remains too dark.

from PIL import Image, ImageFilter, ImageEnhance

def improve_scan(path: str) -> Image.Image:
    """Improves a low-quality scan."""
    img = Image.open(path)

    # Noise reduction
    img = img.filter(ImageFilter.MedianFilter(size=3))

    # Increase contrast
    enhancer = ImageEnhance.Contrast(img)
    img = enhancer.enhance(2.0)

    # Increase brightness if too dark
    enhancer = ImageEnhance.Brightness(img)
    img = enhancer.enhance(1.2)

    return img

Order matters: boosting contrast before removing noise just strengthens the stains as much as the letters.

Processing Volumes

Once the number of files exceeds a handful, you need batch processing with per-file error handling. For very large volumes, Mistral offers the Batch Inference service; the following script shows the basic logic for a folder of PDFs, where each failure is logged in the results instead of breaking the loop:

import os
import json
from pathlib import Path
from mistralai.client import Mistral

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

def process_folder(folder: str) -> dict:
    """Processes all PDFs in a folder."""
    results = {}
    path = Path(folder)

    for file in path.glob("*.pdf"):
        print(f"Processing {file.name}...")

        with open(file, "rb") as f:
            import base64
            doc_b64 = base64.b64encode(f.read()).decode("utf-8")

        try:
            response = client.ocr.process(
                model="mistral-ocr-latest",
                document={
                    "type": "document_url",
                    "document_url": f"data:application/pdf;base64,{doc_b64}"
                },
                table_format="markdown"
            )

            results[file.name] = {
                "pages": len(response.pages),
                "content": "\n\n".join(
                    p.markdown for p in response.pages
                )
            }
        except Exception as e:
            results[file.name] = {"error": str(e)}

    return results

This results dictionary gives you, at the end of processing, the exact list of files to retry. On a batch of several hundred documents, that’s the difference between re-running three files and starting over from scratch.

Verify Before Exploiting

An extraction that succeeded technically may have failed in practice: a blank page returned for an unreadable scan, or a flood of garbage characters on a noisy image. A few simple heuristics are enough to trigger an automatic alert.

def verify_quality(markdown: str) -> dict:
    """Verifies the quality of an OCR extraction."""
    indicators = {
        "length": len(markdown),
        "words": len(markdown.split()),
        "empty_lines_ratio": markdown.count("\n\n") / max(len(markdown), 1),
        "special_chars_ratio": sum(
            1 for c in markdown if not c.isalnum() and c not in " \n\t.,;:!?-"
        ) / max(len(markdown), 1),
    }

    # Alerts
    if indicators["words"] < 10:
        indicators["alert"] = "Very little text extracted"
    elif indicators["special_chars_ratio"] > 0.3:
        indicators["alert"] = "High ratio of special characters"

    return indicators

The two thresholds are indicative and must be calibrated to your corpus: ten words for a contract page signals a problem, but not for a delivery slip that only contains fifteen.

Operational Summary

Remember the chain in its logical order. Scan at 300 DPI minimum for any non-trivial document, and increase contrast before sending if the document is faded. Correct the rotation angle of scans. Always prefer the native PDF to a scan when available: its quality is structurally superior. Explicitly specify table_format="markdown" or "html" whenever tables are involved. Process in batches and parallelize calls for large volumes. And systematically validate extraction quality before feeding it into your information system—bad data costs more than missing data.

Key Takeaways

  • Source document quality is the primary factor in accuracy
  • Image pre-processing (contrast, sharpness, binarization) improves results
  • Adapt your approach to the document type (printed, handwritten, degraded)
  • Use batch processing for large volumes
  • Always validate extraction quality before use