diff --git a/src/OpenColorIO/ops/lut1d/Lut1DOp.cpp b/src/OpenColorIO/ops/lut1d/Lut1DOp.cpp index 412462414b..06fd2c19b4 100644 --- a/src/OpenColorIO/ops/lut1d/Lut1DOp.cpp +++ b/src/OpenColorIO/ops/lut1d/Lut1DOp.cpp @@ -16,6 +16,7 @@ #include "ops/lut1d/Lut1DOpCPU.h" #include "ops/lut1d/Lut1DOpGPU.h" #include "ops/matrix/MatrixOp.h" +#include "ops/range/RangeOp.h" #include "ops/OpTools.h" #include "SSE.h" #include "transforms/Lut1DTransform.h" @@ -119,12 +120,35 @@ void Lut1DOp::combineWith(OpRcPtrVec & ops, ConstOpRcPtr & secondOp) const ConstLut1DOpRcPtr typedRcPtr = DynamicPtrCast(secondOp); auto secondLut = typedRcPtr->lut1DData(); - // We want compose to upsample the LUTs to minimize precision loss. - const auto compFlag = Lut1DOpData::COMPOSE_RESAMPLE_BIG; - auto thisLut = lut1DData(); - Lut1DOpDataRcPtr result = Lut1DOpData::Compose(thisLut, secondLut, compFlag); - auto composedOp = std::make_shared(result); - ops.push_back(composedOp); + // We are trying to combine a pair of forward/inverse LUTs. + // Use similair logic than OpOptimizers::RemoveInverseOps to replace the + // pair by a clamp where appropriate. + if (isInverse(secondOp)) + { + auto opData = lut1DData()->getPairIdentityReplacement(secondLut); + + if (opData->getType() == OpData::MatrixType) + { + // No-op that will be optimized. + auto mat = OCIO_DYNAMIC_POINTER_CAST(opData); + CreateMatrixOp(ops, mat, TRANSFORM_DIR_FORWARD); + } + else if (opData->getType() == OpData::RangeType) + { + // Clamping op. + auto range = OCIO_DYNAMIC_POINTER_CAST(opData); + CreateRangeOp(ops, range, TRANSFORM_DIR_FORWARD); + } + } + else + { + // We want compose to upsample the LUTs to minimize precision loss. + const auto compFlag = Lut1DOpData::COMPOSE_RESAMPLE_BIG; + auto thisLut = lut1DData(); + Lut1DOpDataRcPtr result = Lut1DOpData::Compose(thisLut, secondLut, compFlag); + auto composedOp = std::make_shared(result); + ops.push_back(composedOp); + } } bool Lut1DOp::hasChannelCrosstalk() const diff --git a/tests/cpu/OpOptimizers_tests.cpp b/tests/cpu/OpOptimizers_tests.cpp index 6f606fd6e8..077bbf623b 100644 --- a/tests/cpu/OpOptimizers_tests.cpp +++ b/tests/cpu/OpOptimizers_tests.cpp @@ -368,6 +368,32 @@ OCIO_ADD_TEST(OpOptimizers, combine_ops) OCIO::CombineOps(ops, OCIO::OPTIMIZATION_ALL); OCIO_CHECK_EQUAL(ops.size(), 0); } + + { + // When a pair of forward / inverse LUTs with non 0 to 1 domain are used + // as process space for a Look (eg. CDL), the Optimizer tries to combine + // them when the Look results in a no-op. Here we make sure this result + // in an appropriate clamp instead of a new half-domain LUT resulting + // from the naive composition of the two LUTs. + + OCIO::OpRcPtrVec ops; + const std::string fileName("lut1d_4.spi1d"); + OCIO::ContextRcPtr context = OCIO::Context::Create(); + OCIO_CHECK_NO_THROW(OCIO::BuildOpsTest(ops, fileName, context, + OCIO::TRANSFORM_DIR_INVERSE)); + const double exp_null[4] = {1.0, 1.0, 1.0, 1.0}; + OCIO::CreateExponentOp(ops, exp_null, OCIO::TRANSFORM_DIR_FORWARD); + OCIO_CHECK_NO_THROW(OCIO::BuildOpsTest(ops, fileName, context, + OCIO::TRANSFORM_DIR_FORWARD)); + + OCIO_CHECK_NO_THROW(ops.finalize()); + OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_ALL)); + OCIO_CHECK_EQUAL(ops.size(), 1); + OCIO::ConstOpRcPtr op = ops[0]; + OCIO_REQUIRE_ASSERT(op); + auto range = OCIO_DYNAMIC_POINTER_CAST(op->data()); + OCIO_REQUIRE_ASSERT(range); + } } OCIO_ADD_TEST(OpOptimizers, non_optimizable)