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