It's really annoying to see the lack of simple (i.e. one function) writers of HDR image formats. I wrote my own Radiance HDR (RGB-exponent) and floating point TIFF writers. (forgive the sin of using std::string as a vector of bytes, was done as certain file write function in the framework used strings).
They are rather ignorant as you will see from the source, rather slow too... But making them clever is left as an exercise to the reader, let's say this is really nothing more than documentation on the formats themselves, or better, on the minimal subset of the formats you need to know in order to write out HDR data.
Also you might want, as the TIFF routine writes raw float data, to convert it into and "inline" operation (i.e. BeginTiff, PushFloat, EndTiff kind of interface), which is simple enough especially if you move the IFD before the image data... Also, it would be much easier if it wrote the endian in the header based on your current platform file output order, making it easier than byte-by-byte writing as it is now.
Update, Aras PranckeviÄius tweeted his EXR writer, so I was wrong, where was at least one simple HDR writer out there already. Also, EXR is more widespread than floating point TIF, and even easier... Partially related, Jon Olick has a neat single file JPEG and MPEG writers, handy (and I'm sure everybody knows about stb_image and image write, but just in case...)!
Also you might want, as the TIFF routine writes raw float data, to convert it into and "inline" operation (i.e. BeginTiff, PushFloat, EndTiff kind of interface), which is simple enough especially if you move the IFD before the image data... Also, it would be much easier if it wrote the endian in the header based on your current platform file output order, making it easier than byte-by-byte writing as it is now.
Update, Aras PranckeviÄius tweeted his EXR writer, so I was wrong, where was at least one simple HDR writer out there already. Also, EXR is more widespread than floating point TIF, and even easier... Partially related, Jon Olick has a neat single file JPEG and MPEG writers, handy (and I'm sure everybody knows about stb_image and image write, but just in case...)!
// http://paulbourke.net/dataformats/tiff/ and http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf // Not all programs support floating-point TIFFs, this was tested reading it back using Picturenaut and HDRShop static std::string EncodeFloatTIFF(unsigned int w, unsigned int h, float* RGBdata, unsigned int floatsPerPixel = 4) { assert(floatsPerPixel>=3); // we write only three floats (RGB) but support larger strides std::string outData; unsigned int image_size_bytes = w*h*3 * sizeof(float); outData.reserve(image_size_bytes + 500); // 500 is some slack for headers etc, I should compute it exactly... :) // Header outData.push_back(0x4d); outData.push_back(0x4d); // First two chars specify MM for big endian TODO - convert to little to make it easier on x86 outData.push_back(0); outData.push_back(42); // Tiff version ID unsigned int IFD_offset = 8 + image_size_bytes; // IFD table usually follows image outData.push_back((IFD_offset & 0xff000000) >> 24); outData.push_back((IFD_offset & 0xff0000) >> 16); outData.push_back((IFD_offset & 0xff00) >> 8); outData.push_back(IFD_offset & 0xff); // Image data for (unsigned int y=0; y<h; y++) { for (unsigned int x=0; x<w; x++) { unsigned int f = 0; for(; f< 3; f++,RGBdata++) { uint32_t floatAsInt = *reinterpret_cast<uint32_t*>(RGBdata); outData.push_back((floatAsInt & 0xff000000) >> 24); outData.push_back((floatAsInt & 0xff0000) >> 16); outData.push_back((floatAsInt & 0xff00) >> 8); outData.push_back(floatAsInt & 0xff); } for(; f<floatsPerPixel; f++) RGBdata++; } } // IFD Tags unsigned int NUM_IFD = 12; assert(outData.size() == IFD_offset); outData.push_back(0); outData.push_back(NUM_IFD); // Number of tags outData.push_back(1); outData.push_back(0); // -- width tag outData.push_back(0); outData.push_back(3); // short format outData.push_back(0); outData.push_back(0); outData.push_back(0); outData.push_back(1); // single value outData.push_back((w & 0xff00) >> 8); outData.push_back(w & 0xff); // value outData.push_back(0); outData.push_back(0); // padding (as we specified short value) outData.push_back(1); outData.push_back(1); // -- height tag outData.push_back(0); outData.push_back(3); // short format outData.push_back(0); outData.push_back(0); outData.push_back(0); outData.push_back(1); // single value outData.push_back((h & 0xff00) >> 8); outData.push_back(h & 0xff); // value outData.push_back(0); outData.push_back(0); // padding (as we specified short value) outData.push_back(1); outData.push_back(3); // -- compression outData.push_back(0); outData.push_back(3); // short format outData.push_back(0); outData.push_back(0); outData.push_back(0); outData.push_back(1); // single value outData.push_back(0); outData.push_back(1); // none outData.push_back(0); outData.push_back(0); // padding (as we specified short value) outData.push_back(1); outData.push_back(6); // -- photometric interpretation outData.push_back(0); outData.push_back(3); // short format outData.push_back(0); outData.push_back(0); outData.push_back(0); outData.push_back(1); // single value outData.push_back(0); outData.push_back(2); // RGB outData.push_back(0); outData.push_back(0); // padding (as we specified short value) outData.push_back(1); outData.push_back(0x12); // -- orientation outData.push_back(0); outData.push_back(3); // short format outData.push_back(0); outData.push_back(0); outData.push_back(0); outData.push_back(1); // single value outData.push_back(0); outData.push_back(1); // outData.push_back(0); outData.push_back(0); // padding (as we specified short value) outData.push_back(1); outData.push_back(0x15); // -- samples per pixel outData.push_back(0); outData.push_back(3); // short format outData.push_back(0); outData.push_back(0); outData.push_back(0); outData.push_back(1); // single value outData.push_back(0); outData.push_back(3); // three samples (RGB) outData.push_back(0); outData.push_back(0); // padding (as we specified short value) outData.push_back(1); outData.push_back(0x16); // -- rows per strip outData.push_back(0); outData.push_back(3); // short format outData.push_back(0); outData.push_back(0); outData.push_back(0); outData.push_back(1); // single value outData.push_back((h & 0xff00) >> 8); outData.push_back(h & 0xff); // value outData.push_back(0); outData.push_back(0); // padding (as we specified short value) outData.push_back(1); outData.push_back(0x17); // -- strip byte count (total size) outData.push_back(0); outData.push_back(4); // long format outData.push_back(0); outData.push_back(0); outData.push_back(0); outData.push_back(1); // single value outData.push_back((image_size_bytes & 0xff000000) >> 24); outData.push_back((image_size_bytes & 0xff0000) >> 16); outData.push_back((image_size_bytes & 0xff00) >> 8); outData.push_back(image_size_bytes & 0xff); outData.push_back(1); outData.push_back(0x1c); // -- planar configuration outData.push_back(0); outData.push_back(3); // short format outData.push_back(0); outData.push_back(0); outData.push_back(0); outData.push_back(1); // single value outData.push_back(0); outData.push_back(1); // single image plane outData.push_back(0); outData.push_back(0); // padding (as we specified short value) outData.push_back(1); outData.push_back(0x11); // -- strip offset outData.push_back(0); outData.push_back(4); // long format outData.push_back(0); outData.push_back(0); outData.push_back(0); outData.push_back(1); // single value outData.push_back(0); outData.push_back(0); outData.push_back(0); outData.push_back(8); // image starts right after the 8-byte header outData.push_back(1); outData.push_back(2); // -- bits per sample outData.push_back(0); outData.push_back(3); // short format outData.push_back(0); outData.push_back(0); outData.push_back(0); outData.push_back(3); // three values unsigned int BPS_offset = 8 + image_size_bytes + 2 + (NUM_IFD * 12) + 4; // offset to data (as data is > 4 bytes) outData.push_back((BPS_offset & 0xff000000) >> 24); outData.push_back((BPS_offset & 0xff0000) >> 16); outData.push_back((BPS_offset & 0xff00) >> 8); outData.push_back(BPS_offset & 0xff); outData.push_back(1); outData.push_back(0x53); // -- sample format outData.push_back(0); outData.push_back(3); // short format outData.push_back(0); outData.push_back(0); outData.push_back(0); outData.push_back(3); // three values unsigned int SF_offset = BPS_offset + 3*2; // offset to data (as data is > 4 bytes) outData.push_back((SF_offset & 0xff000000) >> 24); outData.push_back((SF_offset & 0xff0000) >> 16); outData.push_back((SF_offset & 0xff00) >> 8); outData.push_back(SF_offset & 0xff); outData.push_back(0); outData.push_back(0); outData.push_back(0); outData.push_back(0); // IFD END // bits per sample data assert(outData.size() == BPS_offset); outData.push_back(0); outData.push_back(8*sizeof(float)); outData.push_back(0); outData.push_back(8*sizeof(float)); outData.push_back(0); outData.push_back(8*sizeof(float)); // sample format data (1 = uint, 2 = sint, 3 = float) assert(outData.size() == SF_offset); outData.push_back(0); outData.push_back(3); outData.push_back(0); outData.push_back(3); outData.push_back(0); outData.push_back(3); return outData; }
static std::string EncodeRadianceHDR(unsigned int w, unsigned int h, float* RGBdata, unsigned int floatsPerPixel = 4) {assert(floatsPerPixel >= 3); // we write only three floats (RGB) but support larger strides// Key-Value pairs after RADIANCE are optional //const char header[] = "#?RADIANCE\nEXPOSURE=1\nGAMMA=2.2\nFORMAT=32-bit_rle_rgbe\n\n"; const char header[] = "#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n\n"; std::string outData; outData.reserve(w*h*4 + sizeof(header) + 200); // 200 is some slack... std::vector<unsigned char> scanline[4]; scanline[0].resize(w); scanline[1].resize(w); scanline[2].resize(w); scanline[3].resize(w); outData.append(header, sizeof(header)-1); outData.append("-Y ", 3); outData += std::to_string(h); outData.append(" +X ", 4); outData += std::to_string(w); outData.push_back('\n'); for(unsigned int y=0 ; y<h; y++) { // RLE header // TODO looking at stb_image there seems to be also a non RLE line mode, which we should use as we don't really encode RLE here, // but I'm not sure that the way stb_image decodes the line header is standard-compliant... outData.push_back(2); outData.push_back(2); outData.push_back( (unsigned char)((w>>8) & 0xff) ); outData.push_back( (unsigned char)(w & 0xff) ); for(unsigned int x=0 ; x<w; x++) { unsigned char encodedPixel[4]; float r = RGBdata[0], g = RGBdata[1], b= RGBdata[2]; //r /= 179.0; g /= 179.0; b /= 179.0; double maxV = r; if(maxV < g) maxV = g; if(maxV < b) maxV = b; if(maxV < std::numeric_limits<double>::epsilon()) { encodedPixel[0] = encodedPixel[1] = encodedPixel[2] = encodedPixel[3] = 0; } else { int e; maxV = frexp(maxV, &e) * 256.0/maxV; encodedPixel[0] = unsigned char(maxV * r); encodedPixel[1] = unsigned char(maxV * g); encodedPixel[2] = unsigned char(maxV * b); encodedPixel[3] = unsigned char(e + 128); } scanline[0][x] = encodedPixel[0]; scanline[1][x] = encodedPixel[1]; scanline[2][x] = encodedPixel[2]; scanline[3][x] = encodedPixel[3]; RGBdata += floatsPerPixel; } // For simplicity, write all as it was not RLE... for(unsigned int line=0; line < 4; line++) { auto scanIter = scanline[line].begin(); auto scanEnd = scanline[line].end(); while( scanIter < scanEnd ) { size_t remaining = scanEnd-scanIter; // the last bit in a char, if set, would indicate a RLE run, we want to avoid that unsigned char toWrite = remaining>127 ? 127 : (unsigned char)remaining; outData.push_back(toWrite); // length of the "non run" data outData.append((char*)& scanIter[0], (size_t)toWrite); scanIter += (size_t)toWrite; } } } return outData; }
1 comment:
We usually use DevIL (http://sourceforge.net/projects/openil/ ), works fine with Radiance HDR :)
Post a Comment