Table Extraction and Processing#
Table handling in MinerU spans four distinct concerns: structure detection (which model identifies table regions and internal cell layout), cross-page merging (stitching tables split across PDF page breaks), text normalization within cell content, and backend accuracy differences that determine which pipeline to choose for a given document.
Pipeline Position#
Cross-page table merging is a document-level post-processing step, called after para_split() and before title-level assignment . The entry point is cross_page_table_merge(), which reads the MINERU_TABLE_MERGE_ENABLE environment variable (default true) and delegates to merge_table() in mineru/utils/table_merge.py.
Table Structure Detection#
MinerU classifies table regions using PP-DocLayoutV2, an RT-DETR object detector that predicts 25 layout classes per page image . Tables detected here become Level 1 container blocks (type=table) in the intermediate JSON, with table_body, table_caption, and table_footnote sub-blocks .
Once a table region is located, two specialized models handle cell-level structure:
| Model | Class | File |
|---|---|---|
| SLANet+ (wireless) | PaddleTable | mineru/model/table/rec/slanet_plus/main.py |
| UNet (wired) | WiredTable | mineru/model/table/rec/unet_table/main.py |
A table-type classifier (PaddleTableClsModel) routes each detected table to the appropriate recognition model based on whether it has ruled lines. An orientation classifier (MineruTableOrientationClsModel) handles rotated tables.
All table recognition models output HTML (<table> markup with <td>/<th>, colspan, and rowspan attributes), stored in the html field of the table_body span.
Cross-Page Table Merging#
The merge logic lives in mineru/utils/table_merge.py. The top-level merge_table() iterates pages in reverse, checking whether the last block of the previous page and the first block of the current page are both BlockType.TABLE.
Merge Eligibility#
can_merge_tables() gates every merge attempt with these checks (in order):
- Caption guard — if the current table has a caption that does not contain a continuation marker (e.g.,
"(续)","(continued)","续表"), it is treated as a new table and merge is blocked. Continuation markers are detected byis_table_continuation_text(). - Footnote guard — a non-empty footnote on the previous table signals the table has ended; merge is blocked.
- Width check — if the bounding boxes of the two tables differ in width by ≥ 10%, merge is blocked .
- Column count — if
total_colsmatches exactly, merge proceeds immediately; otherwisecheck_rows_match()compares the last row of the previous table to the first data row of the current table using effective column counts and rendered segment counts.
Header Detection and Deduplication#
detect_table_headers() scans up to MAX_HEADER_ROWS = 5 rows from the top of each table and compares them by cell count, colspan/rowspan tuples, and normalized text (full-width → half-width, whitespace stripped). If strict structural comparison fails, _detect_table_headers_visual() falls back to text-only comparison with rendered-segment-count matching — this handles OCR-induced colspan/rowspan loss. Matched header rows are skipped in the continuation table to avoid duplicating column headers .
Header deduplication also accounts for rowspan cells that physically span multiple rows: _expand_header_count_by_rowspan() extends the skip count so that rowspan-covered rows are not left dangling after the merge.
Column Count Reconciliation#
When the two tables have different column counts, adjust_table_rows_colspan() patches colspan attributes on the narrower table's rows to align with the wider one. The reference structure is taken from the boundary rows (last row of the previous table or first data row of the current table).
Rowspan Continuity Across Page Breaks#
_clip_overlapped_blank_rowspan_cells() removes empty structural placeholder cells in the continuation table that would duplicate columns already occupied by rowspans crossing the page break. Only cells with no semantic content (_cell_has_semantic_content() returns False) are clipped .
Cell-Level Partial Merge#
For VLM-backend tables, the cell_merge field on a table block marks individual columns where the last row of the previous table and the first row of the continuation table actually belong to the same logical cell (split mid-cell by the page break). _apply_cell_merge() transfers content between those cells using visual-column mappings, then removes the now-empty row if fully consumed.
Post-Merge Caption Restoration#
Captions attached below a table_body (below its bounding box) that lack continuation markers are not real table captions; they are paragraph headings misclassified by the layout model. _restore_post_table_captions_as_text() re-promotes them to independent TEXT blocks on the current page after the merge.
Text Normalization in Table Cells#
Two normalizations are applied uniformly across cell content:
- Full-width → half-width conversion via
full_to_half()inmineru/utils/char_utils.py. Characters in the Unicode range0xFF01–0xFF5Eare shifted by0xFEE0to their ASCII equivalents. This is applied during header comparison (_normalize_cell_text()) and when parsing continuation markers. - Whitespace collapse —
_normalize_cell_text()strips all whitespace after the conversion, producing a compact string used only for structural comparison. Display output uses_display_cell_text()which preserves internal whitespace but strips leading/trailing space .
Note: MinerU does not normalize decimal/thousands separators (e.g., European , vs .). Numbers that happen to span cell boundaries can be incorrectly merged — this is a known limitation in the pipeline backend's table cell detection .
Backend Accuracy Differences#
Table quality varies significantly across backends. A maintainer confirmed: "Pipeline's accuracy on tables and formulas is far behind Hybrid's. For dense, lightly ruled tables, the default Hybrid backend produces much better results than Pipeline" .
| Backend | Table Accuracy | Notes |
|---|---|---|
| Pipeline | Lowest | Poor on lightly-ruled or borderless tables; better for CPU-only use |
| Hybrid (medium effort) | Mid | Default since v3.3; faster but can miss dense layouts or complex cell structures |
| Hybrid (high effort) | High | Recommended for most documents; more accurate layout and OCR |
| VLM | Highest overall | Handles complex/borderless tables best; risk of hallucination; ~40% slower |
The Pipeline backend's weaker table performance stems from the underlying table recognition models operating without the full layout context that Hybrid provides. When cell boundaries are ambiguous (lightly ruled or borderless tables), recognition errors propagate into cell text — for example, merging numerically adjacent cell values into a single token .
The --effort flag (-e high) controls inference depth for the Hybrid backend. The online API defaults to medium effort and does not expose this parameter; self-hosted deployments can switch freely .
Cross-page table screenshots are a known structural gap: content_list.json cannot hold multiple img_path values per table block, so multi-page table images are incomplete. A redesign is planned .
Key Files#
| File | Purpose |
|---|---|
mineru/utils/table_merge.py | Core cross-page merge logic: eligibility, header detection, column reconciliation, cell merge |
mineru/backend/utils/runtime_utils.py | cross_page_table_merge() wrapper; controlled by MINERU_TABLE_MERGE_ENABLE env var |
mineru/utils/table_continuation.py | is_table_continuation_text() — detects Chinese/English continuation markers |
mineru/utils/char_utils.py | full_to_half() — Unicode normalization for cell text comparison |
mineru/model/table/rec/slanet_plus/ | SLANet+ model for wireless table structure recognition |
mineru/model/table/rec/unet_table/ | UNet model for wired (ruled) table structure recognition |
mineru/model/table/cls/ | Table type and orientation classifiers |