From 4bfb5627c23153ff4fff52d6c63f11549c924b4a Mon Sep 17 00:00:00 2001 From: Zach Lewis Date: Wed, 14 Aug 2024 09:09:31 -0400 Subject: [PATCH] feat!: implement support for OCIO NamedTransforms - ColorProcCacheKey: Update definition, signature, and usage to support caching NamedTransform processor handles - ColorConfig: add NamedTransform convenience functions: `getNumNamedTransforms`, `getNamedTransformNameByIndex`, `getNamedTransformNames`, `getNamedTransformAliases` - ColorConfig: add `createNamedTransform` function returning new ColorProcessorHandle - Add `ImageBufAlgo::ocionamedtransform` functions - oiiotool: add `--ocionamedtransform` operator - python: add NamedTransform convenience function bindings - python: add `ImageBufAlgo.ocionamedtransform(...)` bindings - Update `oiiotool` and `python` documentation with examples Signed-off-by: Zach Lewis --- src/doc/oiiotool.rst | 33 +++++ src/doc/pythonbindings.rst | 13 ++ src/include/OpenImageIO/color.h | 32 +++++ src/include/OpenImageIO/imagebufalgo.h | 44 ++++++ src/libOpenImageIO/color_ocio.cpp | 188 +++++++++++++++++++++++-- src/oiiotool/oiiotool.cpp | 26 ++++ src/python/py_colorconfig.cpp | 9 +- src/python/py_imagebufalgo.cpp | 81 +++++++++++ 8 files changed, 413 insertions(+), 13 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/include/OpenImageIO/color.h b/src/include/OpenImageIO/color.h index a88a5bdb68..32d3854918 100644 --- a/src/include/OpenImageIO/color.h +++ b/src/include/OpenImageIO/color.h @@ -144,6 +144,19 @@ class OIIO_API ColorConfig { /// Retrieve the full list of known look names, as a vector of strings. std::vector getLookNames() const; + /// Get the number of NamedTransforms defined in this configuration + int getNumNamedTransforms() const; + + /// Query the name of the specified NamedTransform. + const char* getNamedTransformNameByIndex(int index) const; + + /// Retrieve the full list of known NamedTransforms, as a vector of strings + std::vector getNamedTransformNames() const; + + /// Retrieve the full list of aliases for the named NamedTransform. + std::vector + getNamedTransformAliases(string_view named_transform) const; + /// Is the color space known to be linear? This is very conservative, and /// will return false if it's not sure. bool isColorSpaceLinear(string_view name) const; @@ -305,6 +318,25 @@ class OIIO_API ColorConfig { ColorProcessorHandle createFileTransform(ustring name, bool inverse = false) const; + /// Construct a processor to perform color transforms determined by an + /// OpenColorIO NamedTransform. It is possible that this will return an + /// empty handle if the NamedTransform doesn't exist or is not allowed. + /// + /// The handle is actually a shared_ptr, so when you're done with a + /// ColorProcess, just discard it. ColorProcessor(s) remain valid even + /// if the ColorConfig that created them no longer exists. + /// + /// Created ColorProcessors are cached, so asking for the same color + /// space transformation multiple times shouldn't be very expensive. + ColorProcessorHandle + createNamedTransform(string_view name, bool inverse = false, + string_view context_key = "", + string_view context_value = "") const; + ColorProcessorHandle + createNamedTransform(ustring name, bool inverse = false, + ustring context_key = ustring(), + ustring context_value = ustring()) const; + /// Construct a processor to perform color transforms specified by a /// 4x4 matrix. /// diff --git a/src/include/OpenImageIO/imagebufalgo.h b/src/include/OpenImageIO/imagebufalgo.h index fcc78e1aa9..9f1113aac8 100644 --- a/src/include/OpenImageIO/imagebufalgo.h +++ b/src/include/OpenImageIO/imagebufalgo.h @@ -2063,6 +2063,50 @@ bool OIIO_API ociofiletransform (ImageBuf &dst, const ImageBuf &src, ROI roi={}, int nthreads=0); +/// Return the pixels of `src` within the ROI, applying an OpenColorIO +/// "named" transform to the pixel values. In-place operations +/// (`dst` == `src`) are supported. +/// +/// The first three channels are presumed to be the color to be +/// transformed, and the fourth channel (if it exists) is presumed to be +/// alpha. Any additional channels will be simply copied unaltered. +/// +/// @param name +/// The name of the OCIO NamedTransform to apply. +/// @param unpremult +/// If true, unpremultiply the image (divide the RGB channels by +/// alpha if it exists and is nonzero) before color conversion, +/// then repremult after the after the color conversion. Passing +/// unpremult=false skips this step, which may be desirable if +/// you know that the image is "unassociated alpha" (a.k.a., +/// "not pre-multiplied colors"). +/// @param inverse +/// If `true`, it will apply the NamedTransform in the inverse +/// direction. +/// @param context_key/context_value +/// Optional key/value to establish a context (for example, a +/// shot-specific transform). +/// @param colorconfig +/// An optional `ColorConfig*` specifying an OpenColorIO +/// configuration. If not supplied, the default OpenColorIO +/// color configuration found by examining the `$OCIO` +/// environment variable will be used instead. +ImageBuf OIIO_API ocionamedtransform (const ImageBuf &src, string_view name, + bool unpremult=true, bool inverse=false, + string_view context_key="", + string_view context_value="", + const ColorConfig* colorconfig = nullptr, + ROI roi={}, int nthreads=0); +/// Write to an existing image `dst` (allocating if it is uninitialized). +bool OIIO_API ocionamedtransform (ImageBuf &dst, const ImageBuf &src, + string_view name, bool unpremult=true, + bool inverse=false, + string_view context_key="", + string_view context_value="", + const ColorConfig* colorconfig = nullptr, + ROI roi={}, int nthreads=0); + + /// @defgroup premult (Premultiply or un-premultiply color by alpha) /// @{ /// diff --git a/src/libOpenImageIO/color_ocio.cpp b/src/libOpenImageIO/color_ocio.cpp index dd48db4985..72644f09dc 100644 --- a/src/libOpenImageIO/color_ocio.cpp +++ b/src/libOpenImageIO/color_ocio.cpp @@ -76,23 +76,25 @@ 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 +102,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 +113,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 +126,7 @@ class ColorProcCacheKey { ustring display; ustring view; ustring file; + ustring namedtransform; bool inverse; size_t hash; }; @@ -1238,6 +1241,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 = getNumNamedTransforms(); 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 +1855,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 +1940,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; @@ -1953,8 +2006,8 @@ ColorConfig::createFileTransform(ustring name, bool inverse) const ustring() /*outputColorSpace*/, ustring() /*context_key*/, ustring() /*context_value*/, ustring() /*looks*/, - ustring() /*display*/, ustring() /*view*/, name, - inverse); + ustring() /*display*/, ustring() /*view*/, + ustring() /*file*/, name, inverse); ColorProcessorHandle handle = getImpl()->findproc(prockey); if (handle) return handle; @@ -1993,6 +2046,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 +2663,54 @@ 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..0c819d9860 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..0ce914b4cc 100644 --- a/src/python/py_colorconfig.cpp +++ b/src/python/py_colorconfig.cpp @@ -107,7 +107,14 @@ 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)); diff --git a/src/python/py_imagebufalgo.cpp b/src/python/py_imagebufalgo.cpp index 22db42d338..6124a8a34b 100644 --- a/src/python/py_imagebufalgo.cpp +++ b/src/python/py_imagebufalgo.cpp @@ -2128,6 +2128,66 @@ IBA_ociofiletransform_colorconfig_ret(const ImageBuf& src, +bool +IBA_ocionamedtransform(ImageBuf& dst, const ImageBuf& src, + const std::string& name, bool unpremult, bool inverse, + const std::string& context_key, + const std::string& context_value, ROI roi = ROI::All(), + int nthreads = 0) +{ + py::gil_scoped_release gil; + return ImageBufAlgo::ocionamedtransform(dst, src, name, unpremult, inverse, + context_key, context_value, NULL, + roi, nthreads); +} + + +bool +IBA_ocionamedtransform_colorconfig(ImageBuf& dst, const ImageBuf& src, + const std::string& name, bool unpremult, + bool inverse, const std::string& context_key, + const std::string& context_value, + const std::string& colorconfig = "", + ROI roi = ROI::All(), int nthreads = 0) +{ + ColorConfig config(colorconfig); + py::gil_scoped_release gil; + return ImageBufAlgo::ocionamedtransform(dst, src, name, unpremult, inverse, + context_key, context_value, &config, + roi, nthreads); +} + + + +ImageBuf +IBA_ocionamedtransform_ret(const ImageBuf& src, const std::string& name, + bool unpremult, bool inverse, + const std::string& context_key, + const std::string& context_value, + ROI roi = ROI::All(), int nthreads = 0) +{ + py::gil_scoped_release gil; + return ImageBufAlgo::ocionamedtransform(src, name, unpremult, inverse, + context_key, context_value, NULL, + roi, nthreads); +} + + +ImageBuf +IBA_ocionamedtransform_colorconfig_ret( + const ImageBuf& src, const std::string& name, bool unpremult, bool inverse, + const std::string& context_key, const std::string& context_value, + const std::string& colorconfig = "", ROI roi = ROI::All(), int nthreads = 0) +{ + ColorConfig config(colorconfig); + py::gil_scoped_release gil; + return ImageBufAlgo::ocionamedtransform(src, name, unpremult, inverse, + context_key, context_value, &config, + roi, nthreads); +} + + + py::object IBA_isConstantColor(const ImageBuf& src, float threshold, ROI roi = ROI::All(), int nthreads = 0) @@ -2825,6 +2885,27 @@ declare_imagebufalgo(py::module& m) "inverse"_a = false, "colorconfig"_a = "", "roi"_a = ROI::All(), "nthreads"_a = 0) + .def_static("ocionamedtransform", &IBA_ocionamedtransform, "dst"_a, + "src"_a, "name"_a, "unpremult"_a = true, + "inverse"_a = false, "context_key"_a = "", + "context_value"_a = "", "roi"_a = ROI::All(), + "nthreads"_a = 0) + .def_static("ocionamedtransform", &IBA_ocionamedtransform_colorconfig, + "dst"_a, "src"_a, "name"_a, "unpremult"_a = true, + "inverse"_a = false, "context_key"_a = "", + "context_value"_a = "", "colorconfig"_a = "", + "roi"_a = ROI::All(), "nthreads"_a = 0) + .def_static("ocionamedtransform", &IBA_ocionamedtransform_ret, "src"_a, + "name"_a, "unpremult"_a = true, "inverse"_a = false, + "context_key"_a = "", "context_value"_a = "", + "roi"_a = ROI::All(), "nthreads"_a = 0) + .def_static("ocionamedtransform", + &IBA_ocionamedtransform_colorconfig_ret, "src"_a, "name"_a, + "unpremult"_a = true, "inverse"_a = false, + "context_key"_a = "", "context_value"_a = "", + "colorconfig"_a = "", "roi"_a = ROI::All(), + "nthreads"_a = 0) + .def_static("computePixelStats", &IBA_computePixelStats, "src"_a, "stats"_a, "roi"_a = ROI::All(), "nthreads"_a = 0) .def_static("computePixelStats", &IBA_computePixelStats_ret, "src"_a,