Quick answer: Pygame surfarrays are (width, height, 3) — column-major. Most image libraries give you (height, width, 3). Transpose axes 0 and 1 before blit_array.

Loading a numpy image (from PIL, OpenCV, etc.) and calling pygame.surfarray.blit_array raises a shape mismatch — the axes are in the wrong order.

Pygame's Axis Convention

Pygame surfarrays index as arr[x][y] — width first. A surface of size (800, 600) needs an array of shape (800, 600, 3). Numpy image loaders almost always produce (600, 800, 3) — rows (height) first.

Transpose Before Blitting

import numpy as np

# img is (height, width, 3) from PIL/OpenCV
img_wh = np.transpose(img, (1, 0, 2))   # -> (width, height, 3)
pygame.surfarray.blit_array(surface, img_wh)

transpose(1, 0, 2) swaps the first two axes and leaves the color channel. Shape now matches the surface.

OpenCV Also Flips Color Order

OpenCV gives BGR, not RGB. After transposing, also reverse the channel axis: img_wh = img_wh[:, :, ::-1] — or convert with cv2.cvtColor first.

make_surface as an Alternative

pygame.surfarray.make_surface(arr) creates a correctly-sized surface from the array — but it still expects (width, height, 3), so transpose first either way.

Verifying

The image blits without a shape error and isn’t rotated/transposed visually. Colors are correct (RGB, not BGR).

“Pygame surfarrays are width-first. Transpose your height-first image arrays before blit_array.”

A rotated-looking image is the tell-tale sign you forgot the transpose — the data’s there, just indexed the wrong way.