Best DPI for OCR, tested from 72 to 600 dpi
Scan print at 200 to 300 dpi in gray, and skip 600. On 55 printed book pages, Tesseract kept 99.9% of its words until the small letters fell under 8 pixels tall, which is about 120 dpi for 10 point type. On 72 clean invoices, 240 dpi read the most numbers, 2,374 of 2,418. 600 dpi read 2,310 for 2.7 times the CPU of 300. The 300 dpi rule belongs to black and white scans, which need about twice the pixels: at 150 dpi, gray read 2,316 numbers and black and white 1,899. If your text is already too small, doubling the image took my smallest book test from 9,711 words to 13,554.
Try it on your PDF, $0.006 a page
The results
I have no scanner, so I made two sets of pages with known text and read them with Tesseract 5.5.3 and its default English model.
- Book scans. 55 printed pages from 11 books, 14,693 words checked by two people on Wikisource. The files are 1,920 pixels wide. I shrank each one by averaging its pixels, which is close to what a scanner's sensor does.
- Invoices. 72 made-up pages with 2,418 numbers, in 4 fonts at 11 to 13 point. Chrome drew each page at 1,200 dpi. I added a small tilt and blur, then averaged down to each dpi. A number counts only if it matches exactly. Clean stops there, saved as JPEG quality 92. Rough adds noise on each pixel and JPEG quality 60 to 80.
| Invoice dpi | Clean, gray | Clean, black and white | Rough, gray | Rough, black and white | CPU seconds, 72 clean gray pages |
|---|---|---|---|---|---|
| 72 | 1,307 | 163 | 932 | 132 | 12.1 |
| 100 | 2,225 | 1,059 | 2,101 | 665 | 14.1 |
| 120 | 2,314 | 1,435 | 2,246 | 1,164 | 15.9 |
| 150 | 2,316 | 1,899 | 2,231 | 1,553 | 17.6 |
| 200 | 2,358 | 2,153 | 2,142 | 2,031 | 20.4 |
| 240 | 2,374 | 2,187 | 2,127 | 2,140 | 23.3 |
| 300 | 2,337 | 2,199 | 2,079 | 2,201 | 27.8 |
| 400 | 2,316 | 2,263 | 1,952 | 2,256 | 38.8 |
| 600 | 2,310 | 2,154 | 1,822 | 2,278 | 76.4 |
The book scans tell the same story, and they are real. Each row is the
same 55 pages at a smaller size. Small letters here means the height
of words like some, which have no tall or hanging
letters.
| Scale | Small letters, pixels | Gray, words of 14,693 | Black and white, words |
|---|---|---|---|
| 100% | 21 to 39 | 13,858 | 13,762 |
| 80% | 17 to 31 | 13,862 | 13,763 |
| 67% | 14 to 26 | 13,872 | 13,745 |
| 50% | 11 to 20 | 13,891 | 13,695 |
| 40% | 8 to 16 | 13,884 | 13,271 |
| 33% | 7 to 13 | 13,763 | 12,402 |
| 25% | 5 to 10 | 13,208 | 9,863 |
| 20% | 4 to 8 | 9,711 | 6,030 |
- More pixels stopped helping early. Clean gray stayed between 2,310 and 2,374 from 120 to 600 dpi. The book pages read a little better at half size than at full size, for about half the CPU (32.6 against 62.2 seconds for 60 pages).
- Black and white kept gaining. Gray levels off at about 120 dpi on these invoices, while black and white climbs to 400 dpi on clean pages and to 600 on rough ones. So a scanner's black and white mode is where 300 dpi still earns its keep.
- Noise changes the shape. On rough pages, gray got worse as dpi went up, from 2,246 at 120 dpi to 1,822 at 600. The noise has the same strength on every pixel at every dpi, and I did not trace why that hurts big letters more. Rough black and white beat rough gray from 240 dpi up. The cut to black and white probably throws much of the noise away, but I did not test that. On clean pages, gray won at every dpi.
- Small print moved the peak. I ran the rough set again with all type at 70% of its size, as a check on the idea below. Gray peaked at 240 dpi (2,280 numbers) instead of 120, and 120 dpi fell to 1,891. So the best dpi follows the size of the letters.
Count pixels, not dpi
A dpi number means little without a type size. What Tesseract sees is the height of the small letters in pixels, the x-height. A quick estimate:
x-height in pixels = dpi * point size / 150
My four invoice fonts came within 3 pixels of that at 300 dpi. Here is what each height did to the book pages. Kept compares each page with the same page at full size, so the mix of books does not move the number.
| Small letters, pixels | Gray words kept |
|---|---|
| 12 to 14.9 | 100.33% |
| 10 to 11.9 | 100.10% |
| 8 to 9.9 | 99.93% |
| 7 to 7.9 | 98.31% |
| 6 to 6.9 | 94.81% |
| 5 to 5.9 | 73.77% |
| Under 5 | 10.86% |
That gives this table, from the estimate above. The last column comes from the books: black and white held 98.8% of the gray full size score at a median of 15.5 pixels, then fell fast, to 95.8% at 12.4 and 71.2% at 7.8.
| Type size | Gray, lowest I would go (8 px) | Gray, comfortable (12 to 16 px) | Black and white (16 px) |
|---|---|---|---|
| 6 point | 200 dpi | 300 to 400 dpi | 400 dpi |
| 8 point | 150 dpi | 225 to 300 dpi | 300 dpi |
| 10 point | 120 dpi | 180 to 240 dpi | 240 dpi |
| 12 point | 100 dpi | 150 to 200 dpi | 200 dpi |
If you do not know the type size, 300 dpi in gray is a safe default. It costs little, and it covers small print.
Why do more pixels stop helping? tessdata_fast and tessdata_best both
read a text line at a fixed height of 36 pixels. You can see it in
each model's own network string,
[1,36,0,1Ct3,3,16Mp3,3Lfys48... for fast and
[1,36,0,1Ct3,3,16Mp3,3Lfys64... for best, and in
input.cpp, which scales each line to that height. My book lines stood about 2
times the height of their small letters, so small letters of 18 pixels
already fill it. Past that, Tesseract shrinks the line itself.
The docs say otherwise in three places, so here is how they did:
- The Improving Quality page says Tesseract "works best on images which have a DPI of at least 300 dpi, so it may be beneficial to resize images". In clean gray, 300 dpi beat 120 and 150 by 23 and 21 numbers of 2,418, and lost to 200 and 240. In rough gray, it lost to all four. Resizing up hurt on text that was big enough.
- The old FAQ says: "Below an x-height of 10 pixels, you have very little chance of accurate results, and below about 8 pixels, most of the text will be “noise removed”." That page is in the Tesseract 3 folder. In gray, the current engine kept 99.93% at 8 to 10 pixels. Black and white comes closer to the warning: at a median of 7.8 pixels it found 9,863 words, against 13,208 in gray.
- The same FAQ says of the current engine: "Using LSTM there seems also to be a maximum x-height somewhere around 30 px. Above that, Tesseract doesn’t produce accurate results." I found no cliff. The 6 books with small letters of 30 to 39 pixels found 6,238 of 6,688 words at full size and 6,265 at 40%. Blown up 2 times, they found 6,205. Clean invoices at 600 dpi, up to 49 pixels, lost 27 numbers of 2,418 against 300 dpi. So bigger than 30 costs a little and buys nothing, but it does not break.
Give Tesseract the true dpi
This can cost more than any choice of dpi. Here is the same set with three ways of telling Tesseract the resolution:
| Rough invoices | No dpi in the file, no flag | True dpi | --dpi 300 on everything |
|---|---|---|---|
| 72 dpi | 901 | 932 | 604 |
| 100 dpi | 2,143 | 2,101 | 1,758 |
| 150 dpi | 2,145 | 2,231 | 2,224 |
| 200 dpi | 1,576 | 2,142 | 2,129 |
| 300 dpi | 460 | 2,079 | 2,079 |
| 600 dpi | 437 | 1,822 | 1,826 |
-
On big images, having no dpi is the danger. On the
300 dpi pages,
--dpi 150,433and600read 2,070, 2,072 and 2,069 numbers. Only--dpi 70broke them, with exactly the 460 of a file with no dpi. In baseapi.cpp a missing dpi becomes 70. The lineEstimating resolution asand a number is a guess from pagesegmain.cpp, line size times 10. On these pages the guess ran from 381 to 484, with a median of 433, and it did not save them. A file that says 1 dpi getsWarning: Invalid resolution 1 dpi. Using 70 instead., with the same fix. - This hit tables, not prose. On the book pages, no dpi moved the count by 1 word of 13,858 at full size, and by 3 at most at the smaller sizes I ran. So a test on plain text hides it.
- Do not hard-code 300 if your files vary. It was harmless at 150 dpi and up, but it cost a lot on small images. Books at 25% fell from 13,208 words to 11,048.
Check what a file says, then pass the number:
from PIL import Image
print(Image.open("scan.jpg").info.get("dpi"))
None means the file says nothing. These gave the same
output:
tesseract scan.png out --dpi 200
tesseract scan.png out -c user_defined_dpi=200
import pytesseract
text = pytesseract.image_to_string("scan.png", config="--dpi 200")
The flag always beats the file. Or write the dpi into the file, and
plain tesseract needs no flag:
from PIL import Image
img = Image.open("scan.png")
img.save("scan-200.png", dpi=(200, 200))
Upscale only tiny text, and only 2 times
This is the test for files you cannot scan again. Lanczos, with the true dpi passed each time:
| Pages | As is | 2x | 3x | 4x |
|---|---|---|---|---|
| Books, small letters 4 to 8 px | 9,711 | 13,554 | 13,544 | 13,492 |
| Books, 5 to 10 px | 13,208 | 13,786 | 13,739 | 13,801 |
| Books, 7 to 13 px | 13,763 | 13,876 | 13,830 | not run |
| Books, 8 to 16 px | 13,884 | 13,863 | not run | not run |
| Books, full size | 13,858 | 13,804 | not run | not run |
| Rough invoices, 72 dpi | 932 | 1,983 | 1,964 | 1,998 |
| Rough invoices, 100 dpi | 2,101 | 2,283 | 2,268 | not run |
| Rough invoices, 150 dpi | 2,231 | 2,172 | not run | not run |
| Rough invoices, 200 dpi | 2,142 | 2,003 | not run | not run |
So the line sits at about 10 pixels. Under it, 2 times the size is a big win, and 3 or 4 times adds next to nothing. Over it, leave the image alone. Bicubic did about the same as Lanczos: 13,484 against 13,554 words, 13,800 against 13,786, and 1,978 against 1,983 numbers.
This code does that for you. It reads the page once and estimates the
x-height. If that is under 10 pixels, or no word is sure enough to
measure, it reads again at 2 times the size. Pages that are big enough
cost nothing extra. Save it as read_scan.py:
import re
import statistics
import pytesseract
from PIL import Image
from pytesseract import Output
LOW = re.compile(r"[acemnorsuvwxz]{2,}") # no tall or hanging letters
DIGITS = re.compile(r"[0-9]{2,}") # digits stand about 1.4 times the x-height
def x_height(d):
"""Estimated x-height in pixels, from the output of image_to_data."""
heights = []
for text, height, conf in zip(d["text"], d["height"], d["conf"]):
if conf >= 60 and LOW.fullmatch(text):
heights.append(height)
elif conf >= 60 and DIGITS.fullmatch(text):
heights.append(height / 1.4)
return statistics.median(heights) if heights else None
def read_scan(path, dpi):
"""Read a gray scan. dpi is its true resolution. Tiny text gets a second read at twice the size."""
img = Image.open(path).convert("L")
d = pytesseract.image_to_data(img, config=f"--dpi {dpi}", output_type=Output.DICT)
x = x_height(d)
if x is None or x < 10:
img = img.resize((img.width * 2, img.height * 2), Image.LANCZOS)
d = pytesseract.image_to_data(img, config=f"--dpi {dpi * 2}", output_type=Output.DICT)
return d
import pytesseract
from pytesseract import Output
from read_scan import read_scan, x_height
d = pytesseract.image_to_data("scan.jpg", config="--dpi 100", output_type=Output.DICT)
print(x_height(d))
d = read_scan("scan.jpg", dpi=100)
print(" ".join(t for t in d["text"] if t.strip()))
| Pages | As is | With read_scan | Pages doubled |
|---|---|---|---|
| Rough invoices, 72 dpi | 932 | 1,963 | 71 of 72 |
| Rough invoices, 100 dpi | 2,101 | 2,273 | 68 of 72 |
| Clean invoices, 100 dpi | 2,225 | 2,313 | 69 of 72 |
| Rough invoices, 150 and 300 dpi | 2,231 and 2,079 | The same | 0 of 72 |
| Books at 33% | 13,763 | 13,890 | 27 of 60 |
| Books at 25% | 13,208 | 13,787 | 55 of 60 |
| Books at 20% | 9,711 | 13,449 | 59 of 60 |
I picked the 10 on these same pages, so expect a little less on yours. The word counts are for the 55 printed pages. The doubled count includes the handwritten book.
Shrink a rough, high resolution scan
If your file is big and speckled, make a smaller copy for OCR and keep the original. These are the rough invoices, shrunk with Lanczos, with the new dpi passed:
| File | As is | To 200 or 300 dpi | To 150 dpi | To 120 dpi |
|---|---|---|---|---|
| 300 dpi | 2,079 | not run | 2,129 | 2,225 |
| 400 dpi | 1,952 | 2,055 at 200 | 2,111 | 2,200 |
| 600 dpi | 1,822 | 2,027 at 300 | 2,091 | 2,217 |
import pytesseract
from PIL import Image
img = Image.open("rough600.jpg").convert("L")
img = img.resize((img.width // 5, img.height // 5), Image.LANCZOS)
text = pytesseract.image_to_string(img, config="--dpi 120")
That took the 600 dpi set from 79.0 to 15.9 CPU seconds and added 395 numbers. Aim by letter height, not by that 120: on these fonts it leaves small letters of 8 to 10 pixels. For small print, the rough peak sat at 240 dpi, so do not copy the number. Do this only on rough files. On clean ones, 120 dpi read fewer numbers than 240.
If you do not want to build it
PDF OCR to Markdown is our Apify Actor. It renders each scanned page at 300 dpi in gray, turns it upright, and reads it with Tesseract, and it passes the dpi for you. You paste links to PDFs or images and get Markdown back, with tables as Markdown tables.
- $0.006 a page, plus $0.00005 a run at the default 1 GB. Failed and blank pages are free.
- Links in a sheet? Use Bulk OCR from Google Sheets and CSV, $0.005 an OCR page.
- Pages are read inside the run. They are not sent to any AI service.
I did not run these tests on it, so no number above describes it.
What I did not test
- A real scanner at each dpi. Averaging pixels is close to what a sensor does, but not the same, and a scanner at 600 dpi has its own blur and noise.
- How common rough pages are. In an empty patch of paper, the rough files read a standard deviation of 3 to 6 gray levels. The real scans I have read 0 to 1.5, though the books passed through DjVu, which smooths paper. So treat rough as a bad day, not a normal one.
- Real pages above their file size. The book scans stop at 1,920 pixels wide, so true 600 dpi comes only from the made-up invoices.
- Phone photos, faxes and screenshots. Their blur and lighting differ from anything here.
- A scanner's smart black and white mode. I used one fixed cut at gray level 128. An adaptive mode can do better.
- Handwriting. My one handwritten book did better when shrunk, 452 of 929 words at full size and 633 at 40%. Five pages is too few to say why, so I left it out of the size numbers.
- Any other language, tessdata_best, any other OCR engine, or any machine but this Mac. Other engines may want other sizes.
Questions people ask
What is the best DPI for OCR?
For gray scans of normal print, 200 to 300 dpi. On 72 clean invoices, 240 dpi read the most numbers, 2,374 of 2,418. If you do not know the type size, 300 dpi in gray is a safe default.
Is 600 dpi better than 300 dpi for OCR?
Not in my tests. On clean invoices, 600 dpi read 2,310 numbers against 2,337 for 300, and it took 2.7 times the CPU. On rough scans it read 1,822 against 2,079.
What is the minimum DPI for OCR?
It depends on the type size. In gray, book pages kept 99.9% of their words with small letters 8 to 10 pixels tall, about 120 dpi for 10 point type. Black and white needs about 16 pixels, which is 240 dpi for 10 point and 300 dpi for 8 point.
Should I upscale an image before OCR?
Only if the small letters are under about 10 pixels tall. Doubling lifted the smallest book test from 9,711 to 13,554 words. Tripling added nothing more, and doubling text that was already big enough read the same or worse.
How do I set the DPI in Tesseract?
Pass the true value with --dpi, as in tesseract scan.png out --dpi 200. The setting -c user_defined_dpi=200 gave the same output. In pytesseract, use config="--dpi 200". A file with its dpi in the metadata needs no flag.
What does Estimating resolution as mean in Tesseract?
The image has no usable dpi, so Tesseract works at 70 dpi and prints a guess from the size of the text. On 300 dpi invoices the guesses ran from 381 to 484, and it read 460 numbers of 2,418. With --dpi 433, the median guess, the same pages read 2,072.
Also in this set: how to improve Tesseract accuracy, pytesseract PDF to text, tessdata_best vs tessdata_fast, Tesseract batch processing, Tesseract digits only, Tesseract psm modes, Image preprocessing for OCR.
How this was made: Tack, our AI personality, ran these tests on
October 31, 2026, and copied every result above from those runs. Apple
M5 Max, Homebrew Tesseract 5.5.3 with its default English model
(tessdata_fast 4.1.0), pytesseract 0.3.13, Pillow 13.0.0, Python 3.13,
8 workers, OMP_THREAD_LIMIT=1. CPU figures are one run
each. Every Tesseract setting but the dpi stayed at its default. The
book pages are the 60 validated Wikisource pages of the
tessdata page, minus the 5
handwritten ones, scored the same way, and the full size run gave the
same 14,310 words of 15,622 on all 60. Their files are 1,920 pixel
wide JPEGs from Wikimedia's DjVu renders. Small letter height for a
book is the median box of its confident words with no tall or hanging
letters, which can read a pixel high. For the invoices it comes from
each font's own x-height: 4.7 to 5.8 pixels at 72 dpi, 19.4 to 24.3 at
300, and 38.8 to 48.6 at 600. The invoices are the 72 made-up pages
and the row scorer of the
confidence score page,
drawn again at 1,200 dpi with blur scaled to stay the same physical
size. Rough noise is 10 to 16 gray levels on each pixel, the same at
every dpi. Black and white is a fixed cut at 128 with no dithering.
Every Python and shell block ran as printed on copies of those files.
The scripts and their output are in our private repo. Sources, read
October 31, 2026:
Improving the quality of the output
and the
old FAQ
(tessdoc), and at Tesseract 5.5.3
the man page,
baseapi.cpp,
pagesegmain.cpp,
input.cpp
and
publictypes.h. We make the Apify Actors. Tesseract, pytesseract, Pillow and
Wikisource are free projects we do not make.