@@ -7,7 +7,7 @@ def rgb2srgb(image):
7
7
return np .uint8 (np .round (np .clip (np .where (
8
8
image <= 0.00304 ,
9
9
12.92 * image ,
10
- 1.055 * np .power (image , 1.0 / 2.4 ) - 0.055
10
+ 1.055 * np .power (np . maximum ( image , 0 ) , 1.0 / 2.4 ) - 0.055
11
11
) * 255 , 0 , 255 )))
12
12
13
13
@@ -20,9 +20,203 @@ def tonemapping(x):
20
20
return rgb2srgb (x * (a * x + b ) / (x * (c * x + d ) + e ))
21
21
22
22
23
+ """
24
+ The fitted AgX implementation is from https://iolite-engine.com/blog_posts/minimal_agx_implementation
25
+
26
+ // MIT License
27
+ //
28
+ // Copyright (c) 2024 Missing Deadlines (Benjamin Wrensch)
29
+ //
30
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
31
+ // of this software and associated documentation files (the "Software"), to deal
32
+ // in the Software without restriction, including without limitation the rights
33
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
34
+ // copies of the Software, and to permit persons to whom the Software is
35
+ // furnished to do so, subject to the following conditions:
36
+ //
37
+ // The above copyright notice and this permission notice shall be included in
38
+ // all copies or substantial portions of the Software.
39
+ //
40
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
46
+ // SOFTWARE.
47
+
48
+ // All values used to derive this implementation are sourced from Troy’s initial AgX implementation/OCIO config file available here:
49
+ // https://github.com/sobotka/AgX
50
+
51
+ // 0: Default, 1: Golden, 2: Punchy
52
+ #define AGX_LOOK 0
53
+
54
+ // Mean error^2: 3.6705141e-06
55
+ vec3 agxDefaultContrastApprox(vec3 x) {
56
+ vec3 x2 = x * x;
57
+ vec3 x4 = x2 * x2;
58
+
59
+ return + 15.5 * x4 * x2
60
+ - 40.14 * x4 * x
61
+ + 31.96 * x4
62
+ - 6.868 * x2 * x
63
+ + 0.4298 * x2
64
+ + 0.1191 * x
65
+ - 0.00232;
66
+ }
67
+
68
+ vec3 agx(vec3 val) {
69
+ const mat3 agx_mat = mat3(
70
+ 0.842479062253094, 0.0423282422610123, 0.0423756549057051,
71
+ 0.0784335999999992, 0.878468636469772, 0.0784336,
72
+ 0.0792237451477643, 0.0791661274605434, 0.879142973793104);
73
+
74
+ const float min_ev = -12.47393f;
75
+ const float max_ev = 4.026069f;
76
+
77
+ // Input transform (inset)
78
+ val = agx_mat * val;
79
+
80
+ // Log2 space encoding
81
+ val = clamp(log2(val), min_ev, max_ev);
82
+ val = (val - min_ev) / (max_ev - min_ev);
83
+
84
+ // Apply sigmoid function approximation
85
+ val = agxDefaultContrastApprox(val);
86
+
87
+ return val;
88
+ }
89
+
90
+ vec3 agxEotf(vec3 val) {
91
+ const mat3 agx_mat_inv = mat3(
92
+ 1.19687900512017, -0.0528968517574562, -0.0529716355144438,
93
+ -0.0980208811401368, 1.15190312990417, -0.0980434501171241,
94
+ -0.0990297440797205, -0.0989611768448433, 1.15107367264116);
95
+
96
+ // Inverse input transform (outset)
97
+ val = agx_mat_inv * val;
98
+
99
+ // sRGB IEC 61966-2-1 2.2 Exponent Reference EOTF Display
100
+ // NOTE: We're linearizing the output here. Comment/adjust when
101
+ // *not* using a sRGB render target
102
+ val = pow(val, vec3(2.2));
103
+
104
+ return val;
105
+ }
106
+
107
+ vec3 agxLook(vec3 val) {
108
+ const vec3 lw = vec3(0.2126, 0.7152, 0.0722);
109
+ float luma = dot(val, lw);
110
+
111
+ // Default
112
+ vec3 offset = vec3(0.0);
113
+ vec3 slope = vec3(1.0);
114
+ vec3 power = vec3(1.0);
115
+ float sat = 1.0;
116
+
117
+ #if AGX_LOOK == 1
118
+ // Golden
119
+ slope = vec3(1.0, 0.9, 0.5);
120
+ power = vec3(0.8);
121
+ sat = 0.8;
122
+ #elif AGX_LOOK == 2
123
+ // Punchy
124
+ slope = vec3(1.0);
125
+ power = vec3(1.35, 1.35, 1.35);
126
+ sat = 1.4;
127
+ #endif
128
+
129
+ // ASC CDL
130
+ val = pow(val * slope + offset, power);
131
+ return luma + sat * (val - luma);
132
+ }
133
+
134
+ // Sample usage
135
+ void main(/*...*/) {
136
+ // ...
137
+ value = agx(value);
138
+ value = agxLook(value); // Optional
139
+ value = agxEotf(value);
140
+ // ...
141
+ }
142
+
143
+ // 7th Order Polynomial Approximation: Mean error^2: 1.85907662e-06
144
+ vec3 agxDefaultContrastApprox(vec3 x) {
145
+ vec3 x2 = x * x;
146
+ vec3 x4 = x2 * x2;
147
+ vec3 x6 = x4 * x2;
148
+
149
+ return - 17.86 * x6 * x
150
+ + 78.01 * x6
151
+ - 126.7 * x4 * x
152
+ + 92.06 * x4
153
+ - 28.72 * x2 * x
154
+ + 4.361 * x2
155
+ - 0.1718 * x
156
+ + 0.002857;
157
+ }
158
+ """
159
+
160
+
161
+ def agx_default_contrast_approx (x : np .ndarray ):
162
+ x2 = x * x
163
+ x4 = x2 * x2
164
+ x6 = x4 * x2
165
+ return (- 17.86 * x6 * x + 78.01 * x6
166
+ - 126.7 * x4 * x + 92.06 * x4
167
+ - 28.72 * x2 * x + 4.361 * x2
168
+ - 0.1718 * x + 0.002857 )
169
+
170
+
171
+ def agx (x : np .ndarray ):
172
+ agx_mat = np .array ([
173
+ [0.842479062253094 , 0.0423282422610123 , 0.0423756549057051 ],
174
+ [0.0784335999999992 , 0.878468636469772 , 0.0784336 ],
175
+ [0.0792237451477643 , 0.0791661274605434 , 0.879142973793104 ]
176
+ ])
177
+ min_ev = - 12.47393
178
+ max_ev = 4.026069
179
+ x = np .maximum (np .matmul (x , agx_mat ), np .power (2 , min_ev - 1 ))
180
+ x = np .clip (np .log2 (x ), min_ev , max_ev )
181
+ x = (x - min_ev ) / (max_ev - min_ev )
182
+ return agx_default_contrast_approx (x )
183
+
184
+
185
+ def agx_eotf (x : np .ndarray ):
186
+ agx_mat_inv = np .array ([
187
+ [1.19687900512017 , - 0.0528968517574562 , - 0.0529716355144438 ],
188
+ [- 0.0980208811401368 , 1.15190312990417 , - 0.0980434501171241 ],
189
+ [- 0.0990297440797205 , - 0.0989611768448433 , 1.15107367264116 ]
190
+ ])
191
+ return np .power (np .maximum (np .matmul (x , agx_mat_inv ), 0 ), 2.2 )
192
+
193
+
194
+ def agx_look (x : np .ndarray , look = "default" ):
195
+ lw = np .array ([0.2126 , 0.7152 , 0.0722 ])
196
+ luma = np .dot (x , lw )[..., None ]
197
+ offset = np .array ([0.0 , 0.0 , 0.0 ])
198
+ slope = np .array ([1.0 , 1.0 , 1.0 ])
199
+ power = np .array ([1.0 , 1.0 , 1.0 ])
200
+ sat = 1.0
201
+ if look == "golden" :
202
+ slope = np .array ([1.0 , 0.9 , 0.5 ])
203
+ power = np .array ([0.8 , 0.8 , 0.8 ])
204
+ sat = 0.8
205
+ elif look == "punchy" :
206
+ slope = np .array ([1.0 , 1.0 , 1.0 ])
207
+ power = np .array ([1.35 , 1.35 , 1.35 ])
208
+ sat = 1.4
209
+ x = np .power (x * slope + offset , power )
210
+ return luma + sat * (x - luma )
211
+
212
+
213
+ def agx_tonemap (x : np .ndarray , look = "default" ):
214
+ return rgb2srgb (agx_eotf (agx_look (agx (x ), look )))
215
+
216
+
23
217
def imread (filename ):
24
218
return np .maximum (
25
- np .nan_to_num (cv .imread (filename , cv .IMREAD_UNCHANGED )[:, :, :3 ], nan = 0.0 , posinf = 1e3 , neginf = 0 ), 0.0 )
219
+ np .nan_to_num (cv .imread (filename , cv .IMREAD_UNCHANGED )[:, :, :3 ], nan = 0.0 , posinf = 1e3 , neginf = 0 ), 0.0 )[..., :: - 1 ]
26
220
27
221
28
222
if __name__ == "__main__" :
@@ -31,6 +225,7 @@ def imread(filename):
31
225
32
226
filename = argv [1 ]
33
227
exp = 0 if len (argv ) == 2 else float (argv [2 ])
228
+ look = "default" if len (argv ) <= 3 else argv [3 ].lower ()
34
229
assert filename .endswith (".exr" )
35
230
image = imread (filename ) * (2 ** exp )
36
- cv .imwrite (f"{ filename [:- 4 ]} .png" , tonemapping (image ) )
231
+ cv .imwrite (f"{ filename [:- 4 ]} .png" , agx_tonemap (image , look )[..., :: - 1 ] )
0 commit comments