From aed7f0d5d43a7cfb7812456f88aaf7bbe64f3cf2 Mon Sep 17 00:00:00 2001 From: Peter Hillman Date: Mon, 11 Dec 2023 16:21:09 +1300 Subject: [PATCH] add deep id/manifest tools and docs Signed-off-by: Peter Hillman --- src/bin/CMakeLists.txt | 1 + src/bin/exrmanifest/CMakeLists.txt | 14 + src/bin/exrmanifest/main.cpp | 210 +++++++++ src/examples/CMakeLists.txt | 10 + src/examples/deepidexample.cpp | 689 +++++++++++++++++++++++++++++ src/examples/deepidselect.cpp | 540 ++++++++++++++++++++++ website/DeepIDsSpecification.rst | 329 ++++++++++++++ website/concepts.rst | 1 + 8 files changed, 1794 insertions(+) create mode 100644 src/bin/exrmanifest/CMakeLists.txt create mode 100644 src/bin/exrmanifest/main.cpp create mode 100644 src/examples/deepidexample.cpp create mode 100644 src/examples/deepidselect.cpp create mode 100644 website/DeepIDsSpecification.rst diff --git a/src/bin/CMakeLists.txt b/src/bin/CMakeLists.txt index 9a76d58071..893fd58d11 100644 --- a/src/bin/CMakeLists.txt +++ b/src/bin/CMakeLists.txt @@ -15,4 +15,5 @@ if(OPENEXR_BUILD_TOOLS) add_subdirectory( exrmultiview ) add_subdirectory( exrmultipart ) add_subdirectory( exrcheck ) + add_subdirectory( exrmanifest ) endif() diff --git a/src/bin/exrmanifest/CMakeLists.txt b/src/bin/exrmanifest/CMakeLists.txt new file mode 100644 index 0000000000..b5b04fb753 --- /dev/null +++ b/src/bin/exrmanifest/CMakeLists.txt @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) Contributors to the OpenEXR Project. + +add_executable(exrmanifest main.cpp) +target_link_libraries(exrmanifest OpenEXR::OpenEXR) +set_target_properties(exrmanifest PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) +if(OPENEXR_INSTALL_TOOLS) + install(TARGETS exrmanifest DESTINATION ${CMAKE_INSTALL_BINDIR}) +endif() +if(WIN32 AND BUILD_SHARED_LIBS) + target_compile_definitions(exrmanifest PRIVATE OPENEXR_DLL) +endif() diff --git a/src/bin/exrmanifest/main.cpp b/src/bin/exrmanifest/main.cpp new file mode 100644 index 0000000000..a0661c49db --- /dev/null +++ b/src/bin/exrmanifest/main.cpp @@ -0,0 +1,210 @@ + +// +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) Contributors to the OpenEXR Project. +// + +// +// output all the idmanifest information found in the file as plain text +// + +#include +#include +#include +#include + +#include +#include +#include + +using namespace OPENEXR_IMF_NAMESPACE; + +using std::cerr; +using std::cout; +using std::endl; +using std::exception; +using std::max; +using std::ostream; +using std::set; +using std::string; +using std::to_string; +using std::vector; + +size_t +dumpManifest (const IDManifest& mfst) +{ + + size_t uncompressedSize = 0; + + for (size_t i = 0; i < mfst.size (); ++i) + { + const IDManifest::ChannelGroupManifest& m = mfst[i]; + bool first = true; + if (i > 0) { cout << "\n\n"; } + cout << " channels : "; + for (set::const_iterator s = m.getChannels ().begin (); + s != m.getChannels ().end (); + ++s) + { + if (!first) { cout << ','; } + else { first = false; } + + cout << *s; + uncompressedSize += s->size () + 1; + } + + cout << "\n hashScheme: " << m.getHashScheme () << endl; + cout << " encoding : " << m.getEncodingScheme () << endl; + switch (m.getLifetime ()) + { + case IDManifest::LIFETIME_FRAME: + cout << " lifetime : frame\n"; + break; + case IDManifest::LIFETIME_SHOT: + cout << " lifetime : shot\n"; + break; + case IDManifest::LIFETIME_STABLE: + cout << " lifetime : stable\n"; + break; + } + + // + // compute max field sizes + // + size_t maxNumLen = 0; + vector componentLength (m.getComponents ().size ()); + for (size_t c = 0; c < m.getComponents ().size (); ++c) + { + size_t componentSize = m.getComponents ()[c].size (); + uncompressedSize += componentSize + 1; + componentLength[c] = max (componentLength[c], componentSize); + } + for (IDManifest::ChannelGroupManifest::ConstIterator q = m.begin (); + q != m.end (); + ++q) + { + + size_t stringLen = to_string (q.id ()).size (); + uncompressedSize += stringLen; + maxNumLen = max (maxNumLen, stringLen); + + for (size_t i = 0; i < q.text ().size (); i++) + { + uncompressedSize += q.text ()[i].size () + 1; + componentLength[i] = + max (componentLength[i], q.text ()[i].size ()); + } + } + + cout << " " << string (maxNumLen + 1, ' '); + for (size_t c = 0; c < m.getComponents ().size (); ++c) + { + string s = m.getComponents ()[c]; + cout << s << string (componentLength[c] + 1 - s.size (), ' '); + } + cout << endl; + for (IDManifest::ChannelGroupManifest::ConstIterator q = m.begin (); + q != m.end (); + ++q) + { + string id = to_string (q.id ()); + cout << " " << id << string (maxNumLen + 1 - id.size (), ' '); + for (size_t i = 0; i < q.text ().size (); i++) + { + string s = q.text ()[i]; + cout << s << string (componentLength[i] + 1 - s.size (), ' '); + } + cout << '\n'; + } + } + + return uncompressedSize; +} + +void +printManifest (const char fileName[]) +{ + + MultiPartInputFile in (fileName); + // + // extract objectID attribute + // + + for (int part = 0; part < in.parts (); part++) + { + if (in.parts () > 1) { cout << fileName << " part " << part << ":\n"; } + if (hasIDManifest (in.header (part))) + { + const Imf::CompressedIDManifest& mfst = + idManifest (in.header (part)); + size_t size = dumpManifest (mfst); + cout << "raw text size : " << size << endl; + cout << "uncompressed size: " << mfst._uncompressedDataSize << endl; + cout << "compressed size : " << mfst._compressedDataSize << endl; + } + else { cout << "no manifest found\n"; } + } +} + +void +usageMessage (ostream& stream, const char* program_name, bool verbose = false) +{ + stream << "Usage: " << program_name << " imagefile [imagefile ...]\n"; + + if (verbose) + stream + << "\n" + "Read exr files and print the contents of the embedded manifest.\n" + "\n" + "Options:\n" + " -h, --help print this message\n" + " --version print version information\n" + "\n" + "Report bugs via https://github.com/AcademySoftwareFoundation/openexr/issues or email security@openexr.com\n" + ""; +} + +int +main (int argc, char* argv[]) +{ + + if (argc < 2) + { + usageMessage (cerr, argv[0], false); + return -1; + } + + for (int i = 1; i < argc; ++i) + { + if (!strcmp (argv[i], "-h") || !strcmp (argv[1], "--help")) + { + usageMessage (cout, "exrmanifest", true); + return 0; + } + else if (!strcmp (argv[i], "--version")) + { + const char* libraryVersion = getLibraryVersion (); + + cout << "exrmanifest (OpenEXR) " << OPENEXR_VERSION_STRING; + if (strcmp (libraryVersion, OPENEXR_VERSION_STRING)) + cout << "(OpenEXR version " << libraryVersion << ")"; + cout << " https://openexr.com" << endl; + cout << "Copyright (c) Contributors to the OpenEXR Project" << endl; + cout << "License BSD-3-Clause" << endl; + return 0; + } + } + + try + { + for (int i = 1; i < argc; ++i) + printManifest (argv[i]); + } + catch (const exception& e) + { + cerr << argv[0] << ": " << e.what () << endl; + return 1; + } + + for (int i = 1; i < argc; ++i) {} +} diff --git a/src/examples/CMakeLists.txt b/src/examples/CMakeLists.txt index 4d8cb57eb7..ea34cb42c9 100644 --- a/src/examples/CMakeLists.txt +++ b/src/examples/CMakeLists.txt @@ -41,6 +41,16 @@ target_link_libraries(OpenEXRExamples OpenEXR::OpenEXR) set_target_properties(OpenEXRExamples PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) + +add_executable(deepidexample deepidexample.cpp) +target_link_libraries(deepidexample OpenEXR::OpenEXR) + +add_executable(deepidselect deepidselect.cpp) +target_link_libraries(deepidselect OpenEXR::OpenEXR) + + + + if(WIN32 AND BUILD_SHARED_LIBS) target_compile_definitions(OpenEXRExamples PRIVATE OPENEXR_DLL) endif() diff --git a/src/examples/deepidexample.cpp b/src/examples/deepidexample.cpp new file mode 100644 index 0000000000..f0587d0306 --- /dev/null +++ b/src/examples/deepidexample.cpp @@ -0,0 +1,689 @@ +// +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) Contributors to the OpenEXR Project. +// + +// +// example of writing a file with DeepIDs. +// Creates a deep file with a series of circles and 'blobs' (2D Guassian-like objects) +// of different colors and sizes, each with two or three ID channels, and an idmanifest +// to store the respective names. +// +// + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +using namespace OPENEXR_IMF_NAMESPACE; + +using std::cerr; +using std::set; +using std::sort; +using std::vector; + +void +printHelp () +{ + cerr + << "syntax: deepidexample options output.deep.exr\n\n"; + cerr << "--multivariate : combine 'material' and 'model' name into a single ID channel, rather than separate channels\n"; + cerr << "--64 : use 64 bit hashes in two channels, rather than a single channel 32 bit hash\n"; + cerr << "--frame number : specify animation frame number. Animation cycles every 200 frames\n"; + cerr << "--objectid : store object ids in a simple stringvector format rather than the idmanifest attribute\n"; + cerr << "--size width height : specify image dimensions for output (default 256 256)\n"; + cerr << "--count number : number of objects to write (default 100)\n"; +} + +// +// a 'sample' as written to the deep file, with RGBA, depth, and up to five ID channels +// +struct Rgbaz +{ + half r, g, b, a, z; + uint32_t id0, id1, id2, id3, id4; + + // + // depth sort (use ID to resolve ties) + // + bool operator<(const Rgbaz& other) + { + if (z < other.z) { return true; } + if (z > other.z) { return false; } + return id4 < other.id4; + } +}; + +// +// object model names +// +static const char* shapeNames[] = {"blob", "circle"}; +static const char* sizeNames[] = {"small", "medium", "big"}; + +// +// seven colors, names and RGB values of each +// +static const char* colorNames[] = { + "white", "red", "green", "blue", "cyan", "magenta", "yellow"}; +struct Rgbaz colors[] = { + {0.9, 0.9, 0.9}, + {0.9, 0.1, 0.1}, + {0.1, 0.9, 0.1}, + {0.1, 0.1, 0.9}, + {0.1, 0.9, 0.9}, + {0.9, 0.1, 0.9}, + {0.9, 0.9, 0.1}}; + +int random (std::default_random_engine& generator, int max); + +void drawBlob ( + std::vector>& image, + int width, + int height, + float x, + float y, + float z, + int size, + int color, + const uint32_t* ids); +void drawCircle ( + std::vector>& image, + int width, + int height, + float x, + float y, + float z, + int size, + int color, + const uint32_t* ids); + +int +main (int argc, char* argv[]) +{ + char* outputFile = nullptr; + bool hash64 = false; + bool multivariate = false; + bool objectID = false; + int width = 256; + int height = 256; + int count = 100; + int frame = 0; + + // parse options + for (int i = 1; i < argc; ++i) + { + if (strncmp (argv[i], "--64", 3) == 0) { hash64 = true; } + else if (strncmp (argv[i], "--multivariate", 3) == 0) + { + multivariate = true; + } + else if (strncmp (argv[i], "--help", 3) == 0) + { + printHelp (); + return 0; + } + else if (strncmp (argv[i], "--size", 3) == 0) + { + if (argc < i + 2) + { + printHelp (); + return 1; + } + width = atoi (argv[i + 1]); + height = atoi (argv[i + 2]); + i += 2; + } + else if (strncmp (argv[i], "--count", 3) == 0) + { + if (argc < i + 1) + { + printHelp (); + return 1; + } + count = atoi (argv[i + 1]); + i += 1; + } + else if (strncmp (argv[i], "--frame", 3) == 0) + { + if (argc < i + 1) + { + printHelp (); + return 1; + } + frame = atoi (argv[i + 1]); + i += 1; + } + else if (strncmp (argv[i], "--objectid", 3) == 0) { objectID = true; } + else if (outputFile == nullptr) { outputFile = argv[i]; } + else + { + printHelp (); + return 1; + } + } + + if (outputFile == nullptr) + { + cerr << "error: need to specify output filename\n"; + printHelp (); + return 1; + } + + if (objectID) + { + if (!multivariate || hash64) + { + cerr + << "error: --objectid mode only works with --multivariate on and --64 off\n"; + return 1; + } + } + + // + // initialize manifest object + // + + IDManifest::ChannelGroupManifest modelOrMultiManifest; + IDManifest::ChannelGroupManifest materialManifest; + + std::vector oldObjectID; + + if (multivariate) + { + if (hash64) + { + set ids; + ids.insert ("id0"); + ids.insert ("id1"); + modelOrMultiManifest.setChannels (ids); + modelOrMultiManifest.setEncodingScheme (IDManifest::ID2_SCHEME); + modelOrMultiManifest.setHashScheme (IDManifest::MURMURHASH3_64); + } + else + { + modelOrMultiManifest.setChannel ("id"); + modelOrMultiManifest.setEncodingScheme (IDManifest::ID_SCHEME); + modelOrMultiManifest.setHashScheme (IDManifest::MURMURHASH3_32); + } + vector components (2); + components[0] = "model"; + components[1] = "material"; + modelOrMultiManifest.setComponents (components); + } + else + { + if (hash64) + { + set model; + model.insert ("model.id0"); + model.insert ("model.id1"); + modelOrMultiManifest.setChannels (model); + modelOrMultiManifest.setEncodingScheme (IDManifest::ID2_SCHEME); + modelOrMultiManifest.setHashScheme (IDManifest::MURMURHASH3_64); + + set material; + material.insert ("material.id0"); + material.insert ("material.id1"); + materialManifest.setChannels (material); + materialManifest.setEncodingScheme (IDManifest::ID2_SCHEME); + materialManifest.setHashScheme (IDManifest::MURMURHASH3_64); + } + else + { + modelOrMultiManifest.setChannel ("modelid"); + materialManifest.setChannel ("materialid"); + + modelOrMultiManifest.setEncodingScheme (IDManifest::ID_SCHEME); + modelOrMultiManifest.setHashScheme (IDManifest::MURMURHASH3_32); + materialManifest.setEncodingScheme (IDManifest::ID_SCHEME); + materialManifest.setHashScheme (IDManifest::MURMURHASH3_32); + } + modelOrMultiManifest.setComponent ("model"); + materialManifest.setComponent ("material"); + } + + modelOrMultiManifest.setLifetime (IDManifest::LIFETIME_STABLE); + materialManifest.setLifetime (IDManifest::LIFETIME_STABLE); + + // + // draw image + // + + vector> deepImage (width * height); + + std::default_random_engine generator; + generator.seed (2); + + uint32_t ids[5]; + + // + // animation oscillates between two random positions + // over 100 frames + // + float blend = 0.5 - cos (double (frame) * M_PI / 100.) / 2; + for (int s = 0; s < count; ++s) + { + int shape = random (generator, 1); + int size = random (generator, 2); + int color = random (generator, 6); + + // + // generate ID + // + if (multivariate) + { + vector s (2); + s[0] = string (shapeNames[shape]) + "/" + string (sizeNames[size]); + s[1] = colorNames[color]; + uint64_t hash = modelOrMultiManifest.insert (s); + ids[0] = hash & 0xFFFFFFFF; + + // only needed for 64 bit hash scheme: store most significant 32 bits in ids[1] + ids[1] = hash >> 32; + + if (objectID) + { + oldObjectID.push_back ( + s[0] + "," + s[1] + "," + std::to_string (ids[0])); + } + } + else + { + uint64_t hash = modelOrMultiManifest.insert ( + string (shapeNames[shape]) + "/" + string (sizeNames[size])); + ids[0] = hash & 0xFFFFFFFF; + ids[1] = hash >> 32; + hash = materialManifest.insert (colorNames[color]); + ids[2] = hash & 0xFFFFFFFF; + ids[3] = hash >> 32; + } + + ids[4] = s; // particle ID + + // + // randomized position, velocity, depth + // + int x1 = random (generator, width); + int y1 = random (generator, height); + int x2 = random (generator, width); + int y2 = random (generator, height); + float z = random (generator, 4096) / 2.f; + + float x = blend * x2 + (1.0 - blend) * x1; + float y = blend * y2 + (1.0 - blend) * y1; + + if (shape == 0) + { + drawBlob (deepImage, width, height, x, y, z, size, color, ids); + } + else + { + drawCircle (deepImage, width, height, x, y, z, size, color, ids); + } + } + + // + // initialize pointers required by OpenEXR API to indicate address of first sample of each channel of each pixel + // + + vector sampleCounts (width * height); + vector ptrR (width * height); + vector ptrG (width * height); + vector ptrB (width * height); + vector ptrA (width * height); + vector ptrZ (width * height); + vector ptrID0 (width * height); + vector ptrID1 (width * height); + vector ptrID2 (width * height); + vector ptrID3 (width * height); + vector ptrID4 (width * height); + for (int i = 0; i < width * height; ++i) + { + sampleCounts[i] = int (deepImage[i].size ()); + if (sampleCounts[i] > 0) + { + // + // store samples depth sorted + // + sort (deepImage[i].begin (), deepImage[i].end ()); + ptrR[i] = (char*) &deepImage[i][0].r; + ptrG[i] = (char*) &deepImage[i][0].g; + ptrB[i] = (char*) &deepImage[i][0].b; + ptrA[i] = (char*) &deepImage[i][0].a; + ptrZ[i] = (char*) &deepImage[i][0].z; + ptrID0[i] = (char*) &deepImage[i][0].id0; + ptrID1[i] = (char*) &deepImage[i][0].id1; + ptrID2[i] = (char*) &deepImage[i][0].id2; + ptrID3[i] = (char*) &deepImage[i][0].id3; + ptrID4[i] = (char*) &deepImage[i][0].id4; + } + } + + // save file + + Header h (width, height); + h.compression () = ZIPS_COMPRESSION; + + DeepFrameBuffer buf; + buf.insertSampleCountSlice (Slice ( + UINT, + (char*) sampleCounts.data (), + sizeof (int), + sizeof (int) * width)); + + h.channels ().insert ("R", Channel (HALF)); + buf.insert ( + "R", + DeepSlice ( + HALF, + (char*) ptrR.data (), + sizeof (char*), + sizeof (char*) * width, + sizeof (Rgbaz))); + + h.channels ().insert ("G", Channel (HALF)); + buf.insert ( + "G", + DeepSlice ( + HALF, + (char*) ptrG.data (), + sizeof (char*), + sizeof (char*) * width, + sizeof (Rgbaz))); + + h.channels ().insert ("B", Channel (HALF)); + buf.insert ( + "B", + DeepSlice ( + HALF, + (char*) ptrB.data (), + sizeof (char*), + sizeof (char*) * width, + sizeof (Rgbaz))); + + h.channels ().insert ("A", Channel (HALF)); + buf.insert ( + "A", + DeepSlice ( + HALF, + (char*) ptrA.data (), + sizeof (char*), + sizeof (char*) * width, + sizeof (Rgbaz))); + + h.channels ().insert ("Z", Channel (HALF)); + buf.insert ( + "Z", + DeepSlice ( + HALF, + (char*) ptrZ.data (), + sizeof (char*), + sizeof (char*) * width, + sizeof (Rgbaz))); + + if (multivariate) + { + if (hash64) + { + h.channels ().insert ("id.id0", UINT); + buf.insert ( + "id.id0", + DeepSlice ( + UINT, + (char*) ptrID0.data (), + sizeof (char*), + sizeof (char*) * width, + sizeof (Rgbaz))); + h.channels ().insert ("id.id1", UINT); + buf.insert ( + "id.id1", + DeepSlice ( + UINT, + (char*) ptrID1.data (), + sizeof (char*), + sizeof (char*) * width, + sizeof (Rgbaz))); + } + else + { + h.channels ().insert ("id", UINT); + buf.insert ( + "id", + DeepSlice ( + UINT, + (char*) ptrID0.data (), + sizeof (char*), + sizeof (char*) * width, + sizeof (Rgbaz))); + } + } + else + { + if (hash64) + { + h.channels ().insert ("model.id0", UINT); + buf.insert ( + "model.id0", + DeepSlice ( + UINT, + (char*) ptrID0.data (), + sizeof (char*), + sizeof (char*) * width, + sizeof (Rgbaz))); + h.channels ().insert ("model.id1", UINT); + buf.insert ( + "model.id1", + DeepSlice ( + UINT, + (char*) ptrID1.data (), + sizeof (char*), + sizeof (char*) * width, + sizeof (Rgbaz))); + + h.channels ().insert ("material.id0", UINT); + buf.insert ( + "material.id0", + DeepSlice ( + UINT, + (char*) ptrID2.data (), + sizeof (char*), + sizeof (char*) * width, + sizeof (Rgbaz))); + h.channels ().insert ("material.id1", UINT); + buf.insert ( + "material.id1", + DeepSlice ( + UINT, + (char*) ptrID3.data (), + sizeof (char*), + sizeof (char*) * width, + sizeof (Rgbaz))); + } + else + { + h.channels ().insert ("modelid", UINT); + buf.insert ( + "modelid", + DeepSlice ( + UINT, + (char*) ptrID0.data (), + sizeof (char*), + sizeof (char*) * width, + sizeof (Rgbaz))); + h.channels ().insert ("materialid", UINT); + buf.insert ( + "materialid", + DeepSlice ( + UINT, + (char*) ptrID2.data (), + sizeof (char*), + sizeof (char*) * width, + sizeof (Rgbaz))); + } + } + + h.channels ().insert ("particleid", UINT); + buf.insert ( + "particleid", + DeepSlice ( + UINT, + (char*) ptrID4.data (), + sizeof (char*), + sizeof (char*) * width, + sizeof (Rgbaz))); + + if (objectID) + { + h.insert ("objectID", StringVectorAttribute (oldObjectID)); + } + else + { + IDManifest manifest; + manifest.add (modelOrMultiManifest); + if (!multivariate) { manifest.add (materialManifest); } + IDManifest::ChannelGroupManifest particleManifest; + particleManifest.setChannel ("particleid"); + particleManifest.setEncodingScheme (IDManifest::ID_SCHEME); + particleManifest.setHashScheme (IDManifest::NOTHASHED); + particleManifest.setLifetime (IDManifest::LIFETIME_SHOT); + manifest.add (particleManifest); + + addIDManifest (h, manifest); + } + + // + // samples are depth sorted, and do not overlap + // + addDeepImageState (h, DIS_TIDY); + + DeepScanLineOutputFile file (outputFile, h); + file.setFrameBuffer (buf); + file.writePixels (height); +} + +int +random (std::default_random_engine& generator, int max) +{ + std::uniform_int_distribution dist (0, max); + return dist (generator); +} + +void +drawBlob ( + std::vector>& image, + int width, + int height, + float x, + float y, + float z, + int size, + int color, + const uint32_t* ids) +{ + // + // draw windowed gaussian centered at (x,y) + // + + float sigma = 5 + 40 * size; + + int ptr = 0; + + Rgbaz point; + point.z = z; + point.id0 = ids[0]; + point.id1 = ids[1]; + point.id2 = ids[2]; + point.id3 = ids[3]; + point.id4 = ids[4]; + + for (int j = 0; j < height; ++j) + { + for (int i = 0; i < width; ++i) + { + float dist_sq = (x - i) * (x - i) + (j - y) * (j - y); + float scale = + exp (-dist_sq / sigma) * (1.f - sqrt (dist_sq) / sigma); + + if (scale > 0.001) + { + point.r = colors[color].r * scale; + point.g = colors[color].g * scale; + point.b = colors[color].b * scale; + point.a = scale; + + image[ptr].push_back (point); + } + + ptr++; + } + } +} + +float +getAlpha (float xmin, float ymin, float xmax, float ymax, float size) +{ + bool in1 = xmin * xmin + ymin * ymin < size * size; + bool in2 = xmax * xmax + ymin * ymin < size * size; + bool in3 = xmin * xmin + ymax * ymax < size * size; + bool in4 = xmin * xmin + ymax * ymax < size * size; + + if (in1 && in2 && in3 && in4) { return (xmax - xmin) * (ymax - ymin); } + if (!in1 && !in2 && !in3 && !in4) { return 0.f; } + if (xmax - xmin < 0.001 && ymax - ymin < 0.001) { return 0.f; } + return getAlpha (xmin, ymin, (xmin + xmax) / 2, (ymin + ymax) / 2, size) + + getAlpha ((xmin + xmax) / 2, ymin, xmax, (ymin + ymax) / 2, size) + + getAlpha (xmin, (ymin + ymax) / 2, (xmin + xmax) / 2, ymax, size) + + getAlpha ((xmin + xmax) / 2, (ymin + ymax) / 2, xmax, ymax, size); +} + +void +drawCircle ( + std::vector>& image, + int width, + int height, + float x, + float y, + float z, + int size, + int color, + const uint32_t* ids) +{ + int ptr = 0; + float radius = 3 + 8 * size; + Rgbaz point; + point.z = z; + point.id0 = ids[0]; + point.id1 = ids[1]; + point.id2 = ids[2]; + point.id3 = ids[3]; + point.id4 = ids[4]; + + for (int j = 0; j < height; ++j) + { + for (int i = 0; i < width; ++i) + { + float alpha = getAlpha ( + i - x - 0.5f, j - y - 0.5f, i - x + 0.5f, j - y + 0.5f, radius); + if (alpha > 0) + { + point.r = colors[color].r * alpha; + point.g = colors[color].g * alpha; + point.b = colors[color].b * alpha; + point.a = alpha; + + image[ptr].push_back (point); + } + ptr++; + } + } +} diff --git a/src/examples/deepidselect.cpp b/src/examples/deepidselect.cpp new file mode 100644 index 0000000000..dab49cda65 --- /dev/null +++ b/src/examples/deepidselect.cpp @@ -0,0 +1,540 @@ +// +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) Contributors to the OpenEXR Project. +// + +// +// example of using an IDManifest to locate given objects in a deep image with IDs +// demonstrates how to use multivariate IDs, and 64 bit IDs spread across two channels +// deepidexample will create images that can be used as input +// (though this tool is intended to support images from other sources) +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace OPENEXR_IMF_NAMESPACE; +using namespace IMATH_NAMESPACE; + +using std::cerr; +using std::dec; +using std::endl; +using std::hex; +using std::list; +using std::map; +using std::set; +using std::stoi; +using std::vector; + +struct match +{ + int channel1; // index of first channel to look up ID in + uint32_t id1; // first ID + int channel2; // index of second channel, or -1 if only one channel + uint32_t id2; // second ID, ignored channel2==-1 +}; + +// +// setIds parses the matches arguments, and populates lists of matching IDs. If there are '--and' statements in the matches +// ids are entered in a new list +// +void setIds ( + const IDManifest& mfst, + list>& ids, + const char* matches[], + int numMatches, + const map& channelToPos); + +int +main (int argc, const char* argv[]) +{ + if (argc < 4) + { + cerr + << "syntax: [--mask] input.exr match [match...] [--and match [match...]] output.exr\n" + << " if --mask specified, writes a shallow EXR with a mask of the selected object(s) in the 'A' channel\n" + << " otherwise, writes a deep EXR only containing the selected object(s)\n" + << '\n' + << " matches can be:\n" + << " searchstring - match any component of any channel\n" + << " componentname:searchstring - only match given component\n" + << " channelname:number - match specified numeric ID in given channel\n" + << '\n' + << "\"A B --and C D\" means \"(must match either A or B) and also (must match either C or D)\"\n" + << " e.g:\n" + << " input.deep.exr blue output.deep.exr\n" + << " input.deep.exr material:blue --and model:blob output.deep.exr\n" + << " input.deep.exr material:blue material:red --and model:blob output.deep.exr\n" + << " input.deep.exr particleid:3 output.deep.exr\n"; + return 1; + } + + bool mask = false; + int numMatchArguments = argc - 2; + const char** matchArguments = argv + 2; + const char* inputFile = argv[1]; + if (strcmp (argv[1], "--mask") == 0) + { + mask = true; + numMatchArguments--; + matchArguments++; + inputFile = argv[2]; + } + + MultiPartInputFile input (inputFile); + + if (!hasIDManifest (input.header (0))) + { + cerr << "deepidselect requires an ID manifest in the EXR header\n"; + return 1; + } + + for (int i = 0; i < input.parts (); ++i) + { + if (input.header (i).type () != DEEPSCANLINE) + { + cerr + << "deepidselect currently only supports files which are entirely deep scanline files\n"; + return 1; + } + } + + // + // build output headers. For deep output, this is easy: just copy them over + // for masks, build scanline images of the same dimensions, each with a single alpha channel + // + vector
hdrs (input.parts ()); + if (mask) + { + for (int h = 0; h < input.parts (); ++h) + { + const Header& inHdr = input.header (h); + hdrs[h].dataWindow () = inHdr.dataWindow (); + hdrs[h].setType (SCANLINEIMAGE); + hdrs[h].displayWindow () = inHdr.displayWindow (); + if (inHdr.hasView ()) { hdrs[h].setView (inHdr.view ()); } + if (inHdr.hasName ()) { hdrs[h].setName (inHdr.name ()); } + hdrs[h].channels ().insert ("A", Channel (HALF)); + } + } + else + { + for (int h = 0; h < input.parts (); ++h) + { + hdrs[h] = input.header (h); + } + } + + MultiPartOutputFile output (argv[argc - 1], hdrs.data (), input.parts ()); + + // process each part individually + + for (int pt = 0; pt < input.parts (); ++pt) + { + const Header& inputHeader = input.header (pt); + const ChannelList& inputChans = inputHeader.channels (); + Box2i dataWindow = inputHeader.dataWindow (); + int width = dataWindow.max.x + 1 - dataWindow.min.x; + + map + channelToPos; // index of each channel as stored in scanLine object + + int alphaChannel = -1; + PixelType alphaChannelType = HALF; + + int channels = 0; + for (ChannelList::ConstIterator i = inputChans.begin (); + i != inputChans.end (); + ++i) + { + channelToPos[i.name ()] = channels; + if (strcmp (i.name (), "A") == 0) + { + alphaChannel = channels; + alphaChannelType = i.channel ().type; + } + channels++; + } + + // if the part has a manifest, use this part's manifest + // otherwise use part 0's manifest. + // (but reparse the manifest for every part in case the channel list has changed) + // + int manifestPart = hasIDManifest (inputHeader) ? pt : 0; + list> ids; + IDManifest mfst = idManifest (input.header (manifestPart)); + + setIds (mfst, ids, matchArguments, numMatchArguments, channelToPos); + + // store for an individual deep scanline. Accessed using scanLine[channelIndex][pixelIndex][sampleIndex] + // where pixelIndex is 0 for the leftmost pixel (even if the dataWindow doesn't start at 0) + vector>> scanLine (channels); + + // pointers to the data in each channel for FrameBuffer + vector> scanPointers (channels); + + for (int i = 0; i < channels; ++i) + { + scanLine[i].resize (width); + scanPointers[i].resize (width); + } + + vector pixelCounts (width); + vector outputAlpha ( + width); // only required for --mask mode: stores output + + DeepFrameBuffer buf; + buf.insertSampleCountSlice (Slice ( + UINT, + (char*) pixelCounts.data () - dataWindow.min.x, + sizeof (int), + 0)); + int c = 0; + + // + // read all channels as their native type, so they round trip when writing deep + // + for (ChannelList::ConstIterator i = inputChans.begin (); + i != inputChans.end (); + ++i, ++c) + { + buf.insert ( + i.name (), + DeepSlice ( + i.channel ().type, + (char*) scanPointers[c].data () - dataWindow.min.x, + sizeof (char*), + 0, + sizeof (int32_t))); + } + + DeepScanLineInputPart inPart (input, pt); + inPart.setFrameBuffer (buf); + + // + // for mask, create an alpha channel and initialize a FrameBuffer with that data + // otherwise, can use the deep frame buffer for both input and output, since the data is processed in-place + // + if (mask) + { + FrameBuffer outBuf; + outBuf.insert ( + "A", + Slice ( + HALF, + (char*) outputAlpha.data () - dataWindow.min.x, + sizeof (half), + 0)); + OutputPart outPart (output, pt); + outPart.setFrameBuffer (outBuf); + } + else + { + DeepScanLineOutputPart outPart (output, pt); + outPart.setFrameBuffer (buf); + } + + for (int y = dataWindow.min.y; y <= dataWindow.max.y; ++y) + { + inPart.readPixelSampleCounts (y); + for (int c = 0; c < channels; ++c) + { + for (int x = 0; x < width; ++x) + { + scanLine[c][x].resize (pixelCounts[x]); + scanPointers[c][x] = scanLine[c][x].data (); + } + } + + inPart.readPixels (y); + + for (int x = 0; x < width; ++x) + { + int outputSample = 0; + + float totalAlpha = 0.f; + float maskAlpha = 0.f; + + for (int s = 0; s < pixelCounts[x]; ++s) + { + // + // should sample s be retained? + // look for an entry in each 'and group' where all the required channels match their + // corresponding values + // + + bool good = true; + for (list>::const_iterator idGroup = + ids.begin (); + idGroup != ids.end () && good; + ++idGroup) + { + good = false; + for (list::const_iterator i = idGroup->begin (); + i != idGroup->end () && !good; + ++i) + { + if (scanLine[i->channel1][x][s] == i->id1 && + (i->channel2 == -1 || + scanLine[i->channel2][x][s] == i->id2)) + { + good = true; + } + } + } + + // + // deep output mode: + // delete unwanted samples + // mask output node: + // composite together wanted sample's alpha + // + + if (mask) + { + //cast alpha to float, and composite + float alpha = 0.f; + switch (alphaChannelType) + { + case FLOAT: + alpha = *(float*) (&scanLine[alphaChannel][x] + .data ()[s]); + break; + case HALF: + alpha = *(half*) (&scanLine[alphaChannel][x] + .data ()[s]); + break; + case UINT: + alpha = scanLine[alphaChannel][x][s]; + break; //wat! this is a weird thing to do,but whatever... + case NUM_PIXELTYPES: break; + } + + if (good) { maskAlpha += (1.0 - totalAlpha) * alpha; } + totalAlpha += (1.0 - totalAlpha) * alpha; + } + + else if (good) + { + // keep Sample: copy from original position into output position + // (so overwrite any samples that are to be deleted) + for (int c = 0; c < channels; ++c) + { + scanLine[c][x][outputSample] = scanLine[c][x][s]; + } + outputSample++; + } + } + + if (mask) + { + if (totalAlpha > 0.f) { maskAlpha /= totalAlpha; } + outputAlpha[x] = maskAlpha; + } + else + { + // update total count of samples + pixelCounts[x] = outputSample; + } + } + + // + // write data out + // + if (mask) + { + OutputPart outPart (output, pt); + outPart.writePixels (1); + } + else + { + DeepScanLineOutputPart outPart (output, pt); + outPart.writePixels (1); + } + } + } +} + +void +setIds ( + const IDManifest& mfst, + list>& ids, + const char* matches[], + int numMatches, + const map& channelToPos) +{ + + // + // initially one single list + // + ids.clear (); + ids.push_back (list ()); + + // + // check each manifest, each entry, against each matching expression + // + for (int c = 0; c < numMatches; ++c) + { + + // + // an 'and' argument means proceed to the next group of ids + // must find a match in every such group + // + if (strcmp (matches[c], "--and") == 0) + { + ids.push_back (list ()); + continue; + } + + string matchString (matches[c]); + + // + // handle strings of the form component:searchstring + // and channel:idnumber + // + string componentName; + string::size_type pos = matchString.find (':'); + if (pos != string::npos) + { + componentName = matchString.substr (0, pos); + matchString = matchString.substr (pos + 1); + } + + if (matchString.find_first_not_of ("0123456789") == string::npos) + { + map::const_iterator chan = + channelToPos.find (componentName); + if (chan != channelToPos.end ()) + { + match m; + m.channel1 = chan->second; + m.id1 = stoi (matchString); + m.channel2 = -1; + ids.back ().push_back (m); + } + continue; // skip parsing the manifests for this string + } + + // check the manifest for each group of channels + for (size_t i = 0; i < mfst.size (); ++i) + { + + for (IDManifest::ChannelGroupManifest::ConstIterator it = + mfst[i].begin (); + it != mfst[i].end (); + ++it) + { + for (size_t stringIndex = 0; stringIndex < it.text ().size (); + ++stringIndex) + { + if (componentName == "" || + mfst[i].getComponents ()[stringIndex] == componentName) + { + // simple substring matching only: could do wildcards or regexes here instead + if (it.text ()[stringIndex].find (matchString) != + string::npos) + { + // a match is found - add it to the corresponding channels + if (mfst[i].getEncodingScheme () == + IDManifest::ID_SCHEME) + { + // simple scheme: the ID channel has to match + for (const string& s: mfst[i].getChannels ()) + { + map::const_iterator chan = + channelToPos.find (s); + if (chan != channelToPos.end ()) + { + //could support matching the ID against a specific channel + //that check would happen here + + match m; + m.channel1 = chan->second; + m.id1 = uint32_t (it.id ()); + m.channel2 = -1; + ids.back ().push_back (m); + cerr << "adding match " << hex + << it.id () << dec + << " for string " + << it.text ()[stringIndex] + << " in channel " << chan->second + << '(' << chan->first << ")\n"; + } + } + } + else if ( + mfst[i].getEncodingScheme () == + IDManifest::ID2_SCHEME) + { + // 64 bit IDs are spread across two channels, with the least significant bits + // in the first channel (alphabetically) and the most significant bits in the second + // so process the channel set in pairs + + set::const_iterator chanLow = + mfst[i].getChannels ().begin (); + set::const_iterator end = + mfst[i].getChannels ().end (); + + while (chanLow != end) + { + set::const_iterator chanHigh = + chanLow; + ++chanHigh; + + if (chanHigh != end) + { + map::const_iterator + chanIdxLow = + channelToPos.find (*chanLow); + map::const_iterator + chanIdxHigh = + channelToPos.find (*chanHigh); + + if (chanIdxLow != channelToPos.end () && + chanIdxHigh != channelToPos.end ()) + { + // to match against specific channels, check at least one channel matches here + match m; + m.channel1 = chanIdxLow->second; + m.id1 = it.id () & 0xFFFFFFFF; + m.channel2 = chanIdxHigh->second; + m.id2 = it.id () >> 32; + ids.back ().push_back (m); + + cerr << "adding match " << hex + << it.id () << dec + << " for string " + << it.text ()[stringIndex] + << ": " << hex << m.id1 + << " in channel " << m.channel1 + << '(' << chanIdxLow->first + << "), " << hex << m.id2 + << " in channel " << m.channel2 + << '(' << chanIdxHigh->first + << ")\n"; + } + + ++chanLow; + if (chanLow != end) { ++chanLow; } + } + } + } + } + } + } + } + } + } +} diff --git a/website/DeepIDsSpecification.rst b/website/DeepIDsSpecification.rst new file mode 100644 index 0000000000..816fd9816e --- /dev/null +++ b/website/DeepIDsSpecification.rst @@ -0,0 +1,329 @@ + +.. + SPDX-License-Identifier: BSD-3-Clause + Copyright Contributors to the OpenEXR Project. + +OpenEXR Deep IDs Specification +############################## + + +Introduction +============ + +Deep IDs are primarily used in compositing applications to select +objects in images: + +- The 3D renderer stores in the final OpenEXR file multiple ids per + pixel as well a scene manifest providing a mapping to a + human-readable identifier, like an object or material name. +- The compositing application, reads in the deep IDs and implements a + UI for the artist to create a selection based on the manifest data. +- The selected IDs will then be used to create a deep (per-sample) or + shallow (per-pixel) mask for later use. + +Typical uses include: + +- Grading selected objects. +- Removing a certain percentage of particles from a snow or dust motes + render. +- Adding consistent variations to different individuals in a crowd or to different + trees in a forest by applying an ID-based color correction. + +Deep IDs can also be used for debugging: + +- To identify an object/material which renders incorrectly. +- To collect all visible IDs over a sequence so as to prune scene + objects that are never on screen. + +Pros +---- + +- Each pixel can contain an arbitrary number of IDs (0 included), + allowing for perfect mattes. +- Deep IDs are combined with coverage data to support anti-aliasing, + motion blur and depth of field. +- A deep image store the color of the individual objects as well as the + coverage, so an object extracted by ID will have the correct color + values along the edges. By contrast, schemes which only store pixel coverage + cannot isolate objects without color contamination + +Cons +---- + +- IDs are not directly inspectable with a simple image viewer. +- Hash collisions can make multiple objects map to the same ID. +- DeepIDs require software to support deepscanline/deeptiled OpenEXR images + +Deep ID Basics +============== + + +The deep OpenEXR files need to contain the following elements: + +- Deep IDs stored using one or two ``Imf::UINT`` (``uint32_t``) + channels: + + - A single id channel stores 32 bits hashes. + - A pair of id, id2 channels stores 64 bits hashes. + +- Manifest data stored in the file’s metadata as an attribute, or in a side-car file. + + - **NOTE**: OpenEXR 3.0+ `provides an <#openexr-idmanifest-container>` + and attribute for efficient storage + + +Sample storage +============== + + +Terminology +----------- + +- **Channel**: Every deep image contains many named channels (``R``, + ``G``, ``B``, ``A``, ``Z``, ``id``, etc) +- **Pixel**: A discrete image element containing 0 or more deep + ``samples``. +- **Sample**: A container storing the value of each channel at a + specific depth. +- **Id**: A unique numerical identifier mapping to an ``item``. +- **Kind**: a named ``id`` category (id, particleid, instanceid, + objectid, etc). +- **Manifest**: a data structure mapping each id to one ``item``. +- **Item**: an entity (object, material, etc) represented by: + + - An artist-friendly string in the manifest, i.e. “Chair16”. + - A collection of ``samples`` matching a particular ``id`` in the + image. + +Principles +---------- + +1. Deep IDs have a single ``id`` of each ``kind`` per ``sample``, and + every ``item`` in the image has a consistent ``id`` for all its + ``samples``. +2. If two different ``items`` overlap the same space, they will be + stored in separate ``samples`` (which themselves may or may not + overlap). +3. Some ``id`` may not have an associated manifest, like ``particleid``, + and still be useful (for example through image-based picking). +4. In complex cases like an instanced particle system, each ``sample`` + may have an ``instanceid``, ``particleid``, and ``id``/``objectid``. + +Standard ID Channel names +------------------------- + +============== ====================================== +Name Contents +============== ====================================== +**id** by default, an object identifier [1]_ +**objectid** identifier of an object +**materialid** identifier of an material +**particleid** identifier of a particle +**instanceid** identifier of an instanced object +============== ====================================== + +To limit the risk of hash collision, a ``uint64_t`` bits can be encoded +with two ``uint32_t`` channels. The convention is then to suffix the +channel name with ``0`` or ``1`` to indicate the channels storing the +least and the most significant 32 bits respectively, +e.g. ``particleid0`` and ``particleid1`` or ``particle.id0`` and ``particle.id1`` + +When sorted alphanumerically, the channel storing the most significant bits should appear immediately +after the chanel storing the least significant bits. +See appendix for details + +ID generation +------------- + +Any non-uint32 identifier can be hashed to generate an id. + +- ``uint32_t`` hashes should be generated with ``MurmurHash3_x86_32``. +- ``uint64_t`` hashes should be generated with the top 8 bytes of + ``MurmurHash3_x64_128``. + +OpenEXR 3.x offers two convenience functions to hash strings [2]_: +``IDManifest::MurmurHash32`` and ``IDManifest::MurmurHash64``. + +Multivariate IDs +---------------- + +Hashing more than one ``kind`` (i.e. object + material) limits storage +requirements without impairing the artist’s ability to generate +arbitrary mattes. + +For examples hashing the object and material names together is common +practice. In that case, a single ``id`` will map to 2 ``kinds`` in the +manifest, providing more flexibility at the cost of a slightly increased +risk of hash collision. + +Manifest data +------------- + +The manifest contains the human-readable data corresponding to a given +hash. It is a big table of strings that may require more storage than +the actual image data. It can be stored using the following mechanisms: + +OpenEXR idManifest container +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Since OpenEXR 3.0, there is a new standard ``idManifest`` attribute +using a ``CompressedIDManifest`` metadata type, specially designed to +transport manifest data efficiently. It is optimized to reduce the storage space required, +and is the most standard approach. + +The utility ``exrmanifest`` outputs the manifest of EXR images as plain text. + +OpenEXR string container +^^^^^^^^^^^^^^^^^^^^^^^^ + +The manifest can be stored in ``string`` or ``stringvector`` attributes, +but this is not very efficient and may significantly increase file size. + +Side-car files +^^^^^^^^^^^^^^ + +Alternatively, the manifest may be stored in a separate file, with an OpenEXR attributes, +a database or a file naming convention used to associate one or more OpenEXR files +with the corresponding sidecar file. Sidecar files can be advantageous because +they can be shared between different images, and also updated as more content is being rendered. + +Such schemes are not supported by the OpenEXR library, nor are they defined here, +since that is outside the scope of the OpenEXR file format specification. +Although sidecar files may be appropriate for temporary usage, it is strongly recommended +that the embedded manifest is used in OpenEXR images which are to be shared between different companies +or for archival usage. + +Example code +============ + +OpenEXR provides two example tools, ``deepidexample`` and ``deepidselect``. +Compiled tools will be found in the ``src/examples`` folder in the build directory. They are not installed. + + +DeepIDExample +------------- + +``deepidexample`` creates a deep image with multiple objects (two different shapes at one of three sizes), +in one of seven colors. It is intended as a tool for generating test sequences and as an example of code +that generates an image with deep IDs and a manifest. + +``deepidexample`` can generate a sequence of frames, to help test that the IDs are consistently +generated and selected. Specify ``--frame`` for the frame number. The animation cycles every 100 frames. +This ``bash`` command generates a sequence of frames: + +.. code:: bash + + for x in `seq 1000 1100` ; do ./deepidexample --frame $x output.$x.deep.exr ; done + +Run ``deepidexample`` to see further options. + + +DeepIDSelect +------------ + +``deepidselect`` selects individual objects within a deep file, and outputs just those objects. +It is intended to serve as an example of parsing idmanifests to find compile a list of IDs which +match a given substring, and using those ids to identify samples. Its usage is not limited solely +to files created by deepidexample; it should handle files with arbitrary channel names and manifests. +deepidselect supports the ``id64`` scheme with the ``--64`` flag. + +In basic usage, specify ``input.deep.exr (matches) output.deep.exr`` + +``matches`` is a list of one or more separate strings. All objects whose names contain any of the +given substring will be included in output.deep.exr (it is a logical OR of the arguments) +The ``--and`` can be used to force matching of (one or more of) the following match as well as the previous. +For example, ``blue --and circle`` will match any object which is both blue, and a circle. +``blue green --and big small --and circle`` will match blue or green objects, +and which are big or small, and which are circles. +This could also be read as `( blue or green ) and ( big or small ) and ( circle )` + +Each match can be limited to a given component name by specifying ``component:match``. +For example ``model:bl`` will match objects whose model is ``blob`` but not ones whose material is ``blue``. +Specifying a channel name followed by a number will select the object by number, rather than by name. +For example, ``particleid:12`` will select the object with particle ID 12. +(Note that this feature means it is not possible to have a purely numeric substring match with this tool) + +``--mask`` outputs a shallow single channel image which indicating the relative coverage of each pixel +for the selected object. For schemes where the deep image only contains ID (and alpha) information, +but does not store color, this can be used to grade only the selected object. +Edge contamination may be observed along transparent edges of a selected object, if an object behind it is not selected. + +To keep the code simple, ``deepidselect`` is only a minimal example of string matching against ID manifests. +For example, it doesn't support regular expressions, or more advanced boolean logic including negative matches. + + +Appendix +======== + + +64 to 2 x 32 bits conversion and back +------------------------------------- + +To limit the risk of hash collision, a ``uint64_t`` can be encoded in 2 +``uint32_t`` channels, like ``materialid`` and ``materialid2``, using +little-endian byte ordering. + +.. code:: cpp + + #include + #include + #include + + int main() + { + using namespace std; + + // uint 64 input + uint64_t x = 0x12345678'87654321ULL; + cout << setw(20) << "uint64 input: " << hex << x << endl; + + // Convert one uint 64 -> two uint 32 + uint32_t lo = uint32_t(x); + uint32_t hi = uint32_t(x >> 32); + cout << setw(20) << "uint32 low: " << hex << lo << " high: " << hi << endl; + + // Convert two uint32 -> one uint64 + uint64_t y = (uint64_t(hi) << 32) | lo; + cout << setw(20) << "uint64 recombined: " << hex << y << endl; + } + +Output: + +:: + + uint64 input: 1234567887654321 + uint32 low: 87654321 high: 12345678 + uint64 recombined: 1234567887654321 + +Computing a shallow mask from Deep IDs +-------------------------------------- + +A shallow mask is a pixel-level mask that can be used with non-deep +compositing operators. + +Here is the pseudo-code to correctly compute an ID selection mask for a +single pixel: + +.. code:: python + + total_combined_alpha = 0.0 + mask_alpha = 0.0 + sorted_pixel = sort_pixel_front_to_back(input_pixel) + + foreach(sample in sorted_pixel): + if id_is_in_selection(sample.id): + mask_alpha += sample.alpha * (1.0 - total_combined_alpha) + total_combined_alpha += sample.alpha * (1.0 - total_combined_alpha) + + if total_combined_alpha == 0.0: + return 0.0 + else: + return mask_alpha / total_combined_alpha + +.. [1] + See `OpenEXR reserved channel + names `__. + +.. [2] + See + `ImfIDManifest `__ diff --git a/website/concepts.rst b/website/concepts.rst index e1dbc397d0..aadcf858fc 100644 --- a/website/concepts.rst +++ b/website/concepts.rst @@ -16,6 +16,7 @@ OpenEXR Concepts MultiViewOpenEXR InterpretingDeepPixels TheoryDeepPixels + DeepIDsSpecification OpenEXRFileLayout PortingGuide SymbolVisibility