#6331·mediapipe

numpy_view() aborts the whole process on non-contiguous float32 segmentation masks: "Check failed: 1 == ChannelSize() (1 vs 4)" — binding regression in 0.10.30, still present in 1.0.0

Author: piezakCreated Aug 9, 2026Updated Sep 11, 2026
Labelsstat:awaiting responseplatform:pythonstaletask:pose landmarkeros:linux-non-arm

Environment

  • mediapipe 0.10.35 (reproduced; by source inspection the bug is present in every release from 0.10.30 through 1.0.0)
  • Python 3.11, Linux x86_64 (also reproduced inside a linux/amd64 Docker container)
  • Task: PoseLandmarker with output_segmentation_masks=True (model: pose_landmarker_full.task)

Summary

Calling numpy_view() on a returned float32 segmentation mask hard-aborts the whole process (SIGABRT via ABSL CHECK, not catchable from Python) whenever the mask is non-contiguous — which for a float32 mask happens exactly when width % 4 != 0 (row stride width*4 bytes vs. the 16-byte ImageFrame alignment).

F0000 00:00:… image_frame.cc:362] Check failed: 1 == ChannelSize() (1 vs 4)
*** Check failure stack trace: ***
    … numpy_view → MpImageDataFloat32 → CopyToBuffer

Versions ≤ 0.10.21 return the expected HxW float32 array for the same input.

Root cause (source level)

The 0.10.30 ctypes rewrite of the Python binding lost the element-type dispatch in the non-contiguous copy path. In mediapipe/tasks/c/vision/core/image_frame_util.h (GenerateContiguousDataArray, current master, lines ~74-78):

cpp
} else {
    size_t buffer_size = image_frame->PixelDataSizeStoredContiguously();
    std::vector<uint8_t> contiguous_data_copy(buffer_size);
    image_frame->CopyToBuffer(contiguous_data_copy.data(), buffer_size);

The uint8_t* argument always resolves to the uint8 overload of CopyToBuffer, whose internal check compares ChannelSize() in bytes (4 for float32) against 1 → CHECK failure → abort. The pre-0.10.30 pybind11 binding (mediapipe/python/pybind/image_frame_util.h at v0.10.21) dispatched on element size (case sizeof(float): return GenerateContiguousDataArrayHelper<float>(…)), which is why this never crashed before.

The mask itself is a perfectly valid single-channel VEC32F1 — only the copy path is wrong.

Minimal reproduction

Any image containing a detectable person, resized so the width is not a multiple of 4 (e.g. 1783 px). A constructor-built mp.Image does NOT reproduce (its buffer is already contiguous) — the mask must come from the model.

python
import mediapipe as mp
import numpy as np
from PIL import Image
from mediapipe.tasks import python as mp_python
from mediapipe.tasks.python import vision

img = Image.open("any_person_photo.jpg").convert("RGB")
img = img.resize((1783, int(img.height * 1783 / img.width)))  # width % 4 != 0

opts = vision.PoseLandmarkerOptions(
    base_options=mp_python.BaseOptions(model_asset_path="pose_landmarker_full.task"),
    output_segmentation_masks=True,
)
lm = vision.PoseLandmarker.create_from_options(opts)
res = lm.detect(mp.Image(image_format=mp.ImageFormat.SRGB, data=np.asarray(img)))

mask = res.segmentation_masks[0]
print(mask.numpy_view().shape)   # ← SIGABRT here; width 1784 works fine

Resize the same photo to width 1784 → everything works and the mask is correct.

Impact

  • Any server-side pipeline consuming float masks dies whole-process on specific input dimensions (the abort cannot be caught in Python), unless every numpy_view() call is wrapped in a sacrificial subprocess.
  • Whether a given photo crashes is a deterministic function of its width after internal resizing — so production crash rates look random but aren't.

Workarounds we use

  1. Pad the input image width to % 4 == 0 (edge-replicate ≤3 px) before detection — masks come back whole.
  2. Defense in depth: check mask.is_contiguous() (a pure getter, safe) and skip numpy_view() when False — loses the mask but saves the process.

Expected behavior

numpy_view() returns the float32 array (as ≤0.10.21), or at minimum raises a Python exception instead of aborting the process.

Source: google-ai-edge/mediapipe