Skip to content
Chapter 3 of 7 · Image inputsResolution and per-field legibility

17 min read

Resolution and per-field legibility

On this page

A decision to send images at a lower resolution is a decision about two quantities, and only one of them is easy to obtain. The token saving comes straight out of the arithmetic in chapter 2, where an image is priced on its pixel dimensions after the provider's own resize. What that saving costs in answers has to be measured on your own documents, and the measurement is worth doing properly because the loss is wildly uneven across the fields a pipeline extracts.

This chapter is the protocol for that measurement: what to sample, what ground truth you need, how to run two resolutions against each other without accidentally comparing an image with itself, and how to read the result. It then explains why the ordering the measurement produces comes out roughly the same on everybody's documents, which is a fact about glyph geometry rather than about models, and gives the rough pixel threshold below which extraction stops being reading.

The protocol

Take a real sample of your own documents, not a curated one — fifty pages pulled at random from last month's traffic, including the ones that are slightly skewed, slightly grey, and photographed rather than scanned. For each page, write down by hand every field your extraction produces and what its correct value is. That ground truth is the expensive part and the part with no shortcut; forty pages of it is a couple of hours, and it is the asset that makes every subsequent resolution decision cheap.

Then run each page through the identical prompt at two resolutions and diff the extracted fields against the truth, field by field. Not a single accuracy number for the page: a per-field score. The whole point of the exercise is that the fields degrade at wildly different rates, and any metric that averages over them hides exactly the thing you are trying to see.

python
from PIL import Image

FIELDS = ["doc_type", "supplier", "invoice_total", "row_count",
          "line_item_amounts", "chart_trend", "chart_axis_ticks",
          "footnote_text", "stamp_date"]

def variant(path: str, scale: float) -> Image.Image:
    im = Image.open(path)
    if scale == 1.0:
        return im
    return im.resize((round(im.width * scale), round(im.height * scale)), Image.LANCZOS)

def score(pages, truth, scale):
    hits = {f: 0 for f in FIELDS}
    for page in pages:
        got = extract(variant(page, scale))       # your existing extraction call
        for f in FIELDS:
            if got.get(f) == truth[page][f]:
                hits[f] += 1
    return hits

full, half = score(pages, truth, 1.0), score(pages, truth, 0.5)
for f in FIELDS:
    print(f"{f:20} {full[f]:>3}/{len(pages)}   {half[f]:>3}/{len(pages)}")

Before trusting a single row of that output, verify one thing: that the two variants are actually different by the time the model sees them. The provider resizes anything above its cap before billing or inference, as chapter 2 described, and that cap can erase your experiment. A page scanned at 300 DPI is 2550×3300 pixels; halved it is 1275×1650. Under a 1568-pixel long-edge cap, the full version arrives as 1212×1568 and the halved version arrives as 1211×1568. They are the same image. You will run forty pages, see identical scores in both columns, conclude that resolution does not matter, and ship a much more aggressive compression that does. Print the post-resize dimensions of both variants and confirm they differ before you interpret anything.

That "same image" needs one condition, and the condition sharpens the warning rather than softening it. Both variants land on the same dimensions, a pixel apart on the short edge, having got there by different routes: the full version is resampled once, by the provider, and the halved version twice, once by the resize call above and again by the provider. Two passes down to a given size lose slightly more fine detail than one, so what the columns compare is the cost of an extra resampling step at identical output dimensions — a real effect and a tiny one. The quantity the experiment was built to measure is absent from its own output.

One more control is worth building in from the start. Model responses vary between identical runs, so a field that scores 33/40 at half resolution might score 35 or 31 on a rerun with nothing changed.

That variation has a cause, and the cause tells you which rows will move. The step that picks each piece of a model's answer is permitted to be random: rather than always taking the most likely continuation, it draws from a distribution over the candidates. Two identical requests are therefore two draws and not one measurement repeated, and a field whose evidence sits near the boundary between two readings — a digit whose pixels support an 8 and a 3 about equally — falls one way this run and the other way next, with nothing about the input changed. Fields with margin are stable across reruns, and the fields at the edge of legibility are exactly the ones that wander.

Back at the level of the experiment: if you run each of the two resolution variants once, you cannot tell a two-page difference caused by resolution from a two-page difference caused by sampling. Run each page at each resolution three times and record the majority answer, or at minimum run the full-resolution baseline twice and see how far it moves against itself. That self-difference is your noise floor, and any resolution effect smaller than it is not a finding.

Two resolutions is the smallest experiment that answers the question in front of you; a sweep is what you want once you are choosing a target rather than approving a proposal. Scales of 1.0, 0.75, 0.5 and 0.35 on the same forty pages cost four times as many calls and produce a curve per field instead of a pair of points, which lets you find the knee — the scale at which a field falls off — rather than discovering only that a specific proposed scale was too aggressive. The knees cluster: everything set in 8- to 10-point type tends to break within one step of each other, and everything structural survives all four.

What forty pages actually showed

Here is the output of that protocol on forty invoice pages scanned at 150 DPI, so the full variant is 1275×1650 and arrives at the model as roughly 1211×1568 — near enough untouched — while the half variant is 638×825 and passes through the cap unchanged. Twenty-two of the forty pages carried a chart.

FieldTypical feature sizeFull (≈142 DPI)Half (≈75 DPI)
Document type: invoice or credit notewhole-page layout40 / 4040 / 40
Supplier name in header12 pt40 / 4040 / 40
Invoice total14 pt bold40 / 4040 / 40
Chart trend directionbars ~200 px tall21 / 2221 / 22
Number of line-item rowsruled table40 / 4038 / 40
Line-item amounts10 pt39 / 4033 / 40
Chart axis tick values9 pt19 / 228 / 22
Footnote text8 pt36 / 409 / 40
Handwritten date stampvariable, ~6 pt strokes31 / 4020 / 40

Read the table top to bottom and the ordering is almost perfectly by feature size. Everything whose meaning is carried by large shapes — which page this is, where the header sits, whether the bars go up or down — is untouched. Everything whose meaning is carried by fine stroke detail collapses, and the collapse is abrupt rather than gradual: the footnote goes from 90% to 22% while the total does not move at all.

The summary a reader reaches for here is that halving the resolution costs about half the accuracy, and it is tempting because most engineering trade-offs do behave that way — half the bitrate gives somewhat worse audio, half the sample gives somewhat wider error bars. Averaged across the nine fields the drop is real but nothing like a halving, and the average is the wrong instrument precisely because it lands somewhere no row does. Row by row it describes nothing that happened: four of the rows lose not a single page, and the footnote row loses three quarters of what it had.

Why the ordering comes out the same on everyone's documents

The mechanism is arithmetic about glyphs, and it combines two quantities that measure different things. Point size is a physical measure — one point is 1/72 of an inch — so an 8-point capital is eight seventy-seconds of an inch of paper, a height fixed when the page was printed. Dots per inch is the other half of the conversion, counting how many pixels the scanner produced per inch of paper it crossed. Multiplying a height in inches by a count of pixels per inch leaves a count of pixels, because the inches in the first quantity cancel the inches underneath the second, and that cancellation is what licenses this multiplication rather than some other way of combining the two figures.

Carrying it out: at 150 DPI an 8-point capital is 8 × 150/72 ≈ 16.7 pixels tall, its lowercase x-height is around 8 pixels, and the strokes making up each letter are one and a half to two pixels wide. Halve the resolution and each of those figures halves with it, since the letter on the paper is the same letter and only the pixels per inch have moved: the capital is 8 pixels, the x-height is 4, and the strokes are under a pixel.

A stroke under a pixel wide is where the ordering in the table comes from, and the reason is what a pixel is. A pixel holds one value for the whole square of the page it covers, so it cannot describe anything smaller than itself; the only report it can make about its square is an average over the whole of it. A black stroke covering a third of a pixel's area therefore does not come back as a thin black line inside a pale square. It comes back as one pixel a third of the way from white towards black, and where inside that square the stroke lay is unrecoverable, since one number was left behind and a single number cannot carry a position.

That smear is what destroys a letter: where two strokes are close together — the two verticals of an n, the bowl and stem of a b, the gap in an e — the smears merge and the letter loses the feature that distinguishes it from its neighbours. The word is still a word-shaped blob of the right length. It is no longer a specific word.

Naming the quantity that was lost makes the rest of the table predictable. Two strokes with a gap narrower than a pixel between them fall inside one average, where the gap contributes its whiteness and stops existing as a gap while the darkness of the region comes out much as before. What survives a downscale is how much ink was in a region; what is destroyed is where in the region it sat — and where it sat is the whole difference between an e and a c, or between the two verticals of an n and the single stem of an r. That is why the collapse is a cliff and not a slope: a distinguishing feature still wider than a pixel comes through every average intact, and once it is narrower it disappears into a single average along with every other feature of about that width, on every letter, at once.

Compare that to a bar in a chart. At 150 DPI a bar an inch and a half tall is 225 pixels; at half it is 112. The claim you are asking the model to make about it — this bar is taller than that one — needs perhaps three or four pixels of difference to survive, and it has a hundred.

The margin is that large because a bar and a glyph are being asked different kinds of question about the same picture. A bar is asked for an ordering between two lengths, and an ordering holds as long as the difference between them stays larger than the error in reading either; halving the resolution halves both lengths and their difference together, so the comparison comes through untouched. A glyph is asked for an identity among a few dozen candidates that differ from one another only in features one pixel wide. One question has a hundred pixels of headroom, the other has none, and the resize is indifferent to which one is being asked of it.

Chart structure therefore has enormous margin, while chart tick labels are 9-point text and sit in the same regime as the footnote, which is why the axis-ticks row in the table drops off a cliff while the trend row does not move. It is common to describe a chart as "surviving downscaling," and what survives is the geometry; the numbers printed on it do not.

The useful rule of thumb is a rough threshold on cap height — the height of a capital letter, which is the honest way to measure how large type actually renders, since point size describes the metal block rather than the ink — counted in pixels as the model receives them, after any provider resize. Above about 20 pixels, text is read reliably. Between 10 and 20 it is read most of the time, with errors concentrated on visually confusable characters — 8 against 3, 5 against 6, rn against m, 1 against l. Those particular pairs are confusable for the reason the mechanism predicts: each pair differs in one small feature — a closed upper bowl, a joined shoulder, a serif — and at that cap height the feature is a pixel or two across.

Below about 10 pixels, extraction stops being reading and becomes inference from context, which is when a footnote about payment terms comes back as a different, entirely reasonable footnote about payment terms.

That sentence is meant literally. With the distinguishing features averaged away, what reaches the model where a sentence used to be is a word-shaped region of roughly the right length in roughly the right place on a document of a recognisable kind, and the machinery that turns an image into text is machinery for producing likely continuations. Given almost no evidence about which words are present and a great deal about what sits at the foot of an invoice, it emits the sentence that most often occupies that position on that sort of page. The tempting reading of such a failure is that the model declined to read the footnote; what it did was read something else and report it in the register it uses for things it has read, which is why the output is fluent, correctly formatted, and offers a reviewer nothing to catch.

Convert that back into a scanning decision: to keep 8-point text above 20 pixels of cap height you need 180 DPI or better at the model, which given a long-edge cap of 1568 pixels on a US Letter page means about 142 DPI — so 8-point text is already marginal at full resolution, before anybody proposes shrinking anything.

Those two DPI figures are one multiplication run in opposite directions, and working them is what turns the last clause from a claim into something you can check. The requirement first: an 8-point capital occupies eight seventy-seconds of an inch, so putting 20 pixels inside that height demands 20 × 72/8 = 180 pixels per inch. What the cap delivers second: a US Letter page is eleven inches on its long edge, and a long-edge cap of 1568 pixels spreads 1568 pixels across those eleven inches, so dividing gives a little over 142 pixels per inch. The requirement sits above the ceiling, and the gap cannot be closed from the scanner end.

That arithmetic also disposes of the belief that scanning at a higher resolution protects the small type. A page scanned at 300 DPI is 2550×3300, far above the cap on its long edge, and it is resized to 1212×1568 before the model sees anything — near enough the 1211×1568 that a 150 DPI scan of the same page arrives as. The extra pixels were paid for, stored, and thrown away upstream of inference, and no scanning setting reaches past that cap.

The limits of what a resolution sweep measures

This kind of result is about your documents. Font choice moves the threshold — a humanist sans with open apertures survives a resize that destroys a condensed serif at the same point size. Scanner quality moves it, because a page with good contrast has more usable signal per pixel than a grey photocopy. Colour moves it: dark text on white survives far better than the pale grey small print that legal boilerplate is often set in. Rerun the measurement when the document mix changes, and treat a number from someone else's sample as a hypothesis rather than a finding.

The experiment compares two ways of shrinking the same whole page, and whole-page shrinking is not the only lever. Cropping keeps resolution and discards area: if the footnote lives in a fixed band across the bottom 15% of the page, sending that band as its own image at full scale, labelled the way chapter 1 described, costs a fraction of a full page and reads the 8-point type perfectly. Splitting a page into two overlapping halves is a middle path that keeps every glyph at full size while roughly doubling the image count. Both options change the shape of the cost, and neither shows up in an experiment that only varies a scale factor, so decide whether your document layout is stable enough to crop before you conclude that resolution is the only dial.

The experiment also says nothing about which failures matter. A field-level accuracy table is an input to a decision, not the decision. Losing 6 of 40 line-item amounts on a spend-analysis dashboard may be tolerable; losing 3 of 40 on an accounts-payable system that pays those amounts is not, and no aggregate score distinguishes the two. Score the fields, then let the people who own the downstream consequence weight them.

Per-field measurement against written-down ground truth is what turns a compression proposal into an engineering decision, and the measurement is not complicated. It costs an afternoon of labelling and a few hundred model calls, it produces a table that stays useful for as long as your document mix holds, and it is the only thing that tells you which of your fields are sitting one halving away from being invented rather than read.

What it does not tell you is whether the halving saves anything. The measurement above assumes the token cost falls roughly in proportion to the pixels removed, which is true under an area-based formula and false under tiled billing, where there are resizes that throw away a third of your pixels for no saving whatsoever.

Image inputs — read the current version at https://daviddominguez.dev/learn/en/image-inputs/resolution-and-per-field-legibility

Back to top