Skip to content

Commit d17cf23

Browse files
committed
Fix to_ndarray crash on negative linesize, closes #2213
1 parent c57fe67 commit d17cf23

4 files changed

Lines changed: 69 additions & 5 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Fixes:
3838
- Warn that ``CodecContext.decode()`` is not memory safe in some cases.
3939
- Fix ``enumerate_input_devices`` and ``enumerate_output_devices`` raising ``AttributeError`` (:issue:`2264`).
4040
- Map HTTP 429 to ``HTTPTooManyRequestsError`` instead of ``UndefinedError`` (:issue:`2267`).
41+
- Fix crash in ``VideoFrame.to_ndarray()`` and ``to_image()`` on bottom-up frames with a negative ``line_size`` (:issue:`2213`).
4142

4243
v17.0.1
4344
-------

av/video/frame.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,8 @@ def useful_array(
447447
import numpy as np
448448

449449
dtype_obj = np.dtype(dtype)
450-
total_line_size = abs(plane.frame.ptr.linesize[plane.index])
450+
line_size = plane.frame.ptr.linesize[plane.index]
451+
total_line_size = abs(line_size)
451452
itemsize = dtype_obj.itemsize
452453
channels = bytes_per_pixel // itemsize
453454

@@ -458,6 +459,13 @@ def useful_array(
458459
shape = (plane.height, plane.width, channels)
459460
strides = (total_line_size, bytes_per_pixel, itemsize)
460461

462+
if line_size < 0:
463+
offset = (plane.height - 1) * total_line_size
464+
strides = (-total_line_size, *strides[1:])
465+
return np.ndarray(
466+
shape, dtype=dtype_obj, buffer=plane, offset=offset, strides=strides
467+
)
468+
461469
return np.ndarray(shape, dtype=dtype_obj, buffer=plane, strides=strides)
462470

463471

@@ -704,17 +712,24 @@ def to_image(self, **kwargs):
704712
plane: VideoPlane = self.reformat(format="rgb24", **kwargs).planes[0]
705713

706714
i_buf: cython.const[uint8_t][:] = plane
707-
i_pos: cython.size_t = 0
708-
i_stride: cython.size_t = plane.line_size
715+
line_size: cython.int = plane.line_size
716+
i_stride: cython.size_t = abs(line_size)
709717

710718
o_pos: cython.size_t = 0
711719
o_stride: cython.size_t = plane.width * 3
712720
o_size: cython.size_t = plane.height * o_stride
713721
o_buf: bytearray = bytearray(o_size)
714722

723+
# For bottom-up frames (negative line_size) the buffer protocol exposes
724+
# rows from the lowest address, so the top display row is at the far end.
725+
i_pos: cython.size_t = (plane.height - 1) * i_stride if line_size < 0 else 0
726+
715727
while o_pos < o_size:
716728
o_buf[o_pos : o_pos + o_stride] = i_buf[i_pos : i_pos + o_stride]
717-
i_pos += i_stride
729+
if line_size < 0:
730+
i_pos -= i_stride
731+
else:
732+
i_pos += i_stride
718733
o_pos += o_stride
719734

720735
return Image.frombytes(

av/video/plane.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,14 @@ def __getbuffer__(self, view: cython.pointer[Py_buffer], flags: cython.int):
7878
)
7979
if flags & PyBUF_WRITABLE and not self._buffer_writable():
8080
raise ValueError("buffer is not writable")
81-
PyBuffer_FillInfo(view, self, self._buffer_ptr(), self._buffer_size(), 0, flags)
81+
82+
ptr: cython.p_void = self._buffer_ptr()
83+
line_size: cython.int = self.frame.ptr.linesize[self.index]
84+
if line_size < 0:
85+
height: cython.int = self.height
86+
ptr = cython.cast(cython.p_char, ptr) + (height - 1) * line_size
87+
88+
PyBuffer_FillInfo(view, self, ptr, self._buffer_size(), 0, flags)
8289

8390
def __dlpack_device__(self):
8491
if self.frame.ptr.hw_frames_ctx:

tests/test_videoframe.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,47 @@ def test_basic_to_ndarray() -> None:
161161
assert array.shape == (480, 640, 3)
162162

163163

164+
def _vflip(frame: VideoFrame) -> VideoFrame:
165+
"""Vertically flip a frame, which yields a bottom-up frame with a negative
166+
``line_size`` (the same layout DirectShow produces, see GH-2213)."""
167+
graph = av.filter.Graph()
168+
src = graph.add_buffer(
169+
template=None,
170+
width=frame.width,
171+
height=frame.height,
172+
format=frame.format.name,
173+
time_base=Fraction(1, 1000),
174+
)
175+
vflip = graph.add("vflip")
176+
sink = graph.add("buffersink")
177+
src.link_to(vflip)
178+
vflip.link_to(sink)
179+
graph.configure()
180+
graph.push(frame)
181+
return graph.pull()
182+
183+
184+
@pytest.mark.parametrize("format", ["rgb24", "bgr24", "gray"])
185+
def test_negative_linesize_to_ndarray(format: str) -> None:
186+
# Bottom-up packed frames have a negative line_size; to_ndarray() must read
187+
# them without crashing (GH-2213) and in the correct top-down order.
188+
height, width = 6, 4
189+
if format == "gray":
190+
array = numpy.arange(height * width, dtype=numpy.uint8).reshape(height, width)
191+
else:
192+
array = numpy.zeros((height, width, 3), dtype=numpy.uint8)
193+
for row in range(height):
194+
array[row, :, :] = row * 10
195+
196+
frame = _vflip(VideoFrame.from_ndarray(array, format=format))
197+
assert frame.planes[0].line_size < 0
198+
199+
result = frame.to_ndarray(format=format)
200+
assertNdarraysEqual(result, array[::-1])
201+
# Fully materializing the array used to segfault on a bottom-up frame.
202+
assert result.copy().sum() == int(array.sum())
203+
204+
164205
def test_ndarray_gray() -> None:
165206
array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8)
166207
for format in ("gray", "gray8"):

0 commit comments

Comments
 (0)