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