From afacb271fcba6389768c43c699169aebd651a2d6 Mon Sep 17 00:00:00 2001 From: "BBC, Esquire" Date: Wed, 21 Oct 2026 11:43:24 -0400 Subject: [PATCH] feat(ocr): improve Tesseract OCR reliability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR makes both Tesseract engines (`tesseract` and `tesserocr`) more reliable on colour pages and on rotated text, and it makes the `tesseract` engine report its failures. It has four changes. I measured each one against `main` on public PDFs and on Docling's own test PDFs, on Windows 10 with Python 3.12, the Tesseract 5.5.0 CLI and tesserocr 2.10.0 (Tesseract 5.5.2). **Before and after.** With forced (full-page) OCR, on a page with yellow highlighting, Tesseract found only 45% of the words (33% of the highlighted ones) in the colour image, against 97% (99% of the highlighted ones) in a grayscale copy. The table shows the share of the ground-truth words that appear in Docling's Markdown export, for all words / for the words under a highlighter mark: | Sample | OCR mode | `main` | This PR | |---|---|---|---| | Newtown draft charter, page 23 as a scanned image | full page | 45.4% / 33.0% | 97.4% / 99.3% | | Newtown draft charter, all 47 pages | full page | 83.7% / 93.8% | 96.8% / 99.5% | | Newtown colour-code legend | full page | 97.3% / 0% | 100% / 100% | | Homer council slide, text on coloured boxes | full page | 94.1% | 95.3% | | Scanned court exhibit, page 3 | full page | 96.2% / 100% | 96.2% / 100% | | Newtown draft charter, page 23 as a scanned image | default | 98.4% / 99.6% | 99.2% / 100% | | Scanned court exhibit, page 3 | default | 96.2% / 100% | 96.2% / 100% | No sample got worse. Forced OCR (`OcrMode.FULL_PAGE`, `--force-ocr`) gains the most. The default mode reads smaller layout regions, and there the colour effect was small in these tests. **1. Grayscale input.** Tesseract thresholds a colour image channel by channel. A yellow highlighter mark is dark in the blue channel, so it can turn into a black block that hides its text. Both engines now convert each OCR region to grayscale before orientation detection and OCR. Direct Tesseract runs on Docling's 216-DPI renders, with the OCR command that the `tesseract` engine runs on `main` (all words / highlighted words): | Sample | Colour | Grayscale | |---|---|---| | Newtown draft charter, 36 highlighted pages (20,314 words, 5,109 highlighted) | 82.6% / 71.1% | 97.6% / 99.5% | | Same document, page 23 | 46.2% / 33.0% | 99.8% / 100% | | Newtown colour-code legend | 97.3% / 0% | 100% / 100% | | Scanned court exhibit, page 3 (19 highlighted words) | 89.9% / 57.9% | 95.9% / 100% | | Homer council slide | 100% | 100% | Grayscale was better on 33 of the 36 charter pages, equal on 2, and worse on 1 (page 46: 95.6% → 95.0%). On a synthetic page with three highlighted lines, the colour image gave 21% of the highlighted words and the grayscale image 100%, with each of five highlighter colours (three yellows, green and pink). With six highlighted lines it was 12% against 100%. A single highlighted line was read in full either way. On 75 ordinary pages from Docling's test PDFs (40,358 words, both arms with the DPI from change 2), grayscale still gained: 91.14% → 91.77%, with 17 pages better and 8 worse. Grayscale has a cost. In colour mode, Tesseract treats a pixel as text when any one channel is dark, so it reads light text on a strongly coloured box and light, saturated text on white; grayscale lowers the contrast of both. On a synthetic page with black body text, lines in orange, yellow, pink or light blue were read in full in colour but only 12–17% in grayscale, while red, blue, green, cyan and grey lines were read in full either way. On the real pages, the largest loss was the white labels on coloured boxes in Figure 4 of arXiv 2203.01017 (page 5): 31 words in direct Tesseract output. Those labels are inside a picture, so they do not reach the Markdown export on `main` or on this branch. I also tried Tesseract's Sauvola thresholding (`-c thresholding_method=2`) instead of plain grayscale, both with the DPI. It reads the orange, pink and light-blue lines (not the yellow ones), and it matched grayscale on the real pages: 97.6% on the highlighted charter pages, 91.7% against 91.8% on the 75 ordinary pages, and 99.1% against 97.7% on the slide, exhibit and legend. But it made more of the ordinary pages worse than `main` (16 against 8), so this PR keeps plain grayscale. **2. Resolution.** Docling renders OCR regions at 72 DPI × `scale` (216 DPI by default) but did not tell Tesseract, so Tesseract estimated the resolution from the text size; on 216-DPI renders it estimated, for example, 229 and 272 DPI. Both engines now pass the real value: `--dpi` for the CLI and `SetSourceResolution` for tesserocr. The effect is a small net gain, and it is mixed page by page (both arms with grayscale input): | Test set | Without the DPI | With the DPI | Better / worse | |---|---|---|---| | 202 layout regions from 16 documents, 41,590 words | 90.95% | 91.61% | 17 / 10 | | 75 full pages from 14 documents, 40,358 words | 91.62% | 91.77% | 10 / 8 | The largest gain was a table region, from 62.5% to 92.1% of its 331 words. The largest losses were 14 words in one region and 9 words on one page. On the Homer slide above, the DPI cost 4 words in Docling (97.6% with grayscale alone, 95.3% with both). I recommend it all the same: the net effect is positive in both sets, the largest single effect is a gain, and Tesseract no longer has to guess a resolution that Docling knows. **3. Orientation confidence.** Docling runs Tesseract's orientation and script detection (OSD) on every OCR region, and it rotated the region on any detection. OSD reports its orientation confidence as the margin between its best and second-best orientation, so a value near 0 is a close call. The new option `orientation_confidence_threshold` (default 1.0) on `TesseractCliOcrOptions` and `TesseractOcrOptions` ignores detections below it; `0` restores the old behaviour. I ran OSD on 277 layout regions from 16 documents and on 90 full pages from 17 documents, as rendered (all upright) and rotated by 90°, 180° and 270°: | | Layout regions | Full pages | |---|---|---| | Upright text turned the wrong way, `main` | 2 of 214 (confidence 0.06 and 1.17) | not measured | | Upright text turned the wrong way, this PR | 1 of 213 (confidence 0.0, now ignored) | 0 of 90 | | Rotated copies corrected, threshold 0 (old behaviour) | 632 of 639 | 268 of 270 | | Rotated copies corrected, threshold 1.0 (new default) | 619 of 639 | 265 of 270 | | Rotated copies corrected, threshold 14 (OCRmyPDF's page default) | 392 of 639 | 220 of 270 | The counts cover the regions and copies on which OSD returned a result. A threshold of 14 would drop 38% of the correct region rotations and 18% of the correct page rotations. With 1.0, the rotated copies that are no longer corrected hold 84 of the 114,209 words that Tesseract reads in all correctly rotated copies (0.07%), while the region that OSD turned the wrong way would have lost all 11 of its words. A region that is really sideways, the arXiv identifier in the margin of a paper (confidence 2.97), is still rotated and read in full. **4. Failures are reported.** When Tesseract failed on a region in the `tesseract` engine, Docling only logged an error, and the document still ended as `SUCCESS` without that text. Now the engine also adds an `ErrorItem` for the page (`DoclingComponentType.MODEL`, `FailureCategory.INFERENCE_FAILURE`), the same way the KServe OCR engine reports a failed region, so the document ends as `PARTIAL_SUCCESS`. The engine now keeps Tesseract's stderr, so the error carries Tesseract's own message. Tesseract exits with 0 on blank, tiny and all-black regions, so the error marks real failures only. In the `tesserocr` engine, a failure raises an exception, which the pipeline already records for the page. None of the reference conversions reported an error. An empty `lang` list still skips, without an error, a region whose script OSD cannot detect. OSD gives up on regions with too few characters, mostly pictures, and reporting those skips would turn 3 of 5 ordinary test documents (converted with `lang=[]`) into `PARTIAL_SUCCESS`. So this PR leaves that case unchanged. **Related pull requests:** - #4709 adds orientation detection for RapidOCR (`detect_orientation`, off by default). It addresses the same rotated-text problem for another engine and shares no code with this PR. Both PRs change `pipeline_options.py` and `docs/concepts/OCR.md`, but in different sections, and they merge cleanly. - #4654 and #4643 detect a broken PDF text layer and send it to OCR, #4654 per page and #4643 per region. This PR does not do that; the PRs complement each other. With a Tesseract engine, the pages and regions that they send to OCR get the changes in this PR. A page that #4654 replaces is OCR'd in full, which is where the grayscale input gains the most. **Review evidence:** - Reference data: unchanged. `tests/test_e2e_ocr_conversion.py` passes against the existing ground truth for every Tesseract configuration of both engines (default, layout-regions and full-page modes, and full-page mode with `lang=[]`), and for EasyOCR and RapidOCR. - New tests: 6 unit tests in `tests/test_tesseract_ocr_cli_model.py` and `tests/test_tesseract_ocr_model.py` (grayscale input, the DPI, the threshold and the error record) fail on `main` and pass here. `tests/test_e2e_tesseract_highlighted_text.py` OCRs a synthetic page with three highlighted lines (`tests/data/ocr/sources/highlighted_text.png`, 63 KB, regenerated by running the test module); on `main`, both engines miss 26 of its 42 highlighted words. - `tests/test_e2e_rotated_ocr.py`: same result as `main` (90° and 270° pass; 180° is still the expected failure of #3839). - One run of the tests above plus `test_tesseract_ocr_cli_lang`, `test_options`, `test_service_datamodels`, `test_ocr_options_force_full_page`, `test_ocr_engine_language_mapping`, `test_ocr_language` and `test_backend_webp`: 306 passed, 6 skipped, 2 xfailed (the 180° cases). - Checks: the prek hooks (ruff, ruff format, ty, tach), tach module coverage and max-lines pass. - Not run: the full test suite, Linux, macOS and Tesseract 4. **Samples.** The ground truth is the PDF text layer, or a manual transcription for image-only pages. The highlighted words are the words under a yellow highlighter mark. The public sources: - Newtown, CT, 2015 draft charter (47 pages): https://cmsarchive.civicplus.com/Newtown%20CT/NewtownCT_BBoard/News%20Archive/S052C770B.2/NEWTOWN%20CHARTER%20Revision%20Commission%202015%20Draft%20%20Charter%203.pdf - Newtown, CT, colour-code legend: https://cmsarchive.civicplus.com/Newtown%20CT/NewtownCT_BBoard/News%20Archive/S052C770B.4/Color%20Codes%207-10-15%205.pdf - City of Homer, AK, council supplemental packet (the slide is page 5): https://www.cityofhomer-ak.gov/sites/default/files/fileattachments/city_council/meeting/75366/supplemental_packet.pdf - "Page 23 as a scanned image" is page 23 of the draft charter, rasterized at 300 DPI into an image-only PDF. - Scanned court exhibit, U.S. District Court for the Northern District of California, No. 4:24-cv-04722, ECF No. 379-75 (the highlighted line is on page 3): https://storage.courtlistener.com/recap/gov.uscourts.cand.433688/gov.uscourts.cand.433688.379.75.pdf **Checklist:** - [x] Documentation has been updated, if necessary. - [x] Examples have been added, if necessary. - [x] Tests have been added, if necessary. Signed-off-by: BBC, Esquire --- docling/datamodel/pipeline_options.py | 26 +++++ .../stages/ocr/tesseract_ocr_cli_model.py | 45 ++++++-- .../models/stages/ocr/tesseract_ocr_model.py | 10 +- docs/concepts/OCR.md | 19 ++++ tests/data/ocr/sources/highlighted_text.png | Bin 0 -> 63263 bytes tests/test_e2e_tesseract_highlighted_text.py | 88 ++++++++++++++ tests/test_tesseract_ocr_cli_model.py | 107 +++++++++++++++++- tests/test_tesseract_ocr_model.py | 71 +++++++++++- 8 files changed, 350 insertions(+), 16 deletions(-) create mode 100644 tests/data/ocr/sources/highlighted_text.png create mode 100644 tests/test_e2e_tesseract_highlighted_text.py diff --git a/docling/datamodel/pipeline_options.py b/docling/datamodel/pipeline_options.py index 910b11cca4..b6e61883d9 100644 --- a/docling/datamodel/pipeline_options.py +++ b/docling/datamodel/pipeline_options.py @@ -662,6 +662,19 @@ class TesseractCliOcrOptions(OcrOptions): ) ), ] = None + orientation_confidence_threshold: Annotated[ + float, + Field( + description=( + "Minimum orientation confidence from Tesseract's orientation and script detection (OSD) for an " + "OCR region to be rotated before recognition. OSD reports it as the margin between its best and " + "second-best orientation scores, so values near 0 are close calls. Below the threshold the region " + "is read as rendered; `0` rotates on every detection." + ), + examples=[0.0, 1.0, 14.0], + ge=0.0, + ), + ] = 1.0 model_config = ConfigDict( extra="forbid", ) @@ -706,6 +719,19 @@ class TesseractOcrOptions(OcrOptions): ) ), ] = None + orientation_confidence_threshold: Annotated[ + float, + Field( + description=( + "Minimum orientation confidence from Tesseract's orientation and script detection (OSD) for an " + "OCR region to be rotated before recognition. OSD reports it as the margin between its best and " + "second-best orientation scores, so values near 0 are close calls. Below the threshold the region " + "is read as rendered; `0` rotates on every detection." + ), + examples=[0.0, 1.0, 14.0], + ge=0.0, + ), + ] = 1.0 model_config = ConfigDict( extra="forbid", ) diff --git a/docling/models/stages/ocr/tesseract_ocr_cli_model.py b/docling/models/stages/ocr/tesseract_ocr_cli_model.py index 2ea2c78137..3df79aaf62 100644 --- a/docling/models/stages/ocr/tesseract_ocr_cli_model.py +++ b/docling/models/stages/ocr/tesseract_ocr_cli_model.py @@ -18,7 +18,12 @@ from docling_core.types.doc.page import TextCell from docling.datamodel.accelerator_options import AcceleratorOptions -from docling.datamodel.base_models import Page +from docling.datamodel.base_models import ( + DoclingComponentType, + ErrorItem, + FailureCategory, + Page, +) from docling.datamodel.document import ConversionResult from docling.datamodel.pipeline_options import ( OcrOptions, @@ -67,6 +72,7 @@ def __init__( # multiplier for 72 dpi; the default 3.0 == 216 dpi. self.scale = self.options.scale + self._dpi = round(72 * self.scale) self._name: Optional[str] = None self._version: Optional[str] = None @@ -224,12 +230,13 @@ def _run_tesseract(self, ifilename: str, osd: Optional[pd.DataFrame]): if self.options.psm is not None: cmd.extend(["--psm", str(int(self.options.psm))]) + cmd.extend(["--dpi", str(self._dpi)]) cmd.append(self._sanitize_filename(ifilename)) cmd.extend(["stdout", "tsv"]) _log.info("command: {}".format(" ".join(cmd))) output = subprocess.run( - cmd, stdout=PIPE, stderr=DEVNULL, stdin=DEVNULL, check=True, shell=False + cmd, capture_output=True, stdin=DEVNULL, check=True, shell=False ) # _log.info(output) @@ -270,6 +277,8 @@ def _perform_osd(self, ifilename: str) -> pd.DataFrame: "0", "-l", "osd", + "--dpi", + str(self._dpi), self._sanitize_filename(ifilename), "stdout", ] @@ -341,7 +350,7 @@ def __call__( continue high_res_image = page._backend.get_page_image( scale=self.scale, cropbox=ocr_rect - ) + ).convert("L") try: with tempfile.NamedTemporaryFile( suffix=".png", mode="w+b", delete=False @@ -352,7 +361,10 @@ def __call__( df_osd: Optional[pd.DataFrame] = None try: df_osd = self._perform_osd(fname) - doc_orientation = _parse_orientation(df_osd) + doc_orientation = _parse_orientation( + df_osd, + self.options.orientation_confidence_threshold, + ) except subprocess.CalledProcessError as exc: _log.error( "OSD failed (doc %s, page: %s, " @@ -384,6 +396,18 @@ def __call__( image_file, exc.stderr, ) + conv_res.errors.append( + ErrorItem( + component_type=DoclingComponentType.MODEL, + module_name=type(self).__name__, + error_message=( + f"Tesseract failed on OCR rectangle {ocr_rect_i}: " + f"{_stderr_text(exc)}" + ), + category=FailureCategory.INFERENCE_FAILURE, + page_no=page.page_no, + ) + ) continue finally: if os.path.exists(fname): @@ -437,9 +461,16 @@ def get_options_type(cls) -> Type[OcrOptions]: return TesseractCliOcrOptions -def _parse_orientation(df_osd: pd.DataFrame) -> int: +def _parse_orientation(df_osd: pd.DataFrame, confidence_threshold: float) -> int: # For strictly optimal performance with invariant dataframe format: - mask = df_osd["key"].to_numpy() == "Orientation in degrees" - orientation_val = df_osd["value"].to_numpy()[mask][0] + keys = df_osd["key"].to_numpy() + values = df_osd["value"].to_numpy() + if float(values[keys == "Orientation confidence"][0]) < confidence_threshold: + return 0 + orientation_val = values[keys == "Orientation in degrees"][0] orientation = parse_tesseract_orientation(orientation_val.strip()) return orientation + + +def _stderr_text(exc: subprocess.CalledProcessError) -> str: + return " ".join((exc.stderr or b"").decode("utf-8", "replace").split()) diff --git a/docling/models/stages/ocr/tesseract_ocr_model.py b/docling/models/stages/ocr/tesseract_ocr_model.py index 19e0ea2d3c..d07ef27ee2 100644 --- a/docling/models/stages/ocr/tesseract_ocr_model.py +++ b/docling/models/stages/ocr/tesseract_ocr_model.py @@ -59,6 +59,7 @@ def __init__( self._auto_script: bool = not self.languages # multiplier for 72 dpi; the default 3.0 == 216 dpi. self.scale = self.options.scale + self._dpi = round(72 * self.scale) self._reader = None self._script_readers: dict[str, tesserocr.PyTessBaseAPI] = {} self._tesseract_vocabulary: list[str] = [] @@ -188,10 +189,11 @@ def __call__( continue high_res_image = page._backend.get_page_image( scale=self.scale, cropbox=ocr_rect - ) + ).convert("L") local_reader = self._reader self._osd_reader.SetImage(high_res_image) + self._osd_reader.SetSourceResolution(self._dpi) doc_orientation = 0 osd = self._osd_reader.DetectOrientationScript() @@ -209,7 +211,10 @@ def __call__( # to OCR in the hope OCR will succeed while OSD failed if self._auto_script: continue - else: + elif ( + osd["orient_conf"] + >= self.options.orientation_confidence_threshold + ): doc_orientation = parse_tesseract_orientation( osd["orient_deg"] ) @@ -244,6 +249,7 @@ def __call__( local_reader = self._script_readers[lang] local_reader.SetImage(high_res_image) + local_reader.SetSourceResolution(self._dpi) boxes = local_reader.GetComponentImages( self._reader_RIL.TEXTLINE, True ) diff --git a/docs/concepts/OCR.md b/docs/concepts/OCR.md index 8ddcdfad60..41cc31d6bc 100644 --- a/docs/concepts/OCR.md +++ b/docs/concepts/OCR.md @@ -301,6 +301,25 @@ tag where one exists, and the bare file name for everything else. An empty `lang` list runs Tesseract's per-page orientation and script detection. That requires the `osd` traineddata; without it, `lang=[]` raises with an install hint. +### Tesseract image input and orientation + +Both Tesseract engines receive a grayscale render of each OCR region, together with its resolution: +72 DPI times `scale`, so 216 DPI by default. Tesseract binarizes a colour image channel by channel, +which can blank out text on a highlighter mark or a coloured background; a grayscale image avoids +that. The cost is lower contrast for light text on a strongly coloured box and for light, saturated +text such as orange or yellow on white, which Tesseract can then miss. + +Before reading a region, Docling runs Tesseract's orientation and script detection (OSD) on it and +turns it upright. OSD reports its orientation confidence as the margin between its best and +second-best orientation, so a value near 0 is a close call. A detection below +`orientation_confidence_threshold` (default `1.0`) is ignored and the region is read as rendered. +Set it to `0` to rotate on every detection, or raise it when a wrong rotation costs you more than a +missed one; OCRmyPDF's page-rotation default, for example, is 14. + +When Tesseract fails on a region in the `tesseract` engine, the failure is reported in +`ConversionResult.errors` with its page number and Tesseract's message, and the document is marked +`PARTIAL_SUCCESS`. + ## OcrMac The engine's own vocabulary, in its own codes, is listed in diff --git a/tests/data/ocr/sources/highlighted_text.png b/tests/data/ocr/sources/highlighted_text.png new file mode 100644 index 0000000000000000000000000000000000000000..330dea587e298663c66ce13cae26ef10fff77919 GIT binary patch literal 63263 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 literal 0 HcmV?d00001 diff --git a/tests/test_e2e_tesseract_highlighted_text.py b/tests/test_e2e_tesseract_highlighted_text.py new file mode 100644 index 0000000000..3ad55820ec --- /dev/null +++ b/tests/test_e2e_tesseract_highlighted_text.py @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: The Docling Contributors +# SPDX-License-Identifier: MIT + +import importlib.util +import re +import shutil +from pathlib import Path + +import pytest + +from docling.datamodel.base_models import InputFormat +from docling.datamodel.pipeline_options import ( + OcrMode, + PdfPipelineOptions, + TesseractCliOcrOptions, + TesseractOcrOptions, +) +from docling.document_converter import DocumentConverter, ImageFormatOption + +pytestmark = pytest.mark.ml_ocr + +_FIXTURE = Path("./tests/data/ocr/sources/highlighted_text.png") + +_LINES = [ + "The parties agree that the landlord will repair the roof before winter.", + "Rent is due on the first day of each month at the office of the manager.", + "Late payments accrue a fee of five percent after a grace period of ten days.", + "The tenant must give sixty days written notice before moving out of the unit.", + "Pets are allowed only with the written consent of the landlord in advance.", + "The security deposit will be returned within thirty days after the lease ends.", + "Either party may terminate this agreement for a material breach of its terms.", + "This agreement is governed by the laws of the state where the unit is located.", +] +_HIGHLIGHTED_ROWS = (2, 3, 4) + +_SKIP_NO_TESSERACT = pytest.mark.skipif( + shutil.which("tesseract") is None, reason="tesseract not installed" +) +_SKIP_NO_TESSEROCR = pytest.mark.skipif( + importlib.util.find_spec("tesserocr") is None, reason="tesserocr not installed" +) + + +def _generate_fixture(): + from PIL import Image, ImageDraw, ImageFont + + dpi = 200 + image = Image.new("RGB", (round(8.5 * dpi), round(11 * dpi)), "white") + draw = ImageDraw.Draw(image) + font = ImageFont.load_default(size=28) + y = dpi + for row, line in enumerate(_LINES): + if row in _HIGHLIGHTED_ROWS: + left, top, right, bottom = draw.textbbox((dpi, y), line, font=font) + draw.rectangle((left - 4, top - 6, right + 4, bottom + 6), fill="yellow") + draw.text((dpi, y), line, fill="black", font=font) + y += 50 + image.save(_FIXTURE, dpi=(dpi, dpi)) + + +def _words(text): + return re.findall(r"[a-z]+", text.lower()) + + +@pytest.mark.parametrize( + "ocr_options_cls", + [ + pytest.param(TesseractCliOcrOptions, marks=_SKIP_NO_TESSERACT, id="tesseract"), + pytest.param(TesseractOcrOptions, marks=_SKIP_NO_TESSEROCR, id="tesserocr"), + ], +) +def test_full_page_ocr_reads_highlighted_lines(ocr_options_cls): + options = PdfPipelineOptions() + options.do_ocr = True + options.ocr_options = ocr_options_cls(lang=["eng"], mode=OcrMode.FULL_PAGE) + converter = DocumentConverter( + format_options={InputFormat.IMAGE: ImageFormatOption(pipeline_options=options)} + ) + + found = set(_words(converter.convert(_FIXTURE).document.export_to_markdown())) + + highlighted = [word for row in _HIGHLIGHTED_ROWS for word in _words(_LINES[row])] + missing = [word for word in highlighted if word not in found] + assert len(missing) <= len(highlighted) // 10, missing + + +if __name__ == "__main__": + _generate_fixture() diff --git a/tests/test_tesseract_ocr_cli_model.py b/tests/test_tesseract_ocr_cli_model.py index cff7377603..921b5d5115 100644 --- a/tests/test_tesseract_ocr_cli_model.py +++ b/tests/test_tesseract_ocr_cli_model.py @@ -1,15 +1,30 @@ # SPDX-FileCopyrightText: The Docling Contributors # SPDX-License-Identifier: MIT +import subprocess from unittest.mock import patch import pytest +from PIL import Image -from docling.datamodel.pipeline_options import TesseractCliOcrOptions +from docling.backend.image_backend import ImageDocumentBackend +from docling.datamodel.accelerator_options import AcceleratorOptions +from docling.datamodel.base_models import FailureCategory, InputFormat, Page +from docling.datamodel.document import ConversionResult, InputDocument +from docling.datamodel.pipeline_options import OcrMode, TesseractCliOcrOptions from docling.models.stages.ocr.tesseract_ocr_cli_model import TesseractOcrCliModel _MODULE = "docling.models.stages.ocr.tesseract_ocr_cli_model" +_OSD_STDOUT = ( + "Page number: 0\n" + "Orientation in degrees: 90\n" + "Rotate: 270\n" + "Orientation confidence: {confidence}\n" + "Script: Latin\n" + "Script confidence: 1.84\n" +) + _TSV_HEADER = ( "level\tpage_num\tblock_num\tpar_num\tline_num\tword_num" "\tleft\ttop\twidth\theight\tconf\ttext" @@ -44,6 +59,7 @@ def test_recognized_words_are_kept_verbatim(words: list[str]) -> None: model._safe_tessdata_path = None model._auto_script = False model._native_codes = ["eng"] + model._dpi = 216 with patch( f"{_MODULE}.subprocess.run", @@ -52,3 +68,92 @@ def test_recognized_words_are_kept_verbatim(words: list[str]) -> None: df = model._run_tesseract("page.png", osd=None) assert list(df["text"]) == words + + +class _FakeTesseract: + def __init__(self, osd_confidence=8.0, ocr_stderr=None): + self.osd_confidence = osd_confidence + self.ocr_stderr = ocr_stderr + self.commands: list[list[str]] = [] + self.ocr_images: list[tuple[str, tuple[int, int]]] = [] + + def __call__(self, cmd, **kwargs): + self.commands.append(cmd) + if "osd" in cmd: + stdout = _OSD_STDOUT.format(confidence=self.osd_confidence) + return _FakeCompletedProcess(stdout.encode("utf-8")) + with Image.open(next(arg for arg in cmd if arg.endswith(".png"))) as image: + self.ocr_images.append((image.mode, image.size)) + if self.ocr_stderr is not None: + raise subprocess.CalledProcessError(1, cmd, b"", self.ocr_stderr) + return _FakeCompletedProcess(_tsv(["Hello"]).encode("utf-8")) + + +def _convert_page(monkeypatch, tmp_path, tesseract, **options): + monkeypatch.setattr(f"{_MODULE}.subprocess.run", tesseract) + monkeypatch.setattr( + TesseractOcrCliModel, + "_get_name_and_version", + lambda self: ("tesseract", "5.5.0"), + ) + monkeypatch.setattr( + TesseractOcrCliModel, + "_set_languages", + lambda self: setattr(self, "_tesseract_vocabulary", ["eng", "osd"]), + ) + model = TesseractOcrCliModel( + enabled=True, + artifacts_path=None, + options=TesseractCliOcrOptions(mode=OcrMode.FULL_PAGE, **options), + accelerator_options=AcceleratorOptions(), + ) + image_path = tmp_path / "page.png" + Image.new("RGB", (200, 100), "white").save(image_path) + in_doc = InputDocument( + path_or_stream=image_path, + format=InputFormat.IMAGE, + backend=ImageDocumentBackend, + ) + conv_res = ConversionResult(input=in_doc) + page_backend = in_doc._backend.load_page(0) + page = Page(page_no=1, size=page_backend.get_size()) + page._backend = page_backend + assert list(model(conv_res, [page])) == [page] + return conv_res + + +@pytest.mark.parametrize( + ("confidence", "ocr_size"), + [(0.4, (600, 300)), (8.0, (300, 600))], + ids=["close-call", "confident"], +) +def test_region_is_rotated_only_on_a_confident_orientation( + monkeypatch, tmp_path, confidence, ocr_size +): + tesseract = _FakeTesseract(osd_confidence=confidence) + + _convert_page(monkeypatch, tmp_path, tesseract, lang=["eng"]) + + assert tesseract.ocr_images == [("L", ocr_size)] + + +def test_tesseract_is_told_the_render_resolution(monkeypatch, tmp_path): + tesseract = _FakeTesseract() + + _convert_page(monkeypatch, tmp_path, tesseract, lang=["eng"], scale=2.0) + + assert [cmd[cmd.index("--dpi") + 1] for cmd in tesseract.commands] == [ + "144", + "144", + ] + + +def test_failed_region_is_recorded_on_the_page(monkeypatch, tmp_path): + tesseract = _FakeTesseract(ocr_stderr=b"Error in pixReadStream: simulated") + + conv_res = _convert_page(monkeypatch, tmp_path, tesseract, lang=["eng"]) + + assert [ + (error.page_no, error.category, error.module_name) for error in conv_res.errors + ] == [(1, FailureCategory.INFERENCE_FAILURE, "TesseractOcrCliModel")] + assert "pixReadStream: simulated" in conv_res.errors[0].error_message diff --git a/tests/test_tesseract_ocr_model.py b/tests/test_tesseract_ocr_model.py index 7a9c3135d5..616d40388c 100644 --- a/tests/test_tesseract_ocr_model.py +++ b/tests/test_tesseract_ocr_model.py @@ -26,6 +26,9 @@ class _OcrReader(_Reader): def SetImage(self, image): pass + def SetSourceResolution(self, ppi): + pass + def DetectOrientationScript(self): return {"orient_deg": 0, "orient_conf": 10.0, "script_name": "Latin"} @@ -106,6 +109,16 @@ def test_cell_confidence_is_scaled_to_unit_range(monkeypatch, tmp_path): accelerator_options=AcceleratorOptions(), ) + conv_res, page = _ocr_blank_page(model, tmp_path, page_no=0) + + assert page.parsed_page is not None + cells = page.parsed_page.textline_cells + assert [cell.confidence for cell in cells] == [pytest.approx(0.42)] + assert conv_res.confidence.pages[0].ocr_score == pytest.approx(0.42) + del model + + +def _ocr_blank_page(model, tmp_path, page_no=1): image_path = tmp_path / "page.png" Image.new("RGB", (200, 100), "white").save(image_path) in_doc = InputDocument( @@ -115,13 +128,59 @@ def test_cell_confidence_is_scaled_to_unit_range(monkeypatch, tmp_path): ) conv_res = ConversionResult(input=in_doc) page_backend = in_doc._backend.load_page(0) - page = Page(page_no=0, size=page_backend.get_size()) + page = Page(page_no=page_no, size=page_backend.get_size()) page._backend = page_backend - list(model(conv_res, [page])) + assert list(model(conv_res, [page])) == [page] + return conv_res, page - assert page.parsed_page is not None - cells = page.parsed_page.textline_cells - assert [cell.confidence for cell in cells] == [pytest.approx(0.42)] - assert conv_res.confidence.pages[0].ocr_score == pytest.approx(0.42) + +def _recording_reader(osd): + class _RecordingReader(_OcrReader): + images: list[tuple[str, tuple[int, int]]] = [] + resolutions: list[int] = [] + + def SetImage(self, image): + _RecordingReader.images.append((image.mode, image.size)) + + def SetSourceResolution(self, ppi): + _RecordingReader.resolutions.append(ppi) + + def DetectOrientationScript(self): + return osd + + return _RecordingReader + + +def _model_with_reader(monkeypatch, reader, **options): + monkeypatch.setitem( + sys.modules, + "tesserocr", + _fake_tesserocr(lambda **kwargs: ("/tessdata", ["eng", "osd"]), reader), + ) + return TesseractOcrModel( + enabled=True, + artifacts_path=None, + options=TesseractOcrOptions(mode=OcrMode.FULL_PAGE, **options), + accelerator_options=AcceleratorOptions(), + ) + + +@pytest.mark.parametrize( + ("orient_conf", "ocr_size"), + [(0.4, (600, 300)), (8.0, (300, 600))], + ids=["close-call", "confident"], +) +def test_region_is_rotated_only_on_a_confident_orientation( + monkeypatch, tmp_path, orient_conf, ocr_size +): + reader = _recording_reader( + {"orient_deg": 90, "orient_conf": orient_conf, "script_name": "Latin"} + ) + model = _model_with_reader(monkeypatch, reader, lang=["eng"]) + + _ocr_blank_page(model, tmp_path) + + assert reader.images == [("L", (600, 300)), ("L", ocr_size)] + assert reader.resolutions == [216, 216] del model