I got burned by this during my metrics research , and understanding it deeply is what eventually let me spot a dimension swap in clean-fid . It might be the single most common source of silent failures in image processing code.
from PIL import Image
import numpy as np
img = Image.open("photo.png")
print(img.size) # (1920, 1080) — width, height
print(np.array(img).shape) # (1080, 1920, 3) — height, width, channels
PIL gives you (width, height) — the way you’d describe a monitor resolution. NumPy gives you (height, width, channels) — rows first, because arrays are row-major. Same image, flipped convention.
The classic failure:
target_shape = some_array.shape[:2] # (height, width) = (256, 512)
resized = img.resize(target_shape) # PIL expects (width, height), so this silently produces (512, 256)
# correct:
resized = img.resize((target_shape[1], target_shape[0]))
No error. No warning. Wrong dimensions flowing through your pipeline. And here’s the part that makes this bug persistent: for square images (which are everywhere in ML — 224x224, 256x256, 299x299), the swap is completely invisible.
That last detail is what matters. This exact pattern caused the issue I found in clean-fid, where a reshape had swapped dimensions that went undetected because FID computation defaults to 299x299 images. Square inputs masked the bug. It was only visible with non-square inputs, which almost nobody tested with.
The fix is to be annoyingly explicit at every boundary:
h, w, c = arr.shape # NumPy convention
resized = img.resize((w, h)) # PIL convention
If you see .resize() anywhere near .shape, check the order. And test with non-square images. Always.