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