mucolor 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ .venv/
mucolor-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jed Smith
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
mucolor-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,318 @@
1
+ Metadata-Version: 2.5
2
+ Name: mucolor
3
+ Version: 0.1.0
4
+ Summary: Minimal pure-python colorimetry: gamuts, transfer functions and colorspace conversion with no dependencies.
5
+ Project-URL: Source, https://github.com/jedypod/mucolor
6
+ Author-email: Jed Smith <jed.coma316@passmail.net>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.7
10
+ Description-Content-Type: text/markdown
11
+
12
+ # mucolor
13
+
14
+ **μcolor**, "micro color". The "mu" is the Greek letter μ, the SI symbol for micro: one
15
+ millionth. The name is the goal: the colorimetry a pipeline needs, made as small as it can be.
16
+
17
+ Minimal pure-python colorimetry calculations, transfer functions and colorspace conversions.
18
+
19
+ Convert stimuli between camera, working and display colorspaces. Pure Python, one file,
20
+ no dependencies.
21
+
22
+ ## Install
23
+
24
+ mucolor is a single file. Either copy `mucolor.py` next to your own code, or install it
25
+ from this folder:
26
+
27
+ ```
28
+ pip install .
29
+ ```
30
+
31
+ ## Quick start
32
+
33
+ ```python
34
+ import mucolor as mc
35
+
36
+ # Convert one RGB triplet: (red, green, blue), from colorspace, to colorspace
37
+ mc.convert((0.41, 0.41, 0.41), 'sony-sgamut3-slog3', 'acescg')
38
+ # -> (0.179, 0.179, 0.179)
39
+ ```
40
+
41
+ An RGB triplet is a tuple of three numbers: `(red, green, blue)`. The result is a tuple too.
42
+
43
+ ## Colorspace names
44
+
45
+ Every colorspace has a long name, a short name and an alias. You can use any of them, and
46
+ upper or lower case does not matter. These all result in the same colorspace conversion:
47
+
48
+ ```python
49
+ rgb = (0.41, 0.41, 0.41)
50
+
51
+ mc.convert(rgb, 'Sony - S-Gamut3 - S-Log3', 'acescg')
52
+ mc.convert(rgb, 'sony-sgamut3-slog3', 'acescg')
53
+ mc.convert(rgb, 'sg3lg3', 'acescg')
54
+ ```
55
+
56
+ The [full list](#colorspaces) is at the end of this page. To list them from Python:
57
+
58
+ ```python
59
+ for cs in mc.colorspaces():
60
+ print(cs.shortname, '|', cs.name)
61
+ ```
62
+
63
+ If you misspell a name, the error suggests close matches:
64
+
65
+ ```
66
+ KeyError: "unknown colorspace 'slog3', did you mean 'smlg', 'sg3ln'?"
67
+ ```
68
+
69
+ ## Converting many stimuli
70
+
71
+ `convert` looks up both colorspaces every time you call it. To convert a lot of stimuli
72
+ between the same two colorspaces, make a converter once and reuse it:
73
+
74
+ ```python
75
+ slog3_to_acescg = mc.converter('sony-sgamut3-slog3', 'acescg')
76
+
77
+ slog3_to_acescg((0.41, 0.41, 0.41)) # -> (0.179, 0.179, 0.179)
78
+ slog3_to_acescg((0.6, 0.5, 0.4)) # -> (1.107, 0.434, 0.160)
79
+ ```
80
+
81
+ For bigger challenges, use [numpy](#numpy-arrays) which is much faster to process.
82
+
83
+ ## Transfer functions on their own
84
+
85
+ A transfer function turns linear signal into code values and back. Each one has two methods:
86
+
87
+ - `encode`: linear signal -> code value
88
+ - `decode`: code value -> linear signal
89
+
90
+ They work on one number at a time, not an RGB triplet:
91
+
92
+ ```python
93
+ mc.sony_slog3.encode(0.18) # -> 0.411 (18% grey in S-Log3)
94
+ mc.sony_slog3.decode(0.41) # -> 0.179
95
+ ```
96
+
97
+ ## Matrices
98
+
99
+ `matrix` gives the 3x3 matrix between the linear versions of two colorspaces, as a tuple of
100
+ three rows:
101
+
102
+ ```python
103
+ mc.matrix('p3d65-lin', 'rec709-lin')
104
+ # -> ((1.2249, -0.2249, 0.0), (-0.0421, 1.0421, 0.0), (-0.0196, -0.0786, 1.0983))
105
+ ```
106
+
107
+ ## numpy arrays
108
+
109
+ The functions above work on one RGB triplet at a time, which is slow for large jobs such as
110
+ baking a LUT or converting an image. If you have [numpy](https://numpy.org) installed,
111
+ add `np=True` to work on whole arrays at once:
112
+
113
+ ```python
114
+ import numpy as np
115
+ import mucolor as mc
116
+
117
+ rgb = np.array([ # two RGB triplets
118
+ [0.41, 0.41, 0.41],
119
+ [0.6, 0.5, 0.4],
120
+ ])
121
+ mc.convert(rgb, 'sony-sgamut3-slog3', 'acescg', np=True)
122
+ # -> array([[0.179, 0.179, 0.179],
123
+ # [1.107, 0.434, 0.160]])
124
+ ```
125
+
126
+ `np=True` works the same way everywhere:
127
+
128
+ ```python
129
+ f = mc.converter('sony-sgamut3-slog3', 'acescg', np=True)
130
+ f(rgb)
131
+
132
+ mc.sony_slog3.decode(np.array([0.1, 0.41, 0.6]), np=True)
133
+ # -> array([0.0011, 0.179, 1.037])
134
+ ```
135
+
136
+ Things to know:
137
+
138
+ - The array can have any shape, as long as the last axis holds the three channels: a list
139
+ of RGB triplets `(n, 3)`, an image `(height, width, 3)`, or a LUT grid `(n, n, n, 3)`.
140
+ - You always get a new array back. Your input array is never changed.
141
+ - numpy is only needed when you use `np=True`. Without numpy installed, everything else
142
+ still works.
143
+ - A value a curve cannot handle, such as a negative number into a pure log curve, gives
144
+ `nan` and a numpy warning. Without `np=True` the same value raises an error.
145
+
146
+ ### Example: bake a 3D LUT to a .cube file
147
+
148
+ ```python
149
+ import numpy as np
150
+ import mucolor as mc
151
+
152
+ size = 33
153
+
154
+ # A grid of every input RGB triplet. In a .cube file red changes fastest, then green, then blue.
155
+ steps = np.linspace(0.0, 1.0, size)
156
+ b, g, r = np.meshgrid(steps, steps, steps, indexing='ij')
157
+ grid = np.stack([r, g, b], axis=-1) # shape (33, 33, 33, 3)
158
+
159
+ lut = mc.convert(grid, 'arri-wg4-logc4', 'davinci-wg-ilog', np=True)
160
+
161
+ with open('logc4_to_davinci.cube', 'w') as file:
162
+ file.write(f'LUT_3D_SIZE {size}\n')
163
+ for red, green, blue in lut.reshape(-1, 3):
164
+ file.write(f'{red:.6f} {green:.6f} {blue:.6f}\n')
165
+ ```
166
+
167
+ ## Conventions
168
+
169
+ - **Linear signal**: scene-linear 0.18 is 18% grey. For display colorspaces, linear 1.0 is
170
+ 100 nits, including `pq` and `hlg`.
171
+ - **No "tone mapping"**: mucolor converts colorimetry exactly. Converting a camera log
172
+ image straight to a display colorspace gives values above 1.0 for bright areas, the
173
+ same as a plain colorspace conversion in other software. It is not a viewing transform.
174
+ - **Chromatic adaptation**: when two colorspaces have different white points, mucolor
175
+ adapts between them with CAT02. Pass `cat='bradford'` to use Bradford, or `cat=None`
176
+ for none: `mc.convert((0.18, 0.18, 0.18), 'aces', 'rec709-lin', cat='bradford')`.
177
+ - **Gamuts** are flat tuples of xy chromaticities: `(rx, ry, gx, gy, bx, by, wx, wy)`.
178
+ `mc.matrix` and `mc.npm` accept these as well as colorspace names.
179
+
180
+ ## Colorspaces
181
+
182
+ The short name is shown. `encoding` tells you what kind of values the colorspace holds.
183
+
184
+ | Short name | Name | Encoding |
185
+ |---|---|---|
186
+ | `xyz-d65` | XYZ D65 | scene-linear |
187
+ | `xyz-e` | XYZ E | scene-linear |
188
+ | `aces` | ACES 2065-1 | scene-linear |
189
+ | `acescg` | ACEScg | scene-linear |
190
+ | `acescct` | ACEScct | log |
191
+ | `jplog2` | JPLog2 | log |
192
+ | `filmlight-egamut-lin` | Filmlight - E-Gamut - Linear | scene-linear |
193
+ | `filmlight-egamut-tlog` | Filmlight - E-Gamut - T-Log | log |
194
+ | `filmlight-egamut2-lin` | Filmlight - E-Gamut2 - Linear | scene-linear |
195
+ | `filmlight-egamut2-tlog` | Filmlight - E-Gamut2 - T-Log | log |
196
+ | `filmlight-egamut2-tloge` | Filmlight - E-Gamut2 - T-LogE | log |
197
+ | `davinci-wg-lin` | DaVinci - WideGamut - Linear | scene-linear |
198
+ | `davinci-wg-ilog` | DaVinci - WideGamut - Intermediate Log | log |
199
+ | `arri-wg3-lin` | Arri - WideGamut3 - Linear | scene-linear |
200
+ | `arri-wg3-logc3` | Arri - WideGamut3 - LogC3 | log |
201
+ | `arri-wg4-lin` | Arri - WideGamut4 - Linear | scene-linear |
202
+ | `arri-wg4-logc4` | Arri - WideGamut4 - LogC4 | log |
203
+ | `blackmagic-wg-lin` | Blackmagic - WideGamut - Linear | scene-linear |
204
+ | `blackmagic-wg-gen5log` | Blackmagic - WideGamut - Gen5 Log | log |
205
+ | `canon-cgamut-lin` | Canon - C-Gamut - Linear | scene-linear |
206
+ | `canon-cgamut-clog2` | Canon - C-Gamut - C-Log2 | log |
207
+ | `canon-cgamut-clog3` | Canon - C-Gamut - C-Log3 | log |
208
+ | `dji-dgamut-lin` | DJI - D-Gamut - Linear | scene-linear |
209
+ | `dji-dgamut-dlog` | DJI - D-Gamut - D-Log | log |
210
+ | `fuji-fgamut-lin` | Fujifilm - F-Gamut - Linear | scene-linear |
211
+ | `fuji-fgamut-flog` | Fujifilm - F-Gamut - F-Log | log |
212
+ | `fuji-fgamut-flog2` | Fujifilm - F-Gamut - F-Log2 | log |
213
+ | `fuji-fgamutc-lin` | Fujifilm - F-GamutC - Linear | scene-linear |
214
+ | `fuji-fgamutc-flog2` | Fujifilm - F-GamutC - F-Log2 | log |
215
+ | `cineon` | Kodak Cineon | log |
216
+ | `gopro-rec709-protune` | GoPro - Rec.709 - Protune | log |
217
+ | `leica-rec2020-lin` | Leica - Rec.2020 - Linear | scene-linear |
218
+ | `leica-rec2020-llog` | Leica - Rec.2020 - L-Log | log |
219
+ | `nikon-rec2020-lin` | Nikon - Rec.2020 - Linear | scene-linear |
220
+ | `nikon-rec2020-nlog` | Nikon - Rec.2020 - N-Log | log |
221
+ | `apple-rec2020-lin` | Apple - Rec.2020 - Linear | scene-linear |
222
+ | `apple-rec2020-alog` | Apple - Rec.2020 - Apple-Log | log |
223
+ | `samsung-rec2020-lin` | Samsung - Rec.2020 - Linear | scene-linear |
224
+ | `samsung-rec2020-slog` | Samsung - Rec.2020 - Samsung-Log | log |
225
+ | `xiaomi-rec2020-lin` | Xiaomi - Rec.2020 - Linear | scene-linear |
226
+ | `xiaomi-rec2020-xlog` | Xiaomi - Rec.2020 - Xiaomi-Log | log |
227
+ | `panasonic-vgamut-lin` | Panasonic - V-Gamut - Linear | scene-linear |
228
+ | `panasonic-vgamut-vlog` | Panasonic - V-Gamut - V-Log | log |
229
+ | `red-wg-lin` | Red - WideGamutRGB - Linear | scene-linear |
230
+ | `red-wg-log3g10` | Red - WideGamutRGB - Log3G10 | log |
231
+ | `sony-sgamut3-lin` | Sony - S-Gamut3 - Linear | scene-linear |
232
+ | `sony-sgamut3-slog3` | Sony - S-Gamut3 - S-Log3 | log |
233
+ | `sony-sgamut3cine-lin` | Sony - S-Gamut3.Cine - Linear | scene-linear |
234
+ | `sony-sgamut3cine-slog3` | Sony - S-Gamut3.Cine - S-Log3 | log |
235
+ | `sony-venice-sgamut3-lin` | Sony - Venice S-Gamut3 - Linear | scene-linear |
236
+ | `sony-venice-sgamut3-slog3` | Sony - Venice S-Gamut3 - S-Log3 | log |
237
+ | `sony-venice-sgamut3cine-lin` | Sony - Venice S-Gamut3.Cine - Linear | scene-linear |
238
+ | `sony-venice-sgamut3cine-slog3` | Sony - Venice S-Gamut3.Cine - S-Log3 | log |
239
+ | `rec709-lin` | Rec.709 - Linear | scene-linear |
240
+ | `rec2020-lin` | Rec.2020 - Linear | scene-linear |
241
+ | `p3d65-lin` | P3D65 - Linear | scene-linear |
242
+ | `rec1886` | Rec.1886 | sdr-video |
243
+ | `srgb-display` | sRGB Display | sdr-video |
244
+ | `srgb-encoding` | sRGB Encoding | sdr-video |
245
+ | `dci-p3d65` | DCIP3 D65 | sdr-video |
246
+ | `display-p3` | Display P3 | sdr-video |
247
+ | `rec2100-pq` | Rec2100 PQ | hdr-video |
248
+ | `rec2100-hlg` | Rec2100 HLG | hdr-video |
249
+
250
+ ## How the curves are defined
251
+
252
+ Most camera log curves are a `Log`: a natural log curve with an optional piecewise linear toe.
253
+
254
+ ```
255
+ y = gain*ln(x + offset) + bias x > toe
256
+ y = slope*(x - toe) + y0 x <= toe
257
+ ```
258
+
259
+ The linear toe always meets the log segment at the same value (C0 continuity). Its slope
260
+ is the one the whitepaper publishes, which is not always the slope of the log segment at
261
+ the join.
262
+
263
+ **Why the slope is kept.** Several curves are designed with a kink at the join: the linear
264
+ segment is steeper than the log segment there (S-Log3 by 27%, F-Log 4.7%, F-Log2 3.7%,
265
+ L-Log 1.3%, D-Log 1.2%). Forcing matching slopes (C1 continuity) would move every code
266
+ value in the toe away from what the camera records. A kink does no harm: the curve still
267
+ rises everywhere, so it decodes exactly. If you build a `Log` without a `slope`, it uses
268
+ the slope of the log segment.
269
+
270
+ **Why the join value is not kept.** Some whitepapers publish the linear segment's offset
271
+ as a separate constant, and with the rounded published values the two segments do not quite
272
+ meet: there is a small step at the join. With a step, decode cannot exactly undo encode
273
+ near the join, because code values inside the step belong to neither segment. F-Log shows
274
+ this in its own whitepaper: the linear segment ends at 0.100639, but its decode formula
275
+ switches segments at 0.100538. mucolor derives the offset so the segments meet, and in
276
+ the toe it differs from the whitepaper by the size of the step:
277
+
278
+ | Curve | Step at the join |
279
+ |---|---|
280
+ | Leica L-Log | 9.0e-4 (about 0.9 of a 10-bit code value) |
281
+ | Fujifilm F-Log | 1.0e-4 |
282
+ | DJI D-Log | 2.0e-6 |
283
+ | Panasonic V-Log | 3.1e-7 |
284
+ | ARRI LogC3 | 2.5e-7 |
285
+ | Fujifilm F-Log2 | 3.6e-8 |
286
+
287
+ Above the join, every curve matches its whitepaper to floating point precision. Below it,
288
+ the difference is a constant offset of exactly the step. `tests/test_external.py` lists the
289
+ tolerance used for each curve.
290
+
291
+ Each curve is built from the constants and formula of its whitepaper, so it can be
292
+ checked against the source:
293
+
294
+ ```python
295
+ # ARRI LogC3 EI800
296
+ # y = c*log10(a*x + b) + d x > cut
297
+ # y = e*x + f x <= cut
298
+ cut, a, b, c, d, e, f = 0.010591, 5.555556, 0.052272, 0.247190, 0.385537, 5.367655, 0.092809
299
+ arri_logc3 = Log.from_base(10, c, a, b, d, toe=cut, slope=e)
300
+ ```
301
+
302
+ Display curves are a `Power` with the same optional toe. Curves that fit neither form are
303
+ a `Curve` of two functions.
304
+
305
+ ## Tests
306
+
307
+ ```
308
+ python -m unittest discover -s tests
309
+ ```
310
+
311
+ `tests/test_external.py` compares every colorspace it can against
312
+ [colour-science](https://www.colour-science.org) and the OCIO built-in ACES studio config.
313
+ It is skipped unless those packages are installed (OCIO 2.6 or newer):
314
+
315
+ ```
316
+ pip install colour-science opencolorio
317
+ python -m unittest discover -s tests
318
+ ```
@@ -0,0 +1,307 @@
1
+ # mucolor
2
+
3
+ **μcolor**, "micro color". The "mu" is the Greek letter μ, the SI symbol for micro: one
4
+ millionth. The name is the goal: the colorimetry a pipeline needs, made as small as it can be.
5
+
6
+ Minimal pure-python colorimetry calculations, transfer functions and colorspace conversions.
7
+
8
+ Convert stimuli between camera, working and display colorspaces. Pure Python, one file,
9
+ no dependencies.
10
+
11
+ ## Install
12
+
13
+ mucolor is a single file. Either copy `mucolor.py` next to your own code, or install it
14
+ from this folder:
15
+
16
+ ```
17
+ pip install .
18
+ ```
19
+
20
+ ## Quick start
21
+
22
+ ```python
23
+ import mucolor as mc
24
+
25
+ # Convert one RGB triplet: (red, green, blue), from colorspace, to colorspace
26
+ mc.convert((0.41, 0.41, 0.41), 'sony-sgamut3-slog3', 'acescg')
27
+ # -> (0.179, 0.179, 0.179)
28
+ ```
29
+
30
+ An RGB triplet is a tuple of three numbers: `(red, green, blue)`. The result is a tuple too.
31
+
32
+ ## Colorspace names
33
+
34
+ Every colorspace has a long name, a short name and an alias. You can use any of them, and
35
+ upper or lower case does not matter. These all result in the same colorspace conversion:
36
+
37
+ ```python
38
+ rgb = (0.41, 0.41, 0.41)
39
+
40
+ mc.convert(rgb, 'Sony - S-Gamut3 - S-Log3', 'acescg')
41
+ mc.convert(rgb, 'sony-sgamut3-slog3', 'acescg')
42
+ mc.convert(rgb, 'sg3lg3', 'acescg')
43
+ ```
44
+
45
+ The [full list](#colorspaces) is at the end of this page. To list them from Python:
46
+
47
+ ```python
48
+ for cs in mc.colorspaces():
49
+ print(cs.shortname, '|', cs.name)
50
+ ```
51
+
52
+ If you misspell a name, the error suggests close matches:
53
+
54
+ ```
55
+ KeyError: "unknown colorspace 'slog3', did you mean 'smlg', 'sg3ln'?"
56
+ ```
57
+
58
+ ## Converting many stimuli
59
+
60
+ `convert` looks up both colorspaces every time you call it. To convert a lot of stimuli
61
+ between the same two colorspaces, make a converter once and reuse it:
62
+
63
+ ```python
64
+ slog3_to_acescg = mc.converter('sony-sgamut3-slog3', 'acescg')
65
+
66
+ slog3_to_acescg((0.41, 0.41, 0.41)) # -> (0.179, 0.179, 0.179)
67
+ slog3_to_acescg((0.6, 0.5, 0.4)) # -> (1.107, 0.434, 0.160)
68
+ ```
69
+
70
+ For bigger challenges, use [numpy](#numpy-arrays) which is much faster to process.
71
+
72
+ ## Transfer functions on their own
73
+
74
+ A transfer function turns linear signal into code values and back. Each one has two methods:
75
+
76
+ - `encode`: linear signal -> code value
77
+ - `decode`: code value -> linear signal
78
+
79
+ They work on one number at a time, not an RGB triplet:
80
+
81
+ ```python
82
+ mc.sony_slog3.encode(0.18) # -> 0.411 (18% grey in S-Log3)
83
+ mc.sony_slog3.decode(0.41) # -> 0.179
84
+ ```
85
+
86
+ ## Matrices
87
+
88
+ `matrix` gives the 3x3 matrix between the linear versions of two colorspaces, as a tuple of
89
+ three rows:
90
+
91
+ ```python
92
+ mc.matrix('p3d65-lin', 'rec709-lin')
93
+ # -> ((1.2249, -0.2249, 0.0), (-0.0421, 1.0421, 0.0), (-0.0196, -0.0786, 1.0983))
94
+ ```
95
+
96
+ ## numpy arrays
97
+
98
+ The functions above work on one RGB triplet at a time, which is slow for large jobs such as
99
+ baking a LUT or converting an image. If you have [numpy](https://numpy.org) installed,
100
+ add `np=True` to work on whole arrays at once:
101
+
102
+ ```python
103
+ import numpy as np
104
+ import mucolor as mc
105
+
106
+ rgb = np.array([ # two RGB triplets
107
+ [0.41, 0.41, 0.41],
108
+ [0.6, 0.5, 0.4],
109
+ ])
110
+ mc.convert(rgb, 'sony-sgamut3-slog3', 'acescg', np=True)
111
+ # -> array([[0.179, 0.179, 0.179],
112
+ # [1.107, 0.434, 0.160]])
113
+ ```
114
+
115
+ `np=True` works the same way everywhere:
116
+
117
+ ```python
118
+ f = mc.converter('sony-sgamut3-slog3', 'acescg', np=True)
119
+ f(rgb)
120
+
121
+ mc.sony_slog3.decode(np.array([0.1, 0.41, 0.6]), np=True)
122
+ # -> array([0.0011, 0.179, 1.037])
123
+ ```
124
+
125
+ Things to know:
126
+
127
+ - The array can have any shape, as long as the last axis holds the three channels: a list
128
+ of RGB triplets `(n, 3)`, an image `(height, width, 3)`, or a LUT grid `(n, n, n, 3)`.
129
+ - You always get a new array back. Your input array is never changed.
130
+ - numpy is only needed when you use `np=True`. Without numpy installed, everything else
131
+ still works.
132
+ - A value a curve cannot handle, such as a negative number into a pure log curve, gives
133
+ `nan` and a numpy warning. Without `np=True` the same value raises an error.
134
+
135
+ ### Example: bake a 3D LUT to a .cube file
136
+
137
+ ```python
138
+ import numpy as np
139
+ import mucolor as mc
140
+
141
+ size = 33
142
+
143
+ # A grid of every input RGB triplet. In a .cube file red changes fastest, then green, then blue.
144
+ steps = np.linspace(0.0, 1.0, size)
145
+ b, g, r = np.meshgrid(steps, steps, steps, indexing='ij')
146
+ grid = np.stack([r, g, b], axis=-1) # shape (33, 33, 33, 3)
147
+
148
+ lut = mc.convert(grid, 'arri-wg4-logc4', 'davinci-wg-ilog', np=True)
149
+
150
+ with open('logc4_to_davinci.cube', 'w') as file:
151
+ file.write(f'LUT_3D_SIZE {size}\n')
152
+ for red, green, blue in lut.reshape(-1, 3):
153
+ file.write(f'{red:.6f} {green:.6f} {blue:.6f}\n')
154
+ ```
155
+
156
+ ## Conventions
157
+
158
+ - **Linear signal**: scene-linear 0.18 is 18% grey. For display colorspaces, linear 1.0 is
159
+ 100 nits, including `pq` and `hlg`.
160
+ - **No "tone mapping"**: mucolor converts colorimetry exactly. Converting a camera log
161
+ image straight to a display colorspace gives values above 1.0 for bright areas, the
162
+ same as a plain colorspace conversion in other software. It is not a viewing transform.
163
+ - **Chromatic adaptation**: when two colorspaces have different white points, mucolor
164
+ adapts between them with CAT02. Pass `cat='bradford'` to use Bradford, or `cat=None`
165
+ for none: `mc.convert((0.18, 0.18, 0.18), 'aces', 'rec709-lin', cat='bradford')`.
166
+ - **Gamuts** are flat tuples of xy chromaticities: `(rx, ry, gx, gy, bx, by, wx, wy)`.
167
+ `mc.matrix` and `mc.npm` accept these as well as colorspace names.
168
+
169
+ ## Colorspaces
170
+
171
+ The short name is shown. `encoding` tells you what kind of values the colorspace holds.
172
+
173
+ | Short name | Name | Encoding |
174
+ |---|---|---|
175
+ | `xyz-d65` | XYZ D65 | scene-linear |
176
+ | `xyz-e` | XYZ E | scene-linear |
177
+ | `aces` | ACES 2065-1 | scene-linear |
178
+ | `acescg` | ACEScg | scene-linear |
179
+ | `acescct` | ACEScct | log |
180
+ | `jplog2` | JPLog2 | log |
181
+ | `filmlight-egamut-lin` | Filmlight - E-Gamut - Linear | scene-linear |
182
+ | `filmlight-egamut-tlog` | Filmlight - E-Gamut - T-Log | log |
183
+ | `filmlight-egamut2-lin` | Filmlight - E-Gamut2 - Linear | scene-linear |
184
+ | `filmlight-egamut2-tlog` | Filmlight - E-Gamut2 - T-Log | log |
185
+ | `filmlight-egamut2-tloge` | Filmlight - E-Gamut2 - T-LogE | log |
186
+ | `davinci-wg-lin` | DaVinci - WideGamut - Linear | scene-linear |
187
+ | `davinci-wg-ilog` | DaVinci - WideGamut - Intermediate Log | log |
188
+ | `arri-wg3-lin` | Arri - WideGamut3 - Linear | scene-linear |
189
+ | `arri-wg3-logc3` | Arri - WideGamut3 - LogC3 | log |
190
+ | `arri-wg4-lin` | Arri - WideGamut4 - Linear | scene-linear |
191
+ | `arri-wg4-logc4` | Arri - WideGamut4 - LogC4 | log |
192
+ | `blackmagic-wg-lin` | Blackmagic - WideGamut - Linear | scene-linear |
193
+ | `blackmagic-wg-gen5log` | Blackmagic - WideGamut - Gen5 Log | log |
194
+ | `canon-cgamut-lin` | Canon - C-Gamut - Linear | scene-linear |
195
+ | `canon-cgamut-clog2` | Canon - C-Gamut - C-Log2 | log |
196
+ | `canon-cgamut-clog3` | Canon - C-Gamut - C-Log3 | log |
197
+ | `dji-dgamut-lin` | DJI - D-Gamut - Linear | scene-linear |
198
+ | `dji-dgamut-dlog` | DJI - D-Gamut - D-Log | log |
199
+ | `fuji-fgamut-lin` | Fujifilm - F-Gamut - Linear | scene-linear |
200
+ | `fuji-fgamut-flog` | Fujifilm - F-Gamut - F-Log | log |
201
+ | `fuji-fgamut-flog2` | Fujifilm - F-Gamut - F-Log2 | log |
202
+ | `fuji-fgamutc-lin` | Fujifilm - F-GamutC - Linear | scene-linear |
203
+ | `fuji-fgamutc-flog2` | Fujifilm - F-GamutC - F-Log2 | log |
204
+ | `cineon` | Kodak Cineon | log |
205
+ | `gopro-rec709-protune` | GoPro - Rec.709 - Protune | log |
206
+ | `leica-rec2020-lin` | Leica - Rec.2020 - Linear | scene-linear |
207
+ | `leica-rec2020-llog` | Leica - Rec.2020 - L-Log | log |
208
+ | `nikon-rec2020-lin` | Nikon - Rec.2020 - Linear | scene-linear |
209
+ | `nikon-rec2020-nlog` | Nikon - Rec.2020 - N-Log | log |
210
+ | `apple-rec2020-lin` | Apple - Rec.2020 - Linear | scene-linear |
211
+ | `apple-rec2020-alog` | Apple - Rec.2020 - Apple-Log | log |
212
+ | `samsung-rec2020-lin` | Samsung - Rec.2020 - Linear | scene-linear |
213
+ | `samsung-rec2020-slog` | Samsung - Rec.2020 - Samsung-Log | log |
214
+ | `xiaomi-rec2020-lin` | Xiaomi - Rec.2020 - Linear | scene-linear |
215
+ | `xiaomi-rec2020-xlog` | Xiaomi - Rec.2020 - Xiaomi-Log | log |
216
+ | `panasonic-vgamut-lin` | Panasonic - V-Gamut - Linear | scene-linear |
217
+ | `panasonic-vgamut-vlog` | Panasonic - V-Gamut - V-Log | log |
218
+ | `red-wg-lin` | Red - WideGamutRGB - Linear | scene-linear |
219
+ | `red-wg-log3g10` | Red - WideGamutRGB - Log3G10 | log |
220
+ | `sony-sgamut3-lin` | Sony - S-Gamut3 - Linear | scene-linear |
221
+ | `sony-sgamut3-slog3` | Sony - S-Gamut3 - S-Log3 | log |
222
+ | `sony-sgamut3cine-lin` | Sony - S-Gamut3.Cine - Linear | scene-linear |
223
+ | `sony-sgamut3cine-slog3` | Sony - S-Gamut3.Cine - S-Log3 | log |
224
+ | `sony-venice-sgamut3-lin` | Sony - Venice S-Gamut3 - Linear | scene-linear |
225
+ | `sony-venice-sgamut3-slog3` | Sony - Venice S-Gamut3 - S-Log3 | log |
226
+ | `sony-venice-sgamut3cine-lin` | Sony - Venice S-Gamut3.Cine - Linear | scene-linear |
227
+ | `sony-venice-sgamut3cine-slog3` | Sony - Venice S-Gamut3.Cine - S-Log3 | log |
228
+ | `rec709-lin` | Rec.709 - Linear | scene-linear |
229
+ | `rec2020-lin` | Rec.2020 - Linear | scene-linear |
230
+ | `p3d65-lin` | P3D65 - Linear | scene-linear |
231
+ | `rec1886` | Rec.1886 | sdr-video |
232
+ | `srgb-display` | sRGB Display | sdr-video |
233
+ | `srgb-encoding` | sRGB Encoding | sdr-video |
234
+ | `dci-p3d65` | DCIP3 D65 | sdr-video |
235
+ | `display-p3` | Display P3 | sdr-video |
236
+ | `rec2100-pq` | Rec2100 PQ | hdr-video |
237
+ | `rec2100-hlg` | Rec2100 HLG | hdr-video |
238
+
239
+ ## How the curves are defined
240
+
241
+ Most camera log curves are a `Log`: a natural log curve with an optional piecewise linear toe.
242
+
243
+ ```
244
+ y = gain*ln(x + offset) + bias x > toe
245
+ y = slope*(x - toe) + y0 x <= toe
246
+ ```
247
+
248
+ The linear toe always meets the log segment at the same value (C0 continuity). Its slope
249
+ is the one the whitepaper publishes, which is not always the slope of the log segment at
250
+ the join.
251
+
252
+ **Why the slope is kept.** Several curves are designed with a kink at the join: the linear
253
+ segment is steeper than the log segment there (S-Log3 by 27%, F-Log 4.7%, F-Log2 3.7%,
254
+ L-Log 1.3%, D-Log 1.2%). Forcing matching slopes (C1 continuity) would move every code
255
+ value in the toe away from what the camera records. A kink does no harm: the curve still
256
+ rises everywhere, so it decodes exactly. If you build a `Log` without a `slope`, it uses
257
+ the slope of the log segment.
258
+
259
+ **Why the join value is not kept.** Some whitepapers publish the linear segment's offset
260
+ as a separate constant, and with the rounded published values the two segments do not quite
261
+ meet: there is a small step at the join. With a step, decode cannot exactly undo encode
262
+ near the join, because code values inside the step belong to neither segment. F-Log shows
263
+ this in its own whitepaper: the linear segment ends at 0.100639, but its decode formula
264
+ switches segments at 0.100538. mucolor derives the offset so the segments meet, and in
265
+ the toe it differs from the whitepaper by the size of the step:
266
+
267
+ | Curve | Step at the join |
268
+ |---|---|
269
+ | Leica L-Log | 9.0e-4 (about 0.9 of a 10-bit code value) |
270
+ | Fujifilm F-Log | 1.0e-4 |
271
+ | DJI D-Log | 2.0e-6 |
272
+ | Panasonic V-Log | 3.1e-7 |
273
+ | ARRI LogC3 | 2.5e-7 |
274
+ | Fujifilm F-Log2 | 3.6e-8 |
275
+
276
+ Above the join, every curve matches its whitepaper to floating point precision. Below it,
277
+ the difference is a constant offset of exactly the step. `tests/test_external.py` lists the
278
+ tolerance used for each curve.
279
+
280
+ Each curve is built from the constants and formula of its whitepaper, so it can be
281
+ checked against the source:
282
+
283
+ ```python
284
+ # ARRI LogC3 EI800
285
+ # y = c*log10(a*x + b) + d x > cut
286
+ # y = e*x + f x <= cut
287
+ cut, a, b, c, d, e, f = 0.010591, 5.555556, 0.052272, 0.247190, 0.385537, 5.367655, 0.092809
288
+ arri_logc3 = Log.from_base(10, c, a, b, d, toe=cut, slope=e)
289
+ ```
290
+
291
+ Display curves are a `Power` with the same optional toe. Curves that fit neither form are
292
+ a `Curve` of two functions.
293
+
294
+ ## Tests
295
+
296
+ ```
297
+ python -m unittest discover -s tests
298
+ ```
299
+
300
+ `tests/test_external.py` compares every colorspace it can against
301
+ [colour-science](https://www.colour-science.org) and the OCIO built-in ACES studio config.
302
+ It is skipped unless those packages are installed (OCIO 2.6 or newer):
303
+
304
+ ```
305
+ pip install colour-science opencolorio
306
+ python -m unittest discover -s tests
307
+ ```