Skip to content

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 #6331

Description

@piezak

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):

} 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.

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.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions