roboflow/supervision · error · ValueError

Image must have 3 or 4 channels.

Error message

Image must have 3 or 4 channels.

What it means

Raised by draw_image (draw/utils.py) when the image to paste is not a 3-D array with exactly 3 or 4 channels. The function supports RGB/BGR (3-channel) and RGBA/BGRA (4-channel, using the alpha channel for blending); grayscale, single-channel, or multi-frame tensors are rejected before any resizing happens.

Source

Thrown at src/supervision/draw/utils.py:448

        >>> scene.shape
        (100, 100, 3)

        ```
    """

    # Validate and load image
    if isinstance(image, str):
        if not os.path.exists(image):
            raise FileNotFoundError(f"Image path ('{image}') does not exist.")
        loaded_image = cv2.imread(image, cv2.IMREAD_UNCHANGED)
        if loaded_image is None:
            raise OSError(f"Could not decode image path ('{image}').")
        image_np = cast(npt.NDArray[np.uint8], loaded_image)
    else:
        image_np = image

    if image_np.ndim != 3 or image_np.shape[2] not in (3, 4):
        raise ValueError("Image must have 3 or 4 channels.")

    # Validate opacity
    if not 0.0 <= opacity <= 1.0:
        raise ValueError("Opacity must be between 0.0 and 1.0.")

    rect_x = int(rect.x)
    rect_y = int(rect.y)
    rect_width = int(rect.width)
    rect_height = int(rect.height)
    # Validate rectangle dimensions
    if (
        rect_x < 0
        or rect_y < 0
        or rect_x + rect_width > scene.shape[1]
        or rect_y + rect_height > scene.shape[0]
    ):
        raise ValueError("Invalid rectangle dimensions.")

View on GitHub (pinned to 7f254d9784)

Solutions

  1. Convert grayscale to BGR: cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
  2. Load color images without IMREAD_GRAYSCALE: cv2.imread(path) yields 3 channels
  3. Convert framework tensors to HWC uint8 numpy before calling (e.g. tensor.permute(1,2,0).numpy())

Example fix

# before
logo = cv2.imread(path, cv2.IMREAD_GRAYSCALE)  # (H, W)
scene = draw_image(scene, logo, opacity=0.8, rect=rect)

# after
logo = cv2.imread(path, cv2.IMREAD_UNCHANGED)  # (H, W, 3|4)
scene = draw_image(scene, logo, opacity=0.8, rect=rect)
Defensive patterns

Strategy: validation

Validate before calling

import cv2

image = np.asarray(image)
if image.ndim == 2:
    image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
if image.ndim != 3 or image.shape[2] not in (3, 4):
    raise ValueError(f'unsupported image shape {image.shape}')
scene = draw_image(scene, image, opacity=opacity, rect=rect)

Type guard

import numpy as np

def is_blendable_image(img: np.ndarray) -> bool:
    """True when img is (H, W, 3|4) uint8-like, as draw_image requires."""
    return img.ndim == 3 and img.shape[2] in (3, 4)

Try / catch

try:
    scene = draw_image(scene, image, opacity=opacity, rect=rect)
except ValueError as e:
    if '3 or 4 channels' in str(e):
        scene = draw_image(scene, cv2.cvtColor(image, cv2.COLOR_GRAY2BGR), opacity=opacity, rect=rect)
    else:
        raise

Prevention

When it happens

Trigger: Calling draw_image(scene, image, ...) with a (H, W) grayscale array, a (H, W, 1) array, or an (N, H, W, 3) batched tensor.

Common situations: Loading an image with cv2.imread(path, cv2.IMREAD_GRAYSCALE) or cv2.IMREAD_UNCHANGED on a PNG with transparency already stripped; palette/label images generated programmatically as 2-D arrays; passing a torch tensor without converting to HWC numpy.

Related errors


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