On this page
- OCR test page: a dense table with 192 cells and invented numbers
- What table OCR returns: one line per cell with text, box and confidence score
- How to rebuild table rows and columns from word bounding boxes
- Table OCR errors: four of 194 cells differ, each with its confidence
- Table test setup: local CPU run on a synthetic page
- Sources
Your table has to come out as rows and columns, not as a heap of words. A pipeline read a dense table with invented numbers and returned every cell as one line with its box. Rows and columns come back from the boxes with a short recipe.
The page is synthetic and the text is invented, so the true text is known cell by cell. Everything below is counted from the stored result; nothing was corrected by hand.
OCR test page: a dense table with 192 cells and invented numbers
The page has a title, a header row, fourteen product groups and a total row. That is sixteen rows of twelve cells: a name column and eleven number columns, in small print, with ruled lines. Some values are negative, and one cell holds -/-, which the footnote explains as "not collected". The numbers come from a fixed random seed.

Source: synthetic page synth08 (invented text, free to reuse, CC0), cropped to the table.
What table OCR returns: one line per cell with text, box and confidence score
The pipeline returns lines. On this page that is 194 lines: the title, 192 table cells, and the footnote. Each line has its text, a box and a confidence. The grid is rebuilt from the boxes (next section), and the plain text file has one cell per line. The table pipelines read table cells themselves; see table structure recognition.
| Line | Text | Box (x0, y0, x1, y1) | Confidence |
|---|---|---|---|
| 1 | Quartalsübersicht Warengruppen (Testdaten, Mio. EUR) | 148, 122, 1289, 162 | 0.999 |
| 2 | Gruppe | 154, 230, 252, 263 | 1.000 |
| 3 | Q1/24 | 361, 229, 441, 262 | 1.000 |
The first lines of the stored result (run scan-ctc-1-cpu-lokal-synth-20261107, page synth08).
That is enough to rebuild the grid: the boxes say where each cell sits.
How to rebuild table rows and columns from word bounding boxes
Group the boxes by the vertical middle of the box, with a tolerance of ten pixels at this resolution, and sort each group from left to right. On this page that gives eighteen groups: the title alone, sixteen groups of twelve boxes, and the footnote alone. The sixteen groups are the sixteen rows of the table, the first one is the header, the last one the total. The columns follow from the order within a row.
rows = []
for b in sorted(boxes, key=lambda b: (b[1] + b[3]) / 2):
yc = (b[1] + b[3]) / 2
if rows and abs(yc - rows[-1]["y"]) <= 10:
rows[-1]["cells"].append(b)
else:
rows.append({"y": yc, "cells": [b]})
grid = [sorted(r["cells"], key=lambda b: b[0]) for r in rows]
This recipe fits a clean page whose rows do not touch. For rotated pages, a pipeline with page orientation turns the page upright first; for cells that span rows, the table pipeline returns the structure.
Table OCR errors: four of 194 cells differ, each with its confidence
Of the 194 lines, 190 equal the invented text. Four differ:
| True text | Read | What differs | Confidence |
|---|---|---|---|
| Σ 24 | ∑24 | another sum sign (U+2211 for U+03A3) and the space is gone | 0.934 |
| Σ 25 | ∑25 | the same | 0.924 |
| -/- | -1- | a slash read as a one | 0.881 |
| ... „-/-“ steht für: nicht erhoben. | ... -/-“ steht für: nicht erhoben. | the opening quotation mark is missing | 0.999 |
The one that matters for a sum is -1-: in a column of numbers it looks like a value, and the cell should have been empty. Its confidence of 0.88 is one of only two below 0.9 on the page; the other belongs to the correctly read header Δ %. So a rule that checks every cell below 0.9 would have caught it with one false alarm; the missing quotation mark (confidence 0.999) lies outside such a rule. The rule was tried on this one page.
Table test setup: local CPU run on a synthetic page
The run is a local CPU run (Intel i7-1355U, two threads), not a run of the service on a GPU: the same models, line boxes as the pipeline delivers them. The confidences are the pipeline's own account.
Sources
| Statement | Where it comes from |
|---|---|
| The page, the true text of each line | synthetic page synth08 (image and true text, both own, CC0) |
| 194 lines, boxes, confidences, the four differences | stored result of the pipeline, run scan-ctc-1-cpu-lokal-synth-20261107, page synth08 |
| Eighteen groups, sixteen rows of twelve | the recipe above applied to those boxes; a test repeats the count |