From da51b32f989f908395db174de6f8cf5d7d58ad9f Mon Sep 17 00:00:00 2001 From: Zach Lewis Date: Tue, 13 Aug 2024 11:13:01 -0400 Subject: [PATCH] feat: implement support for OCIO NamedTransforms (WIP) --- src/doc/oiiotool.rst | 33 ++++++ src/doc/pythonbindings.rst | 13 +++ src/libOpenImageIO/color_ocio.cpp | 186 ++++++++++++++++++++++++++++-- src/oiiotool/oiiotool.cpp | 26 +++++ src/python/py_colorconfig.cpp | 8 +- 5 files changed, 255 insertions(+), 11 deletions(-) diff --git a/src/doc/oiiotool.rst b/src/doc/oiiotool.rst index cfda570a1e..ee9122e3af 100644 --- a/src/doc/oiiotool.rst +++ b/src/doc/oiiotool.rst @@ -4439,6 +4439,39 @@ will be printed with the command `oiiotool --colorconfiginfo`. oiiotool in.jpg --ociofiletransform footransform.csp -o out.jpg + +.. option:: --ocionamedtransform + + Replace the current image with a new image whose pixels are transformed + using the named OpenColorIO named transform. Optional appended arguments + include: + + - `key=` *name*, `value=` *str* + + Adds a key/value pair to the "context" that OpenColorIO will used + when applying the look. Multiple key/value pairs may be specified by + making each one a comma-separated list. + + - `inverse=` *val* : + + If *val* is nonzero, inverts the color transformation. + + - `unpremult=` *val* : + + If the numeric *val* is nonzero, the pixel values will be + "un-premultipled" (divided by alpha) prior to the actual color + conversion, and then re-multipled by alpha afterwards. The default is + 0, meaning the color transformation not will be automatically + bracketed by divide-by-alpha / mult-by-alpha operations. + + `:subimages=` *indices-or-names* + Include/exclude subimages (see :ref:`sec-oiiotool-subimage-modifier`). + + Examples:: + + oiiotool in.exr --ocionamedtransform:inverse=1 srgb_crv -o out.jpg + + .. option:: --unpremult Divide all color channels (those not alpha or z) of the current image by diff --git a/src/doc/pythonbindings.rst b/src/doc/pythonbindings.rst index 4de35f6210..42278b9d7d 100644 --- a/src/doc/pythonbindings.rst +++ b/src/doc/pythonbindings.rst @@ -3623,6 +3623,19 @@ Color manipulation Dst = ImageBufAlgo.ociofiletransform (Src, "foottransform.csp") +.. py:method:: ImageBuf ImageBufAlgo.ocionamedtransform (src, name, unpremult=True, inverse=False, context_key="", context_value="", colorconfig="", roi=ROI.All, nthreads=0) + bool ImageBufAlgo.ocionamedtransform (dst, src, name, unpremult=True, inverse=False, context_key="", context_value="", colorconfig="", roi=ROI.All, nthreads=0) + + Apply an OpenColorIO "named" transform to the pixel values. + + Example: + + .. code-block:: python + + Src = ImageBuf ("tahoe.dpx") + Dst = ImageBufAlgo.ocionamedtransform (Src, "log_to_lin", + context_key="SHOT", context_value="pe0012") + .. py:method:: ImageBuf ImageBufAlgo.unpremult (src, roi=ROI.All, nthreads=0) bool ImageBufAlgo.unpremult (dst, src, roi=ROI.All, nthreads=0) diff --git a/src/libOpenImageIO/color_ocio.cpp b/src/libOpenImageIO/color_ocio.cpp index dd48db4985..c7ef72a9f8 100644 --- a/src/libOpenImageIO/color_ocio.cpp +++ b/src/libOpenImageIO/color_ocio.cpp @@ -76,23 +76,26 @@ class ColorProcCacheKey { ColorProcCacheKey(ustring in, ustring out, ustring key = ustring(), ustring val = ustring(), ustring looks = ustring(), ustring display = ustring(), ustring view = ustring(), - ustring file = ustring(), bool inverse = false) + ustring file = ustring(), + ustring namedtransform = ustring(), + bool inverse = false), : inputColorSpace(in) , outputColorSpace(out) , context_key(key) , context_value(val) , looks(looks) , file(file) + , namedtransform(namedtransform) , inverse(inverse) { hash = inputColorSpace.hash() + 14033ul * outputColorSpace.hash() + 823ul * context_key.hash() + 28411ul * context_value.hash() + 1741ul * (looks.hash() + display.hash() + view.hash() - + file.hash()) + + file.hash() + namedtransform.hash()) + (inverse ? 6421 : 0); - // N.B. no separate multipliers for looks, display, view, file - // because they're never used for the same lookup. + // N.B. no separate multipliers for looks, display, view, file, + // namedtransform, because they're never used for the same lookup. } friend bool operator<(const ColorProcCacheKey& a, @@ -100,10 +103,10 @@ class ColorProcCacheKey { { return std::tie(a.hash, a.inputColorSpace, a.outputColorSpace, a.context_key, a.context_value, a.looks, a.display, - a.view, a.file, a.inverse) + a.view, a.file, a.namedtransform a.inverse) < std::tie(b.hash, b.inputColorSpace, b.outputColorSpace, b.context_key, b.context_value, b.looks, b.display, - b.view, b.file, b.inverse); + b.view, b.file, a.namedtransform, b.inverse); } friend bool operator==(const ColorProcCacheKey& a, @@ -111,10 +114,10 @@ class ColorProcCacheKey { { return std::tie(a.hash, a.inputColorSpace, a.outputColorSpace, a.context_key, a.context_value, a.looks, a.display, - a.view, a.file, a.inverse) + a.view, a.file, a.namedtransform, a.inverse) == std::tie(b.hash, b.inputColorSpace, b.outputColorSpace, b.context_key, b.context_value, b.looks, b.display, - b.view, b.file, b.inverse); + b.view, b.file, a.namedtransform, b.inverse); } ustring inputColorSpace; ustring outputColorSpace; @@ -124,6 +127,7 @@ class ColorProcCacheKey { ustring display; ustring view; ustring file; + ustring namedtransform; bool inverse; size_t hash; }; @@ -1238,6 +1242,54 @@ ColorConfig::getDisplayViewLooks(const std::string& display, +int +ColorConfig::getNumNamedTransforms() const +{ + if (getImpl()->config_ && !disable_ocio) + return getImpl()->config_->getNumNamedTransforms(); + return 0; +} + + + +const char* +ColorConfig::getNamedTransformNameByIndex(int index) const +{ + if (getImpl()->config_ && !disable_ocio) + return getImpl()->config_->getNamedTransformNameByIndex(index); + return nullptr; +} + + + +std::vector +ColorConfig::getNamedTransformNames() const +{ + std::vector result; + for (int i = 0, e = getNumLooks(); i != e; ++i) + result.emplace_back(getNamedTransformNameByIndex(i)); + return result; +} + + + +std::vector +ColorConfig::getNamedTransformAliases(string_view named_transform) const +{ + std::vector result; + auto config = getImpl()->config_; + if (config) { + auto nt = config->getNamedTransform(c_str(named_transform)); + if (nt) { + for (int i = 0, e = nt->getNumAliases(); i < e; ++i) + result.emplace_back(nt->getAlias(i)); + } + } + return result; +} + + + std::string ColorConfig::configname() const { @@ -1804,7 +1856,8 @@ ColorConfig::createLookTransform(ustring looks, ustring inputColorSpace, // exists, just return it. ColorProcCacheKey prockey(inputColorSpace, outputColorSpace, context_key, context_value, looks, ustring() /*display*/, - ustring() /*view*/, ustring() /*file*/, inverse); + ustring() /*view*/, ustring() /*file*/, + ustring() /*namedtransform*/, inverse); ColorProcessorHandle handle = getImpl()->findproc(prockey); if (handle) return handle; @@ -1888,7 +1941,8 @@ ColorConfig::createDisplayTransform(ustring display, ustring view, // exists, just return it. ColorProcCacheKey prockey(inputColorSpace, ustring() /*outputColorSpace*/, context_key, context_value, looks, display, view, - ustring() /*file*/, inverse); + ustring() /*file*/, ustring() /*namedtransform*/, + inverse); ColorProcessorHandle handle = getImpl()->findproc(prockey); if (handle) return handle; @@ -1993,6 +2047,69 @@ ColorConfig::createFileTransform(ustring name, bool inverse) const +ColorProcessorHandle +ColorConfig::createNamedTransform(string_view name, bool inverse, + string_view context_key, + string_view context_value) const +{ + return createNamedTransform(ustring(name), inverse, ustring(context_key), + ustring(context_value)); +} + + + +ColorProcessorHandle +ColorConfig::createNamedTransform(ustring name, bool inverse, + ustring context_key, + ustring context_value) const +{ + // First, look up the requested processor in the cache. If it already + // exists, just return it. + ColorProcCacheKey prockey(ustring() /*inputColorSpace*/, + ustring() /*outputColorSpace*/, context_key, + context_value, ustring() /*looks*/, + ustring() /*display*/, ustring() /*view*/, + ustring() /*file*/, name, inverse); + ColorProcessorHandle handle = getImpl()->findproc(prockey); + if (handle) + return handle; + + // Ask OCIO to make a Processor that can handle the requested + // transformation. + if (getImpl()->config_ && !disable_ocio) { + OCIO::ConstConfigRcPtr config = getImpl()->config_; + auto transform = config->getNamedTransform(name.c_str()); + OCIO::TransformDirection dir = inverse ? OCIO::TRANSFORM_DIR_INVERSE + : OCIO::TRANSFORM_DIR_FORWARD; + auto context = config->getCurrentContext(); + auto keys = Strutil::splits(context_key, ","); + auto values = Strutil::splits(context_value, ","); + if (keys.size() && values.size() && keys.size() == values.size()) { + OCIO::ContextRcPtr ctx = context->createEditableCopy(); + for (size_t i = 0; i < keys.size(); ++i) + ctx->setStringVar(keys[i].c_str(), values[i].c_str()); + context = ctx; + } + + OCIO::ConstProcessorRcPtr p; + try { + // Get the processor corresponding to this transform. + p = config->getProcessor(context, transform, dir); + getImpl()->clear_error(); + handle = ColorProcessorHandle(new ColorProcessor_OCIO(p)); + } catch (OCIO::Exception& e) { + getImpl()->error(e.what()); + } catch (...) { + getImpl()->error( + "An unknown error occurred in OpenColorIO, getProcessor"); + } + } + + return getImpl()->addproc(prockey, handle); +} + + + ColorProcessorHandle ColorConfig::createMatrixTransform(M44fParam M, bool inverse) const { @@ -2547,6 +2664,55 @@ ImageBufAlgo::ociofiletransform(const ImageBuf& src, string_view name, +bool +ImageBufAlgo::ocionamedtransform(ImageBuf& dst, const ImageBuf& src, + string_view name, bool unpremult, + bool inverse, string_view key, + string_view value, + const ColorConfig* colorconfig, ROI roi, + int nthreads) +{ + pvt::LoggedTimer logtime("IBA::ocionamedtransform"); + ColorProcessorHandle processor; + { + if (!colorconfig) + colorconfig = &ColorConfig::default_colorconfig(); + processor + = colorconfig->createNamedTransform(name, inverse, key, value); + if (!processor) { + if (colorconfig->has_error()) + dst.errorfmt("{}", colorconfig->geterror()); + else + dst.errorfmt( + "Could not construct the color transform (unknown error)"); + return false; + } + } + + logtime.stop(); // transition to colorconvert + bool ok = colorconvert(dst, src, processor.get(), unpremult, roi, nthreads); + return ok; +} + + + +ImageBuf +ImageBufAlgo::ocionamedtransform(const ImageBuf& src, string_view name, + bool unpremult, bool inverse, string_view key, + string_view value, + const ColorConfig* colorconfig, ROI roi, + int nthreads) +{ + ImageBuf result; + bool ok = ocionamedtransform(result, src, name, unpremult, inverse, + key, value, colorconfig, roi, nthreads); + if (!ok && !result.has_error()) + result.errorfmt("ImageBufAlgo::ocionamedtransform() error"); + return result; +} + + + bool ImageBufAlgo::colorconvert(span color, const ColorProcessor* processor, bool unpremult) diff --git a/src/oiiotool/oiiotool.cpp b/src/oiiotool/oiiotool.cpp index b266ea641d..c7e1d45241 100644 --- a/src/oiiotool/oiiotool.cpp +++ b/src/oiiotool/oiiotool.cpp @@ -2385,6 +2385,20 @@ OIIOTOOL_OP(ociofiletransform, 1, [&](OiiotoolOp& op, span img) { +// --ocionamedtransform +OIIOTOOL_OP(ocionamedtransform, 1, [&](OiiotoolOp& op, span img) { + string_view name = op.args(1); + std::string contextkey = op.options()["key"]; + std::string contextvalue = op.options()["value"]; + bool unpremult = op.options().get_int("unpremult"); + bool inverse = op.options().get_int("inverse"); + return ImageBufAlgo::ocionamedtransform(*img[0], *img[1], name, unpremult, + inverse, contextkey, contextvalue, + &ot.colorconfig); +}); + + + static void output_tiles(Oiiotool& ot, cspan) { @@ -6015,6 +6029,15 @@ print_ocio_info(Oiiotool& ot, std::ostream& out) out << "\n"; } } + + int nnamed_transforms = ot.colorconfig.getNumNamedTransforms(); + if (nnamed_transforms) { + out << "Named transforms:\n"; + for (int i = 0; i < nnamed_transforms; ++i) { + const char* x = ot.colorconfig.getNamedTransformNameByIndex(i); + out << " - " << quote_if_spaces(x) << "\n"; + } + } if (!ot.colorconfig.supportsOpenColorIO()) out << "No OpenColorIO support was enabled at build time.\n"; } @@ -6848,6 +6871,9 @@ Oiiotool::getargs(int argc, char* argv[]) ap.arg("--ociofiletransform %s:FILENAME") .help("Apply the named OCIO filetransform (options: inverse=, unpremult=)") .OTACTION(action_ociofiletransform); + ap.arg("--ocionamedtransform %s:NAME") + .help("Apply the named OCIO namedtransform (options: inverse=, key=, value=, unpremult=)") + .OTACTION(action_ocionamedtransform); ap.arg("--unpremult") .help("Divide all color channels of the current image by the alpha to \"un-premultiply\"") .OTACTION(action_unpremult); diff --git a/src/python/py_colorconfig.cpp b/src/python/py_colorconfig.cpp index 662d48704c..4d5700833a 100644 --- a/src/python/py_colorconfig.cpp +++ b/src/python/py_colorconfig.cpp @@ -107,7 +107,13 @@ declare_colorconfig(py::module& m) [](const ColorConfig& self, const std::string& color_space) { return self.getAliases(color_space); }) - + .def("getNumNamedTransforms", &ColorConfig::getNumNamedTransforms) + .def("getNamedTransformNameByIndex", &ColorConfig::getNamedTransformNameByIndex) + .def("getNamedTransformNames", &ColorConfig::getNamedTransformNames) + .def("getNamedTransformAliases", + [](const ColorConfig& self, const std::string& named_transform) { + return self.getNamedTransformAliases(named_transform); + }) .def("getColorSpaceFromFilepath", [](const ColorConfig& self, const std::string& str) { return std::string(self.getColorSpaceFromFilepath(str));