Skip to content

Commit

Permalink
Merge pull request #3 from abhinaukumar/standards-update-1
Browse files Browse the repository at this point in the history
Update standards to include bitdepth and pixel datatype information
  • Loading branch information
abhinaukumar authored Sep 6, 2023
2 parents 560eafd + 0cf442a commit 0e5b1a7
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 40 deletions.
65 changes: 46 additions & 19 deletions videolib/standards.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Tuple, Callable, Dict
from typing import Tuple, Callable, Dict, Union
import warnings
from dataclasses import dataclass

import numpy as np
from . import nonlinearities


Expand All @@ -18,6 +19,8 @@ class Standard:
linear_range: float #: Linear light value range
oetf: Callable #: Opto-Electrical (or as appropriate, inverse Electro-Optical) Transfer Function
eotf: Callable #: Electro-Optical Transfer Function
bitdepth: int #: Bitdepth of the standard
dtype: Union[str, np.dtype] #: Data-type of pixel values being read from files

@property
def primaries(self) -> Dict[str, Tuple[float, float]]:
Expand All @@ -35,82 +38,106 @@ def range(self) -> int:
Non-linear encoded range
Returns:
int: Range of non-linear encoded values. Equal to :math:`2^{\\text{bitdepth}}-1`.
int: Range of non-linear encoded values. Equal to :math:`2^{\\text{bitdepth}}-1` if integer dtype, else 1.0.
'''
if self in low_bitdepth_standards:
return (1 << 8) - 1
elif self in high_bitdepth_standards:
return (1 << 10) - 1
else:
warnings.warn('Using default range of 1.0')
return 1
# raise ValueError('Cannot find range for this standard')
if issubclass(np.dtype(self.dtype).type, np.floating):
return 1.0
return (1 << self.bitdepth) - 1

#: The sRGB standard (Recommended default)
sRGB = Standard(
name='sRGB',
white_xy=(0.3127, 0.3290),
red_xy=(0.64, 0.33), green_xy=(0.30, 0.60), blue_xy=(0.15, 0.06),
oetf=nonlinearities.oetf_dict['sRGB'], eotf=nonlinearities.eotf_dict['sRGB'], linear_range=100
oetf=nonlinearities.oetf_dict['sRGB'], eotf=nonlinearities.eotf_dict['sRGB'], linear_range=100,
bitdepth=8, dtype='uint8'
)

#: 10-bit version of the sRGB standard
sRGB_10 = Standard(
name='sRGB.10',
white_xy=(0.3127, 0.3290),
red_xy=(0.64, 0.33), green_xy=(0.30, 0.60), blue_xy=(0.15, 0.06),
oetf=nonlinearities.oetf_dict['sRGB'], eotf=nonlinearities.eotf_dict['sRGB'], linear_range=100,
bitdepth=10, dtype='uint16'
)

#: The ITU Rec.709 standard
rec_709 = Standard(
name='Rec.709',
white_xy=(0.3127, 0.3290),
red_xy=(0.64, 0.33), green_xy=(0.30, 0.60), blue_xy=(0.15, 0.06),
oetf=nonlinearities.oetf_dict['rec.709'], eotf=nonlinearities.eotf_dict['rec.709'], linear_range=100
oetf=nonlinearities.oetf_dict['rec.709'], eotf=nonlinearities.eotf_dict['rec.709'], linear_range=100,
bitdepth=8, dtype='uint8'
)

#: 10-bit version of the ITU Rec.709 standard
rec_709_10 = Standard(
name='Rec.709.10',
white_xy=(0.3127, 0.3290),
red_xy=(0.64, 0.33), green_xy=(0.30, 0.60), blue_xy=(0.15, 0.06),
oetf=nonlinearities.oetf_dict['rec.709'], eotf=nonlinearities.eotf_dict['rec.709'], linear_range=100,
bitdepth=10, dtype='uint16'
)

#: The ITU Rec.2020 UHD standard
rec_2020 = Standard(
name='Rec.2020',
white_xy=(0.3127, 0.3290),
red_xy=(0.708, 0.292), green_xy=(0.170, 0.797), blue_xy=(0.131, 0.046),
oetf=nonlinearities.oetf_dict['rec.2020'], eotf=nonlinearities.eotf_dict['rec.2020'], linear_range=100
oetf=nonlinearities.oetf_dict['rec.2020'], eotf=nonlinearities.eotf_dict['rec.2020'], linear_range=100,
bitdepth=10, dtype='uint16'
)

#: The ITU Rec.2100 standard using Perceptual Quantizer encoding
rec_2100_pq = Standard(
name='Rec.2100.PQ',
white_xy=(0.3127, 0.3290),
red_xy=(0.708, 0.292), green_xy=(0.170, 0.797), blue_xy=(0.131, 0.046),
oetf=nonlinearities.oetf_dict['pq'], eotf=nonlinearities.eotf_dict['pq'], linear_range=10000
oetf=nonlinearities.oetf_dict['pq'], eotf=nonlinearities.eotf_dict['pq'], linear_range=10000,
bitdepth=10, dtype='uint16'
)

#: The ITU Rec.2100 standard using Hybrid Log-Gamma encoding
rec_2100_hlg = Standard(
name='Rec.2100.HLG',
white_xy=(0.3127, 0.3290),
red_xy=(0.708, 0.292), green_xy=(0.170, 0.797), blue_xy=(0.131, 0.046),
oetf=nonlinearities.oetf_dict['hlg'], eotf=nonlinearities.eotf_dict['hlg'], linear_range=1000
oetf=nonlinearities.oetf_dict['hlg'], eotf=nonlinearities.eotf_dict['hlg'], linear_range=1000,
bitdepth=10, dtype='uint16'
)

#: The Radiance HDR format
radiance_hdr = Standard(
name='RadianceHDR',
white_xy=(0.3333, 0.3333),
red_xy=(0.640, 0.330), green_xy=(0.290, 0.600), blue_xy=(0.150, 0.060),
oetf=nonlinearities.oetf_dict['sRGB'], eotf=nonlinearities.eotf_dict['sRGB'], linear_range=None # Peak luminance not defined
oetf=nonlinearities.oetf_dict['sRGB'], eotf=nonlinearities.eotf_dict['sRGB'], linear_range=None, # Peak luminance not defined
bitdepth=64, dtype='float64'
)

#: List of supported standards
supported_standards = [
sRGB,
sRGB_10,
rec_709,
rec_709_10,
rec_2020,
rec_2100_pq,
rec_2100_hlg
rec_2100_hlg,
radiance_hdr
]

#: List of 8-bit standards
low_bitdepth_standards = [
standards_8bit = [
sRGB,
rec_709
]

#: List of 10-bit standards
high_bitdepth_standards = [
standards_10bit = [
sRGB_10,
rec_709_10,
rec_2020,
rec_2100_pq,
rec_2100_hlg
Expand Down
28 changes: 7 additions & 21 deletions videolib/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,6 @@ def __init__(
self.dither: bool = dither
self._base_datatype = 'yuv' # Use non-linear YUV as the base datatype

if self.standard in standards.low_bitdepth_standards:
self._frame_dtype = np.uint8
elif self.standard in standards.high_bitdepth_standards:
self._frame_dtype = np.uint16
else:
raise ValueError('Bit depth unknown for {}'.format(self.standard.name))

if self.quantization is None and self.dither is True:
warn('Dithering is not applied when quantization is not applied.', RuntimeWarning)
elif self.quantization is not None and (not isinstance(self.quantization, int) or self.quantization < 1):
Expand Down Expand Up @@ -315,14 +308,7 @@ def __init__(
self.quantization: int = quantization
self.dither: bool = dither

if self.standard in standards.low_bitdepth_standards:
self._frame_dtype = np.uint8
self._frame_stride = 1.5
elif self.standard in standards.high_bitdepth_standards:
self._frame_dtype = np.uint16
self._frame_stride = 3
else:
raise ValueError('Bit depth unknown for {}'.format(self.standard.name))
self._frame_stride = 1.5 * np.dtype(self.standard).itemsize

if self.quantization is None and self.dither is True:
warn('Dithering is not applied when quantization is not applied.', RuntimeWarning)
Expand All @@ -342,7 +328,7 @@ def __init__(

if format not in ['encoded', 'raw']:
raise ValueError('Invalid format. Must be one of \'encoded\' or \'raw\'.')
if self.standard in standards.high_bitdepth_standards and format != 'raw':
if self.standard.dtype != np.uint8 and format != 'raw':
raise ValueError(f'Format \'{format}\' is not supported for videos of standard {self.standard.name}.')
else:
self.format = format
Expand Down Expand Up @@ -433,9 +419,9 @@ def get_frame(self, frame_ind: int) -> Frame:
if self.format == 'raw':
self._file_object.seek(int(self.width * self.height * frame_ind * self._frame_stride))

y1 = np.fromfile(self._file_object, self._frame_dtype, (self.width * self.height))
u1 = np.fromfile(self._file_object, self._frame_dtype, (self.width * self.height) >> 2)
v1 = np.fromfile(self._file_object, self._frame_dtype, (self.width * self.height) >> 2)
y1 = np.fromfile(self._file_object, self.standard.dtype, (self.width * self.height))
u1 = np.fromfile(self._file_object, self.standard.dtype, (self.width * self.height) >> 2)
v1 = np.fromfile(self._file_object, self.standard.dtype, (self.width * self.height) >> 2)

y = np.reshape(y1, (self.height, self.width)).astype('float64')
u = np.reshape(u1, (self.height >> 1, self.width >> 1)).repeat(2, axis=0).repeat(2, axis=1).astype('float64')
Expand Down Expand Up @@ -496,7 +482,7 @@ def write_yuv_frame(self, yuv: np.ndarray) -> None:
if self.format == 'raw':
u_sub = (yuv[::2, ::2, 1] + yuv[1::2, ::2, 1] + yuv[1::2, 1::2, 1] + yuv[::2, 1::2, 1]) / 4
v_sub = (yuv[::2, ::2, 2] + yuv[1::2, ::2, 2] + yuv[1::2, 1::2, 2] + yuv[::2, 1::2, 2]) / 4
yuv420 = np.concatenate([np.ravel(yuv[..., 0]), np.ravel(u_sub), np.ravel(v_sub)]).astype(self._frame_dtype)
yuv420 = np.concatenate([np.ravel(yuv[..., 0]), np.ravel(u_sub), np.ravel(v_sub)]).astype(self.standard.dtype)
yuv420.tofile(self._file_object)
elif self.format == 'encoded':
self.write_rgb_frame(cvt_color.yuv2rgb(yuv))
Expand All @@ -515,7 +501,7 @@ def write_rgb_frame(self, rgb: np.ndarray) -> None:
yuv = cvt_color.rgb2yuv(rgb, self.standard)
self.write_yuv_frame(yuv)
else:
self._file_object.writeFrame(rgb.astype(self._frame_dtype))
self._file_object.writeFrame(rgb.astype(self.standard.dtype))
self.num_frames += 1

def write_frame(self, frame: Frame) -> None:
Expand Down

0 comments on commit 0e5b1a7

Please sign in to comment.