| 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-2022, 2024 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 "Image/tPicture.h"  |
| 36 | #include <OpenEXR/loadImage.h>  |
| 37 | #include <OpenEXR/ImfMultiPartInputFile.h>  |
| 38 | #include <OpenEXR/halfFunction.h>  |
| 39 | using namespace tSystem;  |
| 40 | using namespace IMF;  |
| 41 | using namespace IMATH;  |
| 42 | using namespace tImage;  |
| 43 |   |
| 44 |   |
| 45 | namespace EXR  |
| 46 | {  |
| 47 | inline float KneeFun(double x, double f);  |
| 48 | float FindKneeFun(float x, float y);  |
| 49 | uint8 Dither(float v, int x, int y);  |
| 50 |   |
| 51 | // This fog colour is used when de-fogging.  |
| 52 | void ComputeFogColour(float& fogR, float& fogG, float& fogB, const IMF::Array<IMF::Rgba>& pixels);  |
| 53 |   |
| 54 | struct Gamma  |
| 55 | {  |
| 56 | Gamma(float gamma, float exposure, float defog, float kneeLow, float kneeHigh);  |
| 57 | float operator()(half h);  |
| 58 | float invg, m, d, kl, f, s;  |
| 59 | };  |
| 60 | }  |
| 61 |   |
| 62 |   |
| 63 | float EXR::KneeFun(double x, double f)  |
| 64 | {  |
| 65 | return float (IMATH::Math<double>::log(x: x * f + 1.0) / f);  |
| 66 | }  |
| 67 |   |
| 68 |   |
| 69 | float EXR::FindKneeFun(float x, float y)  |
| 70 | {  |
| 71 | float f0 = 0; float f1 = 1;  |
| 72 | while (KneeFun(x, f: f1) > y)  |
| 73 | {  |
| 74 | f0 = f1;  |
| 75 | f1 = f1 * 2;  |
| 76 | }  |
| 77 |   |
| 78 | for (int i = 0; i < 30; ++i)  |
| 79 | {  |
| 80 | float f2 = (f0 + f1) / 2.0f;  |
| 81 | float y2 = KneeFun(x, f: f2);  |
| 82 | if (y2 < y)  |
| 83 | f1 = f2;  |
| 84 | else  |
| 85 | f0 = f2;  |
| 86 | }  |
| 87 |   |
| 88 | return (f0 + f1) / 2.0f;  |
| 89 | }  |
| 90 |   |
| 91 |   |
| 92 | void EXR::ComputeFogColour(float& fogR, float& fogG, float& fogB, const IMF::Array<IMF::Rgba>& pixels)  |
| 93 | {  |
| 94 | double fogRd = 0.0;  |
| 95 | double fogGd = 0.0;  |
| 96 | double fogBd = 0.0;  |
| 97 | double WeightSum = 0.0;  |
| 98 | int numPixels = pixels.size();  |
| 99 |   |
| 100 | for (int j = 0; j < numPixels; ++j)  |
| 101 | {  |
| 102 | const IMF::Rgba& rawPixel = pixels[j];  |
| 103 | float weight = tMath::tSaturate(val: float(rawPixel.a)); // Makes sure transparent pixels don't contribute to the colour.  |
| 104 |   |
| 105 | fogRd += rawPixel.r.isFinite() ? double(rawPixel.r) : 0.0;  |
| 106 | fogGd += rawPixel.g.isFinite() ? double(rawPixel.g) : 0.0;  |
| 107 | fogBd += rawPixel.b.isFinite() ? double(rawPixel.b) : 0.0;  |
| 108 | WeightSum += weight;  |
| 109 | }  |
| 110 |   |
| 111 | fogRd /= WeightSum; fogGd /= WeightSum; fogBd /= WeightSum;  |
| 112 | fogR = float(fogRd); fogG = float(fogGd); fogB = float(fogBd);  |
| 113 | }  |
| 114 |   |
| 115 |   |
| 116 | uint8 EXR::Dither(float v, int x, int y)  |
| 117 | {  |
| 118 | static const float d[4][4] =  |
| 119 | {  |
| 120 | { 00.0f/16.0f, 08.0f/16.0f, 02.0f/16.0f, 10.0f/16.0f },  |
| 121 | { 12.0f/16.0f, 04.0f/16.0f, 14.0f/16.0f, 06.0f/16.0f },  |
| 122 | { 03.0f/16.0f, 11.0f/16.0f, 01.0f/16.0f, 09.0f/16.0f },  |
| 123 | { 15.0f/16.0f, 07.0f/16.0f, 13.0f/16.0f, 05.0f/16.0f }  |
| 124 | };  |
| 125 |   |
| 126 | return uint8(v + d[y&3][x&3]);  |
| 127 | }  |
| 128 |   |
| 129 |   |
| 130 | EXR::Gamma::Gamma(float gamma, float exposure, float defog, float kneeLow, float kneeHigh) :  |
| 131 | invg(1.0f/gamma),  |
| 132 | m(tMath::tPow(a: 2.0f, b: exposure + 2.47393f)),  |
| 133 | d(defog),  |
| 134 | kl(tMath::tPow(a: 2.0f, b: kneeLow)),  |
| 135 | f(FindKneeFun(x: tMath::tPow(a: 2.0f, b: kneeHigh) - kl, y: tMath::tPow(a: 2.0f, b: 3.5f) - kl)),  |
| 136 | s(255.0f * tMath::tPow(a: 2.0f, b: -3.5f * invg))  |
| 137 | {  |
| 138 | }  |
| 139 |   |
| 140 |   |
| 141 | float EXR::Gamma::operator()(half h)  |
| 142 | {  |
| 143 | float x = tMath::tMax(a: 0.0f, b: (h - d)); // Defog  |
| 144 | x *= m; // Exposure  |
| 145 | if (x > kl) x = kl + KneeFun(x: x - kl, f); // Knee  |
| 146 | x = tMath::tPow(a: x, b: invg); // Gamma  |
| 147 | return tMath::tClamp(val: x*s, min: 0.0f, max: 255.0f); // Clamp  |
| 148 | }  |
| 149 |   |
| 150 |   |
| 151 | bool tImageEXR::Load(const tString& exrFile, const LoadParams& loadParams)  |
| 152 | {  |
| 153 | Clear();  |
| 154 | if (tSystem::tGetFileType(file: exrFile) != tSystem::tFileType::EXR)  |
| 155 | return false;  |
| 156 |   |
| 157 | if (!tFileExists(file: exrFile))  |
| 158 | return false;  |
| 159 |   |
| 160 | float defog = loadParams.Defog;  |
| 161 | float exposure = loadParams.Exposure;  |
| 162 | float kneeLow = loadParams.KneeLow;  |
| 163 | float kneeHigh = loadParams.KneeHigh;  |
| 164 | float gamma = loadParams.Gamma;  |
| 165 |   |
| 166 | // 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.  |
| 167 | int numThreads = tMath::tClampMin(val: (tSystem::tGetNumCores()) - 2, min: 2);  |
| 168 | setGlobalThreadCount(numThreads);  |
| 169 |   |
| 170 | int outZsize = 0;  |
| 171 | Header ;  |
| 172 | IMF::Array<IMF::Rgba> pixels;  |
| 173 | IMF::Array<float*> zbuffer;  |
| 174 | IMF::Array<uint> sampleCount;  |
| 175 |   |
| 176 | MultiPartInputFile mpfile(exrFile.Chr());  |
| 177 | int numParts = mpfile.parts();  |
| 178 | if (numParts <= 0)  |
| 179 | return false;  |
| 180 |   |
| 181 | for (int partNum = 0; partNum < numParts; partNum++)  |
| 182 | {  |
| 183 | try  |
| 184 | {  |
| 185 | bool preview = false;  |
| 186 | int lx = -1; int ly = -1; // For tiled image shows level (lx,ly)  |
| 187 | bool compositeDeep = true;  |
| 188 |   |
| 189 | EXR::loadImage  |
| 190 | (  |
| 191 | fileName: exrFile.Chr(),  |
| 192 | channel: nullptr, // Channels. Null means all.  |
| 193 | layer: nullptr, // Layers. O means first one.  |
| 194 | preview, lx, ly,  |
| 195 | partnum: partNum,  |
| 196 | zsize&: outZsize, header&: outHeader,  |
| 197 | pixels, zbuffer, sampleCount,  |
| 198 | deepComp: compositeDeep  |
| 199 | );  |
| 200 | }  |
| 201 | catch (IEX_NAMESPACE::BaseExc& err)  |
| 202 | {  |
| 203 | tPrintf(f: "Error: Can't read exr file. %s\n" , err.what());  |
| 204 | return false;  |
| 205 | }  |
| 206 |   |
| 207 | const Box2i& displayWindow = outHeader.displayWindow();  |
| 208 | const Box2i& dataWindow = outHeader.dataWindow();  |
| 209 | float pixelAspectRatio = outHeader.pixelAspectRatio();  |
| 210 | int w = displayWindow.max.x - displayWindow.min.x + 1;  |
| 211 | int h = displayWindow.max.y - displayWindow.min.y + 1;  |
| 212 | int dx = dataWindow.min.x - displayWindow.min.x;  |
| 213 | int dy = dataWindow.min.y - displayWindow.min.y;  |
| 214 | int width = dataWindow.max.x - dataWindow.min.x + 1;  |
| 215 | int height = dataWindow.max.y - dataWindow.min.y + 1;  |
| 216 |   |
| 217 | // Set width, height, and allocate and set Pixels.  |
| 218 | tFrame* newFrame = new tFrame;  |
| 219 | newFrame->PixelFormatSrc = tPixelFormat::OPENEXR;  |
| 220 | newFrame->Width = width;  |
| 221 | newFrame->Height = height;  |
| 222 | newFrame->Pixels = new tPixel4b[width*height];  |
| 223 |   |
| 224 | // Map floating-point pixel values 0.0 and 1.0 to the display's white and black respectively.  |
| 225 | // if bool zerooneexposure true.  |
| 226 | // exposure = 1.02607f;  |
| 227 | // kneeHigh = 3.5f;  |
| 228 |   |
| 229 | float fogR = 0.0f;  |
| 230 | float fogG = 0.0f;  |
| 231 | float fogB = 0.0f;  |
| 232 |   |
| 233 | // Save some time if we can.  |
| 234 | if (defog > 0.0f)  |
| 235 | EXR::ComputeFogColour(fogR, fogG, fogB, pixels);  |
| 236 |   |
| 237 | halfFunction<float> redGamma(EXR::Gamma(gamma, exposure, defog * fogR, kneeLow, kneeHigh), -HALF_MAX, HALF_MAX, 0.0f, 255.0f, 0.0f, 0.0f);  |
| 238 | halfFunction<float> grnGamma(EXR::Gamma(gamma, exposure, defog * fogG, kneeLow, kneeHigh), -HALF_MAX, HALF_MAX, 0.0f, 255.0f, 0.0f, 0.0f);  |
| 239 | halfFunction<float> bluGamma(EXR::Gamma(gamma, exposure, defog * fogB, kneeLow, kneeHigh), -HALF_MAX, HALF_MAX, 0.0f, 255.0f, 0.0f, 0.0f);  |
| 240 |   |
| 241 | // Conversion from raw pixel data to data for the OpenGL frame buffer:  |
| 242 | // 1) Compensate for fogging by subtracting defog from the raw pixel values.  |
| 243 | // 2) Multiply the defogged pixel values by 2^(exposure + 2.47393).  |
| 244 | // 3) Values that are now 1.0 are called "middle gray". If defog and exposure are both set to 0.0, then middle gray  |
| 245 | // corresponds to a raw pixel value of 0.18. In step 6, middle gray values will be mapped to an intensity 3.5  |
| 246 | // f-stops below the display's maximum intensity.  |
| 247 | // 4) Apply a knee function. The knee function has two parameters, kneeLow and kneeHigh. Pixel values below  |
| 248 | // 2^kneeLow are not changed by the knee function. Pixel values above kneeLow are lowered according to a  |
| 249 | // logarithmic curve, such that the value 2^kneeHigh is mapped to 2^3.5. (In step 6 this value will be mapped to  |
| 250 | // the the display's maximum intensity.)  |
| 251 | // 5) Gamma-correct the pixel values, according to the screen's gamma. (We assume that the gamma curve is a simple  |
| 252 | // power function.)  |
| 253 | // 6) Scale the values such that middle gray pixels are mapped to a frame buffer value that is 3.5 f-stops below the  |
| 254 | // display's maximum intensity. (84.65 if the screen's gamma is 2.2)  |
| 255 | // 7) Clamp the values to [0, 255].  |
| 256 | //  |
| 257 | // Texview has 0,0 at bottom-left. Rows start from bottom.  |
| 258 | int p = 0;  |
| 259 | for (int yi = height-1; yi >= 0; yi--)  |
| 260 | {  |
| 261 | for (int xi = 0; xi < width; xi++)  |
| 262 | {  |
| 263 | int idx = yi*width + xi;  |
| 264 | const IMF::Rgba& rawPixel = pixels[idx];  |
| 265 | newFrame->Pixels[p++] = tPixel4b  |
| 266 | (  |
| 267 | EXR::Dither( v: redGamma(rawPixel.r), x: xi, y: yi ),  |
| 268 | EXR::Dither( v: grnGamma(rawPixel.g), x: xi, y: yi ),  |
| 269 | EXR::Dither( v: bluGamma(rawPixel.b), x: xi, y: yi ),  |
| 270 | uint8( tMath::tClamp( val: tMath::tFloatToInt(val: float(rawPixel.a)*255.0f), min: 0, max: 0xFF ) )  |
| 271 | );  |
| 272 | }  |
| 273 | }  |
| 274 |   |
| 275 | Frames.Append(item: newFrame);  |
| 276 | }  |
| 277 |   |
| 278 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 279 | PixelFormatSrc = tPixelFormat::OPENEXR;  |
| 280 | ColourProfile = tColourProfile::sRGB;  |
| 281 | ColourProfileSrc = tColourProfile::HDRa;  |
| 282 | return true;  |
| 283 | }  |
| 284 |   |
| 285 |   |
| 286 | bool tImage::tImageEXR::Set(tList<tFrame>& srcFrames, bool stealFrames)  |
| 287 | {  |
| 288 | Clear();  |
| 289 | if (srcFrames.GetNumItems() <= 0)  |
| 290 | return false;  |
| 291 |   |
| 292 | PixelFormatSrc = srcFrames.Head()->PixelFormatSrc;  |
| 293 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 294 | ColourProfileSrc = tColourProfile::sRGB; // We assume srcFrames must be sRGB.  |
| 295 | ColourProfile = tColourProfile::sRGB;  |
| 296 |   |
| 297 | if (stealFrames)  |
| 298 | {  |
| 299 | while (tFrame* frame = srcFrames.Remove())  |
| 300 | Frames.Append(item: frame);  |
| 301 | }  |
| 302 | else  |
| 303 | {  |
| 304 | for (tFrame* frame = srcFrames.Head(); frame; frame = frame->Next())  |
| 305 | Frames.Append(item: new tFrame(*frame));  |
| 306 | }  |
| 307 |   |
| 308 | return true;  |
| 309 | }  |
| 310 |   |
| 311 |   |
| 312 | bool tImageEXR::Set(tPixel4b* pixels, int width, int height, bool steal)  |
| 313 | {  |
| 314 | Clear();  |
| 315 | if (!pixels || (width <= 0) || (height <= 0))  |
| 316 | return false;  |
| 317 |   |
| 318 | tFrame* frame = new tFrame();  |
| 319 | if (steal)  |
| 320 | frame->StealFrom(src: pixels, width, height);  |
| 321 | else  |
| 322 | frame->Set(srcPixels: pixels, width, height);  |
| 323 | Frames.Append(item: frame);  |
| 324 |   |
| 325 | PixelFormatSrc = tPixelFormat::R8G8B8A8;  |
| 326 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 327 | ColourProfileSrc = tColourProfile::sRGB; // We assume pixels must be sRGB.  |
| 328 | ColourProfile = tColourProfile::sRGB;  |
| 329 |   |
| 330 | return true;  |
| 331 | }  |
| 332 |   |
| 333 |   |
| 334 | bool tImageEXR::Set(tFrame* frame, bool steal)  |
| 335 | {  |
| 336 | Clear();  |
| 337 | if (!frame || !frame->IsValid())  |
| 338 | return false;  |
| 339 |   |
| 340 | PixelFormatSrc = frame->PixelFormatSrc;  |
| 341 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 342 | ColourProfileSrc = tColourProfile::sRGB; // We assume frame must be sRGB.  |
| 343 | ColourProfile = tColourProfile::sRGB;  |
| 344 |   |
| 345 | if (steal)  |
| 346 | Frames.Append(item: frame);  |
| 347 | else  |
| 348 | Frames.Append(item: new tFrame(*frame));  |
| 349 |   |
| 350 | return true;  |
| 351 | }  |
| 352 |   |
| 353 |   |
| 354 | bool tImageEXR::Set(tPicture& picture, bool steal)  |
| 355 | {  |
| 356 | Clear();  |
| 357 | if (!picture.IsValid())  |
| 358 | return false;  |
| 359 |   |
| 360 | PixelFormatSrc = picture.PixelFormatSrc;  |
| 361 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 362 | // We don't know colour profile of tPicture.  |
| 363 |   |
| 364 | // This is worth some explanation. If steal is true the picture becomes invalid and the  |
| 365 | // 'set' call will steal the stolen pixels. If steal is false GetPixels is called and the  |
| 366 | // 'set' call will memcpy them out... which makes sure the picture is still valid after and  |
| 367 | // no-one is sharing the pixel buffer. We don't check the success of 'set' because it must  |
| 368 | // succeed if picture was valid.  |
| 369 | tPixel4b* pixels = steal ? picture.StealPixels() : picture.GetPixels();  |
| 370 | bool success = Set(pixels, width: picture.GetWidth(), height: picture.GetHeight(), steal);  |
| 371 | tAssert(success);  |
| 372 | return true;  |
| 373 | }  |
| 374 |   |
| 375 |   |
| 376 | tFrame* tImageEXR::GetFrame(bool steal)  |
| 377 | {  |
| 378 | if (!IsValid())  |
| 379 | return nullptr;  |
| 380 |   |
| 381 | return steal ? Frames.Remove() : new tFrame( *Frames.First() );  |
| 382 | }  |
| 383 | |