Tools,

Table image to CSV: img2table went from 18% to 85%

85%

of cells right after two settings

Get the code

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=True 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.

Try the Actor

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, and min_confidence=1 added 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=True alone 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.
Sources

Two ways to skip the work

Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.

Try the Actor

Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.

Get one 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.