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