From c45d29e6a5357630b04be8f2267156f9fc175299 Mon Sep 17 00:00:00 2001 From: Anai-Guo Date: Sun, 26 Jul 2026 12:00:00 +0000 Subject: [PATCH] fix(ocr): make the OCR render scale configurable instead of hardcoded Every local OCR engine hardcoded `self.scale = 3`, so pages were always rendered at 216 DPI before OCR. For sources that are already high resolution this upscaling degrades recognition, and there was no way to turn it down short of editing the model class. Add a `scale` field to the shared `OcrOptions` base (default 3.0, so behaviour is unchanged) and have the EasyOCR, RapidOCR, ocrmac, Nemotron and both Tesseract engines read it. This mirrors the existing `KserveV2OcrOptions.scale` and `PictureDescriptionBaseOptions.scale`. Fixes #3866 Signed-off-by: Anai-Guo Assisted-by: Claude --- docling/datamodel/pipeline_options.py | 14 ++++++++ docling/models/stages/ocr/easyocr_model.py | 3 +- .../models/stages/ocr/nemotron_ocr_model.py | 5 +-- docling/models/stages/ocr/ocr_mac_model.py | 3 +- docling/models/stages/ocr/rapid_ocr_model.py | 3 +- .../stages/ocr/tesseract_ocr_cli_model.py | 3 +- .../models/stages/ocr/tesseract_ocr_model.py | 3 +- tests/test_options.py | 32 +++++++++++++++++++ 8 files changed, 59 insertions(+), 7 deletions(-) diff --git a/docling/datamodel/pipeline_options.py b/docling/datamodel/pipeline_options.py index 874e5d8435..23720ead2a 100644 --- a/docling/datamodel/pipeline_options.py +++ b/docling/datamodel/pipeline_options.py @@ -216,6 +216,20 @@ class OcrOptions(BaseOptions): ), ] + scale: Annotated[ + float, + Field( + description=( + "Image scale multiplier applied before running OCR. The page is " + "rendered at 72 DPI times this factor, so the default 3 yields " + "216 DPI. Lower it when the source image is already high " + "resolution and upscaling degrades recognition." + ), + examples=[1.0, 3.0], + gt=0.0, + ), + ] = 3.0 + # Deprecated: superseded by `OcrMode.FULL_PAGE`. Kept for backwards compatibility # When set to True it forces `mode` to FULL_PAGE force_full_page_ocr: Annotated[ diff --git a/docling/models/stages/ocr/easyocr_model.py b/docling/models/stages/ocr/easyocr_model.py index a2c122780a..4fa6c06915 100644 --- a/docling/models/stages/ocr/easyocr_model.py +++ b/docling/models/stages/ocr/easyocr_model.py @@ -88,7 +88,8 @@ def __init__( ) self.options: EasyOcrOptions - self.scale = 3 # multiplier for 72 dpi == 216 dpi. + # multiplier for 72 dpi; the default 3.0 == 216 dpi. + self.scale = self.options.scale if self.enabled: try: diff --git a/docling/models/stages/ocr/nemotron_ocr_model.py b/docling/models/stages/ocr/nemotron_ocr_model.py index b2908e44cd..cb094a108a 100644 --- a/docling/models/stages/ocr/nemotron_ocr_model.py +++ b/docling/models/stages/ocr/nemotron_ocr_model.py @@ -120,7 +120,8 @@ def __init__( accelerator_options=accelerator_options, ) self.options: NemotronOcrOptions - self.scale = 3 # multiplier for 72 dpi == 216 dpi. + # multiplier for 72 dpi; the default 3.0 == 216 dpi. + self.scale = self.options.scale if self.enabled: self.validate_runtime(accelerator_options=accelerator_options) @@ -246,7 +247,7 @@ def _prediction_to_cell( ocr_rect: BoundingBox, image_width: int, image_height: int, - scale: int, + scale: float, ) -> TextCell: # `nemotron_ocr` returns normalized `left/right` and an inverted # pair `lower/upper`, where `lower` is the top Y and `upper` is the diff --git a/docling/models/stages/ocr/ocr_mac_model.py b/docling/models/stages/ocr/ocr_mac_model.py index b28bbf7079..a954e8330b 100644 --- a/docling/models/stages/ocr/ocr_mac_model.py +++ b/docling/models/stages/ocr/ocr_mac_model.py @@ -38,7 +38,8 @@ def __init__( ) self.options: OcrMacOptions - self.scale = 3 # multiplier for 72 dpi == 216 dpi. + # multiplier for 72 dpi; the default 3.0 == 216 dpi. + self.scale = self.options.scale if self.enabled: if "darwin" != sys.platform: diff --git a/docling/models/stages/ocr/rapid_ocr_model.py b/docling/models/stages/ocr/rapid_ocr_model.py index 4730e83c26..db7d887bf2 100644 --- a/docling/models/stages/ocr/rapid_ocr_model.py +++ b/docling/models/stages/ocr/rapid_ocr_model.py @@ -325,7 +325,8 @@ def __init__( ) self.options: RapidOcrOptions - self.scale = 3 # multiplier for 72 dpi == 216 dpi. + # multiplier for 72 dpi; the default 3.0 == 216 dpi. + self.scale = self.options.scale if self.enabled: try: diff --git a/docling/models/stages/ocr/tesseract_ocr_cli_model.py b/docling/models/stages/ocr/tesseract_ocr_cli_model.py index 05948a6b89..3efa6a2ad7 100644 --- a/docling/models/stages/ocr/tesseract_ocr_cli_model.py +++ b/docling/models/stages/ocr/tesseract_ocr_cli_model.py @@ -52,7 +52,8 @@ def __init__( ) self.options: TesseractCliOcrOptions - self.scale = 3 # multiplier for 72 dpi == 216 dpi. + # multiplier for 72 dpi; the default 3.0 == 216 dpi. + self.scale = self.options.scale self._name: Optional[str] = None self._version: Optional[str] = None diff --git a/docling/models/stages/ocr/tesseract_ocr_model.py b/docling/models/stages/ocr/tesseract_ocr_model.py index 5486f9afda..8862edc1d1 100644 --- a/docling/models/stages/ocr/tesseract_ocr_model.py +++ b/docling/models/stages/ocr/tesseract_ocr_model.py @@ -42,7 +42,8 @@ def __init__( ) self.options: TesseractOcrOptions self._is_auto: bool = "auto" in self.options.lang - self.scale = 3 # multiplier for 72 dpi == 216 dpi. + # multiplier for 72 dpi; the default 3.0 == 216 dpi. + self.scale = self.options.scale self.reader = None self.script_readers: dict[str, tesserocr.PyTessBaseAPI] = {} diff --git a/tests/test_options.py b/tests/test_options.py index 1757723382..35a4f159b2 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -3,6 +3,7 @@ from unittest.mock import Mock, patch import pytest +from pydantic import ValidationError from docling.backend.docling_parse_backend import ( DoclingParseDocumentBackend, @@ -21,9 +22,11 @@ ApiKserveV2ImageClassificationEngineOptions, ) from docling.datamodel.pipeline_options import ( + EasyOcrOptions, NemotronOcrOptions, PdfPipelineOptions, TableFormerMode, + TesseractCliOcrOptions, ) from docling.document_converter import ( ConversionError, @@ -31,7 +34,9 @@ PdfFormatOption, ) from docling.models.factories import get_ocr_factory +from docling.models.stages.ocr.easyocr_model import EasyOcrModel from docling.models.stages.ocr.nemotron_ocr_model import NemotronOcrModel +from docling.models.stages.ocr.tesseract_ocr_cli_model import TesseractOcrCliModel from docling.pipeline.legacy_standard_pdf_pipeline import LegacyStandardPdfPipeline @@ -455,6 +460,33 @@ def test_page_error_carries_page_no(test_doc_path): assert not err.error_message.startswith("Page ") +def test_ocr_scale_is_configurable(): + """The OCR render scale comes from the options instead of a hardcoded 3.""" + assert TesseractCliOcrOptions().scale == 3.0 + assert EasyOcrOptions().scale == 3.0 + + accelerator_options = AcceleratorOptions() + + tesseract_model = TesseractOcrCliModel( + enabled=False, + artifacts_path=None, + options=TesseractCliOcrOptions(scale=1.0), + accelerator_options=accelerator_options, + ) + assert tesseract_model.scale == 1.0 + + easyocr_model = EasyOcrModel( + enabled=False, + artifacts_path=None, + options=EasyOcrOptions(scale=1.5), + accelerator_options=accelerator_options, + ) + assert easyocr_model.scale == 1.5 + + with pytest.raises(ValidationError): + TesseractCliOcrOptions(scale=0) + + def test_nemotron_ocr_backend_registration(): factory = get_ocr_factory(allow_external_plugins=False)