Skip to content

Commit ae3cb51

Browse files
committed
Rework some format handling for digital vs floating point
1 parent d9f55b8 commit ae3cb51

3 files changed

Lines changed: 70 additions & 47 deletions

File tree

coloraide/spaces/sycc.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@
66
from __future__ import annotations
77
from .ycbcr_709 import YCbCr, Environment
88
from ..channels import Channel
9-
from ..spaces import Prism
9+
from ..spaces import Labish
1010
from ..cat import WHITES
1111

1212
BT601 = [0.2990, 0.1140]
1313

1414

15-
class sYCC(Prism, YCbCr):
15+
class sYCC(Labish, YCbCr):
1616
"""Y'CbCr color class using sRGB and BT.601 transform (8 bit)."""
1717

1818
BASE = 'srgb'
1919
NAME = "sycc"
2020
SERIALIZE = ("--sycc",)
2121
WHITE = WHITES['2deg']['D65']
22-
ENV = Environment(kr=BT601[0], kb=BT601[1], output='full', bit_depth=8)
22+
ENV = Environment(kr=BT601[0], kb=BT601[1], standard=False, bit_depth=8)
2323
CHANNELS = (
24-
Channel("y", ENV.y_range[0], ENV.y_range[1], bound=True),
25-
Channel("cb", ENV.c_range[0], ENV.c_range[1], bound=True),
26-
Channel("cr", ENV.c_range[0], ENV.c_range[1], bound=True)
24+
Channel("y", ENV.y_range[0], ENV.y_range[1], nans=ENV.y_range[0], bound=True),
25+
Channel("cb", ENV.c_range[0], ENV.c_range[1], nans=ENV.c_range[0], bound=True),
26+
Channel("cr", ENV.c_range[0], ENV.c_range[1], nans=ENV.c_range[0], bound=True)
2727
)
2828
GAMUT_CHECK = 'srgb'

coloraide/spaces/ycbcr_2020.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ class YCbCr2020(Prism, YCbCr):
1919
NAME = "ycbcr-2020"
2020
SERIALIZE = ("--ycbcr-2020",)
2121
WHITE = WHITES['2deg']['D65']
22-
ENV = Environment(kr=BT2020[0], kb=BT2020[1], bit_depth=10)
22+
ENV = Environment(kr=BT2020[0], kb=BT2020[1], bit_depth=10, standard=True)
2323
CHANNELS = (
24-
Channel("y", ENV.y_range[0], ENV.y_range[1], bound=True),
25-
Channel("cb", ENV.c_range[0], ENV.c_range[1], bound=True),
26-
Channel("cr", ENV.c_range[0], ENV.c_range[1], bound=True)
24+
Channel("y", ENV.y_range[0], ENV.y_range[1], nans=ENV.y_range[0], bound=True),
25+
Channel("cb", ENV.c_range[0], ENV.c_range[1], nans=ENV.c_range[0], bound=True),
26+
Channel("cr", ENV.c_range[0], ENV.c_range[1], nans=ENV.c_range[0], bound=True)
2727
)
2828
GAMUT_CHECK = 'rec2020'

coloraide/spaces/ycbcr_709.py

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
BT709 = [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+
2733
class 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

129152
class 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

Comments
 (0)