A 3x3 median cut my OCR errors from 11 to 2, then broke a half-size scan
I wanted to know whether the 3 in cv2.medianBlur(img, 3) is safe to keep as a constant before OCR, so I swept the kernel on one scanned book page. On the page at its original size, a 3Ã3 median was the best thing I tried
I wanted to know whether the 3 in cv2.medianBlur(img, 3) is safe to keep as a constant before OCR, so I swept the kernel on one scanned book page. On the page at its original size, a 3Ã3 median was the best thing I tried: Tesseract went from 11 character errors to 2. On the same page scaled to half width, the same 3Ã3 median took it from 4 errors to 28. The filter did not change. The letters got smaller.
The page is page 8 of H. G. Wells's The Time Machine (William Heinemann, 1895), a clean single-column print scan from Wikimedia Commons at 1516 px wide. The reference text is the validated English Wikisource transcription of that page, 1,002 characters after normalising. "Errors" below means character edit distance against that text, so 11 errors is 98.9% and 90 errors is 91.0%. Everything ran once on an Apple M4 Mac with Tesseract 5.5.3 and OpenCV. One page and one run each, so read these as the shape of a curve, not as accuracy to expect on your documents.
The full size page liked a small median
The sweep runs on the untouched page, no added noise or rotation. The m0 line is the unfiltered run, done just before the loop with tesseract page.jpg m0 -l eng.
$ for k in 3 5 7 9; do python3 -c "import cv2;cv2.imwrite(\"m.png\",cv2.medianBlur(cv2.imread(\"page.jpg\",0),$k))"; tesseract m.png m$k -l eng 2>/dev/null; python3 cer.py truth.txt m$k.txt; done
m0.txt: įžčūč·įĶŧ 11 / 1002 åįŽĶïžåįĄŪį 98.9%
m3.txt: įžčūč·įĶŧ 2 / 1002 åįŽĶïžåįĄŪį 99.8%
m5.txt: įžčūč·įĶŧ 6 / 1002 åįŽĶïžåįĄŪį 99.4%
m7.txt: įžčūč·įĶŧ 14 / 1002 åįŽĶïžåįĄŪį 98.6%
m9.txt: įžčūč·įĶŧ 90 / 1002 åįŽĶïžåįĄŪį 91.0%
The scoring script prints its labels in Chinese: įžčūč·įĶŧ is edit distance, åįŽĶ is characters, åįĄŪį is accuracy. The curve has a clear bottom at 3, is roughly back to baseline by 7, and falls off at 9. Otsu plus a 5Ã5 closing was far worse at 716 errors, since closing on black-on-white text erases thin strokes.
Half the size and the same kernel hurts
Then I resized the page to 758 px wide to imitate a smaller font or a lower-resolution scan and ran the same filters.
| Median kernel | Full size, 1516 px | Half size, 758 px |
|---|---|---|
| none | 11 | 4 |
| 3Ã3 | 2 | 28 |
| 5Ã5 | 6 | 142 |
| 7Ã7 | 14 | 765 |
At half size there is no kernel that helps. The 5Ã5 that was harmless at full size drops accuracy to 85.8%, and Otsu plus a 3Ã3 closing produced an empty output file. The first row bothers me most. Unfiltered, the half-size page had 4 errors and the full-size page had 11. I don't have an explanation for that, and with one page I'm not going to invent one. It does mean the safe default at half size was no filter at all.
My guess at why the best kernel moves with scale
Everything in this section is my inference. I never measured a stroke in pixels on this page, so treat it as a hypothesis to test. The idea starts from scale, not from the filter. Shrinking the page to half width while keeping a 3Ã3 window should behave roughly like running a 6Ã6 window on the full-size page, because the window now covers twice as much of each letter. If that holds, the half-size column is just the full-size curve shifted toward bigger kernels. The numbers fit that loosely: half-size 3Ã3 sits at 28, between full-size 7Ã7 at 14 and 9Ã9 at 90. A median is a local majority vote, and a hairline that fills only a small share of the window loses that vote. At full size a 3Ã3 probably only outvotes paper grain and speckle, which would fit 11 dropping to 2. A Chinese page from the 1986 reissue of the official Simplified Characters Table points the same way. With the macOS built-in recognizer, a 3Ã3 median moved it from 5 to 4 errors out of 557 characters, and 9Ã9 went to 178. From 7Ã7 on, thin horizontal strokes, dots, small radicals and enumeration commas went first.
Recognizers disagree about preprocessing too
What sits after the filter matters too. The macOS built-in recognizer barely reacted to tilts from 0 to 5° and got slightly worse after Otsu in every condition I tried, while Tesseract needed deskewing and denoising. The third recognizer was ImgIng https://imging.ai/ , which runs PP-OCR models inside a desktop browser, on the same Mac. It was the most tilt sensitive of the three. At 3° its line order got scrambled and the Chinese page lost two lines. At 5° it lost about half of the Chinese page, and the interface still showed the job as complete with no warning. After denoising and deskewing, it came back to 2 errors. I didn't sweep kernels through ImgIng.
Pick the kernel per resolution
The kernel shouldn't be a constant. Before a batch goes through OCR, transcribe one representative page by hand, or take an existing transcription, and run the loop above at the resolutions the batch actually contains. For mixed input sizes my plan, untested beyond this page, is to bucket by text line height in pixels and keep a kernel per bucket, with "no filter" as an option. The cheap check is to halve your test page and rerun the sweep. If the best kernel moves, you have my problem.
Sources: page 8 of H. G. Wells, The Time Machine (William Heinemann, London, 1895), scan by University of Illinois via HathiTrust, Wikimedia Commons, public domain. Reference text from English Wikisource, CC BY-SA 4.0. Chinese page from the National Language Commission's 1986 reissue of the Simplified Characters Table, Internet Archive, Public Domain Mark 1.0. The half-size and tilted versions are my own.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes â full credit and traffic to the original publisher.