Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ We are operating with `semantic versioning <https://semver.org>`_.
v19.0.3 (Unreleased)
--------------------

Features:

- Add ``VideoFrame.solid()`` to create a frame filled with one color, in any non-paletted pixel format.

Fixes:

- Support slicing ``Container.streams``, which raised ``TypeError`` despite being typed.
Expand Down
89 changes: 88 additions & 1 deletion av/video/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
)
from cython.cimports.cpython.ref import Py_DECREF, Py_INCREF
from cython.cimports.hwcontext_cuda import AVCUDADeviceContext, CUstream
from cython.cimports.libc.stdint import int64_t, uint8_t, uintptr_t
from cython.cimports.libc.stddef import ptrdiff_t
from cython.cimports.libc.stdint import int64_t, uint8_t, uint32_t, uintptr_t

# Holds the VideoReformatter shared by all frames converted on this thread.
_thread_local = threading.local()
Expand Down Expand Up @@ -562,6 +563,24 @@ def useful_array(
return np.ndarray(shape, dtype=dtype_obj, buffer=plane, strides=strides)


@cython.cfunc
def _fill_color(frame: VideoFrame, color: cython.pointer[uint32_t]) -> cython.void:
linesize = cython.declare(ptrdiff_t[4])
for i in range(4):
linesize[i] = frame.ptr.linesize[i]
err_check(
lib.av_image_fill_color(
frame.ptr.data,
linesize,
cython.cast(lib.AVPixelFormat, frame.ptr.format),
color,
frame.ptr.width,
frame.ptr.height,
0,
)
)


@cython.cfunc
def check_ndarray_shape(array: object, ok: cython.bint) -> cython.void:
if not ok:
Expand Down Expand Up @@ -1599,6 +1618,74 @@ def from_bytes(
raise NotImplementedError(f"Format '{format}' is not supported.")
return frame

@staticmethod
def solid(width: int, height: int, color="black", format="yuv420p"):
"""Create a frame filled with a single color.

:param color: A color FFmpeg can parse, like ``"red"``, ``"#ff8000"``, or
``"white@0.5"``; or an ``(r, g, b)`` or ``(r, g, b, a)`` tuple of ints
from 0 to 255.
:param format: The pixel format. The color is converted to it the same
way :meth:`reformat` converts an RGB frame.

>>> frame = VideoFrame.solid(64, 64, "red", "rgb24")
>>> bytes(frame.planes[0])[:3]
b'\\xff\\x00\\x00'

"""
rgba = cython.declare(uint8_t[4])
if isinstance(color, str):
if lib.av_parse_color(rgba, color.encode(), -1, cython.NULL) < 0:
raise ValueError(f"Invalid color {color!r}")
else:
values = tuple(color)
if len(values) == 3:
values += (255,)
if len(values) != 4 or not all(0 <= v <= 255 for v in values):
raise ValueError(
f"Expected an (r, g, b) or (r, g, b, a) tuple of 0-255, got {color!r}"
)
for i in range(4):
rgba[i] = values[i]

frame: VideoFrame = VideoFrame(width, height, format)
desc: cython.pointer[cython.const[lib.AVPixFmtDescriptor]] = (
lib.av_pix_fmt_desc_get(cython.cast(lib.AVPixelFormat, frame.ptr.format))
)
if desc.flags & (lib.AV_PIX_FMT_FLAG_PAL | lib.AV_PIX_FMT_FLAG_BAYER):
raise ValueError(f"Cannot fill format '{format}' with a solid color")

# Let swscale convert a small RGBA block, so the result matches what
# reformat() would produce, then copy its components across the frame
# without converting every pixel. 16x16 covers any chroma subsampling.
color32 = cython.declare(uint32_t[4])
for i in range(4):
color32[i] = rgba[i]
block: VideoFrame = VideoFrame(16, 16, "rgba")
_fill_color(block, color32)
block = block.reformat(format=format)

for i in range(desc.nb_components):
lib.av_read_image_line2(
cython.address(color32[i]),
cython.cast(
cython.pointer[cython.pointer[cython.const[uint8_t]]],
block.ptr.data,
),
block.ptr.linesize,
desc,
0,
0,
i,
1,
0,
4,
)
_fill_color(frame, color32)
frame.ptr.colorspace = block.ptr.colorspace
frame.ptr.color_range = block.ptr.color_range
return frame

@staticmethod
def from_dlpack(
planes,
Expand Down
7 changes: 7 additions & 0 deletions av/video/frame.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ class VideoFrame(Frame):
flip_vertical: bool = False,
) -> VideoFrame: ...
@staticmethod
def solid(
width: int,
height: int,
color: str | tuple[int, int, int] | tuple[int, int, int, int] = "black",
format: str = "yuv420p",
) -> VideoFrame: ...
@staticmethod
def from_dlpack(
planes: object | tuple[object, ...],
format: str = "nv12",
Expand Down
27 changes: 27 additions & 0 deletions include/avutil.pxd
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from libc.stddef cimport ptrdiff_t
from libc.stdint cimport int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t


Expand Down Expand Up @@ -242,6 +243,15 @@ cdef extern from "libavutil/imgutils.h" nogil:
uint8_t *ptr,
const int linesizes[4]
)
cdef int av_image_fill_color(
uint8_t * const dst_data[4],
const ptrdiff_t dst_linesize[4],
AVPixelFormat pix_fmt,
const uint32_t color[4],
int width,
int height,
int flags
)

cdef extern from "libavutil/log.h" nogil:
cdef struct AVClass:
Expand Down Expand Up @@ -333,6 +343,11 @@ cdef extern from "libavutil/opt.h" nogil:
void *obj, const char *name, int search_flags, uint8_t **out_val
)

cdef extern from "libavutil/parseutils.h" nogil:
cdef int av_parse_color(
uint8_t *rgba_color, const char *color_string, int slen, void *log_ctx
)

cdef extern from "libavutil/pixdesc.h" nogil:
# See: http://ffmpeg.org/doxygen/trunk/structAVComponentDescriptor.html
cdef struct AVComponentDescriptor:
Expand Down Expand Up @@ -363,6 +378,18 @@ cdef extern from "libavutil/pixdesc.h" nogil:
cdef AVPixelFormat av_get_pix_fmt(const char *name)
int av_get_bits_per_pixel(const AVPixFmtDescriptor *pixdesc)
int av_get_padded_bits_per_pixel(const AVPixFmtDescriptor *pixdesc)
void av_read_image_line2(
void *dst,
const uint8_t *data[4],
const int linesize[4],
const AVPixFmtDescriptor *desc,
int x,
int y,
int c,
int w,
int read_pal_component,
int dst_element_size
)

cdef extern from "libavutil/rational.h" nogil:
cdef int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max)
Expand Down
37 changes: 37 additions & 0 deletions tests/test_videoframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,43 @@ def test_rgb24_planes() -> None:
assert frame.planes[0].buffer_size == 640 * 480 * 3


@pytest.mark.parametrize(
"format", ["rgb24", "bgr0", "gbrp", "yuv420p", "nv12", "yuyv422", "yuv420p10le"]
)
def test_solid(format: str) -> None:
# Odd sizes check the last row and column of subsampled planes get filled.
frame = VideoFrame.solid(33, 17, "#ff8000", format)
assert (frame.width, frame.height, frame.format.name) == (33, 17, format)

rgb = frame.to_ndarray(format="rgb24").astype(int)
assert numpy.abs(rgb - (255, 128, 0)).max() <= 2


def test_solid_matches_reformat() -> None:
rgb = VideoFrame.solid(16, 16, (30, 140, 220), "rgb24")
expected = rgb.reformat(format="yuv420p")
frame = VideoFrame.solid(16, 16, (30, 140, 220), "yuv420p")
assertNdarraysEqual(frame.to_ndarray(), expected.to_ndarray())


def test_solid_alpha() -> None:
frame = VideoFrame.solid(8, 8, "white@0.5", "rgba")
assert bytes(frame.planes[0])[:4] == b"\xff\xff\xff\x7f"
frame = VideoFrame.solid(8, 8, (0, 0, 255, 64), "rgba")
assert bytes(frame.planes[0])[:4] == b"\x00\x00\xff\x40"


def test_solid_invalid() -> None:
with pytest.raises(ValueError, match="Invalid color"):
VideoFrame.solid(8, 8, "notacolor")
with pytest.raises(ValueError):
VideoFrame.solid(8, 8, (255, 0)) # type: ignore[arg-type]
with pytest.raises(ValueError):
VideoFrame.solid(8, 8, (256, 0, 0))
with pytest.raises(ValueError, match="pal8"):
VideoFrame.solid(8, 8, "red", "pal8")


def test_memoryview_read() -> None:
frame = VideoFrame(640, 480, "rgb24")
frame.planes[0].update(b"01234" + (b"x" * (640 * 480 * 3 - 5)))
Expand Down
Loading