3. Color space conversion
The terminology and the way equations are constructed are detailed in the documentation.
The available gammas and gamuts are also detailed in the documentation. They come from International Telecommunication Union reports.
[1]:
from IPython.display import display, Math
import numpy as np
import sympy
import torch
from cutcutcodec.core.classes.colorspace import Colorspace
[2]:
src = Colorspace("y'pbpr", "smpte240m", "smpte240m")
dst = Colorspace("y'pbpr", "bt2020", "bt2020")
print(f"Source Colorspace: {src}")
print(f"Target Colorspace: {dst}")
Source Colorspace: Colorspace("y'pbpr", 'ntsc', 'smpte240m')
Target Colorspace: Colorspace("y'pbpr", 'bt2020', 'bt1361e, bt1361')
3.1. Symbolic conversions
This allows you to retrieve the passage equations
[3]:
steps = [ # details of each transition stage
src,
Colorspace("r'g'b'", src.primaries, src.transfer),
Colorspace("rgb", src.primaries),
Colorspace("xyz"),
Colorspace("rgb", dst.primaries),
Colorspace("r'g'b'", dst.primaries, dst.transfer),
dst,
]
for step_n, step_np1 in zip(steps[:-1], steps[1:]):
print(f"{step_n} -> {step_np1}")
eq = step_n.to_equation(step_np1)
display(Math(sympy.latex(eq)))
Colorspace("y'pbpr", 'ntsc', 'smpte240m') -> Colorspace("r'g'b'", 'ntsc', 'smpte240m')
$\displaystyle \left( \frac{614578 p_{r}}{390147} + y', \ - \frac{17699544208 p_{b}}{78464413905} - \frac{7488632930 p_{r}}{15692882781} + y', \ \frac{3563744 p_{b}}{1950735} + y'\right)$
Colorspace("r'g'b'", 'ntsc', 'smpte240m') -> Colorspace('rgb', 'ntsc')
$\displaystyle \left( \begin{cases} \frac{r'}{4} & \text{for}\: r' \leq \frac{5705396382361255551357649599361}{62500000000000000000000000000000} \\\left(\frac{10000000000000000000000000000000 r'}{11115721959217312196709940366097} + \frac{1115721959217312196709940366097}{11115721959217312196709940366097}\right)^{\frac{20}{9}} & \text{otherwise} \end{cases}, \ \begin{cases} \frac{g'}{4} & \text{for}\: g' \leq \frac{5705396382361255551357649599361}{62500000000000000000000000000000} \\\left(\frac{10000000000000000000000000000000 g'}{11115721959217312196709940366097} + \frac{1115721959217312196709940366097}{11115721959217312196709940366097}\right)^{\frac{20}{9}} & \text{otherwise} \end{cases}, \ \begin{cases} \frac{b'}{4} & \text{for}\: b' \leq \frac{5705396382361255551357649599361}{62500000000000000000000000000000} \\\left(\frac{10000000000000000000000000000000 b'}{11115721959217312196709940366097} + \frac{1115721959217312196709940366097}{11115721959217312196709940366097}\right)^{\frac{20}{9}} & \text{otherwise} \end{cases}\right)$
Colorspace('rgb', 'ntsc') -> Colorspace('xyz')
$\displaystyle \left( \frac{5234753 b}{27310290} + \frac{4987652 g}{13655145} + \frac{51177 r}{130049}, \ \frac{168863 b}{1950735} + \frac{1367582 g}{1950735} + \frac{82858 r}{390147}, \ \frac{5234753 b}{5462058} + \frac{1528474 g}{13655145} + \frac{2437 r}{130049}\right)$
Colorspace('xyz') -> Colorspace('rgb', 'bt2020')
$\displaystyle \left( \frac{30757411 x}{17917100} - \frac{6372589 y}{17917100} - \frac{4539589 z}{17917100}, \ - \frac{19765991 x}{29648200} + \frac{47925759 y}{29648200} + \frac{467509 z}{29648200}, \ \frac{792561 x}{44930125} - \frac{1921689 y}{44930125} + \frac{42328811 z}{44930125}\right)$
Colorspace('rgb', 'bt2020') -> Colorspace("r'g'b'", 'bt2020', 'bt1361e, bt1361')
$\displaystyle \left( \min\left(1, \max\left(0, \begin{cases} \frac{5496484134047214701736413796079 r^{\frac{9}{20}}}{5000000000000000000000000000000} - \frac{496484134047214701736413796079}{5000000000000000000000000000000} & \text{for}\: r \geq \frac{1128373031925487958491849536543}{62500000000000000000000000000000} \\\frac{9 r}{2} & \text{otherwise} \end{cases}\right)\right), \ \min\left(1, \max\left(0, \begin{cases} \frac{5496484134047214701736413796079 g^{\frac{9}{20}}}{5000000000000000000000000000000} - \frac{496484134047214701736413796079}{5000000000000000000000000000000} & \text{for}\: g \geq \frac{1128373031925487958491849536543}{62500000000000000000000000000000} \\\frac{9 g}{2} & \text{otherwise} \end{cases}\right)\right), \ \min\left(1, \max\left(0, \begin{cases} \frac{5496484134047214701736413796079 b^{\frac{9}{20}}}{5000000000000000000000000000000} - \frac{496484134047214701736413796079}{5000000000000000000000000000000} & \text{for}\: b \geq \frac{1128373031925487958491849536543}{62500000000000000000000000000000} \\\frac{9 b}{2} & \text{otherwise} \end{cases}\right)\right)\right)$
Colorspace("r'g'b'", 'bt2020', 'bt1361e, bt1361') -> Colorspace("y'pbpr", 'bt2020', 'bt1361e, bt1361')
$\displaystyle \left( \frac{8267143 b'}{139408157} + \frac{472592308 g'}{697040785} + \frac{26158966 r'}{99577255}, \ \frac{b'}{2} - \frac{118148077 g'}{327852535} - \frac{91556381 r'}{655705070}, \ - \frac{41335715 b'}{1027856046} - \frac{236296154 g'}{513928023} + \frac{r'}{2}\right)$
3.2. Numerical convertion
Offers more flexibility than the FilterVideoColorspace.
Compiled in C, this function optimizes cache and overhead.
[4]:
func = src.to_function(dst) # optimize the full chain
print(func)
def lambdify(u, v, y):
# this section is not cached and compiled in C
_0 = 0.25*y
_1 = -_0
_2 = -y
_3 = 0.4771993160534396525930439*v
_4 = _2 + _3
_5 = 0.225574159381723607102498*u
_6 = _4 + _5
_6 = _6 >= -0.09128634211778008882172239
_4 = -_4
_5 = -0.225574159381723607102498*u
_7 = _4 + _5 + 0.111572195921731219670994
_7 = _7**2.222222222222222222222222
_2 = -_1
_3 = -0.119299829013359913148261*v
_5 = -0.0563935398454309017756245*u
_8 = _2 + _3 + _5
_9 = 0.7905262171760351908579861*_7
_8 = Piecewise((_8, _6), (_9, True))
_5 = 1.826872435261580891305072*u
_10 = _5 + y
_10 = _10 <= 0.09128634211778008882172239
_2 = 0.5473835943322528217515063*y
_11 = _2 + u + 0.06107278963117955464951931
_11 = _11**2.222222222222222222222222
_5 = 0.456718108815395222826268*u
_12 = _0 + _5
_13 = 3.016408156183984210419196*_11
_12 = Piecewise((_12, _10), (_13, True))
_3 = 1.575247278589864845814526*v
_4 = _3 + y
_4 = _4 <= 0.09128634211778008882172239
_2 = 0.6348209665819472874069687*y
_14 = _2 + v + 0.07082836925870381003058897
_14 = _14**2.222222222222222222222222
_3 = 0.3938118196474662114536316*v
_15 = _0 + _3
_16 = 2.170046442963432433408288*_14
_15 = Piecewise((_15, _4), (_16, True))
_9 = _10 & _4 & _6
_5 = 0.005617692151755265893588313*u
_3 = 0.1927450512677801493936338*v
_17 = _10 & _4
_18 = 0.2344181421659148399500755*v
_19 = 0.2761418115867228038737819*_7
_20 = 0.02531674060372785791225303*u
_21 = _19 + _20
_22 = _4 & _6
_13 = 0.1672051564654420694163795*_11
_20 = -0.01969904845197259201866472*u
_23 = _13 + _20
_19 = _13 + _19
_24 = _10 & _6
_16 = 1.291729273206217953819297*_14
_25 = -0.04167309089813469055644164*v
_26 = _16 + _25
_27 = 0.5868315015801287473351482*_8
_13 = 0.093123027573748876290337*_12
_27 = _13 + _15 + _27
_27 = _27**0.45
_27 = 0.8704244609706216290974129*_27
_27 = _27 - 0.09929682680944294034728276
_28 = _0 + _3 + _5
_28 = _28 >= 0.01805396851080780733586959
_2 = 0.1626715200625562360883475*y
_29 = _18 + _2 + _21
_29 = _29 >= 0.01805396851080780733586959
_2 = 0.2361420315315541568349301*y
_30 = _2 + _23 + _3
_30 = _30 >= 0.01805396851080780733586959
_2 = 0.1488135515941103929232776*y
_31 = _18 + _19 + _2
_31 = _31 >= 0.01805396851080780733586959
_2 = 0.1011864484058896070767224*y
_32 = _2 + _26 + _5
_32 = _32 >= 0.01805396851080780733586959
_2 = 0.01385796846844584316506992*y
_21 = _16 + _2 + _21
_21 = _21 >= 0.01805396851080780733586959
_2 = 0.08732847993744376391165248*y
_33 = _2 + _23 + _26
_33 = _33 >= 0.01805396851080780733586959
_19 = _16 + _19
_19 = _19 >= 0.01805396851080780733586959
_19 = ITE(_6, _33, _19)
_19 = ITE(_10, _21, _19)
_19 = ITE(_24, _32, _19)
_19 = ITE(_4, _31, _19)
_19 = ITE(_22, _30, _19)
_19 = ITE(_17, _29, _19)
_19 = ITE(_9, _28, _19)
_21 = 1.571912638873987750409745*_8
_16 = 2.678643928693987072618997*_15
_13 = 0.2494434324320251769712585*_12
_21 = _13 + _16 + _21
_19 = Piecewise((_27, _19), (_21, True))
_19 = Max(0, _19)
_19 = Min(1, _19)
_3 = 0.07436147643982580834177564*v
_20 = 0.03782806855056893388934988*u
_5 = 0.0124469580723076611940865*u
_21 = 0.7047567278723893937301154*_7
_18 = 0.03199471429036555333741146*v
_27 = _18 + _21
_18 = 0.1063561907301913616791871*v
_16 = 0.176302519313898867759172*_14
_21 = _16 + _21
_34 = 0.05027502662287659508343639*u
_13 = 0.08220629995681775430977471*_11
_23 = 0.03056976090490033697961524*_12
_26 = 0.09113108409813752794200294*_15
_28 = _23 + _26 + _8
_28 = _28**0.45
_28 = 1.043927415642058773953366*_28
_28 = _28 - 0.09929682680944294034728276
_29 = _1 + _20 + _3
_29 = _29 <= -0.01805396851080780733586959
_1 = 0.02712417609944571865531959*y
_30 = _1 + _27 + _5
_30 = _30 >= 0.01805396851080780733586959
_26 = -_16
_1 = -0.2296890845486768977445807*y
_31 = _1 + _18 + _20 + _26
_31 = _31 <= -0.01805396851080780733586959
_1 = 0.006813260648122616399900311*y
_32 = _1 + _21 + _5
_32 = _32 >= 0.01805396851080780733586959
_23 = -_13
_1 = -0.2431867393518773836000997*y
_33 = _1 + _23 + _3 + _34
_33 = _33 <= -0.01805396851080780733586959
_1 = 0.02031091545132310225541928*y
_27 = _1 + _13 + _27
_27 = _27 >= 0.01805396851080780733586959
_3 = -_18
_34 = -_34
_1 = 0.2228758239005542813446804*y
_35 = _1 + _13 + _16 + _3 + _34
_35 = _35 >= 0.01805396851080780733586959
_21 = _13 + _21
_21 = _21 >= 0.01805396851080780733586959
_35 = ITE(_6, _35, _21)
_35 = ITE(_4, _27, _35)
_35 = ITE(_22, _33, _35)
_35 = ITE(_10, _32, _35)
_35 = ITE(_24, _31, _35)
_35 = ITE(_17, _30, _35)
_35 = ITE(_9, _29, _35)
_21 = 4.011764830209977064204247*_8
_16 = 0.365596478123815840597547*_15
_13 = 0.1226386916662070951982056*_12
_21 = _13 + _16 + _21
_35 = Piecewise((_28, _35), (_21, True))
_35 = Max(0, _35)
_35 = Min(1, _35)
_34 = 0.4076035239144153498747493*u
_3 = 0.00366310156942786358081668*v
_20 = 0.00461928745499979105436793*u
_11 = 2.722537658086137561567867*_11
_18 = 0.006108943332443693255619992*v
_7 = 0.06475330060603677280226993*_7
_5 = 0.4122228113694151409291173*u
_21 = _5 + _7
_7 = _11 + _7
_14 = 0.03366250093941259265185066*_14
_25 = -0.009772044901871556836436671*v
_16 = _14 + _25
_27 = 0.09075317827576484253073413*_8
_26 = 0.01718674212351029848268943*_15
_27 = _12 + _26 + _27
_27 = _27**0.45
_27 = 1.049742255955522285305319*_27
_27 = _27 - 0.09929682680944294034728276
_25 = -_3
_28 = _0 + _25 + _34
_28 = _28 >= 0.01805396851080780733586959
_13 = -_11
_0 = -0.0243559958468632862123762*y
_29 = _0 + _13 + _20 + _3
_29 = _29 <= -0.01805396851080780733586959
_0 = 0.2295220894642329612820027*y
_30 = _0 + _18 + _21
_30 = _30 >= 0.01805396851080780733586959
_0 = 0.003878085311096247494378863*y
_31 = _0 + _18 + _7
_31 = _31 >= 0.01805396851080780733586959
_0 = 0.2461219146889037525056211*y
_32 = _0 + _16 + _34
_32 = _32 >= 0.01805396851080780733586959
_34 = -_20
_0 = 0.02047791053576703871799734*y
_33 = _0 + _11 + _16 + _34
_33 = _33 >= 0.01805396851080780733586959
_0 = 0.2256440041531367137876238*y
_21 = _0 + _14 + _21
_21 = _21 >= 0.01805396851080780733586959
_7 = _14 + _7
_7 = _7 >= 0.01805396851080780733586959
_7 = ITE(_10, _21, _7)
_7 = ITE(_6, _33, _7)
_7 = ITE(_24, _32, _7)
_7 = ITE(_4, _31, _7)
_7 = ITE(_17, _30, _7)
_7 = ITE(_22, _29, _7)
_7 = ITE(_9, _28, _7)
_10 = 4.061592074756460848177228*_12
_14 = 0.06980553559973245489881953*_15
_8 = 0.3686023896438066969239522*_8
_8 = _10 + _14 + _8
_7 = Piecewise((_27, _7), (_8, True))
_7 = Max(0, _7)
_7 = Min(1, _7)
_8 = 0.2627002120112670308093952*_19
_9 = 0.6779980715188710227336267*_35
_17 = 0.05930171646986194645697812*_7
_8 = _17 + _8 + _9
_9 = 0.5*_7
_17 = -0.1396304301871571619844269*_19
_21 = -0.3603695698128428380155731*_35
_9 = _17 + _21 + _9
_17 = 0.5*_19
_35 = -0.4597845290098142789929165*_35
_7 = -0.04021547099018572100708352*_7
_7 = _17 + _35 + _7
_ = (_8, _9, _7)
return _
[5]:
src_yuv = torch.rand(2160, 3840, 3)
src_yuv[..., 1:] -= 0.5
src_yuv *= 0.2 # valid range
[6]:
dst_y, dst_u, dst_v = func(y=src_yuv[..., 0], u=src_yuv[..., 1], v=src_yuv[..., 2])
dst_yuv = torch.cat([dst_y[..., None], dst_u[..., None], dst_v[..., None]], dim=-1)
3.3. Alternatives
[7]:
import timeit
import colour # pip install colour-science
[8]:
def colourscience_convert(src_yuv):
src_colourspace = colour.models.RGB_COLOURSPACE_SMPTE_240M
src_k = colour.WEIGHTS_YCBCR["SMPTE-240M"]
dst_colourspace = colour.models.RGB_COLOURSPACE_BT2020
dst_k = colour.WEIGHTS_YCBCR["ITU-R BT.2020"]
src_rgb = colour.YCbCr_to_RGB(src_yuv, src_k, in_range=(0.0, 1.0, -0.5, 0.5), out_range=(0.0, 1.0))
xyz = colour.RGB_to_XYZ(src_rgb, src_colourspace, apply_cctf_decoding=True)
dst_rgb = colour.XYZ_to_RGB(xyz, dst_colourspace, apply_cctf_encoding=True)
dst_yuv = colour.RGB_to_YCbCr(dst_rgb, dst_k, in_range=(0.0, 1.0), out_range=(0.0, 1.0, -0.5, 0.5))
return dst_yuv
dst_yuv_ = colourscience_convert(src_yuv.numpy(force=True))
[9]:
print(abs(dst_yuv - dst_yuv_).mean())
print(abs(dst_yuv - dst_yuv_).max())
tensor(0.0034, dtype=torch.float64)
tensor(0.0919, dtype=torch.float64)
/tmp/ipykernel_1412128/164622837.py:1: DeprecationWarning: __array_wrap__ must accept context and return_scalar arguments (positionally) in the future. (Deprecated NumPy 2.0)
print(abs(dst_yuv - dst_yuv_).mean())
/tmp/ipykernel_1412128/164622837.py:2: DeprecationWarning: __array_wrap__ must accept context and return_scalar arguments (positionally) in the future. (Deprecated NumPy 2.0)
print(abs(dst_yuv - dst_yuv_).max())
[10]:
number = 5
time_cutcutcodec = timeit.repeat(lambda: func(y=src_yuv[..., 0], u=src_yuv[..., 1], v=src_yuv[..., 2]), number=number, repeat=7)
print(f"cutcutcodec convesion take {1000*np.median(time_cutcutcodec)/number:.2f} ms")
time_colourscience = timeit.repeat(lambda: colourscience_convert(src_yuv.numpy(force=True)), number=number, repeat=5)
print(f"colourscience convesion take {1000*np.median(time_colourscience)/number:.2f} ms")
print(f"cutcutcodec is {np.median(time_colourscience)/np.median(time_cutcutcodec)} times faster than colourscience")
cutcutcodec convesion take 83.14 ms
colourscience convesion take 2829.17 ms
cutcutcodec is 34.03094414242862 times faster than colourscience