Round-trip PSNR sorted eight upscalers into tiers, then put EDSR in the GAN band
A file comes back from an upscaling stage at 1024 wide and there is no clean original behind it. All you still have is the small blurry input it was made from, which is the opposite of what PSNR and SSIM want. One check
A file comes back from an upscaling stage at 1024 wide and there is no clean original behind it. All you still have is the small blurry input it was made from, which is the opposite of what PSNR and SSIM want. One check works from that input: put the upscale back down by the same factor, then compare the two. Two lines, no ground truth.
Three samples went through a run on 2026-11-12, each sharp to begin with at 1024Γ1024: a portrait, a landscape, and a page of text built for the purpose, with Chinese body copy from 40 px down to 11 px, serif Latin at four sizes, and a grid of 1 px rules. Pillow 11.3.0 LANCZOS shrank them to 512 or 256, so the blur here is manufactured. A real blurry photo also carries defocus, a shaken hand, high-ISO noise and rounds of JPEG, none of which is in these files.
Eight methods put them back to 1024. Three are plain interpolation out of Pillow: nearest, bicubic and Lanczos. Four are classic CNN super resolution through OpenCV 5.0.0 dnn_superres with the official pretrained ESPCN, FSRCNN, EDSR and LapSRN weights. The eighth is the GAN tier, and every GAN figure here comes from one preset on ImgIng https://imging.ai/ and nothing else: the fastest of the five it offers, whose panel names Real-ESRGAN General x4v3. The other four were not run. SSIM is scikit-image 0.24.0, on 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.
The check is two lines of Pillow
Here lr is the small input, sr.png is what came back, and psnr() is the usual ten-log-ten of mean squared error, defined a few lines up.
# on a live file you only have the upscale and the small input it came from
back = Image.open("sr.png").resize(lr.size, Image.LANCZOS) # back down by the same factor
print("round-trip PSNR %.2f dB" % psnr(np.array(lr), np.array(back)))
Wrapped in a loop over the eight results for the text sample at 4Γ, each one compared with the 256-pixel file it grew out of and printed after its method name, the run gave this, nothing removed:
nearest 36.35 dB
bicubic 34.68 dB
lanczos 36.38 dB
espcn 31.51 dB
fsrcnn 32.10 dB
edsr 27.86 dB
lapsrn 32.12 dB
GANθΆ
ε 26.45 dB
The last label is the GAN tier, the rest are my script's short names. Lower means more was put in: Lanczos changed the least at 36.38 dB, the CNN models land near 31 to 32, and the floor is edsr at 27.86 with the GAN tier at 26.45.
Every content type needs its own threshold
The other two samples at 4Γ:
| method | portrait | landscape |
|---|---|---|
| nearest | 47.79 | 45.10 |
| bicubic | 46.81 | 44.13 |
| lanczos | 48.73 | 46.28 |
| espcn | 42.58 | 40.02 |
| fsrcnn | 42.50 | 40.09 |
| edsr | 46.11 | 44.56 |
| lapsrn | 42.97 | 40.57 |
| GAN tier | 34.89 | 33.55 |
Down each column the order repeats, interpolation on top and the GAN tier far below, but the figures behind it are nowhere near each other. Interpolation runs 46.81 to 48.73 on the portrait, 44.13 to 46.28 on the landscape and 34.68 to 36.38 on the text page; classic CNN runs 42.50 to 46.11, then 40.02 to 44.56, then 27.86 to 32.12. Every row sits lower on the text page than on the portrait, by 8.44 dB for the GAN tier and by 10.4 to 18.3 dB for the other seven. Pick one global band and that whole column reads as an anomaly. The families overlap inside a column too, EDSR at 44.56 on the landscape sitting above bicubic's 44.13; the only band that keeps clear of its neighbours in all three columns is the GAN one, 8 to 14 dB under the interpolation rows beside it.
Two readings on my side, neither of them tested in this run. Coming back down with LANCZOS probably undoes much of a smooth upscale, and nearest neighbour at 47.79 on the portrait points the same way: it never invented a pixel value to begin with. The text column may sit so low because thin strokes and 1 px rules lose more in the shrink than skin does.
EDSR scored like a GAN for the opposite reason
The 27.86 dB on the text page is why this check cannot be read alone. It lands 1.41 dB above the GAN tier's 26.45 on that sample with nothing in between, so any threshold files the two together as content a model has redrawn. Against the original they are nothing alike. EDSR there is the worst pair in the whole run, PSNR 18.74 and SSIM 0.8018, below nearest neighbour's 19.27 and 0.8176. What it added is a second faint stroke beside the first, with a ghost trailing each Latin letter on the signage. That is ringing, not recovered detail.
The GAN tier's 26.45 comes from the other direction: whole-image SSIM 0.9048 on that page, highest of the eight, against 0.8177 for Lanczos, with PSNR 19.68 a touch under Lanczos at 19.97. It redrew the strokes sharp and placed them a hair off, so per-pixel error and structural similarity went up together.
So the round trip says how much was changed and never whether the change was right. It still tells you which files someone should go and look at, and it keeps working on output from a service you did not write. Where the original is around, read PSNR and SSIM beside it, since those two ranked this sample in opposite orders. Three samples, one machine and one run per cell is all that is behind this, and I have not checked what a box filter on the way back does to the bands. Keep the small input next to the upscale and store the round-trip number with it.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.