Table image to CSV: img2table went from 18% to 85%
85%
of cells right after two settings
Out of the box: 18.5% of cells
73 scanned invoices with grid lines, 4,575 cells, read by img2table 2.0.0 with Tesseract:
| img2table setting | Cells right | Tables found |
|---|---|---|
| Defaults | 18.5% | 22 |
detect_rotation
|
22.0% | 26 |
| The scan's dpi, passed to Tesseract | 38.1% | 37 |
| Both | 81.9% | 74 |
| Both, and drop the word None | 85.3% | 74 |
Each page holds one table. With both settings, img2table found all 73 and cut one in two.
A tilt under 1 degree hides the table
Every scan here is turned a little, like a real one. With the dpi set, tables img2table missed:
| Tilt | Rotation off | Rotation on |
|---|---|---|
| 0.3 to 0.6 degrees, 32 pages | 0 | 0 |
| 0.8 degrees, 17 pages | 12 | 0 |
| 0.9 degrees, 8 pages | 8 | 0 |
| 1.2 degrees, 16 pages | 16 | 0 |
A missed table is not an error. extract_tables returns an
empty list, so check its length.
Fix the dpi with a small class
img2table saves each page to a temporary JPEG with no dpi, then runs
tesseract with no --dpi. So
Tesseract guesses:
300 dpi, the real scan
483 dpi, Tesseract's guess
With the wrong guess, fewer cells came out right. This passes the real dpi:
import subprocess
import tempfile
import cv2
from img2table.ocr import TesseractOCR
class TesseractDpi(TesseractOCR):
dpi = 300 # your scan's dpi; img2table 2.0.0 leaves it out
def hocr(self, image):
with tempfile.NamedTemporaryFile(suffix=".jpg") as f:
cv2.imwrite(f.name, image)
cmd = ["tesseract", f.name, "stdout", "--psm", str(self.psm),
"--dpi", str(self.dpi), "-l", self.lang, "hocr"]
out = subprocess.check_output(cmd, env=self.env, stderr=subprocess.DEVNULL)
return out.decode()
The word None in your cells
img2table turns a word that is only a grid bar, like
|, into Python's None. Then the cell prints it: "None
Amount" for "Amount".
155 cells of 4,502 had the word None
With borderless_tables=True, the same None crashed
extract_tables on 1 page with a TypeError.
Then write the CSV
Below the class, in the same file. It needs
pip install img2table and the
tesseract program:
import csv
from img2table.document import Image
tables = Image("scan.jpg", detect_rotation=True).extract_tables(ocr=TesseractDpi())
with open("table.csv", "w", newline="") as f:
out = csv.writer(f)
for table in tables:
for row in table.content.values():
# img2table writes a lone grid bar as None: drop it
out.writerow([" ".join(w for w in str(c.value or "").split() if w != "None")
for c in row])
The class reaches into img2table's own method, so pin
img2table==2.0.0.
Against plain Tesseract and our Actor
The same 73 scans:
| Method | Cells right | Numbers right |
|---|---|---|
| Tesseract word boxes, rows rebuilt (this code) | 76.5% | 80.0% |
| img2table, the code above | 85.3% | 76.4% |
| Our Apify Actor, live build 0.1.44 | 99.9% | 99.9% |
img2table got more cells right than the word-box code, but fewer of the prices and amounts.
Fair warning: the Actor's table code was tuned on half of these pages. On the 36 pages it never saw, it got 2,242 of 2,245 cells.
How I tested it
- Pages: the 73 scans of the Tesseract table test: 72 made-up invoices and one made-up plumbing invoice. Every value is known, so no person had to check the truth.
- Each page: one table with grid lines and 5 columns, scanned at 150 to 300 dpi, a little tilted, with blur, noise and JPEG.
- A cell is right only if its text matches exactly. Each true row is matched to the output row most like it.
- Tesseract 5.5.4 with the eng model from tessdata_fast 4.1.0, and img2table 2.0.0, on a Mac, 4 pages at once.
-
img2table's own Tesseract call uses
--psm 11. With rotation on and no dpi, psm 6 read 1.0% of cells, andmin_confidence=1added 2 cells. - The Actor rows come from its page code, run here: tables on, English, as in a default run. Build 0.1.59 gave the same cells.
- Seconds a page: word boxes 0.4, img2table 0.8, the Actor 1.6.
What I did not test
-
Tables without grid lines. On these grid tables,
borderless_tables=Truealone read 21.8%. See the PDF table test. - Real scans of real tables, and phone photos.
- img2table with PaddleOCR, EasyOCR or another engine.
- img2table's PDF class. I fed it images.
- Windows. One Mac.
Two ways to skip the work
Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.
Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.
Questions people ask
How do I turn a table image into a CSV in Python?
Use img2table with Tesseract, set
detect_rotation=True, pass your scan's dpi, and write
each row with the csv module, as in the code above. Here that read
85.3% of cells.
Why does img2table find no tables?
A small tilt is the usual cause: without
detect_rotation, it missed every table tilted 0.9
degrees or more here. A tessdata_dir without its
configs folder also gives no tables and no error.
Why does img2table put None in my cells?
img2table 2.0.0 turns a word that is only a grid bar into None, and the cell prints it. Drop the word None from each cell, as the CSV code above does.