Skip to content

Repository files navigation

doc-ocr

Structural extraction pipeline for scanned PDFs. Turns regulations, resolutions and legal documents into structured text, navigable Markdown and hierarchical JSON — preserving exact reading order and keeping tables as tables.

Built to process Colombian health-sector regulation, where the source documents are scans of printed pages full of bordered tables, and where a table flattened into loose lines of text is worse than no extraction at all.


The idea: find the geometry first, OCR second

Most OCR pipelines hand the model a whole page and try to reconstruct structure from the text it returns. That loses tables — cells come back as a stream of fragments, and any attempt to rebuild rows and columns from coordinates afterwards is guesswork.

This pipeline inverts the order. OpenCV finds the structure before the OCR model sees anything, so the model never looks at a full page — only at regions already known to be paragraphs, or at the exact pixels of a single table cell:

PDF
 └─▶ Pages → PNG (pdf2image, 300 DPI)
      └─▶ For each page:
           ├─▶ TableLineDetector  (morphological ops: erode/dilate to isolate
           │    │                  horizontal + vertical rules, intersect to a grid)
           │    └─▶ Crop each cell → batch OCR → TableStructure(rows, cols, spans)
           └─▶ TextRegionDetector (table areas masked out first)
                └─▶ Crop each region → OCR → paragraph
 └─▶ Merge into one stream ordered by (page, Y)
      └─▶ Markdown + JSON

Two consequences worth noting:

  • A cell's text is unambiguous. It came from a crop of that cell, so row/column assignment is geometric fact, not inference. Row and column spans survive.
  • Reading order is the page's own. Regions are sorted top-to-bottom by their bounding box, so a Markdown table lands exactly where it sat between two paragraphs.

If a page yields no text regions, the pipeline falls back to full-page OCR with the table areas masked, so nothing is silently dropped.

PDFs that already carry vector text skip all of this. is_vectorial_pdf() samples the first pages, and if there is a real text layer the pipeline reads it with pdfplumber — no rasterising, no OCR, no GPU.


Requirements

  • Docker with the NVIDIA Container Toolkit
  • An NVIDIA GPU with CUDA 12.3 support, driver ≥ 525

The OCR engine is PP-OCRv5 server (detection + recognition) via PaddleOCR 3.x on PaddlePaddle GPU 3.1.0. --no-gpu forces CPU, at a considerable cost in speed.


Quick start

docker compose build                                     # first time only

docker compose run --rm ocr data/document.pdf -o data/output/

docker compose run --rm ocr data/document.pdf --pages 1 2 3 -o data/output/
docker compose run --rm ocr data/document.pdf --mode ocr      # force OCR on a vector PDF
docker compose run --rm ocr data/document.pdf --save-images   # dump detection overlays

PaddleOCR downloads its weights on first run and persists them in the paddlex-models volume, so later runs start immediately.


CLI

Argument Default Description
pdf Path to the input PDF
-o, --output output Output directory
--dpi 300 Resolution for PDF → image conversion
--mode auto auto · native (pdfplumber) · ocr (force OCR)
--pages N [N ...] all 1-based page numbers to process
--min-conf 0.45 Minimum OCR confidence [0.0–1.0]
--save-images off Save PNGs with detections drawn on them
--tensorrt off Enable TensorRT (must be installed in the image)
--no-gpu off Run on CPU
-v, --verbose off Debug logging

Output

Three files per document:

File Contents
<name>.md Markdown, with GFM tables inline at their original position
<name>.json The full DocumentStructure serialised
<name>_ocr.txt Plain text per page — useful when debugging a bad extraction

--save-images adds an images/ directory with page_NNN_detection.png (detected table cells) and page_NNN_text_regions.png (detected text regions). When an extraction goes wrong, these show whether the failure was in detection or in recognition — which is usually the whole question.


Layout

main.py                        CLI entry point
pipeline.py                    ResolutionPipeline — orchestrates the whole run
detector/
  table_detector.py            TableLineDetector: morphological grid → CellBBox
  text_region_detector.py      TextRegionDetector: text regions, table areas masked
  layout_analyzer.py           column detection and reading-order reordering
  block_classifier.py          block typing
ocr/
  engine.py                    PaddleOCR wrapper (single + batch), confidence filtering
  pdf_converter.py             PDF → PNG, page by page
  preprocessor.py              binarisation and deskew
extractors/
  native_extractor.py          vector-PDF path via pdfplumber
  structure_builder.py         assembles DocumentStructure, drops text already in tables
  table_extractor.py
  text_extractor.py
models/schemas.py              RawBlock · CellData · TableStructure · Section · DocumentStructure
output/
  markdown_renderer.py         Markdown / HTML rendering for tables with spans
  json_writer.py

Limits

  • Bordered tables only. The detector works from ruled lines. Tables laid out purely by whitespace are not detected as tables and fall through to the text-region path.
  • Tuned on Colombian health-sector regulation: A4 portrait scans, printed text. Handwriting and dense multi-column layouts are outside what it was built for.
  • No test suite. The debug overlays are the verification tool in practice.

Licence

MIT — see LICENSE.

About

Structural extraction from scanned PDFs: OpenCV finds tables and text regions before OCR runs, so tables survive as tables. Outputs Markdown and JSON.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages