Files
OpenColorIO/tests/cpu/OpOptimizers_tests.cpp
2023-01-04 16:00:36 -05:00

1563 lines
61 KiB
C++

// SPDX-License-Identifier: BSD-3-Clause
// Copyright Contributors to the OpenColorIO Project.
#include "OpOptimizers.cpp"
#include "ops/cdl/CDLOp.h"
#include "ops/exponent/ExponentOp.h"
#include "ops/exposurecontrast/ExposureContrastOp.h"
#include "ops/fixedfunction/FixedFunctionOp.h"
#include "ops/gamma/GammaOp.h"
#include "ops/log/LogOp.h"
#include "ops/matrix/MatrixOp.h"
#include "ops/range/RangeOp.h"
#include "testutils/UnitTest.h"
#include "transforms/FileTransform.h"
#include "UnitTestUtils.h"
namespace OCIO = OCIO_NAMESPACE;
namespace
{
OCIO::OptimizationFlags AllBut(OCIO::OptimizationFlags notFlag)
{
return static_cast<OCIO::OptimizationFlags>(OCIO::OPTIMIZATION_ALL & ~notFlag);
}
void CompareRender(OCIO::OpRcPtrVec & ops1, OCIO::OpRcPtrVec & ops2,
unsigned line, float errorThreshold,
bool forceAlphaInRange = false)
{
std::vector<float> img1 = {
0.778f, 0.824f, 0.885f, 0.153f,
0.044f, 0.014f, 0.088f, 0.999f,
0.488f, 0.381f, 0.f, 0.f,
1.000f, 1.52e-4f, 0.0229f, 1.f,
0.f, -0.1f, -2.f, -0.1f,
2.f, 1.9f, 0.f, 2.f };
if (forceAlphaInRange)
{
img1[19] = 0.f;
}
std::vector<float> img2 = img1;
const long nbPixels = (long)img1.size() / 4;
for (const auto & op : ops1)
{
op->apply(&img1[0], &img1[0], nbPixels);
}
for (const auto & op : ops2)
{
op->apply(&img2[0], &img2[0], nbPixels);
}
for (size_t idx = 0; idx < img1.size(); ++idx)
{
OCIO_CHECK_CLOSE_FROM(img1[idx], img2[idx], errorThreshold, line);
}
}
} // namespace
OCIO_ADD_TEST(OpOptimizers, remove_leading_clamp_identity)
{
OCIO::OpRcPtrVec ops;
auto range = std::make_shared<OCIO::RangeOpData>(0., 1., 0., 1.);
auto range2 = std::make_shared<OCIO::RangeOpData>(0., 1., 0., 2.);
auto matrix = std::make_shared<OCIO::MatrixOpData>();
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateMatrixOp(ops, matrix, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_EQUAL(ops.size(), 4);
OCIO::RemoveLeadingClampIdentity(ops);
OCIO_REQUIRE_EQUAL(ops.size(), 1);
OCIO::ConstOpRcPtr o0 = ops[0];
OCIO_CHECK_EQUAL(o0->data()->getType(), OCIO::OpData::MatrixType);
ops.clear();
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_EQUAL(ops.size(), 3);
OCIO::RemoveLeadingClampIdentity(ops);
OCIO_CHECK_EQUAL(ops.size(), 0);
ops.clear();
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateMatrixOp(ops, matrix, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_EQUAL(ops.size(), 4);
OCIO::RemoveLeadingClampIdentity(ops);
OCIO_REQUIRE_EQUAL(ops.size(), 3);
o0 = ops[0];
OCIO::ConstOpRcPtr o1 = ops[1];
OCIO::ConstOpRcPtr o2 = ops[2];
OCIO_CHECK_EQUAL(o0->data()->getType(), OCIO::OpData::MatrixType);
OCIO_CHECK_EQUAL(o1->data()->getType(), OCIO::OpData::RangeType);
OCIO_CHECK_EQUAL(o2->data()->getType(), OCIO::OpData::RangeType);
ops.clear();
// First range is not an identity, nothing to remove.
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range2, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateMatrixOp(ops, matrix, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_EQUAL(ops.size(), 4);
OCIO::RemoveLeadingClampIdentity(ops);
OCIO_REQUIRE_EQUAL(ops.size(), 4);
o0 = ops[0];
o1 = ops[1];
o2 = ops[2];
OCIO::ConstOpRcPtr o3 = ops[3];
OCIO_CHECK_EQUAL(o0->data()->getType(), OCIO::OpData::RangeType);
OCIO_CHECK_EQUAL(o1->data()->getType(), OCIO::OpData::MatrixType);
OCIO_CHECK_EQUAL(o2->data()->getType(), OCIO::OpData::RangeType);
OCIO_CHECK_EQUAL(o3->data()->getType(), OCIO::OpData::RangeType);
ops.clear();
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range2, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateMatrixOp(ops, matrix, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_EQUAL(ops.size(), 4);
OCIO::RemoveLeadingClampIdentity(ops);
OCIO_REQUIRE_EQUAL(ops.size(), 2);
o0 = ops[0];
o1 = ops[1];
OCIO_CHECK_EQUAL(o0->data()->getType(), OCIO::OpData::RangeType);
OCIO_CHECK_EQUAL(o1->data()->getType(), OCIO::OpData::MatrixType);
ops.clear();
}
OCIO_ADD_TEST(OpOptimizers, remove_trailing_clamp_identity)
{
OCIO::OpRcPtrVec ops;
auto range = std::make_shared<OCIO::RangeOpData>(0., 1., 0., 1.);
auto range2 = std::make_shared<OCIO::RangeOpData>(0., 1., 0., 2.);
auto matrix = std::make_shared<OCIO::MatrixOpData>();
OCIO_CHECK_NO_THROW(OCIO::CreateMatrixOp(ops, matrix, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_EQUAL(ops.size(), 4);
OCIO::RemoveTrailingClampIdentity(ops);
OCIO_REQUIRE_EQUAL(ops.size(), 1);
OCIO::ConstOpRcPtr o0 = ops[0];
OCIO_CHECK_EQUAL(o0->data()->getType(), OCIO::OpData::MatrixType);
ops.clear();
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_EQUAL(ops.size(), 3);
OCIO::RemoveTrailingClampIdentity(ops);
OCIO_CHECK_EQUAL(ops.size(), 0);
ops.clear();
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateMatrixOp(ops, matrix, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_EQUAL(ops.size(), 4);
OCIO::RemoveTrailingClampIdentity(ops);
OCIO_REQUIRE_EQUAL(ops.size(), 2);
o0 = ops[0];
OCIO::ConstOpRcPtr o1 = ops[1];
OCIO_CHECK_EQUAL(o0->data()->getType(), OCIO::OpData::RangeType);
OCIO_CHECK_EQUAL(o1->data()->getType(), OCIO::OpData::MatrixType);
ops.clear();
// Last range is not an identity, nothing to remove.
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range2, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateMatrixOp(ops, matrix, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range2, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_EQUAL(ops.size(), 4);
OCIO::RemoveTrailingClampIdentity(ops);
OCIO_REQUIRE_EQUAL(ops.size(), 4);
o0 = ops[0];
o1 = ops[1];
OCIO::ConstOpRcPtr o2 = ops[2];
OCIO::ConstOpRcPtr o3 = ops[3];
OCIO_CHECK_EQUAL(o0->data()->getType(), OCIO::OpData::RangeType);
OCIO_CHECK_EQUAL(o1->data()->getType(), OCIO::OpData::MatrixType);
OCIO_CHECK_EQUAL(o2->data()->getType(), OCIO::OpData::RangeType);
OCIO_CHECK_EQUAL(o3->data()->getType(), OCIO::OpData::RangeType);
ops.clear();
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range2, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateMatrixOp(ops, matrix, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(ops, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_EQUAL(ops.size(), 4);
OCIO::RemoveTrailingClampIdentity(ops);
OCIO_REQUIRE_EQUAL(ops.size(), 2);
o0 = ops[0];
o1 = ops[1];
OCIO_CHECK_EQUAL(o0->data()->getType(), OCIO::OpData::RangeType);
OCIO_CHECK_EQUAL(o1->data()->getType(), OCIO::OpData::MatrixType);
ops.clear();
}
OCIO_ADD_TEST(OpOptimizers, remove_inverse_ops)
{
OCIO::OpRcPtrVec ops;
auto func = std::make_shared<OCIO::FixedFunctionOpData>(
OCIO::FixedFunctionOpData::ACES_RED_MOD_03_FWD);
const double logSlope[3] = {0.18, 0.18, 0.18};
const double linSlope[3] = {2.0, 2.0, 2.0};
const double linOffset[3] = {0.1, 0.1, 0.1};
const double base = 10.0;
const double logOffset[3] = {1.0, 1.0, 1.0};
OCIO::CreateFixedFunctionOp(ops, func, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateLogOp(ops, base, logSlope, logOffset, linSlope, linOffset,
OCIO::TRANSFORM_DIR_INVERSE);
OCIO::CreateLogOp(ops, base, logSlope, logOffset, linSlope, linOffset,
OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateFixedFunctionOp(ops, func, OCIO::TRANSFORM_DIR_INVERSE);
OCIO_CHECK_EQUAL(ops.size(), 4);
// Inverse + forward log are optimized as no-op then forward and inverse exponent are
// optimized as no-op within the same call.
OCIO::RemoveInverseOps(ops, OCIO::OPTIMIZATION_ALL);
OCIO_CHECK_EQUAL(ops.size(), 0);
ops.clear();
OCIO::CreateFixedFunctionOp(ops, func, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateLogOp(ops, base, logSlope, logOffset, linSlope, linOffset,
OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateLogOp(ops, base, logSlope, logOffset, linSlope, linOffset,
OCIO::TRANSFORM_DIR_INVERSE);
OCIO::CreateFixedFunctionOp(ops, func, OCIO::TRANSFORM_DIR_INVERSE);
OCIO_CHECK_EQUAL(ops.size(), 4);
// Forward + inverse log are optimized as a clamping range that stays between
// forward and inverse exponents.
OCIO::RemoveInverseOps(ops, OCIO::OPTIMIZATION_ALL);
OCIO_REQUIRE_EQUAL(ops.size(), 3);
OCIO_CHECK_EQUAL(ops[0]->getInfo(), "<FixedFunctionOp>");
OCIO_CHECK_EQUAL(ops[1]->getInfo(), "<RangeOp>");
OCIO_CHECK_EQUAL(ops[2]->getInfo(), "<FixedFunctionOp>");
ops.clear();
OCIO::CreateFixedFunctionOp(ops, func, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateFixedFunctionOp(ops, func, OCIO::TRANSFORM_DIR_INVERSE);
OCIO::CreateLogOp(ops, base, logSlope, logOffset, linSlope, linOffset,
OCIO::TRANSFORM_DIR_INVERSE);
OCIO::CreateLogOp(ops, base, logSlope, logOffset, linSlope, linOffset,
OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateFixedFunctionOp(ops, func, OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_EQUAL(ops.size(), 5);
OCIO::RemoveInverseOps(ops, OCIO::OPTIMIZATION_ALL);
OCIO_REQUIRE_EQUAL(ops.size(), 1);
OCIO_CHECK_EQUAL(ops[0]->getInfo(), "<FixedFunctionOp>");
}
OCIO_ADD_TEST(OpOptimizers, combine_ops)
{
double m1[4] = {2.0, 2.0, 2.0, 1.0};
double m2[4] = {0.5, 0.5, 0.5, 1.0};
double m3[4] = {0.6, 0.6, 0.6, 1.0};
double m4[4] = {0.7, 0.7, 0.7, 1.0};
const double exp[4] = {1.2, 1.3, 1.4, 1.5};
{
OCIO::OpRcPtrVec ops;
OCIO::CreateScaleOp(ops, m1, OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_EQUAL(ops.size(), 1);
OCIO::CombineOps(ops, OCIO::OPTIMIZATION_ALL);
OCIO_CHECK_EQUAL(ops.size(), 1);
}
{
OCIO::OpRcPtrVec ops;
OCIO::CreateScaleOp(ops, m1, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateScaleOp(ops, m3, OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_EQUAL(ops.size(), 2);
OCIO::CombineOps(ops, AllBut(OCIO::OPTIMIZATION_COMP_MATRIX));
OCIO_CHECK_EQUAL(ops.size(), 2);
OCIO::CombineOps(ops, OCIO::OPTIMIZATION_ALL);
OCIO_CHECK_EQUAL(ops.size(), 1);
}
{
OCIO::OpRcPtrVec ops;
OCIO::CreateScaleOp(ops, m1, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateScaleOp(ops, m3, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateScaleOp(ops, m4, OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_EQUAL(ops.size(), 3);
OCIO::CombineOps(ops, AllBut(OCIO::OPTIMIZATION_COMP_MATRIX));
OCIO_CHECK_EQUAL(ops.size(), 3);
OCIO::CombineOps(ops, OCIO::OPTIMIZATION_ALL);
OCIO_CHECK_EQUAL(ops.size(), 1);
}
{
OCIO::OpRcPtrVec ops;
OCIO::CreateScaleOp(ops, m1, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateScaleOp(ops, m2, OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_EQUAL(ops.size(), 2);
OCIO::CombineOps(ops, AllBut(OCIO::OPTIMIZATION_COMP_MATRIX));
OCIO_CHECK_EQUAL(ops.size(), 2);
OCIO::CombineOps(ops, OCIO::OPTIMIZATION_ALL);
OCIO_CHECK_EQUAL(ops.size(), 0);
}
{
OCIO::OpRcPtrVec ops;
OCIO::CreateScaleOp(ops, m1, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateScaleOp(ops, m1, OCIO::TRANSFORM_DIR_INVERSE);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_EQUAL(ops.size(), 2);
OCIO::CombineOps(ops, AllBut(OCIO::OPTIMIZATION_COMP_MATRIX));
OCIO_CHECK_EQUAL(ops.size(), 2);
OCIO::CombineOps(ops, OCIO::OPTIMIZATION_ALL);
OCIO_CHECK_EQUAL(ops.size(), 0);
}
{
OCIO::OpRcPtrVec ops;
OCIO::CreateScaleOp(ops, m1, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateScaleOp(ops, m1, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateScaleOp(ops, m1, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateScaleOp(ops, m1, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateScaleOp(ops, m1, OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_EQUAL(ops.size(), 5);
OCIO::CombineOps(ops, AllBut(OCIO::OPTIMIZATION_COMP_MATRIX));
OCIO_CHECK_EQUAL(ops.size(), 5);
OCIO::CombineOps(ops, OCIO::OPTIMIZATION_ALL);
OCIO_CHECK_EQUAL(ops.size(), 1);
}
{
OCIO::OpRcPtrVec ops;
OCIO::CreateExponentOp(ops, exp, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateScaleOp(ops, m1, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateScaleOp(ops, m2, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateExponentOp(ops, exp, OCIO::TRANSFORM_DIR_INVERSE);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_EQUAL(ops.size(), 4);
OCIO::CombineOps(ops, AllBut(OCIO::OPTIMIZATION_COMP_MATRIX));
OCIO_CHECK_EQUAL(ops.size(), 4);
OCIO::CombineOps(ops, OCIO::OPTIMIZATION_ALL);
OCIO_CHECK_EQUAL(ops.size(), 0);
}
}
OCIO_ADD_TEST(OpOptimizers, non_optimizable)
{
OCIO::OpRcPtrVec ops;
// Create non identity Matrix.
const double m44[16] = { 2., 0., 0., 0.,
0., 1., 0., 0.,
0., 0., 1., 0.,
0., 0., 0., 1. };
const double offset4[4] = { 0., 0., 0., 0. };
OCIO::CreateMatrixOffsetOp(ops, m44, offset4, OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_EQUAL(ops.size(), 1);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(ops.size(), 1);
OCIO::ConstOpRcPtr op = ops[0];
OCIO_REQUIRE_ASSERT(op);
auto mat = OCIO_DYNAMIC_POINTER_CAST<const OCIO::MatrixOpData>(op->data());
OCIO_REQUIRE_ASSERT(mat);
OCIO_CHECK_EQUAL(mat->getArray().getValues()[0], 2.);
OCIO_CHECK_ASSERT(mat->isDiagonal());
}
OCIO_ADD_TEST(OpOptimizers, optimizable)
{
OCIO::OpRcPtrVec ops;
// Create identity Matrix.
double m44[16] = { 1., 0., 0., 0.,
0., 1., 0., 0.,
0., 0., 1., 0.,
0., 0., 0., 1. };
const double offset4[4] = { 0., 0., 0., 0. };
OCIO::CreateMatrixOffsetOp(ops, m44, offset4, OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_EQUAL(ops.size(), 1);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_DEFAULT));
// Identity matrix is a no-op and is removed. CPU processor will re-add an identity matrix
// if there are no ops left.
OCIO_CHECK_EQUAL(ops.size(), 0);
ops.clear();
// Add identity matrix.
OCIO::CreateMatrixOffsetOp(ops, m44, offset4, OCIO::TRANSFORM_DIR_FORWARD);
// No more an 'identity matrix'.
m44[0] = 2.;
m44[1] = 2.;
OCIO::CreateMatrixOffsetOp(ops, m44, offset4, OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_EQUAL(ops.size(), 2);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(ops.size(), 1);
OCIO::ConstOpRcPtr op = ops[0];
OCIO_REQUIRE_ASSERT(op);
auto mat = OCIO_DYNAMIC_POINTER_CAST<const OCIO::MatrixOpData>(op->data());
OCIO_REQUIRE_ASSERT(mat);
OCIO_CHECK_ASSERT(!mat->isIdentity());
OCIO_CHECK_ASSERT(!mat->isDiagonal());
}
OCIO_ADD_TEST(OpOptimizers, optimization)
{
// This is a transform consisting of a Lut1d, Matrix, Matrix, Lut1d.
// The matrices and luts are inverses of one another, so when they are
// composed they become identities which are then replaced.
// So this one test actually tests quite a lot of the optimize and compose
// functionality.
const std::string fileName("opt_test1.ctf");
OCIO::OpRcPtrVec ops;
OCIO::ContextRcPtr context = OCIO::Context::Create();
OCIO_CHECK_NO_THROW(OCIO::BuildOpsTest(ops, fileName, context,
OCIO::TRANSFORM_DIR_FORWARD));
// First one is the file no op.
OCIO_CHECK_EQUAL(ops.size(), 5);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_NONE));
OCIO_CHECK_EQUAL(ops.size(), 4);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO_CHECK_EQUAL(optOps.size(), 4);
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
// Compare renders.
CompareRender(ops, optOps, __LINE__, 1e-6f);
}
OCIO_ADD_TEST(OpOptimizers, optimization2)
{
// This transform has the following ops:
// 1 Lut1D, half domain, effectively an identity
// 2 Matrix, bit depth conversion identity
// 3 Matrix, bit depth conversion identity
// 4 Matrix, almost identity
// 5 Range, clamp identity
// 6 Lut1D, half domain, raw halfs, identity
// 7 Lut1D, raw halfs, identity
// 8 Matrix, not identity
// 9 Matrix, not identity
// 10 Lut1D, almost identity
// 11 Lut1D, almost identity that composes to an identity with the previous one
// 12 Lut3D, not identity
// 13 Lut3D, not identity but composes to an identity with the previous one
const std::string fileName("opt_test2.ctf");
OCIO::OpRcPtrVec ops;
OCIO::ContextRcPtr context = OCIO::Context::Create();
OCIO_CHECK_NO_THROW(OCIO::BuildOpsTest(ops, fileName, context,
OCIO::TRANSFORM_DIR_FORWARD));
// First one is the file no op.
OCIO_CHECK_EQUAL(ops.size(), 14);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_NONE));
OCIO_CHECK_EQUAL(ops.size(), 13);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO_CHECK_EQUAL(optOps.size(), 13);
// No need to remove OPTIMIZATION_COMP_SEPARABLE_PREFIX because optimization is for F32.
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_GOOD));
OCIO_REQUIRE_EQUAL(optOps.size(), 4);
// Op 1 is exactly an identity except for the first value which is 0.000001. Since the
// outDepth=16i, this gets normalized by 1/65536, which puts it well under the noise
// threshold so it's optimized it out as an identity.
// Ops 2 & 3 are identities and removed.
// This is op 4.
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<MatrixOffsetOp>");
// Op 5 is a clamp identity and removed.
// Ops 6 & 7 are identities and are replaced by ranges, one is removed.
OCIO_CHECK_EQUAL(optOps[1]->getInfo(), "<RangeOp>");
// This is op 8 composed with op 9.
OCIO_CHECK_EQUAL(optOps[2]->getInfo(), "<MatrixOffsetOp>");
// Ops 10 & 11 composed become an identity, is replaced with a range,
// which is then removed as a clamp identity
// This is op 12 composed with op 13. It is an identity.
// NB: We don't try to detect Lut3DOp identities.
OCIO_CHECK_EQUAL(optOps[3]->getInfo(), "<Lut3DOp>");
// Compare renders.
CompareRender(ops, optOps, __LINE__, 1e-6f);
}
OCIO_ADD_TEST(OpOptimizers, lut1d_identities)
{
// This transform has the following ops:
// 1 Lut1D, identity
// 2 Lut1D, identity
// 3 Lut1D, not quite identity
// 4 Lut1D, half-domain identity (note 16i outDepth)
// 5 Lut1D, half-domain not an identity (values clamped due to rawHalfs encoding)
// 6 Lut1D, identity
const std::string fileName("lut1d_identity_test.ctf");
OCIO::OpRcPtrVec ops;
OCIO::ContextRcPtr context = OCIO::Context::Create();
OCIO_CHECK_NO_THROW(OCIO::BuildOpsTest(ops, fileName, context,
OCIO::TRANSFORM_DIR_FORWARD));
// First one is the file no op.
OCIO_REQUIRE_EQUAL(ops.size(), 7);
OCIO_CHECK_ASSERT(ops[1]->isIdentity());
OCIO_CHECK_ASSERT(ops[2]->isIdentity());
OCIO_CHECK_ASSERT(false == ops[3]->isIdentity());
OCIO_CHECK_ASSERT(ops[4]->isIdentity());
OCIO_CHECK_ASSERT(false == ops[5]->isIdentity());
OCIO_CHECK_ASSERT(ops[6]->isIdentity());
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_NONE));
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO_CHECK_EQUAL(optOps.size(), 6);
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 2);
// The first two LUTs should get detected as identities and replaced with ranges which then
// get removed as clamp identities. LUT 3 is not identity and is not removed.
// The next LUT should also get detected as an identity, and replaced with a matrix (rather
// than a range since it is half-domain) which is then optimized out. The next LUT is almost
// an identity except the large values are clamped due to the rawHalfs encoding, so it is not
// removed. The final LUT is a normal domain to be replaced with a range. The two Luts get
// combined into a single Lut with a standard domain.
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<Lut1DOp>");
OCIO_CHECK_EQUAL(optOps[1]->getInfo(), "<RangeOp>");
OCIO::ConstOpRcPtr lutOp = optOps[0];
auto lutData = OCIO::DynamicPtrCast<const OCIO::Lut1DOpData>(lutOp->data());
OCIO_REQUIRE_ASSERT(lutData);
OCIO_CHECK_EQUAL(lutData->getArray().getLength(), 65536);
OCIO_CHECK_ASSERT(!lutData->isInputHalfDomain());
// Now check that the optimized transform renders the same as the original.
// TODO: Shall investigate why this test requires a bigger error.
CompareRender(ops, optOps, __LINE__, 3e-4f);
}
OCIO_ADD_TEST(OpOptimizers, lut1d_identity_replacement)
{
// Test that an identity Lut1D becomes a range but a half-domain becomes a matrix.
{
auto lutData = std::make_shared<OCIO::Lut1DOpData>(3);
OCIO_CHECK_ASSERT(lutData->isIdentity());
OCIO::OpRcPtrVec ops;
OCIO::CreateLut1DOp(ops, lutData, OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_EQUAL(ops.size(), 1);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(ops.size(), 1);
OCIO_CHECK_EQUAL(ops[0]->getInfo(), "<RangeOp>");
}
{
// By setting the filterNaNs argument to true, the constructor replaces NaN values with 0
// and this causes the LUT to technically no longer be an identity since the values are no
// longer exactly what is in a half float.
OCIO::Lut1DOpDataRcPtr lutData
= std::make_shared<OCIO::Lut1DOpData>(OCIO::Lut1DOpData::LUT_INPUT_OUTPUT_HALF_CODE,
65536, true);
lutData->setFileOutputBitDepth(OCIO::BIT_DEPTH_F32);
OCIO_CHECK_ASSERT(!lutData->isIdentity());
OCIO_CHECK_ASSERT(lutData->isInputHalfDomain());
}
{
// By default, this constructor creates an 'identity lut'.
OCIO::Lut1DOpDataRcPtr lutData
= std::make_shared<OCIO::Lut1DOpData>(OCIO::Lut1DOpData::LUT_INPUT_OUTPUT_HALF_CODE,
65536, false);
lutData->setFileOutputBitDepth(OCIO::BIT_DEPTH_F32);
OCIO_CHECK_ASSERT(lutData->isIdentity());
OCIO_CHECK_ASSERT(lutData->isInputHalfDomain());
OCIO::OpRcPtrVec ops;
OCIO::CreateLut1DOp(ops, lutData, OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_EQUAL(ops.size(), 1);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_DEFAULT));
// Half domain LUT 1d is a no-op.
// CPU processor will add an identity matrix.
OCIO_CHECK_EQUAL(ops.size(), 0);
}
}
OCIO_ADD_TEST(OpOptimizers, lut1d_identity_replacement_order)
{
// See issue #1737, https://github.com/AcademySoftwareFoundation/OpenColorIO/issues/1737.
// This CTF contains a single LUT1D, inverse direction, normal (not half) domain.
// It contains values from -6 to +3.4.
const std::string fileName("lut1d_inverse_gpu.ctf");
OCIO::ContextRcPtr context = OCIO::Context::Create();
OCIO::OpRcPtrVec inv_ops;
OCIO_CHECK_NO_THROW(OCIO::BuildOpsTest(inv_ops, fileName, context,
// FWD direction simply means don't swap the direction, the
// file contains an inverse LUT1D and leave it that way.
OCIO::TRANSFORM_DIR_FORWARD));
OCIO::OpRcPtrVec fwd_ops;
OCIO_CHECK_NO_THROW(OCIO::BuildOpsTest(fwd_ops, fileName, context,
OCIO::TRANSFORM_DIR_INVERSE));
// Check forward LUT1D followed by inverse LUT1D.
{
OCIO::OpRcPtrVec fwd_inv_ops = fwd_ops;
fwd_inv_ops += inv_ops;
OCIO_CHECK_NO_THROW(fwd_inv_ops.finalize());
OCIO_CHECK_NO_THROW(fwd_inv_ops.optimize(OCIO::OPTIMIZATION_NONE));
OCIO_CHECK_EQUAL(fwd_inv_ops.size(), 2); // no optmization was done
OCIO::OpRcPtrVec optOps = fwd_inv_ops.clone();
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_CHECK_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
// Compare renders.
CompareRender(fwd_inv_ops, optOps, __LINE__, 1e-6f);
}
// Check inverse LUT1D followed by forward LUT1D.
{
OCIO::OpRcPtrVec inv_fwd_ops = inv_ops;
inv_fwd_ops += fwd_ops;
OCIO_CHECK_NO_THROW(inv_fwd_ops.finalize());
OCIO_CHECK_NO_THROW(inv_fwd_ops.optimize(OCIO::OPTIMIZATION_NONE));
OCIO_CHECK_EQUAL(inv_fwd_ops.size(), 2); // no optmization was done
OCIO::OpRcPtrVec optOps = inv_fwd_ops.clone();
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_CHECK_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
// Compare renders.
CompareRender(inv_fwd_ops, optOps, __LINE__, 1e-6f);
}
}
OCIO_ADD_TEST(OpOptimizers, lut1d_half_domain_keep_prior_range)
{
// A half-domain LUT should not allow removal of a prior range op.
OCIO::OpRcPtrVec ops;
OCIO::CreateRangeOp(ops, 0., 1., 0., 1., OCIO::TRANSFORM_DIR_FORWARD);
OCIO::Lut1DOpDataRcPtr lutData
= std::make_shared<OCIO::Lut1DOpData>(OCIO::Lut1DOpData::LUT_INPUT_OUTPUT_HALF_CODE,
65536, false);
lutData->setFileOutputBitDepth(OCIO::BIT_DEPTH_F32);
// Add no-op LUT.
OCIO::CreateLut1DOp(ops, lutData, OCIO::TRANSFORM_DIR_INVERSE);
// Add another LUT.
lutData = lutData->clone();
auto & lutArray = lutData->getArray();
for (auto & val : lutArray.getValues())
{
val = -val;
}
OCIO::CreateLut1DOp(ops, lutData, OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_EQUAL(ops.size(), 3);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO_CHECK_EQUAL(optOps.size(), 3);
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_CHECK_EQUAL(optOps.size(), 2);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
OCIO_CHECK_EQUAL(optOps[1]->getInfo(), "<Lut1DOp>");
// Now check that the optimized transform renders the same as the original.
CompareRender(ops, optOps, __LINE__, 1e-6f);
}
OCIO_ADD_TEST(OpOptimizers, range_composition)
{
const double emptyValue = OCIO::RangeOpData::EmptyValue();
{
// Two identity clamp negs ranges are collapsed into one.
OCIO::OpRcPtrVec ops;
OCIO::CreateRangeOp(ops, 0., emptyValue, 0., emptyValue, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateRangeOp(ops, 0., emptyValue, 0., emptyValue, OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_EQUAL(ops.size(), 2);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
CompareRender(ops, optOps, __LINE__, 1e-6f);
}
{
// Non identity ranges are combined.
OCIO::OpRcPtrVec ops;
OCIO::CreateRangeOp(ops, 0.1, emptyValue, 0.1, emptyValue, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateRangeOp(ops, 0.2, emptyValue, 0.2, emptyValue, OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_EQUAL(ops.size(), 2);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
CompareRender(ops, optOps, __LINE__, 1e-6f);
}
{
// Non identity ranges are combined.
OCIO::OpRcPtrVec ops;
OCIO::CreateRangeOp(ops, 0.1, emptyValue, 0.1, emptyValue, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateRangeOp(ops, 0.1, emptyValue, 0.1, emptyValue, OCIO::TRANSFORM_DIR_INVERSE);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_REQUIRE_EQUAL(ops.size(), 2);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
CompareRender(ops, optOps, __LINE__, 1e-6f);
}
{
// Non identity ranges are combined.
OCIO::OpRcPtrVec ops;
OCIO::CreateRangeOp(ops, -0.1, emptyValue, -0.1, emptyValue, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateRangeOp(ops, -0.1, emptyValue, -0.1, emptyValue, OCIO::TRANSFORM_DIR_INVERSE);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_REQUIRE_EQUAL(ops.size(), 2);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
CompareRender(ops, optOps, __LINE__, 1e-6f);
}
{
// A clamp negs range is dropped before a more restrictive one.
OCIO::OpRcPtrVec ops;
OCIO::CreateRangeOp(ops, 0., emptyValue, 0., emptyValue, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateRangeOp(ops, 0.1, 2., 0., 2., OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_EQUAL(ops.size(), 2);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
CompareRender(ops, optOps, __LINE__, 1e-6f);
}
{
OCIO::OpRcPtrVec ops;
OCIO::CreateRangeOp(ops, 0., 0.5, 0., 0.5, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateRangeOp(ops, 0.1, 1., 0.1, 1., OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_EQUAL(ops.size(), 2);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
CompareRender(ops, optOps, __LINE__, 1e-6f);
}
{
OCIO::OpRcPtrVec ops;
OCIO::CreateRangeOp(ops, 0., 0.6, 0., 0.5, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateRangeOp(ops, 0.1, 1., 0.2, 1., OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateRangeOp(ops, 0., 1., 0., 2., OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_EQUAL(ops.size(), 3);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
CompareRender(ops, optOps, __LINE__, 1e-6f);
}
{
OCIO::OpRcPtrVec ops;
OCIO::CreateRangeOp(ops, 0., 0.5, 0., 0.5, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateRangeOp(ops, 0.6, 1., 0.6, 1., OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_EQUAL(ops.size(), 2);
OCIO::OpRcPtrVec optOps = ops.clone();
// Two Ranges with non-overlapping pass regions are replaced with a clamp to a constant.
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
CompareRender(ops, optOps, __LINE__, 1e-6f);
}
{
OCIO::OpRcPtrVec ops;
OCIO::CreateRangeOp(ops, 0.6, 1., 0.6, 1., OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateRangeOp(ops, 0., 0.5, 0., 0.5, OCIO::TRANSFORM_DIR_FORWARD);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_EQUAL(ops.size(), 2);
OCIO::OpRcPtrVec optOps = ops.clone();
// Ranges can not be combined out domain of the first does not intersect
// with in domain of the second.
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
CompareRender(ops, optOps, __LINE__, 1e-6f);
}
{
OCIO::OpRcPtrVec ops;
OCIO::CreateRangeOp(ops, 0., 0.6, 0., 0.5, OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateRangeOp(ops, 0.1, 1., 0.2, 1., OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateRangeOp(ops, 0.1, 1., 0.2, 1., OCIO::TRANSFORM_DIR_INVERSE);
OCIO::CreateRangeOp(ops, 0., 1., 0., 2., OCIO::TRANSFORM_DIR_FORWARD);
OCIO::CreateRangeOp(ops, 0.1, 0.8, 0.2, 1., OCIO::TRANSFORM_DIR_INVERSE);
OCIO::CreateRangeOp(ops, 0.2, 0.6, 0.1, 0.7, OCIO::TRANSFORM_DIR_INVERSE);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_EQUAL(ops.size(), 6);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
CompareRender(ops, optOps, __LINE__, 1e-6f);
}
}
OCIO_ADD_TEST(OpOptimizers, invlut_pair_identities)
{
// The file contains an InverseLUT1D and LUT1D, both with the same array, followed by
// an InverseLUT3D and LUT3D, also both with the same array. The pairs should each get
// replaced by a range and then the ranges should be combined.
const std::string fileName("lut_inv_pairs.ctf");
OCIO::OpRcPtrVec ops;
OCIO::ContextRcPtr context = OCIO::Context::Create();
OCIO_CHECK_NO_THROW(OCIO::BuildOpsTest(ops, fileName, context,
OCIO::TRANSFORM_DIR_FORWARD));
// First one is the file no op.
OCIO_CHECK_EQUAL(ops.size(), 5);
// Remove no ops & finalize for computation.
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_NONE));
OCIO_CHECK_EQUAL(ops.size(), 4);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO_CHECK_EQUAL(optOps.size(), 4);
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
// (A range that clamps on either side is not a no-op.)
OCIO_CHECK_ASSERT(!optOps[0]->isNoOp());
// Now check that the optimized transform renders the same as the original.
CompareRender(ops, optOps, __LINE__, 1e-6f);
}
OCIO_ADD_TEST(OpOptimizers, mntr_identities)
{
// Forward and inverse monitor transforms should become an identity.
const std::string fileName("mntr_srgb_identity.ctf");
OCIO::OpRcPtrVec ops;
OCIO::ContextRcPtr context = OCIO::Context::Create();
OCIO_CHECK_NO_THROW(OCIO::BuildOpsTest(ops, fileName, context,
OCIO::TRANSFORM_DIR_FORWARD));
// First one is the file no op.
OCIO_CHECK_EQUAL(ops.size(), 5);
// Remove no ops & finalize for computation.
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_NONE));
OCIO_CHECK_EQUAL(ops.size(), 4);
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_DEFAULT));
// Identity transform.
OCIO_CHECK_EQUAL(ops.size(), 0);
}
OCIO_ADD_TEST(OpOptimizers, gamma_comp)
{
// This transform has a pair of gammas separated by an identity matrix
// that should compose into a single (non-identity) gamma that then should
// be identified as a pair identity with another gamma and be replaced with
// a clamp-negs range.
const std::string fileName("gamma_comp_test.ctf");
OCIO::OpRcPtrVec ops;
OCIO::ContextRcPtr context = OCIO::Context::Create();
OCIO_CHECK_NO_THROW(OCIO::BuildOpsTest(ops, fileName, context,
OCIO::TRANSFORM_DIR_FORWARD));
// First one is the file no op.
OCIO_CHECK_EQUAL(ops.size(), 5);
// Remove no ops & finalize for computation.
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_NONE));
OCIO_CHECK_EQUAL(ops.size(), 4);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO::OpRcPtrVec optOps_noComp = ops.clone();
OCIO::OpRcPtrVec optOps_noIdentity = ops.clone();
OCIO_CHECK_EQUAL(optOps_noComp.size(), 4);
OCIO_CHECK_NO_THROW(optOps_noComp.finalize());
OCIO_CHECK_NO_THROW(optOps_noComp.optimize(AllBut(OCIO::OPTIMIZATION_COMP_GAMMA)));
// Identity matrix is removed but gamma are not combined.
OCIO_CHECK_EQUAL(optOps_noComp.size(), 3);
OCIO_CHECK_EQUAL(optOps_noComp[0]->getInfo(), "<GammaOp>");
OCIO_CHECK_EQUAL(optOps_noComp[1]->getInfo(), "<GammaOp>");
OCIO_CHECK_EQUAL(optOps_noComp[2]->getInfo(), "<GammaOp>");
CompareRender(ops, optOps_noComp, __LINE__, 1e-6f);
OCIO_CHECK_EQUAL(optOps_noIdentity.size(), 4);
OCIO_CHECK_NO_THROW(optOps_noIdentity.finalize());
OCIO_CHECK_NO_THROW(optOps_noIdentity.optimize(AllBut(OCIO::OPTIMIZATION_IDENTITY_GAMMA)));
// Identity matrix is removed and gammas are combined into an identity gamma
// but it is not removed.
OCIO_CHECK_EQUAL(optOps_noIdentity.size(), 1);
OCIO_CHECK_EQUAL(optOps_noIdentity[0]->getInfo(), "<GammaOp>");
CompareRender(ops, optOps_noIdentity, __LINE__, 5e-5f);
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
// Identity matrix is removed and gammas combined and optimized as a range.
OCIO_REQUIRE_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
// Now check that the optimized transform renders the same as the original.
// TODO: Gamma is clamping alpha, and Range does not.
CompareRender(ops, optOps, __LINE__, 1e-4f, true);
}
OCIO_ADD_TEST(OpOptimizers, gamma_comp_identity)
{
OCIO::OpRcPtrVec ops;
OCIO::GammaOpData::Params params1 = { 0.45 };
OCIO::GammaOpData::Params paramsA = { 1. };
auto gamma1 = std::make_shared<OCIO::GammaOpData>(OCIO::GammaOpData::BASIC_FWD,
params1, params1, params1, paramsA);
OCIO::GammaOpData::Params params2 = { 1. / 0.45 };
auto gamma2 = std::make_shared<OCIO::GammaOpData>(OCIO::GammaOpData::BASIC_FWD,
params2, params2, params2, paramsA);
OCIO_CHECK_NO_THROW(OCIO::CreateGammaOp(ops, gamma1, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateGammaOp(ops, gamma2, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_EQUAL(ops.size(), 2);
OCIO::OpRcPtrVec optOps = ops.clone();
// BASIC gamma are composed resulting into identity, that get optimized as a range.
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 1);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
ops.clear();
optOps.clear();
params1 = { 2., 0.5 };
params2 = { 2., 0.6 };
paramsA = { 1., 0. };
gamma1 = std::make_shared<OCIO::GammaOpData>(OCIO::GammaOpData::MONCURVE_FWD,
params1, params1, params1, paramsA);
gamma2 = std::make_shared<OCIO::GammaOpData>(OCIO::GammaOpData::MONCURVE_FWD,
params2, params2, params2, paramsA);
OCIO_CHECK_NO_THROW(OCIO::CreateGammaOp(ops, gamma1, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateGammaOp(ops, gamma2, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_EQUAL(ops.size(), 2);
optOps = ops.clone();
// MONCURVE composition is not supported yet.
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 2);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<GammaOp>");
OCIO_CHECK_EQUAL(optOps[1]->getInfo(), "<GammaOp>");
}
OCIO_ADD_TEST(OpOptimizers, log_identities)
{
// Log fwd and rev transforms should become a range.
// This transform has two pair of LogOps separated by an identity matrix
// that should optimize into a range.
const std::string fileName("log_identities.ctf");
OCIO::OpRcPtrVec ops;
OCIO::ContextRcPtr context = OCIO::Context::Create();
OCIO_CHECK_NO_THROW(OCIO::BuildOpsTest(ops, fileName, context,
OCIO::TRANSFORM_DIR_FORWARD));
// First one is the file no op.
OCIO_CHECK_EQUAL(ops.size(), 6);
// Remove no ops & finalize for computation.
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_NONE));
OCIO_CHECK_EQUAL(ops.size(), 5);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO::OpRcPtrVec optOpsOff = ops.clone();
OCIO_CHECK_EQUAL(optOpsOff.size(), 5);
OCIO_CHECK_NO_THROW(optOpsOff.finalize());
OCIO_CHECK_NO_THROW(optOpsOff.optimize(AllBut(OCIO::OPTIMIZATION_PAIR_IDENTITY_LOG)));
// Only the identity matrix is optimized.
OCIO_CHECK_EQUAL(optOpsOff.size(), 4);
OCIO_CHECK_EQUAL(optOps.size(), 5);
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
// Identity matrix is optimized and forward/inverse log are combined.
OCIO_REQUIRE_EQUAL(optOps.size(), 1);
// (A range that clamps on either side is not a no-op.)
OCIO_CHECK_ASSERT(!optOps[0]->isNoOp());
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
// Now check that the optimized transform renders the same as the original.
CompareRender(ops, optOps, __LINE__, 1e-4f);
}
OCIO_ADD_TEST(OpOptimizers, range_lut)
{
// Non-identity range before a Lut1D should not be removed.
const std::string fileName("range_lut.ctf");
OCIO::OpRcPtrVec ops;
OCIO::ContextRcPtr context = OCIO::Context::Create();
OCIO_CHECK_NO_THROW(OCIO::BuildOpsTest(ops, fileName, context,
OCIO::TRANSFORM_DIR_FORWARD));
// First one is the file no op.
OCIO_CHECK_EQUAL(ops.size(), 3);
// Remove no ops & finalize for computation.
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_NONE));
OCIO_CHECK_EQUAL(ops.size(), 2);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO_CHECK_EQUAL(optOps.size(), 2);
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optOps.size(), 2);
OCIO_CHECK_EQUAL(optOps[0]->getInfo(), "<RangeOp>");
OCIO_CHECK_EQUAL(optOps[1]->getInfo(), "<Lut1DOp>");
// Now check that the optimized transform renders the same as the original.
CompareRender(ops, optOps, __LINE__, 1e-6f);
}
OCIO_ADD_TEST(OpOptimizers, dynamic_ops)
{
// Non-identity matrix.
OCIO::MatrixOpDataRcPtr matrix = std::make_shared<OCIO::MatrixOpData>();
matrix->setArrayValue(0, 2.);
// Identity exposure contrast.
OCIO::ExposureContrastOpDataRcPtr exposure = std::make_shared<OCIO::ExposureContrastOpData>();
OCIO::ExposureContrastOpDataRcPtr exposureDyn = exposure->clone();
exposureDyn->getExposureProperty()->makeDynamic();
// Test with non dynamic exposure contrast.
{
OCIO::OpRcPtrVec ops;
OCIO_CHECK_NO_THROW(OCIO::CreateMatrixOp(ops, matrix, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateExposureContrastOp(ops, exposure,
OCIO::TRANSFORM_DIR_FORWARD));
OCIO_REQUIRE_EQUAL(ops.size(), 2);
OCIO_CHECK_ASSERT(!ops[0]->isIdentity());
OCIO_CHECK_ASSERT(ops[1]->isIdentity());
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_DEFAULT));
// It gets optimized.
OCIO_REQUIRE_EQUAL(ops.size(), 1);
OCIO_CHECK_EQUAL(ops[0]->getInfo(), "<MatrixOffsetOp>");
}
// Test with dynamic exposure contrast.
{
OCIO::OpRcPtrVec ops;
OCIO_CHECK_NO_THROW(OCIO::CreateMatrixOp(ops, matrix, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateExposureContrastOp(ops, exposureDyn,
OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(OCIO::CreateMatrixOp(ops, matrix, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_REQUIRE_EQUAL(ops.size(), 3);
OCIO_CHECK_ASSERT(!ops[0]->isIdentity());
// Exposure contrast is dynamic.
OCIO_CHECK_ASSERT(ops[1]->isDynamic());
OCIO_CHECK_ASSERT(!ops[1]->isIdentity());
OCIO_CHECK_ASSERT(!ops[2]->isIdentity());
// It does not get optimized with default flags (OPTIMIZATION_NO_DYNAMIC_PROPERTIES off).
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(ops.size(), 3);
OCIO_CHECK_EQUAL(ops[0]->getInfo(), "<MatrixOffsetOp>");
OCIO_CHECK_EQUAL(ops[1]->getInfo(), "<ExposureContrastOp>");
OCIO_CHECK_EQUAL(ops[2]->getInfo(), "<MatrixOffsetOp>");
// It does get optimized if flag is set.
// OPTIMIZATION_ALL includes OPTIMIZATION_NO_DYNAMIC_PROPERTIES.
// Exposure contrast will get optimized and the 2 matrix will be composed.
OCIO_CHECK_NO_THROW(ops.finalize());
OCIO_CHECK_NO_THROW(ops.optimize(OCIO::OPTIMIZATION_ALL));
OCIO_REQUIRE_EQUAL(ops.size(), 1);
OCIO_CHECK_EQUAL(ops[0]->getInfo(), "<MatrixOffsetOp>");
}
}
OCIO_ADD_TEST(OpOptimizers, gamma_prefix)
{
OCIO::OpRcPtrVec originalOps;
OCIO::GammaOpData::Params params1 = {2.6};
OCIO::GammaOpData::Params paramsA = {1.};
OCIO::GammaOpDataRcPtr gamma1
= std::make_shared<OCIO::GammaOpData>(OCIO::GammaOpData::BASIC_REV,
params1, params1, params1, paramsA);
OCIO_CHECK_NO_THROW(OCIO::CreateGammaOp(originalOps, gamma1, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_REQUIRE_EQUAL(originalOps.size(), 1);
OCIO::OpRcPtrVec optimizedOps = originalOps.clone();
// Optimize it.
OCIO_CHECK_NO_THROW(optimizedOps.finalize());
OCIO_CHECK_NO_THROW(optimizedOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_CHECK_NO_THROW(optimizedOps.optimizeForBitdepth(OCIO::BIT_DEPTH_UINT16,
OCIO::BIT_DEPTH_F32,
OCIO::OPTIMIZATION_COMP_SEPARABLE_PREFIX));
// Validate the result.
OCIO_REQUIRE_EQUAL(optimizedOps.size(), 1);
OCIO::ConstOpRcPtr o1 = optimizedOps[0];
OCIO::ConstLut1DOpDataRcPtr oData1 = OCIO::DynamicPtrCast<const OCIO::Lut1DOpData>(o1->data());
OCIO_REQUIRE_ASSERT(oData1);
OCIO_CHECK_EQUAL(oData1->getType(), OCIO::OpData::Lut1DType);
OCIO_CHECK_EQUAL(oData1->getArray().getLength(), 65536);
originalOps.clear();
// However, if the input bit depth is F32, it should not be optimized.
OCIO::GammaOpDataRcPtr gamma2
= std::make_shared<OCIO::GammaOpData>(OCIO::GammaOpData::BASIC_REV,
params1, params1, params1, paramsA);
OCIO_CHECK_NO_THROW(OCIO::CreateGammaOp(originalOps, gamma2, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_REQUIRE_EQUAL(originalOps.size(), 1);
// Optimize it.
OCIO_CHECK_NO_THROW(originalOps.finalize());
OCIO_CHECK_NO_THROW(originalOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(originalOps.size(), 1);
OCIO::ConstOpRcPtr o2 = originalOps[0];
OCIO_CHECK_EQUAL(o2->data()->getType(), OCIO::OpData::GammaType);
}
OCIO_ADD_TEST(OpOptimizers, multi_op_prefix)
{
// Test prefix optimization of a complex transform.
OCIO::OpRcPtrVec originalOps;
OCIO::MatrixOpDataRcPtr matrix = std::make_shared<OCIO::MatrixOpData>();
matrix->setArrayValue(0, 2.);
OCIO_CHECK_NO_THROW(OCIO::CreateMatrixOp(originalOps, matrix, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_REQUIRE_EQUAL(originalOps.size(), 1);
OCIO::RangeOpDataRcPtr range
= std::make_shared<OCIO::RangeOpData>(0., 1., -1000./65535., 66000./65535);
OCIO_CHECK_NO_THROW(OCIO::CreateRangeOp(originalOps, range, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_REQUIRE_EQUAL(originalOps.size(), 2);
OCIO::OpRcPtrVec optimizedOps = originalOps.clone();
// Nothing to optimize.
OCIO_CHECK_NO_THROW(optimizedOps.finalize());
OCIO_CHECK_NO_THROW(optimizedOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_CHECK_NO_THROW(optimizedOps.optimizeForBitdepth(OCIO::BIT_DEPTH_UINT8,
OCIO::BIT_DEPTH_F32,
OCIO::OPTIMIZATION_COMP_SEPARABLE_PREFIX));
// Validate ops are unchanged.
OCIO_REQUIRE_EQUAL(optimizedOps.size(), 2U);
std::string originalID;
std::string optimizedID;
OCIO_CHECK_NO_THROW(originalID = originalOps[0]->getCacheID());
OCIO_CHECK_NO_THROW(optimizedID = optimizedOps[0]->getCacheID());
OCIO_CHECK_EQUAL(originalID, optimizedID);
OCIO_CHECK_NO_THROW(originalID = originalOps[1]->getCacheID());
OCIO_CHECK_NO_THROW(optimizedID = optimizedOps[1]->getCacheID());
OCIO_CHECK_EQUAL(originalID, optimizedID);
// Add more ops to originalOps.
const OCIO::CDLOpData::ChannelParams slope(1.35, 1.1, 0.071);
const OCIO::CDLOpData::ChannelParams offset(0.05, -0.23, 0.11);
const OCIO::CDLOpData::ChannelParams power(1.27, 0.81, 0.2);
const double saturation = 1.;
OCIO::CDLOpDataRcPtr cdl
= std::make_shared<OCIO::CDLOpData>(OCIO::CDLOpData::CDL_V1_2_FWD,
slope, offset, power, saturation);
OCIO_CHECK_NO_THROW(OCIO::CreateCDLOp(originalOps, cdl, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_CHECK_NO_THROW(originalOps.finalize());
OCIO_REQUIRE_EQUAL(originalOps.size(), 3);
optimizedOps = originalOps.clone();
// Optimize it.
OCIO_CHECK_NO_THROW(optimizedOps.finalize());
OCIO_CHECK_NO_THROW(optimizedOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_CHECK_NO_THROW(optimizedOps.optimizeForBitdepth(OCIO::BIT_DEPTH_UINT8,
OCIO::BIT_DEPTH_F32,
OCIO::OPTIMIZATION_COMP_SEPARABLE_PREFIX));
// Validate the result.
OCIO_REQUIRE_EQUAL(optimizedOps.size(), 1U);
OCIO::ConstOpRcPtr o = optimizedOps[0];
OCIO::ConstLut1DOpDataRcPtr oData = OCIO::DynamicPtrCast<const OCIO::Lut1DOpData>(o->data());
OCIO_CHECK_EQUAL(oData->getType(), OCIO::OpData::Lut1DType);
OCIO_CHECK_EQUAL(oData->getArray().getLength(), 256);
// Make sure originalOps are ready to render.
OCIO_CHECK_NO_THROW(originalOps.finalize());
// Although finalized for UINT8, the transform may still be evaluated at 32f to verify that
// it is a good approximation to the original.
CompareRender(originalOps, optimizedOps, __LINE__, 5e-5f);
}
OCIO_ADD_TEST(OpOptimizers, dyn_properties_prefix)
{
// Test prefix optimization of a complex transform.
OCIO::OpRcPtrVec originalOps;
OCIO::MatrixOpDataRcPtr matrix = std::make_shared<OCIO::MatrixOpData>();
matrix->setArrayValue(0, 2.);
OCIO_CHECK_NO_THROW(OCIO::CreateMatrixOp(originalOps, matrix, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_REQUIRE_EQUAL(originalOps.size(), 1);
OCIO::ExposureContrastOpDataRcPtr exposure = std::make_shared<OCIO::ExposureContrastOpData>();
exposure->setExposure(1.2);
exposure->setPivot(0.5);
OCIO_CHECK_NO_THROW(
OCIO::CreateExposureContrastOp(originalOps, exposure, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_REQUIRE_EQUAL(originalOps.size(), 2);
exposure = exposure->clone();
exposure->getExposureProperty()->makeDynamic();
OCIO_CHECK_NO_THROW(
OCIO::CreateExposureContrastOp(originalOps, exposure, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_REQUIRE_EQUAL(originalOps.size(), 3);
OCIO::OpRcPtrVec optimizedOps = originalOps.clone();
// Optimize it.
OCIO_CHECK_NO_THROW(optimizedOps.finalize());
OCIO_CHECK_NO_THROW(optimizedOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_CHECK_NO_THROW(optimizedOps.optimizeForBitdepth(OCIO::BIT_DEPTH_UINT8,
OCIO::BIT_DEPTH_F32,
OCIO::OPTIMIZATION_COMP_SEPARABLE_PREFIX));
// Validate the result.
OCIO_REQUIRE_EQUAL(optimizedOps.size(), 2U);
OCIO::ConstOpRcPtr o = optimizedOps[0];
OCIO::ConstLut1DOpDataRcPtr oData = OCIO::DynamicPtrCast<const OCIO::Lut1DOpData>(o->data());
OCIO_CHECK_ASSERT(oData);
OCIO_CHECK_EQUAL(oData->getType(), OCIO::OpData::Lut1DType);
OCIO_CHECK_EQUAL(oData->getArray().getLength(), 256);
OCIO_CHECK_CLOSE(oData->getArray()[255 * 3 + 0], 4.5947948f, 1.e-6f);
OCIO_CHECK_CLOSE(oData->getArray()[255 * 3 + 1], 2.2973969f, 1.e-6f);
OCIO_CHECK_CLOSE(oData->getArray()[255 * 3 + 2], 2.2973969f, 1.e-6f);
o = optimizedOps[1];
OCIO::ConstExposureContrastOpDataRcPtr exp =
OCIO::DynamicPtrCast<const OCIO::ExposureContrastOpData>(o->data());
OCIO_CHECK_ASSERT(exp);
OCIO_CHECK_EQUAL(exp->getType(), OCIO::OpData::ExposureContrastType);
OCIO_CHECK_ASSERT(exp->isDynamic());
}
OCIO_ADD_TEST(OpOptimizers, opt_prefix_test1)
{
const std::string fileName("opt_prefix_test1.ctf");
OCIO::OpRcPtrVec ops;
OCIO::ContextRcPtr context = OCIO::Context::Create();
OCIO_CHECK_NO_THROW(OCIO::BuildOpsTest(ops, fileName, context,
OCIO::TRANSFORM_DIR_FORWARD));
// First one is the file no op.
OCIO_CHECK_EQUAL(ops.size(), 12);
OCIO_CHECK_NO_THROW(OCIO::RemoveNoOpTypes(ops));
OCIO_CHECK_EQUAL(ops.size(), 11);
OCIO::OpRcPtrVec optOps = ops.clone();
OCIO_CHECK_EQUAL(optOps.size(), 11);
// Ignore dynamic properties.
OCIO_CHECK_NO_THROW(optOps.finalize());
OCIO_CHECK_NO_THROW(optOps.optimize(OCIO::OPTIMIZATION_ALL));
OCIO_CHECK_NO_THROW(optOps.optimizeForBitdepth(OCIO::BIT_DEPTH_F16, OCIO::BIT_DEPTH_F32,
OCIO::OPTIMIZATION_COMP_SEPARABLE_PREFIX));
OCIO_REQUIRE_EQUAL(optOps.size(), 3);
OCIO::ConstOpRcPtr o0 = optOps[0];
OCIO::ConstOpRcPtr o1 = optOps[1];
OCIO::ConstOpRcPtr o2 = optOps[2];
OCIO_CHECK_EQUAL(o0->data()->getType(), OCIO::OpData::Lut1DType);
OCIO_CHECK_EQUAL(o1->data()->getType(), OCIO::OpData::MatrixType);
OCIO_CHECK_EQUAL(o2->data()->getType(), OCIO::OpData::GammaType);
auto lut0 = OCIO_DYNAMIC_POINTER_CAST<const OCIO::Lut1DOpData>(o0->data());
OCIO_CHECK_ASSERT(!lut0->isIdentity());
OCIO_CHECK_EQUAL(lut0->getArray().getLength(), 65536u);
}
OCIO_ADD_TEST(OpOptimizers, replace_ops)
{
auto cdlData = std::make_shared<OCIO::CDLOpData>();
OCIO::CDLOpData::ChannelParams newOffsetParams(0.09);
cdlData->setOffsetParams(newOffsetParams);
cdlData->setSaturation(1.23);
cdlData->setSlopeParams(OCIO::CDLOpData::ChannelParams(0.8, 0.9, 1.1));
cdlData->setStyle(OCIO::CDLOpData::CDL_NO_CLAMP_FWD);
OCIO::OpRcPtrVec originalOps;
OCIO_CHECK_NO_THROW(
OCIO::CreateCDLOp(originalOps, cdlData, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_REQUIRE_EQUAL(originalOps.size(), 1);
OCIO::OpRcPtrVec optimizedOps = originalOps.clone();
// Verify that default optimization includes replacing ops.
OCIO_CHECK_ASSERT(OCIO::HasFlag(OCIO::OPTIMIZATION_DEFAULT,
OCIO::OPTIMIZATION_SIMPLIFY_OPS));
// Optimize it: CDL is replaced by a matrix.
OCIO_CHECK_NO_THROW(optimizedOps.finalize());
OCIO_CHECK_NO_THROW(optimizedOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optimizedOps.size(), 1);
OCIO::ConstOpRcPtr o = optimizedOps[0];
auto oData = o->data();
OCIO_CHECK_EQUAL(oData->getType(), OCIO::OpData::MatrixType);
// No optimization: keep CDL.
optimizedOps = originalOps.clone();
OCIO_CHECK_NO_THROW(optimizedOps.finalize());
OCIO_REQUIRE_EQUAL(optimizedOps.size(), 1);
o = optimizedOps[0];
oData = o->data();
OCIO_CHECK_EQUAL(oData->getType(), OCIO::OpData::CDLType);
// Only replace. CDL is replaced by 2 matrices, one for offset and slope, one for saturation.
// Default optimization would combine them.
optimizedOps = originalOps.clone();
OCIO_CHECK_NO_THROW(optimizedOps.finalize());
OCIO_CHECK_NO_THROW(optimizedOps.optimize(OCIO::OPTIMIZATION_SIMPLIFY_OPS));
OCIO_REQUIRE_EQUAL(optimizedOps.size(), 2);
o = optimizedOps[0];
oData = o->data();
OCIO_CHECK_EQUAL(oData->getType(), OCIO::OpData::MatrixType);
o = optimizedOps[1];
oData = o->data();
OCIO_CHECK_EQUAL(oData->getType(), OCIO::OpData::MatrixType);
// Use clamping style.
cdlData->setStyle(OCIO::CDLOpData::CDL_V1_2_FWD);
optimizedOps.clear();
OCIO_CHECK_NO_THROW(
OCIO::CreateCDLOp(optimizedOps, cdlData, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_REQUIRE_EQUAL(optimizedOps.size(), 1);
// Optimize it: CDL replaced by 2 matrices and 2 clamps.
OCIO_CHECK_NO_THROW(optimizedOps.finalize());
OCIO_CHECK_NO_THROW(optimizedOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optimizedOps.size(), 4);
o = optimizedOps[0];
oData = o->data();
OCIO_CHECK_EQUAL(oData->getType(), OCIO::OpData::MatrixType);
o = optimizedOps[1];
oData = o->data();
OCIO_CHECK_EQUAL(oData->getType(), OCIO::OpData::RangeType);
o = optimizedOps[2];
oData = o->data();
OCIO_CHECK_EQUAL(oData->getType(), OCIO::OpData::MatrixType);
o = optimizedOps[3];
oData = o->data();
OCIO_CHECK_EQUAL(oData->getType(), OCIO::OpData::RangeType);
// With a non-identity power.
cdlData->setPowerParams(OCIO::CDLOpData::ChannelParams(1, 1, 1.0001));
optimizedOps.clear();
OCIO_CHECK_NO_THROW(
OCIO::CreateCDLOp(optimizedOps, cdlData, OCIO::TRANSFORM_DIR_FORWARD));
OCIO_REQUIRE_EQUAL(optimizedOps.size(), 1);
// Optimize it: CDL is not replaced.
OCIO_CHECK_NO_THROW(optimizedOps.finalize());
OCIO_CHECK_NO_THROW(optimizedOps.optimize(OCIO::OPTIMIZATION_DEFAULT));
OCIO_REQUIRE_EQUAL(optimizedOps.size(), 1);
o = optimizedOps[0];
oData = o->data();
OCIO_CHECK_EQUAL(oData->getType(), OCIO::OpData::CDLType);
}