mucolor 0.1.0__py3-none-any.whl
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.dist-info/METADATA +318 -0
- mucolor-0.1.0.dist-info/RECORD +5 -0
- mucolor-0.1.0.dist-info/WHEEL +4 -0
- mucolor-0.1.0.dist-info/licenses/LICENSE +21 -0
- mucolor.py +819 -0
|
@@ -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,5 @@
|
|
|
1
|
+
mucolor.py,sha256=w65bHc4bgRD2dj0FPQVYDA2x9qWfQ7wYQG2eEmyG1ZY,35776
|
|
2
|
+
mucolor-0.1.0.dist-info/METADATA,sha256=cL7mE0KDvEDIbNiuHCPxc_CXAc1QEWPkv4b5Nh-fC_A,12439
|
|
3
|
+
mucolor-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
4
|
+
mucolor-0.1.0.dist-info/licenses/LICENSE,sha256=6pUWAfL3ibwwmlcv1c32hXOBjTkODpviMp6iFazgfQA,1066
|
|
5
|
+
mucolor-0.1.0.dist-info/RECORD,,
|
|
@@ -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.py
ADDED
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
''' mucolor - minimal pure-python colorimetry
|
|
2
|
+
|
|
3
|
+
import mucolor as mc
|
|
4
|
+
mc.convert((0.4, 0.4, 0.4), 'sony-sgamut3-slog3', 'rec709-lin')
|
|
5
|
+
f = mc.converter('arri-wg4-logc4', 'srgb-display') # resolve once, call many times
|
|
6
|
+
mc.matrix('p3d65-lin', 'rec709-lin') # 3x3 tuple of tuples
|
|
7
|
+
mc.sony_slog3.decode(0.41) # transfer functions on their own
|
|
8
|
+
|
|
9
|
+
Conventions
|
|
10
|
+
encode: linear signal -> code value. decode: code value -> linear signal.
|
|
11
|
+
Gamuts are flat xy chromaticity tuples: (rx, ry, gx, gy, bx, by, wx, wy).
|
|
12
|
+
Display linear 1.0 is 100 nits for every display transfer function.
|
|
13
|
+
|
|
14
|
+
numpy
|
|
15
|
+
Pass np=True to any encode, decode, converter or convert to process numpy arrays.
|
|
16
|
+
numpy is imported only then. RGB arrays have shape (..., 3) and results are float64.
|
|
17
|
+
Out of domain values give nan with a numpy warning where the scalar version raises.
|
|
18
|
+
|
|
19
|
+
f = mc.converter('arri-wg4-logc4', 'srgb-display', np=True)
|
|
20
|
+
f(lut_grid) # array of shape (n, n, n, 3)
|
|
21
|
+
'''
|
|
22
|
+
import difflib
|
|
23
|
+
import math
|
|
24
|
+
from types import SimpleNamespace
|
|
25
|
+
from typing import Callable, NamedTuple
|
|
26
|
+
|
|
27
|
+
__version__ = '0.1.0'
|
|
28
|
+
__license__ = 'MIT License'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
###################################################################################################
|
|
32
|
+
# Scalar or numpy: each formula is written once against xp, which is numpy with np=True,
|
|
33
|
+
# or this stdlib stand-in for the numpy functions the formulas use
|
|
34
|
+
|
|
35
|
+
_math = SimpleNamespace(log=math.log, log2=math.log2, log10=math.log10, exp=math.exp, sqrt=math.sqrt,
|
|
36
|
+
copysign=math.copysign, maximum=max)
|
|
37
|
+
_np = None
|
|
38
|
+
|
|
39
|
+
def _array(x):
|
|
40
|
+
# x as a float64 numpy array, importing numpy on first use
|
|
41
|
+
global _np
|
|
42
|
+
if _np is None:
|
|
43
|
+
try:
|
|
44
|
+
import numpy
|
|
45
|
+
except ImportError:
|
|
46
|
+
raise ImportError('np=True requires numpy') from None
|
|
47
|
+
_np = numpy
|
|
48
|
+
return _np.asarray(x, dtype=float)
|
|
49
|
+
|
|
50
|
+
def _xp(x, np):
|
|
51
|
+
# x and the namespace to compute it with
|
|
52
|
+
return (_array(x), _np) if np else (x, _math)
|
|
53
|
+
|
|
54
|
+
def _split(rgb, np):
|
|
55
|
+
# RGB as three channels: scalars, or arrays of shape (...)
|
|
56
|
+
return (rgb[..., 0], rgb[..., 1], rgb[..., 2]) if np else tuple(rgb)
|
|
57
|
+
|
|
58
|
+
def _join(channels, np):
|
|
59
|
+
return _np.stack(channels, axis=-1) if np else tuple(channels)
|
|
60
|
+
|
|
61
|
+
def _select(x, *pieces):
|
|
62
|
+
''' if / elif / else on a scalar or a numpy array.
|
|
63
|
+
pieces are (condition, value) pairs then a final value. Each value is a constant or
|
|
64
|
+
a function, evaluated only where its condition is the first one true, so out of
|
|
65
|
+
domain branches never see the wrong values.
|
|
66
|
+
'''
|
|
67
|
+
if _np is None or not isinstance(x, _np.ndarray):
|
|
68
|
+
for p in pieces:
|
|
69
|
+
cond, value = p if type(p) is tuple else (True, p)
|
|
70
|
+
if cond:
|
|
71
|
+
return value(x) if callable(value) else value
|
|
72
|
+
out = _np.empty_like(x)
|
|
73
|
+
todo = _np.ones(x.shape, dtype=bool)
|
|
74
|
+
for cond, value in [p if isinstance(p, tuple) else (True, p) for p in pieces]:
|
|
75
|
+
m = todo & cond
|
|
76
|
+
out[m] = value(x[m]) if callable(value) else value
|
|
77
|
+
todo &= ~m
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
###################################################################################################
|
|
82
|
+
# Matrix helpers: 3x3 matrices are tuples of row tuples
|
|
83
|
+
|
|
84
|
+
_IDENTITY = ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0))
|
|
85
|
+
|
|
86
|
+
def _matmul(m0, m1):
|
|
87
|
+
return tuple(tuple(sum(a*b for a, b in zip(r, c)) for c in zip(*m1)) for r in m0)
|
|
88
|
+
|
|
89
|
+
def _dot(m, v):
|
|
90
|
+
return tuple(sum(a*b for a, b in zip(r, v)) for r in m)
|
|
91
|
+
|
|
92
|
+
def _diag(v):
|
|
93
|
+
return ((v[0], 0.0, 0.0), (0.0, v[1], 0.0), (0.0, 0.0, v[2]))
|
|
94
|
+
|
|
95
|
+
def _inv(m):
|
|
96
|
+
(a, b, c), (d, e, f), (g, h, i) = m
|
|
97
|
+
det = a*(e*i - f*h) - b*(d*i - f*g) + c*(d*h - e*g)
|
|
98
|
+
if det == 0.0:
|
|
99
|
+
raise ValueError(f'matrix is not invertible: {m}')
|
|
100
|
+
return (
|
|
101
|
+
((e*i - f*h)/det, (c*h - b*i)/det, (b*f - c*e)/det),
|
|
102
|
+
((f*g - d*i)/det, (a*i - c*g)/det, (c*d - a*f)/det),
|
|
103
|
+
((d*h - e*g)/det, (b*g - a*h)/det, (a*e - b*d)/det))
|
|
104
|
+
|
|
105
|
+
def _is_identity(m, tol=1e-12):
|
|
106
|
+
return all(abs(a - b) <= tol for r0, r1 in zip(m, _IDENTITY) for a, b in zip(r0, r1))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
###################################################################################################
|
|
110
|
+
# Colorimetry
|
|
111
|
+
|
|
112
|
+
def xy_to_XYZ(xy):
|
|
113
|
+
# xy chromaticity to XYZ tristimulus with Y=1
|
|
114
|
+
x, y = xy
|
|
115
|
+
return (x/y, 1.0, (1.0 - x - y)/y)
|
|
116
|
+
|
|
117
|
+
def npm(gamut):
|
|
118
|
+
''' Normalized Primaries Matrix: RGB in gamut -> CIE XYZ
|
|
119
|
+
Primaries (1,0) (0,1) (0,0) denote CIE XYZ itself, which returns identity.
|
|
120
|
+
SMPTE RP 177 - Derivation of Basic Television Color Equations
|
|
121
|
+
http://doi.org/10.5594/S9781614821915
|
|
122
|
+
'''
|
|
123
|
+
rx, ry, gx, gy, bx, by, wx, wy = gamut
|
|
124
|
+
if (rx, ry, gx, gy, bx, by) == (1, 0, 0, 1, 0, 0):
|
|
125
|
+
return _IDENTITY
|
|
126
|
+
P = ((rx, gx, bx), (ry, gy, by), (1.0 - rx - ry, 1.0 - gx - gy, 1.0 - bx - by))
|
|
127
|
+
C = _dot(_inv(P), xy_to_XYZ((wx, wy)))
|
|
128
|
+
return _matmul(P, _diag(C))
|
|
129
|
+
|
|
130
|
+
_CAT = {
|
|
131
|
+
'bradford': ((0.8951, 0.2664, -0.1614), (-0.7502, 1.7135, 0.0367), (0.0389, -0.0685, 1.0296)),
|
|
132
|
+
'cat02': ((0.7328, 0.4296, -0.1624), (-0.7036, 1.6975, 0.0061), (0.003, 0.0136, 0.9834)),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def cat(src_white, dst_white, method='cat02'):
|
|
136
|
+
''' von Kries chromatic adaptation matrix (XYZ -> XYZ) between two xy whitepoints
|
|
137
|
+
method is 'cat02', 'bradford', or None for no adaptation.
|
|
138
|
+
Mark D. Fairchild - Color Appearance Models Third Edition p. 181-186
|
|
139
|
+
Bruce Lindbloom - http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
|
|
140
|
+
'''
|
|
141
|
+
if method is None or tuple(src_white) == tuple(dst_white):
|
|
142
|
+
return _IDENTITY
|
|
143
|
+
try:
|
|
144
|
+
m = _CAT[method]
|
|
145
|
+
except KeyError:
|
|
146
|
+
raise ValueError(f'unknown cat method {method!r}, use one of {sorted(_CAT)} or None') from None
|
|
147
|
+
s = _dot(m, xy_to_XYZ(src_white))
|
|
148
|
+
d = _dot(m, xy_to_XYZ(dst_white))
|
|
149
|
+
return _matmul(_matmul(_inv(m), _diag([b/a for a, b in zip(s, d)])), m)
|
|
150
|
+
|
|
151
|
+
_cat = cat # matrix() has a parameter named cat
|
|
152
|
+
|
|
153
|
+
def matrix(src, dst, cat='cat02'):
|
|
154
|
+
''' 3x3 matrix converting linear RGB in src to linear RGB in dst, with chromatic adaptation
|
|
155
|
+
src and dst are colorspaces, colorspace names, or gamut tuples.
|
|
156
|
+
'''
|
|
157
|
+
src, dst = _gamut(src), _gamut(dst)
|
|
158
|
+
return _matmul(_inv(npm(dst)), _matmul(_cat(src[6:], dst[6:], cat), npm(src)))
|
|
159
|
+
|
|
160
|
+
def _gamut(x):
|
|
161
|
+
if isinstance(x, (tuple, list)) and len(x) == 8:
|
|
162
|
+
return tuple(x)
|
|
163
|
+
cs = get(x)
|
|
164
|
+
if cs.gamut is None:
|
|
165
|
+
raise ValueError(f'{cs.name!r} has no gamut')
|
|
166
|
+
return cs.gamut
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
###################################################################################################
|
|
170
|
+
# Transfer function types
|
|
171
|
+
|
|
172
|
+
class Log(NamedTuple):
|
|
173
|
+
''' Log curve in natural log form, with an optional piecewise linear extension toe.
|
|
174
|
+
encode (linear -> log):
|
|
175
|
+
y = gain*ln(x + offset) + bias x > toe
|
|
176
|
+
y = slope*(x - toe) + y0 x <= toe, y0 = gain*ln(toe + offset) + bias
|
|
177
|
+
slope defaults to the slope of the log segment at toe.
|
|
178
|
+
toe=None means a pure log curve, defined only for x > -offset.
|
|
179
|
+
'''
|
|
180
|
+
gain: float
|
|
181
|
+
offset: float
|
|
182
|
+
bias: float
|
|
183
|
+
toe: float = None
|
|
184
|
+
slope: float = None
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def from_base(cls, base, gain, scale, offset, bias, toe=None, slope=None):
|
|
188
|
+
''' Build from the form most whitepapers use: y = gain*log_base(scale*x + offset) + bias '''
|
|
189
|
+
g = gain/math.log(base)
|
|
190
|
+
return cls(g, offset/scale, bias + g*math.log(scale), toe, slope)
|
|
191
|
+
|
|
192
|
+
def _toe(self):
|
|
193
|
+
slope = self.gain/(self.toe + self.offset) if self.slope is None else self.slope
|
|
194
|
+
return slope, self.gain*math.log(self.toe + self.offset) + self.bias
|
|
195
|
+
|
|
196
|
+
def encode(self, x, np=False):
|
|
197
|
+
x, xp = _xp(x, np)
|
|
198
|
+
log = lambda x: self.gain*xp.log(x + self.offset) + self.bias
|
|
199
|
+
if self.toe is None:
|
|
200
|
+
return log(x)
|
|
201
|
+
slope, y0 = self._toe()
|
|
202
|
+
return _select(x, (x <= self.toe, lambda x: slope*(x - self.toe) + y0), log)
|
|
203
|
+
|
|
204
|
+
def decode(self, y, np=False):
|
|
205
|
+
y, xp = _xp(y, np)
|
|
206
|
+
exp = lambda y: xp.exp((y - self.bias)/self.gain) - self.offset
|
|
207
|
+
if self.toe is None:
|
|
208
|
+
return exp(y)
|
|
209
|
+
slope, y0 = self._toe()
|
|
210
|
+
return _select(y, (y <= y0, lambda y: (y - y0)/slope + self.toe), exp)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Power(NamedTuple):
|
|
214
|
+
''' Power curve with an optional piecewise linear extension toe.
|
|
215
|
+
decode (code -> linear): x = ((y + offset)/(1 + offset))**p
|
|
216
|
+
encode (linear -> code):
|
|
217
|
+
y = (1 + offset)*x**(1/p) - offset x > toe
|
|
218
|
+
y = slope*(x - toe) + y0 x <= toe, y0 = the power segment at toe
|
|
219
|
+
slope defaults to the slope of the power segment at toe.
|
|
220
|
+
Without a toe, negative values clamp to 0.
|
|
221
|
+
'''
|
|
222
|
+
p: float
|
|
223
|
+
offset: float = 0.0
|
|
224
|
+
toe: float = None
|
|
225
|
+
slope: float = None
|
|
226
|
+
|
|
227
|
+
def _pow(self, x):
|
|
228
|
+
return (1.0 + self.offset)*x**(1.0/self.p) - self.offset
|
|
229
|
+
|
|
230
|
+
def _inv_pow(self, y):
|
|
231
|
+
return ((y + self.offset)/(1.0 + self.offset))**self.p
|
|
232
|
+
|
|
233
|
+
def _toe(self):
|
|
234
|
+
slope = (1.0 + self.offset)/self.p*self.toe**(1.0/self.p - 1.0) if self.slope is None else self.slope
|
|
235
|
+
return slope, self._pow(self.toe)
|
|
236
|
+
|
|
237
|
+
def encode(self, x, np=False):
|
|
238
|
+
x, _ = _xp(x, np)
|
|
239
|
+
if self.toe is None:
|
|
240
|
+
return _select(x, (x > 0.0, self._pow), 0.0)
|
|
241
|
+
slope, y0 = self._toe()
|
|
242
|
+
return _select(x, (x <= self.toe, lambda x: slope*(x - self.toe) + y0), (x > 0.0, self._pow), 0.0)
|
|
243
|
+
|
|
244
|
+
def decode(self, y, np=False):
|
|
245
|
+
y, _ = _xp(y, np)
|
|
246
|
+
if self.toe is None:
|
|
247
|
+
return _select(y, (y > 0.0, self._inv_pow), 0.0)
|
|
248
|
+
slope, y0 = self._toe()
|
|
249
|
+
return _select(y, (y <= y0, lambda y: (y - y0)/slope + self.toe), (y > 0.0, self._inv_pow), 0.0)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class Curve(NamedTuple):
|
|
253
|
+
''' A transfer function that fits no parametric form: a pair of functions f(x, xp).
|
|
254
|
+
xp is numpy with np=True, or a stdlib stand-in with the same names, so each
|
|
255
|
+
formula is written once. Branch with _select rather than if / else.
|
|
256
|
+
'''
|
|
257
|
+
encode_fn: Callable
|
|
258
|
+
decode_fn: Callable
|
|
259
|
+
|
|
260
|
+
def encode(self, x, np=False):
|
|
261
|
+
return self.encode_fn(*_xp(x, np))
|
|
262
|
+
|
|
263
|
+
def decode(self, y, np=False):
|
|
264
|
+
return self.decode_fn(*_xp(y, np))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class HLG(NamedTuple):
|
|
268
|
+
''' ITU-R BT.2100 Hybrid Log-Gamma, display referred: inverse OETF then OOTF.
|
|
269
|
+
Works on RGB triplets because the OOTF depends on luminance.
|
|
270
|
+
peak is the nominal display peak in nits. Negative values clamp to 0.
|
|
271
|
+
https://www.itu.int/rec/R-REC-BT.2100
|
|
272
|
+
'''
|
|
273
|
+
peak: float = 1000.0
|
|
274
|
+
rgb = True # class attribute, not a field
|
|
275
|
+
|
|
276
|
+
_a = 0.17883277
|
|
277
|
+
_b = 1.0 - 4.0*0.17883277
|
|
278
|
+
_c = 0.5 - 0.17883277*math.log(4.0*0.17883277)
|
|
279
|
+
|
|
280
|
+
def _gamma(self):
|
|
281
|
+
return 1.2 + 0.42*math.log10(self.peak/1000.0)
|
|
282
|
+
|
|
283
|
+
def encode(self, rgb, np=False):
|
|
284
|
+
rgb, xp = _xp(rgb, np)
|
|
285
|
+
F = [xp.maximum(v, 0.0)*100.0/self.peak for v in _split(rgb, np)]
|
|
286
|
+
Yd = 0.2627*F[0] + 0.6780*F[1] + 0.0593*F[2]
|
|
287
|
+
g = self._gamma()
|
|
288
|
+
s = _select(Yd, (Yd > 0.0, lambda Y: Y**((1.0 - g)/g)), 0.0)
|
|
289
|
+
return _join([_select(e, (e <= 1.0/12.0, lambda e: xp.sqrt(3.0*e)), lambda e: self._a*xp.log(12.0*e - self._b) + self._c)
|
|
290
|
+
for e in (f*s for f in F)], np)
|
|
291
|
+
|
|
292
|
+
def decode(self, rgb, np=False):
|
|
293
|
+
rgb, xp = _xp(rgb, np)
|
|
294
|
+
E = [_select(e, (e <= 0.5, lambda e: e*e/3.0), lambda e: (xp.exp((e - self._c)/self._a) + self._b)/12.0)
|
|
295
|
+
for e in (xp.maximum(v, 0.0) for v in _split(rgb, np))]
|
|
296
|
+
Ys = 0.2627*E[0] + 0.6780*E[1] + 0.0593*E[2]
|
|
297
|
+
s = _select(Ys, (Ys > 0.0, lambda Y: self.peak/100.0*Y**(self._gamma() - 1.0)), 0.0)
|
|
298
|
+
return _join([e*s for e in E], np)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
###################################################################################################
|
|
302
|
+
# Camera log transfer functions
|
|
303
|
+
|
|
304
|
+
# ACEScct - S-2016-001 http://j.mp/S-2016-001_
|
|
305
|
+
# y = (log2(x) + 9.72)/17.52 x > 0.0078125
|
|
306
|
+
# y = A*x + B x <= 0.0078125
|
|
307
|
+
acescct = Log.from_base(2, 1/17.52, 1.0, 0.0, 9.72/17.52, toe=0.0078125, slope=10.5402377416545)
|
|
308
|
+
|
|
309
|
+
# JPLog2 - https://community.acescentral.com/t/aceslog-strawman-proposal/5270
|
|
310
|
+
# y = (log2(x) + 10.5)/20.46 x > 0.006801176276
|
|
311
|
+
# y = ln_s*x + ln_o x <= 0.006801176276
|
|
312
|
+
jplog2 = Log.from_base(2, 1/20.46, 1.0, 0.0, 10.5/20.46, toe=0.006801176276, slope=10.36773919972907075549)
|
|
313
|
+
|
|
314
|
+
def _filmlight_tlog(w=128.0, g=16.0, o=0.075):
|
|
315
|
+
# FilmLight T-Log - Baselight /etc/colourspaces/FilmLight_TLog_EGamut.flspace
|
|
316
|
+
# y = A + B*ln(x + C) x >= 0
|
|
317
|
+
# y = G*x + o x < 0
|
|
318
|
+
b = 1.0/(0.7107 + 1.2359*math.log(w*g))
|
|
319
|
+
gs = g/(1.0 - o)
|
|
320
|
+
C = b/gs
|
|
321
|
+
a = 1.0 - b*math.log(w + C)
|
|
322
|
+
y0 = a + b*math.log(C)
|
|
323
|
+
s = (1.0 - o)/(1.0 - y0)
|
|
324
|
+
A = 1.0 + (a - 1.0)*s
|
|
325
|
+
B = b*s
|
|
326
|
+
return Log(gain=B, offset=C, bias=A, toe=0.0) # G = gs*s is the log slope at 0
|
|
327
|
+
|
|
328
|
+
filmlight_tlog = _filmlight_tlog()
|
|
329
|
+
filmlight_tloge = _filmlight_tlog(w=512.0) # extended scene-linear range for 3D LUT shapers
|
|
330
|
+
|
|
331
|
+
def _davinci_intermediate():
|
|
332
|
+
# DaVinci Intermediate
|
|
333
|
+
# https://documents.blackmagicdesign.com/InformationNotes/DaVinci_Resolve_17_Wide_Gamut_Intermediate.pdf
|
|
334
|
+
# y = (log2(x + A) + B)*C x > LIN_CUT
|
|
335
|
+
# y = x*M x <= LIN_CUT
|
|
336
|
+
A, B, C, M, LIN_CUT = 0.0075, 7.0, 0.07329248, 10.44426855, 0.00262409
|
|
337
|
+
return Log.from_base(2, C, 1.0, A, B*C, toe=LIN_CUT, slope=M)
|
|
338
|
+
|
|
339
|
+
davinci_intermediate = _davinci_intermediate()
|
|
340
|
+
|
|
341
|
+
def _arri_logc3():
|
|
342
|
+
# ARRI LogC3 EI800
|
|
343
|
+
# https://www.arri.com/resource/blob/31918/66f56e6abb6e5b6553929edf9aa7483e/2017-03-alexa-logc-curve-in-vfx-data.pdf
|
|
344
|
+
# y = c*log10(a*x + b) + d x > cut
|
|
345
|
+
# y = e*x + f x <= cut
|
|
346
|
+
cut, a, b, c, d, e, f = 0.010591, 5.555556, 0.052272, 0.247190, 0.385537, 5.367655, 0.092809
|
|
347
|
+
return Log.from_base(10, c, a, b, d, toe=cut, slope=e)
|
|
348
|
+
|
|
349
|
+
arri_logc3 = _arri_logc3()
|
|
350
|
+
|
|
351
|
+
def _arri_logc4():
|
|
352
|
+
# ARRI LogC4
|
|
353
|
+
# https://www.arri.com/resource/blob/278790/bea879ac0d041a925bed27a096ab3ec2/2022-05-arri-logc4-specification-data.pdf
|
|
354
|
+
# y = (log2(a*x + 64) - 6)/14*b + c x >= t
|
|
355
|
+
# y = (x - t)/s x < t
|
|
356
|
+
a = (2.0**18.0 - 16.0)/117.45
|
|
357
|
+
b = (1023.0 - 95.0)/1023.0
|
|
358
|
+
c = 95.0/1023.0
|
|
359
|
+
s = (7.0*math.log(2.0)*2.0**(7.0 - 14.0*c/b))/(a*b)
|
|
360
|
+
t = (2.0**(14.0*(-c/b) + 6.0) - 64.0)/a
|
|
361
|
+
return Log.from_base(2, b/14.0, a, 64.0, c - 6.0*b/14.0, toe=t, slope=1.0/s)
|
|
362
|
+
|
|
363
|
+
arri_logc4 = _arri_logc4()
|
|
364
|
+
|
|
365
|
+
def _blackmagic_film_gen5():
|
|
366
|
+
# Blackmagic Film Generation 5
|
|
367
|
+
# Blackmagic RAW SDK: Blackmagic Generation 5 Color Science Technical Reference.pdf
|
|
368
|
+
# y = A*ln(x + B) + C x >= LIN_CUT
|
|
369
|
+
# y = D*x + E x < LIN_CUT
|
|
370
|
+
A, B, C, D = 0.08692876065491224, 0.005494072432257808, 0.5300133392291939, 8.283605932402494
|
|
371
|
+
LIN_CUT = 0.005
|
|
372
|
+
return Log(gain=A, offset=B, bias=C, toe=LIN_CUT, slope=D)
|
|
373
|
+
|
|
374
|
+
blackmagic_film_gen5 = _blackmagic_film_gen5()
|
|
375
|
+
|
|
376
|
+
# DJI D-Log - https://dl.djicdn.com/downloads/zenmuse+x7/20171010/D-Log_D-Gamut_Whitepaper.pdf
|
|
377
|
+
# y = log10(x*0.9892 + 0.0108)*0.256663 + 0.584555 x > 0.0078
|
|
378
|
+
# y = 6.025*x + 0.0929 x <= 0.0078
|
|
379
|
+
dji_dlog = Log.from_base(10, 0.256663, 0.9892, 0.0108, 0.584555, toe=0.0078, slope=6.025)
|
|
380
|
+
|
|
381
|
+
def _fujifilm_flog(a, b, c, d, e, cut1):
|
|
382
|
+
# Fujifilm F-Log / F-Log2
|
|
383
|
+
# y = c*log10(a*x + b) + d x >= cut1
|
|
384
|
+
# y = e*x + f x < cut1
|
|
385
|
+
return Log.from_base(10, c, a, b, d, toe=cut1, slope=e)
|
|
386
|
+
|
|
387
|
+
# https://dl.fujifilm-x.com/support/lut/F-Log_DataSheet_E_Ver.1.0.pdf
|
|
388
|
+
fujifilm_flog = _fujifilm_flog(a=0.555556, b=0.009468, c=0.344676, d=0.790453, e=8.735631, cut1=0.00089)
|
|
389
|
+
# https://dl.fujifilm-x.com/support/lut/F-Log2_DataSheet_E_Ver.1.0.pdf
|
|
390
|
+
fujifilm_flog2 = _fujifilm_flog(a=5.555556, b=0.064829, c=0.245281, d=0.384316, e=8.799461, cut1=0.000889)
|
|
391
|
+
|
|
392
|
+
def _leica_llog():
|
|
393
|
+
# Leica L-Log - https://leica-camera.com/sites/default/files/pm-65977-210914__L-Log_Reference_Manual_EN.pdf
|
|
394
|
+
# y = c*log10(d*x + e) + f x >= c0
|
|
395
|
+
# y = a*x + b x < c0
|
|
396
|
+
a, b, c, d, e, f, c0 = 8.0, 0.09, 0.27, 1.3, 0.0115, 0.6, 0.006
|
|
397
|
+
return Log.from_base(10, c, d, e, f, toe=c0, slope=a)
|
|
398
|
+
|
|
399
|
+
leica_llog = _leica_llog()
|
|
400
|
+
|
|
401
|
+
def _panasonic_vlog():
|
|
402
|
+
# Panasonic V-Log - https://pro-av.panasonic.net/en/cinema_camera_varicam_eva/support/pdf/VARICAM_V-Log_V-Gamut.pdf
|
|
403
|
+
# y = c*log10(x + b) + d x >= cut1
|
|
404
|
+
# y = 5.6*x + 0.125 x < cut1
|
|
405
|
+
cut1, b, c, d = 0.01, 0.00873, 0.241514, 0.598206
|
|
406
|
+
return Log.from_base(10, c, 1.0, b, d, toe=cut1, slope=5.6)
|
|
407
|
+
|
|
408
|
+
panasonic_vlog = _panasonic_vlog()
|
|
409
|
+
|
|
410
|
+
def _red_log3g10():
|
|
411
|
+
# RED Log3G10
|
|
412
|
+
# https://docs.red.com/955-0187/PDF/915-0187%20Rev-C%20%20%20RED%20OPS,%20White%20Paper%20on%20REDWideGamutRGB%20and%20Log3G10.pdf
|
|
413
|
+
# y = a*log10((x + c)*b + 1) x >= -c
|
|
414
|
+
# y = (x + c)*g x < -c
|
|
415
|
+
a, b, c, g = 0.224282, 155.975327, 0.01, 15.1927
|
|
416
|
+
return Log.from_base(10, a, b, c*b + 1.0, 0.0, toe=-c, slope=g)
|
|
417
|
+
|
|
418
|
+
red_log3g10 = _red_log3g10()
|
|
419
|
+
|
|
420
|
+
def _sony_slog2():
|
|
421
|
+
# Sony S-Log2 - S-Log2_Technical_PaperV1_0.pdf
|
|
422
|
+
# x' = x/0.9
|
|
423
|
+
# y' = c0*log10(c1*x'/c2 + c3) + c4 + c5 x' >= 0
|
|
424
|
+
# y' = x'*c6 + c7 x' < 0
|
|
425
|
+
# y = y'*876/1023 + 64/1023
|
|
426
|
+
c0, c1, c2, c3, c4, c5, c6 = 0.432699, 155.0, 219.0, 0.037584, 0.616596, 0.03, 3.53881278538813
|
|
427
|
+
return Log.from_base(10, c0*876/1023, c1/(c2*0.9), c3, (c4 + c5)*876/1023 + 64/1023,
|
|
428
|
+
toe=0.0, slope=c6/0.9*876/1023)
|
|
429
|
+
|
|
430
|
+
sony_slog2 = _sony_slog2()
|
|
431
|
+
|
|
432
|
+
def _sony_slog3():
|
|
433
|
+
# Sony S-Log3 - https://pro.sony/s3/cms-static-content/uploadfile/06/1237494271406.pdf
|
|
434
|
+
# y = (420 + log10((x + 0.01)/(0.18 + 0.01))*261.5)/1023 x >= 0.01125
|
|
435
|
+
# y = (x*(171.2102946929 - 95)/0.01125 + 95)/1023 x < 0.01125
|
|
436
|
+
return Log.from_base(10, 261.5/1023, 1/(0.18 + 0.01), 0.01/(0.18 + 0.01), 420/1023,
|
|
437
|
+
toe=0.01125, slope=(171.2102946929 - 95)/0.01125/1023)
|
|
438
|
+
|
|
439
|
+
sony_slog3 = _sony_slog3()
|
|
440
|
+
|
|
441
|
+
def _kodak_cineon():
|
|
442
|
+
# Kodak Cineon - https://github.com/imageworks/OpenColorIO-Configs/blob/master/nuke-default/make.py
|
|
443
|
+
# y = (a + b*log10(x*(1 - off) + off))/1023, off = 10**((c - a)/b)
|
|
444
|
+
a, b, c = 685.0, 300.0, 95.0
|
|
445
|
+
off = 10.0**((c - a)/b)
|
|
446
|
+
return Log.from_base(10, b/1023, 1.0 - off, off, a/1023)
|
|
447
|
+
|
|
448
|
+
kodak_cineon = _kodak_cineon()
|
|
449
|
+
|
|
450
|
+
# GoPro Protune - https://github.com/hpd/OpenColorIO-Configs/blob/master/aces_1.0.3/python/aces_ocio/colorspaces/gopro.py
|
|
451
|
+
# y = ln(x*112 + 1)/ln(113)
|
|
452
|
+
gopro_protune = Log.from_base(113, 1.0, 112.0, 1.0, 0.0)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# Curves below fit no parametric form
|
|
456
|
+
|
|
457
|
+
def _acescc_encode(x, xp):
|
|
458
|
+
A, B = 9.72, 17.52
|
|
459
|
+
return _select(x,
|
|
460
|
+
(x <= 0.0, (-16.0 + A)/B),
|
|
461
|
+
(x < 2.0**-15, lambda x: (xp.log2(2.0**-16 + x/2.0) + A)/B),
|
|
462
|
+
lambda x: (xp.log2(x) + A)/B)
|
|
463
|
+
|
|
464
|
+
def _acescc_decode(y, xp):
|
|
465
|
+
A, B = 9.72, 17.52
|
|
466
|
+
return _select(y,
|
|
467
|
+
(y <= (A - 15.0)/B, lambda y: (2.0**(y*B - A) - 2.0**-16)*2.0),
|
|
468
|
+
(y < (math.log2(65504.0) + A)/B, lambda y: 2.0**(y*B - A)),
|
|
469
|
+
65504.0)
|
|
470
|
+
|
|
471
|
+
acescc = Curve(_acescc_encode, _acescc_decode)
|
|
472
|
+
|
|
473
|
+
# Canon Log - https://downloads.canon.com/nw/learn/white-papers/cinema-eos/white-paper-canon-log-gamma-curves.pdf
|
|
474
|
+
# The /0.9 on scene-linear matches Canon's CTL IDTs ("Input Transform Version 202007 for EOS C500 Mark II").
|
|
475
|
+
# The negative side is a mirrored log rather than a straight line.
|
|
476
|
+
|
|
477
|
+
def _canon_clog2_encode(x, xp):
|
|
478
|
+
c0, c1, c2 = 0.092864125, 0.24136077, 87.099375
|
|
479
|
+
x = x/0.9
|
|
480
|
+
return _select(x, (x < 0.0, lambda x: -c1*xp.log10(1.0 - c2*x) + c0), lambda x: c1*xp.log10(c2*x + 1.0) + c0)
|
|
481
|
+
|
|
482
|
+
def _canon_clog2_decode(y, xp):
|
|
483
|
+
c0, c1, c2 = 0.092864125, 0.24136077, 87.099375
|
|
484
|
+
x = _select(y, (y < c0, lambda y: -(10.0**((c0 - y)/c1) - 1.0)/c2), lambda y: (10.0**((y - c0)/c1) - 1.0)/c2)
|
|
485
|
+
return x*0.9
|
|
486
|
+
|
|
487
|
+
canon_clog2 = Curve(_canon_clog2_encode, _canon_clog2_decode)
|
|
488
|
+
|
|
489
|
+
_CLOG3 = (0.014, 0.09746547, 0.15277891, 0.36726845, 14.98325, 0.12783901, 1.9754798, 0.12512219, 0.12240537)
|
|
490
|
+
|
|
491
|
+
def _canon_clog3_encode(x, xp):
|
|
492
|
+
sp0, sp1, sp2, c0, c1, c2, c3, c4, c5 = _CLOG3
|
|
493
|
+
x = x/0.9
|
|
494
|
+
return _select(x,
|
|
495
|
+
(x < -sp0, lambda x: -c0*xp.log10(1.0 - c1*x) + c2),
|
|
496
|
+
(x <= sp0, lambda x: c3*x + c4),
|
|
497
|
+
lambda x: c0*xp.log10(c1*x + 1.0) + c5)
|
|
498
|
+
|
|
499
|
+
def _canon_clog3_decode(y, xp):
|
|
500
|
+
sp0, sp1, sp2, c0, c1, c2, c3, c4, c5 = _CLOG3
|
|
501
|
+
x = _select(y,
|
|
502
|
+
(y < sp1, lambda y: -(10.0**((c2 - y)/c0) - 1.0)/c1),
|
|
503
|
+
(y <= sp2, lambda y: (y - c4)/c3),
|
|
504
|
+
lambda y: (10.0**((y - c5)/c0) - 1.0)/c1)
|
|
505
|
+
return x*0.9
|
|
506
|
+
|
|
507
|
+
canon_clog3 = Curve(_canon_clog3_encode, _canon_clog3_decode)
|
|
508
|
+
|
|
509
|
+
# Nikon N-Log - http://download.nikonimglib.com/archive3/hDCmK00m9JDI03RPruD74xpoU905/N-Log_Specification_(En)01.pdf
|
|
510
|
+
# y = b*ln(x) + a x >= 0.328
|
|
511
|
+
# y = c*(x + 0.0075)**(1/3) x < 0.328
|
|
512
|
+
|
|
513
|
+
def _nikon_nlog_encode(x, xp):
|
|
514
|
+
a, b, c, d = 619.0/1023.0, 150.0/1023.0, 650.0/1023.0, 0.0075
|
|
515
|
+
return _select(x, (x >= 0.328, lambda x: b*xp.log(x) + a), lambda x: c*xp.copysign(abs(x + d)**(1.0/3.0), x + d))
|
|
516
|
+
|
|
517
|
+
def _nikon_nlog_decode(y, xp):
|
|
518
|
+
a, b, c, d = 619.0/1023.0, 150.0/1023.0, 650.0/1023.0, 0.0075
|
|
519
|
+
return _select(y, (y >= 452.0/1023.0, lambda y: xp.exp((y - a)/b)), lambda y: (y/c)**3 - d)
|
|
520
|
+
|
|
521
|
+
nikon_nlog = Curve(_nikon_nlog_encode, _nikon_nlog_decode)
|
|
522
|
+
|
|
523
|
+
# Apple Log - Apple_Log_Profile_White_Paper.pdf (Apple Developer)
|
|
524
|
+
# y = g*log2(x + b) + d x >= R_t
|
|
525
|
+
# y = c*(x - R_0)**2 R_0 <= x < R_t
|
|
526
|
+
# y = 0 x < R_0
|
|
527
|
+
_APPLE = (-0.05641088, 0.01, 47.28711236, 0.00964052, 0.08550479, 0.69336945) # R_0, R_t, c, b, g, d
|
|
528
|
+
|
|
529
|
+
def _apple_log_encode(x, xp):
|
|
530
|
+
R_0, R_t, c, b, g, d = _APPLE
|
|
531
|
+
return _select(x, (x >= R_t, lambda x: g*xp.log2(x + b) + d), (x >= R_0, lambda x: c*(x - R_0)**2), 0.0)
|
|
532
|
+
|
|
533
|
+
def _apple_log_decode(y, xp):
|
|
534
|
+
R_0, R_t, c, b, g, d = _APPLE
|
|
535
|
+
return _select(y,
|
|
536
|
+
(y >= c*(R_t - R_0)**2, lambda y: 2.0**((y - d)/g) - b),
|
|
537
|
+
(y >= 0.0, lambda y: xp.sqrt(y/c) + R_0),
|
|
538
|
+
R_0)
|
|
539
|
+
|
|
540
|
+
apple_log = Curve(_apple_log_encode, _apple_log_decode)
|
|
541
|
+
|
|
542
|
+
# Samsung Log - Samsung Log White Paper 2025, https://developer.samsung.com/mobile/samsung-log-video.html
|
|
543
|
+
# y = a1*log10(x + b1) + y1 x >= xt
|
|
544
|
+
# y = a2*log10(b2 - x) + y2 x0 <= x < xt
|
|
545
|
+
# y = 0 x < x0
|
|
546
|
+
_SAMSUNG = (-0.05, 0.01, 0.258984868, 0.0003645, 0.720504856, -0.20942, 0.016904, -0.24597, 0.206561909)
|
|
547
|
+
# x0, xt, a1, b1, y1, a2, b2, y2, yt
|
|
548
|
+
|
|
549
|
+
def _samsung_log_encode(x, xp):
|
|
550
|
+
x0, xt, a1, b1, y1, a2, b2, y2, yt = _SAMSUNG
|
|
551
|
+
return _select(x,
|
|
552
|
+
(x < x0, 0.0),
|
|
553
|
+
(x < xt, lambda x: a2*xp.log10(b2 - x) + y2),
|
|
554
|
+
lambda x: a1*xp.log10(x + b1) + y1)
|
|
555
|
+
|
|
556
|
+
def _samsung_log_decode(y, xp):
|
|
557
|
+
x0, xt, a1, b1, y1, a2, b2, y2, yt = _SAMSUNG
|
|
558
|
+
return _select(y,
|
|
559
|
+
(y < 0.0, x0),
|
|
560
|
+
(y < yt, lambda y: -10.0**((y - y2)/a2) + b2),
|
|
561
|
+
lambda y: 10.0**((y - y1)/a1) - b1)
|
|
562
|
+
|
|
563
|
+
samsung_log = Curve(_samsung_log_encode, _samsung_log_decode)
|
|
564
|
+
|
|
565
|
+
# Xiaomi Log - Xiaomi Log Profile White Paper, December 2024, https://www.mi.com/global/product/aces/
|
|
566
|
+
# y = a*log2(x + b) + g x >= xt
|
|
567
|
+
# y = c*(x - x0)**2 x0 <= x < xt
|
|
568
|
+
# y = 0 x < x0
|
|
569
|
+
_XIAOMI = (-0.09023729, 0.01974185, 0.09271529, 0.01384578, 0.67291850, 18.10531998) # x0, xt, a, b, g, c
|
|
570
|
+
|
|
571
|
+
def _xiaomi_log_encode(x, xp):
|
|
572
|
+
x0, xt, a, b, g, c = _XIAOMI
|
|
573
|
+
return _select(x, (x < x0, 0.0), (x < xt, lambda x: c*(x - x0)**2), lambda x: a*xp.log2(x + b) + g)
|
|
574
|
+
|
|
575
|
+
def _xiaomi_log_decode(y, xp):
|
|
576
|
+
x0, xt, a, b, g, c = _XIAOMI
|
|
577
|
+
return _select(y,
|
|
578
|
+
(y < 0.0, x0),
|
|
579
|
+
(y < c*(xt - x0)**2, lambda y: xp.sqrt(y/c) + x0),
|
|
580
|
+
lambda y: 2.0**((y - g)/a) - b)
|
|
581
|
+
|
|
582
|
+
xiaomi_log = Curve(_xiaomi_log_encode, _xiaomi_log_decode)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
###################################################################################################
|
|
586
|
+
# Display transfer functions
|
|
587
|
+
|
|
588
|
+
gamma22 = Power(2.2) # sRGB display, Display P3
|
|
589
|
+
gamma24 = Power(2.4) # ITU-R BT.1886
|
|
590
|
+
gamma26 = Power(2.6) # DCI theatrical projection
|
|
591
|
+
|
|
592
|
+
# IEC 61966-2-1 sRGB piecewise encoding. The correct EOTF for an sRGB display is gamma22.
|
|
593
|
+
srgb_piecewise = Power(2.4, offset=0.055, toe=0.04045/12.92, slope=12.92)
|
|
594
|
+
|
|
595
|
+
# ITU-R BT.709 camera OETF, using the precise continuous constants from ITU-R BT.2020
|
|
596
|
+
# y = alpha*x**0.45 - (alpha - 1) x >= beta
|
|
597
|
+
# y = 4.5*x x < beta
|
|
598
|
+
rec709_oetf = Power(1/0.45, offset=1.09929682680944 - 1.0, toe=0.018053968510807, slope=4.5)
|
|
599
|
+
|
|
600
|
+
# SMPTE ST 2084 PQ - https://www.itu.int/rec/R-REC-BT.2100
|
|
601
|
+
_PQ = (2610.0/16384.0, 2523.0/32.0, 107.0/128.0, 2413.0/128.0, 2392.0/128.0) # m1, m2, c1, c2, c3
|
|
602
|
+
|
|
603
|
+
def _pq_encode(x, xp):
|
|
604
|
+
m1, m2, c1, c2, c3 = _PQ
|
|
605
|
+
def pq(x):
|
|
606
|
+
x = (x/100.0)**m1
|
|
607
|
+
return ((c1 + c2*x)/(1.0 + c3*x))**m2
|
|
608
|
+
return _select(x, (x <= 0.0, 0.0), pq)
|
|
609
|
+
|
|
610
|
+
def _pq_decode(y, xp):
|
|
611
|
+
m1, m2, c1, c2, c3 = _PQ
|
|
612
|
+
def pq(y):
|
|
613
|
+
y = y**(1.0/m2)
|
|
614
|
+
return (xp.maximum(y - c1, 0.0)/(c2 - c3*y))**(1.0/m1)*100.0
|
|
615
|
+
return _select(y, (y <= 0.0, 0.0), pq)
|
|
616
|
+
|
|
617
|
+
pq = Curve(_pq_encode, _pq_decode)
|
|
618
|
+
|
|
619
|
+
hlg = HLG(1000.0)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
###################################################################################################
|
|
623
|
+
# Colorspaces
|
|
624
|
+
|
|
625
|
+
class Colorspace(NamedTuple):
|
|
626
|
+
name: str
|
|
627
|
+
shortname: str
|
|
628
|
+
alias: str
|
|
629
|
+
encoding: str # 'scene-linear', 'log', 'sdr-video' or 'hdr-video'
|
|
630
|
+
gamut: tuple
|
|
631
|
+
tf: object = None # None means linear
|
|
632
|
+
|
|
633
|
+
def decode(self, rgb, np=False):
|
|
634
|
+
''' code values -> linear RGB '''
|
|
635
|
+
if self.tf is None:
|
|
636
|
+
return _array(rgb).copy() if np else tuple(rgb)
|
|
637
|
+
if np or getattr(self.tf, 'rgb', False):
|
|
638
|
+
return self.tf.decode(rgb, np=np) # elementwise on the whole array, or an RGB transfer function
|
|
639
|
+
return tuple(self.tf.decode(v) for v in rgb)
|
|
640
|
+
|
|
641
|
+
def encode(self, rgb, np=False):
|
|
642
|
+
''' linear RGB -> code values '''
|
|
643
|
+
if self.tf is None:
|
|
644
|
+
return _array(rgb).copy() if np else tuple(rgb)
|
|
645
|
+
if np or getattr(self.tf, 'rgb', False):
|
|
646
|
+
return self.tf.encode(rgb, np=np) # elementwise on the whole array, or an RGB transfer function
|
|
647
|
+
return tuple(self.tf.encode(v) for v in rgb)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
# Whitepoints
|
|
651
|
+
d65 = (0.3127, 0.329)
|
|
652
|
+
aces_white = (0.32168, 0.33767)
|
|
653
|
+
|
|
654
|
+
# Gamuts
|
|
655
|
+
xyz_d65 = (1, 0, 0, 1, 0, 0) + d65
|
|
656
|
+
xyz_e = (1, 0, 0, 1, 0, 0, 1/3, 1/3)
|
|
657
|
+
# TB-2014-004 http://j.mp/TB-2014-004
|
|
658
|
+
aces_ap0 = (0.7347, 0.2653, 0.0, 1.0, 0.0001, -0.077) + aces_white
|
|
659
|
+
# S-2014-004 http://j.mp/S-2014-004
|
|
660
|
+
aces_ap1 = (0.713, 0.293, 0.165, 0.83, 0.128, 0.044) + aces_white
|
|
661
|
+
rec709 = (0.64, 0.33, 0.3, 0.6, 0.15, 0.06) + d65
|
|
662
|
+
rec2020 = (0.708, 0.292, 0.17, 0.797, 0.131, 0.046) + d65
|
|
663
|
+
p3d65 = (0.68, 0.32, 0.265, 0.69, 0.15, 0.06) + d65
|
|
664
|
+
# Baselight /etc/colourspaces/FilmLight_Linear_EGamut.flspace
|
|
665
|
+
filmlight_egamut = (0.8, 0.3177, 0.18, 0.9, 0.065, -0.0805) + d65
|
|
666
|
+
# Baselight /etc/colourspaces/FilmLight_Linear_EGamut2.flspace
|
|
667
|
+
filmlight_egamut2 = (0.83, 0.31, 0.15, 0.95, 0.065, -0.0805) + d65
|
|
668
|
+
# https://documents.blackmagicdesign.com/InformationNotes/DaVinci_Resolve_17_Wide_Gamut_Intermediate.pdf
|
|
669
|
+
davinci_wide_gamut = (0.8, 0.313, 0.1682, 0.9877, 0.079, -0.1155) + d65
|
|
670
|
+
# https://www.arri.com/resource/blob/31918/66f56e6abb6e5b6553929edf9aa7483e/2017-03-alexa-logc-curve-in-vfx-data.pdf
|
|
671
|
+
arri_wide_gamut3 = (0.684, 0.313, 0.221, 0.848, 0.0861, -0.102) + d65
|
|
672
|
+
# https://www.arri.com/resource/blob/278790/bea879ac0d041a925bed27a096ab3ec2/2022-05-arri-logc4-specification-data.pdf
|
|
673
|
+
arri_wide_gamut4 = (0.7347, 0.2653, 0.1424, 0.8576, 0.0991, -0.0308) + d65
|
|
674
|
+
# Blackmagic RAW SDK: Blackmagic Generation 5 Color Science Technical Reference.pdf
|
|
675
|
+
# The whitepoint is specified as (0.3127170, 0.3290312), not exactly D65.
|
|
676
|
+
blackmagic_wide_gamut = (0.7177215, 0.3171181, 0.228041, 0.861569, 0.1005841, -0.0820452, 0.3127170, 0.3290312)
|
|
677
|
+
# Canon Cinema Gamut, D55 daylight IDT (not tungsten)
|
|
678
|
+
# Input Transform Version 202007 for EOS C300 Mark III - canon-eos-c300mk3-idt-202007.zip
|
|
679
|
+
canon_cinema_gamut = (0.74, 0.27, 0.17, 1.14, 0.08, -0.1) + d65
|
|
680
|
+
# https://dl.djicdn.com/downloads/zenmuse+x7/20171010/D-Log_D-Gamut_Whitepaper.pdf
|
|
681
|
+
dji_dgamut = (0.71, 0.31, 0.21, 0.88, 0.09, -0.08) + d65
|
|
682
|
+
# F-Gamut is Rec.2020 - https://dl.fujifilm-x.com/support/lut/F-Log_DataSheet_E_Ver.1.0.pdf
|
|
683
|
+
fujifilm_fgamut = rec2020
|
|
684
|
+
# https://dl.fujifilm-x.com/support/lut/F-Log2C_DataSheet_E_Ver.1.0.pdf
|
|
685
|
+
fujifilm_fgamut_c = (0.7347, 0.2653, 0.0263, 0.9737, 0.1173, -0.0224) + d65
|
|
686
|
+
# https://pro-av.panasonic.net/en/cinema_camera_varicam_eva/support/pdf/VARICAM_V-Log_V-Gamut.pdf
|
|
687
|
+
panasonic_vgamut = (0.73, 0.28, 0.165, 0.84, 0.1, -0.03) + d65
|
|
688
|
+
# https://docs.red.com/955-0187/PDF/915-0187%20Rev-C%20%20%20RED%20OPS,%20White%20Paper%20on%20REDWideGamutRGB%20and%20Log3G10.pdf
|
|
689
|
+
red_wide_gamut = (0.780308, 0.304253, 0.121595, 1.493994, 0.095612, -0.084589) + d65
|
|
690
|
+
# S-Gamut and S-Gamut3 share primaries - https://pro.sony/ue_US/support/article/CA000001
|
|
691
|
+
sony_sgamut3 = (0.73, 0.28, 0.14, 0.855, 0.1, -0.05) + d65
|
|
692
|
+
sony_sgamut3_cine = (0.766, 0.275, 0.225, 0.8, 0.089, -0.087) + d65
|
|
693
|
+
# Venice gamuts: chromaticities calculated from the AP0 matrices in the Sony Venice IDTs
|
|
694
|
+
# https://github.com/ampas/aces-core/blob/710ecbe52c87ce9f4a1e02c8ddf7ea0d6b611cc8/transforms/ctl/idt/vendorSupplied/sony/IDT.Sony.Venice_SLog3_SGamut3.ctl
|
|
695
|
+
sony_venice_sgamut3 = (0.740464264304292, 0.27936437475066, 0.089241145423286, 0.893809528608105,
|
|
696
|
+
0.110488236673827, -0.052579333080476) + d65
|
|
697
|
+
# https://github.com/ampas/aces-core/blob/710ecbe52c87ce9f4a1e02c8ddf7ea0d6b611cc8/transforms/ctl/idt/vendorSupplied/sony/IDT.Sony.Venice_SLog3_SGamut3Cine.ctl
|
|
698
|
+
sony_venice_sgamut3_cine = (0.775901871567345, 0.274502392854799, 0.188682902773355, 0.828684937020288,
|
|
699
|
+
0.101337382499301, -0.089187517306263) + d65
|
|
700
|
+
|
|
701
|
+
_C = Colorspace
|
|
702
|
+
_COLORSPACES = (
|
|
703
|
+
_C('XYZ D65', 'xyz-d65', 'xyzd65', 'scene-linear', xyz_d65),
|
|
704
|
+
_C('XYZ E', 'xyz-e', 'xyze', 'scene-linear', xyz_e),
|
|
705
|
+
_C('ACES 2065-1', 'aces', 'ap0', 'scene-linear', aces_ap0),
|
|
706
|
+
_C('ACEScg', 'acescg', 'ap1', 'scene-linear', aces_ap1),
|
|
707
|
+
_C('ACEScct', 'acescct', 'acescct', 'log', aces_ap1, acescct),
|
|
708
|
+
_C('JPLog2', 'jplog2', 'jplg2', 'log', aces_ap1, jplog2),
|
|
709
|
+
_C('Filmlight - E-Gamut - Linear', 'filmlight-egamut-lin', 'fegln', 'scene-linear', filmlight_egamut),
|
|
710
|
+
_C('Filmlight - E-Gamut - T-Log', 'filmlight-egamut-tlog', 'feglg', 'log', filmlight_egamut, filmlight_tlog),
|
|
711
|
+
_C('Filmlight - E-Gamut2 - Linear', 'filmlight-egamut2-lin', 'feg2ln', 'scene-linear', filmlight_egamut2),
|
|
712
|
+
_C('Filmlight - E-Gamut2 - T-Log', 'filmlight-egamut2-tlog', 'feg2lg', 'log', filmlight_egamut2, filmlight_tlog),
|
|
713
|
+
_C('Filmlight - E-Gamut2 - T-LogE', 'filmlight-egamut2-tloge', 'feg2lge', 'log', filmlight_egamut2, filmlight_tloge),
|
|
714
|
+
_C('DaVinci - WideGamut - Linear', 'davinci-wg-lin', 'dwgln', 'scene-linear', davinci_wide_gamut),
|
|
715
|
+
_C('DaVinci - WideGamut - Intermediate Log', 'davinci-wg-ilog', 'dwglg', 'log', davinci_wide_gamut, davinci_intermediate),
|
|
716
|
+
_C('Arri - WideGamut3 - Linear', 'arri-wg3-lin', 'awgln', 'scene-linear', arri_wide_gamut3),
|
|
717
|
+
_C('Arri - WideGamut3 - LogC3', 'arri-wg3-logc3', 'awglg', 'log', arri_wide_gamut3, arri_logc3),
|
|
718
|
+
_C('Arri - WideGamut4 - Linear', 'arri-wg4-lin', 'awg4ln', 'scene-linear', arri_wide_gamut4),
|
|
719
|
+
_C('Arri - WideGamut4 - LogC4', 'arri-wg4-logc4', 'awg4lg', 'log', arri_wide_gamut4, arri_logc4),
|
|
720
|
+
_C('Blackmagic - WideGamut - Linear', 'blackmagic-wg-lin', 'bwgln', 'scene-linear', blackmagic_wide_gamut),
|
|
721
|
+
_C('Blackmagic - WideGamut - Gen5 Log', 'blackmagic-wg-gen5log', 'bwglg', 'log', blackmagic_wide_gamut, blackmagic_film_gen5),
|
|
722
|
+
_C('Canon - C-Gamut - Linear', 'canon-cgamut-lin', 'ccgln', 'scene-linear', canon_cinema_gamut),
|
|
723
|
+
_C('Canon - C-Gamut - C-Log2', 'canon-cgamut-clog2', 'ccglg2', 'log', canon_cinema_gamut, canon_clog2),
|
|
724
|
+
_C('Canon - C-Gamut - C-Log3', 'canon-cgamut-clog3', 'ccglg3', 'log', canon_cinema_gamut, canon_clog3),
|
|
725
|
+
_C('DJI - D-Gamut - Linear', 'dji-dgamut-lin', 'dgln', 'scene-linear', dji_dgamut),
|
|
726
|
+
_C('DJI - D-Gamut - D-Log', 'dji-dgamut-dlog', 'dglg', 'log', dji_dgamut, dji_dlog),
|
|
727
|
+
_C('Fujifilm - F-Gamut - Linear', 'fuji-fgamut-lin', 'fgln', 'scene-linear', fujifilm_fgamut),
|
|
728
|
+
_C('Fujifilm - F-Gamut - F-Log', 'fuji-fgamut-flog', 'fglg', 'log', fujifilm_fgamut, fujifilm_flog),
|
|
729
|
+
_C('Fujifilm - F-Gamut - F-Log2', 'fuji-fgamut-flog2', 'fglg2', 'log', fujifilm_fgamut, fujifilm_flog2),
|
|
730
|
+
_C('Fujifilm - F-GamutC - Linear', 'fuji-fgamutc-lin', 'fgcln', 'scene-linear', fujifilm_fgamut_c),
|
|
731
|
+
_C('Fujifilm - F-GamutC - F-Log2', 'fuji-fgamutc-flog2', 'fgclg2', 'log', fujifilm_fgamut_c, fujifilm_flog2),
|
|
732
|
+
_C('Kodak Cineon', 'cineon', 'cin', 'log', rec709, kodak_cineon),
|
|
733
|
+
_C('GoPro - Rec.709 - Protune', 'gopro-rec709-protune', 'gplg', 'log', rec709, gopro_protune),
|
|
734
|
+
_C('Leica - Rec.2020 - Linear', 'leica-rec2020-lin', 'llgln', 'scene-linear', rec2020),
|
|
735
|
+
_C('Leica - Rec.2020 - L-Log', 'leica-rec2020-llog', 'llglg', 'log', rec2020, leica_llog),
|
|
736
|
+
_C('Nikon - Rec.2020 - Linear', 'nikon-rec2020-lin', 'ngln', 'scene-linear', rec2020),
|
|
737
|
+
_C('Nikon - Rec.2020 - N-Log', 'nikon-rec2020-nlog', 'nglg', 'log', rec2020, nikon_nlog),
|
|
738
|
+
_C('Apple - Rec.2020 - Linear', 'apple-rec2020-lin', 'apln', 'scene-linear', rec2020),
|
|
739
|
+
_C('Apple - Rec.2020 - Apple-Log', 'apple-rec2020-alog', 'aplg', 'log', rec2020, apple_log),
|
|
740
|
+
_C('Samsung - Rec.2020 - Linear', 'samsung-rec2020-lin', 'smln', 'scene-linear', rec2020),
|
|
741
|
+
_C('Samsung - Rec.2020 - Samsung-Log', 'samsung-rec2020-slog', 'smlg', 'log', rec2020, samsung_log),
|
|
742
|
+
_C('Xiaomi - Rec.2020 - Linear', 'xiaomi-rec2020-lin', 'xmln', 'scene-linear', rec2020),
|
|
743
|
+
_C('Xiaomi - Rec.2020 - Xiaomi-Log', 'xiaomi-rec2020-xlog', 'xmlg', 'log', rec2020, xiaomi_log),
|
|
744
|
+
_C('Panasonic - V-Gamut - Linear', 'panasonic-vgamut-lin', 'vgln', 'scene-linear', panasonic_vgamut),
|
|
745
|
+
_C('Panasonic - V-Gamut - V-Log', 'panasonic-vgamut-vlog', 'vglg', 'log', panasonic_vgamut, panasonic_vlog),
|
|
746
|
+
_C('Red - WideGamutRGB - Linear', 'red-wg-lin', 'rwgln', 'scene-linear', red_wide_gamut),
|
|
747
|
+
_C('Red - WideGamutRGB - Log3G10', 'red-wg-log3g10', 'rwglg', 'log', red_wide_gamut, red_log3g10),
|
|
748
|
+
_C('Sony - S-Gamut3 - Linear', 'sony-sgamut3-lin', 'sg3ln', 'scene-linear', sony_sgamut3),
|
|
749
|
+
_C('Sony - S-Gamut3 - S-Log3', 'sony-sgamut3-slog3', 'sg3lg3', 'log', sony_sgamut3, sony_slog3),
|
|
750
|
+
_C('Sony - S-Gamut3.Cine - Linear', 'sony-sgamut3cine-lin', 'sg3cln', 'scene-linear', sony_sgamut3_cine),
|
|
751
|
+
_C('Sony - S-Gamut3.Cine - S-Log3', 'sony-sgamut3cine-slog3', 'sg3clg3', 'log', sony_sgamut3_cine, sony_slog3),
|
|
752
|
+
_C('Sony - Venice S-Gamut3 - Linear', 'sony-venice-sgamut3-lin', 'vsg3ln', 'scene-linear', sony_venice_sgamut3),
|
|
753
|
+
_C('Sony - Venice S-Gamut3 - S-Log3', 'sony-venice-sgamut3-slog3', 'vsg3lg3', 'log', sony_venice_sgamut3, sony_slog3),
|
|
754
|
+
_C('Sony - Venice S-Gamut3.Cine - Linear', 'sony-venice-sgamut3cine-lin', 'vsg3cln', 'scene-linear', sony_venice_sgamut3_cine),
|
|
755
|
+
_C('Sony - Venice S-Gamut3.Cine - S-Log3', 'sony-venice-sgamut3cine-slog3', 'vsg3clg3', 'log', sony_venice_sgamut3_cine, sony_slog3),
|
|
756
|
+
_C('Rec.709 - Linear', 'rec709-lin', 'rec709ln', 'scene-linear', rec709),
|
|
757
|
+
_C('Rec.2020 - Linear', 'rec2020-lin', 'rec2020ln', 'scene-linear', rec2020),
|
|
758
|
+
_C('P3D65 - Linear', 'p3d65-lin', 'p3d65ln', 'scene-linear', p3d65),
|
|
759
|
+
_C('Rec.1886', 'rec1886', 'rec1886', 'sdr-video', rec709, gamma24),
|
|
760
|
+
_C('sRGB Display', 'srgb-display', 'srgb-display', 'sdr-video', rec709, gamma22),
|
|
761
|
+
_C('sRGB Encoding', 'srgb-encoding', 'srgb-encoding', 'sdr-video', rec709, srgb_piecewise),
|
|
762
|
+
_C('DCIP3 D65', 'dci-p3d65', 'dci-p3d65', 'sdr-video', p3d65, gamma26),
|
|
763
|
+
_C('Display P3', 'display-p3', 'display-p3', 'sdr-video', p3d65, gamma22),
|
|
764
|
+
_C('Rec2100 PQ', 'rec2100-pq', 'pq', 'hdr-video', rec2020, pq),
|
|
765
|
+
_C('Rec2100 HLG', 'rec2100-hlg', 'hlg', 'hdr-video', rec2020, hlg),
|
|
766
|
+
)
|
|
767
|
+
del _C
|
|
768
|
+
|
|
769
|
+
def _build_index():
|
|
770
|
+
# Every name, shortname and alias must identify exactly one colorspace
|
|
771
|
+
index = {}
|
|
772
|
+
for cs in _COLORSPACES:
|
|
773
|
+
for key in {cs.name.lower(), cs.shortname.lower(), cs.alias.lower()}:
|
|
774
|
+
if key in index:
|
|
775
|
+
raise ValueError(f'colorspace key {key!r} is used by both {index[key].name!r} and {cs.name!r}')
|
|
776
|
+
index[key] = cs
|
|
777
|
+
return index
|
|
778
|
+
|
|
779
|
+
_INDEX = _build_index()
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
###################################################################################################
|
|
783
|
+
# Public interface
|
|
784
|
+
|
|
785
|
+
def colorspaces():
|
|
786
|
+
''' All available colorspaces '''
|
|
787
|
+
return _COLORSPACES
|
|
788
|
+
|
|
789
|
+
def get(name):
|
|
790
|
+
''' Look up a colorspace by name, shortname or alias (case insensitive) '''
|
|
791
|
+
if isinstance(name, Colorspace):
|
|
792
|
+
return name
|
|
793
|
+
try:
|
|
794
|
+
return _INDEX[name.lower()]
|
|
795
|
+
except KeyError:
|
|
796
|
+
close = difflib.get_close_matches(name.lower(), _INDEX, n=3)
|
|
797
|
+
hint = f', did you mean {", ".join(map(repr, close))}?' if close else ''
|
|
798
|
+
raise KeyError(f'unknown colorspace {name!r}{hint}') from None
|
|
799
|
+
|
|
800
|
+
def converter(src, dst, cat='cat02', np=False):
|
|
801
|
+
''' Return a function converting an RGB triplet from colorspace src to colorspace dst
|
|
802
|
+
With np=True it converts a numpy array of shape (..., 3) instead.
|
|
803
|
+
'''
|
|
804
|
+
src, dst = get(src), get(dst)
|
|
805
|
+
m = matrix(src, dst, cat=cat)
|
|
806
|
+
if np:
|
|
807
|
+
if _is_identity(m):
|
|
808
|
+
return lambda rgb: dst.encode(src.decode(rgb, np=True), np=True)
|
|
809
|
+
mt = _array(m).T
|
|
810
|
+
return lambda rgb: dst.encode(src.decode(rgb, np=True) @ mt, np=True)
|
|
811
|
+
if _is_identity(m):
|
|
812
|
+
return lambda rgb: dst.encode(src.decode(rgb))
|
|
813
|
+
return lambda rgb: dst.encode(_dot(m, src.decode(rgb)))
|
|
814
|
+
|
|
815
|
+
def convert(rgb, src, dst, cat='cat02', np=False):
|
|
816
|
+
''' Convert an RGB triplet from colorspace src to colorspace dst
|
|
817
|
+
With np=True rgb is a numpy array of shape (..., 3).
|
|
818
|
+
'''
|
|
819
|
+
return converter(src, dst, cat=cat, np=np)(rgb)
|