1.1.0版本

This commit is contained in:
xinzhu.yin
2026-04-16 16:51:05 +08:00
commit c157e774e5
333 changed files with 70759 additions and 0 deletions

View 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
View 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
View 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

View 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

View 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

View 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

View 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
View 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

View 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

View 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