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 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) -> Union[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"),
),
# 32 bit formats
# "grayf32le": FFmpegFormat(
# pix_fmt_file="grayf32le",
# pix_fmt_data="grayf32le",
# codec="pfm", # pfm or dpx
# channels=1,
# bits_per_channel=32,
# dtype=np.dtype("<f4"),
# ),
# "gbrpf32le": FFmpegFormat(
# pix_fmt_file="gbrpf32le",
# pix_fmt_data="gbrpf32le",
# codec="pfm", # or dpx?
# channels=3,
# bits_per_channel=32,
# dtype=np.dtype("<f4"),
# ),
# "gbrapf32le": FFmpegFormat(
# pix_fmt_file="gbrapf32le",
# pix_fmt_data="gbrapf32le",
# channels=4,
# bits_per_channel=32,
# dtype=np.dtype("<f4"),
# ),
}
"""dict of pre-defined :class:`FFmpegFormat` formats"""
[docs]
def find_format(channels: int, bits_per_channel: int = 8) -> Optional[str]:
"""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:
# this format satisfies the requirements
if (
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