I spent two hours confused by this before I thought to check img.dtype. I was building evaluation pipelines for my resizing disagreement research and getting PSNR values that looked plausible but were, in hindsight, completely wrong. The dangerous kind of wrong — not “obviously broken,” just “silently incomparable.”
PSNR is defined as:
For uint8 images, MAX = 255. For float32 normalized to [0, 1], MAX = 1.0. The difference between and is about 48 dB. That’s not a rounding error. That’s a chasm.
import numpy as np
from skimage.metrics import peak_signal_noise_ratio
img_uint8 = np.random.randint(0, 256, (256, 256, 3), dtype=np.uint8)
noise = np.random.randint(0, 6, (256, 256, 3), dtype=np.uint8)
noisy_uint8 = np.clip(img_uint8.astype(int) + noise, 0, 255).astype(np.uint8)
img_f32 = img_uint8.astype(np.float32) / 255.0
noisy_f32 = noisy_uint8.astype(np.float32) / 255.0
psnr_uint8 = peak_signal_noise_ratio(img_uint8, noisy_uint8)
psnr_f32 = peak_signal_noise_ratio(img_f32, noisy_f32)
print(f"PSNR (uint8): {psnr_uint8:.2f} dB") # ~40 dB
print(f"PSNR (float32): {psnr_f32:.2f} dB") # ~-8 dB
Same image pair. Same pixel relationships. One reports 40 dB (looks great), the other reports negative 8 dB (looks catastrophic). The float32 version can go negative because MAX = 1.0 makes the ratio tiny when MSE is even moderate.
What’s happening: skimage.peak_signal_noise_ratio infers data_range from the input dtype. For uint8 it assumes 255; for float32 it assumes 1.0. The MSE stays proportional, but the MAX term shifts everything by ~48 dB.
You can force consistency:
psnr_a = peak_signal_noise_ratio(img_uint8, noisy_uint8, data_range=255)
psnr_b = peak_signal_noise_ratio(img_f32, noisy_f32, data_range=1.0)
The rule: always know your dtype, always set data_range explicitly. A “PSNR of 35 dB” is meaningless without knowing whether it was computed on [0, 255] or [0, 1] data. This bites hardest in pipelines where model inference runs in float32 but image I/O happens in uint8 — if you forget to normalize one of the two images, your metrics reflect the type mismatch, not actual quality.