Skip to content

Commit 8cb4261

Browse files
committed
Make VideoFrame.from_numpy_buffer support buffers with padding
Some devices have hardware that creates image buffers with padding, so adding support here means less frame buffer copying is required. Specifically, we extend the support to buffers where the pixel rows are contiguous, though the image doesn't comprise all the pixels on the row (and is therefore not strictly contiguous). We also support yuv420p images with padding. These have padding in the middle of the UV rows as well as at the end, so can't be trimmed by the application before being passed in. Instead, the true image width must be passed. Tests are also added to ensure all these cases now avoid copying.
1 parent 23a1cc4 commit 8cb4261

File tree

2 files changed

+235
-16
lines changed

2 files changed

+235
-16
lines changed

av/video/frame.pyx

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -374,31 +374,54 @@ cdef class VideoFrame(Frame):
374374
return frame
375375

376376
@staticmethod
377-
def from_numpy_buffer(array, format="rgb24"):
377+
def from_numpy_buffer(array, format="rgb24", width=0):
378+
# Usually the width of the array is the same as the width of the image. But sometimes
379+
# this is not possible, for example with yuv420p images that have padding. These are
380+
# awkward because the UV rows at the bottom have padding bytes in the middle of the
381+
# row as well as at the end. To cope with these, callers need to be able to pass the
382+
# actual width to us.
383+
height = array.shape[0]
384+
if not width:
385+
width = array.shape[1]
386+
378387
if format in ("rgb24", "bgr24"):
379388
check_ndarray(array, "uint8", 3)
380389
check_ndarray_shape(array, array.shape[2] == 3)
381-
height, width = array.shape[:2]
390+
if array.strides[1:] != (3, 1):
391+
raise ValueError("provided array does not have C_CONTIGUOUS rows")
392+
linesizes = (array.strides[0], )
393+
elif format in ("rgba", "bgra"):
394+
check_ndarray(array, "uint8", 3)
395+
check_ndarray_shape(array, array.shape[2] == 4)
396+
if array.strides[1:] != (4, 1):
397+
raise ValueError("provided array does not have C_CONTIGUOUS rows")
398+
linesizes = (array.strides[0], )
382399
elif format in ("gray", "gray8", "rgb8", "bgr8"):
383400
check_ndarray(array, "uint8", 2)
384-
height, width = array.shape[:2]
401+
if array.strides[1] != 1:
402+
raise ValueError("provided array does not have C_CONTIGUOUS rows")
403+
linesizes = (array.strides[0], )
385404
elif format in ("yuv420p", "yuvj420p", "nv12"):
386405
check_ndarray(array, "uint8", 2)
387406
check_ndarray_shape(array, array.shape[0] % 3 == 0)
388407
check_ndarray_shape(array, array.shape[1] % 2 == 0)
389-
height, width = array.shape[:2]
390408
height = height // 6 * 4
409+
if array.strides[1] != 1:
410+
raise ValueError("provided array does not have C_CONTIGUOUS rows")
411+
if format in ("yuv420p", "yuvj420p"):
412+
# For YUV420 planar formats, the UV plane stride is always half the Y stride.
413+
linesizes = (array.strides[0], array.strides[0] // 2, array.strides[0] // 2)
414+
else:
415+
# Planes where U and V are interleaved have the same stride as Y.
416+
linesizes = (array.strides[0], array.strides[0])
391417
else:
392418
raise ValueError(f"Conversion from numpy array with format `{format}` is not yet supported")
393419

394-
if not array.flags["C_CONTIGUOUS"]:
395-
raise ValueError("provided array must be C_CONTIGUOUS")
396-
397420
frame = alloc_video_frame()
398-
frame._image_fill_pointers_numpy(array, width, height, format)
421+
frame._image_fill_pointers_numpy(array, width, height, linesizes, format)
399422
return frame
400423

401-
def _image_fill_pointers_numpy(self, buffer, width, height, format):
424+
def _image_fill_pointers_numpy(self, buffer, width, height, linesizes, format):
402425
cdef lib.AVPixelFormat c_format
403426
cdef uint8_t * c_ptr
404427
cdef size_t c_data
@@ -433,13 +456,8 @@ cdef class VideoFrame(Frame):
433456
self.ptr.format = c_format
434457
self.ptr.width = width
435458
self.ptr.height = height
436-
res = lib.av_image_fill_linesizes(
437-
self.ptr.linesize,
438-
<lib.AVPixelFormat>self.ptr.format,
439-
width,
440-
)
441-
if res:
442-
err_check(res)
459+
for i, linesize in enumerate(linesizes):
460+
self.ptr.linesize[i] = linesize
443461

444462
res = lib.av_image_fill_pointers(
445463
self.ptr.data,

tests/test_videoframe.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,19 @@ def test_shares_memory_gray() -> None:
528528
# Make sure the frame reflects that
529529
assertNdarraysEqual(frame.to_ndarray(), array)
530530

531+
# repeat the test, but with an array that is not fully contiguous, though the
532+
# pixels in a row are
533+
array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8)
534+
array = array[:, :300]
535+
assert not array.data.c_contiguous
536+
frame = VideoFrame.from_numpy_buffer(array, "gray")
537+
assertNdarraysEqual(frame.to_ndarray(), array)
538+
539+
# overwrite the array, the contents thereof
540+
array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8)
541+
# Make sure the frame reflects that
542+
assertNdarraysEqual(frame.to_ndarray(), array)
543+
531544

532545
def test_shares_memory_gray8() -> None:
533546
array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8)
@@ -539,6 +552,19 @@ def test_shares_memory_gray8() -> None:
539552
# Make sure the frame reflects that
540553
assertNdarraysEqual(frame.to_ndarray(), array)
541554

555+
# repeat the test, but with an array that is not fully contiguous, though the
556+
# pixels in a row are
557+
array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8)
558+
array = array[:, :300]
559+
assert not array.data.c_contiguous
560+
frame = VideoFrame.from_numpy_buffer(array, "gray8")
561+
assertNdarraysEqual(frame.to_ndarray(), array)
562+
563+
# overwrite the array, the contents thereof
564+
array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8)
565+
# Make sure the frame reflects that
566+
assertNdarraysEqual(frame.to_ndarray(), array)
567+
542568

543569
def test_shares_memory_rgb8() -> None:
544570
array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8)
@@ -550,6 +576,19 @@ def test_shares_memory_rgb8() -> None:
550576
# Make sure the frame reflects that
551577
assertNdarraysEqual(frame.to_ndarray(), array)
552578

579+
# repeat the test, but with an array that is not fully contiguous, though the
580+
# pixels in a row are
581+
array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8)
582+
array = array[:, :300]
583+
assert not array.data.c_contiguous
584+
frame = VideoFrame.from_numpy_buffer(array, "rgb8")
585+
assertNdarraysEqual(frame.to_ndarray(), array)
586+
587+
# overwrite the array, the contents thereof
588+
array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8)
589+
# Make sure the frame reflects that
590+
assertNdarraysEqual(frame.to_ndarray(), array)
591+
553592

554593
def test_shares_memory_bgr8() -> None:
555594
array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8)
@@ -561,6 +600,19 @@ def test_shares_memory_bgr8() -> None:
561600
# Make sure the frame reflects that
562601
assertNdarraysEqual(frame.to_ndarray(), array)
563602

603+
# repeat the test, but with an array that is not fully contiguous, though the
604+
# pixels in a row are
605+
array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8)
606+
array = array[:, :300]
607+
assert not array.data.c_contiguous
608+
frame = VideoFrame.from_numpy_buffer(array, "bgr8")
609+
assertNdarraysEqual(frame.to_ndarray(), array)
610+
611+
# overwrite the array, the contents thereof
612+
array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8)
613+
# Make sure the frame reflects that
614+
assertNdarraysEqual(frame.to_ndarray(), array)
615+
564616

565617
def test_shares_memory_rgb24() -> None:
566618
array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8)
@@ -572,6 +624,43 @@ def test_shares_memory_rgb24() -> None:
572624
# Make sure the frame reflects that
573625
assertNdarraysEqual(frame.to_ndarray(), array)
574626

627+
# repeat the test, but with an array that is not fully contiguous, though the
628+
# pixels in a row are
629+
array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8)
630+
array = array[:, :300, :]
631+
assert not array.data.c_contiguous
632+
frame = VideoFrame.from_numpy_buffer(array, "rgb24")
633+
assertNdarraysEqual(frame.to_ndarray(), array)
634+
635+
# overwrite the array, the contents thereof
636+
array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8)
637+
# Make sure the frame reflects that
638+
assertNdarraysEqual(frame.to_ndarray(), array)
639+
640+
641+
def test_shares_memory_rgba() -> None:
642+
array = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8)
643+
frame = VideoFrame.from_numpy_buffer(array, "rgba")
644+
assertNdarraysEqual(frame.to_ndarray(), array)
645+
646+
# overwrite the array, the contents thereof
647+
array[...] = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8)
648+
# Make sure the frame reflects that
649+
assertNdarraysEqual(frame.to_ndarray(), array)
650+
651+
# repeat the test, but with an array that is not fully contiguous, though the
652+
# pixels in a row are
653+
array = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8)
654+
array = array[:, :300, :]
655+
assert not array.data.c_contiguous
656+
frame = VideoFrame.from_numpy_buffer(array, "rgba")
657+
assertNdarraysEqual(frame.to_ndarray(), array)
658+
659+
# overwrite the array, the contents thereof
660+
array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8)
661+
# Make sure the frame reflects that
662+
assertNdarraysEqual(frame.to_ndarray(), array)
663+
575664

576665
def test_shares_memory_yuv420p() -> None:
577666
array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8)
@@ -583,6 +672,38 @@ def test_shares_memory_yuv420p() -> None:
583672
# Make sure the frame reflects that
584673
assertNdarraysEqual(frame.to_ndarray(), array)
585674

675+
# repeat the test, but with an array where there are some padding bytes
676+
# note that the uv rows have half the padding in the middle of a row, and the
677+
# other half at the end
678+
height = 512
679+
stride = 256
680+
width = 200
681+
array = numpy.random.randint(
682+
0, 256, size=(height * 6 // 4, stride), dtype=numpy.uint8
683+
)
684+
uv_width = width // 2
685+
uv_stride = stride // 2
686+
687+
# compare carefully, avoiding all the padding bytes which to_ndarray strips out
688+
frame = VideoFrame.from_numpy_buffer(array, "yuv420p", width=width)
689+
frame_array = frame.to_ndarray()
690+
assertNdarraysEqual(frame_array[:height, :width], array[:height, :width])
691+
assertNdarraysEqual(frame_array[height:, :uv_width], array[height:, :uv_width])
692+
assertNdarraysEqual(
693+
frame_array[height:, uv_width:],
694+
array[height:, uv_stride : uv_stride + uv_width],
695+
)
696+
697+
# overwrite the array, and check the shared frame buffer changed too!
698+
array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8)
699+
frame_array = frame.to_ndarray()
700+
assertNdarraysEqual(frame_array[:height, :width], array[:height, :width])
701+
assertNdarraysEqual(frame_array[height:, :uv_width], array[height:, :uv_width])
702+
assertNdarraysEqual(
703+
frame_array[height:, uv_width:],
704+
array[height:, uv_stride : uv_stride + uv_width],
705+
)
706+
586707

587708
def test_shares_memory_yuvj420p() -> None:
588709
array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8)
@@ -594,6 +715,36 @@ def test_shares_memory_yuvj420p() -> None:
594715
# Make sure the frame reflects that
595716
assertNdarraysEqual(frame.to_ndarray(), array)
596717

718+
# repeat the test with padding, just as we did in the yuv420p case
719+
height = 512
720+
stride = 256
721+
width = 200
722+
array = numpy.random.randint(
723+
0, 256, size=(height * 6 // 4, stride), dtype=numpy.uint8
724+
)
725+
uv_width = width // 2
726+
uv_stride = stride // 2
727+
728+
# compare carefully, avoiding all the padding bytes which to_ndarray strips out
729+
frame = VideoFrame.from_numpy_buffer(array, "yuvj420p", width=width)
730+
frame_array = frame.to_ndarray()
731+
assertNdarraysEqual(frame_array[:height, :width], array[:height, :width])
732+
assertNdarraysEqual(frame_array[height:, :uv_width], array[height:, :uv_width])
733+
assertNdarraysEqual(
734+
frame_array[height:, uv_width:],
735+
array[height:, uv_stride : uv_stride + uv_width],
736+
)
737+
738+
# overwrite the array, and check the shared frame buffer changed too!
739+
array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8)
740+
frame_array = frame.to_ndarray()
741+
assertNdarraysEqual(frame_array[:height, :width], array[:height, :width])
742+
assertNdarraysEqual(frame_array[height:, :uv_width], array[height:, :uv_width])
743+
assertNdarraysEqual(
744+
frame_array[height:, uv_width:],
745+
array[height:, uv_stride : uv_stride + uv_width],
746+
)
747+
597748

598749
def test_shares_memory_nv12() -> None:
599750
array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8)
@@ -605,6 +756,19 @@ def test_shares_memory_nv12() -> None:
605756
# Make sure the frame reflects that
606757
assertNdarraysEqual(frame.to_ndarray(), array)
607758

759+
# repeat the test, but with an array that is not fully contiguous, though the
760+
# pixels in a row are
761+
array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8)
762+
array = array[:, :200]
763+
assert not array.data.c_contiguous
764+
frame = VideoFrame.from_numpy_buffer(array, "nv12")
765+
assertNdarraysEqual(frame.to_ndarray(), array)
766+
767+
# overwrite the array, the contents thereof
768+
array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8)
769+
# Make sure the frame reflects that
770+
assertNdarraysEqual(frame.to_ndarray(), array)
771+
608772

609773
def test_shares_memory_bgr24() -> None:
610774
array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8)
@@ -616,6 +780,43 @@ def test_shares_memory_bgr24() -> None:
616780
# Make sure the frame reflects that
617781
assertNdarraysEqual(frame.to_ndarray(), array)
618782

783+
# repeat the test, but with an array that is not fully contiguous, though the
784+
# pixels in a row are
785+
array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8)
786+
array = array[:, :300, :]
787+
assert not array.data.c_contiguous
788+
frame = VideoFrame.from_numpy_buffer(array, "bgr24")
789+
assertNdarraysEqual(frame.to_ndarray(), array)
790+
791+
# overwrite the array, the contents thereof
792+
array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8)
793+
# Make sure the frame reflects that
794+
assertNdarraysEqual(frame.to_ndarray(), array)
795+
796+
797+
def test_shares_memory_bgra() -> None:
798+
array = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8)
799+
frame = VideoFrame.from_numpy_buffer(array, "bgra")
800+
assertNdarraysEqual(frame.to_ndarray(), array)
801+
802+
# overwrite the array, the contents thereof
803+
array[...] = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8)
804+
# Make sure the frame reflects that
805+
assertNdarraysEqual(frame.to_ndarray(), array)
806+
807+
# repeat the test, but with an array that is not fully contiguous, though the
808+
# pixels in a row are
809+
array = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8)
810+
array = array[:, :300, :]
811+
assert not array.data.c_contiguous
812+
frame = VideoFrame.from_numpy_buffer(array, "bgra")
813+
assertNdarraysEqual(frame.to_ndarray(), array)
814+
815+
# overwrite the array, the contents thereof
816+
array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8)
817+
# Make sure the frame reflects that
818+
assertNdarraysEqual(frame.to_ndarray(), array)
819+
619820

620821
def test_reformat_pts() -> None:
621822
frame = VideoFrame(640, 480, "rgb24")

0 commit comments

Comments
 (0)