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 \\\left(0.8996266762239280431763196027708491089 r' + 0.1003733237760719568236803972291508911\right)^{2.222222222222222222222222222222222222} & \text{otherwise} \end{cases}, \ \begin{cases} 0.25 g' & \text{for}\: g' \leq 0.09128634211778008882172239358977671184 \\\left(0.8996266762239280431763196027708491089 g' + 0.1003733237760719568236803972291508911\right)^{2.222222222222222222222222222222222222} & \text{otherwise} \end{cases}, \ \begin{cases} 0.25 b' & \text{for}\: b' \leq 0.09128634211778008882172239358977671184 \\\left(0.8996266762239280431763196027708491089 b' + 0.1003733237760719568236803972291508911\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
    _1 = _1 + y
    _1 = _1 <= 0.09128634211778008882172239358977671184
    _2 = 0.8996266762239280431763196027708491089*y
    _2 = _2 + 0.1003733237760719568236803972291508911
    _3 = 1.417116903110769752212105343075630963*v
    _3 = _2 + _3
    _3 = _3**2.222222222222222222222222222222222222
    _4 = 0.393806936967160388281671932168692471*v
    _4 = _0 + _4
    _4 = Piecewise((_4, _1), (_3, True))
    _5 = -_0
    _6 = -y
    _7 = 0.4772063583101252952258757092198383568*v
    _8 = 0.2255157459120897865523043115836921751*u
    _6 = _6 + _7 + _8
    _6 = _6 >= -0.09128634211778008882172239358977671184
    _7 = 0.8996266762239280431763196027708491089*y
    _8 = -0.2028799809310532225771413906512419189*u
    _9 = -0.4293075699994628824952572134562781054*v
    _7 = _7 + _8 + _9 + 0.1003733237760719568236803972291508911
    _7 = _7**2.222222222222222222222222222222222222
    _8 = -_5
    _9 = -0.1193015895775313238064689273049595892*v
    _10 = -0.05637893647802244663807607789592304378*u
    _8 = _10 + _8 + _9
    _8 = Piecewise((_8, _6), (_7, True))
    _9 = 1.826918360266677061800259126310916066*u
    _9 = _9 + y
    _9 = _9 <= 0.09128634211778008882172239358977671184
    _10 = 1.643544492179179412130221165880667149*u
    _10 = _10 + _2
    _10 = _10**2.222222222222222222222222222222222222
    _2 = 0.4567295900666692654500647815777290165*u
    _2 = _0 + _2
    _2 = Piecewise((_2, _9), (_10, True))
    _11 = _1 & _6 & _9
    _12 = 0.005616235079712466335963477384737015402*u
    _13 = 0.1927478152272564917376197799690692823*v
    _14 = _6 & _9
    _15 = 0.5952701227710405783213474436823374812*_3
    _16 = -0.04167368848927251101449620675513716215*v
    _16 = _15 + _16
    _17 = _1 & _9
    _18 = 0.02531017415011096215842454741477759892*u
    _19 = 0.3493137739140495835481416592001061499*_7
    _20 = 0.2344215037165290027521159867242064444*v
    _20 = _19 + _20
    _15 = _15 + _19
    _19 = _1 & _6
    _21 = 0.05541610331490983813051089711755636883*_10
    _22 = -0.01969393907039849582246107003004058352*u
    _22 = _21 + _22
    _23 = 2.678715552469682602446063496570518666*_4
    _24 = 1.571911982613223125966637466400477675*_8
    _25 = 0.2493724649170942715872990370290036597*_2
    _23 = _23 + _24 + _25
    _24 = _0 + _12 + _13
    _24 = _24 <= 0.01805396851080780733586959258468773494
    _25 = 0.1011824693072398554196631390794156297*y
    _12 = _12 + _16 + _25
    _12 = _12 <= 0.01805396851080780733586959258468773494
    _25 = 0.1626715565214876041129645851999734625*y
    _25 = _18 + _20 + _25
    _25 = _25 <= 0.01805396851080780733586959258468773494
    _26 = 0.01385402582872745953262772427938909221*y
    _18 = _15 + _18 + _26
    _18 = _18 <= 0.01805396851080780733586959258468773494
    _26 = 0.2361459741712725404673722757206109078*y
    _13 = _13 + _22 + _26
    _13 = _13 <= 0.01805396851080780733586959258468773494
    _26 = 0.08732844347851239588703541480002653748*y
    _16 = _16 + _22 + _26
    _16 = _16 <= 0.01805396851080780733586959258468773494
    _22 = 0.1488175306927601445803368609205843703*y
    _20 = _20 + _21 + _22
    _20 = _20 <= 0.01805396851080780733586959258468773494
    _15 = _15 + _21
    _15 = _15 <= 0.01805396851080780733586959258468773494
    _15 = ITE(_1, _20, _15)
    _15 = ITE(_6, _16, _15)
    _13 = ITE(_19, _13, _15)
    _13 = ITE(_9, _18, _13)
    _13 = ITE(_17, _25, _13)
    _12 = ITE(_14, _12, _13)
    _12 = ITE(_11, _24, _12)
    _13 = 0.3493137739140495835481416592001061499*_8
    _15 = 0.05541610331490983813051089711755636883*_2
    _16 = 0.5952701227710405783213474436823374812*_4
    _13 = _13 + _15 + _16
    _13 = _13**0.45
    _13 = 1.099296826809442940347282759215782542*_13
    _13 = _13 - 0.099296826809442940347282759215782542
    _12 = Piecewise((_23, _12), (_13, True))
    _13 = 0.07436298549573965448441799964934607902*v
    _15 = 0.03781848215812902175715342131049288056*u
    _16 = 0.0124438037631045686968790177159063483*u
    _18 = 0.8915082309299459767548066740119863528*_7
    _20 = 0.03199536357565577701992240690343537089*v
    _20 = _18 + _20
    _21 = 0.1063583490713954315043404065527814499*v
    _22 = 0.08124631785834660030870500609373494137*_3
    _18 = _18 + _22
    _23 = 0.05026228592123359045403243902639922886*u
    _24 = 0.02724545121170742293648831989427870584*_10
    _25 = 4.011787039184756895396630033053938588*_8
    _26 = 0.3656084303625597013891725274218072362*_4
    _27 = 0.1226045304526834032141974395242541763*_2
    _27 = _25 + _26 + _27
    _5 = _13 + _15 + _5
    _5 = _5 >= -0.01805396851080780733586959258468773494
    _25 = 0.0271229422675135058112983314970034118*y
    _25 = _16 + _20 + _25
    _25 = _25 <= 0.01805396851080780733586959258468773494
    _26 = -_22
    _28 = -0.2296884205354133499228237484765662647*y
    _28 = _15 + _21 + _26 + _28
    _28 = _28 >= -0.01805396851080780733586959258468773494
    _15 = 0.006811362802926855734122079973569676459*y
    _15 = _15 + _16 + _18
    _15 = _15 <= 0.01805396851080780733586959258468773494
    _16 = -_24
    _26 = -0.2431886371970731442658779200264303235*y
    _13 = _13 + _16 + _23 + _26
    _13 = _13 >= -0.01805396851080780733586959258468773494
    _16 = 0.02031157946458665007717625152343373534*y
    _16 = _16 + _20 + _24
    _16 = _16 <= 0.01805396851080780733586959258468773494
    _20 = -_21
    _21 = -_23
    _23 = 0.2228770577324864941887016685029965882*y
    _20 = _20 + _21 + _22 + _23 + _24
    _20 = _20 <= 0.01805396851080780733586959258468773494
    _18 = _18 + _24
    _18 = _18 <= 0.01805396851080780733586959258468773494
    _18 = ITE(_6, _20, _18)
    _16 = ITE(_1, _16, _18)
    _13 = ITE(_19, _13, _16)
    _13 = ITE(_9, _15, _13)
    _28 = ITE(_14, _28, _13)
    _28 = ITE(_17, _25, _28)
    _28 = ITE(_11, _5, _28)
    _5 = 0.8915082309299459767548066740119863528*_8
    _13 = 0.08124631785834660030870500609373494137*_4
    _15 = 0.02724545121170742293648831989427870584*_2
    _5 = _13 + _15 + _5
    _5 = _5**0.45
    _5 = 1.099296826809442940347282759215782542*_5
    _5 = _5 - 0.099296826809442940347282759215782542
    _27 = Piecewise((_27, _28), (_5, True))
    _28 = 0.4076023144074993873444640152111481931*u
    _5 = 0.003664093591595216860355383337237092768*v
    _13 = 0.004619273747905038815867694158849998107*u
    _10 = 0.9025506494887533722687808235712819565*_10
    _15 = 0.006110597724791278555250393601747546687*v
    _7 = 0.0819326159106551655887679794018842299*_7
    _16 = 0.4122215881554044261603317093699981912*u
    _16 = _16 + _7
    _7 = _10 + _7
    _18 = 0.01551673460059146214245119702683381359*_3
    _20 = -0.009774691316386495415605776938984639455*v
    _20 = _18 + _20
    _21 = 4.061477922699390175209513706070768804*_2
    _22 = 0.06982530570266157964103038662075216118*_4
    _23 = 0.3686967715979482451494559073084790346*_8
    _21 = _21 + _22 + _23
    _22 = -_5
    _0 = _0 + _22 + _28
    _0 = _0 <= 0.01805396851080780733586959258468773494
    _22 = -_10
    _23 = -0.02436233762781165693280479410717951087*y
    _5 = _13 + _22 + _23 + _5
    _5 = _5 >= -0.01805396851080780733586959258468773494
    _22 = 0.2295168460223362086028080051495289425*y
    _22 = _15 + _16 + _22
    _22 = _22 <= 0.01805396851080780733586959258468773494
    _23 = 0.003879183650147865535612799256708453399*y
    _15 = _15 + _23 + _7
    _15 = _15 <= 0.01805396851080780733586959258468773494
    _23 = 0.2461208163498521344643872007432915466*y
    _28 = _20 + _23 + _28
    _28 = _28 <= 0.01805396851080780733586959258468773494
    _13 = -_13
    _23 = 0.02048315397766379139719199485047105748*y
    _10 = _10 + _13 + _20 + _23
    _10 = _10 <= 0.01805396851080780733586959258468773494
    _13 = 0.2256376623721883430671952058928204891*y
    _13 = _13 + _16 + _18
    _13 = _13 <= 0.01805396851080780733586959258468773494
    _7 = _18 + _7
    _7 = _7 <= 0.01805396851080780733586959258468773494
    _7 = ITE(_9, _13, _7)
    _6 = ITE(_6, _10, _7)
    _28 = ITE(_14, _28, _6)
    _1 = ITE(_1, _15, _28)
    _1 = ITE(_17, _22, _1)
    _1 = ITE(_19, _5, _1)
    _0 = ITE(_11, _0, _1)
    _1 = 0.0819326159106551655887679794018842299*_8
    _28 = 0.9025506494887533722687808235712819565*_2
    _5 = 0.01551673460059146214245119702683381359*_4
    _1 = _1 + _28 + _5
    _1 = _1**0.45
    _1 = 1.099296826809442940347282759215782542*_1
    _1 = _1 - 0.099296826809442940347282759215782542
    _0 = Piecewise((_21, _0), (_1, True))
    _1 = 0.2627052669039600712074144417509988725*_12
    _28 = 0.6780070811119393525736767106632850431*_27
    _5 = 0.05928765198410057621890884758571608441*_0
    _1 = _1 + _28 + _5
    _28 = 0.5*_0
    _5 = -0.1396310293247686621154916082076398282*_12
    _6 = -0.3603689706752313378845083917923601718*_27
    _28 = _28 + _5 + _6
    _5 = 0.5*_12
    _27 = -0.4597937911917934633089336856193296705*_27
    _0 = -0.04020620880820653669106631438067032949*_0
    _0 = _0 + _27 + _5
    _ = (_1, _28, _0)
    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.3777e-05, dtype=torch.float64)
tensor(0.0001, dtype=torch.float64)
/tmp/ipykernel_1100019/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_1100019/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 138.70 ms
colourscience convesion take 2449.14 ms
cutcutcodec is 17.65779239114693 times faster than colourscience