2424BT709 = [0.2126 , 0.0722 ]
2525
2626
27+ def digital_round (x : float , env : Environment ) -> float :
28+ """Rounding for digital values."""
29+
30+ return alg .clamp (alg .round_half_up (x ), 0 , env .max_integer_size )
31+
32+
2733class Environment :
2834 """Environment."""
2935
@@ -33,13 +39,14 @@ def __init__(
3339 kr : float ,
3440 kb : float ,
3541 integer : bool = False ,
36- output : str = 'standard' ,
42+ standard : bool = False ,
3743 bit_depth : int = 8
3844 ) -> None :
3945 """Initialize."""
4046
41- if bit_depth not in (8 , 10 , 12 , 14 ):
42- raise ValueError (f"Unsupported bit depth of '{ bit_depth } '" )
47+ self .max_integer_size = (1 << bit_depth ) - 1
48+ self .standard = standard
49+ self .integer = integer
4350
4451 # Construct the Y'CbCr matrix
4552 kg = 1 - kr - kb
@@ -51,33 +58,34 @@ def __init__(
5158 self .ycbcr_to_rgb = alg .inv (self .rgb_to_ycbcr )
5259
5360 # Standard form which removes negative values and adds headroom/footroom
54- if output == ' standard' :
61+ if standard :
5562 self .y_scale = 219 * (1 << (bit_depth - 8 )) # type: float
5663 self .y_offset = 1 << (bit_depth - 4 ) # type: float
5764 self .c_scale = 224 * (1 << (bit_depth - 8 )) # type: float
5865 self .c_offset = 1 << (bit_depth - 1 ) # type: float
66+
5967 # Removes negative values but extends values to full range without adding headroom/footroom
60- elif output == 'full' :
61- self .y_scale = (1 << bit_depth ) - 1
62- self .y_offset = 0
63- self .c_scale = (1 << bit_depth ) - 1
64- self .c_offset = 1 << (bit_depth - 1 )
65- # Negative values remain unchanged
66- elif output == 'default' :
67- self .y_scale = (1 << bit_depth ) - 1
68- self .y_offset = 0
69- self .c_scale = (1 << bit_depth ) - 1
70- self .c_offset = 0
68+ # The default form cannot be in unsigned integer form and must be shifted
7169 else :
72- raise ValueError (f"Unrecognized output '{ output } '" )
70+ if integer :
71+ self .y_scale = self .max_integer_size
72+ self .y_offset = 0
73+ self .c_scale = self .max_integer_size
74+ self .c_offset = 1 << (bit_depth - 1 )
75+
76+ # Floating point should revert to normal
77+ else :
78+ self .y_scale = self .max_integer_size
79+ self .y_offset = 0
80+ self .c_scale = self .max_integer_size
81+ self .c_offset = 0
7382
7483 # Scale integer range down to 0 - 1
7584 if not integer :
76- div = (1 << bit_depth ) - 1
77- self .y_scale /= div
78- self .y_offset /= div
79- self .c_scale /= div
80- self .c_offset /= div
85+ self .y_scale /= self .max_integer_size
86+ self .y_offset /= self .max_integer_size
87+ self .c_scale /= self .max_integer_size
88+ self .c_offset /= self .max_integer_size
8189
8290 # Calculate minimum and maximum ranges for color channels
8391 self .y_range = [self .y_offset + 0 * self .y_scale , self .y_offset + 1 * self .y_scale ]
@@ -89,6 +97,10 @@ class YCbCr(Luminant, Space):
8997
9098 ENV : Environment
9199
100+ CHANNEL_ALIASES = {
101+ 'lightness' : 'y'
102+ }
103+
92104 def lightness_name (self ) -> str :
93105 """Get lightness name."""
94106
@@ -97,33 +109,44 @@ def lightness_name(self) -> str:
97109 def is_achromatic (self , coords : Vector ) -> bool :
98110 """Check if color is achromatic."""
99111
100- o = self .ENV .c_offset
101- s = self .ENV .c_scale
102- return alg .rect_to_polar ((coords [1 ] - o ) / s , (coords [2 ] - o ) / s )[0 ] < util .ACHROMATIC_THRESHOLD_SM
112+ env = self .ENV
113+ if env .standard or env .integer :
114+ o = env .c_offset
115+ s = env .c_scale
116+ return alg .rect_to_polar ((coords [1 ] - o ) / s , (coords [2 ] - o ) / s )[0 ] < util .ACHROMATIC_THRESHOLD_SM
117+ else :
118+ return alg .rect_to_polar (coords [1 ], coords [2 ])[0 ] < util .ACHROMATIC_THRESHOLD_SM
103119
104120 def to_base (self , coords : Vector ) -> Vector :
105121 """To base from oRGB."""
106122
107- co = self .ENV .c_offset
108- cs = self .ENV .c_scale
123+ env = self .ENV
124+ if env .integer :
125+ coords = [digital_round (c , env ) for c in coords ]
126+ co = env .c_offset
127+ cs = env .c_scale
109128 coords = [
110- (coords [0 ] - self . ENV . y_offset ) / self . ENV .y_scale ,
129+ (coords [0 ] - env . y_offset ) / env .y_scale ,
111130 (coords [1 ] - co ) / cs ,
112131 (coords [2 ] - co ) / cs ,
113132 ]
114- return alg .matmul (self . ENV .ycbcr_to_rgb , coords , dims = alg .D2_D1 )
133+ return alg .matmul (env .ycbcr_to_rgb , coords , dims = alg .D2_D1 )
115134
116135 def from_base (self , coords : Vector ) -> Vector :
117136 """From base to oRGB."""
118137
119- co = self .ENV .c_offset
120- cs = self .ENV .c_scale
121- coords = alg .matmul (self .ENV .rgb_to_ycbcr , coords , dims = alg .D2_D1 )
122- return [
123- self .ENV .y_offset + coords [0 ] * self .ENV .y_scale ,
138+ env = self .ENV
139+ co = env .c_offset
140+ cs = env .c_scale
141+ coords = alg .matmul (env .rgb_to_ycbcr , coords , dims = alg .D2_D1 )
142+ coords = [
143+ env .y_offset + coords [0 ] * env .y_scale ,
124144 co + coords [1 ] * cs ,
125145 co + coords [2 ] * cs ,
126146 ]
147+ if env .integer :
148+ coords = [digital_round (c , env ) for c in coords ]
149+ return coords
127150
128151
129152class YCbCr709 (Prism , YCbCr ):
@@ -133,10 +156,10 @@ class YCbCr709(Prism, YCbCr):
133156 NAME = "ycbcr-709"
134157 SERIALIZE = ("--ycbcr-709" ,)
135158 WHITE = WHITES ['2deg' ]['D65' ]
136- ENV = Environment (kr = BT709 [0 ], kb = BT709 [1 ], bit_depth = 8 )
159+ ENV = Environment (kr = BT709 [0 ], kb = BT709 [1 ], bit_depth = 8 , standard = True )
137160 CHANNELS = (
138- Channel ("y" , ENV .y_range [0 ], ENV .y_range [1 ], bound = True ),
139- Channel ("cb" , ENV .c_range [0 ], ENV .c_range [1 ], bound = True ),
140- Channel ("cr" , ENV .c_range [0 ], ENV .c_range [1 ], bound = True )
161+ Channel ("y" , ENV .y_range [0 ], ENV .y_range [1 ], nans = ENV . y_range [ 0 ], bound = True ),
162+ Channel ("cb" , ENV .c_range [0 ], ENV .c_range [1 ], nans = ENV . c_range [ 0 ], bound = True ),
163+ Channel ("cr" , ENV .c_range [0 ], ENV .c_range [1 ], nans = ENV . c_range [ 0 ], bound = True )
141164 )
142165 GAMUT_CHECK = 'rec709'
0 commit comments