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

import cutcutcodec
[2]:
src = cutcutcodec.Colorspace("y'pbpr", "smpte240m", "smpte240m")
dst = cutcutcodec.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,
    cutcutcodec.Colorspace("r'g'b'", src.primaries, src.transfer),
    cutcutcodec.Colorspace("rgb", src.primaries),
    cutcutcodec.Colorspace("xyz"),
    cutcutcodec.Colorspace("rgb", dst.primaries),
    cutcutcodec.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{430238494 p_{r}}{273127803} + y', \ - \frac{7623978355136 p_{b}}{33806856032607} - \frac{209727006492056 p_{r}}{439489128423891} + y', \ \frac{38383246 p_{b}}{21009831} + y'\right)$
Colorspace("r'g'b'", 'ntsc', 'smpte240m') -> Colorspace('rgb', 'ntsc')
$\displaystyle \left( \begin{cases} 0.25 r' & \text{for}\: r' \leq 0.09128634211778008882172239358977671184 \\0.790526217176035190857986080494319491 \left(r' + 0.111572195921731219670994036609727092\right)^{2.222222222222222222222222222222222222} & \text{otherwise} \end{cases}, \ \begin{cases} 0.25 g' & \text{for}\: g' \leq 0.09128634211778008882172239358977671184 \\0.790526217176035190857986080494319491 \left(g' + 0.111572195921731219670994036609727092\right)^{2.222222222222222222222222222222222222} & \text{otherwise} \end{cases}, \ \begin{cases} 0.25 b' & \text{for}\: b' \leq 0.09128634211778008882172239358977671184 \\0.790526217176035190857986080494319491 \left(b' + 0.111572195921731219670994036609727092\right)^{2.222222222222222222222222222222222222} & \text{otherwise} \end{cases}\right)$
Colorspace('rgb', 'ntsc') -> Colorspace('xyz')
$\displaystyle \left( \frac{4026032 b}{21009831} + \frac{99764014 g}{273127803} + \frac{35828814 r}{91042601}, \ \frac{1818208 b}{21009831} + \frac{191482543 g}{273127803} + \frac{58008556 r}{273127803}, \ \frac{20130160 b}{21009831} + \frac{30572843 g}{273127803} + \frac{1706134 r}{91042601}\right)$
Colorspace('xyz') -> Colorspace('rgb', 'bt2020')
$\displaystyle \left( \frac{21532150939 x}{12543355000} - \frac{4461219061 y}{12543355000} - \frac{3178002061 z}{12543355000}, \ - \frac{1976779337 x}{2965129750} + \frac{4793012913 y}{2965129750} + \frac{46755163 z}{2965129750}, \ \frac{79263327 x}{4492356500} - \frac{192186423 y}{4492356500} + \frac{4233267077 z}{4492356500}\right)$
Colorspace('rgb', 'bt2020') -> Colorspace("r'g'b'", 'bt2020', 'bt1361e, bt1361')
$\displaystyle \left( \begin{cases} 4.5 r & \text{for}\: r \leq 0.01805396851080780733586959258468773494 \\1.099296826809442940347282759215782542 r^{0.45} - 0.099296826809442940347282759215782542 & \text{otherwise} \end{cases}, \ \begin{cases} 4.5 g & \text{for}\: g \leq 0.01805396851080780733586959258468773494 \\1.099296826809442940347282759215782542 g^{0.45} - 0.099296826809442940347282759215782542 & \text{otherwise} \end{cases}, \ \begin{cases} 4.5 b & \text{for}\: b \leq 0.01805396851080780733586959258468773494 \\1.099296826809442940347282759215782542 b^{0.45} - 0.099296826809442940347282759215782542 & \text{otherwise} \end{cases}\right)$
Colorspace("r'g'b'", 'bt2020', 'bt1361e, bt1361') -> Colorspace("y'pbpr", 'bt2020', 'bt1361e, bt1361')
$\displaystyle \left( \frac{826593596 b'}{13942086899} + \frac{9452833643 g'}{13942086899} + \frac{3662659660 r'}{13942086899}, \ \frac{b'}{2} - \frac{859348513 g'}{2384635146} - \frac{166484530 r'}{1192317573}, \ - \frac{413296798 b'}{10279427239} - \frac{9452833643 g'}{20558854478} + \frac{r'}{2}\right)$

3.2. Numerical conversion

  • 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 = 1.575227747868641553126687728674769884*v
    _2 = _1 + y
    _2 = _2 <= 0.09128634211778008882172239358977671184
    _3 = 0.6348288375144786556453500415980909416*y
    _4 = _3 + v + 0.07082924743593028657554055134201062229
    _4 = _4**2.222222222222222222222222222222222222
    _1 = 0.393806936967160388281671932168692471*v
    _5 = _0 + _1
    _6 = 2.169986653760008383303681644539441443*_4
    _5 = Piecewise((_5, _2), (_6, True))
    _3 = -_0
    _7 = -y
    _1 = 0.4772063583101252952258757092198383568*v
    _8 = 0.2255157459120897865523043115836921751*u
    _9 = _1 + _7 + _8
    _10 = _9 >= -0.09128634211778008882172239358977671184
    _9 = -_9
    _9 = _9 + 0.111572195921731219670994036609727092
    _9 = _9**2.222222222222222222222222222222222222
    _7 = -_3
    _1 = -0.1193015895775313238064689273049595892*v
    _8 = -0.05637893647802244663807607789592304378*u
    _11 = _1 + _7 + _8
    _12 = 0.790526217176035190857986080494319491*_9
    _11 = Piecewise((_11, _10), (_12, True))
    _8 = 1.826918360266677061800259126310916066*u
    _13 = _8 + y
    _13 = _13 <= 0.09128634211778008882172239358977671184
    _7 = 0.547369834224025763740773774057566679*y
    _14 = _7 + u + 0.06107125438568854110753062185460237414
    _14 = _14**2.222222222222222222222222222222222222
    _8 = 0.4567295900666692654500647815777290165*u
    _15 = _0 + _8
    _16 = 3.016576666019477949253855712670004261*_14
    _15 = Piecewise((_15, _13), (_16, True))
    _12 = _10 & _13 & _2
    _8 = 0.005616235079712466335963477384737015402*u
    _1 = 0.1927478152272564917376197799690692823*v
    _17 = _10 & _13
    _6 = 1.291728221795239713514927736752038444*_4
    _18 = -0.04167368848927251101449620675513716215*v
    _19 = _18 + _6
    _20 = _13 & _2
    _21 = 0.02531017415011096215842454741477759892*u
    _22 = 0.2761416962997584172930209810800754179*_9
    _18 = 0.2344215037165290027521159867242064444*v
    _23 = _18 + _22
    _22 = _22 + _6
    _24 = _10 & _2
    _16 = 0.1671669241814816596465504740706525702*_14
    _25 = -0.01969393907039849582246107003004058352*u
    _26 = _16 + _25
    _6 = 2.678715552469682602446063496570518666*_5
    _27 = 1.571911982613223125966637466400477675*_11
    _28 = 0.2493724649170942715872990370290036597*_15
    _27 = _27 + _28 + _6
    _29 = _0 + _1 + _8
    _29 = _29 <= 0.01805396851080780733586959258468773494
    _7 = 0.1011824693072398554196631390794156297*y
    _30 = _19 + _7 + _8
    _30 = _30 <= 0.01805396851080780733586959258468773494
    _7 = 0.1626715565214876041129645851999734625*y
    _31 = _21 + _23 + _7
    _31 = _31 <= 0.01805396851080780733586959258468773494
    _7 = 0.01385402582872745953262772427938909221*y
    _32 = _21 + _22 + _7
    _32 = _32 <= 0.01805396851080780733586959258468773494
    _7 = 0.2361459741712725404673722757206109078*y
    _33 = _1 + _26 + _7
    _33 = _33 <= 0.01805396851080780733586959258468773494
    _7 = 0.08732844347851239588703541480002653748*y
    _34 = _19 + _26 + _7
    _34 = _34 <= 0.01805396851080780733586959258468773494
    _7 = 0.1488175306927601445803368609205843703*y
    _23 = _16 + _23 + _7
    _23 = _23 <= 0.01805396851080780733586959258468773494
    _22 = _16 + _22
    _22 = _22 <= 0.01805396851080780733586959258468773494
    _22 = ITE(_2, _23, _22)
    _22 = ITE(_10, _34, _22)
    _22 = ITE(_24, _33, _22)
    _22 = ITE(_13, _32, _22)
    _22 = ITE(_20, _31, _22)
    _22 = ITE(_17, _30, _22)
    _22 = ITE(_12, _29, _22)
    _16 = 0.09309404452711730793041398121145267568*_15
    _23 = 0.5868155658274261080830700630726390165*_11
    _23 = _16 + _23 + _5
    _23 = _23**0.45
    _23 = 0.8704349342486382027632689798713363333*_23
    _23 = _23 - 0.099296826809442940347282759215782542
    _22 = Piecewise((_27, _22), (_23, True))
    _1 = 0.07436298549573965448441799964934607902*v
    _8 = 0.03781848215812902175715342131049288056*u
    _21 = 0.0124438037631045686968790177159063483*u
    _23 = 0.7047606293783494066015658260648247585*_9
    _18 = 0.03199536357565577701992240690343537089*v
    _27 = _18 + _23
    _18 = 0.1063583490713954315043404065527814499*v
    _19 = 0.1763034254197555500027145811118515679*_4
    _23 = _19 + _23
    _25 = 0.05026228592123359045403243902639922886*u
    _16 = 0.08218799238040872356530390459586689964*_14
    _29 = 4.011787039184756895396630033053938588*_11
    _6 = 0.3656084303625597013891725274218072362*_5
    _26 = 0.1226045304526834032141974395242541763*_15
    _29 = _26 + _29 + _6
    _30 = _1 + _3 + _8
    _30 = _30 >= -0.01805396851080780733586959258468773494
    _7 = 0.0271229422675135058112983314970034118*y
    _31 = _21 + _27 + _7
    _31 = _31 <= 0.01805396851080780733586959258468773494
    _6 = -_19
    _7 = -0.2296884205354133499228237484765662647*y
    _32 = _18 + _6 + _7 + _8
    _32 = _32 >= -0.01805396851080780733586959258468773494
    _7 = 0.006811362802926855734122079973569676459*y
    _33 = _21 + _23 + _7
    _33 = _33 <= 0.01805396851080780733586959258468773494
    _26 = -_16
    _7 = -0.2431886371970731442658779200264303235*y
    _34 = _1 + _25 + _26 + _7
    _34 = _34 >= -0.01805396851080780733586959258468773494
    _7 = 0.02031157946458665007717625152343373534*y
    _27 = _16 + _27 + _7
    _27 = _27 <= 0.01805396851080780733586959258468773494
    _1 = -_18
    _8 = -_25
    _7 = 0.2228770577324864941887016685029965882*y
    _35 = _1 + _16 + _19 + _7 + _8
    _35 = _35 <= 0.01805396851080780733586959258468773494
    _23 = _16 + _23
    _23 = _23 <= 0.01805396851080780733586959258468773494
    _35 = ITE(_10, _35, _23)
    _35 = ITE(_2, _27, _35)
    _35 = ITE(_24, _34, _35)
    _35 = ITE(_13, _33, _35)
    _35 = ITE(_17, _32, _35)
    _35 = ITE(_20, _31, _35)
    _35 = ITE(_12, _30, _35)
    _19 = 0.09113355888323915291044422946884672446*_5
    _16 = 0.0305610764617251742427671942086476572*_15
    _23 = _11 + _16 + _19
    _23 = _23**0.45
    _23 = 1.043930016251889889020578059853752167*_23
    _23 = _23 - 0.099296826809442940347282759215782542
    _35 = Piecewise((_29, _35), (_23, True))
    _8 = 0.4076023144074993873444640152111481931*u
    _1 = 0.003664093591595216860355383337237092768*v
    _21 = 0.004619273747905038815867694158849998107*u
    _14 = 2.722613229148498087964592165258763969*_14
    _18 = 0.006110597724791278555250393601747546687*v
    _9 = 0.0647698809191872617237235775360352408*_9
    _25 = 0.4122215881554044261603317093699981912*u
    _23 = _25 + _9
    _9 = _14 + _9
    _19 = 0.03367110699321960713282169771998237656*_4
    _36 = -0.009774691316386495415605776938984639455*v
    _4 = _19 + _36
    _16 = 4.061477922699390175209513706070768804*_15
    _6 = 0.06982530570266157964103038662075216118*_5
    _27 = 0.3686967715979482451494559073084790346*_11
    _27 = _16 + _27 + _6
    _36 = -_1
    _29 = _0 + _36 + _8
    _29 = _29 <= 0.01805396851080780733586959258468773494
    _16 = -_14
    _0 = -0.02436233762781165693280479410717951087*y
    _30 = _0 + _1 + _16 + _21
    _30 = _30 >= -0.01805396851080780733586959258468773494
    _0 = 0.2295168460223362086028080051495289425*y
    _31 = _0 + _18 + _23
    _31 = _31 <= 0.01805396851080780733586959258468773494
    _0 = 0.003879183650147865535612799256708453399*y
    _32 = _0 + _18 + _9
    _32 = _32 <= 0.01805396851080780733586959258468773494
    _0 = 0.2461208163498521344643872007432915466*y
    _33 = _0 + _4 + _8
    _33 = _33 <= 0.01805396851080780733586959258468773494
    _8 = -_21
    _0 = 0.02048315397766379139719199485047105748*y
    _34 = _0 + _14 + _4 + _8
    _34 = _34 <= 0.01805396851080780733586959258468773494
    _0 = 0.2256376623721883430671952058928204891*y
    _23 = _0 + _19 + _23
    _23 = _23 <= 0.01805396851080780733586959258468773494
    _9 = _19 + _9
    _9 = _9 <= 0.01805396851080780733586959258468773494
    _9 = ITE(_13, _23, _9)
    _9 = ITE(_10, _34, _9)
    _9 = ITE(_17, _33, _9)
    _9 = ITE(_2, _32, _9)
    _9 = ITE(_20, _31, _9)
    _9 = ITE(_24, _30, _9)
    _9 = ITE(_12, _29, _9)
    _10 = 0.09077896731564661384689587977436820083*_11
    _2 = 0.01719209288628938625683614354268095072*_5
    _10 = _10 + _15 + _2
    _10 = _10**0.45
    _10 = 1.049728979382482414626981540495914549*_10
    _10 = _10 - 0.099296826809442940347282759215782542
    _9 = Piecewise((_27, _9), (_10, True))
    _10 = 0.2627052669039600712074144417509988725*_22
    _11 = 0.6780070811119393525736767106632850431*_35
    _12 = 0.05928765198410057621890884758571608441*_9
    _10 = _10 + _11 + _12
    _11 = 0.5*_9
    _12 = -0.1396310293247686621154916082076398282*_22
    _17 = -0.3603689706752313378845083917923601718*_35
    _11 = _11 + _12 + _17
    _12 = 0.5*_22
    _35 = -0.4597937911917934633089336856193296705*_35
    _9 = -0.04020620880820653669106631438067032949*_9
    _35 = _12 + _35 + _9
    _ = (_10, _11, _35)
    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[..., 2] - dst_yuv_[..., 2]).mean())
print(abs(dst_yuv[..., 2] - dst_yuv_[..., 2]).max())
tensor(3.3795e-05, dtype=torch.float64)
tensor(0.0001, dtype=torch.float64)
/tmp/ipykernel_614229/2375015244.py:1: DeprecationWarning: __array_wrap__ must accept context and return_scalar arguments (positionally) in the future. (Deprecated NumPy 2.0)
  print(abs(dst_yuv[..., 2] - dst_yuv_[..., 2]).mean())
/tmp/ipykernel_614229/2375015244.py:2: DeprecationWarning: __array_wrap__ must accept context and return_scalar arguments (positionally) in the future. (Deprecated NumPy 2.0)
  print(abs(dst_yuv[..., 2] - dst_yuv_[..., 2]).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 280.15 ms
colourscience convesion take 7838.57 ms
cutcutcodec is 27.97981168338215 times faster than colourscience