1.1.0版本
This commit is contained in:
9
UniTAP/common/__init__.py
Normal file
9
UniTAP/common/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from .color_info import ColorInfo
|
||||
from .data_info import DataInfo
|
||||
from .timing import Timing
|
||||
from .video_mode import VideoMode
|
||||
from .video_frame import VideoFrame, ImageFileFormat, get_vf_from_image
|
||||
from .audio_mode import AudioMode, AudioFileFormat, AudioFrameData, AudioFormat
|
||||
from .timestamp import Timestamp
|
||||
from .dsc_video_frame import VideoFrameDSC, CompressionInfo
|
||||
from .dsc_compression_info import create_from_pps
|
||||
271
UniTAP/common/audio_mode.py
Normal file
271
UniTAP/common/audio_mode.py
Normal file
@@ -0,0 +1,271 @@
|
||||
from enum import IntEnum
|
||||
from UniTAP.common.timestamp import Timestamp
|
||||
|
||||
|
||||
class AudioFileFormat(IntEnum):
|
||||
"""
|
||||
Describe all supported audio file formats for saving audio:
|
||||
- BIN.
|
||||
- WAV.
|
||||
"""
|
||||
UNKNOWN = -1
|
||||
BIN = 0
|
||||
WAV = 1
|
||||
|
||||
|
||||
class AudioFormat(IntEnum):
|
||||
"""
|
||||
Describe all supported audio formats:
|
||||
- PCMAudio.
|
||||
"""
|
||||
Unknown = -1
|
||||
L_PCM = 0xFFFF
|
||||
|
||||
|
||||
class AudioMode:
|
||||
"""
|
||||
Class `AudioMode` contains part information of audio: sample rate, count of bits and channel count.
|
||||
"""
|
||||
def __init__(self, sample_rate: int = 44100, bits: int = 16, channel_count: int = 2):
|
||||
self.sample_rate = sample_rate
|
||||
self.bits = bits
|
||||
self.channel_count = channel_count
|
||||
|
||||
def __str__(self):
|
||||
return f"Sample rate: {self.sample_rate}\n" \
|
||||
f"Bits: {self.bits}\n" \
|
||||
f"Channel count: {self.channel_count}\n"
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.sample_rate == other.sample_rate and \
|
||||
self.bits == other.bits and \
|
||||
self.channel_count == other.channel_count
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""
|
||||
|
||||
Check that information is valid (all values more than 0).
|
||||
|
||||
Returns:
|
||||
object of bool type.
|
||||
"""
|
||||
return self.sample_rate > 0 and \
|
||||
self.bits > 0 and \
|
||||
self.channel_count > 0
|
||||
|
||||
|
||||
class AudioFrameData:
|
||||
"""
|
||||
Class `AudioFrameData` describes captured frame from Sink (RX - receiver) side. Contains information of audio:
|
||||
`AudioMode`, samples, `AudioFormat`, frame counter, `Timestamp`, audio data.
|
||||
"""
|
||||
def __init__(self, audio_mode: AudioMode = AudioMode(), samples: int = 0,
|
||||
sample_format: AudioFormat = AudioFormat.Unknown, frame_counter: int = 0,
|
||||
timestamp: Timestamp = Timestamp(0), data: bytearray = bytearray()):
|
||||
self.__audio_mode = audio_mode
|
||||
self.__samples = samples
|
||||
self.__sample_format = sample_format
|
||||
self.__frame_counter = frame_counter
|
||||
self.__timestamp = timestamp
|
||||
self.__data = data
|
||||
|
||||
@property
|
||||
def channel_count(self) -> int:
|
||||
"""
|
||||
|
||||
Returns channel count.
|
||||
|
||||
Returns:
|
||||
object of int type.
|
||||
"""
|
||||
return self.__audio_mode.channel_count
|
||||
|
||||
@property
|
||||
def samples(self) -> int:
|
||||
"""
|
||||
|
||||
Returns samples.
|
||||
|
||||
Returns:
|
||||
object of int type.
|
||||
"""
|
||||
return self.__samples
|
||||
|
||||
@property
|
||||
def sample_size(self) -> int:
|
||||
"""
|
||||
|
||||
Returns sample size.
|
||||
|
||||
Returns:
|
||||
object of int type.
|
||||
"""
|
||||
return self.__audio_mode.bits
|
||||
|
||||
@property
|
||||
def sample_rate(self) -> int:
|
||||
"""
|
||||
|
||||
Returns sample rate.
|
||||
|
||||
Returns:
|
||||
object of int type.
|
||||
"""
|
||||
return self.__audio_mode.sample_rate
|
||||
|
||||
@property
|
||||
def sample_format(self) -> AudioFormat:
|
||||
"""
|
||||
|
||||
Returns sample format.
|
||||
|
||||
Returns:
|
||||
object of AudioFormat type.
|
||||
"""
|
||||
return self.__sample_format
|
||||
|
||||
@property
|
||||
def frame_counter(self) -> int:
|
||||
"""
|
||||
|
||||
Returns frame counter.
|
||||
|
||||
Returns:
|
||||
object of int type.
|
||||
"""
|
||||
return self.__frame_counter
|
||||
|
||||
@property
|
||||
def timestamp(self) -> Timestamp:
|
||||
"""
|
||||
|
||||
Returns timestamp.
|
||||
|
||||
Returns:
|
||||
object of Timestamp type.
|
||||
"""
|
||||
return self.__timestamp
|
||||
|
||||
@property
|
||||
def data(self) -> bytearray:
|
||||
"""
|
||||
|
||||
Returns data.
|
||||
|
||||
Returns:
|
||||
object of bytearray type.
|
||||
"""
|
||||
return self.__data
|
||||
|
||||
@channel_count.setter
|
||||
def channel_count(self, channel_count: int):
|
||||
"""
|
||||
|
||||
Allows setting new value to channel count.
|
||||
|
||||
Args:
|
||||
channel_count (int) - must be more than 0
|
||||
"""
|
||||
if channel_count <= 0:
|
||||
raise ValueError(f"Channel count must be more than 0.")
|
||||
self.__audio_mode.channel_count = channel_count
|
||||
|
||||
@samples.setter
|
||||
def samples(self, samples: int):
|
||||
"""
|
||||
|
||||
Allows setting new value to samples.
|
||||
|
||||
Args:
|
||||
samples (int) - must be more than 0
|
||||
"""
|
||||
if samples <= 0:
|
||||
raise ValueError(f"Samples must be more than 0.")
|
||||
self.__samples = samples
|
||||
|
||||
@sample_size.setter
|
||||
def sample_size(self, sample_size: int):
|
||||
"""
|
||||
|
||||
Allows setting new value to sample size.
|
||||
|
||||
Args:
|
||||
sample_size (int) - must be more than 0
|
||||
"""
|
||||
if sample_size <= 0:
|
||||
raise ValueError(f"Sample size must be more than 0.")
|
||||
self.__audio_mode.bits = sample_size
|
||||
|
||||
@sample_rate.setter
|
||||
def sample_rate(self, sample_rate: int):
|
||||
"""
|
||||
|
||||
Allows setting new value to sample rate.
|
||||
|
||||
Args:
|
||||
sample_rate (int) - must be more than 0
|
||||
"""
|
||||
if sample_rate <= 0:
|
||||
raise ValueError(f"Sample rate must be more than 0.")
|
||||
self.__audio_mode.sample_rate = sample_rate
|
||||
|
||||
@sample_format.setter
|
||||
def sample_format(self, sample_format: int):
|
||||
"""
|
||||
|
||||
Allows setting new value to sample format.
|
||||
|
||||
Args:
|
||||
sample_format (int) - must be more than 0
|
||||
"""
|
||||
if sample_format <= 0:
|
||||
raise ValueError(f"Sample format must be more than 0.")
|
||||
self.__sample_format = AudioFormat(sample_format)
|
||||
|
||||
@timestamp.setter
|
||||
def timestamp(self, timestamp: int):
|
||||
"""
|
||||
|
||||
Allows setting new value to timestamp.
|
||||
|
||||
Args:
|
||||
timestamp (int) - must be more than 0
|
||||
"""
|
||||
if timestamp <= 0:
|
||||
raise ValueError(f"Timestamp must be more than 0.")
|
||||
self.__timestamp.value = timestamp
|
||||
|
||||
@frame_counter.setter
|
||||
def frame_counter(self, frame_counter: int):
|
||||
"""
|
||||
|
||||
Allows setting new value to frame counter.
|
||||
|
||||
Args:
|
||||
frame_counter (int) - must be more than -1
|
||||
"""
|
||||
if frame_counter < 0:
|
||||
raise ValueError(f"Frame counter must be more than 0.")
|
||||
self.__frame_counter = frame_counter
|
||||
|
||||
@data.setter
|
||||
def data(self, value: bytearray):
|
||||
"""
|
||||
|
||||
Allows setting new value to data.
|
||||
|
||||
Args:
|
||||
value (bytearray) - length of value must be more than 0
|
||||
"""
|
||||
if len(value) <= 0:
|
||||
raise ValueError(f"Audio data length must be more than 0.")
|
||||
self.__data = value
|
||||
|
||||
def __str__(self):
|
||||
return f"Sample rate: {self.sample_rate}\n" \
|
||||
f"Bits (Sample size): {self.sample_size}\n" \
|
||||
f"Channel count: {self.channel_count}\n" \
|
||||
f"Sample format: {self.sample_format.name}\n" \
|
||||
f"Timestamp: {self.timestamp.to_n_sec} n sec\n" \
|
||||
f"Frame counter: {self.frame_counter}\n" \
|
||||
f"Length of data: {self.data} bytes\n"
|
||||
111
UniTAP/common/color_info.py
Normal file
111
UniTAP/common/color_info.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class ColorInfo:
|
||||
"""
|
||||
|
||||
Class contains information of frame `ColorFormat`, `DynamicRange`, `Colorimetry`.
|
||||
|
||||
"""
|
||||
class ColorFormat(IntEnum):
|
||||
"""
|
||||
Contains values of possible color format.
|
||||
"""
|
||||
CF_NONE = 0
|
||||
CF_UNKNOWN = 1
|
||||
CF_RGB = 2
|
||||
CF_YCbCr_422 = 3
|
||||
CF_YCbCr_444 = 4
|
||||
CF_YCbCr_420 = 5
|
||||
CF_IDO_DEFINED = 6
|
||||
CF_Y_ONLY = 7
|
||||
CF_RAW = 8
|
||||
CF_DSC = 9
|
||||
|
||||
class DynamicRange(IntEnum):
|
||||
"""
|
||||
Contains values of possible dynamic range.
|
||||
"""
|
||||
DR_UNKNOWN = -1
|
||||
DR_VESA = 0
|
||||
DR_CTA = 1
|
||||
|
||||
class Colorimetry(IntEnum):
|
||||
"""
|
||||
Contains values of possible colorimetry.
|
||||
"""
|
||||
CM_NONE = 0
|
||||
CM_RESERVED = 1
|
||||
CM_sRGB = 2
|
||||
CM_SMPTE_170M = 3
|
||||
CM_ITUR_BT601 = 4
|
||||
CM_ITUR_BT709 = 5
|
||||
CM_xvYCC601 = 6
|
||||
CM_xvYCC709 = 7
|
||||
CM_sYCC601 = 8
|
||||
CM_AdobeYCC601 = 9
|
||||
CM_AdobeRGB = 10
|
||||
CM_ITUR_BT2020_YcCbcCrc = 11
|
||||
CM_ITUR_BT2020_YCbCr = 12
|
||||
CM_ITUR_BT2020_RGB = 13
|
||||
CM_RGB_WIDE_GAMUT_FIX = 14
|
||||
CM_RGB_WIDE_GAMUT_FLT = 15
|
||||
CM_DCI_P3 = 16
|
||||
CM_DICOM_1_4_GRAY_SCALE = 17
|
||||
CM_CUSTOM_COLOR_PROFILE = 18
|
||||
|
||||
CM_opYCC601 = CM_AdobeYCC601
|
||||
CM_opRGB = CM_AdobeRGB
|
||||
|
||||
__COMPONENT_MULTIPLIER = {
|
||||
ColorFormat.CF_RGB: 3,
|
||||
ColorFormat.CF_YCbCr_422: 2,
|
||||
ColorFormat.CF_YCbCr_444: 3,
|
||||
ColorFormat.CF_YCbCr_420: 3 / 2,
|
||||
ColorFormat.CF_Y_ONLY: 1,
|
||||
ColorFormat.CF_RAW: 3
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.colorimetry = self.Colorimetry.CM_NONE
|
||||
self.color_format = self.ColorFormat.CF_NONE
|
||||
self.dynamic_range = self.DynamicRange.DR_VESA
|
||||
self.bpc = 0
|
||||
|
||||
def __str__(self):
|
||||
return f"Color format: {self.color_format.name}\n" \
|
||||
f"Colorimetry: {self.colorimetry.name}\n" \
|
||||
f"Dynamic Range: {self.dynamic_range.name}\n" \
|
||||
f"BPC: {self.bpc}"
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.bpc == other.bpc and\
|
||||
self.color_format == other.color_format and\
|
||||
self.colorimetry == other.colorimetry and\
|
||||
self.dynamic_range == other.dynamic_range
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""
|
||||
|
||||
Check that information is valid (not equal NONE state and bpc more than 0).
|
||||
|
||||
Returns:
|
||||
object of bool type.
|
||||
"""
|
||||
return self.bpc > 0 and\
|
||||
self.color_format != self.ColorFormat.CF_NONE and\
|
||||
self.colorimetry != self.Colorimetry.CM_NONE
|
||||
|
||||
@property
|
||||
def bpp(self) -> int:
|
||||
"""
|
||||
|
||||
Returns calculated bits per pixel for this color info (except DSC). 0 if color info is not valid.
|
||||
|
||||
Returns:
|
||||
object of int type.
|
||||
"""
|
||||
if self.is_valid() and self.color_format != self.ColorFormat.CF_DSC:
|
||||
return round(self.bpc * self.__COMPONENT_MULTIPLIER.get(self.color_format, 1))
|
||||
else:
|
||||
return 0
|
||||
64
UniTAP/common/data_info.py
Normal file
64
UniTAP/common/data_info.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class DataInfo:
|
||||
"""
|
||||
|
||||
Class contains information of frame `Packing`, `ComponentOrder`, `Alignment`.
|
||||
|
||||
"""
|
||||
class Packing(IntEnum):
|
||||
"""
|
||||
Contains values of possible packing.
|
||||
"""
|
||||
P_UNKNOWN = 0
|
||||
P_PLANAR = 1
|
||||
P_PACKED = 2
|
||||
|
||||
class ComponentOrder(IntEnum):
|
||||
"""
|
||||
Contains values of possible component order.
|
||||
"""
|
||||
CO_UNKNOWN = 0
|
||||
CO_UCDRX = 1
|
||||
CO_RGB = 2
|
||||
CO_RGBA = 3
|
||||
CO_BGR = 4
|
||||
CO_BGRA = 5
|
||||
CO_YCbCr = 6
|
||||
CO_CbY0CrY1 = 7
|
||||
|
||||
class Alignment(IntEnum):
|
||||
"""
|
||||
Contains values of possible alignment.
|
||||
"""
|
||||
A_UNKNOWN = 0
|
||||
A_MSB = 1
|
||||
A_LSB = 2
|
||||
|
||||
def __init__(self):
|
||||
self.packing = self.Packing.P_UNKNOWN
|
||||
self.component_order = self.ComponentOrder.CO_UNKNOWN
|
||||
self.alignment = self.Alignment.A_UNKNOWN
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""
|
||||
|
||||
Check that information is valid (not equal UNKNOWN state).
|
||||
|
||||
Returns:
|
||||
object of bool type.
|
||||
"""
|
||||
return self.packing != self.Packing.P_UNKNOWN and\
|
||||
self.component_order != self.ComponentOrder.CO_UNKNOWN and\
|
||||
self.alignment != self.Alignment.A_UNKNOWN
|
||||
|
||||
def __str__(self):
|
||||
return f"Packing: {self.packing.name}\n" \
|
||||
f"Component Order: {self.component_order.name}\n" \
|
||||
f"Alignment: {self.alignment.name}\n"
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.packing == other.packing and \
|
||||
self.component_order == other.component_order and \
|
||||
self.alignment == other.alignment
|
||||
96
UniTAP/common/dsc_compression_info.py
Normal file
96
UniTAP/common/dsc_compression_info.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class DscCompressionInfo:
|
||||
"""
|
||||
|
||||
Class contains information about DSC compression used on frame.
|
||||
|
||||
"""
|
||||
class DscColorFormat(IntEnum):
|
||||
"""
|
||||
Contains values of possible color format.
|
||||
"""
|
||||
CF_NONE = -1
|
||||
CF_RGB = 0
|
||||
CF_YCbCr_422 = 1
|
||||
CF_YCbCr_444 = 2
|
||||
CF_YCbCr_420 = 3
|
||||
CF_Simple_422 = 4
|
||||
|
||||
def __init__(self):
|
||||
self.color_format = DscCompressionInfo.DscColorFormat.CF_NONE
|
||||
self.bpp = 0
|
||||
self.is_block_prediction_enabled = False
|
||||
self.h_slice_size = 0
|
||||
self.v_slice_size = 0
|
||||
self.buffer_bit_depth = 0
|
||||
self.version = (0, 0)
|
||||
self.is_simple_as_444 = False
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""
|
||||
Return state of the video frame and check color_format, bpp, h and v slice_size and DSC version.
|
||||
If everything ok, return True, otherwise - False.
|
||||
|
||||
Returns:
|
||||
object of `bool` type.
|
||||
"""
|
||||
return self.color_format is not None and\
|
||||
self.bpp > 0 and\
|
||||
self.h_slice_size > 0 and\
|
||||
self.v_slice_size > 0 and\
|
||||
self.version in [(1, 2), (1, 1)]
|
||||
|
||||
|
||||
def create_from_pps(pps_bytearray: bytearray) -> DscCompressionInfo:
|
||||
"""
|
||||
Fill structure 'DscCompressionInfo' from PPS header of the DSC image.
|
||||
|
||||
Returns:
|
||||
object of `DscCompressionInfo` type.
|
||||
"""
|
||||
if b'DSCF' not in pps_bytearray:
|
||||
if len(pps_bytearray) < 128:
|
||||
raise ValueError("Incorrect PPS size!")
|
||||
pps = pps_bytearray
|
||||
else:
|
||||
if len(pps_bytearray) < 132:
|
||||
raise ValueError("Incorrect PPS size!")
|
||||
pps = pps_bytearray[4:]
|
||||
|
||||
is_yuv = ((pps[4] >> 4) & 1) == 0
|
||||
is_simple422 = ((pps[4] >> 3) & 1) == 1
|
||||
is_native422 = (pps[88] & 1) == 1
|
||||
is_native420 = ((pps[88] >> 1) & 1) == 1
|
||||
width = (pps[8] << 8) | pps[9]
|
||||
height = (pps[6] << 8) | pps[7]
|
||||
slice_height = (pps[10] << 8) | pps[11]
|
||||
slice_width = (pps[12] << 8) | pps[13]
|
||||
|
||||
info = DscCompressionInfo()
|
||||
|
||||
info.version = (pps[0] >> 4 & 0xf, pps[0] & 0xf)
|
||||
info.buffer_bit_depth = pps[3] & 0xf
|
||||
info.is_block_prediction_enabled = bool(pps[4] >> 5 & 0x1)
|
||||
info.h_slice_size = int(width / slice_width)
|
||||
info.v_slice_size = int(height / slice_height)
|
||||
|
||||
if is_native422 or is_native420:
|
||||
info.bpp = int(pps[4] & 0x3 << 8 | pps[5]) / 2
|
||||
else:
|
||||
info.bpp = int(pps[4] & 0x3 << 8 | pps[5])
|
||||
|
||||
if is_yuv:
|
||||
if is_simple422:
|
||||
info.color_format = DscCompressionInfo.DscColorFormat.CF_Simple_422
|
||||
elif is_native422:
|
||||
info.color_format = DscCompressionInfo.DscColorFormat.CF_YCbCr_422
|
||||
elif is_native420:
|
||||
info.color_format = DscCompressionInfo.DscColorFormat.CF_YCbCr_420
|
||||
else:
|
||||
info.color_format = DscCompressionInfo.DscColorFormat.CF_YCbCr_444
|
||||
else:
|
||||
info.color_format = DscCompressionInfo.DscColorFormat.CF_RGB
|
||||
|
||||
return info
|
||||
29
UniTAP/common/dsc_video_frame.py
Normal file
29
UniTAP/common/dsc_video_frame.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from .dsc_compression_info import DscCompressionInfo as CompressionInfo
|
||||
from .video_frame import VideoFrame
|
||||
|
||||
|
||||
class VideoFrameDSC(VideoFrame):
|
||||
|
||||
"""
|
||||
Class `VideoFrameDSC` contains base information about DSC compressed video frame:
|
||||
- Height (int).
|
||||
- Width (int).
|
||||
- Data (bytearray).
|
||||
- Color info (object of `ColorInfo`).
|
||||
- Data info (object of `DataInfo`).
|
||||
- Timestamp (object of `Timestamp`).
|
||||
- CompressionInfo (object of `CompressionInfo`)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.compression_info = CompressionInfo()
|
||||
|
||||
def is_compressed(self) -> bool:
|
||||
"""
|
||||
Return state of the video frame, compressed it or not.
|
||||
|
||||
Returns:
|
||||
object of `bool` type.
|
||||
"""
|
||||
return not self._compressed
|
||||
64
UniTAP/common/timestamp.py
Normal file
64
UniTAP/common/timestamp.py
Normal file
@@ -0,0 +1,64 @@
|
||||
class Timestamp:
|
||||
|
||||
"""
|
||||
|
||||
Class contains information about timestamp in several representation variant:
|
||||
- Seconds `to_sec`.
|
||||
- Milliseconds `to_m_sec`.
|
||||
- Microseconds `to_u_sec`.
|
||||
- Nanoseconds `to_n_sec` or `value`.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, nano_secs: int):
|
||||
self.__value = nano_secs
|
||||
|
||||
@property
|
||||
def to_sec(self) -> float:
|
||||
"""
|
||||
Returns time in seconds.
|
||||
"""
|
||||
return self.__value / (10 ** 9)
|
||||
|
||||
@property
|
||||
def to_m_sec(self) -> float:
|
||||
"""
|
||||
Returns time milliseconds seconds.
|
||||
"""
|
||||
return self.__value / (10 ** 6)
|
||||
|
||||
@property
|
||||
def to_u_sec(self) -> float:
|
||||
"""
|
||||
Returns time microseconds seconds.
|
||||
"""
|
||||
return self.__value / (10 ** 3)
|
||||
|
||||
@property
|
||||
def to_n_sec(self) -> float:
|
||||
"""
|
||||
Returns time nanoseconds seconds.
|
||||
"""
|
||||
return self.__value
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
"""
|
||||
Returns time nanoseconds seconds.
|
||||
"""
|
||||
return self.__value
|
||||
|
||||
@value.setter
|
||||
def value(self, value: int):
|
||||
"""
|
||||
Set time in nanoseconds seconds.
|
||||
"""
|
||||
if value <= 0:
|
||||
raise ValueError(f"Value of timestamp cannot be less than 0.")
|
||||
self.__value = value
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.to_u_sec} milliseconds"
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__value == other.__value
|
||||
90
UniTAP/common/timing.py
Normal file
90
UniTAP/common/timing.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class Timing:
|
||||
|
||||
"""
|
||||
Class `Timing` contains information about Timing: all resolutions, timing id, frame rate, `AspectRatio`,
|
||||
`Standard`, `ReduceBlanking`.
|
||||
"""
|
||||
|
||||
class Standard(IntEnum):
|
||||
"""
|
||||
Class `Standard` contains all possible variants of timing standards.
|
||||
"""
|
||||
SD_NONE = 0
|
||||
SD_CVT = 1
|
||||
SD_DMT = 2
|
||||
SD_CTA = 3
|
||||
SD_UGF = 4
|
||||
SD_OVT = 5
|
||||
|
||||
class AspectRatio(IntEnum):
|
||||
"""
|
||||
Class `AspectRatio` contains all possible variants of timing aspect ratio.
|
||||
"""
|
||||
AR_NONE = 0
|
||||
AR_4_3 = 1
|
||||
AR_16_9 = 2
|
||||
|
||||
class ReduceBlanking(IntEnum):
|
||||
"""
|
||||
Class `ReduceBlanking` contains all possible variants of timing reduce blanking.
|
||||
"""
|
||||
RB_NONE = 0
|
||||
RB1 = 1
|
||||
RB2 = 2
|
||||
RB3 = 3
|
||||
|
||||
def __init__(self):
|
||||
self.frame_rate = 0.0
|
||||
self.hactive = 0
|
||||
self.vactive = 0
|
||||
self.htotal = 0
|
||||
self.vtotal = 0
|
||||
self.hstart = 0
|
||||
self.vstart = 0
|
||||
self.hswidth = 0
|
||||
self.vswidth = 0
|
||||
self.id = 0
|
||||
self.aspect_ratio = self.AspectRatio.AR_NONE
|
||||
self.standard = self.Standard.SD_NONE
|
||||
self.reduce_blanking = self.ReduceBlanking.RB_NONE
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.frame_rate / 1000:03} " \
|
||||
f"{self.htotal} {self.hstart} {self.hactive} {self.hswidth:+} " \
|
||||
f"{self.vtotal} {self.vstart} {self.vactive} {self.vswidth:+}"
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
return self.hactive == other.hactive and self.vactive == other.vactive and self.htotal == other.htotal and \
|
||||
self.vtotal == other.vtotal and self.hstart == other.hstart and self.vstart == other.vstart and \
|
||||
self.hswidth == other.hswidth and self.vswidth == other.vswidth
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""
|
||||
Check that timing is correct (Resolutions and frame rate more than 0)
|
||||
|
||||
Returns:
|
||||
is valid (bool) - valid (True) or not (False)
|
||||
"""
|
||||
return self.hactive > 0 and \
|
||||
self.vactive > 0 and \
|
||||
self.htotal > 0 and \
|
||||
self.vtotal > 0 and \
|
||||
self.hstart > 0 and \
|
||||
self.vstart > 0 and \
|
||||
self.frame_rate > 0
|
||||
|
||||
@property
|
||||
def pixel_clock(self) -> float:
|
||||
"""
|
||||
Returns calculated pixel clock required for this video mode in MHz. 0.0 if video mode is not valid.
|
||||
|
||||
Returns:
|
||||
pixel clock (float)
|
||||
"""
|
||||
if self.is_valid():
|
||||
return round(self.htotal * self.vtotal * self.frame_rate / 1000000000.0, 3)
|
||||
else:
|
||||
return 0.0
|
||||
94
UniTAP/common/video_frame.py
Normal file
94
UniTAP/common/video_frame.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import os
|
||||
|
||||
from enum import IntEnum
|
||||
from .color_info import ColorInfo
|
||||
from .data_info import DataInfo
|
||||
from .timestamp import Timestamp
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class ImageFileFormat(IntEnum):
|
||||
"""
|
||||
Describe all supported image file formats for saving `VideoFrame`:
|
||||
- BIN.
|
||||
- PPM.
|
||||
- BMP.
|
||||
- DSC.
|
||||
"""
|
||||
IFF_BIN = 0
|
||||
IFF_PPM = 1
|
||||
IFF_BMP = 2
|
||||
IFF_DSC = 3
|
||||
|
||||
|
||||
class VideoFrame:
|
||||
|
||||
"""
|
||||
Class `VideoFrame` contains base information about video frame:
|
||||
- Height (int).
|
||||
- Width (int).
|
||||
- Data (bytearray).
|
||||
- Color info (object of `ColorInfo`).
|
||||
- Data info (object of `DataInfo`).
|
||||
- Timestamp (object of `Timestamp`).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.width = 0
|
||||
self.height = 0
|
||||
self.data = bytearray()
|
||||
self.color_info = ColorInfo()
|
||||
self.data_info = DataInfo()
|
||||
self.timestamp = Timestamp(0)
|
||||
self._compressed = False
|
||||
|
||||
def __str__(self):
|
||||
return f"Resolution: {self.width}x{self.height}\n" \
|
||||
f"Data length: {len(self.data)}\n" \
|
||||
f"Color Info:\n{self.color_info}\n" \
|
||||
f"Data Info:\n{self.data_info}\n" \
|
||||
f"Timestamp:\n{self.timestamp}\n"
|
||||
|
||||
def is_compressed(self) -> bool:
|
||||
return self._compressed
|
||||
|
||||
|
||||
def get_vf_from_image(path: str, width: int, height: int) -> VideoFrame:
|
||||
"""
|
||||
|
||||
Function allows getting prepared object of `VideoFrame` from external (custom) image.
|
||||
|
||||
Args:
|
||||
path (str) - full path to image.
|
||||
width (int) - width of image.
|
||||
height (int) - height of image.
|
||||
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError(f"Image: {path} is missing!")
|
||||
|
||||
image = Image.open(path)
|
||||
|
||||
if width < image.size[0] and height < image.size[1]:
|
||||
image = image.crop((0, 0, width, height))
|
||||
|
||||
size = image.size
|
||||
|
||||
if size == [0, 0]:
|
||||
raise ValueError("Invalid image size.")
|
||||
|
||||
convert_image = image.convert('RGB').resize(size=(width, height), resample=Image.Resampling.LANCZOS)
|
||||
|
||||
video_frame = VideoFrame()
|
||||
video_frame.data = bytearray(convert_image.tobytes())
|
||||
video_frame.width = width
|
||||
video_frame.height = height
|
||||
video_frame.color_info.bpc = 8
|
||||
video_frame.color_info.color_format = ColorInfo.ColorFormat.CF_RGB
|
||||
video_frame.color_info.colorimetry = ColorInfo.Colorimetry.CM_sRGB
|
||||
# video_frame.color_info.dynamic_range = ColorInfo.DynamicRange.DR_VESA
|
||||
video_frame.data_info.packing = DataInfo.Packing.P_PACKED
|
||||
video_frame.data_info.component_order = DataInfo.ComponentOrder.CO_RGB
|
||||
video_frame.data_info.alignment = DataInfo.Alignment.A_LSB
|
||||
|
||||
return video_frame
|
||||
52
UniTAP/common/video_mode.py
Normal file
52
UniTAP/common/video_mode.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from .timing import Timing
|
||||
from .color_info import ColorInfo
|
||||
|
||||
|
||||
class VideoMode:
|
||||
|
||||
"""
|
||||
|
||||
Class `VideoMode` combines information about `Timing` and `ColorInfo`.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, timing: Timing = Timing(), color_info: ColorInfo = ColorInfo()):
|
||||
self.timing = timing
|
||||
self.color_info = color_info
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.timing.frame_rate / 1000:03} " \
|
||||
f"{self.timing.htotal} {self.timing.hstart} " \
|
||||
f"{self.timing.hactive} {self.timing.hswidth:+} " \
|
||||
f"{self.timing.vtotal} {self.timing.vstart} " \
|
||||
f"{self.timing.vactive} {self.timing.vswidth:+} " \
|
||||
f"{self.color_info.color_format.name}/" \
|
||||
f"{self.color_info.colorimetry.name}/" \
|
||||
f"{self.color_info.dynamic_range.name} {self.color_info.bpc}"
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.timing == other.timing and self.color_info == other.color_info
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""
|
||||
|
||||
Check that `Timing` and `ColorInfo` of Video mode is valid.
|
||||
|
||||
Returns:
|
||||
object of bool type - Video mode valid or not
|
||||
"""
|
||||
return self.timing.is_valid() and self.color_info.is_valid()
|
||||
|
||||
@property
|
||||
def bit_rate(self) -> float:
|
||||
"""
|
||||
|
||||
Returns calculated bit rate required for this video mode in Gbps. 0 if video mode is not valid
|
||||
|
||||
Returns:
|
||||
object of float type
|
||||
"""
|
||||
if self.is_valid():
|
||||
return round(self.timing.pixel_clock * self.color_info.bpp / 1000.0, 3)
|
||||
else:
|
||||
return 0.0
|
||||
Reference in New Issue
Block a user