roboflow/supervision · error · ValueError

Cannot merge an empty list of CompactMask objects.

Error message

Cannot merge an empty list of CompactMask objects.

What it means

Raised by CompactMask.merge when masks_list is empty. Merging zero objects has no meaningful result (there is no image shape to inherit), so the API refuses rather than guessing. Pass at least one CompactMask.

Source

Thrown at src/supervision/detection/compact_mask.py:1356

        Examples:
            ```pycon
            >>> import numpy as np
            >>> from supervision.detection.compact_mask import CompactMask
            >>> masks1 = np.zeros((2, 50, 50), dtype=bool)
            >>> masks2 = np.zeros((3, 50, 50), dtype=bool)
            >>> xyxy1 = np.array([[0,0,10,10],[10,10,20,20]], dtype=np.float32)
            >>> xyxy2 = np.array(
            ...     [[0,0,5,5],[5,5,10,10],[10,10,15,15]], dtype=np.float32)
            >>> cm1 = CompactMask.from_dense(masks1, xyxy1, image_shape=(50, 50))
            >>> cm2 = CompactMask.from_dense(masks2, xyxy2, image_shape=(50, 50))
            >>> len(CompactMask.merge([cm1, cm2]))
            5

            ```
        """
        if not masks_list:
            raise ValueError("Cannot merge an empty list of CompactMask objects.")

        image_shape = masks_list[0]._image_shape
        for cm in masks_list[1:]:
            if cm._image_shape != image_shape:
                raise ValueError(
                    f"Cannot merge CompactMask objects with different image shapes: "
                    f"{image_shape} vs {cm._image_shape}"
                )

        # list.extend is a C-level call and avoids the per-element Python
        # bytecode overhead of a flat list comprehension.  This matters under
        # GIL contention when multiple threads call merge concurrently.
        new_rles: list[npt.NDArray[np.int32]] = []
        for cm in masks_list:
            new_rles.extend(cm._rles)

        # np.concatenate handles (0, 2) arrays correctly.
        # No .astype() needed — _crop_shapes and _offsets are already int32.

View on GitHub (pinned to 7f254d9784)

Solutions

  1. Skip the merge when the list is empty: if not masks_list: continue / return an empty CompactMask built from the known image_shape.
  2. If detections were expected, debug why the producing step yielded no CompactMask objects before calling merge.
  3. Construct an empty result explicitly via CompactMask.from_dense(np.zeros((0, h, w), bool), np.empty((0, 4)), image_shape) when an empty object is needed.

Example fix

# before
merged = CompactMask.merge(tile_masks)

# after
if tile_masks:
    merged = CompactMask.merge(tile_masks)
else:
    merged = CompactMask.from_dense(
        np.zeros((0, h, w), dtype=bool), np.empty((0, 4), dtype=np.float32),
        image_shape=(h, w))
Defensive patterns

Strategy: validation

Validate before calling

if not tile_masks:
    merged = sv.CompactMask.from_dense(
        np.zeros((0, h, w), dtype=bool),
        np.empty((0, 4), dtype=np.float32),
        image_shape=(h, w),
    )
else:
    merged = sv.CompactMask.merge(tile_masks)

Type guard

def is_mergeable(masks_list) -> bool:
    return isinstance(masks_list, (list, tuple)) and len(masks_list) > 0

Try / catch

try:
    merged = sv.CompactMask.merge(tile_masks)
except ValueError as e:
    if "empty list" in str(e):
        merged = None  # nothing detected on any tile
    else:
        raise

Prevention

When it happens

Trigger: Calling CompactMask.merge([]) — typically because a loop collecting per-tile or per-batch CompactMask objects produced an empty list (no detections anywhere, empty directory, all tiles filtered out).

Common situations: Aggregating slicer outputs when the model returned nothing on any tile; merging per-image masks in a loop over an empty folder; a guard upstream filtered everything.

Related errors


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