From 541b5e792f718bdae2593e18674d68bde0a1609a Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Thu, 5 Dec 2024 14:44:00 +0000 Subject: [PATCH 1/4] Expose `av_display_rotation_get` --- av/video/__init__.py | 1 + av/video/display.pyi | 16 ++++++++++++++++ av/video/display.pyx | 17 +++++++++++++++++ include/libavutil/avutil.pxd | 3 +++ tests/test_decode.py | 13 +++++++++++++ 5 files changed, 50 insertions(+) create mode 100644 av/video/display.pyi create mode 100644 av/video/display.pyx diff --git a/av/video/__init__.py b/av/video/__init__.py index 4a25d8837..a97170a93 100644 --- a/av/video/__init__.py +++ b/av/video/__init__.py @@ -1,2 +1,3 @@ +from . import display from .frame import VideoFrame from .stream import VideoStream diff --git a/av/video/display.pyi b/av/video/display.pyi new file mode 100644 index 000000000..e1988c2e0 --- /dev/null +++ b/av/video/display.pyi @@ -0,0 +1,16 @@ +from av.sidedata.sidedata import SideData + +def get_display_rotation(matrix: SideData) -> float: + """Extract the rotation component of the `DISPLAYMATRIX` transformation matrix. + + Args: + matrix (SideData): The transformation matrix. + + Returns: + float: The angle (in degrees) by which the transformation rotates the frame + counterclockwise. The angle will be in range [-180.0, 180.0]. + + Note: + Floating point numbers are inherently inexact, so callers are + recommended to round the return value to the nearest integer before use. + """ diff --git a/av/video/display.pyx b/av/video/display.pyx new file mode 100644 index 000000000..c2ef138cc --- /dev/null +++ b/av/video/display.pyx @@ -0,0 +1,17 @@ +cimport libav as lib +from libc.stdint cimport int32_t + +import numpy as np + +from av.sidedata.sidedata import SideData +from av.sidedata.sidedata import Type as SideDataType + + +def get_display_rotation(matrix): + if not isinstance(matrix, SideData) or matrix.type != SideDataType.DISPLAYMATRIX: + raise ValueError("Matrix must be `SideData` of type `DISPLAYMATRIX`") + cdef const int32_t[:] view = np.frombuffer(matrix, dtype=np.int32) + if view.shape[0] != 9: + raise ValueError("Matrix must be 3x3 represented as a 9-element array") + return lib.av_display_rotation_get(&view[0]) + diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index ed281aeaf..58dd43922 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -4,6 +4,9 @@ from libc.stdint cimport int64_t, uint8_t, uint64_t, int32_t cdef extern from "libavutil/mathematics.h" nogil: pass +cdef extern from "libavutil/display.h" nogil: + cdef double av_display_rotation_get(const int32_t matrix[9]) + 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) diff --git a/tests/test_decode.py b/tests/test_decode.py index 20abdf840..452fd237f 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -155,3 +155,16 @@ def test_flush_decoded_video_frame_count(self) -> None: output_count += 1 assert output_count == input_count + + def test_no_side_data(self): + container = av.open(fate_suite("h264/interlaced_crop.mp4")) + frame = next(container.decode(video=0)) + matrix = frame.side_data.get(av.sidedata.sidedata.Type.DISPLAYMATRIX) + assert matrix is None + + def test_side_data(self): + container = av.open(fate_suite("mov/displaymatrix.mov")) + frame = next(container.decode(video=0)) + matrix = frame.side_data.get(av.sidedata.sidedata.Type.DISPLAYMATRIX) + rotation = av.video.display.get_display_rotation(matrix) + self.assertEqual(rotation, -90.0) From 1691b7cb3278b5ca3a8af3568202c5429381f54f Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Thu, 5 Dec 2024 15:22:05 +0000 Subject: [PATCH 2/4] Lazily import numpy --- av/video/display.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/av/video/display.pyx b/av/video/display.pyx index c2ef138cc..4c725c1dc 100644 --- a/av/video/display.pyx +++ b/av/video/display.pyx @@ -1,13 +1,13 @@ cimport libav as lib from libc.stdint cimport int32_t -import numpy as np - from av.sidedata.sidedata import SideData from av.sidedata.sidedata import Type as SideDataType def get_display_rotation(matrix): + import numpy as np + if not isinstance(matrix, SideData) or matrix.type != SideDataType.DISPLAYMATRIX: raise ValueError("Matrix must be `SideData` of type `DISPLAYMATRIX`") cdef const int32_t[:] view = np.frombuffer(matrix, dtype=np.int32) From 17b0b96991dc2b9b749ce76b2468c47f128767a0 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Sun, 8 Dec 2024 23:10:04 +0000 Subject: [PATCH 3/4] Add `frame.rotation` property --- av/sidedata/sidedata.pxd | 2 ++ av/sidedata/sidedata.pyx | 12 ++++++++++-- av/video/__init__.py | 1 - av/video/display.pyi | 16 ---------------- av/video/display.pyx | 17 ----------------- av/video/frame.pyx | 11 +++++++++++ tests/test_decode.py | 7 ++----- 7 files changed, 25 insertions(+), 41 deletions(-) delete mode 100644 av/video/display.pyi delete mode 100644 av/video/display.pyx diff --git a/av/sidedata/sidedata.pxd b/av/sidedata/sidedata.pxd index 8a2f6d07c..5e6e5bf4c 100644 --- a/av/sidedata/sidedata.pxd +++ b/av/sidedata/sidedata.pxd @@ -14,6 +14,8 @@ cdef class SideData(Buffer): cdef SideData wrap_side_data(Frame frame, int index) +cdef int get_display_rotation(Frame frame) + cdef class _SideDataContainer: cdef Frame frame diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index 753496fea..24dbae119 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -1,3 +1,5 @@ +from libc.stdint cimport int32_t + from collections.abc import Mapping from enum import Enum @@ -45,13 +47,19 @@ class Type(Enum): cdef SideData wrap_side_data(Frame frame, int index): - cdef lib.AVFrameSideDataType type_ = frame.ptr.side_data[index].type - if type_ == lib.AV_FRAME_DATA_MOTION_VECTORS: + if frame.ptr.side_data[index].type == lib.AV_FRAME_DATA_MOTION_VECTORS: return MotionVectors(_cinit_bypass_sentinel, frame, index) else: return SideData(_cinit_bypass_sentinel, frame, index) +cdef int get_display_rotation(Frame frame): + for i in range(frame.ptr.nb_side_data): + if frame.ptr.side_data[i].type == lib.AV_FRAME_DATA_DISPLAYMATRIX: + return int(lib.av_display_rotation_get(frame.ptr.side_data[i].data)) + return 0 + + cdef class SideData(Buffer): def __init__(self, sentinel, Frame frame, int index): if sentinel is not _cinit_bypass_sentinel: diff --git a/av/video/__init__.py b/av/video/__init__.py index a97170a93..4a25d8837 100644 --- a/av/video/__init__.py +++ b/av/video/__init__.py @@ -1,3 +1,2 @@ -from . import display from .frame import VideoFrame from .stream import VideoStream diff --git a/av/video/display.pyi b/av/video/display.pyi deleted file mode 100644 index e1988c2e0..000000000 --- a/av/video/display.pyi +++ /dev/null @@ -1,16 +0,0 @@ -from av.sidedata.sidedata import SideData - -def get_display_rotation(matrix: SideData) -> float: - """Extract the rotation component of the `DISPLAYMATRIX` transformation matrix. - - Args: - matrix (SideData): The transformation matrix. - - Returns: - float: The angle (in degrees) by which the transformation rotates the frame - counterclockwise. The angle will be in range [-180.0, 180.0]. - - Note: - Floating point numbers are inherently inexact, so callers are - recommended to round the return value to the nearest integer before use. - """ diff --git a/av/video/display.pyx b/av/video/display.pyx deleted file mode 100644 index 4c725c1dc..000000000 --- a/av/video/display.pyx +++ /dev/null @@ -1,17 +0,0 @@ -cimport libav as lib -from libc.stdint cimport int32_t - -from av.sidedata.sidedata import SideData -from av.sidedata.sidedata import Type as SideDataType - - -def get_display_rotation(matrix): - import numpy as np - - if not isinstance(matrix, SideData) or matrix.type != SideDataType.DISPLAYMATRIX: - raise ValueError("Matrix must be `SideData` of type `DISPLAYMATRIX`") - cdef const int32_t[:] view = np.frombuffer(matrix, dtype=np.int32) - if view.shape[0] != 9: - raise ValueError("Matrix must be 3x3 represented as a 9-element array") - return lib.av_display_rotation_get(&view[0]) - diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 862db8513..02cde3187 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -4,6 +4,7 @@ from enum import IntEnum from libc.stdint cimport uint8_t from av.error cimport err_check +from av.sidedata.sidedata cimport get_display_rotation from av.utils cimport check_ndarray from av.video.format cimport get_pix_fmt, get_video_format from av.video.plane cimport VideoPlane @@ -172,6 +173,16 @@ cdef class VideoFrame(Frame): """Height of the image, in pixels.""" return self.ptr.height + @property + def rotation(self): + """The rotation component of the `DISPLAYMATRIX` transformation matrix. + + Returns: + int: The angle (in degrees) by which the transformation rotates the frame + counterclockwise. The angle will be in range [-180, 180]. + """ + return get_display_rotation(self) + @property def interlaced_frame(self): """Is this frame an interlaced or progressive?""" diff --git a/tests/test_decode.py b/tests/test_decode.py index 452fd237f..28fe2b044 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -159,12 +159,9 @@ def test_flush_decoded_video_frame_count(self) -> None: def test_no_side_data(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) frame = next(container.decode(video=0)) - matrix = frame.side_data.get(av.sidedata.sidedata.Type.DISPLAYMATRIX) - assert matrix is None + assert frame.rotation == 0 def test_side_data(self): container = av.open(fate_suite("mov/displaymatrix.mov")) frame = next(container.decode(video=0)) - matrix = frame.side_data.get(av.sidedata.sidedata.Type.DISPLAYMATRIX) - rotation = av.video.display.get_display_rotation(matrix) - self.assertEqual(rotation, -90.0) + assert frame.rotation == -90 From 84c4106a472dae3cdbe2e64e34edd7a5531ebe6c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 8 Dec 2024 19:50:07 -0500 Subject: [PATCH 4/4] Add type stub --- av/video/frame.pyi | 2 ++ tests/test_decode.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 0739010c1..a3eea373d 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -41,6 +41,8 @@ class VideoFrame(Frame): def height(self) -> int: ... @property def interlaced_frame(self) -> bool: ... + @property + def rotation(self) -> int: ... def __init__( self, width: int = 0, height: int = 0, format: str = "yuv420p" ) -> None: ... diff --git a/tests/test_decode.py b/tests/test_decode.py index 28fe2b044..05f636977 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -156,12 +156,12 @@ def test_flush_decoded_video_frame_count(self) -> None: assert output_count == input_count - def test_no_side_data(self): + def test_no_side_data(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) frame = next(container.decode(video=0)) assert frame.rotation == 0 - def test_side_data(self): + def test_side_data(self) -> None: container = av.open(fate_suite("mov/displaymatrix.mov")) frame = next(container.decode(video=0)) assert frame.rotation == -90