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.
- mucolor-0.1.0/.gitignore +4 -0
- mucolor-0.1.0/LICENSE +21 -0
- mucolor-0.1.0/PKG-INFO +318 -0
- mucolor-0.1.0/README.md +307 -0
- mucolor-0.1.0/mucolor.py +819 -0
- mucolor-0.1.0/pyproject.toml +24 -0
mucolor-0.1.0/.gitignore
ADDED
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
|
+
```
|
mucolor-0.1.0/README.md
ADDED
|
@@ -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
|
+
```
|