Source code for pde.tools.ffmpeg

"""Functions for interacting with FFmpeg.

.. autosummary::
   :nosignatures:

   FFmpegFormat
   formats
   find_format

.. codeauthor:: David Zwicker <david.zwicker@ds.mpg.de>
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Optional, Union

import numpy as np
from numpy.typing import DTypeLike


[docs] @dataclass class FFmpegFormat: """Defines a FFmpeg format used for storing field data in a video. Note: All pixel formats supported by FFmpeg can be obtained by running :code:`ffmpeg -pix_fmts`. However, not all pixel formats are supported by all codecs. Supported pixel formats are listed in the output of :code:`ffmpeg -h encoder=<ENCODER>`, where `<ENCODER>` is one of the encoders listed in :code:`ffmpeg -codecs`. """ pix_fmt_file: str """str: name of the pixel format used in the codec""" pix_fmt_data: str """str: name of the pixel format used in the frame data""" channels: int """int: number of color channels in this pixel format""" bits_per_channel: int """int: number of bits per color channel in this pixel format""" dtype: DTypeLike """Numpy dtype corresponding to the data of a single channel.""" codec: str = "ffv1" """str: name of the codec that supports this pixel format""" @property def bytes_per_channel(self) -> int: """Int:number of bytes per color channel.""" return self.bits_per_channel // 8 @property def max_value(self) -> float | int: """Maximal value stored in a color channel.""" if np.issubdtype(self.dtype, np.integer): return 2**self.bits_per_channel - 1 # type: ignore else: return 1.0
[docs] def data_to_frame(self, normalized_data: np.ndarray) -> np.ndarray: """Converts normalized data to data being stored in a color channel.""" return np.ascontiguousarray(normalized_data * self.max_value, dtype=self.dtype)
[docs] def data_from_frame(self, frame_data: np.ndarray): """Converts data stored in a color channel to normalized data.""" return frame_data.astype(float) / self.max_value
formats = { # 8 bit formats "gray": FFmpegFormat( pix_fmt_file="gray", pix_fmt_data="gray", channels=1, bits_per_channel=8, dtype=np.uint8, ), "rgb24": FFmpegFormat( pix_fmt_file="rgb24", pix_fmt_data="rgb24", channels=3, bits_per_channel=8, dtype=np.uint8, ), "rgb32": FFmpegFormat( pix_fmt_file="rgb32", pix_fmt_data="rgb32", channels=4, bits_per_channel=8, dtype=np.uint8, ), # 16 bit formats "gray16le": FFmpegFormat( pix_fmt_file="gray16le", pix_fmt_data="gray16le", channels=1, bits_per_channel=16, dtype=np.dtype("<u2"), ), "gbrp16le": FFmpegFormat( pix_fmt_file="gbrp16le", pix_fmt_data="gbrp16le", channels=3, bits_per_channel=16, dtype=np.dtype("<u2"), ), "gbrap16le": FFmpegFormat( pix_fmt_file="gbrap16le", pix_fmt_data="gbrap16le", channels=4, bits_per_channel=16, dtype=np.dtype("<u2"), ), } """Dict of pre-defined :class:`FFmpegFormat` formats."""
[docs] def find_format(channels: int, bits_per_channel: int = 8) -> str | None: """Find a defined FFmpegFormat that satisifies the requirements. Args: channels (int): Minimal number of color channels bits_per_channel (int): Minimal number of bits per channel Returns: str: Identifier for a format that satisifies the requirements (but might have more channels or more bits per channel then requested. `None` is returned if no format can be identified. """ n_best, f_best = None, None for n, f in formats.items(): # iterate through all defined formats if ( f.channels >= channels and f.bits_per_channel >= bits_per_channel # satisfies the requirements and ( f_best is None or f.bits_per_channel < f_best.bits_per_channel or f.channels < f_best.channels ) # the current format is better than the previous one ): n_best, f_best = n, f return n_best