A pre-OCR gate that decides whether to upscale a screenshot needs one number from the image itself: how tall the text is in pixels. The two cheapest ways to get it are a horizontal projection profile and connected components, both a few lines of OpenCV. On clean Chinese renders both stayed within 2 px of the true height up to 30 px; above that projection ran 3 to 5 px high and components 1 to 3 px. On English, projection reported the whole line, about 1.37 times the cap height. On a table screenshot with borders, projection returned 559 px while connected components returned 23.
Why the height decides the upscale
The cutoff only matters because accuracy falls off a cliff below it. In this test, Chinese text rendered at 12 to 40 px came back at 99.64% to 100% character accuracy from two recognizers: macOS's built-in text recognition in accurate mode, and ImgIng https://imging.ai/ image text recognition running in a desktop browser (it downloads its recognition model, about 31.2 MB, on first use). At 60 px macOS still read 99.27% on Songti, the misses being dropped characters at a line end. At 8 px macOS dropped to 92.0% for Songti and 88.0% for Heiti, and at 6 px it was 1.82% and 6.91%. So a rule like "upscale when text is under 12 px" is reasonable for this sample, and an estimator that is wrong by 2 or 3 px right around 12 can push an image to the wrong side of it.
The samples are deliberately clean, which limits everything below. One self-written Chinese passage of 275 characters and one English passage of 452 characters were rendered black on white, single column, single font (Songti SC, Heiti SC, Times New Roman), at nine heights from 6 to 60 px. Text height has a strict definition here: the ink bounding box height of the glyph 国 for Chinese and of a capital H for English. Everything ran once on an Apple M4 Mac with OpenCV and Pillow, with no compression, noise or background.
Two estimators in a few lines each
Projection binarizes with Otsu, marks every row that contains ink, treats each run of inked rows as a text line, and takes the median run height. Connected components also starts from Otsu, labels 8-connected blobs, drops anything under 3 px of area, and takes the 90th percentile of blob heights. The percentile choice is deliberate. A Chinese character often splits into several separate parts, so the median component is a fragment and would come out too small.
Chinese stayed within 2 px up to 30 px
| True height (px) | 6 | 8 | 10 | 12 | 16 | 20 | 30 | 40 | 60 |
|---|---|---|---|---|---|---|---|---|---|
| Songti, projection / components | 6 / 6 | 9 / 8 | 10 / 10 | 12 / 12 | 17 / 17 | 21 / 20 | 32 / 31 | 43 / 41 | 65 / 63 |
| Heiti, projection / components | 7 / 7 | 8 / 8 | 11 / 11 | 14 / 13 | 17 / 17 | 21 / 20 | 32 / 31 | 43 / 42 | 65 / 62 |
Up to 20 px the two methods are within a pixel of each other, and up to 30 px both are within 2 px of the truth. At 40 and 60 px projection runs about 8% high, because some characters and punctuation in a line reach above or below 国. With a cutoff of "under 12 px means upscale", every estimate in this table landed on the same side as the true height, including Heiti at 12 px where projection said 14.
English made projection measure the wrong thing
| True cap height (px) | 6 | 8 | 10 | 12 | 16 | 20 | 30 | 40 | 60 |
|---|---|---|---|---|---|---|---|---|---|
| Projection | 8 | 10 | 13 | 17 | 22 | 27 | 40 | 55 | 82 |
| Connected components | 6 | 8 | 10 | 12 | 17 | 21 | 31 | 42 | 63 |
Projection measures a line, and an English line includes ascenders and descenders, so it reported about 1.37 times the cap height, a little less at the smallest sizes. Connected components with the 90th percentile stayed within 3 px of the cap height. The gap shows up in decisions. Text with a true cap height of 10 px reads as 13 under projection, which a 12 px cutoff would leave alone. In the same test, Tesseract 5.5.3 made 3 errors on the 10 px English sample and 0 after a bilinear upscale to around 32 px. A projection-based gate would have skipped that fix; a components-based one, reading 10, would not.
A bordered table broke projection entirely
On a table screenshot with full borders, projection found exactly one inked row segment and estimated 559 px. The vertical rules put ink in every row, so the blank gaps between text lines never appear. Connected components returned 23 on the same image. I don't have a ground-truth height for that table, so 23 is plausible but has not been checked the way the renders were. If a projection result comes back as one huge segment, that is a cheap signal to strip lines first or fall back to components.
Measured versus inferred
Everything in the tables is measured, once, on clean renders. Inferred and not tested: that the median of components would underestimate Chinese; that real screenshots with JPEG artifacts, colored backgrounds or mixed fonts will make both estimators worse; and that components would behave on a screenshot mixing Chinese and English in one line. The last one is the case I care about most for user uploads, and I have no number for it yet.
For a gate in front of OCR I'd use connected components with the 90th percentile, compare its result against projection, and treat a large disagreement or a single line segment spanning most of the image as "probably a table or a layout I can't size", which goes to line removal or skips the upscale step entirely.


