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>  |
38 | using namespace tSystem;  |
39 | using namespace IMF;  |
40 | using namespace IMATH;  |
41 |   |
42 |   |
43 | namespace 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 |   |
61 | float EXR::KneeFun(double x, double f)  |
62 | {  |
63 | return float (IMATH::Math<double>::log(x * f + 1.0) / f);  |
64 | }  |
65 |   |
66 |   |
67 | float 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 |   |
90 | void 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 |   |
114 | uint8 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 |   |
128 | EXR::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 |   |
139 | float 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 |   |
149 | bool 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 ;  |
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 |   |
275 | bool 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 |   |
298 | bool 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 |   |
318 | tPixel* tImage::tImageEXR::StealPixels()  |
319 | {  |
320 | tPixel* pixels = Pixels;  |
321 | Pixels = nullptr;  |
322 | Width = 0;  |
323 | Height = 0;  |
324 | return pixels;  |
325 | }  |
326 | |