{"record":{"id":"c176652e331316d5","repo":"roboflow/supervision","slug":"panoptic-png-masks-must-have-at-least-3-channels","errorCode":null,"errorMessage":"Panoptic PNG masks must have at least 3 channels.","messagePattern":"Panoptic PNG masks must have at least 3 channels\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/supervision/detection/tools/transformers.py","lineNumber":229,"sourceCode":"\ndef png_string_to_segmentation_array(png_string: bytes) -> npt.NDArray[Any]:\n    \"\"\"\n    Convert a PNG byte string to a panoptic segmentation array.\n\n    Args:\n        png_string: A byte string representing the PNG image.\n\n    Returns:\n        A segmentation ID array with shape (H, W), where each unique value\n            represents a different object or category. RGB-encoded panoptic\n            PNGs are decoded as little-endian 24-bit integers; alpha is ignored.\n    \"\"\"\n    image = Image.open(io.BytesIO(png_string))\n    mask = np.array(image, dtype=np.uint8)\n    if mask.ndim == 2:\n        return mask.astype(np.uint32)\n    if mask.shape[2] < 3:\n        raise ValueError(\"Panoptic PNG masks must have at least 3 channels.\")\n\n    segmentation = (\n        mask[:, :, 0].astype(np.uint32)\n        + (mask[:, :, 1].astype(np.uint32) << 8)\n        + (mask[:, :, 2].astype(np.uint32) << 16)\n    )\n    return cast(npt.NDArray[Any], segmentation)\n\n\ndef append_class_names_to_data(\n    class_ids: npt.NDArray[Any],\n    id2label: dict[int, str] | None,\n    data: dict[str, Any] | None = None,\n) -> dict[str, Any]:\n    \"\"\"\n    Helper function to create or append to a data dictionary with class names if\n    available.\n","sourceCodeStart":211,"sourceCodeEnd":247,"githubUrl":"https://github.com/roboflow/supervision/blob/7f254d9784d4c37e0f03cd89ddee164c8db099c0/src/supervision/detection/tools/transformers.py#L211-L247","documentation":"Raised when decoding a panoptic-segmentation PNG whose decoded array has 3 dimensions but fewer than 3 channels in the last axis. Panoptic IDs are RGB-encoded as little-endian 24-bit integers (R + G<<8 + B<<16), so at least R, G, and B channels must exist; a 1- or 2-channel PNG (grayscale with an extra axis, or LA alpha-only layouts) cannot carry the encoding. Pure 2-D grayscale masks are handled earlier and returned as-is.","triggerScenarios":"Calling the panoptic-PNG decoder (used by Transformers connectors, e.g. for Mask2Former/MaskFormer post-processing) with an LA-mode (luminance+alpha) PNG or another 2-channel image saved by an upstream tool.","commonSituations":"A model or preprocessing step converts the panoptic PNG to grayscale-with-alpha before it reaches supervision; corrupted or re-encoded PNG files from a dataset pipeline; version changes in an upstream library that alters the saved PNG mode.","solutions":["Regenerate or re-save the PNG in RGB mode: Image.open(p).convert('RGB').save(p) before passing the bytes.","If the mask is genuinely class-ID grayscale, pass a 2-D single-channel PNG so it takes the grayscale path instead.","Check the producer of png_string — it should emit the original model panoptic PNG unmodified."],"exampleFix":"# before\npng_bytes = open('panoptic_la.png', 'rb').read()\nsegmentation = decode_png_string(png_bytes)  # ValueError\n\n# after\nfrom PIL import Image\nimg = Image.open('panoptic_la.png').convert('RGB')\nimport io\nbuf = io.BytesIO(); img.save(buf, format='PNG')\nsegmentation = decode_png_string(buf.getvalue())","handlingStrategy":"validation","validationCode":"import io\nimport numpy as np\nfrom PIL import Image\n\ndef load_panoptic_png(png_bytes: bytes) -> np.ndarray:\n    img = Image.open(io.BytesIO(png_bytes))\n    if img.mode not in ('RGB', 'L'):\n        img = img.convert('RGB')\n    return np.array(img)\n\nsegmentation = decode_png_string(png_bytes) if is_valid_panoptic_png(png_bytes) else ...","typeGuard":"def is_valid_panoptic_png(png_bytes: bytes) -> bool:\n    import io\n    from PIL import Image\n    arr = np.array(Image.open(io.BytesIO(png_bytes)))\n    return arr.ndim == 2 or arr.shape[2] >= 3","tryCatchPattern":"try:\n    segmentation = decode_png_string(png_bytes)\nexcept ValueError as err:\n    if 'at least 3 channels' in str(err):\n        img = Image.open(io.BytesIO(png_bytes)).convert('RGB')\n        buf = io.BytesIO(); img.save(buf, format='PNG')\n        segmentation = decode_png_string(buf.getvalue())\n    else:\n        raise","preventionTips":["Pass model-produced panoptic PNGs through unmodified.","Normalize image mode to RGB (or L for class-ID maps) in your data-loading layer."],"tags":["panoptic-segmentation","png","transformers","image-decoding","valueerror"],"backgroundTag":null,"analyzedSha":"7f254d9784d4c37e0f03cd89ddee164c8db099c0","analyzedAt":"2026-08-15T05:13:01.950Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}