From f6eadf538d73df71d6d635725fb3f43b2dbfdcf7 Mon Sep 17 00:00:00 2001 From: Peter Hillman Date: Tue, 18 Jun 2024 14:09:00 +1200 Subject: [PATCH 1/8] add exrmetrics tool (no tile support yet) Signed-off-by: Peter Hillman --- src/bin/CMakeLists.txt | 1 + src/bin/exrmetrics/CMakeLists.txt | 14 ++ src/bin/exrmetrics/exrmetrics.cpp | 355 ++++++++++++++++++++++++++++++ src/bin/exrmetrics/exrmetrics.h | 23 ++ src/bin/exrmetrics/main.cpp | 203 +++++++++++++++++ src/test/bin/CMakeLists.txt | 1 + src/test/bin/test_exrmetrics.py | 71 ++++++ 7 files changed, 668 insertions(+) create mode 100644 src/bin/exrmetrics/CMakeLists.txt create mode 100644 src/bin/exrmetrics/exrmetrics.cpp create mode 100644 src/bin/exrmetrics/exrmetrics.h create mode 100644 src/bin/exrmetrics/main.cpp create mode 100644 src/test/bin/test_exrmetrics.py diff --git a/src/bin/CMakeLists.txt b/src/bin/CMakeLists.txt index 99785681b1..c70a90ef8e 100644 --- a/src/bin/CMakeLists.txt +++ b/src/bin/CMakeLists.txt @@ -11,6 +11,7 @@ add_subdirectory( exr2aces ) add_subdirectory( exrheader ) add_subdirectory( exrinfo ) add_subdirectory( exrmaketiled ) +add_subdirectory( exrmetrics ) add_subdirectory( exrstdattr ) add_subdirectory( exrmakepreview ) add_subdirectory( exrenvmap ) diff --git a/src/bin/exrmetrics/CMakeLists.txt b/src/bin/exrmetrics/CMakeLists.txt new file mode 100644 index 0000000000..8739e3169f --- /dev/null +++ b/src/bin/exrmetrics/CMakeLists.txt @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) Contributors to the OpenEXR Project. + +add_executable(exrmetrics main.cpp exrmetrics.cpp) +target_link_libraries(exrmetrics OpenEXR::OpenEXR) +set_target_properties(exrmetrics PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) +if(OPENEXR_INSTALL_TOOLS) + install(TARGETS exrmetrics DESTINATION ${CMAKE_INSTALL_BINDIR}) +endif() +if(WIN32 AND BUILD_SHARED_LIBS) + target_compile_definitions(exrmetrics PRIVATE OPENEXR_DLL) +endif() diff --git a/src/bin/exrmetrics/exrmetrics.cpp b/src/bin/exrmetrics/exrmetrics.cpp new file mode 100644 index 0000000000..d5b2dc5c6c --- /dev/null +++ b/src/bin/exrmetrics/exrmetrics.cpp @@ -0,0 +1,355 @@ + +// +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) Contributors to the OpenEXR Project. +// + +#include "exrmetrics.h" + +#include "ImfChannelList.h" +#include "ImfDeepFrameBuffer.h" +#include "ImfDeepScanLineInputPart.h" +#include "ImfDeepScanLineOutputPart.h" +#include "ImfDeepTiledInputPart.h" +#include "ImfDeepTiledOutputPart.h" +#include "ImfHeader.h" +#include "ImfInputPart.h" +#include "ImfMisc.h" +#include "ImfMultiPartInputFile.h" +#include "ImfMultiPartOutputFile.h" +#include "ImfOutputPart.h" +#include "ImfPartType.h" +#include "ImfTiledInputPart.h" +#include "ImfTiledOutputPart.h" + +#include +#include +#include +#include +#include + +using namespace Imf; +using Imath::Box2i; + +using std::cerr; +using std::clock; +using std::cout; +using std::list; +using std::runtime_error; +using std::string; +using std::to_string; +using std::vector; + +double +timing (clock_t start, clock_t end) +{ + return double (end - start) / double (CLOCKS_PER_SEC); +} + +int +channelCount (const Header& h) +{ + int channels = 0; + for (ChannelList::ConstIterator i = h.channels ().begin (); + i != h.channels ().end (); + ++i) + { + ++channels; + } + return channels; +} + +void +copyDeepTiled (DeepTiledInputPart& in, DeepTiledOutputPart& out, bool verbose) +{ + throw runtime_error ("deep tiled parts are not yet supported"); +} + +void +copyScanLine (InputPart& in, OutputPart& out, bool verbose) +{ + Box2i dw = in.header ().dataWindow (); + uint64_t width = dw.max.x + 1 - dw.min.x; + uint64_t height = dw.max.y + 1 - dw.min.y; + uint64_t numPixels = width * height; + int numChans = channelCount (in.header ()); + + vector> pixelData (numChans); + uint64_t offsetToOrigin = width * static_cast (dw.min.y) + + static_cast (dw.min.x); + + int channelNumber = 0; + int pixelSize = 0; + FrameBuffer buf; + + for (ChannelList::ConstIterator i = out.header ().channels ().begin (); + i != out.header ().channels ().end (); + ++i) + { + int samplesize = pixelTypeSize (i.channel ().type); + pixelData[channelNumber].resize (numPixels * samplesize); + + buf.insert ( + i.name (), + Slice ( + i.channel ().type, + pixelData[channelNumber].data () - offsetToOrigin * samplesize, + samplesize, + samplesize * width)); + ++channelNumber; + pixelSize += samplesize; + } + + in.setFrameBuffer (buf); + out.setFrameBuffer (buf); + clock_t startRead = clock (); + in.readPixels (dw.min.y, dw.max.y); + clock_t endRead = clock (); + + clock_t startWrite = clock (); + out.writePixels (height); + clock_t endWrite = clock (); + + cout << " \"read time\": " << timing (startRead, endRead) << ",\n"; + cout << " \"write time\": " << timing (startWrite, endWrite) << ",\n"; + cout << " \"pixel count\": " << numPixels << ",\n"; + cout << " \"raw size\": " << numPixels * pixelSize << ",\n"; +} + +void +copyTiled (const TiledInputPart& in, TiledOutputPart& out, bool verbose) +{ + throw runtime_error ("tiled parts are not yet supported"); +} + +void +copyDeepScanLine ( + DeepScanLineInputPart& in, DeepScanLineOutputPart& out, bool verbose) +{ + Box2i dw = in.header ().dataWindow (); + uint64_t width = dw.max.x + 1 - dw.min.x; + uint64_t height = dw.max.y + 1 - dw.min.y; + uint64_t numPixels = width * height; + int numChans = channelCount (in.header ()); + vector sampleCount (numPixels); + + uint64_t offsetToOrigin = width * static_cast (dw.min.y) + + static_cast (dw.min.x); + vector> pixelPtrs (numChans); + + DeepFrameBuffer buffer; + + buffer.insertSampleCountSlice (Slice ( + UINT, + (char*) (sampleCount.data () - offsetToOrigin), + sizeof (int), + sizeof (int) * width)); + int channelNumber = 0; + int bytesPerSample = 0; + for (ChannelList::ConstIterator i = out.header ().channels ().begin (); + i != out.header ().channels ().end (); + ++i) + { + pixelPtrs[channelNumber].resize (numPixels); + int samplesize = pixelTypeSize (i.channel ().type); + buffer.insert ( + i.name (), + DeepSlice ( + i.channel ().type, + (char*) (pixelPtrs[channelNumber].data () - offsetToOrigin), + sizeof (char*), + sizeof (char*) * width, + samplesize)); + ++channelNumber; + bytesPerSample += samplesize; + } + + in.setFrameBuffer (buffer); + out.setFrameBuffer (buffer); + + clock_t startCountRead = clock (); + in.readPixelSampleCounts (dw.min.y, dw.max.y); + clock_t endCountRead = clock (); + + size_t totalSamples = 0; + + for (int i: sampleCount) + { + totalSamples += i; + } + + vector> sampleData (numChans); + channelNumber = 0; + for (ChannelList::ConstIterator i = in.header ().channels ().begin (); + i != in.header ().channels ().end (); + ++i) + { + int samplesize = pixelTypeSize (i.channel ().type); + sampleData[channelNumber].resize (samplesize * totalSamples); + int offset = 0; + for (int p = 0; p < numPixels; ++p) + { + pixelPtrs[channelNumber][p] = + sampleData[channelNumber].data () + offset * samplesize; + offset += sampleCount[p]; + } + + ++channelNumber; + } + + clock_t startSampleRead = clock (); + in.readPixels (dw.min.y, dw.max.y); + clock_t endSampleRead = clock (); + + clock_t startWrite = clock (); + out.writePixels (height); + clock_t endWrite = clock (); + + cout << " \"count read time\": " << timing (startCountRead, endCountRead) + << ",\n"; + cout << " \"sample read time\": " + << timing (startSampleRead, endSampleRead) << ",\n"; + cout << " \"write time\": " << timing (startWrite, endWrite) << ",\n"; + cout << " \"pixel count\": " << numPixels << ",\n"; + cout << " \"raw size\": " + << totalSamples * bytesPerSample + numPixels * sizeof (int) << ",\n"; +} + +void +exrmetrics ( + const char inFileName[], + const char outFileName[], + int part, + Imf::Compression compression, + float level, + int halfMode, + bool verbose) +{ + MultiPartInputFile in (inFileName); + if (part >= in.parts ()) + { + throw runtime_error ((string (inFileName) + " only contains " + + to_string (in.parts ()) + + " parts. Cannot copy part " + to_string (part)) + .c_str ()); + } + Header outHeader = in.header (part); + + if (compression < NUM_COMPRESSION_METHODS) + { + outHeader.compression () = compression; + } + else { compression = outHeader.compression (); } + + if (!isinf (level) && level >= -1) + { + switch (outHeader.compression ()) + { + case DWAA_COMPRESSION: + case DWAB_COMPRESSION: + outHeader.dwaCompressionLevel () = level; + break; + case ZIP_COMPRESSION: + case ZIPS_COMPRESSION: + outHeader.zipCompressionLevel () = level; + break; + // case ZSTD_COMPRESSION : + // outHeader.zstdCompressionLevel()=level; + // break; + default: + throw runtime_error ( + "-l option only works for DWAA/DWAB,ZIP/ZIPS or ZSTD compression"); + } + } + + if (halfMode > 0) + { + for (ChannelList::Iterator i = outHeader.channels ().begin (); + i != outHeader.channels ().end (); + ++i) + { + if (halfMode == 2 || !strcmp (i.name (), "R") || + !strcmp (i.name (), "G") || !strcmp (i.name (), "B") || + !strcmp (i.name (), "A")) + { + i.channel ().type = HALF; + } + } + } + + string inCompress, outCompress; + getCompressionNameFromId (in.header (part).compression (), inCompress); + getCompressionNameFromId (outHeader.compression (), outCompress); + cout << "{\n"; + cout << " \"input file\": \"" << inFileName << "\",\n"; + cout << " \"output file\": \"" << outFileName << "\",\n"; + cout << " \"input compression\": \"" << inCompress << "\",\n"; + cout << " \"output compression\": \"" << outCompress << "\",\n"; + if (compression == ZIP_COMPRESSION || compression == ZIPS_COMPRESSION) + { + cout << " \"zipCompressionLevel\": " + << outHeader.zipCompressionLevel () << ",\n"; + } + + if (compression == DWAA_COMPRESSION || compression == DWAB_COMPRESSION) + { + cout << " \"dwaCompressionLevel\": " + << outHeader.dwaCompressionLevel () << ",\n"; + } + + std::string type = outHeader.type (); + cout << " \"part type\": \"" << type << "\",\n"; + + if (type == SCANLINEIMAGE) + { + cout << " \"scanlines per chunk:\" : " + << getCompressionNumScanlines (compression) << ",\n"; + } + + { + MultiPartOutputFile out (outFileName, &outHeader, 1); + + if (type == TILEDIMAGE) + { + TiledInputPart inpart (in, part); + TiledOutputPart outpart (out, 0); + copyTiled (inpart, outpart, verbose); + } + else if (type == SCANLINEIMAGE) + { + InputPart inpart (in, part); + OutputPart outpart (out, 0); + copyScanLine (inpart, outpart, verbose); + } + else if (type == DEEPSCANLINE) + { + DeepScanLineInputPart inpart (in, part); + DeepScanLineOutputPart outpart (out, 0); + copyDeepScanLine (inpart, outpart, verbose); + } + else if (type == DEEPTILE) + { + DeepTiledInputPart inpart (in, part); + DeepTiledOutputPart outpart (out, 0); + copyDeepTiled (inpart, outpart, verbose); + } + else + { + throw runtime_error ( + (inFileName + string (" contains unknown part type ") + type) + .c_str ()); + } + } +#ifdef __APPLE__ + struct stat instats, outstats; + stat (inFileName, &instats); + stat (outFileName, &outstats); +#else + struct stat64 instats, outstats; + stat64 (inFileName, &instats); + stat64 (outFileName, &outstats); +#endif + cout << " \"input file size\": " << instats.st_size << ",\n"; + cout << " \"output file size\": " << outstats.st_size << "\n"; + cout << "}\n"; +} diff --git a/src/bin/exrmetrics/exrmetrics.h b/src/bin/exrmetrics/exrmetrics.h new file mode 100644 index 0000000000..673d4ad78b --- /dev/null +++ b/src/bin/exrmetrics/exrmetrics.h @@ -0,0 +1,23 @@ + +#ifndef INCLUDED_EXR_METRICS_H +#define INCLUDED_EXR_METRICS_H + +//---------------------------------------------------------------------------- +// +// Copy input to output, reporting file size and timing +// +//---------------------------------------------------------------------------- + +#include "ImfCompression.h" + +extern "C" { +void exrmetrics ( + const char inFileName[], + const char outFileName[], + int part, + Imf::Compression compression, + float level, + int halfMode, + bool verbose); +} +#endif diff --git a/src/bin/exrmetrics/main.cpp b/src/bin/exrmetrics/main.cpp new file mode 100644 index 0000000000..2e6dd36f33 --- /dev/null +++ b/src/bin/exrmetrics/main.cpp @@ -0,0 +1,203 @@ + +// +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) Contributors to the OpenEXR Project. +// + +#include "exrmetrics.h" + +#include "ImfCompression.h" +#include "ImfMisc.h" + +#include +#include + +#include +#include +#include + +using std::cerr; +using std::cout; +using std::endl; +using std::ostream; +using std::vector; +using namespace Imf; + +void +usageMessage (ostream& stream, const char* program_name, bool verbose = false) +{ + stream << "Usage: " << program_name << " [options] infile outfile" << endl; + + if (verbose) + { + std::string compressionNames; + getCompressionNamesString ("/", compressionNames); + stream + << "Read a part OpenEXR image from infile, write an identical copy to outfile" + " reporting time taken to read/write and file sizes.\n" + "\n" + "Options:\n" + "\n" + " -p n part number to copy (only one part will be written to output file)\n" + " default is part 0\n" + "\n" + " -l level set DWA or ZIP compression level\n" + "\n" + " -z x sets the data compression method to x\n" + " (" + << compressionNames.c_str () + << ",\n" + " default retains original method)\n" + "\n" + " -16 rgba|all force 16 bit half float: either just RGBA, or all channels\n" + " default retains original type for all channels\n" + "\n" + " -v verbose mode\n" + "\n" + " -h, --help print this message\n" + "\n" + " --version print version information\n" + "\n"; + } +} + +int +main (int argc, char** argv) +{ + + const char* outFile = nullptr; + const char* inFile = nullptr; + bool verbose = false; + int part = 0; + float level = INFINITY; + int halfMode = 0; // 0 - leave alone, 1 - just RGBA, 2 - everything + Compression compression; + + int i = 1; + + if (argc == 1) + { + usageMessage (cerr, "exrmetrics", true); + return 1; + } + + while (i < argc) + { + if (!strcmp (argv[i], "-h") || !strcmp (argv[i], "--help")) + { + usageMessage (cout, "exrmetrics", true); + return 0; + } + + else if (!strcmp (argv[i], "--version")) + { + const char* libraryVersion = getLibraryVersion (); + + cout << "exrmetrics (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; + } + else if (!strcmp (argv[i], "-z")) + { + if (i > argc - 2) + { + cerr << "Missing compression value with -z option\n"; + return 1; + } + + getCompressionIdFromName (argv[i + 1], compression); + if (compression == Compression::NUM_COMPRESSION_METHODS) + { + cerr << "unknown compression type " << argv[i + 1] << endl; + return 1; + } + i += 2; + } + else if (!strcmp (argv[i], "-p")) + { + if (i > argc - 2) + { + cerr << "Missing part number with -p option\n"; + return 1; + } + part = atoi (argv[i + 1]); + if (part < 0) + { + cerr << "bad part " << part << " specified to -p option\n"; + return 1; + } + + i += 2; + } + else if (!strcmp (argv[i], "-l")) + { + if (i > argc - 2) + { + cerr << "Missing compression level number with -l option\n"; + return 1; + } + level = atof (argv[i + 1]); + if (level < 0) + { + cerr << "bad level " << level << " specified to -l option\n"; + return 1; + } + + i += 2; + } + else if (!strcmp (argv[i], "-16")) + { + if (i > argc - 2) + { + cerr << "Missing mode with -16 option\n"; + return 1; + } + if (!strcmp (argv[i + 1], "all")) { halfMode = 2; } + else if (!strcmp (argv[i + 1], "rgba")) { halfMode = 1; } + else + { + cerr << " bad mode for -16 option: must be 'all' or 'rgba'\n"; + return 1; + } + i += 2; + } + else if (!inFile) + { + inFile = argv[i]; + i += 1; + } + else if (!outFile) + { + outFile = argv[i]; + i += 1; + } + else + { + cerr << "unknown argument or extra filename specified\n"; + usageMessage (cerr, "exrmetrics", false); + return 1; + } + } + if (!inFile || !outFile) + { + cerr << "Missing input or output file\n"; + usageMessage (cerr, "exrmetrics", false); + return 1; + } + + try + { + exrmetrics ( + inFile, outFile, part, compression, level, halfMode, verbose); + } + catch (std::exception& what) + { + cerr << "error from exrmetrics: " << what.what () << endl; + return 1; + } + return 0; +} diff --git a/src/test/bin/CMakeLists.txt b/src/test/bin/CMakeLists.txt index 32a204ad38..49c89928fc 100644 --- a/src/test/bin/CMakeLists.txt +++ b/src/test/bin/CMakeLists.txt @@ -67,6 +67,7 @@ if(BUILD_TESTING) exrmultiview exrmultipart exrstdattr + exrmetrics ) foreach(test ${tests}) diff --git a/src/test/bin/test_exrmetrics.py b/src/test/bin/test_exrmetrics.py new file mode 100644 index 0000000000..9420fa7442 --- /dev/null +++ b/src/test/bin/test_exrmetrics.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) Contributors to the OpenEXR Project. + +import sys, os, tempfile, atexit, json +from subprocess import PIPE, run + +print(f"testing exrmetrics: {' '.join(sys.argv)}") + +exrmetrics = sys.argv[1] +image_dir = sys.argv[3] +version = sys.argv[4] + +assert(os.path.isfile(exrmetrics)), "\nMissing " + exrmetrics +assert(os.path.isdir(image_dir)), "\nMissing " + image_dir + +fd, outimage = tempfile.mkstemp(".exr") +os.close(fd) + +def cleanup(): + print(f"deleting {outimage}") +atexit.register(cleanup) + +# no args = usage message +result = run ([exrmetrics], stdout=PIPE, stderr=PIPE, universal_newlines=True) +print(" ".join(result.args)) +assert(result.returncode != 0), "\n"+result.stderr +assert(result.stderr.startswith ("Usage: ")), "\n"+result.stderr + +# -h = usage message +result = run ([exrmetrics, "-h"], stdout=PIPE, stderr=PIPE, universal_newlines=True) +print(" ".join(result.args)) +assert(result.returncode == 0), "\n"+result.stderr +assert(result.stdout.startswith ("Usage: ")), "\n"+result.stdout + +result = run ([exrmetrics, "--help"], stdout=PIPE, stderr=PIPE, universal_newlines=True) +print(" ".join(result.args)) +assert(result.returncode == 0), "\n"+result.stderr +assert(result.stdout.startswith ("Usage: ")), "\n"+result.stdout + +# --version +result = run ([exrmetrics, "--version"], stdout=PIPE, stderr=PIPE, universal_newlines=True) +print(" ".join(result.args)) +assert(result.returncode == 0), "\n"+result.stderr +assert(result.stdout.startswith ("exrmetrics")), "\n"+result.stdout +assert(version in result.stdout), "\n"+result.stdout + +# test missing arguments, using just the -option but no value + +for a in ["-p","-l","-16","-z"]: + result = run ([exrmetrics, a[0]], stdout=PIPE, stderr=PIPE, universal_newlines=True) + print(" ".join(result.args)) + print(result.stderr) + assert(result.returncode != 0), "\n"+result.stderr + +command = [exrmetrics] +image = f"{image_dir}/TestImages/GrayRampsHorizontal.exr" +command += [image, outimage] + +result = run (command, stdout=PIPE, stderr=PIPE, universal_newlines=True) +print(" ".join(result.args)) +assert(result.returncode == 0), "\n"+result.stderr +assert(os.path.isfile(outimage)), "\nMissing " + outimage + +# confirm data is valid JSON (will not be true if filename contains quotes) +data = json.loads(result.stdout) +for x in ['write time','output file size','input file size']: + assert(x in data),"\n Missing field "+x + +print("success") From 89720cb26814f809fe0674e443d758ff830efdaa Mon Sep 17 00:00:00 2001 From: Peter Hillman Date: Tue, 18 Jun 2024 15:18:17 +1200 Subject: [PATCH 2/8] fix windows build Signed-off-by: Peter Hillman --- src/bin/exrmetrics/exrmetrics.cpp | 6 ------ src/bin/exrmetrics/exrmetrics.h | 2 -- 2 files changed, 8 deletions(-) diff --git a/src/bin/exrmetrics/exrmetrics.cpp b/src/bin/exrmetrics/exrmetrics.cpp index d5b2dc5c6c..07a3dfd409 100644 --- a/src/bin/exrmetrics/exrmetrics.cpp +++ b/src/bin/exrmetrics/exrmetrics.cpp @@ -340,15 +340,9 @@ exrmetrics ( .c_str ()); } } -#ifdef __APPLE__ struct stat instats, outstats; stat (inFileName, &instats); stat (outFileName, &outstats); -#else - struct stat64 instats, outstats; - stat64 (inFileName, &instats); - stat64 (outFileName, &outstats); -#endif cout << " \"input file size\": " << instats.st_size << ",\n"; cout << " \"output file size\": " << outstats.st_size << "\n"; cout << "}\n"; diff --git a/src/bin/exrmetrics/exrmetrics.h b/src/bin/exrmetrics/exrmetrics.h index 673d4ad78b..92c880b44d 100644 --- a/src/bin/exrmetrics/exrmetrics.h +++ b/src/bin/exrmetrics/exrmetrics.h @@ -10,7 +10,6 @@ #include "ImfCompression.h" -extern "C" { void exrmetrics ( const char inFileName[], const char outFileName[], @@ -19,5 +18,4 @@ void exrmetrics ( float level, int halfMode, bool verbose); -} #endif From 88c87ea067b801801f10bfee3bf46af46522e605 Mon Sep 17 00:00:00 2001 From: Peter Hillman Date: Tue, 18 Jun 2024 16:05:49 +1200 Subject: [PATCH 3/8] fix test for parameters Signed-off-by: Peter Hillman --- src/test/bin/test_exrmetrics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/bin/test_exrmetrics.py b/src/test/bin/test_exrmetrics.py index 9420fa7442..debdb44847 100644 --- a/src/test/bin/test_exrmetrics.py +++ b/src/test/bin/test_exrmetrics.py @@ -49,7 +49,7 @@ def cleanup(): # test missing arguments, using just the -option but no value for a in ["-p","-l","-16","-z"]: - result = run ([exrmetrics, a[0]], stdout=PIPE, stderr=PIPE, universal_newlines=True) + result = run ([exrmetrics, a], stdout=PIPE, stderr=PIPE, universal_newlines=True) print(" ".join(result.args)) print(result.stderr) assert(result.returncode != 0), "\n"+result.stderr @@ -60,6 +60,7 @@ def cleanup(): result = run (command, stdout=PIPE, stderr=PIPE, universal_newlines=True) print(" ".join(result.args)) +print(result.stdout) assert(result.returncode == 0), "\n"+result.stderr assert(os.path.isfile(outimage)), "\nMissing " + outimage From be790b3de867d844fd30d7a0b81f9ff2edd518ec Mon Sep 17 00:00:00 2001 From: Peter Hillman Date: Tue, 18 Jun 2024 16:30:22 +1200 Subject: [PATCH 4/8] omit filenames from exrmetrics output to simplify JSON compatibility Signed-off-by: Peter Hillman --- src/bin/exrmetrics/exrmetrics.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/bin/exrmetrics/exrmetrics.cpp b/src/bin/exrmetrics/exrmetrics.cpp index 07a3dfd409..a6f00785e6 100644 --- a/src/bin/exrmetrics/exrmetrics.cpp +++ b/src/bin/exrmetrics/exrmetrics.cpp @@ -281,8 +281,6 @@ exrmetrics ( getCompressionNameFromId (in.header (part).compression (), inCompress); getCompressionNameFromId (outHeader.compression (), outCompress); cout << "{\n"; - cout << " \"input file\": \"" << inFileName << "\",\n"; - cout << " \"output file\": \"" << outFileName << "\",\n"; cout << " \"input compression\": \"" << inCompress << "\",\n"; cout << " \"output compression\": \"" << outCompress << "\",\n"; if (compression == ZIP_COMPRESSION || compression == ZIPS_COMPRESSION) From 1c884d412edee37ea3449f793a482863aaf2b501 Mon Sep 17 00:00:00 2001 From: Peter Hillman Date: Tue, 18 Jun 2024 17:32:13 +1200 Subject: [PATCH 5/8] better diagnostics in exrmetrics test Signed-off-by: Peter Hillman --- src/test/bin/test_exrmetrics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/bin/test_exrmetrics.py b/src/test/bin/test_exrmetrics.py index debdb44847..5debd3f830 100644 --- a/src/test/bin/test_exrmetrics.py +++ b/src/test/bin/test_exrmetrics.py @@ -61,6 +61,7 @@ def cleanup(): result = run (command, stdout=PIPE, stderr=PIPE, universal_newlines=True) print(" ".join(result.args)) print(result.stdout) +print(result.stderr) assert(result.returncode == 0), "\n"+result.stderr assert(os.path.isfile(outimage)), "\nMissing " + outimage From c2dc7bc907124c86e56adbcd00ba11ae70334064 Mon Sep 17 00:00:00 2001 From: Peter Hillman Date: Tue, 18 Jun 2024 18:09:58 +1200 Subject: [PATCH 6/8] print return code of mysteriously failing test Signed-off-by: Peter Hillman --- src/test/bin/test_exrmetrics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/bin/test_exrmetrics.py b/src/test/bin/test_exrmetrics.py index 5debd3f830..d6f5b14a09 100644 --- a/src/test/bin/test_exrmetrics.py +++ b/src/test/bin/test_exrmetrics.py @@ -60,6 +60,7 @@ def cleanup(): result = run (command, stdout=PIPE, stderr=PIPE, universal_newlines=True) print(" ".join(result.args)) +print(result.returncode) print(result.stdout) print(result.stderr) assert(result.returncode == 0), "\n"+result.stderr From 67452fae6c433c02cd95c94403959f73bbc39350 Mon Sep 17 00:00:00 2001 From: Peter Hillman Date: Wed, 19 Jun 2024 08:09:02 +1200 Subject: [PATCH 7/8] initialize compression method Signed-off-by: Peter Hillman --- src/bin/exrmetrics/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/exrmetrics/main.cpp b/src/bin/exrmetrics/main.cpp index 2e6dd36f33..d16f50016e 100644 --- a/src/bin/exrmetrics/main.cpp +++ b/src/bin/exrmetrics/main.cpp @@ -71,7 +71,7 @@ main (int argc, char** argv) int part = 0; float level = INFINITY; int halfMode = 0; // 0 - leave alone, 1 - just RGBA, 2 - everything - Compression compression; + Compression compression = Compression::NUM_COMPRESSION_METHODS; int i = 1; From c1a41a18f51311f14add17e9317cfbaf513f1f29 Mon Sep 17 00:00:00 2001 From: Peter Hillman Date: Fri, 21 Jun 2024 14:38:35 +1200 Subject: [PATCH 8/8] support tiled images, drop verbose option Signed-off-by: Peter Hillman --- src/bin/exrmetrics/exrmetrics.cpp | 265 ++++++++++++++++++++++++++++-- src/bin/exrmetrics/exrmetrics.h | 3 +- src/bin/exrmetrics/main.cpp | 8 +- 3 files changed, 251 insertions(+), 25 deletions(-) diff --git a/src/bin/exrmetrics/exrmetrics.cpp b/src/bin/exrmetrics/exrmetrics.cpp index a6f00785e6..023aa807fc 100644 --- a/src/bin/exrmetrics/exrmetrics.cpp +++ b/src/bin/exrmetrics/exrmetrics.cpp @@ -20,6 +20,7 @@ #include "ImfOutputPart.h" #include "ImfPartType.h" #include "ImfTiledInputPart.h" +#include "ImfTiledMisc.h" #include "ImfTiledOutputPart.h" #include @@ -34,6 +35,7 @@ using Imath::Box2i; using std::cerr; using std::clock; using std::cout; +using std::endl; using std::list; using std::runtime_error; using std::string; @@ -60,13 +62,7 @@ channelCount (const Header& h) } void -copyDeepTiled (DeepTiledInputPart& in, DeepTiledOutputPart& out, bool verbose) -{ - throw runtime_error ("deep tiled parts are not yet supported"); -} - -void -copyScanLine (InputPart& in, OutputPart& out, bool verbose) +copyScanLine (InputPart& in, OutputPart& out) { Box2i dw = in.header ().dataWindow (); uint64_t width = dw.max.x + 1 - dw.min.x; @@ -117,14 +113,142 @@ copyScanLine (InputPart& in, OutputPart& out, bool verbose) } void -copyTiled (const TiledInputPart& in, TiledOutputPart& out, bool verbose) +copyTiled (TiledInputPart& in, TiledOutputPart& out) { - throw runtime_error ("tiled parts are not yet supported"); + int numChans = channelCount (in.header ()); + TileDescription tiling = in.header ().tileDescription (); + + Box2i imageDw = in.header ().dataWindow (); + int totalLevels; + switch (tiling.mode) + { + case ONE_LEVEL: totalLevels = 1; //break; + case MIPMAP_LEVELS: totalLevels = in.numLevels (); break; + case RIPMAP_LEVELS: + totalLevels = in.numXLevels () * in.numYLevels (); + break; + case NUM_LEVELMODES: throw runtime_error ("unknown tile mode"); + } + + vector>> pixelData (totalLevels); + vector frameBuffer (totalLevels); + + int levelIndex = 0; + int pixelSize = 0; + size_t totalPixels = 0; + + // + // allocate memory and initialize frameBuffer for each level + // + for (int xLevel = 0; xLevel < in.numXLevels (); ++xLevel) + { + for (int yLevel = 0; yLevel < in.numYLevels (); ++yLevel) + { + if (tiling.mode == RIPMAP_LEVELS || xLevel == yLevel) + { + Box2i dw = dataWindowForLevel ( + tiling, + imageDw.min.x, + imageDw.max.x, + imageDw.min.y, + imageDw.max.y, + xLevel, + yLevel); + uint64_t width = dw.max.x + 1 - dw.min.x; + uint64_t height = dw.max.y + 1 - dw.min.y; + uint64_t numPixels = width * height; + uint64_t offsetToOrigin = + width * static_cast (dw.min.y) + + static_cast (dw.min.x); + int channelNumber = 0; + pixelSize = 0; + + pixelData[levelIndex].resize (numChans); + + for (ChannelList::ConstIterator i = + out.header ().channels ().begin (); + i != out.header ().channels ().end (); + ++i) + { + int samplesize = pixelTypeSize (i.channel ().type); + pixelData[levelIndex][channelNumber].resize ( + numPixels * samplesize); + + frameBuffer[levelIndex].insert ( + i.name (), + Slice ( + i.channel ().type, + pixelData[levelIndex][channelNumber].data () - + offsetToOrigin * samplesize, + samplesize, + samplesize * width)); + ++channelNumber; + pixelSize += samplesize; + } + totalPixels += numPixels; + ++levelIndex; + } + } + } + + clock_t startRead = clock (); + levelIndex = 0; + + for (int xLevel = 0; xLevel < in.numXLevels (); ++xLevel) + { + for (int yLevel = 0; yLevel < in.numYLevels (); ++yLevel) + { + if (tiling.mode == RIPMAP_LEVELS || xLevel == yLevel) + { + in.setFrameBuffer (frameBuffer[levelIndex]); + in.readTiles ( + 0, + in.numXTiles (xLevel) - 1, + 0, + in.numYTiles (yLevel) - 1, + xLevel, + yLevel); + ++levelIndex; + } + } + } + + clock_t endRead = clock (); + + clock_t startWrite = clock (); + levelIndex = 0; + int tileCount = 0; + + for (int xLevel = 0; xLevel < in.numXLevels (); ++xLevel) + { + for (int yLevel = 0; yLevel < in.numYLevels (); ++yLevel) + { + if (tiling.mode == RIPMAP_LEVELS || xLevel == yLevel) + { + out.setFrameBuffer (frameBuffer[levelIndex]); + out.writeTiles ( + 0, + in.numXTiles (xLevel) - 1, + 0, + in.numYTiles (yLevel) - 1, + xLevel, + yLevel); + tileCount += in.numXTiles (xLevel) * in.numYTiles (yLevel); + ++levelIndex; + } + } + } + clock_t endWrite = clock (); + + cout << " \"read time\": " << timing (startRead, endRead) << ",\n"; + cout << " \"write time\": " << timing (startWrite, endWrite) << ",\n"; + cout << " \"total tiles\": " << tileCount << ",\n"; + cout << " \"pixel count\": " << totalPixels << ",\n"; + cout << " \"raw size\": " << totalPixels * pixelSize << ",\n"; } void -copyDeepScanLine ( - DeepScanLineInputPart& in, DeepScanLineOutputPart& out, bool verbose) +copyDeepScanLine (DeepScanLineInputPart& in, DeepScanLineOutputPart& out) { Box2i dw = in.header ().dataWindow (); uint64_t width = dw.max.x + 1 - dw.min.x; @@ -215,6 +339,114 @@ copyDeepScanLine ( << totalSamples * bytesPerSample + numPixels * sizeof (int) << ",\n"; } +void +copyDeepTiled (DeepTiledInputPart& in, DeepTiledOutputPart& out) +{ + + TileDescription tiling = in.header ().tileDescription (); + + if (tiling.mode == MIPMAP_LEVELS) + { + throw runtime_error ( + "exrmetrics does not support mipmapped deep tiled parts"); + } + + if (tiling.mode == RIPMAP_LEVELS) + { + throw runtime_error ( + "exrmetrics does not support ripmapped deep tiled parts"); + } + + Box2i dw = in.header ().dataWindow (); + uint64_t width = dw.max.x + 1 - dw.min.x; + uint64_t height = dw.max.y + 1 - dw.min.y; + uint64_t numPixels = width * height; + int numChans = channelCount (in.header ()); + vector sampleCount (numPixels); + + uint64_t offsetToOrigin = width * static_cast (dw.min.y) + + static_cast (dw.min.x); + vector> pixelPtrs (numChans); + + DeepFrameBuffer buffer; + + buffer.insertSampleCountSlice (Slice ( + UINT, + (char*) (sampleCount.data () - offsetToOrigin), + sizeof (int), + sizeof (int) * width)); + int channelNumber = 0; + int bytesPerSample = 0; + for (ChannelList::ConstIterator i = out.header ().channels ().begin (); + i != out.header ().channels ().end (); + ++i) + { + pixelPtrs[channelNumber].resize (numPixels); + int samplesize = pixelTypeSize (i.channel ().type); + buffer.insert ( + i.name (), + DeepSlice ( + i.channel ().type, + (char*) (pixelPtrs[channelNumber].data () - offsetToOrigin), + sizeof (char*), + sizeof (char*) * width, + samplesize)); + ++channelNumber; + bytesPerSample += samplesize; + } + + in.setFrameBuffer (buffer); + out.setFrameBuffer (buffer); + + clock_t startCountRead = clock (); + in.readPixelSampleCounts ( + 0, in.numXTiles (0) - 1, 0, in.numYTiles (0) - 1, 0, 0); + clock_t endCountRead = clock (); + + size_t totalSamples = 0; + + for (int i: sampleCount) + { + totalSamples += i; + } + + vector> sampleData (numChans); + channelNumber = 0; + for (ChannelList::ConstIterator i = in.header ().channels ().begin (); + i != in.header ().channels ().end (); + ++i) + { + int samplesize = pixelTypeSize (i.channel ().type); + sampleData[channelNumber].resize (samplesize * totalSamples); + int offset = 0; + for (int p = 0; p < numPixels; ++p) + { + pixelPtrs[channelNumber][p] = + sampleData[channelNumber].data () + offset * samplesize; + offset += sampleCount[p]; + } + + ++channelNumber; + } + + clock_t startSampleRead = clock (); + in.readTiles (0, in.numXTiles (0) - 1, 0, in.numYTiles (0) - 1, 0, 0); + clock_t endSampleRead = clock (); + + clock_t startWrite = clock (); + out.writeTiles (0, in.numXTiles (0) - 1, 0, in.numYTiles (0) - 1, 0, 0); + clock_t endWrite = clock (); + + cout << " \"count read time\": " << timing (startCountRead, endCountRead) + << ",\n"; + cout << " \"sample read time\": " + << timing (startSampleRead, endSampleRead) << ",\n"; + cout << " \"write time\": " << timing (startWrite, endWrite) << ",\n"; + cout << " \"pixel count\": " << numPixels << ",\n"; + cout << " \"raw size\": " + << totalSamples * bytesPerSample + numPixels * sizeof (int) << ",\n"; +} + void exrmetrics ( const char inFileName[], @@ -222,8 +454,7 @@ exrmetrics ( int part, Imf::Compression compression, float level, - int halfMode, - bool verbose) + int halfMode) { MultiPartInputFile in (inFileName); if (part >= in.parts ()) @@ -311,25 +542,25 @@ exrmetrics ( { TiledInputPart inpart (in, part); TiledOutputPart outpart (out, 0); - copyTiled (inpart, outpart, verbose); + copyTiled (inpart, outpart); } else if (type == SCANLINEIMAGE) { InputPart inpart (in, part); OutputPart outpart (out, 0); - copyScanLine (inpart, outpart, verbose); + copyScanLine (inpart, outpart); } else if (type == DEEPSCANLINE) { DeepScanLineInputPart inpart (in, part); DeepScanLineOutputPart outpart (out, 0); - copyDeepScanLine (inpart, outpart, verbose); + copyDeepScanLine (inpart, outpart); } else if (type == DEEPTILE) { DeepTiledInputPart inpart (in, part); DeepTiledOutputPart outpart (out, 0); - copyDeepTiled (inpart, outpart, verbose); + copyDeepTiled (inpart, outpart); } else { diff --git a/src/bin/exrmetrics/exrmetrics.h b/src/bin/exrmetrics/exrmetrics.h index 92c880b44d..8a97085ddc 100644 --- a/src/bin/exrmetrics/exrmetrics.h +++ b/src/bin/exrmetrics/exrmetrics.h @@ -16,6 +16,5 @@ void exrmetrics ( int part, Imf::Compression compression, float level, - int halfMode, - bool verbose); + int halfMode); #endif diff --git a/src/bin/exrmetrics/main.cpp b/src/bin/exrmetrics/main.cpp index d16f50016e..a7d0a13b6e 100644 --- a/src/bin/exrmetrics/main.cpp +++ b/src/bin/exrmetrics/main.cpp @@ -33,7 +33,7 @@ usageMessage (ostream& stream, const char* program_name, bool verbose = false) std::string compressionNames; getCompressionNamesString ("/", compressionNames); stream - << "Read a part OpenEXR image from infile, write an identical copy to outfile" + << "Read an OpenEXR image from infile, write an identical copy to outfile" " reporting time taken to read/write and file sizes.\n" "\n" "Options:\n" @@ -52,8 +52,6 @@ usageMessage (ostream& stream, const char* program_name, bool verbose = false) " -16 rgba|all force 16 bit half float: either just RGBA, or all channels\n" " default retains original type for all channels\n" "\n" - " -v verbose mode\n" - "\n" " -h, --help print this message\n" "\n" " --version print version information\n" @@ -67,7 +65,6 @@ main (int argc, char** argv) const char* outFile = nullptr; const char* inFile = nullptr; - bool verbose = false; int part = 0; float level = INFINITY; int halfMode = 0; // 0 - leave alone, 1 - just RGBA, 2 - everything @@ -191,8 +188,7 @@ main (int argc, char** argv) try { - exrmetrics ( - inFile, outFile, part, compression, level, halfMode, verbose); + exrmetrics (inFile, outFile, part, compression, level, halfMode); } catch (std::exception& what) {