1// tImageEXR.cpp 
2// 
3// This knows how to load and save OpenEXR images (.exr). It knows the details of the exr high dynamic range 
4// file format and loads the data into a tPixel array. These tPixels may be 'stolen' by the tPicture's constructor if 
5// an EXR file is specified. After the array is stolen the tImageEXR is invalid. This is purely for performance. 
6// 
7// Copyright (c) 2020 Tristan Grimmer. 
8// Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby 
9// granted, provided that the above copyright notice and this permission notice appear in all copies. 
10// 
11// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL 
12// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 
13// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 
14// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 
15// PERFORMANCE OF THIS SOFTWARE. 
16// 
17// The EXR namespace and functions are a modification of ImageView.cpp from OpenEXR under the following licence: 
18// 
19// Copyright (c) 2012, Industrial Light & Magic, a division of Lucas Digital Ltd. LLC. All rights reserved. 
20// 
21// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 
22// * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 
23// * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 
24// * Neither the name of Industrial Light & Magic nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.  
25// 
26// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
27// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
28// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
29 
30#include <Foundation/tStandard.h> 
31#include <Foundation/tString.h> 
32#include <System/tMachine.h> 
33#include <System/tFile.h> 
34#include "Image/tImageEXR.h" 
35#include <OpenEXR/loadImage.h> 
36#include <OpenEXR/ImfMultiPartInputFile.h> 
37#include <OpenEXR/halfFunction.h> 
38using namespace tSystem
39using namespace IMF
40using namespace IMATH
41 
42 
43namespace EXR 
44
45 inline float KneeFun(double x, double f); 
46 float FindKneeFun(float x, float y); 
47 uint8 Dither(float v, int x, int y); 
48 
49 // This fog colour is used when de-fogging. 
50 void ComputeFogColour(float& fogR, float& fogG, float& fogB, const IMF::Array<IMF::Rgba>& pixels); 
51 
52 struct Gamma 
53
54 Gamma(float gamma, float exposure, float defog, float kneeLow, float kneeHigh); 
55 float operator()(half h); 
56 float invg, m, d, kl, f, s
57 }; 
58
59 
60 
61float EXR::KneeFun(double x, double f
62
63 return float (IMATH::Math<double>::log(x * f + 1.0) / f); 
64
65 
66  
67float EXR::FindKneeFun(float x, float y
68
69 float f0 = 0; float f1 = 1
70 while (KneeFun(x, f1) > y
71
72 f0 = f1
73 f1 = f1 * 2
74
75 
76 for (int i = 0; i < 30; ++i
77
78 float f2 = (f0 + f1) / 2.0f
79 float y2 = KneeFun(x, f2); 
80 if (y2 < y
81 f1 = f2
82 else 
83 f0 = f2
84
85 
86 return (f0 + f1) / 2.0f
87
88 
89 
90void EXR::ComputeFogColour(float& fogR, float& fogG, float& fogB, const IMF::Array<IMF::Rgba>& pixels
91
92 double fogRd = 0.0
93 double fogGd = 0.0
94 double fogBd = 0.0
95 double WeightSum = 0.0
96 int numPixels = pixels.size(); 
97 
98 for (int j = 0; j < numPixels; ++j
99
100 const IMF::Rgba& rawPixel = pixels[j]; 
101 float weight = tMath::tSaturate(float(rawPixel.a)); // Makes sure transparent pixels don't contribute to the colour. 
102 
103 fogRd += rawPixel.r.isFinite() ? double(rawPixel.r) : 0.0
104 fogGd += rawPixel.g.isFinite() ? double(rawPixel.g) : 0.0
105 fogBd += rawPixel.b.isFinite() ? double(rawPixel.b) : 0.0
106 WeightSum += weight
107
108 
109 fogRd /= WeightSum; fogGd /= WeightSum; fogBd /= WeightSum
110 fogR = float(fogRd); fogG = float(fogGd); fogB = float(fogBd); 
111
112 
113 
114uint8 EXR::Dither(float v, int x, int y
115
116 static const float d[4][4] = 
117
118 { 00.0f/16.0f, 08.0f/16.0f, 02.0f/16.0f, 10.0f/16.0f }, 
119 { 12.0f/16.0f, 04.0f/16.0f, 14.0f/16.0f, 06.0f/16.0f }, 
120 { 03.0f/16.0f, 11.0f/16.0f, 01.0f/16.0f, 09.0f/16.0f }, 
121 { 15.0f/16.0f, 07.0f/16.0f, 13.0f/16.0f, 05.0f/16.0f
122 }; 
123 
124 return uint8(v + d[y&3][x&3]); 
125
126 
127 
128EXR::Gamma::Gamma(float gamma, float exposure, float defog, float kneeLow, float kneeHigh) : 
129 invg(1.0f/gamma), 
130 m(tMath::tPow(2.0f, exposure + 2.47393f)), 
131 d(defog), 
132 kl(tMath::tPow(2.0f, kneeLow)), 
133 f(FindKneeFun(tMath::tPow(2.0f, kneeHigh) - kl, tMath::tPow(2.0f, 3.5f) - kl)), 
134 s(255.0f * tMath::tPow(2.0f, -3.5f * invg)) 
135
136
137 
138 
139float EXR::Gamma::operator()(half h
140
141 float x = tMath::tMax(0.0f, (h - d)); // Defog 
142 x *= m; // Exposure 
143 if (x > kl) x = kl + KneeFun(x - kl, f); // Knee 
144 x = tMath::tPow(x, invg); // Gamma 
145 return tMath::tClamp(x*s, 0.0f, 255.0f); // Clamp 
146
147 
148 
149bool tImage::tImageEXR::Load 
150
151 const tString& exrFile, int partNum, float gamma, float exposure
152 float defog, float kneeLow, float kneeHigh 
153
154
155 Clear(); 
156 if (tSystem::tGetFileType(exrFile) != tSystem::tFileType::EXR
157 return false
158 
159 if (!tFileExists(exrFile)) 
160 return false
161 
162 // Leave two cores free unless we are on a three core or lower machine, in which case we always use a min of 2 threads. 
163 int numThreads = tMath::tClampMin((tSystem::tGetNumCores()) - 2, 2); 
164 setGlobalThreadCount(numThreads); 
165 
166 int numParts = 0
167 int outZsize = 0
168 Header outHeader
169 IMF::Array<IMF::Rgba> pixels
170 IMF::Array<float*> zbuffer
171 IMF::Array<uint> sampleCount
172 
173 try 
174
175 MultiPartInputFile mpfile(exrFile.Chars()); 
176 numParts = mpfile.parts(); 
177 if ((numParts <= 0) || (partNum >= numParts)) 
178 return false
179 
180 // const char* channels = "AZRGB"; 
181 // const char* layers = "0"; 
182 bool preview = false
183 int lx = -1; int ly = -1; // For tiled image shows level (lx,ly) 
184 bool compositeDeep = true
185 
186 EXR::loadImage 
187
188 exrFile.Chars(), 
189 nullptr, // Channels. Null means all. 
190 nullptr, // Layers. O means first one. 
191 preview, lx, ly
192 partNum
193 outZsize, outHeader
194 pixels, zbuffer, sampleCount
195 compositeDeep 
196 ); 
197
198 catch (IEX_NAMESPACE::BaseExc& err
199
200 tPrintf("Error: Can't read exr file. %s\n", err.what()); 
201 return false
202
203 
204 const Box2i& displayWindow = outHeader.displayWindow(); 
205 const Box2i& dataWindow = outHeader.dataWindow(); 
206 float pixelAspectRatio = outHeader.pixelAspectRatio(); 
207 int w = displayWindow.max.x - displayWindow.min.x + 1
208 int h = displayWindow.max.y - displayWindow.min.y + 1
209 int dw = dataWindow.max.x - dataWindow.min.x + 1
210 int dh = dataWindow.max.y - dataWindow.min.y + 1
211 int dx = dataWindow.min.x - displayWindow.min.x
212 int dy = dataWindow.min.y - displayWindow.min.y
213 
214 // Set width, height, and allocate and set Pixels. 
215 Width = dw
216 Height = dh
217 Pixels = new tPixel[Width*Height]; 
218 
219 // Map floating-point pixel values 0.0 and 1.0 to the display's white and black respectively. 
220 // if bool zerooneexposure true. 
221 // exposure = 1.02607f; 
222 // kneeHigh = 3.5f; 
223  
224 float fogR = 0.0f
225 float fogG = 0.0f
226 float fogB = 0.0f
227 
228 // Save some time if we can. 
229 if (defog > 0.0f
230 EXR::ComputeFogColour(fogR, fogG, fogB, pixels); 
231 
232 halfFunction<float> redGamma(EXR::Gamma(gamma, exposure, defog * fogR, kneeLow, kneeHigh), -HALF_MAX, HALF_MAX, 0.0f, 255.0f, 0.0f, 0.0f); 
233 halfFunction<float> grnGamma(EXR::Gamma(gamma, exposure, defog * fogG, kneeLow, kneeHigh), -HALF_MAX, HALF_MAX, 0.0f, 255.0f, 0.0f, 0.0f); 
234 halfFunction<float> bluGamma(EXR::Gamma(gamma, exposure, defog * fogB, kneeLow, kneeHigh), -HALF_MAX, HALF_MAX, 0.0f, 255.0f, 0.0f, 0.0f); 
235 
236 // Conversion from raw pixel data to data for the OpenGL frame buffer: 
237 // 1) Compensate for fogging by subtracting defog from the raw pixel values. 
238 // 2) Multiply the defogged pixel values by 2^(exposure + 2.47393). 
239 // 3) Values that are now 1.0 are called "middle gray". If defog and exposure are both set to 0.0, then middle gray 
240 // corresponds to a raw pixel value of 0.18. In step 6, middle gray values will be mapped to an intensity 3.5 
241 // f-stops below the display's maximum intensity. 
242 // 4) Apply a knee function. The knee function has two parameters, kneeLow and kneeHigh. Pixel values below 
243 // 2^kneeLow are not changed by the knee function. Pixel values above kneeLow are lowered according to a 
244 // logarithmic curve, such that the value 2^kneeHigh is mapped to 2^3.5. (In step 6 this value will be mapped to 
245 // the the display's maximum intensity.) 
246 // 5) Gamma-correct the pixel values, according to the screen's gamma. (We assume that the gamma curve is a simple 
247 // power function.) 
248 // 6) Scale the values such that middle gray pixels are mapped to a frame buffer value that is 3.5 f-stops below the 
249 // display's maximum intensity. (84.65 if the screen's gamma is 2.2) 
250 // 7) Clamp the values to [0, 255]. 
251 // 
252 // Texview has 0,0 at bottom-left. Rows start from bottom. 
253 int p = 0
254 for (int yi = Height-1; yi >= 0; yi--) 
255
256 for (int xi = 0; xi < Width; xi++) 
257
258 int idx = yi*Width + xi
259 const IMF::Rgba& rawPixel = pixels[idx]; 
260 Pixels[p++] = tPixel 
261 ( 
262 EXR::Dither( redGamma(rawPixel.r), xi, yi ), 
263 EXR::Dither( grnGamma(rawPixel.g), xi, yi ), 
264 EXR::Dither( bluGamma(rawPixel.b), xi, yi ), 
265 uint8( tMath::tClamp( tMath::tFloatToInt(float(rawPixel.a)*255.0f), 0, 0xFF ) ) 
266 ); 
267
268
269 
270 SrcPixelFormat = tPixelFormat::HDR_EXR
271 return true
272
273 
274 
275bool tImage::tImageEXR::Set(tPixel* pixels, int width, int height, bool steal
276
277 Clear(); 
278 if (!pixels || (width <= 0) || (height <= 0)) 
279 return false
280 
281 Width = width
282 Height = height
283 if (steal
284
285 Pixels = pixels
286
287 else 
288
289 Pixels = new tPixel[Width*Height]; 
290 tStd::tMemcpy(Pixels, pixels, Width*Height*sizeof(tPixel)); 
291
292 
293 SrcPixelFormat = tPixelFormat::R8G8B8A8
294 return true
295
296 
297 
298bool tImage::tImageEXR::Save(const tString& exrFile) const 
299
300 tAssertMsg(false, "EXR Save not implemented."); 
301 if (!IsValid()) 
302 return false
303 
304 if (tSystem::tGetFileType(exrFile) != tSystem::tFileType::EXR
305 return false
306 
307 tFileHandle file = tOpenFile(exrFile.ConstText(), "wb"); 
308 if (!file
309 return false
310 
311 // Write the data.... 
312 
313 tCloseFile(file); 
314 return true
315
316 
317 
318tPixel* tImage::tImageEXR::StealPixels() 
319
320 tPixel* pixels = Pixels
321 Pixels = nullptr
322 Width = 0
323 Height = 0
324 return pixels
325
326