-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathAgXBaseRec2020.py
221 lines (177 loc) · 8.47 KB
/
AgXBaseRec2020.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
import colour
import numpy
import sigmoid
import argparse
import luminance_compenstation_bt2020 as lu2020
import Guard_Rail_Upper as high_rail
# Log range parameters
midgrey = 0.18
normalized_log2_minimum = -10
normalized_log2_maximum = +6.5
# define color space matrices
bt2020_id65_to_xyz_id65 = numpy.array([[0.6369535067850740, 0.1446191846692331, 0.1688558539228734],
[0.2626983389565560, 0.6780087657728165, 0.0592928952706273],
[0.0000000000000000, 0.0280731358475570, 1.0608272349505707]])
xyz_id65_to_bt2020_id65 = numpy.array([[1.7166634277958805, -0.3556733197301399, -0.2533680878902478],
[-0.6666738361988869, 1.6164557398246981, 0.0157682970961337],
[0.0176424817849772, -0.0427769763827532, 0.9422432810184308]])
# inset matrix from Troy's SB2383 script, setting is rotate = [3.0, -1, -2.0], inset = [0.4, 0.22, 0.13]
# link to the script: https://github.com/sobotka/SB2383-Configuration-Generation/blob/main/generate_config.py
# the relevant part is at line 88 and 89
inset_matrix = numpy.array([[0.856627153315983, 0.0951212405381588, 0.0482516061458583],
[0.137318972929847, 0.761241990602591, 0.101439036467562],
[0.11189821299995, 0.0767994186031903, 0.811302368396859]])
# outset matrix from Troy's SB2383 script, setting is rotate = [0, 0, 0] inset = [0.4, 0.22, 0.04], used on inverse
# link to the script: https://github.com/sobotka/SB2383-Configuration-Generation/blob/main/generate_config.py
# the relevant part is at line 88 and 89
outset_matrix = numpy.linalg.inv(numpy.array([[0.899796955911611, 0.0871996192028351, 0.013003424885555],
[0.11142098895748, 0.875575586156966, 0.0130034248855548],
[0.11142098895748, 0.0871996192028349, 0.801379391839686]]))
# these lines are dependencies from Troy's AgX script
x_pivot = numpy.abs(normalized_log2_minimum) / (
normalized_log2_maximum - normalized_log2_minimum
)
# define middle grey
y_pivot = 0.18 ** (1.0 / 2.4)
exponent = [1.5, 1.5]
slope = 2.4
argparser = argparse.ArgumentParser(
description="Generates an OpenColorIO configuration",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
argparser.add_argument(
"-et",
"--exponent_toe",
help="Set toe curve rate of change as an exponential power, hello Sean Cooper",
type=float,
default=exponent[0],
)
argparser.add_argument(
"-ps",
"--exponent_shoulder",
help="Set shoulder curve rate of change as an exponential power",
type=float,
default=exponent[1],
)
argparser.add_argument(
"-fs",
"--fulcrum_slope",
help="Set central section rate of change as rise over run slope",
type=float,
default=slope,
)
argparser.add_argument(
"-fi",
"--fulcrum_input",
help="Input fulcrum point relative to the normalized log2 range",
type=float,
default=x_pivot,
)
argparser.add_argument(
"-fo",
"--fulcrum_output",
help="Output fulcrum point relative to the normalized log2 range",
type=float,
default=y_pivot,
)
argparser.add_argument(
"-ll",
"--limit_low",
help="Lowest value of the normalized log2 range",
type=float,
default=normalized_log2_minimum,
)
argparser.add_argument(
"-lh",
"--limit_high",
help="Highest value of the normalized log2 range",
type=float,
default=normalized_log2_maximum,
)
args = argparser.parse_args()
# these lines are dependencies from Troy's AgX script
def apply_sigmoid(x):
sig_x_input = x
col = sigmoid.calculate_sigmoid(
sig_x_input,
pivots=[args.fulcrum_input, args.fulcrum_output],
slope=args.fulcrum_slope,
powers=[args.exponent_toe, args.exponent_shoulder],
)
return col
def AgX_Base_Rec2020(col, mix_percent):
# apply lower guard rail
col = lu2020.compensate_low_side(col)
# apply inset matrix
col = numpy.tensordot(col, inset_matrix, axes=(0, 1))
# record current chromaticity angle
pre_form_hsv = colour.RGB_to_HSV(col)
# apply Log2 curve to prepare for sigmoid
log = colour.log_encoding(col,
function='Log2',
min_exposure=normalized_log2_minimum,
max_exposure=normalized_log2_maximum,
middle_grey=midgrey)
# apply sigmoid
col = apply_sigmoid(log)
# Linearize
col = colour.models.exponent_function_basic(col, 2.4, 'basicFwd')
# record post-sigmoid chroma angle
col = colour.RGB_to_HSV(col)
# mix pre-formation chroma angle with post formation chroma angle.
col[0] = colour.algebra.lerp(mix_percent / 100, pre_form_hsv[0], col[0], False)
col = colour.HSV_to_RGB(col)
# apply outset to make the result more chroma-laden
col = numpy.tensordot(col, outset_matrix, axes=(0, 1))
return col
colour.utilities.filter_warnings(python_warnings=True)
def main():
# resolution of the 3D LUT
LUT_res = 37
# The mix_percent here is the mixing factor of the pre- and post-formation chroma angle. Specifically, a simple HSV here was used.
# Mixing, or lerp-ing the H is a hack here that does not fit a first-principle design.
# I tried other methods but this seems to be the most straight forward way.
# I just can't bare to see our rotation of primaries, the "flourish", is messed up with a per-channel notorious six hue shift.
# This means if we rotate red a bit towards orange for countering abney effect, the orange will then be skewed to yellow.
# Then we apply the rotation in different primaries, like in BT.2020, where BT.709 red is already more orangish in the first place,
# this gets magnified. Troy's original version has outset that also includes the inverse rotation, but because the original rotation
# has already been skewed by the per-channel N6, the outset matrix in his version didn't cancel the rotation. This seems like such a
# mess to me, so I decided to take this hacky approach at least to get the flourish rotation somewhat in control.
# The result is also that my outset matrix now doesn't contain any rotation, otherwise the original rotation can actually be cancelled.
# The number of 40% here is based on personal testing, you can try to test which number works better if you would like to change it.
mix_percent = 40
LUT = colour.LUT3D(name=f'AgX_Formation Rec.2020',
size=LUT_res)
LUT.domain = ([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]])
LUT.comments = [f'AgX Base Rec.2020 Formation LUT',
f'This LUT expects input to be E Gamut Log2 encoding from -10 stops to +15 stops',
f'But the end image formation will be from {normalized_log2_minimum} to {normalized_log2_maximum} encoded in power 2.4',
f' rotate = [3.0, -1, -2.0], inset = [0.4, 0.22, 0.13], outset = [0.4, 0.22, 0.04]',
f'The image formed has {mix_percent}% per-channel shifts']
x, y, z, _ = LUT.table.shape
for i in range(x):
for j in range(y):
for k in range(z):
col = numpy.array(LUT.table[i][j][k], dtype=numpy.longdouble)
# decode LUT input transfer function
col = colour.log_decoding(col,
function='Log2',
min_exposure=-10,
max_exposure=+15,
middle_grey=midgrey)
# decode LUT input primaries from E-Gamut to Rec.2020
col = numpy.tensordot(col, lu2020.e_gamut_to_xyz_id65, axes=(0, 1))
col = numpy.tensordot(col, lu2020.xyz_id65_to_bt2020_id65, axes=(0, 1))
col = AgX_Base_Rec2020(col, mix_percent)
# re-encode transfer function
col = colour.models.exponent_function_basic(col, 2.4, 'basicRev')
LUT.table[i][j][k] = numpy.array(col, dtype=LUT.table.dtype)
colour.write_LUT(
LUT,
f"AgX_Base_Rec2020.cube")
print(LUT)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
pass