roboflow/supervision · error · ValueError

Expected shape (H,W), (H,W,3), or (H,W,4), got {image.shape}

Error message

Expected shape (H,W), (H,W,3), or (H,W,4), got {image.shape}.

What it means

Raised by cv2_to_pillow() in supervision.utils.conversion when the input NumPy array is not a 2-D grayscale image, a 3-D 3-channel BGR image, or a 3-D 4-channel BGRA image. The function converts OpenCV-convention arrays to Pillow images, so any other rank or channel count has no defined conversion. The message echoes the offending shape so you can see exactly which dimension is wrong.

Source

Thrown at src/supervision/utils/conversion.py:235

        >>> from supervision.utils.conversion import cv2_to_pillow
        >>> scene = np.zeros((10, 10, 3), dtype=np.uint8)
        >>> scene[:, :, 2] = 255
        >>> image = cv2_to_pillow(scene)
        >>> image.size
        (10, 10)
        >>> image.getpixel((0, 0))
        (255, 0, 0)

        ```
    """
    if image.ndim == 2:
        return Image.fromarray(np.ascontiguousarray(image))
    if image.ndim == 3 and image.shape[2] == 3:
        rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        return Image.fromarray(rgb_image)
    if image.ndim == 3 and image.shape[2] == 4:
        return Image.fromarray(np.ascontiguousarray(image[..., [2, 1, 0, 3]]))
    raise ValueError(f"Expected shape (H,W), (H,W,3), or (H,W,4), got {image.shape}.")

View on GitHub (pinned to 7f254d9784)

Solutions

  1. Squeeze spurious dimensions first: np.squeeze(image) or image[0] if you accidentally passed a batch.
  2. If the image is (H,W,1) grayscale, reshape with image.reshape(image.shape[0], image.shape[1]).
  3. If channels > 4 (e.g. multispectral data), slice to the first 3 or 4 channels: image[..., :3].
  4. Print image.shape right before the call and verify it is exactly (H,W), (H,W,3), or (H,W,4).

Example fix

// before
img = model_output  # shape (1, 480, 640, 3)
pil = cv2_to_pillow(img)  # ValueError

// after
img = np.squeeze(model_output)  # shape (480, 640, 3)
pil = cv2_to_pillow(img)
Defensive patterns

Strategy: validation

Validate before calling

def is_valid_image_shape(image: np.ndarray) -> bool:
    return image.ndim == 2 or (image.ndim == 3 and image.shape[2] in (3, 4))

if not is_valid_image_shape(image):
    image = np.squeeze(image)
    if image.ndim == 3 and image.shape[2] == 1:
        image = image[..., 0]
assert is_valid_image_shape(image), image.shape

Type guard

from typing import TypeGuard

def is_convertible_image(image: object) -> TypeGuard[np.ndarray]:
    return isinstance(image, np.ndarray) and (
        image.ndim == 2 or (image.ndim == 3 and image.shape[2] in (3, 4))
    )

Try / catch

try:
    pil = cv2_to_pillow(image)
except ValueError as e:
    raise ValueError(f'Bad image for annotation: {image.shape}') from e

Prevention

When it happens

Trigger: Calling cv2_to_pillow(image) (or an annotator decorated with ensure_pil_image_for_* that routes through it) with an array of shape (H,W,2), (H,W,N) where N>4, (H,W,3,1) (extra batch axis), a 1-D flattened array, or a 0-D scalar. Also happens when you pass a stacked batch of frames (N,H,W,3) instead of a single frame.

Common situations: Feeding a batched model-output tensor converted to NumPy directly into a Pillow-based annotator; images loaded with unusual loaders that keep an extra dimension; grayscale images manually expanded to (H,W,1); sliced arrays like frame[None] passed by mistake.

Related errors


AI-assisted analysis of roboflow/supervision@7f254d9784 (2026-08-15). Data as JSON: /api/errors/20717bef49b84051. Report an issue: GitHub.