Marginal Musings

OpenCV and PIL Agree on (width, height) — Then Disagree on Everything Else

Author
Shlomo Stept
Published
Updated

I hit this face-first while building pipelines for my resizing disagreement research . OpenCV and PIL both take (width, height) for resize — a rare moment of agreement between two libraries that seem to disagree on principle.

The problem is everything around that agreement.

import cv2
from PIL import Image
import numpy as np

img_cv = cv2.imread("photo.png")
print(img_cv.shape)  # (1080, 1920, 3) — H, W, C in NumPy

resized_cv = cv2.resize(img_cv, (256, 128))  # width=256, height=128
print(resized_cv.shape)  # (128, 256, 3)

img_pil = Image.open("photo.png")
resized_pil = img_pil.resize((256, 128))
print(np.array(resized_pil).shape)  # (128, 256, 3) — same

Fine so far. The trap springs when you extract dimensions from a NumPy array and feed them back:

h, w = img_cv.shape[:2]

bad = cv2.resize(img_cv, (h, w))   # height where width should be — no error, just a distorted image
good = cv2.resize(img_cv, (w, h))  # correct

For square images this produces identical output. For non-square images, silent transposition. No exception, no warning, just wrong data. This is the same class of bug I found in clean-fid — dimension swaps hiding behind square defaults.

Then there’s the color channel situation. OpenCV loads as BGR. PIL loads as RGB. (Why BGR? Historical reasons involving camera hardware from the 1990s, as far as anyone can tell.)

img_cv = cv2.imread("photo.png")              # BGR
img_pil = np.array(Image.open("photo.png"))   # RGB

print(np.array_equal(img_cv, img_pil))              # False
print(np.array_equal(img_cv[:,:,0], img_pil[:,:,2])) # True — blue channels match

I’ve mixed this up enough times that I now convert at the boundary and never think about it again.

AspectPILOpenCV
Resize argument(width, height)(width, height)
.size / .shape(W, H)(H, W, C) via NumPy
Color orderRGBBGR

The rule I follow: convert BGR to RGB at every library boundary. Never pass .shape[:2] directly to a resize function without swapping. Comment every dimension tuple — # (w, h) or # (h, w) — until it becomes reflex. These conventions are easy to remember individually. They compound when you’re writing code at speed, and the failures are always silent.