Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

One text upscale won on SSIM and lost on PSNR, so let the downstream pick the metric

On 2026-11-12 one run pushed eight upscaling methods through the same loop test, and the text sample came back with two answers. Measured per pixel, plain Lanczos won: PSNR 19.97 dB against 19.68 for the fast GAN tier. M

On 2026-11-12 one run pushed eight upscaling methods through the same loop test, and the text sample came back with two answers. Measured per pixel, plain Lanczos won: PSNR 19.97 dB against 19.68 for the fast GAN tier. Measured on structure, the GAN tier won by a wide margin: SSIM 0.9048 against 0.8177. Same pair of files, same script, two metrics that are both standard, and they ranked the methods in opposite orders. Everything below is read off the outputs, so treat it as one outside reading of a table rather than a claim about how any of these models work inside.

The numbers came out of a loop: a clean 1024×1024 original goes down to 512 or 256 with Pillow 11.3.0 LANCZOS, every method puts it back to 1024, and the result is compared against the original it was made from. Three samples, two scales, seven methods at 2× and eight at 4×, so 21 plus 24 is 45 rows. The interpolation side is Pillow nearest, bicubic and Lanczos. The classic CNN side is OpenCV 5.0.0 dnn_superres with the official ESPCN, FSRCNN, EDSR and LapSRN weights. SSIM comes from scikit-image 0.24.0, PSNR from a few lines beside it. Every GAN number here comes from a single tier of ImgIng https://imging.ai/ , the fast preset whose panel names Real-ESRGAN General x4v3, and only that tier was run: one Apple-silicon Mac, desktop Chromium, one run per cell. Portrait and landscape samples are CC0 images from Wikimedia Commons by William Stitt and Bonnie Moreland; the text sample is my own.

Opposite orders on the same text sample

PSNR asks how far each pixel moved. SSIM asks whether the local structure still looks like the structure in the original. The GAN tier redraws a stroke sharp and lands it a hair off from where it used to be, so subtracting pixel by pixel makes that displacement expensive, while a structural comparison puts it closer to the original than anything else in the table. Neither number is wrong. They answer different questions, and on this sample the two questions disagree.

EDSR x4 is the other half of the story. On the same text sample it scored 18.74 / 0.8018, the worst pair in the whole run, below even nearest neighbour at 19.27, and the failure is visible as a second faint stroke beside the first. The larger model bought nothing here, on this one text sample with this set of pretrained weights.

Whoever reads the pixels picks the metric

So the useful question is not which of the two metrics is correct. It is what happens to the image after the stage finishes.

What the image is for next Which number to read What this run showed
Text somebody will read back word for word per-pixel error inside the text region the fast tier's body-text region came in at 17.81 dB, lowest of the eight, while its whole-image SSIM was the highest at 0.9048
A person glancing at a photo on screen structural similarity 0.9048 against 0.8177 on the same text sample
A face whose details have to be recognised both, which here happened to agree the fast tier's portrait was 30.28 / 0.8145, against Lanczos at 32.50 / 0.8523
A flat area such as sky or paper neither, and skip the upscale all eight methods landed between 46.57 and 51.06 dB in the sky region

Both region figures come from rectangles drawn by hand on the original, one over a block of body text and one over open sky, not from any automatic segmentation. The first row is the interesting one. The version that looks clearest on screen is the version that is least trustworthy for checking a word against the original, and a single quality column would have hidden that completely. ImgIng's own notes, read on 2026-11-12, draw the same line: the output suits visual enhancement, not verification against the original text.

Record both numbers and the consumer they are for

That leaves something cheap to do inside the stage. Write down both numbers, the downstream they were judged for, and enough about the pixels to say later where they came from. A record carrying one number is where the trouble begins. Six months on, a 0.90 sitting in a column called quality says nothing about whether the text in that image can be quoted.

# shape of the record, not the measuring script; the numbers above come from one loop-test run
record = {
    "src": "invoice_0412.png",
    "method": "gan_sr",          # interpolation | classic_cnn | gan_sr
    "scale": 4,
    "weights": "real-esrgan-general-x4v3",
    "psnr": 19.68,
    "ssim": 0.9048,
    "judged_for": "preview",     # preview | text_readback | archive
    "original_kept": True,
}

The limits are worth stating plainly, because a table of 45 rows invites over-reading. Three samples, one machine, one run per cell, one tier out of five on the GAN side, and the low-resolution inputs were produced by downscaling clean originals. A genuinely blurry photo also carries focus error, hand shake, high-ISO noise and the damage of being re-encoded by a few apps on the way, so none of this predicts what a method will do to that photo. With no repeats, no cell here has an error bar either. And how much of the GAN tier's per-pixel penalty is pure displacement that a small alignment step would give back, this run does not say.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.