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
- Pad the input image width to
% 4 == 0 (edge-replicate ≤3 px) before detection — masks come back whole.
- 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.
Environment
linux/amd64Docker container)PoseLandmarkerwithoutput_segmentation_masks=True(model:pose_landmarker_full.task)Summary
Calling
numpy_view()on a returned float32 segmentation mask hard-aborts the whole process (SIGABRT viaABSL CHECK, not catchable from Python) whenever the mask is non-contiguous — which for a float32 mask happens exactly whenwidth % 4 != 0(row stridewidth*4bytes vs. the 16-byteImageFramealignment).Versions ≤ 0.10.21 return the expected
HxW float32array 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):The
uint8_t*argument always resolves to the uint8 overload ofCopyToBuffer, whose internal check comparesChannelSize()in bytes (4 for float32) against 1 →CHECKfailure → abort. The pre-0.10.30 pybind11 binding (mediapipe/python/pybind/image_frame_util.hat 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.Imagedoes NOT reproduce (its buffer is already contiguous) — the mask must come from the model.Resize the same photo to width 1784 → everything works and the mask is correct.
Impact
numpy_view()call is wrapped in a sacrificial subprocess.Workarounds we use
% 4 == 0(edge-replicate ≤3 px) before detection — masks come back whole.mask.is_contiguous()(a pure getter, safe) and skipnumpy_view()whenFalse— 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.