diff --git a/include/OpenColorIO/OpenColorTransforms.h b/include/OpenColorIO/OpenColorTransforms.h index e37c59e4c2..8ef04be8c8 100644 --- a/include/OpenColorIO/OpenColorTransforms.h +++ b/include/OpenColorIO/OpenColorTransforms.h @@ -513,10 +513,15 @@ class OCIOEXPORT GradingBSplineCurve public: /// Create a BSpline curve with a specified number of control points. static GradingBSplineCurveRcPtr Create(size_t size); + static GradingBSplineCurveRcPtr Create(size_t size, BSplineType splineType); + static GradingBSplineCurveRcPtr Create(size_t size, HueCurveType curveType); /// Create a BSpline curve with a list of control points. static GradingBSplineCurveRcPtr Create(std::initializer_list values); + static GradingBSplineCurveRcPtr Create(std::initializer_list values, BSplineType splineType); + static GradingBSplineCurveRcPtr Create(std::initializer_list values, HueCurveType curveType); virtual GradingBSplineCurveRcPtr createEditableCopy() const = 0; + /// Get the number of ControlPoint objects (and the number of slopes). virtual size_t getNumControlPoints() const noexcept = 0; virtual void setNumControlPoints(size_t size) = 0; virtual const GradingControlPoint & getControlPoint(size_t index) const = 0; @@ -525,6 +530,8 @@ class OCIOEXPORT GradingBSplineCurve virtual void setSlope(size_t index, float slope) = 0; virtual bool slopesAreDefault() const = 0; virtual void validate() const = 0; + virtual BSplineType getSplineType() const = 0; + virtual void setSplineType(BSplineType splineType) = 0; GradingBSplineCurve(const GradingBSplineCurve &) = delete; GradingBSplineCurve & operator= (const GradingBSplineCurve &) = delete; @@ -547,6 +554,8 @@ extern OCIOEXPORT std::ostream & operator<<(std::ostream &, const GradingBSpline class OCIOEXPORT GradingRGBCurve { public: + /// Create a GradingRGBCurve. (The style argument is not part of the object, it is simply + /// used to initialize the proper default curves.) static GradingRGBCurveRcPtr Create(GradingStyle style); static GradingRGBCurveRcPtr Create(const ConstGradingRGBCurveRcPtr & rhs); static GradingRGBCurveRcPtr Create(const ConstGradingBSplineCurveRcPtr & red, @@ -571,6 +580,58 @@ extern OCIOEXPORT bool operator==(const GradingRGBCurve & lhs, const GradingRGBC extern OCIOEXPORT bool operator!=(const GradingRGBCurve & lhs, const GradingRGBCurve & rhs); extern OCIOEXPORT std::ostream & operator<<(std::ostream &, const GradingRGBCurve &); +/** + * A set of HUE/SAT/LUM curves. It is used by GradingHueCurveTransform and can be used as + * a dynamic property (see \ref DynamicPropertyGradingHueCurve). + */ +class OCIOEXPORT GradingHueCurve +{ +public: + /// Create a GradingHueCurve. (The style argument is not part of the object, it is simply + /// used to initialize the proper default curves.) + static GradingHueCurveRcPtr Create(GradingStyle style); + static GradingHueCurveRcPtr Create(const ConstGradingHueCurveRcPtr & rhs); + static GradingHueCurveRcPtr Create( + ConstGradingBSplineCurveRcPtr hueHue, + ConstGradingBSplineCurveRcPtr hueSat, + ConstGradingBSplineCurveRcPtr hueLum, + ConstGradingBSplineCurveRcPtr lumSat, + ConstGradingBSplineCurveRcPtr satSat, + ConstGradingBSplineCurveRcPtr lumLum, + ConstGradingBSplineCurveRcPtr satLum, + ConstGradingBSplineCurveRcPtr hueFx); + + static BSplineType GetBSplineTypeForHueCurveType(HueCurveType curveType); + + virtual GradingHueCurveRcPtr createEditableCopy() const = 0; + virtual void validate() const = 0; + virtual bool isIdentity() const = 0; + /** + * Enable drawCurveOnly mode to return the output value of a spline curve without any of the + * other associated processing of the RGB values. This is useful when the curves need to be + * graphed independently in a user interface. To use this, set the curve parameters on the + * Hue-Sat curve. The R, G, and B values will be sent through that curve with the interpretation + * that they are the input axis to the curve (which would be hue, sat, or luma) rather than RGB. + * This mode does not apply the RGB-to-HSY or Lin-to-Log, so for scene-linear curves the luma + * values are interpreted as already being in the logarithmic (f-stop) space. The forward curve + * evaluation is done regardless of the transform direction. + */ + virtual bool getDrawCurveOnly() const = 0; + virtual void setDrawCurveOnly(bool drawCurveOnly) = 0; + virtual ConstGradingBSplineCurveRcPtr getCurve(HueCurveType c) const = 0; + virtual GradingBSplineCurveRcPtr getCurve(HueCurveType c) = 0; + + /// Do not use (needed only for pybind11). + virtual ~GradingHueCurve() = default; + +protected: + GradingHueCurve() = default; +}; + +extern OCIOEXPORT bool operator==(const GradingHueCurve & lhs, const GradingHueCurve & rhs); +extern OCIOEXPORT bool operator!=(const GradingHueCurve & lhs, const GradingHueCurve & rhs); +extern OCIOEXPORT std::ostream & operator<<(std::ostream &, const GradingHueCurve &); + /** * Used by the grading tone transforms to hold the red, green, blue, master, start, * and width components of a single parameter. The master component affects all three channels @@ -734,6 +795,11 @@ extern OCIOEXPORT DynamicPropertyGradingPrimaryRcPtr AsGradingPrimary(DynamicPro * value. Will throw if property type is not DYNAMIC_PROPERTY_GRADING_RGBCURVE. */ extern OCIOEXPORT DynamicPropertyGradingRGBCurveRcPtr AsGradingRGBCurve(DynamicPropertyRcPtr & prop); +/** + * Get the property as DynamicPropertyGradingHueCurveRcPtr to access the GradingHueCurveRcPtr + * value. Will throw if property type is not DYNAMIC_PROPERTY_GRADING_HUECURVE. + */ +extern OCIOEXPORT DynamicPropertyGradingHueCurveRcPtr AsGradingHueCurve(DynamicPropertyRcPtr & prop); /** * Get the property as DynamicPropertyGradingToneRcPtr to access the GradingTone value. Will throw * if property type is not DYNAMIC_PROPERTY_GRADING_TONE. @@ -791,6 +857,23 @@ class OCIOEXPORT DynamicPropertyGradingRGBCurve DynamicPropertyGradingRGBCurve() = default; }; +/// Interface used to access dynamic property ConstGradingHueCurveRcPtr value. +class OCIOEXPORT DynamicPropertyGradingHueCurve +{ +public: + virtual const ConstGradingHueCurveRcPtr & getValue() const = 0; + /// Will throw if value is not valid. + virtual void setValue(const ConstGradingHueCurveRcPtr & value) = 0; + + DynamicPropertyGradingHueCurve(const DynamicPropertyGradingHueCurve &) = delete; + DynamicPropertyGradingHueCurve & operator=(const DynamicPropertyGradingHueCurve &) = delete; + /// Do not use (needed only for pybind11). + virtual ~DynamicPropertyGradingHueCurve() = default; + +protected: + DynamicPropertyGradingHueCurve() = default; +}; + /// Interface used to access dynamic property GradingTone value. class OCIOEXPORT DynamicPropertyGradingTone { @@ -1196,13 +1279,120 @@ class OCIOEXPORT GradingPrimaryTransform : public Transform extern OCIOEXPORT std::ostream & operator<<(std::ostream &, const GradingPrimaryTransform &) noexcept; +/** + * Hue curve color correction controls. + * + * This transform provides eight spline curves to make the following adjustments: + * + * - Hue-Hue: Map input hue to output hue (where a diagonal line is the identity). + * - Hue-Sat: Adjust saturation as a function of hue (a value of 1.0 is the identity). + * - Hue-Lum: Adjust luma as a function of hue (a value of 1.0 is the identity). + * - Lum-Sat: Adjust saturation as a function of luma (a value of 1.0 is the identity). + * - Sat-Sat: Adjust saturation as a function of saturation (a diagonal is the identity). + * - Lum-Lum: Adjust luma as a function of luma, maintaining hue & sat (diagonal is identity). + * - Sat-Lum: Adjust luma as a function of saturation (a value of 1.0 is the identity). + * - Hue-FX : Map input hue to delta output hue (a value of 0.0 is the identity). + * + * The algorithm is different for scene-linear, logarithmic, and video color spaces, so + * initialize the style argument appropriately before setting the curves. + * + * An RGB-to-HSY FixedFunction is used to convert RGB into a hue, saturation, luma color + * space. However, there is an option to bypass that conversion to use an outboard transform. + * + * Like the GradingRGBCurveTransform, the curves are defined by the x and y coordinates of a + * set of control points. A spline will be fit to the control points. Monotonicity is preserved + * for curves where the diagonal is the identity. For curves that take luma as input, if the + * style is scene-linear, the units are in photographic stops relative to 0.18. For log and + * video, the luma is scaled the same as the input RGB. + * + * The hue variable is [0,1] and is periodic. For example, -0.2, 0.8, and 1.8 are equivalent. + * The domain of the curves is [0,1] and control points outside that domain are mapped into it. + * A hue of 0 or 1 corresponds to a magenta hue. + * + * The control points are dynamic, so they may be adjusted even after the Transform is included + * in a Processor. However, creating a curve or setting the parameters will call the + * GradingBSplineCurveImpl::validate function, which will throw an exception if the control + * points do not meet certain requirements, for example that the X-coordinates are non-decreasing + * Please review that function for details on the validation. Applications that provide a UI to + * edit curves must ensure that they prevent users from creating control points that are not valid. + * + * The transform is invertible as long as the curves allow it. For example, if saturation is + * mapped to zero, obviously that cannot be resaturated. Care should be taken with the Hue-FX + * curve because it is possible to fold hues over on themselves, which also cannot be inverted. + * In most cases the Hue-FX curve is not necessary since the Hue-Hue curve provides similar + * functionality with the added benefit of being strictly invertible. + */ +class OCIOEXPORT GradingHueCurveTransform : public Transform +{ +public: + /// Creates an instance of GradingHueCurveTransform. + static GradingHueCurveTransformRcPtr Create(GradingStyle style); + + TransformType getTransformType() const noexcept override { return TRANSFORM_TYPE_GRADING_HUE_CURVE; } + + virtual const FormatMetadata & getFormatMetadata() const noexcept = 0; + virtual FormatMetadata & getFormatMetadata() noexcept = 0; + + /// Checks if this equals other. + virtual bool equals(const GradingHueCurveTransform & other) const noexcept = 0; + + /// Adjusts the behavior of the transform for log, linear, or video color space encodings. + virtual GradingStyle getStyle() const noexcept = 0; + + /// Will reset value to style's defaults if style is not the current style. + virtual void setStyle(GradingStyle style) noexcept = 0; + + virtual const ConstGradingHueCurveRcPtr getValue() const = 0; + + /// Throws if value is not valid. + virtual void setValue(const ConstGradingHueCurveRcPtr & value) = 0; + + /** + * It is possible to provide a desired slope value for each control point. The number of slopes is + * always the same as the number of control points and so the control points must be set before + * setting the slopes. The slopes are primarily intended for use by config authors looking to match + * a specific shape with as few control points as possible, they are not intended to be exposed to + * a user interface for direct manipulation. When a curve is being generated for creative purposes + * it is better to let OCIO calculate the slopes automatically. + */ + virtual float getSlope(HueCurveType c, size_t index) const = 0; + virtual void setSlope(HueCurveType c, size_t index, float slope) = 0; + virtual bool slopesAreDefault(HueCurveType c) const = 0; + + /** + * By default, the input is transformed into HSY space to apply the hue curves and then the result is + * transformed back to RGB. However, this may be set to HSY_TRANSFORM_NONE to bypass this in order to + * use other hue/sat/luma type transforms applied separately before and after this transform. + */ + virtual HSYTransformStyle getRGBToHSY() const = 0; + virtual void setRGBToHSY(HSYTransformStyle style) = 0; + + ///** + // * Parameters can be made dynamic so the values can be changed through the CPU or GPU processor, + // * but if there are several GradingHueCurveTransform only one can have dynamic parameters. + // */ + virtual bool isDynamic() const noexcept = 0; + virtual void makeDynamic() noexcept = 0; + virtual void makeNonDynamic() noexcept = 0; + + GradingHueCurveTransform(const GradingHueCurveTransform &) = delete; + GradingHueCurveTransform & operator= (const GradingHueCurveTransform &) = delete; + + /// Do not use (needed only for pybind11). + virtual ~GradingHueCurveTransform() = default; + +protected: + GradingHueCurveTransform() = default; +}; + +extern OCIOEXPORT std::ostream & operator<<(std::ostream &, const GradingHueCurveTransform &) noexcept; /** * RGB curve color correction controls. * * This transform allows for modifying tone reproduction via B-spline curves. * - * There is an R, G, and B curve along with a Master curve (that applies to R, G, and B). Each + * There is an R, G, and B curve followed by a Master curve (that applies to R, G, and B). Each * curve is specified via the x and y coordinates of its control points. A monotonic spline is * fit to the control points. The x coordinates must be non-decreasing. When the grading style * is linear, the units for the control points are photographic stops relative to 0.18. diff --git a/include/OpenColorIO/OpenColorTypes.h b/include/OpenColorIO/OpenColorTypes.h index 4893c78234..a995c849a2 100644 --- a/include/OpenColorIO/OpenColorTypes.h +++ b/include/OpenColorIO/OpenColorTypes.h @@ -114,6 +114,10 @@ class OCIOEXPORT GradingRGBCurve; typedef OCIO_SHARED_PTR ConstGradingRGBCurveRcPtr; typedef OCIO_SHARED_PTR GradingRGBCurveRcPtr; +class OCIOEXPORT GradingHueCurve; +typedef OCIO_SHARED_PTR ConstGradingHueCurveRcPtr; +typedef OCIO_SHARED_PTR GradingHueCurveRcPtr; + class OCIOEXPORT ConfigIOProxy; typedef OCIO_SHARED_PTR ConstConfigIOProxyRcPtr; typedef OCIO_SHARED_PTR ConfigIOProxyRcPtr; @@ -163,6 +167,10 @@ class OCIOEXPORT DynamicPropertyGradingRGBCurve; typedef OCIO_SHARED_PTR ConstDynamicPropertyGradingRGBCurveRcPtr; typedef OCIO_SHARED_PTR DynamicPropertyGradingRGBCurveRcPtr; +class OCIOEXPORT DynamicPropertyGradingHueCurve; +typedef OCIO_SHARED_PTR ConstDynamicPropertyGradingHueCurveRcPtr; +typedef OCIO_SHARED_PTR DynamicPropertyGradingHueCurveRcPtr; + class OCIOEXPORT DynamicPropertyGradingTone; typedef OCIO_SHARED_PTR ConstDynamicPropertyGradingToneRcPtr; typedef OCIO_SHARED_PTR DynamicPropertyGradingToneRcPtr; @@ -191,6 +199,10 @@ class OCIOEXPORT GradingPrimaryTransform; typedef OCIO_SHARED_PTR ConstGradingPrimaryTransformRcPtr; typedef OCIO_SHARED_PTR GradingPrimaryTransformRcPtr; +class OCIOEXPORT GradingHueCurveTransform; +typedef OCIO_SHARED_PTR ConstGradingHueCurveTransformRcPtr; +typedef OCIO_SHARED_PTR GradingHueCurveTransformRcPtr; + class OCIOEXPORT GradingRGBCurveTransform; typedef OCIO_SHARED_PTR ConstGradingRGBCurveTransformRcPtr; typedef OCIO_SHARED_PTR GradingRGBCurveTransformRcPtr; @@ -351,6 +363,7 @@ enum TransformType TRANSFORM_TYPE_EXPOSURE_CONTRAST, TRANSFORM_TYPE_FILE, TRANSFORM_TYPE_FIXED_FUNCTION, + TRANSFORM_TYPE_GRADING_HUE_CURVE, TRANSFORM_TYPE_GRADING_PRIMARY, TRANSFORM_TYPE_GRADING_RGB_CURVE, TRANSFORM_TYPE_GRADING_TONE, @@ -497,7 +510,10 @@ enum FixedFunctionStyle FIXED_FUNCTION_ACES_OUTPUT_TRANSFORM_20, ///< ACES 2.0 Display Rendering -- EXPERIMENTAL FIXED_FUNCTION_ACES_RGB_TO_JMH_20, ///< ACES 2.0 RGB to JMh -- EXPERIMENTAL FIXED_FUNCTION_ACES_TONESCALE_COMPRESS_20, ///< ACES 2.0 Tonescale and chroma compression -- EXPERIMENTAL - FIXED_FUNCTION_ACES_GAMUT_COMPRESS_20 ///< ACES 2.0 Gamut compression -- EXPERIMENTAL + FIXED_FUNCTION_ACES_GAMUT_COMPRESS_20, ///< ACES 2.0 Gamut compression -- EXPERIMENTAL + FIXED_FUNCTION_RGB_TO_HSY_LIN, ///< RGB to HSY (Hue, Saturation, Luminance) for linear spaces + FIXED_FUNCTION_RGB_TO_HSY_LOG, ///< RGB to HSY (Hue, Saturation, Luma) for log spaces + FIXED_FUNCTION_RGB_TO_HSY_VID, ///< RGB to HSY (Hue, Saturation, Luma) for video spaces }; /// Enumeration of the :cpp:class:`ExposureContrastTransform` transform algorithms. @@ -550,7 +566,8 @@ enum DynamicPropertyType DYNAMIC_PROPERTY_GAMMA, ///< Image gamma value (double floating point value) DYNAMIC_PROPERTY_GRADING_PRIMARY, ///< Used by GradingPrimaryTransform DYNAMIC_PROPERTY_GRADING_RGBCURVE, ///< Used by GradingRGBCurveTransform - DYNAMIC_PROPERTY_GRADING_TONE ///< Used by GradingToneTransform + DYNAMIC_PROPERTY_GRADING_TONE, ///< Used by GradingToneTransform + DYNAMIC_PROPERTY_GRADING_HUECURVE ///< Used by GradingHueCurveTransform }; /// Types for GradingRGBCurve. @@ -563,6 +580,38 @@ enum RGBCurveType RGB_NUM_CURVES }; +/// Types for GradingHueCurve. +enum HueCurveType +{ + HUE_HUE = 0, //!< Map input hue to output hue (where a diagonal line is the identity). + HUE_SAT, //!< Adjust saturation as a function of hue (a value of 1.0 is the identity). + HUE_LUM, //!< Adjust luma as a function of hue (a value of 1.0 is the identity). + LUM_SAT, //!< Adjust saturation as a function of luma (a value of 1.0 is the identity). + SAT_SAT, //!< Adjust saturation as a function of saturation (a diagonal is the identity). + LUM_LUM, //!< Adjust luma as a function of luma, maintaining hue & sat (diagonal is identity). + SAT_LUM, //!< Adjust luma as a function of saturation (a value of 1.0 is the identity). + HUE_FX, //!< Map input hue to delta output hue (a value of 0.0 is the identity). + HUE_NUM_CURVES +}; + +/// Types for GradingHueCurve. +enum HSYTransformStyle +{ + HSY_TRANSFORM_NONE = 0, //!< No RGB to HSY conversion (use an outboard conversion). + HSY_TRANSFORM_1 //!< Default RGB to Hue, Saturation, Luma conversion. +}; + +/// Types for GradingBSplineCurve. +enum BSplineType +{ + B_SPLINE = 0, //!< Monotonic quadratic B-spline used for the RGBM curves. + DIAGONAL_B_SPLINE, //!< Monotonic quadratic B-spline for the sat-sat and lum-lum curves. + HUE_HUE_B_SPLINE, //!< Monotonic and periodic B-spline used for the hue-hue curve. + PERIODIC_1_B_SPLINE, //!< Periodic, horizontal (at 1) B-spline for hue-sat and hue-lum curves. + PERIODIC_0_B_SPLINE, //!< Periodic, horizontal (at 0) B-spline used for the hue-fx curve. + HORIZONTAL1_B_SPLINE, //!< Horizontal (at 1) B-spline used for the lum-sat and sat-lum curves. +}; + /// Types for uniform data. enum UniformDataType { diff --git a/src/OpenColorIO/CMakeLists.txt b/src/OpenColorIO/CMakeLists.txt index ebef9328a3..1f50b28d35 100755 --- a/src/OpenColorIO/CMakeLists.txt +++ b/src/OpenColorIO/CMakeLists.txt @@ -94,6 +94,11 @@ set(SOURCES ops/gamma/GammaOpGPU.cpp ops/gamma/GammaOpUtils.cpp ops/gamma/GammaOp.cpp + ops/gradinghuecurve/GradingHueCurve.cpp + ops/gradinghuecurve/GradingHueCurveOpCPU.cpp + ops/gradinghuecurve/GradingHueCurveOpData.cpp + ops/gradinghuecurve/GradingHueCurveOpGPU.cpp + ops/gradinghuecurve/GradingHueCurveOp.cpp ops/gradingprimary/GradingPrimary.cpp ops/gradingprimary/GradingPrimaryOpCPU.cpp ops/gradingprimary/GradingPrimaryOpData.cpp @@ -169,6 +174,7 @@ set(SOURCES transforms/ExposureContrastTransform.cpp transforms/FileTransform.cpp transforms/FixedFunctionTransform.cpp + transforms/GradingHueCurveTransform.cpp transforms/GradingPrimaryTransform.cpp transforms/GradingRGBCurveTransform.cpp transforms/GradingToneTransform.cpp diff --git a/src/OpenColorIO/Config.cpp b/src/OpenColorIO/Config.cpp index 24d5b413fa..93fb4325e2 100644 --- a/src/OpenColorIO/Config.cpp +++ b/src/OpenColorIO/Config.cpp @@ -5725,6 +5725,19 @@ void Config::Impl::checkVersionConsistency(ConstTransformRcPtr & transform) cons throw Exception(ss.str().c_str()); } } + + if (m_majorVersion == 2 && m_minorVersion < 5 ) + { + if( ffstyle == FIXED_FUNCTION_RGB_TO_HSY_LIN || + ffstyle == FIXED_FUNCTION_RGB_TO_HSY_LOG || + ffstyle == FIXED_FUNCTION_RGB_TO_HSY_VID ) + { + std::ostringstream ss; + ss << "Only config version 2.5 (or higher) can have FixedFunctionTransform style '" + << FixedFunctionStyleToString(ffstyle) << "'."; + throw Exception(ss.str().c_str()); + } + } } else if (DynamicPtrCast(transform)) { @@ -5742,6 +5755,14 @@ void Config::Impl::checkVersionConsistency(ConstTransformRcPtr & transform) cons "GradingRGBCurveTransform."); } } + else if (DynamicPtrCast(transform)) + { + if (m_majorVersion == 2 && m_minorVersion < 5 ) + { + throw Exception("Only config version 2.5 (or higher) can have " + "GradingHueCurveTransform."); + } + } else if (DynamicPtrCast(transform)) { if (m_majorVersion < 2) diff --git a/src/OpenColorIO/DynamicProperty.cpp b/src/OpenColorIO/DynamicProperty.cpp index 5885492cfe..075973412f 100644 --- a/src/OpenColorIO/DynamicProperty.cpp +++ b/src/OpenColorIO/DynamicProperty.cpp @@ -8,6 +8,7 @@ #include "ops/gradingprimary/GradingPrimaryOpData.h" #include "ops/gradingrgbcurve/GradingRGBCurve.h" #include "ops/gradingtone/GradingToneOpData.h" +#include "ops/gradinghuecurve/GradingHueCurve.h" namespace OCIO_NAMESPACE { @@ -32,6 +33,12 @@ DynamicPropertyGradingRGBCurveRcPtr AsGradingRGBCurve(DynamicPropertyRcPtr & pro if (res) return res; throw Exception("Dynamic property value is not a grading RGB curve."); } +DynamicPropertyGradingHueCurveRcPtr AsGradingHueCurve(DynamicPropertyRcPtr & prop) +{ + auto res = OCIO_DYNAMIC_POINTER_CAST(prop); + if (res) return res; + throw Exception("Dynamic property value is not a grading hue curve."); +} DynamicPropertyGradingToneRcPtr AsGradingTone(DynamicPropertyRcPtr & prop) { auto res = OCIO_DYNAMIC_POINTER_CAST(prop); @@ -96,6 +103,11 @@ bool DynamicPropertyImpl::equals(const DynamicPropertyImpl & rhs) const auto rhst = dynamic_cast(&rhs); return lhst && rhst && (lhst->getValue() == rhst->getValue()); } + case DYNAMIC_PROPERTY_GRADING_HUECURVE: + { + auto lhst = dynamic_cast(this); + auto rhst = dynamic_cast(&rhs); + return lhst && rhst && (*lhst->getValue() == *rhst->getValue());} } // Different values. return false; @@ -125,6 +137,8 @@ DynamicPropertyDoubleImplRcPtr DynamicPropertyDoubleImpl::createEditableCopy() c return std::make_shared(getType(), getValue(), isDynamic()); } +//======================================================================================== + DynamicPropertyGradingPrimaryImpl::DynamicPropertyGradingPrimaryImpl(GradingStyle style, TransformDirection dir, const GradingPrimary & value, @@ -183,6 +197,8 @@ void DynamicPropertyGradingPrimaryImpl::setDirection(TransformDirection dir) noe } } +//======================================================================================== + DynamicPropertyGradingRGBCurveImpl::DynamicPropertyGradingRGBCurveImpl( const ConstGradingRGBCurveRcPtr & value, bool dynamic) : DynamicPropertyImpl(DYNAMIC_PROPERTY_GRADING_RGBCURVE, dynamic) @@ -213,12 +229,12 @@ bool DynamicPropertyGradingRGBCurveImpl::getLocalBypass() const int DynamicPropertyGradingRGBCurveImpl::getNumKnots() const { - return static_cast(m_knotsCoefs.m_knotsArray.size()); + return static_cast(m_knotsCoefs.m_numKnots); } int DynamicPropertyGradingRGBCurveImpl::getNumCoefs() const { - return static_cast(m_knotsCoefs.m_coefsArray.size()); + return static_cast(m_knotsCoefs.m_numCoefs); } const int * DynamicPropertyGradingRGBCurveImpl::getKnotsOffsetsArray() const @@ -254,8 +270,8 @@ unsigned int DynamicPropertyGradingRGBCurveImpl::GetMaxCoefs() void DynamicPropertyGradingRGBCurveImpl::precompute() { m_knotsCoefs.m_localBypass = false; - m_knotsCoefs.m_knotsArray.resize(0); - m_knotsCoefs.m_coefsArray.resize(0); + m_knotsCoefs.m_numCoefs = 0; + m_knotsCoefs.m_numKnots = 0; // Compute knots and coefficients for each control point and pack all knots and coefs of // all curves in one knots array and one coef array, using an offset array to find specific @@ -264,9 +280,9 @@ void DynamicPropertyGradingRGBCurveImpl::precompute() { ConstGradingBSplineCurveRcPtr curve = m_gradingRGBCurve->getCurve(c); auto curveImpl = dynamic_cast(curve.get()); - curveImpl->computeKnotsAndCoefs(m_knotsCoefs, static_cast(c)); + curveImpl->computeKnotsAndCoefs(m_knotsCoefs, static_cast(c), false); } - if (m_knotsCoefs.m_knotsArray.empty()) m_knotsCoefs.m_localBypass = true; + if (m_knotsCoefs.m_numKnots <= 0) m_knotsCoefs.m_localBypass = true; } DynamicPropertyGradingRGBCurveImplRcPtr DynamicPropertyGradingRGBCurveImpl::createEditableCopy() const @@ -276,6 +292,104 @@ DynamicPropertyGradingRGBCurveImplRcPtr DynamicPropertyGradingRGBCurveImpl::crea return res; } +//======================================================================================== + +DynamicPropertyGradingHueCurveImpl::DynamicPropertyGradingHueCurveImpl( + const ConstGradingHueCurveRcPtr & value, bool dynamic) + : DynamicPropertyImpl(DYNAMIC_PROPERTY_GRADING_HUECURVE, dynamic) +{ + m_gradingHueCurve = GradingHueCurve::Create(value); + // Convert control points from the UI into knots and coefficients for the apply. + precompute(); +} + +const ConstGradingHueCurveRcPtr & DynamicPropertyGradingHueCurveImpl::getValue() const +{ + return m_gradingHueCurve; +} + +void DynamicPropertyGradingHueCurveImpl::setValue(const ConstGradingHueCurveRcPtr & value) +{ + value->validate(); + + m_gradingHueCurve = value->createEditableCopy(); + // Convert control points from the UI into knots and coefficients for the apply. + precompute(); +} + +bool DynamicPropertyGradingHueCurveImpl::getLocalBypass() const +{ + return m_knotsCoefs.m_localBypass; +} + +int DynamicPropertyGradingHueCurveImpl::getNumKnots() const +{ + return static_cast(m_knotsCoefs.m_numKnots); +} + +int DynamicPropertyGradingHueCurveImpl::getNumCoefs() const +{ + return static_cast(m_knotsCoefs.m_numCoefs); +} + +const int * DynamicPropertyGradingHueCurveImpl::getKnotsOffsetsArray() const +{ + return m_knotsCoefs.m_knotsOffsetsArray.data(); +} + +const int * DynamicPropertyGradingHueCurveImpl::getCoefsOffsetsArray() const +{ + return m_knotsCoefs.m_coefsOffsetsArray.data(); +} + +const float * DynamicPropertyGradingHueCurveImpl::getKnotsArray() const +{ + return m_knotsCoefs.m_knotsArray.data(); +} + +const float * DynamicPropertyGradingHueCurveImpl::getCoefsArray() const +{ + return m_knotsCoefs.m_coefsArray.data(); +} + +unsigned int DynamicPropertyGradingHueCurveImpl::GetMaxKnots() +{ + return GradingBSplineCurveImpl::KnotsCoefs::MAX_NUM_KNOTS; +} + +unsigned int DynamicPropertyGradingHueCurveImpl::GetMaxCoefs() +{ + return GradingBSplineCurveImpl::KnotsCoefs::MAX_NUM_COEFS; +} + +void DynamicPropertyGradingHueCurveImpl::precompute() +{ + m_knotsCoefs.m_localBypass = false; + m_knotsCoefs.m_numCoefs = 0; + m_knotsCoefs.m_numKnots = 0; + + // Compute knots and coefficients for each control point and pack all knots and coefs of + // all curves in one knots array and one coef array, using an offset array to find specific + // curve data. + for (const auto c : { HUE_HUE, HUE_SAT, HUE_LUM, LUM_SAT, SAT_SAT, LUM_LUM, SAT_LUM, HUE_FX }) + { + ConstGradingBSplineCurveRcPtr curve = m_gradingHueCurve->getCurve(c); + auto curveImpl = dynamic_cast(curve.get()); + curveImpl->computeKnotsAndCoefs(m_knotsCoefs, static_cast(c), + m_gradingHueCurve->getDrawCurveOnly()); + } + if (m_knotsCoefs.m_numKnots <= 0) m_knotsCoefs.m_localBypass = true; +} + +DynamicPropertyGradingHueCurveImplRcPtr DynamicPropertyGradingHueCurveImpl::createEditableCopy() const +{ + auto res = std::make_shared(getValue(), isDynamic()); + res->m_knotsCoefs = m_knotsCoefs; + return res; +} + +//======================================================================================== + DynamicPropertyGradingToneImpl::DynamicPropertyGradingToneImpl(const GradingTone & value, GradingStyle style, bool dynamic) diff --git a/src/OpenColorIO/DynamicProperty.h b/src/OpenColorIO/DynamicProperty.h index 0e398d9b30..8257116e2d 100644 --- a/src/OpenColorIO/DynamicProperty.h +++ b/src/OpenColorIO/DynamicProperty.h @@ -151,7 +151,7 @@ class DynamicPropertyGradingRGBCurveImpl : public DynamicPropertyImpl, bool getLocalBypass() const; int getNumKnots() const; int getNumCoefs() const; - static int GetNumOffsetValues() { return 8; } + static int GetNumOffsetValues() { return 8; } // offset and num vals for four curves const int * getKnotsOffsetsArray() const; const int * getCoefsOffsetsArray() const; const float * getKnotsArray() const; @@ -173,6 +173,44 @@ class DynamicPropertyGradingRGBCurveImpl : public DynamicPropertyImpl, GradingBSplineCurveImpl::KnotsCoefs m_knotsCoefs{ 4 }; }; +class DynamicPropertyGradingHueCurveImpl; +typedef OCIO_SHARED_PTR DynamicPropertyGradingHueCurveImplRcPtr; + +class DynamicPropertyGradingHueCurveImpl : public DynamicPropertyImpl, + public DynamicPropertyGradingHueCurve +{ +public: + DynamicPropertyGradingHueCurveImpl() = delete; + DynamicPropertyGradingHueCurveImpl(const ConstGradingHueCurveRcPtr & value, bool dynamic); + ~DynamicPropertyGradingHueCurveImpl() = default; + const ConstGradingHueCurveRcPtr & getValue() const override; + void setValue(const ConstGradingHueCurveRcPtr & value) override; + + bool getLocalBypass() const; + int getNumKnots() const; + int getNumCoefs() const; + static int GetNumOffsetValues() { return 16; } // offset and num vals for eight curves + const int * getKnotsOffsetsArray() const; + const int * getCoefsOffsetsArray() const; + const float * getKnotsArray() const; + const float * getCoefsArray() const; + + const GradingBSplineCurveImpl::KnotsCoefs & getKnotsCoefs() const { return m_knotsCoefs; } + + static unsigned int GetMaxKnots(); + static unsigned int GetMaxCoefs(); + + DynamicPropertyGradingHueCurveImplRcPtr createEditableCopy() const; + +private: + void precompute(); + + ConstGradingHueCurveRcPtr m_gradingHueCurve; + + // Holds curve data as knots and coefs. There are 8 curves. + GradingBSplineCurveImpl::KnotsCoefs m_knotsCoefs{ 8 }; +}; + class DynamicPropertyGradingToneImpl; typedef OCIO_SHARED_PTR DynamicPropertyGradingToneImplRcPtr; diff --git a/src/OpenColorIO/GpuShaderUtils.cpp b/src/OpenColorIO/GpuShaderUtils.cpp index a125fa0fd1..6b32c01cf1 100644 --- a/src/OpenColorIO/GpuShaderUtils.cpp +++ b/src/OpenColorIO/GpuShaderUtils.cpp @@ -1428,6 +1428,25 @@ void AddLinToLogShader(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & st st.newLine() << "}"; } +void AddLinToLogShaderChannelBlue(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & st) +{ + const std::string pix(shaderCreator->getPixelName()); + + st.newLine() << "{"; // establish scope so local variable names won't conflict + st.indent(); + st.newLine() << st.floatKeywordConst() << " xbrk = 0.0041318374739483946;"; + st.newLine() << st.floatKeywordConst() << " shift = -0.000157849851665374;"; + st.newLine() << st.floatKeywordConst() << " m = 1. / (0.18 + shift);"; + st.newLine() << st.floatKeywordConst() << " base2 = 1.4426950408889634;"; // 1/log(2) + st.newLine() << st.floatKeywordConst() << " gain = 363.034608563;"; + st.newLine() << st.floatKeywordConst() << " offs = -7.;"; + st.newLine() << st.float3Decl("ylin") << " = " << pix << ".rgb * gain + offs;"; + st.newLine() << st.float3Decl("ylog") << " = base2 * log( ( " << pix << ".rgb + shift ) * m );"; + st.newLine() << pix << ".rgb.b = (" << pix << ".rgb.b < xbrk) ? ylin.z : ylog.z;"; + st.dedent(); + st.newLine() << "}"; +} + void AddLogToLinShader(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & st) { const std::string pix(shaderCreator->getPixelName()); @@ -1448,4 +1467,22 @@ void AddLogToLinShader(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & st st.newLine() << "}"; } +void AddLogToLinShaderChannelBlue(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & st) +{ + const std::string pix(shaderCreator->getPixelName()); + + st.newLine() << "{"; // establish scope so local variable names won't conflict + st.indent(); + st.newLine() << st.floatKeywordConst() << " ybrk = -5.5;"; + st.newLine() << st.floatKeywordConst() << " shift = -0.000157849851665374;"; + st.newLine() << st.floatKeywordConst() << " gain = 363.034608563;"; + st.newLine() << st.floatKeywordConst() << " offs = -7.;"; + st.newLine() << st.float3Decl("xlin") << " = (" << pix << ".rgb - offs) / gain;"; + st.newLine() << st.float3Decl("xlog") << " = pow( " << st.float3Const(2.0f) + << ", " << pix << ".rgb ) * (0.18 + shift) - shift;"; + st.newLine() << pix << ".rgb.b = (" << pix << ".rgb.b < ybrk) ? xlin.z : xlog.z;"; + st.dedent(); + st.newLine() << "}"; +} + } // namespace OCIO_NAMESPACE diff --git a/src/OpenColorIO/GpuShaderUtils.h b/src/OpenColorIO/GpuShaderUtils.h index f9ccd17ab1..72ef1c670d 100644 --- a/src/OpenColorIO/GpuShaderUtils.h +++ b/src/OpenColorIO/GpuShaderUtils.h @@ -275,9 +275,14 @@ std::string BuildResourceName(GpuShaderCreatorRcPtr & shaderCreator, const std:: // Convert scene-linear values to "grading log". void AddLinToLogShader(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & st); + +void AddLinToLogShaderChannelBlue(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & st); + // Convert "grading log" values to scene-linear. void AddLogToLinShader(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & st); +void AddLogToLinShaderChannelBlue(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & st); + } // namespace OCIO_NAMESPACE #endif diff --git a/src/OpenColorIO/OCIOYaml.cpp b/src/OpenColorIO/OCIOYaml.cpp index 02fbcb3ed6..c9361579fe 100644 --- a/src/OpenColorIO/OCIOYaml.cpp +++ b/src/OpenColorIO/OCIOYaml.cpp @@ -16,6 +16,7 @@ #include "ops/exposurecontrast/ExposureContrastOpData.h" #include "ops/gradingprimary/GradingPrimaryOpData.h" #include "ops/gradingrgbcurve/GradingRGBCurve.h" +#include "ops/gradinghuecurve/GradingHueCurve.h" #include "ops/gradingtone/GradingToneOpData.h" #include "ops/log/LogUtils.h" #include "ParseUtils.h" @@ -2142,6 +2143,168 @@ inline void save(YAML::Emitter & out, ConstGradingRGBCurveTransformRcPtr t) out << YAML::EndMap; } +// GradingHueCurveTransform + +inline void load(const YAML::Node & node, GradingHueCurveTransformRcPtr & t) +{ + CheckDuplicates(node); + + t = GradingHueCurveTransform::Create(GRADING_LOG); + + GradingBSplineCurveRcPtr hh; + GradingBSplineCurveRcPtr hs; + GradingBSplineCurveRcPtr hl; + GradingBSplineCurveRcPtr ls; + GradingBSplineCurveRcPtr ss; + GradingBSplineCurveRcPtr ll; + GradingBSplineCurveRcPtr sl; + GradingBSplineCurveRcPtr hfx; + + for (Iterator iter = node.begin(); iter != node.end(); ++iter) + { + const std::string & key = iter->first.as(); + + if (iter->second.IsNull() || !iter->second.IsDefined()) continue; + + if (key == "style") + { + std::string style; + load(iter->second, style); + t->setStyle(GradingStyleFromString(style.c_str())); + } + else if (key == "direction") + { + TransformDirection val; + load(iter->second, val); + t->setDirection(val); + } + else if (key == "hsy_transform") + { + std::string value; + load(iter->second, value); + if (value != "none") + { + throwValueError(node.Tag(), iter->first, "Unknown hsy_transform value."); + } + t->setRGBToHSY(HSY_TRANSFORM_NONE); + } + else if (key == "hue_hue") + { + hh = GradingBSplineCurve::Create(0, HUE_HUE); + load(iter->first, iter->second, hh); + } + else if (key == "hue_sat") + { + hs = GradingBSplineCurve::Create(0, HUE_SAT); + load(iter->first, iter->second, hs); + } + else if (key == "hue_lum") + { + hl = GradingBSplineCurve::Create(0, HUE_LUM); + load(iter->first, iter->second, hl); + } + else if (key == "lum_sat") + { + ls = GradingBSplineCurve::Create(0, LUM_SAT); + load(iter->first, iter->second, ls); + } + else if (key == "sat_sat") + { + ss = GradingBSplineCurve::Create(0, SAT_SAT); + load(iter->first, iter->second, ss); + } + else if (key == "lum_lum") + { + ll = GradingBSplineCurve::Create(0, LUM_LUM); + load(iter->first, iter->second, ll); + } + else if (key == "sat_lum") + { + sl = GradingBSplineCurve::Create(0, SAT_LUM); + load(iter->first, iter->second, sl); + } + else if (key == "hue_fx") + { + hfx = GradingBSplineCurve::Create(0, HUE_FX); + load(iter->first, iter->second, hfx); + } + else if (key == "name") + { + std::string name; + load(iter->second, name); + t->getFormatMetadata().setName(name.c_str()); + } + else + { + LogUnknownKeyWarning(node.Tag(), iter->first); + } + } + + if (!hh) hh = GradingHueCurveImpl::DefaultHueHue.createEditableCopy(); + if (!hs) hs = GradingHueCurveImpl::DefaultHueSat.createEditableCopy(); + if (!hl) hl = GradingHueCurveImpl::DefaultHueLum.createEditableCopy(); + if (!ls) ls = t->getStyle() == GRADING_LIN ? + GradingHueCurveImpl::DefaultLumSatLin.createEditableCopy() : + GradingHueCurveImpl::DefaultLumSat.createEditableCopy(); + if (!ss) ss = GradingHueCurveImpl::DefaultSatSat.createEditableCopy(); + if (!ll) ll = t->getStyle() == GRADING_LIN ? + GradingHueCurveImpl::DefaultLumLumLin.createEditableCopy() : + GradingHueCurveImpl::DefaultLumLum.createEditableCopy(); + if (!sl) sl = GradingHueCurveImpl::DefaultSatLum.createEditableCopy(); + if (!hfx) hfx = GradingHueCurveImpl::DefaultHueFx.createEditableCopy(); + + auto curves = GradingHueCurve::Create(hh, hs, hl, ls, ss, ll, sl, hfx); + + t->setValue(curves); +} + +inline void save(YAML::Emitter & out, ConstGradingHueCurveTransformRcPtr t) +{ + const auto & vals = t->getValue(); + auto & defCurves = t->getStyle() == GRADING_LIN ? GradingHueCurveImpl::DefaultCurvesLin : + GradingHueCurveImpl::DefaultCurves; + bool useLineBreaks = false; + for (int c = 0; c < HUE_NUM_CURVES; ++c) + { + const auto & curve = vals->getCurve(static_cast(c)); + if (*curve != defCurves[c]) + { + useLineBreaks = true; + break; + } + } + + out << YAML::VerbatimTag("GradingHueCurveTransform"); + if (!useLineBreaks) out << YAML::Flow; + out << YAML::BeginMap; + + EmitTransformName(out, t->getFormatMetadata()); + + const auto style = t->getStyle(); + out << YAML::Key << "style"; + out << YAML::Value << YAML::Flow << GradingStyleToString(style); + + if (t->getRGBToHSY() == HSY_TRANSFORM_NONE) + { + out << YAML::Key << "hsy_transform"; + out << YAML::Value << YAML::Flow << "none"; + } + + static const std::vector curveNames = { "hue_hue", "hue_sat", "hue_lum", + "lum_sat", "sat_sat", "lum_lum", "sat_lum", "hue_fx" }; + for (int c = 0; c < HUE_NUM_CURVES; ++c) + { + const auto & curve = vals->getCurve(static_cast(c)); + if ((*curve != defCurves[c]) || !(curve->slopesAreDefault())) + { + save(out, curveNames[c], curve); + } + } + + EmitBaseTransformKeyValues(out, t); + out << YAML::EndMap; +} + // GradingToneTransform inline void load(const YAML::Node & parent, const YAML::Node & node, GradingRGBMSW & rgbm, @@ -3115,6 +3278,12 @@ void load(const YAML::Node& node, TransformRcPtr& t) load(node, temp); t = temp; } + else if (type == "GradingHueCurveTransform") + { + GradingHueCurveTransformRcPtr temp; + load(node, temp); + t = temp; + } else if (type == "GradingToneTransform") { GradingToneTransformRcPtr temp; @@ -3219,6 +3388,9 @@ void save(YAML::Emitter& out, ConstTransformRcPtr t, unsigned int majorVersion) else if (ConstGradingRGBCurveTransformRcPtr GC_tran = \ DynamicPtrCast(t)) save(out, GC_tran); + else if (ConstGradingHueCurveTransformRcPtr GC_tran = \ + DynamicPtrCast(t)) + save(out, GC_tran); else if (ConstGradingToneTransformRcPtr GT_tran = \ DynamicPtrCast(t)) save(out, GT_tran); diff --git a/src/OpenColorIO/Op.cpp b/src/OpenColorIO/Op.cpp index 81dfe9867e..7e95baecfc 100755 --- a/src/OpenColorIO/Op.cpp +++ b/src/OpenColorIO/Op.cpp @@ -17,6 +17,7 @@ #include "ops/gamma/GammaOp.h" #include "ops/gradingprimary/GradingPrimaryOp.h" #include "ops/gradingrgbcurve/GradingRGBCurveOp.h" +#include "ops/gradinghuecurve/GradingHueCurveOp.h" #include "ops/gradingtone/GradingToneOp.h" #include "ops/log/LogOp.h" #include "ops/lut1d/Lut1DOp.h" @@ -120,6 +121,8 @@ const char * GetTypeName(OpData::Type type) return "GradingPrimary"; case OpData::GradingRGBCurveType: return "GradingRGBCurve"; + case OpData::GradingHueCurveType : + return "GradingHueCurve"; case OpData::GradingToneType: return "GradingTone"; case OpData::LogType: @@ -404,6 +407,9 @@ void ValidateDynamicProperty(OpRcPtr op, std::shared_ptr & prop, DynamicPrope case DYNAMIC_PROPERTY_GRADING_TONE: os << "Grading tone"; break; + case DYNAMIC_PROPERTY_GRADING_HUECURVE: + os << "Grading hue curve"; + break; } os << " dynamic property can only be there once."; LogWarning(os.str()); @@ -423,6 +429,7 @@ void OpRcPtrVec::validateDynamicProperties() DynamicPropertyDoubleImplRcPtr dpGamma; DynamicPropertyGradingPrimaryImplRcPtr dpGradingPrimary; DynamicPropertyGradingRGBCurveImplRcPtr dpGradingRGBCurve; + DynamicPropertyGradingHueCurveImplRcPtr dpGradingHueCurve; DynamicPropertyGradingToneImplRcPtr dpGradingTone; for (auto op : m_ops) @@ -433,6 +440,7 @@ void OpRcPtrVec::validateDynamicProperties() ValidateDynamicProperty(op, dpGamma, DYNAMIC_PROPERTY_GAMMA); ValidateDynamicProperty(op, dpGradingPrimary, DYNAMIC_PROPERTY_GRADING_PRIMARY); ValidateDynamicProperty(op, dpGradingRGBCurve, DYNAMIC_PROPERTY_GRADING_RGBCURVE); + ValidateDynamicProperty(op, dpGradingHueCurve, DYNAMIC_PROPERTY_GRADING_HUECURVE); ValidateDynamicProperty(op, dpGradingTone, DYNAMIC_PROPERTY_GRADING_TONE); } } @@ -542,6 +550,14 @@ void CreateOpVecFromOpData(OpRcPtrVec & ops, break; } + case OpData::GradingHueCurveType: + { + auto hueSrc = std::dynamic_pointer_cast(opData); + auto hue = std::make_shared(*hueSrc); + CreateGradingHueCurveOp(ops, hue, dir); + break; + } + case OpData::GradingToneType: { auto toneSrc = std::dynamic_pointer_cast(opData); diff --git a/src/OpenColorIO/Op.h b/src/OpenColorIO/Op.h index d88d8a1f4b..895c426d80 100644 --- a/src/OpenColorIO/Op.h +++ b/src/OpenColorIO/Op.h @@ -104,6 +104,7 @@ class OpData GammaType, // A gamma (i.e. enhancement of the Exponent) GradingPrimaryType, // A set of primary grading controls GradingRGBCurveType, // A rgb curve + GradingHueCurveType, // A hue curve GradingToneType, // A set of grading controls for tonal ranges LogType, // A log Lut1DType, // A 1D LUT @@ -267,6 +268,11 @@ class Op { throw Exception("Op does not implement grading rgb curve dynamic property."); } + virtual void replaceDynamicProperty(DynamicPropertyType /* type */, + DynamicPropertyGradingHueCurveImplRcPtr & /* prop */) + { + throw Exception("Op does not implement grading hue curve dynamic property."); + } virtual void replaceDynamicProperty(DynamicPropertyType /* type */, DynamicPropertyGradingToneImplRcPtr & /* prop */) { diff --git a/src/OpenColorIO/OpBuilders.h b/src/OpenColorIO/OpBuilders.h index cfeabcf6dd..369c2d001b 100644 --- a/src/OpenColorIO/OpBuilders.h +++ b/src/OpenColorIO/OpBuilders.h @@ -100,6 +100,12 @@ void BuildGradingPrimaryOp(OpRcPtrVec & ops, const GradingPrimaryTransform & transform, TransformDirection dir); +void BuildGradingHueCurveOp(OpRcPtrVec & ops, + const Config & config, + const ConstContextRcPtr & context, + const GradingHueCurveTransform & transform, + TransformDirection dir); + void BuildGradingRGBCurveOp(OpRcPtrVec & ops, const Config & config, const ConstContextRcPtr & context, diff --git a/src/OpenColorIO/OpOptimizers.cpp b/src/OpenColorIO/OpOptimizers.cpp index 58b395d5c6..164123f413 100755 --- a/src/OpenColorIO/OpOptimizers.cpp +++ b/src/OpenColorIO/OpOptimizers.cpp @@ -41,6 +41,7 @@ bool IsPairInverseEnabled(OpData::Type type, OptimizationFlags flags) case OpData::GradingPrimaryType: case OpData::GradingRGBCurveType: + case OpData::GradingHueCurveType: case OpData::GradingToneType: return HasFlag(flags, OPTIMIZATION_PAIR_IDENTITY_GRADING); diff --git a/src/OpenColorIO/ParseUtils.cpp b/src/OpenColorIO/ParseUtils.cpp index d028460ce5..47d4f2641a 100644 --- a/src/OpenColorIO/ParseUtils.cpp +++ b/src/OpenColorIO/ParseUtils.cpp @@ -373,6 +373,9 @@ const char * FixedFunctionStyleToString(FixedFunctionStyle style) case FIXED_FUNCTION_LIN_TO_PQ: return "Lin_TO_PQ"; case FIXED_FUNCTION_LIN_TO_GAMMA_LOG: return "Lin_TO_GammaLog"; case FIXED_FUNCTION_LIN_TO_DOUBLE_LOG: return "Lin_TO_DoubleLog"; + case FIXED_FUNCTION_RGB_TO_HSY_LIN: return "RGB_TO_HSY_LIN"; + case FIXED_FUNCTION_RGB_TO_HSY_LOG: return "RGB_TO_HSY_LOG"; + case FIXED_FUNCTION_RGB_TO_HSY_VID: return "RGB_TO_HSY_VID"; case FIXED_FUNCTION_ACES_GAMUTMAP_02: case FIXED_FUNCTION_ACES_GAMUTMAP_07: throw Exception("Unimplemented fixed function types: " @@ -407,6 +410,9 @@ FixedFunctionStyle FixedFunctionStyleFromString(const char * style) else if(str == "lin_to_pq") return FIXED_FUNCTION_LIN_TO_PQ; else if(str == "lin_to_gammalog") return FIXED_FUNCTION_LIN_TO_GAMMA_LOG; else if(str == "lin_to_doublelog") return FIXED_FUNCTION_LIN_TO_DOUBLE_LOG; + else if(str == "rgb_to_hsy_lin") return FIXED_FUNCTION_RGB_TO_HSY_LIN; + else if(str == "rgb_to_hsy_log") return FIXED_FUNCTION_RGB_TO_HSY_LOG; + else if(str == "rgb_to_hsy_vid") return FIXED_FUNCTION_RGB_TO_HSY_VID; // Default style is meaningless. std::stringstream ss; diff --git a/src/OpenColorIO/Transform.cpp b/src/OpenColorIO/Transform.cpp index 0d723212e6..74223096fa 100755 --- a/src/OpenColorIO/Transform.cpp +++ b/src/OpenColorIO/Transform.cpp @@ -15,6 +15,7 @@ #include "ops/gamma/GammaOp.h" #include "ops/gradingprimary/GradingPrimaryOp.h" #include "ops/gradingrgbcurve/GradingRGBCurveOp.h" +#include "ops/gradinghuecurve/GradingHueCurveOp.h" #include "ops/gradingtone/GradingToneOp.h" #include "ops/log/LogOp.h" #include "ops/lut1d/Lut1DOp.h" @@ -108,6 +109,11 @@ void BuildOps(OpRcPtrVec & ops, { BuildGradingRGBCurveOp(ops, config, context, *gradingCurveTransform, dir); } + else if (ConstGradingHueCurveTransformRcPtr hueCurveTransform = \ + DynamicPtrCast(transform)) + { + BuildGradingHueCurveOp(ops, config, context, *hueCurveTransform, dir); + } else if (ConstGradingToneTransformRcPtr gradingToneTransform = \ DynamicPtrCast(transform)) { @@ -232,6 +238,11 @@ std::ostream& operator<< (std::ostream & os, const Transform & transform) { os << *gradingRGBCurveTransform; } + else if (const GradingHueCurveTransform * hueCurveTransform = \ + dynamic_cast(t)) + { + os << *hueCurveTransform; + } else if (const GradingToneTransform * gradingToneTransform = \ dynamic_cast(t)) { @@ -328,6 +339,10 @@ void CreateTransform(GroupTransformRcPtr & group, ConstOpRcPtr & op) { CreateGradingPrimaryTransform(group, op); } + else if (DynamicPtrCast(data)) + { + CreateGradingHueCurveTransform(group, op); + } else if (DynamicPtrCast(data)) { CreateGradingRGBCurveTransform(group, op); diff --git a/src/OpenColorIO/fileformats/FileFormatCTF.cpp b/src/OpenColorIO/fileformats/FileFormatCTF.cpp index c9ada57cda..b1f0393038 100644 --- a/src/OpenColorIO/fileformats/FileFormatCTF.cpp +++ b/src/OpenColorIO/fileformats/FileFormatCTF.cpp @@ -465,6 +465,17 @@ class XMLParserHelper TAG_RGB_CURVE_RED }; + static const std::vector gradingHueCurveSubElements = { + TAG_HUE_CURVE_HUE_HUE, + TAG_HUE_CURVE_HUE_SAT, + TAG_HUE_CURVE_HUE_LUM, + TAG_HUE_CURVE_LUM_SAT, + TAG_HUE_CURVE_SAT_SAT, + TAG_HUE_CURVE_LUM_LUM, + TAG_HUE_CURVE_SAT_LUM, + TAG_HUE_CURVE_HUE_FX, + }; + XMLParserHelper * pImpl = (XMLParserHelper*)userData; if (!pImpl || !name || !*name) @@ -539,7 +550,7 @@ class XMLParserHelper } // Safety check to try and ensure that all new elements will get handled here. - static_assert(CTFReaderOpElt::NoType == 17, "Need to handle new type here"); + static_assert(CTFReaderOpElt::NoType == 18, "Need to handle new type here"); // Will allow to give better error feedback to the user if the // element name is not handled. If any case recognizes the name, @@ -586,6 +597,11 @@ class XMLParserHelper { pImpl->AddOpReader(CTFReaderOpElt::GradingRGBCurveType, name); } + else if (SupportedElement(name, pElt, TAG_HUE_CURVE, + TAG_PROCESS_LIST, recognizedName)) + { + pImpl->AddOpReader(CTFReaderOpElt::GradingHueCurveType, name); + } else if (SupportedElement(name, pElt, TAG_TONE, TAG_PROCESS_LIST, recognizedName)) { pImpl->AddOpReader(CTFReaderOpElt::GradingToneType, name); @@ -897,7 +913,9 @@ class XMLParserHelper pImpl->getXmlFilename())); } else if (SupportedElement(name, pElt, gradingRGBCurveSubElements, - TAG_RGB_CURVE, recognizedName)) + TAG_RGB_CURVE, recognizedName) || + SupportedElement(name, pElt, gradingHueCurveSubElements, + TAG_HUE_CURVE, recognizedName)) { pImpl->m_elms.push_back( std::make_shared( @@ -907,7 +925,9 @@ class XMLParserHelper pImpl->getXmlFilename())); } else if (SupportedElement(name, pElt, TAG_CURVE_CTRL_PNTS, - gradingRGBCurveSubElements, recognizedName)) + gradingRGBCurveSubElements, recognizedName) || + SupportedElement(name, pElt, TAG_CURVE_CTRL_PNTS, + gradingHueCurveSubElements, recognizedName)) { pImpl->m_elms.push_back( std::make_shared( @@ -917,7 +937,9 @@ class XMLParserHelper pImpl->getXmlFilename())); } else if (SupportedElement(name, pElt, TAG_CURVE_SLOPES, - gradingRGBCurveSubElements, recognizedName)) + gradingRGBCurveSubElements, recognizedName) || + SupportedElement(name, pElt, TAG_CURVE_SLOPES, + gradingHueCurveSubElements, recognizedName)) { pImpl->m_elms.push_back( std::make_shared( diff --git a/src/OpenColorIO/fileformats/ctf/CTFReaderHelper.cpp b/src/OpenColorIO/fileformats/ctf/CTFReaderHelper.cpp index 146913f4a3..79393fc489 100644 --- a/src/OpenColorIO/fileformats/ctf/CTFReaderHelper.cpp +++ b/src/OpenColorIO/fileformats/ctf/CTFReaderHelper.cpp @@ -1079,6 +1079,14 @@ CTFReaderOpEltRcPtr CTFReaderOpElt::GetReader(CTFReaderOpElt::Type type, } break; } + case CTFReaderOpElt::GradingHueCurveType: + { + if (!isCLF) + { + ADD_READER_FOR_VERSIONS_STARTING_AT(CTFReaderGradingHueCurveElt, 2_5); + } + break; + } case CTFReaderOpElt::GradingPrimaryType: { if (!isCLF) @@ -1636,6 +1644,19 @@ void CTFReaderDynamicParamElt::start(const char ** atts) GradingRGBCurveOpDataRcPtr pGCOp = pGC->getGradingRGBCurve(); pGCOp->getDynamicPropertyInternal()->makeDynamic(); } + else if (0 == Platform::Strcasecmp(TAG_DYN_PROP_HUECURVE, atts[i + 1])) + { + CTFReaderGradingHueCurveElt* pGC = + dynamic_cast(container.get()); + if (!pGC) + { + ThrowM(*this, "Dynamic parameter '", atts[i + 1], + "' is not supported in '", container->getName().c_str(), "'."); + } + + GradingHueCurveOpDataRcPtr pGCOp = pGC->getGradingHueCurve(); + pGCOp->getDynamicPropertyInternal()->makeDynamic(); + } else if (0 == Platform::Strcasecmp(TAG_DYN_PROP_TONE, atts[i + 1])) { CTFReaderGradingToneElt* pGT = @@ -2635,6 +2656,85 @@ const OpDataRcPtr CTFReaderGradingRGBCurveElt::getOp() const ////////////////////////////////////////////////////////// +CTFReaderGradingHueCurveElt::CTFReaderGradingHueCurveElt() + : m_gradingHueCurve(std::make_shared(GRADING_LOG)) +{ +} + +bool CTFReaderGradingHueCurveElt::isOpParameterValid(const char * att) const noexcept +{ + return CTFReaderOpElt::isOpParameterValid(att) || + 0 == Platform::Strcasecmp(ATTR_STYLE, att) || + 0 == Platform::Strcasecmp(ATTR_RGB_TO_HSY, att); +} + +void CTFReaderGradingHueCurveElt::start(const char ** atts) +{ + CTFReaderOpElt::start(atts); + + bool isStyleFound = false; + + unsigned i = 0; + while (atts[i]) + { + if (0 == Platform::Strcasecmp(ATTR_STYLE, atts[i])) + { + try + { + GradingStyle style; + TransformDirection dir; + ConvertStringToGradingStyleAndDir(atts[i + 1], style, dir); + m_gradingHueCurve->setStyle(style); + m_gradingHueCurve->setDirection(dir); + + // Initialize loading curve with corresponding style. + m_loadingHueCurve = GradingHueCurve::Create(style); + } + catch (Exception &) + { + ThrowM(*this, "Required attribute 'style' '", atts[i + 1], "' is invalid."); + } + isStyleFound = true; + } + else if (0 == Platform::Strcasecmp(ATTR_RGB_TO_HSY, atts[i])) + { + if (0 != Platform::Strcasecmp("none", atts[i + 1])) + { + std::ostringstream oss; + oss << "Unknown hsyTransform value: '" << atts[i + 1]; + oss << "' while parsing HueCurve."; + throwMessage(oss.str()); + } + + m_gradingHueCurve->setRGBToHSY(HSYTransformStyle::HSY_TRANSFORM_NONE); + } + + i += 2; + } + + if (!isStyleFound) + { + ThrowM(*this, "Required attribute 'style' is missing."); + } +} + +void CTFReaderGradingHueCurveElt::end() +{ + CTFReaderOpElt::end(); + + // Set the loaded data. + m_gradingHueCurve->setValue(m_loadingHueCurve); + // Validate the end result. + m_gradingHueCurve->validate(); +} + +const OpDataRcPtr CTFReaderGradingHueCurveElt::getOp() const +{ + return m_gradingHueCurve; +} + +////////////////////////////////////////////////////////// + CTFReaderGradingCurveElt::CTFReaderGradingCurveElt(const std::string & name, ContainerEltRcPtr pParent, unsigned int xmlLineNumber, @@ -2649,6 +2749,32 @@ CTFReaderGradingCurveElt::~CTFReaderGradingCurveElt() namespace { + +bool IsRGBCurveType(const std::string & name) +{ + if (0 == Platform::Strcasecmp(TAG_RGB_CURVE_RED, name.c_str()) || + 0 == Platform::Strcasecmp(TAG_RGB_CURVE_GREEN, name.c_str()) || + 0 == Platform::Strcasecmp(TAG_RGB_CURVE_BLUE, name.c_str()) || + 0 == Platform::Strcasecmp(TAG_RGB_CURVE_MASTER, name.c_str())) + { + return true; + } + else if (0 == Platform::Strcasecmp(TAG_HUE_CURVE_HUE_HUE, name.c_str()) || + 0 == Platform::Strcasecmp(TAG_HUE_CURVE_HUE_SAT, name.c_str()) || + 0 == Platform::Strcasecmp(TAG_HUE_CURVE_HUE_LUM, name.c_str()) || + 0 == Platform::Strcasecmp(TAG_HUE_CURVE_LUM_SAT, name.c_str()) || + 0 == Platform::Strcasecmp(TAG_HUE_CURVE_SAT_SAT, name.c_str()) || + 0 == Platform::Strcasecmp(TAG_HUE_CURVE_LUM_LUM, name.c_str()) || + 0 == Platform::Strcasecmp(TAG_HUE_CURVE_SAT_LUM, name.c_str()) || + 0 == Platform::Strcasecmp(TAG_HUE_CURVE_HUE_FX, name.c_str())) + { + return false; + } + std::ostringstream err; + err << "Illegal grading curve name '" << name << "'."; + throw Exception(err.str().c_str()); +} + RGBCurveType GetRGBCurveType(const std::string & name) { if (0 == Platform::Strcasecmp(TAG_RGB_CURVE_RED, name.c_str())) @@ -2668,18 +2794,67 @@ RGBCurveType GetRGBCurveType(const std::string & name) return RGB_MASTER; } std::ostringstream err; - err << "Invalid curve name '" << name << "'."; + err << "Illegal grading curve name '" << name << "'."; throw Exception(err.str().c_str()); } + +HueCurveType GetHueCurveType(const std::string & name) +{ + if (0 == Platform::Strcasecmp(TAG_HUE_CURVE_HUE_HUE, name.c_str())) + { + return HUE_HUE; + } + else if (0 == Platform::Strcasecmp(TAG_HUE_CURVE_HUE_SAT, name.c_str())) + { + return HUE_SAT; + } + else if (0 == Platform::Strcasecmp(TAG_HUE_CURVE_HUE_LUM, name.c_str())) + { + return HUE_LUM; + } + else if (0 == Platform::Strcasecmp(TAG_HUE_CURVE_LUM_SAT, name.c_str())) + { + return LUM_SAT; + } + else if (0 == Platform::Strcasecmp(TAG_HUE_CURVE_SAT_SAT, name.c_str())) + { + return SAT_SAT; + } + else if (0 == Platform::Strcasecmp(TAG_HUE_CURVE_LUM_LUM, name.c_str())) + { + return LUM_LUM; + } + else if (0 == Platform::Strcasecmp(TAG_HUE_CURVE_SAT_LUM, name.c_str())) + { + return SAT_LUM; + } + else if (0 == Platform::Strcasecmp(TAG_HUE_CURVE_HUE_FX, name.c_str())) + { + return HUE_FX; + } + std::ostringstream err; + err << "Illegal grading curve name '" << name << "'."; + throw Exception(err.str().c_str()); } +} // anon + void CTFReaderGradingCurveElt::start(const char ** /* atts */) { try { - const RGBCurveType type = GetRGBCurveType(getName()); - auto pRGBCurveElt = dynamic_cast(getParent().get()); - m_curve = pRGBCurveElt->getLoadingRGBCurve()->getCurve(type); + if (IsRGBCurveType(getName())) + { + const RGBCurveType type = GetRGBCurveType(getName()); + auto pRGBCurveElt = dynamic_cast(getParent().get()); + m_curve = pRGBCurveElt->getLoadingRGBCurve()->getCurve(type); + } + else + { + const HueCurveType type = GetHueCurveType(getName()); + auto pHueCurveElt = dynamic_cast(getParent().get()); + m_curve = pHueCurveElt->getLoadingHueCurve()->getCurve(type); + } } catch (Exception& ce) { diff --git a/src/OpenColorIO/fileformats/ctf/CTFReaderHelper.h b/src/OpenColorIO/fileformats/ctf/CTFReaderHelper.h index dff332595d..e4e1bacecd 100644 --- a/src/OpenColorIO/fileformats/ctf/CTFReaderHelper.h +++ b/src/OpenColorIO/fileformats/ctf/CTFReaderHelper.h @@ -15,6 +15,7 @@ #include "ops/gamma/GammaOpData.h" #include "ops/gradingprimary/GradingPrimaryOpData.h" #include "ops/gradingrgbcurve/GradingRGBCurveOpData.h" +#include "ops/gradinghuecurve/GradingHueCurveOpData.h" #include "ops/gradingtone/GradingToneOpData.h" #include "ops/log/LogOpData.h" #include "ops/log/LogUtils.h" @@ -323,6 +324,7 @@ class CTFReaderOpElt : public XmlReaderContainerElt GammaType, GradingPrimaryType, GradingRGBCurveType, + GradingHueCurveType, GradingToneType, InvLut1DType, InvLut3DType, @@ -698,6 +700,34 @@ class CTFReaderGradingRGBCurveElt : public CTFReaderOpElt GradingRGBCurveOpDataRcPtr m_gradingRGBCurve; }; +class CTFReaderGradingHueCurveElt : public CTFReaderOpElt +{ +public: + CTFReaderGradingHueCurveElt(); + ~CTFReaderGradingHueCurveElt() = default; + + void start(const char ** atts) override; + void end() override; + + const OpDataRcPtr getOp() const override; + + const GradingHueCurveOpDataRcPtr & getGradingHueCurve() const + { + return m_gradingHueCurve; + } + + // For sub-elements. + GradingHueCurveRcPtr & getLoadingHueCurve() { return m_loadingHueCurve; } + +protected: + bool isOpParameterValid(const char * att) const noexcept override; + +private: + // Editable HueCurve that will be set to the OpData when parsing of the element is done. + GradingHueCurveRcPtr m_loadingHueCurve; + GradingHueCurveOpDataRcPtr m_gradingHueCurve; +}; + class CTFReaderGradingCurveElt : public XmlReaderComplexElt { public: diff --git a/src/OpenColorIO/fileformats/ctf/CTFReaderUtils.h b/src/OpenColorIO/fileformats/ctf/CTFReaderUtils.h index 03f2d9c087..805e95bb12 100644 --- a/src/OpenColorIO/fileformats/ctf/CTFReaderUtils.h +++ b/src/OpenColorIO/fileformats/ctf/CTFReaderUtils.h @@ -29,6 +29,7 @@ static constexpr char TAG_DYN_PROP_CONTRAST[] = "CONTRAST"; static constexpr char TAG_DYN_PROP_EXPOSURE[] = "EXPOSURE"; static constexpr char TAG_DYN_PROP_GAMMA[] = "GAMMA"; static constexpr char TAG_DYN_PROP_PRIMARY[] = "PRIMARY"; +static constexpr char TAG_DYN_PROP_HUECURVE[] = "HUE_CURVE"; static constexpr char TAG_DYN_PROP_RGBCURVE[] = "RGB_CURVE"; static constexpr char TAG_DYN_PROP_TONE[] = "TONE"; static constexpr char TAG_DYN_PROP_LOOK[] = "LOOK_SWITCH"; @@ -68,6 +69,15 @@ static constexpr char TAG_PRIMARY_SATURATION[] = "Saturation"; static constexpr char TAG_PROCESS_LIST[] = "ProcessList"; static constexpr char TAG_RANGE[] = "Range"; static constexpr char TAG_REFERENCE[] = "Reference"; +static constexpr char TAG_HUE_CURVE[] = "GradingHueCurve"; +static constexpr char TAG_HUE_CURVE_HUE_HUE[] = "HueHue"; +static constexpr char TAG_HUE_CURVE_HUE_SAT[] = "HueSat"; +static constexpr char TAG_HUE_CURVE_HUE_LUM[] = "HueLum"; +static constexpr char TAG_HUE_CURVE_LUM_SAT[] = "LumSat"; +static constexpr char TAG_HUE_CURVE_SAT_SAT[] = "SatSat"; +static constexpr char TAG_HUE_CURVE_LUM_LUM[] = "LumLum"; +static constexpr char TAG_HUE_CURVE_SAT_LUM[] = "SatLum"; +static constexpr char TAG_HUE_CURVE_HUE_FX[] = "HueFx"; static constexpr char TAG_RGB_CURVE[] = "GradingRGBCurve"; static constexpr char TAG_RGB_CURVE_BLUE[] = "Blue"; static constexpr char TAG_RGB_CURVE_GREEN[] = "Green"; @@ -88,6 +98,7 @@ static constexpr char ATTR_BITDEPTH_IN[] = "inBitDepth"; static constexpr char ATTR_BITDEPTH_OUT[] = "outBitDepth"; static constexpr char ATTR_BYPASS[] = "bypass"; static constexpr char ATTR_BYPASS_LIN_TO_LOG[] = "bypassLinToLog"; +static constexpr char ATTR_RGB_TO_HSY[] = "hsyTransform"; static constexpr char ATTR_CENTER[] = "center"; static constexpr char ATTR_CHAN[] = "channel"; static constexpr char ATTR_COMP_CLF_VERSION[] = "compCLFversion"; diff --git a/src/OpenColorIO/fileformats/ctf/CTFTransform.cpp b/src/OpenColorIO/fileformats/ctf/CTFTransform.cpp index cf07176568..5a21b6cb1d 100644 --- a/src/OpenColorIO/fileformats/ctf/CTFTransform.cpp +++ b/src/OpenColorIO/fileformats/ctf/CTFTransform.cpp @@ -16,6 +16,8 @@ #include "ops/gradingprimary/GradingPrimaryOpData.h" #include "ops/gradingrgbcurve/GradingRGBCurve.h" #include "ops/gradingrgbcurve/GradingRGBCurveOpData.h" +#include "ops/gradinghuecurve/GradingHueCurve.h" +#include "ops/gradinghuecurve/GradingHueCurveOpData.h" #include "ops/gradingtone/GradingToneOpData.h" #include "ops/log/LogOpData.h" #include "ops/log/LogUtils.h" @@ -272,6 +274,15 @@ CTFVersion GetOpMinimumVersion(const ConstOpDataRcPtr & op) { minVersion = CTF_PROCESS_LIST_VERSION_2_4; } + else if ( ff->getStyle() == FixedFunctionOpData::RGB_TO_HSY_LOG + || ff->getStyle() == FixedFunctionOpData::HSY_LOG_TO_RGB + || ff->getStyle() == FixedFunctionOpData::RGB_TO_HSY_LIN + || ff->getStyle() == FixedFunctionOpData::HSY_LIN_TO_RGB + || ff->getStyle() == FixedFunctionOpData::RGB_TO_HSY_VID + || ff->getStyle() == FixedFunctionOpData::HSY_VID_TO_RGB ) + { + minVersion = CTF_PROCESS_LIST_VERSION_2_5; + } break; } case OpData::GradingPrimaryType: @@ -282,6 +293,11 @@ CTFVersion GetOpMinimumVersion(const ConstOpDataRcPtr & op) minVersion = CTF_PROCESS_LIST_VERSION_2_0; break; } + case OpData::GradingHueCurveType: + { + minVersion = CTF_PROCESS_LIST_VERSION_2_5; + break; + } case OpData::ExponentType: { auto exp = OCIO_DYNAMIC_POINTER_CAST(op); @@ -1613,6 +1629,142 @@ void GradingRGBCurveWriter::writeContent() const m_formatter.writeEmptyTag(TAG_DYNAMIC_PARAMETER, attributes); } } +/////////////////////////////////////////////////////////////////////////////// + +class GradingHueCurveWriter : public OpWriter +{ +public: + GradingHueCurveWriter() = delete; + GradingHueCurveWriter(const GradingHueCurveWriter&) = delete; + GradingHueCurveWriter& operator=(const GradingHueCurveWriter&) = delete; + GradingHueCurveWriter(XmlFormatter & formatter, ConstGradingHueCurveOpDataRcPtr primary); + virtual ~GradingHueCurveWriter(); + +protected: + ConstOpDataRcPtr getOp() const override; + const char * getTagName() const override; + void getAttributes(XmlFormatter::Attributes & attributes) const override; + void writeContent() const override; + +private: + void writeCurve(const char * tag, const ConstGradingBSplineCurveRcPtr & curve) const; + ConstGradingHueCurveOpDataRcPtr m_curves; +}; + +GradingHueCurveWriter::GradingHueCurveWriter(XmlFormatter & formatter, + ConstGradingHueCurveOpDataRcPtr curves) + : OpWriter(formatter) + , m_curves(curves) +{ +} + +GradingHueCurveWriter::~GradingHueCurveWriter() +{ +} + +ConstOpDataRcPtr GradingHueCurveWriter::getOp() const +{ + return m_curves; +} + +const char * GradingHueCurveWriter::getTagName() const +{ + return TAG_HUE_CURVE; +} + +void GradingHueCurveWriter::getAttributes(XmlFormatter::Attributes& attributes) const +{ + OpWriter::getAttributes(attributes); + + const auto style = m_curves->getStyle(); + const auto dir = m_curves->getDirection(); + + const auto styleStr = ConvertGradingStyleAndDirToString(style, dir); + attributes.push_back(XmlFormatter::Attribute(ATTR_STYLE, styleStr)); + + if (m_curves->getRGBToHSY() == HSYTransformStyle::HSY_TRANSFORM_NONE) + { + attributes.push_back(XmlFormatter::Attribute(ATTR_RGB_TO_HSY, "none")); + } +} + +void GradingHueCurveWriter::writeCurve(const char * tag, + const ConstGradingBSplineCurveRcPtr & curve) const +{ + m_formatter.writeStartTag(tag, XmlFormatter::Attributes()); + { + XmlScopeIndent si0(m_formatter); + m_formatter.writeStartTag(TAG_CURVE_CTRL_PNTS, XmlFormatter::Attributes()); + { + XmlScopeIndent si1(m_formatter); + const size_t numPnts = curve->getNumControlPoints(); + + // Write 1 control point per line in the form of "X Y" + for (size_t i = 0; i < numPnts; ++i) + { + const auto & ctPt = curve->getControlPoint(i); + std::ostringstream oss; + SetOStream(0.f, oss); + oss << ctPt.m_x << " " << ctPt.m_y; + m_formatter.writeContent(oss.str()); + } + } + m_formatter.writeEndTag(TAG_CURVE_CTRL_PNTS); + + if (!curve->slopesAreDefault()) + { + m_formatter.writeStartTag(TAG_CURVE_SLOPES, XmlFormatter::Attributes()); + { + XmlScopeIndent si1(m_formatter); + // (Number of slopes is always the same as control points.) + const size_t numSlopes = curve->getNumControlPoints(); + std::ostringstream oss; + SetOStream(0.f, oss); + for (size_t i = 0; i < numSlopes; ++i) + { + const float val = curve->getSlope(i); + oss << val << " "; + } + m_formatter.writeContent(oss.str()); + } + m_formatter.writeEndTag(TAG_CURVE_SLOPES); + } + } + + m_formatter.writeEndTag(tag); +} + +void GradingHueCurveWriter::writeContent() const +{ + const auto & vals = m_curves->getValue(); + + auto & defCurve = (m_curves->getStyle() == GRADING_LIN) ? GradingHueCurveImpl::DefaultCurvesLin: + GradingHueCurveImpl::DefaultCurves; + static const std::vector curveTags = { + TAG_HUE_CURVE_HUE_HUE, + TAG_HUE_CURVE_HUE_SAT, + TAG_HUE_CURVE_HUE_LUM, + TAG_HUE_CURVE_LUM_SAT, + TAG_HUE_CURVE_SAT_SAT, + TAG_HUE_CURVE_LUM_LUM, + TAG_HUE_CURVE_SAT_LUM, + TAG_HUE_CURVE_HUE_FX + }; + for (int c = 0; c < HUE_NUM_CURVES; ++c) + { + const auto & curve = vals->getCurve(static_cast(c)); + if ((*curve != defCurve[c]) || !(curve->slopesAreDefault())) + { + writeCurve(curveTags[c], curve); + } + } + if (m_curves->isDynamic()) + { + XmlFormatter::Attributes attributes; + attributes.push_back(XmlFormatter::Attribute(ATTR_PARAM, TAG_DYN_PROP_HUECURVE)); + m_formatter.writeEmptyTag(TAG_DYNAMIC_PARAMETER, attributes); + } +} /////////////////////////////////////////////////////////////////////////////// @@ -2694,6 +2846,20 @@ void TransformWriter::writeOps(const CTFVersion & version) const opWriter.write(); break; } + case OpData::GradingHueCurveType: + { + if (m_isCLF) + { + ThrowWriteOp("GradingHueCurve"); + } + + auto hue = OCIO_DYNAMIC_POINTER_CAST(op); + GradingHueCurveWriter opWriter(m_formatter, hue); + opWriter.setInputBitdepth(inBD); + opWriter.setOutputBitdepth(outBD); + opWriter.write(); + break; + } case OpData::GradingToneType: { if (m_isCLF) diff --git a/src/OpenColorIO/fileformats/ctf/CTFTransform.h b/src/OpenColorIO/fileformats/ctf/CTFTransform.h index 5473f6ef36..f8fb741c26 100644 --- a/src/OpenColorIO/fileformats/ctf/CTFTransform.h +++ b/src/OpenColorIO/fileformats/ctf/CTFTransform.h @@ -119,9 +119,12 @@ static const CTFVersion CTF_PROCESS_LIST_VERSION_2_1 = CTFVersion(2, 1); // the LIN_TO_PQ, LIN_TO_GAMMA_LOG, AND LIN_TO_DOUBLE_LOG FixedFunctionOps. static const CTFVersion CTF_PROCESS_LIST_VERSION_2_4 = CTFVersion(2, 4); +// Version 2.5 2025-08 adds the GradingHueCurve. +static const CTFVersion CTF_PROCESS_LIST_VERSION_2_5 = CTFVersion(2, 5); + // Add new version before this line // and do not forget to update the following line. -static const CTFVersion CTF_PROCESS_LIST_VERSION = CTF_PROCESS_LIST_VERSION_2_4; +static const CTFVersion CTF_PROCESS_LIST_VERSION = CTF_PROCESS_LIST_VERSION_2_5; // Version 1.0 initial Autodesk version for InfoElt. diff --git a/src/OpenColorIO/ops/exposurecontrast/ExposureContrastOpCPU.cpp b/src/OpenColorIO/ops/exposurecontrast/ExposureContrastOpCPU.cpp index d2288344c9..6a3e7383a5 100644 --- a/src/OpenColorIO/ops/exposurecontrast/ExposureContrastOpCPU.cpp +++ b/src/OpenColorIO/ops/exposurecontrast/ExposureContrastOpCPU.cpp @@ -86,6 +86,7 @@ bool ECRendererBase::hasDynamicProperty(DynamicPropertyType type) const break; case DYNAMIC_PROPERTY_GRADING_PRIMARY: case DYNAMIC_PROPERTY_GRADING_RGBCURVE: + case DYNAMIC_PROPERTY_GRADING_HUECURVE: case DYNAMIC_PROPERTY_GRADING_TONE: default: break; @@ -118,6 +119,7 @@ DynamicPropertyRcPtr ECRendererBase::getDynamicProperty(DynamicPropertyType type break; case DYNAMIC_PROPERTY_GRADING_PRIMARY: case DYNAMIC_PROPERTY_GRADING_RGBCURVE: + case DYNAMIC_PROPERTY_GRADING_HUECURVE: case DYNAMIC_PROPERTY_GRADING_TONE: default: throw Exception("Dynamic property type not supported by ExposureContrast."); diff --git a/src/OpenColorIO/ops/exposurecontrast/ExposureContrastOpData.cpp b/src/OpenColorIO/ops/exposurecontrast/ExposureContrastOpData.cpp index 4197b7dde4..869b28e607 100644 --- a/src/OpenColorIO/ops/exposurecontrast/ExposureContrastOpData.cpp +++ b/src/OpenColorIO/ops/exposurecontrast/ExposureContrastOpData.cpp @@ -311,6 +311,7 @@ bool ExposureContrastOpData::hasDynamicProperty(DynamicPropertyType type) const break; case DYNAMIC_PROPERTY_GRADING_PRIMARY: case DYNAMIC_PROPERTY_GRADING_RGBCURVE: + case DYNAMIC_PROPERTY_GRADING_HUECURVE: case DYNAMIC_PROPERTY_GRADING_TONE: default: break; @@ -344,6 +345,7 @@ ExposureContrastOpData::getDynamicProperty(DynamicPropertyType type) const break; case DYNAMIC_PROPERTY_GRADING_PRIMARY: case DYNAMIC_PROPERTY_GRADING_RGBCURVE: + case DYNAMIC_PROPERTY_GRADING_HUECURVE: case DYNAMIC_PROPERTY_GRADING_TONE: default: throw Exception("Dynamic property type not supported by ExposureContrast."); @@ -383,6 +385,7 @@ void ExposureContrastOpData::replaceDynamicProperty(DynamicPropertyType type, break; case DYNAMIC_PROPERTY_GRADING_PRIMARY: case DYNAMIC_PROPERTY_GRADING_RGBCURVE: + case DYNAMIC_PROPERTY_GRADING_HUECURVE: case DYNAMIC_PROPERTY_GRADING_TONE: default: throw Exception("Dynamic property type not supported by ExposureContrast."); diff --git a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.cpp b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.cpp index feb387db5f..ed7fcb362e 100644 --- a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.cpp +++ b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.cpp @@ -238,6 +238,60 @@ class Renderer_HSV_TO_RGB : public OpCPU void apply(const void * inImg, void * outImg, long numPixels) const override; }; +class Renderer_RGB_TO_HSY_LOG : public OpCPU +{ +public: + Renderer_RGB_TO_HSY_LOG() = delete; + explicit Renderer_RGB_TO_HSY_LOG(ConstFixedFunctionOpDataRcPtr & data); + + void apply(const void * inImg, void * outImg, long numPixels) const override; +}; + +class Renderer_HSY_LOG_TO_RGB : public OpCPU +{ +public: + Renderer_HSY_LOG_TO_RGB() = delete; + explicit Renderer_HSY_LOG_TO_RGB(ConstFixedFunctionOpDataRcPtr & data); + + void apply(const void * inImg, void * outImg, long numPixels) const override; +}; + +class Renderer_RGB_TO_HSY_VID : public OpCPU +{ +public: + Renderer_RGB_TO_HSY_VID() = delete; + explicit Renderer_RGB_TO_HSY_VID(ConstFixedFunctionOpDataRcPtr & data); + + void apply(const void * inImg, void * outImg, long numPixels) const override; +}; + +class Renderer_HSY_VID_TO_RGB : public OpCPU +{ +public: + Renderer_HSY_VID_TO_RGB() = delete; + explicit Renderer_HSY_VID_TO_RGB(ConstFixedFunctionOpDataRcPtr & data); + + void apply(const void * inImg, void * outImg, long numPixels) const override; +}; + +class Renderer_RGB_TO_HSY_LIN : public OpCPU +{ +public: + Renderer_RGB_TO_HSY_LIN() = delete; + explicit Renderer_RGB_TO_HSY_LIN(ConstFixedFunctionOpDataRcPtr & data); + + void apply(const void * inImg, void * outImg, long numPixels) const override; +}; + +class Renderer_HSY_LIN_TO_RGB : public OpCPU +{ +public: + Renderer_HSY_LIN_TO_RGB() = delete; + explicit Renderer_HSY_LIN_TO_RGB(ConstFixedFunctionOpDataRcPtr & data); + + void apply(const void * inImg, void * outImg, long numPixels) const override; +}; + class Renderer_XYZ_TO_xyY : public OpCPU { public: @@ -1534,6 +1588,240 @@ void Renderer_HSV_TO_RGB::apply(const void * inImg, void * outImg, long numPixel } } +void applyHSYToRGB(const void * inImg, void * outImg, long numPixels, FixedFunctionOpData::Style funcStyle) +{ + const float * in = (const float *)inImg; + float * out = (float *)outImg; + + for(long idx=0; idx= 0.f ? gainS : (2.f * c) / (denom + discrim * 2.f); + } + } + else if (funcStyle == FixedFunctionOpData::HSY_LOG_TO_RGB) + { + const float satGain = 4.f; + const float currSat = distRgb * satGain; + gainS = sat / std::max(1e-10f, currSat); + } + else // funcStyle == FixedFunctionOpData::HSY_VID_TO_RGB + { + const float satGain = 1.25f; + const float currSat = distRgb * satGain; + gainS = sat / std::max(1e-10f, currSat); + } + + out[0] = luma + gainS * (red - luma); // red + out[1] = luma + gainS * (grn - luma); // grn + out[2] = luma + gainS * (blu - luma); // blu + out[3] = in[3]; // alpha + + in += 4; + out += 4; + } + +} + +void applyRGBToHSY(const void * inImg, void * outImg, long numPixels, FixedFunctionOpData::Style funcStyle) +{ + const float * in = (const float *)inImg; + float * out = (float *)outImg; + + for (long idx=0; idx(func); } + + case FixedFunctionOpData::RGB_TO_HSY_LOG: + { + return std::make_shared(func); + } + case FixedFunctionOpData::HSY_LOG_TO_RGB: + { + return std::make_shared(func); + } + + case FixedFunctionOpData::RGB_TO_HSY_LIN: + { + return std::make_shared(func); + } + case FixedFunctionOpData::HSY_LIN_TO_RGB: + { + return std::make_shared(func); + } + + case FixedFunctionOpData::RGB_TO_HSY_VID: + { + return std::make_shared(func); + } + case FixedFunctionOpData::HSY_VID_TO_RGB: + { + return std::make_shared(func); + } } throw Exception("Unsupported FixedFunction style"); diff --git a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.cpp b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.cpp index f4b32c8c15..8e5d5833d0 100644 --- a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.cpp +++ b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.cpp @@ -77,7 +77,12 @@ constexpr char LIN_TO_GAMMA_LOG_STR[] = "Lin_TO_GammaLog"; constexpr char GAMMA_LOG_TO_LIN_STR[] = "GammaLog_TO_Lin"; constexpr char LIN_TO_DOUBLE_LOG_STR[] = "Lin_TO_DoubleLog"; constexpr char DOUBLE_LOG_TO_LIN_STR[] = "DoubleLog_TO_Lin"; - +constexpr char RGB_TO_HSY_LIN_STR[] = "RGB_TO_HSY_LIN"; +constexpr char RGB_TO_HSY_LOG_STR[] = "RGB_TO_HSY_LOG"; +constexpr char RGB_TO_HSY_VID_STR[] = "RGB_TO_HSY_VID"; +constexpr char HSY_LOG_TO_RGB_STR[] = "HSY_LOG_TO_RGB"; +constexpr char HSY_LIN_TO_RGB_STR[] = "HSY_LIN_TO_RGB"; +constexpr char HSY_VID_TO_RGB_STR[] = "HSY_VID_TO_RGB"; // NOTE: Converts the enumeration value to its string representation (i.e. CLF reader). // It could add details for error reporting. @@ -160,6 +165,18 @@ const char * FixedFunctionOpData::ConvertStyleToString(Style style, bool detaile return LIN_TO_DOUBLE_LOG_STR; case DOUBLE_LOG_TO_LIN: return DOUBLE_LOG_TO_LIN_STR; + case RGB_TO_HSY_LIN: + return RGB_TO_HSY_LIN_STR; + case RGB_TO_HSY_LOG: + return RGB_TO_HSY_LOG_STR; + case RGB_TO_HSY_VID: + return RGB_TO_HSY_VID_STR; + case HSY_LOG_TO_RGB: + return HSY_LOG_TO_RGB_STR; + case HSY_LIN_TO_RGB: + return HSY_LIN_TO_RGB_STR; + case HSY_VID_TO_RGB: + return HSY_VID_TO_RGB_STR; } std::stringstream ss("Unknown FixedFunction style: "); @@ -318,6 +335,30 @@ FixedFunctionOpData::Style FixedFunctionOpData::GetStyle(const char * name) { return DOUBLE_LOG_TO_LIN; } + else if (0 == Platform::Strcasecmp(name, RGB_TO_HSY_LIN_STR)) + { + return RGB_TO_HSY_LIN; + } + else if (0 == Platform::Strcasecmp(name, RGB_TO_HSY_LOG_STR)) + { + return RGB_TO_HSY_LOG; + } + else if (0 == Platform::Strcasecmp(name, RGB_TO_HSY_VID_STR)) + { + return RGB_TO_HSY_VID; + } + else if (0 == Platform::Strcasecmp(name, HSY_LOG_TO_RGB_STR)) + { + return HSY_LOG_TO_RGB; + } + else if (0 == Platform::Strcasecmp(name, HSY_LIN_TO_RGB_STR)) + { + return HSY_LIN_TO_RGB; + } + else if (0 == Platform::Strcasecmp(name, HSY_VID_TO_RGB_STR)) + { + return HSY_VID_TO_RGB; + } } std::string st("Unknown FixedFunction style: "); @@ -393,6 +434,21 @@ FixedFunctionOpData::Style FixedFunctionOpData::ConvertStyle(FixedFunctionStyle { return FixedFunctionOpData::RGB_TO_HSV; } + case FIXED_FUNCTION_RGB_TO_HSY_LIN: + { + return isForward ? FixedFunctionOpData::RGB_TO_HSY_LIN : + FixedFunctionOpData::HSY_LIN_TO_RGB; + } + case FIXED_FUNCTION_RGB_TO_HSY_LOG: + { + return isForward ? FixedFunctionOpData::RGB_TO_HSY_LOG : + FixedFunctionOpData::HSY_LOG_TO_RGB; + } + case FIXED_FUNCTION_RGB_TO_HSY_VID: + { + return isForward ? FixedFunctionOpData::RGB_TO_HSY_VID : + FixedFunctionOpData::HSY_VID_TO_RGB; + } case FIXED_FUNCTION_XYZ_TO_xyY: { return FixedFunctionOpData::XYZ_TO_xyY; @@ -511,6 +567,19 @@ FixedFunctionStyle FixedFunctionOpData::ConvertStyle(FixedFunctionOpData::Style case FixedFunctionOpData::LIN_TO_DOUBLE_LOG: case FixedFunctionOpData::DOUBLE_LOG_TO_LIN: return FIXED_FUNCTION_LIN_TO_DOUBLE_LOG; + + case FixedFunctionOpData::RGB_TO_HSY_LIN: + case FixedFunctionOpData::HSY_LIN_TO_RGB: + return FIXED_FUNCTION_RGB_TO_HSY_LIN; + + case FixedFunctionOpData::RGB_TO_HSY_LOG: + case FixedFunctionOpData::HSY_LOG_TO_RGB: + return FIXED_FUNCTION_RGB_TO_HSY_LOG; + + case FixedFunctionOpData::RGB_TO_HSY_VID: + case FixedFunctionOpData::HSY_VID_TO_RGB: + return FIXED_FUNCTION_RGB_TO_HSY_VID; + } std::stringstream ss("Unknown FixedFunction style: "); @@ -913,6 +982,39 @@ void FixedFunctionOpData::invert() noexcept break; } + case RGB_TO_HSY_LOG: + { + setStyle(HSY_LOG_TO_RGB); + break; + } + case HSY_LOG_TO_RGB: + { + setStyle(RGB_TO_HSY_LOG); + break; + } + + case RGB_TO_HSY_LIN: + { + setStyle(HSY_LIN_TO_RGB); + break; + } + case HSY_LIN_TO_RGB: + { + setStyle(RGB_TO_HSY_LIN); + break; + } + + case RGB_TO_HSY_VID: + { + setStyle(HSY_VID_TO_RGB); + break; + } + case HSY_VID_TO_RGB: + { + setStyle(RGB_TO_HSY_VID); + break; + } + case XYZ_TO_xyY: { setStyle(xyY_TO_XYZ); @@ -1009,6 +1111,9 @@ TransformDirection FixedFunctionOpData::getDirection() const noexcept case FixedFunctionOpData::ACES_GAMUT_COMPRESS_20_FWD: case FixedFunctionOpData::REC2100_SURROUND_FWD: case FixedFunctionOpData::RGB_TO_HSV: + case FixedFunctionOpData::RGB_TO_HSY_LOG: + case FixedFunctionOpData::RGB_TO_HSY_LIN: + case FixedFunctionOpData::RGB_TO_HSY_VID: case FixedFunctionOpData::XYZ_TO_xyY: case FixedFunctionOpData::XYZ_TO_uvY: case FixedFunctionOpData::XYZ_TO_LUV: @@ -1029,6 +1134,9 @@ TransformDirection FixedFunctionOpData::getDirection() const noexcept case FixedFunctionOpData::ACES_GAMUT_COMPRESS_20_INV: case FixedFunctionOpData::REC2100_SURROUND_INV: case FixedFunctionOpData::HSV_TO_RGB: + case FixedFunctionOpData::HSY_LOG_TO_RGB: + case FixedFunctionOpData::HSY_LIN_TO_RGB: + case FixedFunctionOpData::HSY_VID_TO_RGB: case FixedFunctionOpData::xyY_TO_XYZ: case FixedFunctionOpData::uvY_TO_XYZ: case FixedFunctionOpData::LUV_TO_XYZ: diff --git a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.h b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.h index b3441ed158..c7c92589d0 100644 --- a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.h +++ b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.h @@ -61,7 +61,13 @@ class FixedFunctionOpData : public OpData ACES_TONESCALE_COMPRESS_20_FWD, // ACES2 Tonescale and chroma compression ACES_TONESCALE_COMPRESS_20_INV, // ACES2 Tonescale and chroma compression (inv) ACES_GAMUT_COMPRESS_20_FWD, // ACES2 Gamut compression - ACES_GAMUT_COMPRESS_20_INV // ACES2 Gamut compression (inv) + ACES_GAMUT_COMPRESS_20_INV, // ACES2 Gamut compression (inv) + RGB_TO_HSY_LIN, // RGB to HSY (Hue, Saturation, Luminance) for linear spaces + RGB_TO_HSY_LOG, // RGB to HSY (Hue, Saturation, Luma) for log spaces + RGB_TO_HSY_VID, // RGB to HSY (Hue, Saturation, Luma) for video spaces + HSY_LIN_TO_RGB, // HSY (Hue, Saturation, Luminance) to RGB for linear spaces + HSY_LOG_TO_RGB, // HSY (Hue, Saturation, Luma) to RGB for log spaces + HSY_VID_TO_RGB // HSY (Hue, Saturation, Luma) to RGB for video spaces }; static const char * ConvertStyleToString(Style style, bool detailed); diff --git a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.cpp b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.cpp index 0acd73c102..0ead67f67c 100644 --- a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.cpp +++ b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.cpp @@ -1692,6 +1692,134 @@ void Add_RGB_TO_HSV(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & ss) ss.newLine() << pxl << ".rgb = " << ss.float3Const("hue * 1./6.", "sat", "val") << ";"; } +void Add_RGB_TO_HSY(GpuShaderCreatorRcPtr & shaderCreator, + GpuShaderText & ss, + FixedFunctionOpData::Style funcStyle) +{ + const std::string pxl(shaderCreator->getPixelName()); + + ss.newLine() << ss.float3Decl("lumaWeights") << " = " << ss.float3Const(0.2126f, 0.7152f, 0.0722f) << ";"; + ss.newLine() << ss.float3Decl("ones") << " = " << ss.float3Const(1.f, 1.f, 1.f) << ";"; + ss.newLine() << "float luma = dot(" << pxl << ".rgb, lumaWeights);"; + ss.newLine() << "float minRGB = min( " << pxl << ".x, min( " << pxl << ".y, " << pxl << ".z ) );"; + ss.newLine() << "float maxRGB = max( " << pxl << ".x, max( " << pxl << ".y, " << pxl << ".z ) );"; + ss.newLine() << ss.float3Decl("RGBm") << " = " << pxl << ".rgb - luma;"; + ss.newLine() << "float distRGB = dot( abs(RGBm), ones );"; + if (funcStyle == FixedFunctionOpData::RGB_TO_HSY_LIN) + { + ss.newLine() << "float sumRGB = dot( " << pxl << ".rgb, ones );"; + ss.newLine() << "float sat_hi = distRGB / max(0.07 * distRGB + 1e-6, 0.15 + sumRGB);"; + ss.newLine() << "float sat_lo = distRGB * 5.;"; + ss.newLine() << "float alpha = clamp( (luma - 0.001) / (0.01 - 0.001), 0., 1.);"; + + ss.newLine() << "float sat = sat_lo + alpha * (sat_hi - sat_lo);"; + ss.newLine() << "sat *= 1.4;"; + } + else if (funcStyle == FixedFunctionOpData::RGB_TO_HSY_LOG) + { + ss.newLine() << "float sat = distRGB * 4.;"; + } + else // RGB_TO_HSY_VID + { + ss.newLine() << "float sat = distRGB * 1.25;"; + } + // NB: Unlike typical HSV, HSY maps magenta rather than red to a hue of zero. + // (This allows for better placement of red when manipulating curves in a UI.) + ss.newLine() << "float hue = 0.0;"; + ss.newLine() << "if (minRGB != maxRGB) {"; + ss.newLine() << " float OneOverMaxMinusMin = 1.0 / (maxRGB - minRGB);"; + ss.newLine() << " if ( maxRGB == " << pxl << ".r ) hue = 1.0 + (" << pxl << ".g - " << pxl << ".b) * OneOverMaxMinusMin;"; + ss.newLine() << " else if ( maxRGB == " << pxl << ".g ) hue = 3.0 + (" << pxl << ".b - " << pxl << ".r) * OneOverMaxMinusMin;"; + ss.newLine() << " else hue = 5.0 + (" << pxl << ".r - " << pxl << ".g) * OneOverMaxMinusMin;"; + ss.newLine() << "}"; + ss.newLine() << "" << pxl << ".r = hue * 1./6.; " << pxl << ".g = sat; " << pxl << ".b = luma;"; +} + +void Add_HSY_TO_RGB(GpuShaderCreatorRcPtr & shaderCreator, + GpuShaderText & ss, + FixedFunctionOpData::Style funcStyle) +{ + const std::string pxl(shaderCreator->getPixelName()); + + ss.newLine() << "float luma = " << pxl << ".z;"; + ss.newLine() << "float Hue = " << pxl << ".x - 1./6.;"; + ss.newLine() << "Hue = (luma < 0.) ? Hue + 0.5 : Hue;"; + ss.newLine() << "Hue = ( Hue - floor( Hue ) ) * 6.0;"; + ss.newLine() << "float R = abs(Hue - 3.0) - 1.0;"; + ss.newLine() << "float G = 2.0 - abs(Hue - 2.0);"; + ss.newLine() << "float B = 2.0 - abs(Hue - 4.0);"; + ss.newLine() << ss.float3Decl("RGB0") << " = " << ss.float3Const("R", "G", "B") << ";"; + ss.newLine() << "RGB0 = clamp( RGB0, 0., 1. );"; + + ss.newLine() << ss.float3Decl("lumaWeights") << " = " << ss.float3Const(0.2126f, 0.7152f, 0.0722f ) << ";"; + ss.newLine() << ss.float3Decl("ones") << " = " << ss.float3Const(1.f, 1.f, 1.f ) << ";"; + ss.newLine() << "float currY = dot(RGB0, lumaWeights);"; + ss.newLine() << "RGB0 *= luma / currY;"; + + ss.newLine() << "float sat = " << pxl << ".y;"; + ss.newLine() << "float distRGB = dot( abs(RGB0 - luma), ones );"; + if (funcStyle == FixedFunctionOpData::HSY_LIN_TO_RGB) + { + ss.newLine() << "float sumRGB = dot( RGB0, ones );"; + ss.newLine() << "float k = 0.15;"; + ss.newLine() << "float lo_gain = 5.;"; + ss.newLine() << "sat /= 1.4;"; + ss.newLine() << "float tmp = -sat * sumRGB + sat * 3. * luma + distRGB;"; + ss.newLine() << "tmp = max(1e-6, tmp);"; + ss.newLine() << "float s1 = sat * (k + 3. * luma) / tmp;"; + ss.newLine() << "s1 = min(s1, 50.);"; + ss.newLine() << "float s0 = sat / max(1e-10, distRGB * lo_gain);"; + ss.newLine() << "float alpha = clamp( (luma - 0.001) / (0.01 - 0.001), 0., 1.);"; + ss.newLine() << "float a = distRGB * lo_gain * (1. - alpha) * (sumRGB - 3. * luma);"; + ss.newLine() << "float b = distRGB * lo_gain * (1. - alpha) * (k + 3. * luma) + distRGB * alpha - sat * (sumRGB - 3. * luma);"; + ss.newLine() << "float c = -sat * (k + 3. * luma);"; + ss.newLine() << "float discrim = sqrt( b * b - 4. * a * c );"; + ss.newLine() << "float denom = -discrim - b;"; + ss.newLine() << "float sm = (2. * c) / denom;"; + ss.newLine() << "sm = (sm >= 0.) ? sm : (2. * c) / (denom + discrim * 2.);"; + ss.newLine() << "float gainS = (alpha == 1.) ? s1 : (alpha == 0.) ? s0 : sm;"; + } + else if (funcStyle == FixedFunctionOpData::HSY_LOG_TO_RGB) + { + ss.newLine() << "float gainS = sat / max(1e-10, distRGB * 4.);"; + } + else // HSY_VID_TO_RGB + { + ss.newLine() << "float gainS = sat / max(1e-10, distRGB * 1.25);"; + } + ss.newLine() << "" << pxl << ".rgb = luma + gainS * (RGB0 - luma);"; +} + +void Add_RGB_TO_HSY_LOG(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & ss) +{ + Add_RGB_TO_HSY(shaderCreator, ss, FixedFunctionOpData::RGB_TO_HSY_LOG); +} + +void Add_RGB_TO_HSY_LIN(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & ss) +{ + Add_RGB_TO_HSY(shaderCreator, ss, FixedFunctionOpData::RGB_TO_HSY_LIN); +} + +void Add_RGB_TO_HSY_VID(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & ss) +{ + Add_RGB_TO_HSY(shaderCreator, ss, FixedFunctionOpData::RGB_TO_HSY_VID); +} + +void Add_HSY_LOG_TO_RGB(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & ss) +{ + Add_HSY_TO_RGB(shaderCreator, ss, FixedFunctionOpData::HSY_LOG_TO_RGB); +} + +void Add_HSY_LIN_TO_RGB(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & ss) +{ + Add_HSY_TO_RGB(shaderCreator, ss, FixedFunctionOpData::HSY_LIN_TO_RGB); +} + +void Add_HSY_VID_TO_RGB(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & ss) +{ + Add_HSY_TO_RGB(shaderCreator, ss, FixedFunctionOpData::HSY_VID_TO_RGB); +} + void Add_HSV_TO_RGB(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & ss) { const std::string pxl(shaderCreator->getPixelName()); @@ -2089,6 +2217,14 @@ void GetFixedFunctionGPUShaderProgram(GpuShaderCreatorRcPtr & shaderCreator, ConstFixedFunctionOpDataRcPtr & func) { GpuShaderText ss(shaderCreator->getLanguage()); + GetFixedFunctionGPUProcessingText(shaderCreator, ss, func); + shaderCreator->addToFunctionShaderCode(ss.string().c_str()); +} + +void GetFixedFunctionGPUProcessingText(GpuShaderCreatorRcPtr & shaderCreator, + GpuShaderText & ss, + ConstFixedFunctionOpDataRcPtr & func) +{ ss.indent(); ss.newLine() << ""; @@ -2241,6 +2377,36 @@ void GetFixedFunctionGPUShaderProgram(GpuShaderCreatorRcPtr & shaderCreator, Add_RGB_TO_HSV(shaderCreator, ss); break; } + case FixedFunctionOpData::RGB_TO_HSY_LOG: + { + Add_RGB_TO_HSY_LOG(shaderCreator, ss); + break; + } + case FixedFunctionOpData::RGB_TO_HSY_LIN: + { + Add_RGB_TO_HSY_LIN(shaderCreator, ss); + break; + } + case FixedFunctionOpData::RGB_TO_HSY_VID: + { + Add_RGB_TO_HSY_VID(shaderCreator, ss); + break; + } + case FixedFunctionOpData::HSY_LOG_TO_RGB: + { + Add_HSY_LOG_TO_RGB(shaderCreator, ss); + break; + } + case FixedFunctionOpData::HSY_LIN_TO_RGB: + { + Add_HSY_LIN_TO_RGB(shaderCreator, ss); + break; + } + case FixedFunctionOpData::HSY_VID_TO_RGB: + { + Add_HSY_VID_TO_RGB(shaderCreator, ss); + break; + } case FixedFunctionOpData::HSV_TO_RGB: { Add_HSV_TO_RGB(shaderCreator, ss); @@ -2313,7 +2479,6 @@ void GetFixedFunctionGPUShaderProgram(GpuShaderCreatorRcPtr & shaderCreator, ss.newLine() << "}"; ss.dedent(); - shaderCreator->addToFunctionShaderCode(ss.string().c_str()); } } // OCIO_NAMESPACE diff --git a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.h b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.h index 802c99af54..71708fa03c 100644 --- a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.h +++ b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.h @@ -15,6 +15,10 @@ namespace OCIO_NAMESPACE void GetFixedFunctionGPUShaderProgram(GpuShaderCreatorRcPtr & shaderCreator, ConstFixedFunctionOpDataRcPtr & func); +void GetFixedFunctionGPUProcessingText(GpuShaderCreatorRcPtr & shaderCreator, + GpuShaderText & shaderText, + ConstFixedFunctionOpDataRcPtr & func); + } // namespace OCIO_NAMESPACE #endif diff --git a/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurve.cpp b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurve.cpp new file mode 100644 index 0000000000..84e9a71773 --- /dev/null +++ b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurve.cpp @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + +#include +#include + +#include + +#include "ops/gradinghuecurve/GradingHueCurve.h" + +namespace OCIO_NAMESPACE +{ + +namespace +{ + +static const std::vector DefaultHueHueCtrl{ {0.0f, 0.0f}, + {1.f/6.f, 1.f/6.f}, + {2.f/6.f, 2.f/6.f}, + {0.5f, 0.5f}, + {4.f/6.f, 4.f/6.f}, + {5.f/6.f, 5.f/6.f} }; + +static const std::vector DefaultHueSatCtrl{ {0.0f, 1.0f}, + {1.f/6.f, 1.0f}, + {2.f/6.f, 1.0f}, + {0.5f, 1.0f}, + {4.f/6.f, 1.0f}, + {5.f/6.f, 1.0f} }; + +static const std::vector DefaultHueFxCtrl{ {0.0f, 0.0f}, + {1.f/6.f, 0.f}, + {2.f/6.f, 0.f}, + {0.5f, 0.f}, + {4.f/6.f, 0.f}, + {5.f/6.f, 0.f} }; + +static const std::vector DefaultLumSatCtrl{ {0.0f, 1.0f}, {0.5f, 1.0f}, {1.0f, 1.0f} }; +static const std::vector DefaultLumSatLinCtrl{ {-7.0f, 1.0f}, {0.f, 1.0f}, {7.0f, 1.0f} }; + +static const std::vector DefaultSatSatCtrl{ {0.0f, 0.0f}, {0.5f, 0.5f}, {1.0f, 1.0f} }; +static const std::vector DefaultSatLumCtrl{ {0.0f, 1.0f}, {0.5f, 1.0f}, {1.0f, 1.0f} }; + +static const std::vector DefaultLumLumCtrl{ { 0.f, 0.f },{ 0.5f, 0.5f },{ 1.0f, 1.0f } }; +static const std::vector DefaultLumLumLinCtrl{ { -7.0f, -7.0f },{ 0.f, 0.f },{ 7.0f, 7.0f } }; + +} // anon + +const GradingBSplineCurveImpl GradingHueCurveImpl::DefaultHueHue(DefaultHueHueCtrl, BSplineType::HUE_HUE_B_SPLINE ); +const GradingBSplineCurveImpl GradingHueCurveImpl::DefaultHueSat(DefaultHueSatCtrl, BSplineType::PERIODIC_1_B_SPLINE ); +const GradingBSplineCurveImpl GradingHueCurveImpl::DefaultHueLum(DefaultHueSatCtrl, BSplineType::PERIODIC_1_B_SPLINE ); // HUE_LUM use the same as HUE_SAT +const GradingBSplineCurveImpl GradingHueCurveImpl::DefaultHueFx(DefaultHueFxCtrl, BSplineType::PERIODIC_0_B_SPLINE ); +const GradingBSplineCurveImpl GradingHueCurveImpl::DefaultLumSat(DefaultLumSatCtrl, BSplineType::HORIZONTAL1_B_SPLINE); +const GradingBSplineCurveImpl GradingHueCurveImpl::DefaultLumSatLin(DefaultLumSatLinCtrl, BSplineType::HORIZONTAL1_B_SPLINE); +const GradingBSplineCurveImpl GradingHueCurveImpl::DefaultSatSat(DefaultSatSatCtrl, BSplineType::DIAGONAL_B_SPLINE); +const GradingBSplineCurveImpl GradingHueCurveImpl::DefaultSatLum(DefaultSatLumCtrl, BSplineType::HORIZONTAL1_B_SPLINE); +const GradingBSplineCurveImpl GradingHueCurveImpl::DefaultLumLum(DefaultLumLumCtrl, BSplineType::DIAGONAL_B_SPLINE); +const GradingBSplineCurveImpl GradingHueCurveImpl::DefaultLumLumLin(DefaultLumLumLinCtrl, BSplineType::DIAGONAL_B_SPLINE); + +const std::array, static_cast(HUE_NUM_CURVES)> + GradingHueCurveImpl::DefaultCurvesLin( { std::ref(DefaultHueHue), + std::ref(DefaultHueSat), + std::ref(DefaultHueLum), + std::ref(DefaultLumSatLin), + std::ref(DefaultSatSat), + std::ref(DefaultLumLumLin), + std::ref(DefaultSatLum), + std::ref(DefaultHueFx) }); + + +const std::array, static_cast(HUE_NUM_CURVES)> + GradingHueCurveImpl::DefaultCurves( { std::ref(DefaultHueHue), + std::ref(DefaultHueSat), + std::ref(DefaultHueLum), + std::ref(DefaultLumSat), + std::ref(DefaultSatSat), + std::ref(DefaultLumLum), + std::ref(DefaultSatLum), + std::ref(DefaultHueFx) }); + +GradingHueCurveImpl::GradingHueCurveImpl() : + GradingHueCurveImpl(GRADING_LOG) +{} + +GradingHueCurveImpl::GradingHueCurveImpl(GradingStyle style) +{ + auto & curves = (style == GRADING_LIN) ? GradingHueCurveImpl::DefaultCurvesLin: + GradingHueCurveImpl::DefaultCurves; + + for (int c = 0; c < HUE_NUM_CURVES; ++c) + { + m_curves[c] = curves[c].get().createEditableCopy(); + } +} + +GradingHueCurveImpl::GradingHueCurveImpl( + ConstGradingBSplineCurveRcPtr hueHue, + ConstGradingBSplineCurveRcPtr hueSat, + ConstGradingBSplineCurveRcPtr hueLum, + ConstGradingBSplineCurveRcPtr lumSat, + ConstGradingBSplineCurveRcPtr satSat, + ConstGradingBSplineCurveRcPtr lumLum, + ConstGradingBSplineCurveRcPtr satLum, + ConstGradingBSplineCurveRcPtr hueFx) +{ + m_curves[HUE_HUE] = hueHue->createEditableCopy(); + m_curves[HUE_SAT] = hueSat->createEditableCopy(); + m_curves[HUE_LUM] = hueLum->createEditableCopy(); + m_curves[LUM_SAT] = lumSat->createEditableCopy(); + m_curves[SAT_SAT] = satSat->createEditableCopy(); + m_curves[LUM_LUM] = lumLum->createEditableCopy(); + m_curves[SAT_LUM] = satLum->createEditableCopy(); + m_curves[HUE_FX] = hueFx->createEditableCopy(); +} + +GradingHueCurveImpl::GradingHueCurveImpl(const ConstGradingHueCurveRcPtr & rhs) +{ + auto impl = dynamic_cast(rhs.get()); + if (impl) + { + for (int c = 0; c < HUE_NUM_CURVES; ++c) + { + m_curves[c] = impl->m_curves[c]->createEditableCopy(); + } + m_drawCurveOnly = rhs->getDrawCurveOnly(); + } +} + +GradingHueCurveRcPtr GradingHueCurveImpl::createEditableCopy() const +{ + auto newCurve = std::make_shared(); + for (int c = 0; c < HUE_NUM_CURVES; ++c) + { + newCurve->m_curves[c] = m_curves[c]->createEditableCopy(); + } + newCurve->setDrawCurveOnly(m_drawCurveOnly); + + GradingHueCurveRcPtr res = newCurve; + return res; +} + +namespace +{ +const char * CurveTypeName(int c) +{ + const HueCurveType curveType = static_cast(c); + switch (curveType) + { + case HUE_HUE: + return "hue_hue"; + case HUE_SAT: + return "hue_sat"; + case HUE_LUM: + return "hue_lum"; + case LUM_SAT: + return "lum_sat"; + case SAT_SAT: + return "sat_sat"; + case LUM_LUM: + return "lum_lum"; + case SAT_LUM: + return "sat_lum"; + case HUE_FX: + return "hue_fx"; + case HUE_NUM_CURVES: + default: + break; + } + return "illegal"; +} +} + +void GradingHueCurveImpl::validate() const +{ + for (int c = 0; c < HUE_NUM_CURVES; ++c) + { + try + { + m_curves[c]->validate(); + } + catch (Exception & e) + { + std::ostringstream oss; + oss << "GradingHueCurve validation failed for '" << CurveTypeName(c) << "' curve " + << "with: " << e.what(); + throw Exception(oss.str().c_str()); + } + + // Unless drawCurveOnly is enabled, check that the spline type is correct for + // the given hue curve type. + if (!getDrawCurveOnly()) + { + const BSplineType splineType = m_curves[c]->getSplineType(); + const HueCurveType hueType = static_cast(c); + if (splineType != GetBSplineTypeForHueCurveType(hueType)) + { + std::ostringstream oss; + oss << "GradingHueCurve validation failed: '" << CurveTypeName(c) << "' curve " + << "is of the wrong BSplineType."; + throw Exception(oss.str().c_str()); + } + } + } +} + +bool GradingHueCurveImpl::isIdentity() const +{ + for (int c = 0; c < HUE_NUM_CURVES; ++c) + { + if (!IsGradingCurveIdentity(m_curves[c])) + { + return false; + } + } + return true; +} + +bool GradingHueCurveImpl::getDrawCurveOnly() const +{ + return m_drawCurveOnly; +} + +void GradingHueCurveImpl::setDrawCurveOnly(bool drawCurveOnly) +{ + m_drawCurveOnly = drawCurveOnly; +} + +bool GradingHueCurveImpl::isHueCurveTypeValid(HueCurveType c) const +{ + return ( c >= HUE_HUE && + c < HUE_NUM_CURVES ); +} + +ConstGradingBSplineCurveRcPtr GradingHueCurveImpl::getCurve(HueCurveType c) const +{ + if(!isHueCurveTypeValid(c)) + { + throw Exception("The HueCurveType provided is illegal"); + } + + return m_curves[c]; +} + +GradingBSplineCurveRcPtr GradingHueCurveImpl::getCurve(HueCurveType c) +{ + if(!isHueCurveTypeValid(c)) + { + throw Exception("The HueCurveType provided is illegal"); + } + + return m_curves[c]; +} + +BSplineType GradingHueCurve::GetBSplineTypeForHueCurveType(HueCurveType curveType) +{ + switch (curveType) + { + case HUE_HUE: + return BSplineType::HUE_HUE_B_SPLINE; + case HUE_SAT: + return BSplineType::PERIODIC_1_B_SPLINE; + case HUE_LUM: + return BSplineType::PERIODIC_1_B_SPLINE; + case LUM_SAT: + return BSplineType::HORIZONTAL1_B_SPLINE; + case SAT_SAT: + return BSplineType::DIAGONAL_B_SPLINE; + case LUM_LUM: + return BSplineType::DIAGONAL_B_SPLINE; + case SAT_LUM: + return BSplineType::HORIZONTAL1_B_SPLINE; + case HUE_FX: + return BSplineType::PERIODIC_0_B_SPLINE; + case HUE_NUM_CURVES: + default: + return BSplineType::B_SPLINE; + } +} + +GradingHueCurveRcPtr GradingHueCurve::Create(GradingStyle style) +{ + auto newCurve = std::make_shared(style); + GradingHueCurveRcPtr res = newCurve; + return res; +} + +GradingHueCurveRcPtr GradingHueCurve::Create(const ConstGradingHueCurveRcPtr & rhs) +{ + auto newCurve = std::make_shared(rhs); + GradingHueCurveRcPtr res = newCurve; + return res; +} + +GradingHueCurveRcPtr GradingHueCurve::Create( + ConstGradingBSplineCurveRcPtr hueHue, + ConstGradingBSplineCurveRcPtr hueSat, + ConstGradingBSplineCurveRcPtr hueLum, + ConstGradingBSplineCurveRcPtr lumSat, + ConstGradingBSplineCurveRcPtr satSat, + ConstGradingBSplineCurveRcPtr lumLum, + ConstGradingBSplineCurveRcPtr satLum, + ConstGradingBSplineCurveRcPtr hueFx) +{ + auto newCurve = std::make_shared( + hueHue, + hueSat, + hueLum, + lumSat, + satSat, + lumLum, + satLum, + hueFx); + + newCurve->validate(); + + GradingHueCurveRcPtr res = newCurve; + return res; +} + +bool operator==(const GradingHueCurve & lhs, const GradingHueCurve & rhs) +{ + + for (int c = 0; c < HUE_NUM_CURVES; ++c) + { + if (*(lhs.getCurve(static_cast(c))) != *(rhs.getCurve(static_cast(c)))) + { + return false; + } + } + if (lhs.getDrawCurveOnly() != rhs.getDrawCurveOnly()) + { + return false; + } + return true; +} + +bool operator!=(const GradingHueCurve & lhs, const GradingHueCurve & rhs) +{ + return !(lhs == rhs); +} + +} // namespace OCIO_NAMESPACE + diff --git a/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurve.h b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurve.h new file mode 100644 index 0000000000..6d9bd35522 --- /dev/null +++ b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurve.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + + +#ifndef INCLUDED_OCIO_GRADINGHUECURVE_H +#define INCLUDED_OCIO_GRADINGHUECURVE_H + + +#include + +#include "ops/gradingrgbcurve/GradingBSplineCurve.h" + +namespace OCIO_NAMESPACE +{ + +// Class to hold the hue curve data that is used in the corresponding dynamic property and in +// the CTF reader. This allows moving some of the code from DynamicProperty to here. The +// dynamic property is then used by the OpData, which is then used by the Op and Transform. +class GradingHueCurveImpl : public GradingHueCurve +{ +public: + GradingHueCurveImpl(); + GradingHueCurveImpl(GradingStyle style); + GradingHueCurveImpl( + ConstGradingBSplineCurveRcPtr hueHue, + ConstGradingBSplineCurveRcPtr hueSat, + ConstGradingBSplineCurveRcPtr hueLum, + ConstGradingBSplineCurveRcPtr lumSat, + ConstGradingBSplineCurveRcPtr satSat, + ConstGradingBSplineCurveRcPtr lumLum, + ConstGradingBSplineCurveRcPtr satLum, + ConstGradingBSplineCurveRcPtr hueFx ); + GradingHueCurveImpl(const ConstGradingHueCurveRcPtr & rhs); + + GradingHueCurveRcPtr createEditableCopy() const override; + + void validate() const override; + bool isIdentity() const override; + bool getDrawCurveOnly() const override; + void setDrawCurveOnly(bool drawCurveOnly) override; + ConstGradingBSplineCurveRcPtr getCurve(HueCurveType) const override; + GradingBSplineCurveRcPtr getCurve(HueCurveType) override; + + static const GradingBSplineCurveImpl DefaultHueHue; + static const GradingBSplineCurveImpl DefaultHueSat; + static const GradingBSplineCurveImpl DefaultHueLum; + static const GradingBSplineCurveImpl DefaultHueFx; + static const GradingBSplineCurveImpl DefaultLumSat; + static const GradingBSplineCurveImpl DefaultLumSatLin; + static const GradingBSplineCurveImpl DefaultSatSat; + static const GradingBSplineCurveImpl DefaultSatLum; + static const GradingBSplineCurveImpl DefaultLumLum; + static const GradingBSplineCurveImpl DefaultLumLumLin; + + static const std::array, static_cast(HUE_NUM_CURVES)> DefaultCurvesLin; + static const std::array, static_cast(HUE_NUM_CURVES)> DefaultCurves; + +private: + bool isHueCurveTypeValid(HueCurveType c) const; + + bool m_drawCurveOnly = false; + std::array(HUE_NUM_CURVES)> m_curves; +}; + +typedef OCIO_SHARED_PTR ConstHueCurveImplRcPtr; +typedef OCIO_SHARED_PTR HueCurveImplRcPtr; + +} + +#endif //INCLUDED_OCIO_GRADINGHUECURVE_H diff --git a/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOp.cpp b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOp.cpp new file mode 100644 index 0000000000..1915301319 --- /dev/null +++ b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOp.cpp @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + +#include +#include + +#include + +#include "GpuShaderUtils.h" +#include "ops/gradinghuecurve/GradingHueCurveOpCPU.h" +#include "ops/gradinghuecurve/GradingHueCurveOpGPU.h" +#include "ops/gradinghuecurve/GradingHueCurveOp.h" +#include "transforms/GradingHueCurveTransform.h" + +namespace OCIO_NAMESPACE +{ + +namespace +{ + +class GradingHueCurveOp; +typedef OCIO_SHARED_PTR GradingHueCurveOpRcPtr; +typedef OCIO_SHARED_PTR ConstGradingHueCurveOpRcPtr; + +class GradingHueCurveOp : public Op +{ +public: + GradingHueCurveOp() = delete; + GradingHueCurveOp(const GradingHueCurveOp &) = delete; + explicit GradingHueCurveOp(GradingHueCurveOpDataRcPtr & data); + + virtual ~GradingHueCurveOp(); + + OpRcPtr clone() const override; + + std::string getInfo() const override; + + bool isIdentity() const override; + bool isSameType(ConstOpRcPtr & op) const override; + bool isInverse(ConstOpRcPtr & op) const override; + bool canCombineWith(ConstOpRcPtr & op) const override; + void combineWith(OpRcPtrVec & ops, ConstOpRcPtr & secondOp) const override; + + std::string getCacheID() const override; + + bool isDynamic() const override; + bool hasDynamicProperty(DynamicPropertyType type) const override; + DynamicPropertyRcPtr getDynamicProperty(DynamicPropertyType type) const override; + void replaceDynamicProperty(DynamicPropertyType type, + DynamicPropertyGradingHueCurveImplRcPtr & prop) override; + void removeDynamicProperties() override; + + ConstOpCPURcPtr getCPUOp(bool fastLogExpPow) const override; + + void extractGpuShaderInfo(GpuShaderCreatorRcPtr & shaderCreator) const override; + +protected: + ConstGradingHueCurveOpDataRcPtr hueCurveData() const + { + return DynamicPtrCast(data()); + } + GradingHueCurveOpDataRcPtr hueCurveData() + { + return DynamicPtrCast(data()); + } +}; + +GradingHueCurveOp::GradingHueCurveOp(GradingHueCurveOpDataRcPtr & hueCurveData) + : Op() +{ + data() = hueCurveData; +} + +OpRcPtr GradingHueCurveOp::clone() const +{ + GradingHueCurveOpDataRcPtr p = hueCurveData()->clone(); + return std::make_shared(p); +} + +GradingHueCurveOp::~GradingHueCurveOp() +{ +} + +std::string GradingHueCurveOp::getInfo() const +{ + return ""; +} + +bool GradingHueCurveOp::isIdentity() const +{ + return hueCurveData()->isIdentity(); +} + +bool GradingHueCurveOp::isSameType(ConstOpRcPtr & op) const +{ + ConstGradingHueCurveOpRcPtr typedRcPtr = DynamicPtrCast(op); + return (bool)typedRcPtr; +} + +bool GradingHueCurveOp::isInverse(ConstOpRcPtr & op) const +{ + ConstGradingHueCurveOpRcPtr typedRcPtr = DynamicPtrCast(op); + if (!typedRcPtr) return false; + + ConstGradingHueCurveOpDataRcPtr hueOpData = typedRcPtr->hueCurveData(); + return hueCurveData()->isInverse(hueOpData); +} + +bool GradingHueCurveOp::canCombineWith(ConstOpRcPtr & /*op*/) const +{ + return false; +} + +void GradingHueCurveOp::combineWith(OpRcPtrVec & /*ops*/, ConstOpRcPtr & secondOp) const +{ + if (!canCombineWith(secondOp)) + { + throw Exception("GradingHueCurveOp: canCombineWith must be checked " + "before calling combineWith."); + } +} + +std::string GradingHueCurveOp::getCacheID() const +{ + // Create the cacheID. + std::ostringstream cacheIDStream; + cacheIDStream << "getCacheID(); + cacheIDStream << ">"; + + return cacheIDStream.str(); +} + +bool GradingHueCurveOp::isDynamic() const +{ + return hueCurveData()->isDynamic(); +} + +bool GradingHueCurveOp::hasDynamicProperty(DynamicPropertyType type) const +{ + if (type != DYNAMIC_PROPERTY_GRADING_HUECURVE) + { + return false; + } + + return hueCurveData()->isDynamic(); +} + +DynamicPropertyRcPtr GradingHueCurveOp::getDynamicProperty(DynamicPropertyType type) const +{ + if (type != DYNAMIC_PROPERTY_GRADING_HUECURVE) + { + throw Exception("Dynamic property type not supported by hue curve op."); + } + if (!isDynamic()) + { + throw Exception("Hue curve property is not dynamic."); + } + + return hueCurveData()->getDynamicProperty(); +} + +void GradingHueCurveOp::replaceDynamicProperty(DynamicPropertyType type, + DynamicPropertyGradingHueCurveImplRcPtr & prop) +{ + if (type != DYNAMIC_PROPERTY_GRADING_HUECURVE) + { + throw Exception("Dynamic property type not supported by hue curve op."); + } + if (!isDynamic()) + { + throw Exception("Hue curve property is not dynamic."); + } + auto propGC = OCIO_DYNAMIC_POINTER_CAST(prop); + if (!propGC) + { + throw Exception("Dynamic property type not supported by hue curve op."); + } + + hueCurveData()->replaceDynamicProperty(propGC); +} + +void GradingHueCurveOp::removeDynamicProperties() +{ + hueCurveData()->removeDynamicProperty(); +} + +ConstOpCPURcPtr GradingHueCurveOp::getCPUOp(bool /*fastLogExpPow*/) const +{ + ConstGradingHueCurveOpDataRcPtr data = hueCurveData(); + return GetGradingHueCurveCPURenderer(data); +} + +void GradingHueCurveOp::extractGpuShaderInfo(GpuShaderCreatorRcPtr & shaderCreator) const +{ + ConstGradingHueCurveOpDataRcPtr data = hueCurveData(); + GetGradingHueCurveGPUShaderProgram(shaderCreator, data); +} + +} // Anon namespace + +/////////////////////////////////////////////////////////////////////////// + +void CreateGradingHueCurveOp(OpRcPtrVec & ops, + GradingHueCurveOpDataRcPtr & curveData, + TransformDirection direction) +{ + auto curve = curveData; + if (direction == TRANSFORM_DIR_INVERSE) + { + curve = curve->inverse(); + } + + ops.push_back(std::make_shared(curve)); +} + +/////////////////////////////////////////////////////////////////////////// + +void CreateGradingHueCurveTransform(GroupTransformRcPtr & group, ConstOpRcPtr & op) +{ + auto gc = DynamicPtrCast(op); + if (!gc) + { + throw Exception("CreateGradingHueCurveTransform: op has to be a GradingHueCurveOp."); + } + auto gcData = DynamicPtrCast(op->data()); + auto gcTransform = GradingHueCurveTransform::Create(gcData->getStyle()); + auto & data = dynamic_cast(gcTransform.get())->data(); + data = *gcData; + + group->appendTransform(gcTransform); +} + +void BuildGradingHueCurveOp(OpRcPtrVec & ops, + const Config & /*config*/, + const ConstContextRcPtr & /*context*/, + const GradingHueCurveTransform & transform, + TransformDirection dir) +{ + const auto & data = dynamic_cast(transform).data(); + data.validate(); + + auto curveData = data.clone(); + CreateGradingHueCurveOp(ops, curveData, dir); +} + +} // namespace OCIO_NAMESPACE + diff --git a/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOp.h b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOp.h new file mode 100644 index 0000000000..02181e0b54 --- /dev/null +++ b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOp.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + + +#ifndef INCLUDED_OCIO_HUECURVE_OP_H +#define INCLUDED_OCIO_HUECURVE_OP_H + + +#include + +#include + +#include "Op.h" +#include "ops/gradinghuecurve/GradingHueCurveOpData.h" + + +namespace OCIO_NAMESPACE +{ + +void CreateGradingHueCurveOp(OpRcPtrVec & ops, + GradingHueCurveOpDataRcPtr & gpData, + TransformDirection direction); + +// Create a copy of the hue curve transform in the op and append it to +// the GroupTransform. +void CreateGradingHueCurveTransform(GroupTransformRcPtr & group, ConstOpRcPtr & op); + +} // namespace OCIO_NAMESPACE + +#endif diff --git a/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpCPU.cpp b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpCPU.cpp new file mode 100644 index 0000000000..c3728eb882 --- /dev/null +++ b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpCPU.cpp @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + +#include +#include +#include + +#include + +#include "BitDepthUtils.h" +#include "MathUtils.h" +#include "ops/gradinghuecurve/GradingHueCurveOpCPU.h" +#include "ops/fixedfunction/FixedFunctionOpCPU.h" +#include "ops/fixedfunction/FixedFunctionOpData.h" +#include "ops/matrix/MatrixOpCPU.h" +#include "ops/matrix/MatrixOpData.h" + +namespace OCIO_NAMESPACE +{ + +namespace +{ + +namespace LogLinConstants +{ + static constexpr float xbrk = 0.0041318374739483946f; + static constexpr float shift = -0.000157849851665374f; + static constexpr float m = 1.f / (0.18f + shift); + static constexpr float gain = 363.034608563f; + static constexpr float offs = -7.f; + static constexpr float ybrk = -5.5f; + static constexpr float base2 = 1.4426950408889634f; // 1/log(2) +} + +inline void LinLog(float * out) +{ + out[2] = (out[2] < LogLinConstants::xbrk) ? + out[2] * LogLinConstants::gain + LogLinConstants::offs : + LogLinConstants::base2 * std::log((out[2] + LogLinConstants::shift) * LogLinConstants::m); +} + +inline void LogLin(float * out) +{ + out[2] = (out[2] < LogLinConstants::ybrk) ? + (out[2] - LogLinConstants::offs) / LogLinConstants::gain : + std::pow(2.0f, out[2]) * (0.18f + LogLinConstants::shift) - LogLinConstants::shift; +} + +inline void NoOp(float * /* out */) +{ +} + +typedef void (apply_nonlin_func)(float *out); + +class GradingHueCurveOpCPU : public OpCPU +{ +public: + GradingHueCurveOpCPU() = delete; + GradingHueCurveOpCPU(const GradingHueCurveOpCPU &) = delete; + + explicit GradingHueCurveOpCPU(ConstGradingHueCurveOpDataRcPtr & ghuec); + + bool isDynamic() const override; + bool hasDynamicProperty(DynamicPropertyType type) const override; + DynamicPropertyRcPtr getDynamicProperty(DynamicPropertyType type) const override; + +protected: + DynamicPropertyGradingHueCurveImplRcPtr m_ghuecurve; + bool m_isLinear = false; + + ConstOpCPURcPtr m_rgbToHsyOp; + ConstOpCPURcPtr m_hsyToRgbOp; + + apply_nonlin_func *m_applyLinLog = NoOp; + apply_nonlin_func *m_applyLogLin = NoOp; +}; + +GradingHueCurveOpCPU::GradingHueCurveOpCPU(ConstGradingHueCurveOpDataRcPtr & gcData) + : OpCPU() +{ + m_ghuecurve = gcData->getDynamicPropertyInternal(); + if (m_ghuecurve->isDynamic()) + { + m_ghuecurve = m_ghuecurve->createEditableCopy(); + } + + const GradingStyle style = gcData->getStyle(); + + FixedFunctionOpData::Style fwdStyle = FixedFunctionOpData::RGB_TO_HSY_LIN; + FixedFunctionOpData::Style invStyle = FixedFunctionOpData::HSY_LIN_TO_RGB; + switch(style) + { + case GRADING_LIN: + fwdStyle = FixedFunctionOpData::RGB_TO_HSY_LIN; + invStyle = FixedFunctionOpData::HSY_LIN_TO_RGB; + m_isLinear = true; + break; + case GRADING_LOG: + fwdStyle = FixedFunctionOpData::RGB_TO_HSY_LOG; + invStyle = FixedFunctionOpData::HSY_LOG_TO_RGB; + break; + case GRADING_VIDEO: + fwdStyle = FixedFunctionOpData::RGB_TO_HSY_VID; + invStyle = FixedFunctionOpData::HSY_VID_TO_RGB; + break; + } + + if (gcData->getRGBToHSY() == HSYTransformStyle::HSY_TRANSFORM_NONE) + { + // TODO: Could template the apply function to avoid the need for a matrix op. + ConstMatrixOpDataRcPtr fwdOpData = std::make_shared(TRANSFORM_DIR_FORWARD); + m_rgbToHsyOp = GetMatrixRenderer(fwdOpData); + m_hsyToRgbOp = GetMatrixRenderer(fwdOpData); + } + else + { + ConstFixedFunctionOpDataRcPtr fwdOpData = std::make_shared(fwdStyle); + m_rgbToHsyOp = GetFixedFunctionCPURenderer(fwdOpData, false /* fastLogExpPow */); + ConstFixedFunctionOpDataRcPtr invOpData = std::make_shared(invStyle); + m_hsyToRgbOp = GetFixedFunctionCPURenderer(invOpData, false /* fastLogExpPow */); + } + + if (style == GRADING_LIN) + { + m_applyLinLog = LinLog; + m_applyLogLin = LogLin; + } +} + +bool GradingHueCurveOpCPU::isDynamic() const +{ + return m_ghuecurve->isDynamic(); +} + +bool GradingHueCurveOpCPU::hasDynamicProperty(DynamicPropertyType type) const +{ + bool res = false; + if (type == DYNAMIC_PROPERTY_GRADING_HUECURVE) + { + res = m_ghuecurve->isDynamic(); + } + return res; +} + +DynamicPropertyRcPtr GradingHueCurveOpCPU::getDynamicProperty(DynamicPropertyType type) const +{ + if (type == DYNAMIC_PROPERTY_GRADING_HUECURVE) + { + if (m_ghuecurve->isDynamic()) + { + return m_ghuecurve; + } + } + else + { + throw Exception("Dynamic property type not supported by GradingHueCurve."); + } + + throw Exception("GradingHueCurve property is not dynamic."); +} + +class GradingHueCurveDrawOpCPU : public GradingHueCurveOpCPU +{ +public: + GradingHueCurveDrawOpCPU(const GradingHueCurveOpCPU &) = delete; + GradingHueCurveDrawOpCPU() = delete; + + explicit GradingHueCurveDrawOpCPU(ConstGradingHueCurveOpDataRcPtr & ghuec); + void apply(const void * inImg, void * outImg, long numPixels) const override; +}; + +GradingHueCurveDrawOpCPU::GradingHueCurveDrawOpCPU(ConstGradingHueCurveOpDataRcPtr & ghuec) + : GradingHueCurveOpCPU(ghuec) +{ +} + +void GradingHueCurveDrawOpCPU::apply(const void * inImg, void * outImg, long numPixels) const +{ + // NB: LocalBypass does not matter, need to evaluate even if it's an identity. + + const GradingBSplineCurveImpl::KnotsCoefs & knotsCoefs = m_ghuecurve->getKnotsCoefs(); + + const float * in = (float *)inImg; + float * out = (float *)outImg; + + for (long idx = 0; idx < numPixels; ++idx) + { + // In drawCurveOnly mode, only evaluate the HueSat curve, with no RGB-to-HSY or LogLin. + // (In practice it may be any of the curves, but only eval the one stored in that slot.) + out[0] = knotsCoefs.evalCurve(static_cast(HUE_SAT), in[0], 1.f); + out[1] = knotsCoefs.evalCurve(static_cast(HUE_SAT), in[1], 1.f); + out[2] = knotsCoefs.evalCurve(static_cast(HUE_SAT), in[2], 1.f); + out[3] = in[3]; + + in += 4; + out += 4; + } +} + +class GradingHueCurveFwdOpCPU : public GradingHueCurveOpCPU +{ +public: + GradingHueCurveFwdOpCPU(const GradingHueCurveOpCPU &) = delete; + GradingHueCurveFwdOpCPU() = delete; + + explicit GradingHueCurveFwdOpCPU(ConstGradingHueCurveOpDataRcPtr & ghuec); + void apply(const void * inImg, void * outImg, long numPixels) const override; +}; + +GradingHueCurveFwdOpCPU::GradingHueCurveFwdOpCPU(ConstGradingHueCurveOpDataRcPtr & ghuec) + : GradingHueCurveOpCPU(ghuec) +{ +} + +static constexpr auto PixelSize = 4 * sizeof(float); + +void GradingHueCurveFwdOpCPU::apply(const void * inImg, void * outImg, long numPixels) const +{ + if (m_ghuecurve->getLocalBypass()) + { + if (inImg != outImg) + { + memcpy(outImg, inImg, numPixels * PixelSize); + } + return; + } + + const GradingBSplineCurveImpl::KnotsCoefs & knotsCoefs = m_ghuecurve->getKnotsCoefs(); + + const float * in = (float *)inImg; + float * out = (float *)outImg; + + for (long idx = 0; idx < numPixels; ++idx) + { + m_rgbToHsyOp->apply(in, out, 1L); + + m_applyLinLog(out); + + // HUE-SAT + const float hueSatGain = std::max(0.f, knotsCoefs.evalCurve(static_cast(HUE_SAT), out[0], 1.f)); + // HUE-LUM + float hueLumGain = std::max(0.f, knotsCoefs.evalCurve(static_cast(HUE_LUM), out[0], 1.f)); + // HUE-HUE + out[0] = knotsCoefs.evalCurve(static_cast(HUE_HUE), out[0], out[0]); + // SAT-SAT + out[1] = std::max(0.f, knotsCoefs.evalCurve(static_cast(SAT_SAT), out[1], out[1])); + // LUM-SAT + const float lumSatGain = std::max(0.f, knotsCoefs.evalCurve(static_cast(LUM_SAT), out[2], 1.f)); + + // Apply sat gain. + const float satGain = lumSatGain * hueSatGain; + out[1] *= satGain; + + // SAT-LUM + const float satLumGain = std::max(0.f, knotsCoefs.evalCurve(static_cast(SAT_LUM), out[1], 1.f)); + // LUM-LUM + out[2] = knotsCoefs.evalCurve(static_cast(LUM_LUM), out[2], out[2]); + + m_applyLogLin(out); + + // Limit hue-lum gain at low sat, since the hue is more noisy, + // and when sat is 0 the hue becomes unknown (and is not invertible). + hueLumGain = 1.f - (1.f - hueLumGain) * std::min(out[1], 1.f); + + // Apply lum gain. + out[2] = m_isLinear ? out[2] * hueLumGain * satLumGain : + out[2] + (hueLumGain + satLumGain - 2.f) * 0.1f; + + // HUE-FX + out[0] = out[0] - std::floor(out[0]); // wrap to [0,1) + out[0] = out[0] + knotsCoefs.evalCurve(static_cast(HUE_FX), out[0], 0.f); + + m_hsyToRgbOp->apply(out, out, 1L); + + in += 4; + out += 4; + } +} + +class GradingHueCurveRevOpCPU : public GradingHueCurveOpCPU +{ +public: + GradingHueCurveRevOpCPU(const GradingHueCurveOpCPU &) = delete; + GradingHueCurveRevOpCPU() = delete; + + explicit GradingHueCurveRevOpCPU(ConstGradingHueCurveOpDataRcPtr & ghuec); + void apply(const void * inImg, void * outImg, long numPixels) const override; +}; + +GradingHueCurveRevOpCPU::GradingHueCurveRevOpCPU(ConstGradingHueCurveOpDataRcPtr & ghuec) + : GradingHueCurveOpCPU(ghuec) +{ +} + +void GradingHueCurveRevOpCPU::apply(const void * inImg, void * outImg, long numPixels) const +{ + if (m_ghuecurve->getLocalBypass()) + { + if (inImg != outImg) + { + memcpy(outImg, inImg, numPixels * PixelSize); + } + return; + } + + const GradingBSplineCurveImpl::KnotsCoefs & knotsCoefs = m_ghuecurve->getKnotsCoefs(); + + const float * in = (float *)inImg; + float * out = (float *)outImg; + + for (long idx = 0; idx < numPixels; ++idx) + { + m_rgbToHsyOp->apply(in, out, 1L); + + // Invert HUE-FX. + out[0] = knotsCoefs.evalCurveRevHue(static_cast(HUE_FX), out[0]); + + // Invert HUE-HUE. + out[0] = knotsCoefs.evalCurveRevHue(static_cast(HUE_HUE), out[0]); + + // Use the inverted hue to calculate the HUE-SAT & HUE-LUM gains. + out[0] = out[0] - std::floor(out[0]); // wrap to [0,1) + const float hue_sat_gain = std::max( 0.f, knotsCoefs.evalCurve(static_cast(HUE_SAT), out[0], 1.f) ); + float hue_lum_gain = std::max( 0.f, knotsCoefs.evalCurve(static_cast(HUE_LUM), out[0], 1.f) ); + + // Use the output sat to calculate the SAT-LUM gain. + out[1] = std::max(0.f, out[1]); // guard against negative saturation + const float sat_lum_gain = std::max( 0.f, knotsCoefs.evalCurve(static_cast(SAT_LUM), out[1], 1.f) ); + + hue_lum_gain = 1.f - (1.f - hue_lum_gain) * std::min(out[1], 1.f); + + // Invert the lum gain. + const float lum_gain = hue_lum_gain * sat_lum_gain; + out[2] = m_isLinear ? out[2] / std::max(0.01f, lum_gain) : + out[2] - (hue_lum_gain + sat_lum_gain - 2.f) * 0.1f; + + m_applyLinLog(out); + + // Invert LUM-LUM. + out[2] = knotsCoefs.evalCurveRev(static_cast(LUM_LUM), out[2]); + + // Use it to calc the LUM-SAT gain. + const float lum_sat_gain = std::max( 0.f, knotsCoefs.evalCurve(static_cast(LUM_SAT), out[2], 1.f) ); + + m_applyLogLin(out); + + // Invert the sat gain. + const float sat_gain = lum_sat_gain * hue_sat_gain; + out[1] /= std::max(0.01f, sat_gain); + + // Invert SAT-SAT. + out[1] = std::max( 0.f, knotsCoefs.evalCurveRev(static_cast(SAT_SAT), out[1]) ); + + m_hsyToRgbOp->apply(out, out, 1L); + + in += 4; + out += 4; + } +} + +} // Anonymous namespace + +/////////////////////////////////////////////////////////////////////////////// + +ConstOpCPURcPtr GetGradingHueCurveCPURenderer(ConstGradingHueCurveOpDataRcPtr & prim) +{ + + // DrawCurveOnly mode ignores the direction, it's always the forward transform. + auto dynProp = prim->getDynamicPropertyInternal(); + if (dynProp->getValue()->getDrawCurveOnly()) + { + return std::make_shared(prim); + } + + switch (prim->getDirection()) + { + case TRANSFORM_DIR_FORWARD: + { + return std::make_shared(prim); + break; + } + case TRANSFORM_DIR_INVERSE: + { + return std::make_shared(prim); + break; + } + } + + throw Exception("Illegal GradingHueCurve direction."); +} + +} // namespace OCIO_NAMESPACE diff --git a/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpCPU.h b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpCPU.h new file mode 100644 index 0000000000..934e84ce0f --- /dev/null +++ b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpCPU.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + +#ifndef INCLUDED_OCIO_GRADINGHUECURVE_CPU_H +#define INCLUDED_OCIO_GRADINGHUECURVE_CPU_H + +#include + +#include + +#include "Op.h" +#include "ops/gradinghuecurve/GradingHueCurveOpData.h" + +namespace OCIO_NAMESPACE +{ + +ConstOpCPURcPtr GetGradingHueCurveCPURenderer(ConstGradingHueCurveOpDataRcPtr & hueCurve); + +} // namespace OCIO_NAMESPACE + +#endif diff --git a/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpData.cpp b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpData.cpp new file mode 100644 index 0000000000..8d19439b2f --- /dev/null +++ b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpData.cpp @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + +#include + +#include + +#include "ops/gradinghuecurve/GradingHueCurve.h" +#include "ops/gradinghuecurve/GradingHueCurveOpData.h" +#include "Platform.h" + +namespace OCIO_NAMESPACE +{ + +namespace DefaultValues +{ +const std::streamsize FLOAT_DECIMALS = 7; +} + +GradingHueCurveOpData::GradingHueCurveOpData(GradingStyle style) + : OpData() + , m_style(style) +{ + ConstGradingHueCurveRcPtr hueCurve = GradingHueCurve::Create(style); + m_value = std::make_shared(hueCurve, false); +} + +GradingHueCurveOpData::GradingHueCurveOpData(const GradingHueCurveOpData & rhs) + : OpData(rhs) + , m_style(rhs.m_style) +{ + ConstGradingHueCurveRcPtr hueCurve = GradingHueCurve::Create(rhs.m_style); + m_value = std::make_shared(hueCurve, false); + + *this = rhs; +} + +GradingHueCurveOpData::GradingHueCurveOpData(GradingStyle style, + ConstGradingBSplineCurveRcPtr hueHue, + ConstGradingBSplineCurveRcPtr hueSat, + ConstGradingBSplineCurveRcPtr hueLum, + ConstGradingBSplineCurveRcPtr lumSat, + ConstGradingBSplineCurveRcPtr satSat, + ConstGradingBSplineCurveRcPtr lumLum, + ConstGradingBSplineCurveRcPtr satLum, + ConstGradingBSplineCurveRcPtr hueFx) + : OpData() + , m_style(style) +{ + ConstGradingHueCurveRcPtr hueCurve = GradingHueCurve::Create( + hueHue, + hueSat, + hueLum, + lumSat, + satSat, + lumLum, + satLum, + hueFx); + m_value = std::make_shared(hueCurve, false); +} + +GradingHueCurveOpData & GradingHueCurveOpData::operator=(const GradingHueCurveOpData & rhs) +{ + if (this == &rhs) return *this; + + OpData::operator=(rhs); + + m_direction = rhs.m_direction; + m_style = rhs.m_style; + m_RGBToHSY = rhs.m_RGBToHSY; + + // Copy dynamic properties. Sharing happens when needed, with CPUOp for instance. + m_value->setValue(rhs.m_value->getValue()); + if (rhs.m_value->isDynamic()) + { + m_value->makeDynamic(); + } + + return *this; +} + +GradingHueCurveOpData::~GradingHueCurveOpData() +{ +} + +GradingHueCurveOpDataRcPtr GradingHueCurveOpData::clone() const +{ + return std::make_shared(*this); +} + +void GradingHueCurveOpData::validate() const +{ + // This should already be valid. + m_value->getValue()->validate(); + return; +} + +bool GradingHueCurveOpData::isNoOp() const +{ + return isIdentity(); +} + +bool GradingHueCurveOpData::isIdentity() const +{ + if (isDynamic()) return false; + + return m_value->getValue()->isIdentity(); +} + +bool GradingHueCurveOpData::isInverse(ConstGradingHueCurveOpDataRcPtr & r) const +{ + if (isDynamic() || r->isDynamic()) + { + return false; + } + + if (m_style == r->m_style && + (m_style != GRADING_LIN || m_RGBToHSY == r->m_RGBToHSY) && + m_value->equals(*r->m_value)) + { + if (CombineTransformDirections(getDirection(), r->getDirection()) == TRANSFORM_DIR_INVERSE) + { + return true; + } + } + return false; +} + +GradingHueCurveOpDataRcPtr GradingHueCurveOpData::inverse() const +{ + auto res = clone(); + res->m_direction = GetInverseTransformDirection(m_direction); + return res; +} + +std::string GradingHueCurveOpData::getCacheID() const +{ + AutoMutex lock(m_mutex); + + std::ostringstream cacheIDStream; + if (!getID().empty()) + { + cacheIDStream << getID() << " "; + } + + cacheIDStream.precision(DefaultValues::FLOAT_DECIMALS); + + cacheIDStream << GradingStyleToString(getStyle()) << " "; + cacheIDStream << TransformDirectionToString(getDirection()) << " "; + if (m_RGBToHSY != HSY_TRANSFORM_1) + { + cacheIDStream << " bypassRGBToHSY "; + } + if (!isDynamic()) + { + cacheIDStream << *(m_value->getValue()); + } + return cacheIDStream.str(); +} + +void GradingHueCurveOpData::setStyle(GradingStyle style) noexcept +{ + if (style != m_style) + { + m_style = style; + // Reset value to default when style is changing. + ConstGradingHueCurveRcPtr reset = GradingHueCurve::Create(style); + m_value->setValue(reset); + } +} + +float GradingHueCurveOpData::getSlope(HueCurveType c, size_t index) const +{ + ConstGradingBSplineCurveRcPtr curve = m_value->getValue()->getCurve(c); + return curve->getSlope(index); +} + +void GradingHueCurveOpData::setSlope(HueCurveType c, size_t index, float slope) +{ + GradingHueCurveRcPtr hueCurve( m_value->getValue()->createEditableCopy() ); + GradingBSplineCurveRcPtr curve = hueCurve->getCurve(c); + curve->setSlope(index, slope); + m_value->setValue(hueCurve); +} + +bool GradingHueCurveOpData::slopesAreDefault(HueCurveType c) const +{ + ConstGradingBSplineCurveRcPtr curve = m_value->getValue()->getCurve(c); + return curve->slopesAreDefault(); +} + +TransformDirection GradingHueCurveOpData::getDirection() const noexcept +{ + return m_direction; +} + +void GradingHueCurveOpData::setDirection(TransformDirection dir) noexcept +{ + m_direction = dir; +} + +bool GradingHueCurveOpData::isDynamic() const noexcept +{ + return m_value->isDynamic(); +} + +DynamicPropertyRcPtr GradingHueCurveOpData::getDynamicProperty() const noexcept +{ + return m_value; +} + +void GradingHueCurveOpData::replaceDynamicProperty(DynamicPropertyGradingHueCurveImplRcPtr prop) noexcept +{ + m_value = prop; +} + +void GradingHueCurveOpData::removeDynamicProperty() noexcept +{ + m_value->makeNonDynamic(); +} + +bool GradingHueCurveOpData::equals(const OpData & other) const +{ + if (!OpData::equals(other)) return false; + + const GradingHueCurveOpData* rop = static_cast(&other); + + if (m_direction != rop->m_direction || + m_style != rop->m_style || + m_RGBToHSY != rop->m_RGBToHSY || + !m_value->equals( *(rop->m_value) )) + { + return false; + } + + return true; +} + +bool operator==(const GradingHueCurveOpData & lhs, const GradingHueCurveOpData & rhs) +{ + return lhs.equals(rhs); +} + +} // namespace OCIO_NAMESPACE diff --git a/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpData.h b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpData.h new file mode 100644 index 0000000000..665acfed56 --- /dev/null +++ b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpData.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + + +#ifndef INCLUDED_OCIO_HUECURVE_OPDATA_H +#define INCLUDED_OCIO_HUECURVE_OPDATA_H + + +#include + +#include "Op.h" + + +namespace OCIO_NAMESPACE +{ + +class GradingHueCurveOpData; +typedef OCIO_SHARED_PTR GradingHueCurveOpDataRcPtr; +typedef OCIO_SHARED_PTR ConstGradingHueCurveOpDataRcPtr; + + +class GradingHueCurveOpData : public OpData +{ +public: + + GradingHueCurveOpData(GradingStyle style); + GradingHueCurveOpData(const GradingHueCurveOpData & rhs); + GradingHueCurveOpData(GradingStyle style, + ConstGradingBSplineCurveRcPtr hueHue, + ConstGradingBSplineCurveRcPtr hueSat, + ConstGradingBSplineCurveRcPtr hueLum, + ConstGradingBSplineCurveRcPtr lumSat, + ConstGradingBSplineCurveRcPtr satSat, + ConstGradingBSplineCurveRcPtr lumLum, + ConstGradingBSplineCurveRcPtr satLum, + ConstGradingBSplineCurveRcPtr hueFx); + GradingHueCurveOpData & operator=(const GradingHueCurveOpData & rhs); + virtual ~GradingHueCurveOpData(); + + GradingHueCurveOpDataRcPtr clone() const; + + void validate() const override; + + Type getType() const override { return GradingHueCurveType; } + + bool isNoOp() const override; + bool isIdentity() const override; + + bool hasChannelCrosstalk() const override { return true; } + + bool isInverse(ConstGradingHueCurveOpDataRcPtr & r) const; + GradingHueCurveOpDataRcPtr inverse() const; + + std::string getCacheID() const override; + + GradingStyle getStyle() const noexcept { return m_style; } + void setStyle(GradingStyle style) noexcept; + + const ConstGradingHueCurveRcPtr getValue() const { return m_value->getValue(); } + void setValue(const ConstGradingHueCurveRcPtr & values) { m_value->setValue(values); } + + float getSlope(HueCurveType c, size_t index) const; + void setSlope(HueCurveType c, size_t index, float slope); + bool slopesAreDefault(HueCurveType c) const; + + HSYTransformStyle getRGBToHSY() const noexcept { return m_RGBToHSY; } + void setRGBToHSY(HSYTransformStyle style) noexcept { m_RGBToHSY = style; } + + TransformDirection getDirection() const noexcept; + void setDirection(TransformDirection dir) noexcept; + + bool isDynamic() const noexcept; + DynamicPropertyRcPtr getDynamicProperty() const noexcept; + void replaceDynamicProperty(DynamicPropertyGradingHueCurveImplRcPtr prop) noexcept; + void removeDynamicProperty() noexcept; + + DynamicPropertyGradingHueCurveImplRcPtr getDynamicPropertyInternal() const noexcept + { + return m_value; + } + + bool equals(const OpData & other) const override; + +private: + GradingStyle m_style; + DynamicPropertyGradingHueCurveImplRcPtr m_value; + HSYTransformStyle m_RGBToHSY{ HSYTransformStyle::HSY_TRANSFORM_1 }; + TransformDirection m_direction{ TRANSFORM_DIR_FORWARD }; +}; + +bool operator==(const GradingHueCurveOpData & lhs, const GradingHueCurveOpData & rhs); + +} // namespace OCIO_NAMESPACE + +#endif diff --git a/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpGPU.cpp b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpGPU.cpp new file mode 100644 index 0000000000..4050c11fc9 --- /dev/null +++ b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpGPU.cpp @@ -0,0 +1,684 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + + +#include + +#include "Logging.h" +#include "ops/gradinghuecurve/GradingHueCurveOpGPU.h" +#include "ops/fixedfunction/FixedFunctionOpGPU.h" +#include "ops/fixedfunction/FixedFunctionOpData.h" +#include "utils/StringUtils.h" + + +namespace OCIO_NAMESPACE +{ + +// The curve evaluation is done using a piecewise quadratic polynomial function. The shader +// may handle a dynamic number of curves and a dynamic number of knots and coefficients per +// curve. +// +// For optimization, the knots of ALL the curves are packed in one single array. This is +// exactly the same for coefficients. For example : +// +// KnotsArray = { Curve1[kn0, kn1], Curve2[kn0, kn1, kn2], Curve3[kn0, kn1] } +// +// In order to access knots of a specific curve in this single array, the position of the +// first knot and the number of knots of each curve is stored in an offset array. +// This array is dynamic according to the number of curves. For example : +// +// KnotOffsetArray = {Curve1StartPos, Curve1NumKnots, Curve2StartPos, Curve2NumKnots} +// +// Here is an example of what the arrays would look like in memory with the following +// curve information: +// +// Curve 1 : Knots = { 0, 1, 2 } Coefficients = { 10, 11, 12, 13, 14, 15 } +// Curve 2 : Knots = { 0.1, 0.5, 1, 3 } Coefficients = { 20, 21, 22, 23, 24, 25, 26, 27, 28 } +// +// KnotsArray : { 0, 1, 2, 0.1, 0.5, 1, 3 } +// CoefsArray : { 10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25, 26, 27, 28 } +// +// KnotsOffsetsArray : { 0, 3, 3, 4 } +// CoefsOffsetsArray : { 0, 6, 6, 9 } +// +// To access the knots of the second curve in C++, you would do the following : +// +// { +// const unsigned curveIdx = 1; // Second curve. This is 0 based. +// const unsigned startPos = KnotsOffsetsArray[curveIdx*2]; // Data is in pairs. +// const unsigned numKnots = KnotsOffsetsArray[curveIdx*2+1]; +// +// const float firstKnot = KnotsArray[startPos]; +// const float lastKnot = KnotsArray[startPos+numKnots-1]; +// } +// +// In GLSL, offset arrays are loaded as vec2 uniforms. To achieve the previous example +// in GLSL, you would do the following : +// +// { +// const int curveIdx = 1; +// const int startPos = KnotsOffsetsArray[curveIdx*2]; +// const int numKnots = KnotsOffsetsArray[curveIdx*2+1]; +// +// const float firstKnot = KnotsArray[startPos].x; +// const float lastKnot = KnotsArray[startPos+numKnots-1].x; +// } +// +// The coefficients array contains the polynomial coefficients which are stored +// as all the quadratic terms for the first curve, then all the linear terms for +// the first curve, then all the constant terms for the first curve. The number +// of coefficient sets is the number of knots minus one. + +namespace +{ +struct GCProperties +{ + std::string m_knotsOffsets{ "knotsOffsets" }; + std::string m_knots{ "knots" }; + std::string m_coefsOffsets{ "coefsOffsets" }; + std::string m_coefs{ "coefs" }; + std::string m_localBypass{ "localBypass" }; + std::string m_eval{ "evalBSplineCurve" }; + std::string m_evalRev{ "evalBSplineCurveRev" }; + std::string m_evalRevHue{ "evalBSplineCurveRevHue" }; +}; + +void AddUniform(GpuShaderCreatorRcPtr & shaderCreator, + const GpuShaderCreator::SizeGetter & getSize, + const GpuShaderCreator::VectorFloatGetter & getVector, + unsigned int maxSize, + const std::string & name) +{ + // Add the uniform if it does not already exist. + if (shaderCreator->addUniform(name.c_str(), getSize, getVector, maxSize)) + { + // Declare uniform. + GpuShaderText stDecl(shaderCreator->getLanguage()); + stDecl.declareUniformArrayFloat(name, maxSize); + shaderCreator->addToParameterDeclareShaderCode(stDecl.string().c_str()); + } +} + +void AddUniform(GpuShaderCreatorRcPtr & shaderCreator, + const GpuShaderCreator::SizeGetter & getSize, + const GpuShaderCreator::VectorIntGetter & getVector, + const std::string & name) +{ + // 8 curves x 2 values (count and offset). + static constexpr unsigned arrayLen = HueCurveType::HUE_NUM_CURVES * 2; + + // Add the uniform if it does not already exist. + if (shaderCreator->addUniform(name.c_str(), getSize, getVector, arrayLen)) + { + // Declare uniform. + GpuShaderText stDecl(shaderCreator->getLanguage()); + // Need 2 ints for each curve. + stDecl.declareUniformArrayInt(name, arrayLen); + shaderCreator->addToParameterDeclareShaderCode(stDecl.string().c_str()); + } +} + +void AddUniform(GpuShaderCreatorRcPtr & shaderCreator, + const GpuShaderCreator::BoolGetter & getBool, + const std::string & name) +{ + // Add the uniform if it does not already exist. + if (shaderCreator->addUniform(name.c_str(), getBool)) + { + // Declare uniform. + GpuShaderText stDecl(shaderCreator->getLanguage()); + stDecl.declareUniformBool(name); + shaderCreator->addToParameterDeclareShaderCode(stDecl.string().c_str()); + } +} + +std::string BuildResourceNameIndexed(GpuShaderCreatorRcPtr & shaderCreator, const std::string & prefix, + const std::string & base, unsigned int index) +{ + std::string name{ BuildResourceName(shaderCreator, prefix, base) }; + name += "_"; + name += std::to_string(index); + // Note: Remove potentially problematic double underscores from GLSL resource names. + StringUtils::ReplaceInPlace(name, "__", "_"); + return name; +} + +static const std::string opPrefix{ "grading_huecurve" }; + +void SetGCProperties(GpuShaderCreatorRcPtr & shaderCreator, bool dynamic, GCProperties & propNames) +{ + if (dynamic) + { + // If there are several dynamic ops, they will use the same names for uniforms. + propNames.m_knotsOffsets = BuildResourceName(shaderCreator, opPrefix, + propNames.m_knotsOffsets); + propNames.m_knots = BuildResourceName(shaderCreator, opPrefix, propNames.m_knots); + propNames.m_coefsOffsets = BuildResourceName(shaderCreator, opPrefix, + propNames.m_coefsOffsets); + propNames.m_coefs = BuildResourceName(shaderCreator, opPrefix, propNames.m_coefs); + propNames.m_localBypass = BuildResourceName(shaderCreator, opPrefix, + propNames.m_localBypass); + propNames.m_eval = BuildResourceName(shaderCreator, opPrefix, propNames.m_eval); + propNames.m_evalRev = BuildResourceName(shaderCreator, opPrefix, propNames.m_evalRev); + propNames.m_evalRevHue = BuildResourceName(shaderCreator, opPrefix, propNames.m_evalRevHue); + } + else + { + // Non-dynamic ops need a helper function for each op. + const auto resIndex = shaderCreator->getNextResourceIndex(); + + propNames.m_knotsOffsets = BuildResourceNameIndexed(shaderCreator, opPrefix, + propNames.m_knotsOffsets, resIndex); + propNames.m_knots = BuildResourceNameIndexed(shaderCreator, opPrefix, + propNames.m_knots, resIndex); + propNames.m_coefsOffsets = BuildResourceNameIndexed(shaderCreator, opPrefix, + propNames.m_coefsOffsets, resIndex); + propNames.m_coefs = BuildResourceNameIndexed(shaderCreator, opPrefix, + propNames.m_coefs, resIndex); + propNames.m_eval = BuildResourceNameIndexed(shaderCreator, opPrefix, + propNames.m_eval, resIndex); + propNames.m_evalRev = BuildResourceNameIndexed(shaderCreator, opPrefix, + propNames.m_evalRev, resIndex); + propNames.m_evalRevHue = BuildResourceNameIndexed(shaderCreator, opPrefix, + propNames.m_evalRevHue, resIndex); + } +} + +// Only called once for dynamic ops. +void AddGCPropertiesUniforms(GpuShaderCreatorRcPtr & shaderCreator, + DynamicPropertyGradingHueCurveImplRcPtr & shaderProp, + const GCProperties & propNames) +{ + // Use the shader dynamic property to bind the uniforms. + auto curveProp = shaderProp.get(); + + // Note: No need to add an index to the name to avoid collisions as the dynamic properties + // are unique. + + auto getNK = std::bind(&DynamicPropertyGradingHueCurveImpl::getNumKnots, curveProp); + auto getKO = std::bind(&DynamicPropertyGradingHueCurveImpl::getKnotsOffsetsArray, + curveProp); + auto getK = std::bind(&DynamicPropertyGradingHueCurveImpl::getKnotsArray, curveProp); + auto getNC = std::bind(&DynamicPropertyGradingHueCurveImpl::getNumCoefs, curveProp); + auto getCO = std::bind(&DynamicPropertyGradingHueCurveImpl::getCoefsOffsetsArray, + curveProp); + auto getC = std::bind(&DynamicPropertyGradingHueCurveImpl::getCoefsArray, curveProp); + auto getLB = std::bind(&DynamicPropertyGradingHueCurveImpl::getLocalBypass, curveProp); + // Uniforms are added if they are not already there (added by another op). + AddUniform(shaderCreator, DynamicPropertyGradingHueCurveImpl::GetNumOffsetValues, + getKO, propNames.m_knotsOffsets); + AddUniform(shaderCreator, getNK, getK, + DynamicPropertyGradingHueCurveImpl::GetMaxKnots(), + propNames.m_knots); + AddUniform(shaderCreator, DynamicPropertyGradingHueCurveImpl::GetNumOffsetValues, + getCO, propNames.m_coefsOffsets); + AddUniform(shaderCreator, getNC, getC, + DynamicPropertyGradingHueCurveImpl::GetMaxCoefs(), + propNames.m_coefs); + AddUniform(shaderCreator, getLB, propNames.m_localBypass); +} + +void AddCurveFunctionName(GpuShaderCreatorRcPtr & shaderCreator, + GpuShaderText & st, + const std::string & funcName, + bool isFwd) +{ + st.newLine() << ""; + if (shaderCreator->getLanguage() == LANGUAGE_OSL_1 || shaderCreator->getLanguage() == GPU_LANGUAGE_MSL_2_0) + { + if (isFwd) + { + st.newLine() << st.floatKeyword() << " " << funcName << "(int curveIdx, float x, float identity_x)"; + } + else + { + st.newLine() << st.floatKeyword() << " " << funcName << "(int curveIdx, float x)"; + } + } + else + { + if (isFwd) + { + st.newLine() << st.floatKeyword() << " " << funcName << "(in int curveIdx, in float x, in float identity_x)"; + } + else + { + st.newLine() << st.floatKeyword() << " " << funcName << "(in int curveIdx, in float x)"; + } + } +} + +void AddCurveEvalMethodTextToShaderProgram(GpuShaderCreatorRcPtr & shaderCreator, + ConstGradingHueCurveOpDataRcPtr & gcData, + const GCProperties & props, + bool dyn) +{ + GpuShaderText st(shaderCreator->getLanguage()); + + // Dynamic version uses uniforms declared globally. Non-dynamic version declares local + // variables in the op specific helper function. + if (!dyn) + { + auto propGC = gcData->getDynamicPropertyInternal(); + const int numOffsets = propGC->GetNumOffsetValues(); + + // 2 ints for each curve. + st.newLine() << ""; + st.declareIntArrayConst(props.m_knotsOffsets, numOffsets, propGC->getKnotsOffsetsArray()); + st.declareFloatArrayConst(props.m_knots, propGC->getNumKnots(), propGC->getKnotsArray()); + st.declareIntArrayConst(props.m_coefsOffsets, numOffsets, propGC->getCoefsOffsetsArray()); + st.declareFloatArrayConst(props.m_coefs, propGC->getNumCoefs(), propGC->getCoefsArray()); + } + + // Both the forward and inverse hue curve eval need the forward spline eval, so always add that. + + AddCurveFunctionName(shaderCreator, st, props.m_eval, true); + + st.newLine() << "{"; + st.indent(); + GradingBSplineCurveImpl::AddShaderEvalFwd(st, props.m_knotsOffsets, props.m_coefsOffsets, + props.m_knots, props.m_coefs); + st.dedent(); + st.newLine() << "}"; + + if (gcData->getDirection() == TRANSFORM_DIR_INVERSE) + { + // Add inverse curve eval. + AddCurveFunctionName(shaderCreator, st, props.m_evalRev, false); + + st.newLine() << "{"; + st.indent(); + GradingBSplineCurveImpl::AddShaderEvalRev(st, props.m_knotsOffsets, props.m_coefsOffsets, + props.m_knots, props.m_coefs); + st.dedent(); + st.newLine() << "}"; + + // Add inverse hue curve eval. + AddCurveFunctionName(shaderCreator, st, props.m_evalRevHue, false); + + st.newLine() << "{"; + st.indent(); + GradingBSplineCurveImpl::AddShaderEvalRevHue(st, props.m_knotsOffsets, props.m_coefsOffsets, + props.m_knots, props.m_coefs); + st.dedent(); + st.newLine() << "}"; + } + + shaderCreator->addToHelperShaderCode(st.string().c_str()); +} + +void AddGCForwardShader(GpuShaderCreatorRcPtr & shaderCreator, + GpuShaderText & st, + const GCProperties & props, + bool dyn, + bool doLinToLog, + bool doRGBToHSY, + bool drawCurveOnly, + GradingStyle style) +{ + const std::string pix(shaderCreator->getPixelName()); + if (drawCurveOnly) + { + // Note that this is not within the localBypass if-statement since outColor + // needs to get processed even if isDefault is true for all curves since + // the default for the horizontal curves is all 1 rather than an identity. + st.newLine() << pix << ".r = " << props.m_eval << "(1, " << pix << ".r, 1.);"; // HUE-SAT + st.newLine() << pix << ".g = " << props.m_eval << "(1, " << pix << ".g, 1.);"; // HUE-SAT + st.newLine() << pix << ".b = " << props.m_eval << "(1, " << pix << ".b, 1.);"; // HUE-SAT + return; + } + + if (dyn) + { + st.newLine() << "if (!" << st.castToBool(props.m_localBypass) << ")"; + st.newLine() << "{"; + st.indent(); + } + + // Add the conversion from RGB to HSY. + if (doRGBToHSY) + { + FixedFunctionOpData::Style hsyStyle = FixedFunctionOpData::RGB_TO_HSY_LIN; + switch (style) + { + case GRADING_LIN: + hsyStyle = FixedFunctionOpData::RGB_TO_HSY_LIN; + break; + case GRADING_LOG: + hsyStyle = FixedFunctionOpData::RGB_TO_HSY_LOG; + break; + case GRADING_VIDEO: + hsyStyle = FixedFunctionOpData::RGB_TO_HSY_VID; + break; + } + + st.newLine() << "{"; // establish scope so local variable names won't conflict + st.indent(); + ConstFixedFunctionOpDataRcPtr funcOpData = std::make_shared(hsyStyle); + GetFixedFunctionGPUProcessingText(shaderCreator, st, funcOpData); + st.dedent(); + st.newLine() << "}"; + } + + // Lin to Log (on Luma only). + if (doLinToLog) + { + // NB: Although the linToLog and logToLin are correct inverses, the limits of + // floating-point arithmetic cause errors in the lowest bit of the round trip. + + st.newLine() << "// Convert from lin to log."; + AddLinToLogShaderChannelBlue(shaderCreator, st); + st.newLine() << ""; + } + + // Apply the hue curves. + + st.newLine() << ""; + // HUE-SAT + st.newLine() << "float hueSatGain = max(0., " << props.m_eval << "(1, " << pix << ".r, 1.));"; + // HUE-LUM + st.newLine() << "float hueLumGain = max(0., " << props.m_eval << "(2, " << pix << ".r, 1.));"; + // HUE-HUE + st.newLine() << pix << ".r = " << props.m_eval << "(0, " << pix << ".r, " << pix << ".r);"; + // SAT-SAT + st.newLine() << "" << pix << ".g = max(0., " << props.m_eval << "(4, " << pix << ".g, " << pix << ".g));"; + // LUM-SAT + st.newLine() << "float lumSatGain = max(0., " << props.m_eval << "(3, " << pix << ".b, 1.));"; + // SAT-LUM + st.newLine() << "float satGain = lumSatGain * hueSatGain;"; + st.newLine() << "" << pix << ".g = satGain * " << pix << ".g;"; + st.newLine() << "float satLumGain = max(0., " << props.m_eval << "(6, " << pix << ".g, 1.));"; + // LUM-LUM + st.newLine() << pix << ".b = " << props.m_eval << "(5, " << pix << ".b, " << pix << ".b);"; + st.newLine() << ""; + + // Log to Lin. + if (doLinToLog) + { + st.newLine() << ""; + st.newLine() << "// Convert from log to lin."; + AddLogToLinShaderChannelBlue(shaderCreator, st); + } + + st.newLine() << ""; + st.newLine() << "hueLumGain = 1. - (1. - hueLumGain) * min( 1., " << pix << ".g );"; + if (style == GRADING_LOG) + { + // Use shift rather than scale for log mode. + st.newLine() << pix << ".b = " << pix << ".b + (hueLumGain + satLumGain - 2.) * 0.1;"; + } + else + { + // Note this is applied in linear space, for linear style. + st.newLine() << pix << ".b = " << pix << ".b * hueLumGain * satLumGain;"; + } + st.newLine() << ""; + + // HUE-FX + st.newLine() << pix << ".r = " << pix << ".r - floor( " << pix << ".r );"; + st.newLine() << pix << ".r = " << pix << ".r + " << props.m_eval << "(7, " << pix << ".r, 0.);"; + + // Add the conversion from HSY to RGB. + if (doRGBToHSY) + { + FixedFunctionOpData::Style hsyStyle = FixedFunctionOpData::RGB_TO_HSY_LIN; + switch (style) + { + case GRADING_LIN: + hsyStyle = FixedFunctionOpData::HSY_LIN_TO_RGB; + break; + case GRADING_LOG: + hsyStyle = FixedFunctionOpData::HSY_LOG_TO_RGB; + break; + case GRADING_VIDEO: + hsyStyle = FixedFunctionOpData::HSY_VID_TO_RGB; + break; + } + st.newLine() << "{"; + st.indent(); + ConstFixedFunctionOpDataRcPtr funcOpData = std::make_shared(hsyStyle); + GetFixedFunctionGPUProcessingText(shaderCreator, st, funcOpData); + st.dedent(); + st.newLine() << "}"; + } + + if (dyn) + { + st.dedent(); + st.newLine() << "}"; + } +} + +void AddGCInverseShader(GpuShaderCreatorRcPtr & shaderCreator, + GpuShaderText & st, + const GCProperties & props, + bool dyn, + bool doLinToLog, + bool doRGBToHSY, + bool drawCurveOnly, + GradingStyle style) +{ + const std::string pix(shaderCreator->getPixelName()); + + if (drawCurveOnly) + { + // Note that this is not within the localBypass if-statement since outColor + // needs to get processed even if isDefault is true for all curves since + // the default for the horizontal curves is all 1 rather than an identity. + st.newLine() << pix << ".r = " << props.m_eval << "(1, " << pix << ".r, 1.);"; // HUE-SAT + st.newLine() << pix << ".g = " << props.m_eval << "(1, " << pix << ".g, 1.);"; // HUE-SAT + st.newLine() << pix << ".b = " << props.m_eval << "(1, " << pix << ".b, 1.);"; // HUE-SAT + return; + } + + if (dyn) + { + st.newLine() << "if (!" << st.castToBool(props.m_localBypass) << ")"; + st.newLine() << "{"; + st.indent(); + } + + // Add the conversion from RGB to HSY. + if (doRGBToHSY) + { + FixedFunctionOpData::Style hsyStyle = FixedFunctionOpData::RGB_TO_HSY_LIN; + switch (style) + { + case GRADING_LIN: + hsyStyle = FixedFunctionOpData::RGB_TO_HSY_LIN; + break; + case GRADING_LOG: + hsyStyle = FixedFunctionOpData::RGB_TO_HSY_LOG; + break; + case GRADING_VIDEO: + hsyStyle = FixedFunctionOpData::RGB_TO_HSY_VID; + break; + } + + st.newLine() << "{"; // establish scope so local variable names won't conflict + st.indent(); + ConstFixedFunctionOpDataRcPtr funcOpData = std::make_shared(hsyStyle); + GetFixedFunctionGPUProcessingText(shaderCreator, st, funcOpData); + st.dedent(); + st.newLine() << "}"; + } + + // Apply the hue curves inverse. + + // Invert HUE-FX. + st.newLine() << pix << ".r = " << props.m_evalRevHue << "(7, " << pix << ".r);"; + + // Invert HUE-HUE. + st.newLine() << pix << ".r = " << props.m_evalRevHue << "(0, " << pix << ".r);"; + st.newLine() << ""; + + // Use the inverted hue to calculate the HUE-SAT & HUE-LUM gains. + st.newLine() << pix << ".r = " << pix << ".r - floor( " << pix << ".r );"; + st.newLine() << "float hueSatGain = max(0., " << props.m_eval << "(1, " << pix << ".r, 1.));"; + st.newLine() << "float hueLumGain = max(0., " << props.m_eval << "(2, " << pix << ".r, 1.));"; + + // Use the output sat to calculate the SAT-LUM gain. + st.newLine() << "" << pix << ".g = max(0., " << pix << ".g);"; + st.newLine() << "float satLumGain = max(0., " << props.m_eval << "(6, " << pix << ".g, 1.));"; + + st.newLine() << ""; + st.newLine() << "hueLumGain = 1. - (1. - hueLumGain) * min( 1., " << pix << ".g );"; + + // Invert the lum gain. + if (style == GRADING_LOG) + { + // Use shift rather than scale for log mode. + st.newLine() << pix << ".b = " << pix << ".b - (hueLumGain + satLumGain - 2.) * 0.1;"; + } + else + { + // Note this is applied in linear space, for linear style. + st.newLine() << pix << ".b = " << pix << ".b / max(0.01, hueLumGain * satLumGain);"; + } + st.newLine() << ""; + + if (doLinToLog) + { + st.newLine() << "// Convert from lin to log."; + AddLinToLogShaderChannelBlue(shaderCreator, st); + st.newLine() << ""; + } + + // Invert LUM-LUM. + st.newLine() << pix << ".b = " << props.m_evalRev << "(5, " << pix << ".b);"; + st.newLine() << ""; + + // Use it to calc the LUM-SAT gain. + st.newLine() << "float lumSatGain = max(0., " << props.m_eval << "(3, " << pix << ".b, 1.));"; + + if (doLinToLog) + { + st.newLine() << ""; + st.newLine() << "// Convert from log to lin."; + AddLogToLinShaderChannelBlue(shaderCreator, st); + } + + // Invert the sat gain. + st.newLine() << "float satGain = max(0.01, lumSatGain * hueSatGain);"; + st.newLine() << "" << pix << ".g = " << pix << ".g / satGain;"; + + // Invert SAT-SAT. + st.newLine() << "" << pix << ".g = max(0., " << props.m_evalRev << "(4, " << pix << ".g));"; + + // Add the conversion from HSY to RGB. + if (doRGBToHSY) + { + FixedFunctionOpData::Style hsyStyle = FixedFunctionOpData::RGB_TO_HSY_LIN; + switch (style) + { + case GRADING_LIN: + hsyStyle = FixedFunctionOpData::HSY_LIN_TO_RGB; + break; + case GRADING_LOG: + hsyStyle = FixedFunctionOpData::HSY_LOG_TO_RGB; + break; + case GRADING_VIDEO: + hsyStyle = FixedFunctionOpData::HSY_VID_TO_RGB; + break; + } + st.newLine() << "{"; + st.indent(); + ConstFixedFunctionOpDataRcPtr funcOpData = std::make_shared(hsyStyle); + GetFixedFunctionGPUProcessingText(shaderCreator, st, funcOpData); + st.dedent(); + st.newLine() << "}"; + } + + if (dyn) + { + st.dedent(); + st.newLine() << "}"; + } +} + +} // anon + +void GetGradingHueCurveGPUShaderProgram(GpuShaderCreatorRcPtr & shaderCreator, + ConstGradingHueCurveOpDataRcPtr & gcData) +{ + const bool dyn = gcData->isDynamic() && shaderCreator->getLanguage() != LANGUAGE_OSL_1; + if (!dyn) + { + auto propGC = gcData->getDynamicPropertyInternal(); + if (propGC->getLocalBypass()) + { + return; + } + } + + if (gcData->isDynamic() && shaderCreator->getLanguage() == LANGUAGE_OSL_1) + { + std::string msg("The dynamic properties are not yet supported by the 'Open Shading language"\ + " (OSL)' translation: The '"); + msg += opPrefix; + msg += "' dynamic property is replaced by a local variable."; + + LogWarning(msg); + } + + const GradingStyle style = gcData->getStyle(); + const TransformDirection dir = gcData->getDirection(); + + GpuShaderText st(shaderCreator->getLanguage()); + st.indent(); + + st.newLine() << ""; + st.newLine() << "// Add GradingHueCurve " + << TransformDirectionToString(dir) << " processing"; + st.newLine() << ""; + st.newLine() << "{"; + st.indent(); + + GCProperties properties; + SetGCProperties(shaderCreator, dyn, properties); + + auto dynProp = gcData->getDynamicPropertyInternal(); + + if (dyn) + { + // Add the dynamic property to the shader creator. + + // Property is decoupled. + auto shaderProp = dynProp->createEditableCopy(); + DynamicPropertyRcPtr newProp = shaderProp; + shaderCreator->addDynamicProperty(newProp); + + // Add uniforms only if needed. + AddGCPropertiesUniforms(shaderCreator, shaderProp, properties); + + // Add helper function plus global variables if they are not dynamic. + AddCurveEvalMethodTextToShaderProgram(shaderCreator, gcData, properties, dyn); + } + else + { + // Declare the op specific helper function. + AddCurveEvalMethodTextToShaderProgram(shaderCreator, gcData, properties, dyn); + } + + const bool doLinToLog = style == GRADING_LIN; + const bool doRGBToHSY = gcData->getRGBToHSY() == HSY_TRANSFORM_1; + switch (dir) + { + case TRANSFORM_DIR_FORWARD: + AddGCForwardShader(shaderCreator, st, properties, dyn, doLinToLog, doRGBToHSY, + dynProp->getValue()->getDrawCurveOnly(), style); + break; + case TRANSFORM_DIR_INVERSE: + AddGCInverseShader(shaderCreator, st, properties, dyn, doLinToLog, doRGBToHSY, + dynProp->getValue()->getDrawCurveOnly(), style); + break; + } + + st.dedent(); + st.newLine() << "}"; + + st.dedent(); + shaderCreator->addToFunctionShaderCode(st.string().c_str()); +} + +} // OCIO_NAMESPACE diff --git a/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpGPU.h b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpGPU.h new file mode 100644 index 0000000000..a0c6630211 --- /dev/null +++ b/src/OpenColorIO/ops/gradinghuecurve/GradingHueCurveOpGPU.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + +#ifndef INCLUDED_OCIO_HUECURVE_GPU_H +#define INCLUDED_OCIO_HUECURVE_GPU_H + +#include + +#include "GpuShaderUtils.h" +#include "ops/gradinghuecurve/GradingHueCurveOpData.h" + +namespace OCIO_NAMESPACE +{ + +void GetGradingHueCurveGPUShaderProgram(GpuShaderCreatorRcPtr & shaderCreator, + ConstGradingHueCurveOpDataRcPtr & gpData); + +} // namespace OCIO_NAMESPACE + +#endif diff --git a/src/OpenColorIO/ops/gradingrgbcurve/GradingBSplineCurve.cpp b/src/OpenColorIO/ops/gradingrgbcurve/GradingBSplineCurve.cpp index 72c656725a..8dc8d8d23d 100644 --- a/src/OpenColorIO/ops/gradingrgbcurve/GradingBSplineCurve.cpp +++ b/src/OpenColorIO/ops/gradingrgbcurve/GradingBSplineCurve.cpp @@ -6,6 +6,7 @@ #include #include +#include "OpenColorIO/DynamicProperty.h" #include "GpuShaderUtils.h" #include "ops/gradingrgbcurve/GradingBSplineCurve.h" @@ -13,163 +14,326 @@ namespace OCIO_NAMESPACE { -GradingBSplineCurveRcPtr GradingBSplineCurve::Create(size_t size) -{ - auto newSpline = std::make_shared(size); - GradingBSplineCurveRcPtr res = newSpline; - return res; -} - -GradingBSplineCurveRcPtr GradingBSplineCurve::Create(std::initializer_list values) -{ - auto newSpline = std::make_shared(values.size()); - size_t i = 0; - for (const auto & c : values) - { - newSpline->getControlPoint(i++) = c; - } - GradingBSplineCurveRcPtr res; - res = newSpline; - return res; -} - -GradingBSplineCurveImpl::GradingBSplineCurveImpl(size_t size) - : m_controlPoints(size), m_slopesArray(size, 0.f) -{ -} - -GradingBSplineCurveImpl::GradingBSplineCurveImpl(const std::vector & controlPoints) - : m_controlPoints(controlPoints), m_slopesArray(controlPoints.size(), 0.f) +namespace { -} -GradingBSplineCurveRcPtr GradingBSplineCurveImpl::createEditableCopy() const -{ - auto copy = std::make_shared(0); - copy->m_controlPoints = m_controlPoints; - copy->m_slopesArray = m_slopesArray; - GradingBSplineCurveRcPtr res; - res = copy; - return res; -} +//------------------------------------------------------------------------------------------------ +// +void PrepHueCurveData(const std::vector& ctrlPnts, + std::vector& outCtrlPnts, + bool isPeriodic, + bool isHorizontal) + { + size_t numCtrlPnts = ctrlPnts.size(); + for (unsigned i = 0; i < numCtrlPnts; ++i) + { + const float xval = ctrlPnts[ i ].m_x; + const float yval = ctrlPnts[ i ].m_y; + // Wrap periodic x values into [0,1). + if (isPeriodic && (xval < 0.f)) + { + outCtrlPnts.push_back(GradingControlPoint(xval + 1.f, isHorizontal ? yval : yval + 1.f)); + } + else if (isPeriodic && (xval >= 1.f)) + { + outCtrlPnts.push_back(GradingControlPoint(xval - 1.f, isHorizontal ? yval : yval - 1.f)); + } + else + { + outCtrlPnts.push_back(GradingControlPoint(xval, yval)); + } + } + + // Sort x and y based on x order. + for (unsigned i = 0; i < numCtrlPnts; ++i) + { + unsigned min_index = i; + float min_val = outCtrlPnts[i].m_x; + for (unsigned j = i + 1; j < numCtrlPnts; ++j) + { + if (outCtrlPnts[j].m_x < min_val) + { + min_val = outCtrlPnts[j].m_x; + min_index = j; + } + } + + std::swap( outCtrlPnts[i], outCtrlPnts[min_index] ); + } + + // Ensure that there is a minimum space between the x values. + const float tol = 2e-3f; + const float x_span = outCtrlPnts[numCtrlPnts - 1].m_x - outCtrlPnts[0].m_x; + for (unsigned i = 1; i < outCtrlPnts.size(); ++i) + { + if ( (outCtrlPnts[i].m_x - outCtrlPnts[i - 1].m_x) < x_span * tol ) + { + outCtrlPnts[i].m_x = outCtrlPnts[i - 1].m_x + x_span * tol; + } + } + if (!isHorizontal) + { + // Ensure that there is a minimum space between the y values. + const float y_span = outCtrlPnts[numCtrlPnts - 1].m_y - outCtrlPnts[0].m_y; + for (unsigned i = 1; i < outCtrlPnts.size(); ++i) + { + if ( (outCtrlPnts[i].m_y - outCtrlPnts[i - 1].m_y) < y_span * tol ) + { + outCtrlPnts[i].m_y = outCtrlPnts[i - 1].m_y + y_span * tol; + } + } + } -size_t GradingBSplineCurveImpl::getNumControlPoints() const noexcept -{ - return m_controlPoints.size(); -} + if (isPeriodic) + { + // Copy a value from each side and wrap it around to the other side. + GradingControlPoint firstCtrlPnt = outCtrlPnts[numCtrlPnts - 1]; + firstCtrlPnt.m_x -= 1.f; + firstCtrlPnt.m_y = isHorizontal ? firstCtrlPnt.m_y : firstCtrlPnt.m_y - 1.f; + outCtrlPnts.insert(outCtrlPnts.begin(), firstCtrlPnt); + + GradingControlPoint lastCtrlPnt = outCtrlPnts[1]; + lastCtrlPnt.m_x += 1.f; + lastCtrlPnt.m_y = isHorizontal ? lastCtrlPnt.m_y : lastCtrlPnt.m_y + 1.f; + outCtrlPnts.push_back(lastCtrlPnt); + } + } -void GradingBSplineCurveImpl::setNumControlPoints(size_t size) +//------------------------------------------------------------------------------------------------ +// +float CalcKsi(unsigned i, + const std::vector& outCtrlPnts, + const std::vector& slopes) { - m_controlPoints.resize(size); - m_slopesArray.resize(size, 0.f); -} + const GradingControlPoint& p0 = outCtrlPnts[i]; + const GradingControlPoint& p1 = outCtrlPnts[i + 1]; -void GradingBSplineCurveImpl::validateIndex(size_t index) const -{ - const size_t numPoints = m_controlPoints.size(); - if (index >= numPoints) + const float k = 0.2f; + const float dx = p1.m_x - p0.m_x; + const float secantSlope = (p1.m_y - p0.m_y) / dx; + float secant = secantSlope; + float m0 = slopes[i]; + float m1 = slopes[i + 1]; + if (secant < 0.f) { - std::ostringstream oss; - oss << "There are '"<< numPoints << "' control points. '" << index << "' is invalid."; - throw Exception(oss.str().c_str()); + m0 = -slopes[i]; m1 = -slopes[i + 1]; + secant = -secant; } -} -const GradingControlPoint & GradingBSplineCurveImpl::getControlPoint(size_t index) const -{ - validateIndex(index); - return m_controlPoints[index]; -} - -GradingControlPoint & GradingBSplineCurveImpl::getControlPoint(size_t index) -{ - validateIndex(index); - return m_controlPoints[index]; -} + const float x_mid = p0.m_x + 0.5f * dx; + const float left_bnd = p0.m_x + dx * k; + const float right_bnd = p1.m_x - dx * k; + float top_bnd = left_bnd; + float bottom_bnd = right_bnd; + float m_min = m0; + float m_max = m1; + if (m0 > m1) + { + m_max = m0; m_min = m1; + top_bnd = right_bnd; bottom_bnd = left_bnd; + } -float GradingBSplineCurveImpl::getSlope(size_t index) const -{ - validateIndex(index); - return m_slopesArray[index]; -} + const float dm = m_max - m_min; + const float b = 1.f - 0.5f * k; + const float b_high = m_min + b * dm; + const float b_low = m_min + (1.f - b) * dm; + const float bbb = m_max * 4.f; + const float bb = m_max * 1.1f; + const float m_rel_diff = dm / std::max(0.01f, m_max); + const float alpha = std::max( 0.f, std::min( (m_rel_diff - 0.05f) / (0.75f - 0.05f), 1.f ) ); + top_bnd = x_mid + alpha * (top_bnd - x_mid); + bottom_bnd = x_mid + alpha * (bottom_bnd - x_mid); -void GradingBSplineCurveImpl::setSlope(size_t index, float slope) -{ - validateIndex(index); - m_slopesArray[index] = slope; + // Calculate the middle knot. + float ksi = 0.f; + if (secant >= bbb) + { + ksi = x_mid; + } + else if (secant > bb) + { + const float blend = (secant - bb) / (bbb - bb); + ksi = top_bnd + blend * (x_mid - top_bnd); + } + else if (secant >= b_high) + { + ksi = top_bnd; + } + else if ((secant > b_low) && (b_high != b_low)) + { + const float blend = (secant - b_low) / (b_high - b_low); + ksi = bottom_bnd + blend * (top_bnd - bottom_bnd); + } + else + { + ksi = bottom_bnd; + } + return ksi; } -bool GradingBSplineCurveImpl::slopesAreDefault() const +//------------------------------------------------------------------------------------------------ +// +void FitHueSpline(const std::vector& ctrlPnts, + const std::vector& slopes, + std::vector& knots, + std::vector& coefsA, + std::vector& coefsB, + std::vector& coefsC) { - for (size_t i = 0; i < m_slopesArray.size(); ++i) + knots.push_back( ctrlPnts[0].m_x ); + size_t numCtrlPnts = ctrlPnts.size(); + for (unsigned i = 0; i < numCtrlPnts - 1; ++i) { - if (m_slopesArray[i] != 0.f) + const GradingControlPoint& p0 = ctrlPnts[i]; + const GradingControlPoint& p1 = ctrlPnts[i + 1]; + + const float dx = p1.m_x - p0.m_x; + const float secantSlope = (p1.m_y - p0.m_y) / dx; + + if ( fabsf( (slopes[i] + slopes[i + 1]) - 2.f * secantSlope ) <= 1e-5f ) { - return false; + coefsC.push_back( p0.m_y ); + coefsB.push_back( slopes[i] ); + coefsA.push_back( 0.5f * (slopes[i + 1] - slopes[i]) / dx ); } + else + { + // Calculate the middle knot. + const float ksi = CalcKsi(i, ctrlPnts, slopes); + + // Calculate the coefficients. + const float m_bar = (2.f * secantSlope - slopes[i + 1]) + + (slopes[i + 1] - slopes[i]) * (ksi - p0.m_x) / (p1.m_x - p0.m_x); + const float eta = (m_bar - slopes[i]) / (ksi - p0.m_x); + coefsC.push_back( p0.m_y ); + coefsB.push_back( slopes[i] ); + coefsA.push_back( 0.5f * eta ); + coefsC.push_back( p0.m_y + slopes[i] * (ksi - p0.m_x) + 0.5f * eta * (ksi - p0.m_x) * (ksi - p0.m_x) ); + coefsB.push_back( m_bar ); + coefsA.push_back( 0.5f * (slopes[i + 1] - m_bar) / (p1.m_x - ksi) ); + knots.push_back( ksi ); + } + + knots.push_back( p1.m_x ); } - return true; } - -void GradingBSplineCurveImpl::validate() const + +//------------------------------------------------------------------------------------------------ +// +void EstimateHueSlopes(const std::vector& ctrlPnts, + std::vector& slopes, + bool isPeriodic, + bool isHorizontal) { - const size_t numPoints = m_controlPoints.size(); - if (numPoints < 2) + slopes.clear(); + size_t numCtrlPnts = ctrlPnts.size(); + std::vector secantSlope; + std::vector secantLen; + for (unsigned i = 0; i < numCtrlPnts - 1; ++i) { - throw Exception("There must be at least 2 control points."); + const GradingControlPoint& p0 = ctrlPnts[i]; + const GradingControlPoint& p1 = ctrlPnts[i + 1]; + + const float del_x = p1.m_x - p0.m_x; // PrepHueCurveData ensures this is > 0 + const float del_y = p1.m_y - p0.m_y; + secantSlope.push_back( del_y / del_x ); + secantLen.push_back( sqrt( del_x * del_x + del_y * del_y ) ); } - if (numPoints != m_slopesArray.size()) + + if (numCtrlPnts == 2) { - throw Exception("The slopes array must be the same length as the control points."); + slopes.push_back( secantSlope[0] ); + slopes.push_back( secantSlope[0] ); + return; } - // Make sure the points are non-decreasing. - float lastX = -std::numeric_limits::max(); - for (size_t i = 0; i < numPoints; ++i) + slopes.push_back(0.f); + + if (isHorizontal) // All horizontal curves and diagonal hue-hue. { - // Test x values only. - const float x = m_controlPoints[i].m_x; - if (x < lastX) + for (unsigned i = 1; i < numCtrlPnts - 1; ++i) { - std::ostringstream oss; - oss << "Control point at index " << i << " has a x coordinate '" << x << "' that is "; - oss << "less from previous control point x cooordinate '" << lastX << "'."; - throw Exception(oss.str().c_str()); + float s = 0.f; + float denom = secantSlope[i] + secantSlope[i - 1]; + if (fabsf(denom) < 1e-3f) + { + const float minval = denom < 0.f ? -1e-3f : 1e-3f; + s = 2.f * secantSlope[i] * secantSlope[i - 1] / minval; + } + else + { + s = 2.f * secantSlope[i] * secantSlope[i - 1] / denom; + } + // Set slope to zero at flat areas or extrema. + if ( secantSlope[i] * secantSlope[i - 1] <= 0.f ) + { + s = 0.f; + } + slopes.push_back( s ); } - lastX = x; + slopes.push_back( 0.5f * ( 3.f * secantSlope[numCtrlPnts - 2] - slopes[numCtrlPnts - 2] ) ); + slopes[0] = 0.5f * ( 3.f * secantSlope[0] - slopes[1] ); } -} - -bool GradingBSplineCurveImpl::isIdentity() const -{ - for (const auto & cp : m_controlPoints) + else // Diagonal curves except hue-hue (LvL and SvS). { - if (cp.m_x != cp.m_y) + unsigned i = 0; + while (true) { - return false; + unsigned j = i; + float DL = secantLen[i]; + while ( ( j < numCtrlPnts - 2 ) && ( fabsf( secantSlope[j + 1] - secantSlope[j] ) < 1e-6f ) ) + { + DL += secantLen[ j + 1 ]; + j++; + } + for (unsigned k = i; k <= j; ++k) + secantLen[k] = DL; + if (j >= numCtrlPnts - 3) + break; + i = j + 1; } + + for (unsigned k = 1; k < numCtrlPnts - 1; ++k) + { + const float s = ( secantLen[k] * secantSlope[k] + secantLen[k - 1] * secantSlope[k - 1] ) / + ( secantLen[k] + secantLen[k - 1] ); + slopes.push_back( s ); + } + + const float minSlope = 0.01f; + slopes.push_back( std::max(minSlope, 0.5f * ( 3.f * secantSlope[numCtrlPnts - 2] - slopes[numCtrlPnts - 2] )) ); + slopes[0] = std::max(minSlope, 0.5f * ( 3.f * secantSlope[0] - slopes[1] )); } - if (!slopesAreDefault()) - { - return false; - } - return true; -} -bool IsGradingCurveIdentity(const ConstGradingBSplineCurveRcPtr & curve) -{ - auto curveImpl = dynamic_cast(curve.get()); - if (curveImpl) - { - return curveImpl->isIdentity(); - } - return false; -} + // Adjust slopes that are not shape-preserving. + for (unsigned i = 0; i < numCtrlPnts - 1; ++i) + { + float k = 0.2f; + if (fabsf(slopes[i]) > fabsf(slopes[i+1])) + k = 1.f - k; + const float m_near_min = slopes[i] + k * (slopes[i + 1] - slopes[i]); + float scale = 1.f; + if (m_near_min != 0.f) + scale = 0.75f * 2.f * secantSlope[i] / m_near_min; + if (scale < 1.f) + { + slopes[i] = scale * slopes[i]; + slopes[i + 1] = scale * slopes[i + 1]; + } + } -namespace -{ + // Copy end slopes from the opposite side. + if (isPeriodic) + { + slopes[0] = slopes[numCtrlPnts - 2]; + slopes[numCtrlPnts - 1] = slopes[1]; + } +} -void EstimateSlopes(const std::vector & ctrlPnts, std::vector & slopes) +//------------------------------------------------------------------------------------------------ +// +void EstimateRGBSlopes(const std::vector & ctrlPnts, + std::vector & slopes) { std::vector secantSlope; std::vector secantLen; @@ -219,12 +383,14 @@ void EstimateSlopes(const std::vector & ctrlPnts, std::vect slopes[0] = std::max(0.01f, 0.5f * (3.f * secantSlope[0] - slopes[1])); } -void FitSpline(const std::vector & ctrlPnts, - const std::vector & slopes, - std::vector & knots, - std::vector & coefsA, - std::vector & coefsB, - std::vector & coefsC) +//------------------------------------------------------------------------------------------------ +// +void FitRGBSpline(const std::vector & ctrlPnts, + const std::vector & slopes, + std::vector & knots, + std::vector & coefsA, + std::vector & coefsB, + std::vector & coefsC) { const size_t numCtrlPnts = ctrlPnts.size(); @@ -279,9 +445,11 @@ void FitSpline(const std::vector & ctrlPnts, } } -bool AdjustSlopes(const std::vector & ctrlPnts, - std::vector & slopes, - std::vector & knots) +//------------------------------------------------------------------------------------------------ +// +bool AdjustRGBSlopes(const std::vector & ctrlPnts, + std::vector & slopes, + std::vector & knots) { bool adjustment_done = false; size_t i = 0, j = 0; @@ -321,7 +489,308 @@ bool AdjustSlopes(const std::vector & ctrlPnts, } // namespace -void GradingBSplineCurveImpl::computeKnotsAndCoefs(KnotsCoefs & knotsCoefs, int curveIdx) const +//------------------------------------------------------------------------------------------------ +// +GradingBSplineCurveRcPtr GradingBSplineCurve::Create(size_t size) +{ + auto newSpline = std::make_shared(size); + GradingBSplineCurveRcPtr res = newSpline; + return res; +} + +GradingBSplineCurveRcPtr GradingBSplineCurve::Create(size_t size, BSplineType splineType) +{ + auto newSpline = std::make_shared(size, splineType); + GradingBSplineCurveRcPtr res = newSpline; + return res; +} + +GradingBSplineCurveRcPtr GradingBSplineCurve::Create(size_t size, HueCurveType curveType) +{ + const BSplineType splineType = GradingHueCurve::GetBSplineTypeForHueCurveType(curveType); + auto newSpline = std::make_shared(size, splineType); + GradingBSplineCurveRcPtr res = newSpline; + return res; +} + +GradingBSplineCurveRcPtr GradingBSplineCurve::Create(std::initializer_list values) +{ + auto newSpline = std::make_shared(values.size()); + size_t i = 0; + for (const auto & c : values) + { + newSpline->getControlPoint(i++) = c; + } + GradingBSplineCurveRcPtr res; + res = newSpline; + return res; +} + +GradingBSplineCurveRcPtr GradingBSplineCurve::Create(std::initializer_list values, BSplineType splineType) +{ + auto newSpline = std::make_shared(values.size(), splineType); + size_t i = 0; + for (const auto & c : values) + { + newSpline->getControlPoint(i++) = c; + } + GradingBSplineCurveRcPtr res; + res = newSpline; + return res; +} + +GradingBSplineCurveRcPtr GradingBSplineCurve::Create(std::initializer_list values, HueCurveType curveType) +{ + const BSplineType splineType = GradingHueCurve::GetBSplineTypeForHueCurveType(curveType); + auto newSpline = std::make_shared(values.size(), splineType); + size_t i = 0; + for (const auto & c : values) + { + newSpline->getControlPoint(i++) = c; + } + GradingBSplineCurveRcPtr res; + res = newSpline; + return res; +} + +GradingBSplineCurveImpl::GradingBSplineCurveImpl(size_t size) + : m_controlPoints(size), m_slopesArray(size, 0.f), m_splineType(B_SPLINE) +{ +} + +GradingBSplineCurveImpl::GradingBSplineCurveImpl(size_t size, BSplineType splineType) + : m_controlPoints(size), m_slopesArray(size, 0.f), m_splineType(splineType) +{ +} + +GradingBSplineCurveImpl::GradingBSplineCurveImpl(const std::vector & controlPoints) + : m_controlPoints(controlPoints), m_slopesArray(controlPoints.size(), 0.f), m_splineType(B_SPLINE) +{ +} + +GradingBSplineCurveImpl::GradingBSplineCurveImpl(const std::vector & controlPoints, BSplineType splineType) + : m_controlPoints(controlPoints), m_slopesArray(controlPoints.size(), 0.f), m_splineType(splineType) +{ +} + +GradingBSplineCurveRcPtr GradingBSplineCurveImpl::createEditableCopy() const +{ + auto copy = std::make_shared(0); + copy->m_controlPoints = m_controlPoints; + copy->m_slopesArray = m_slopesArray; + copy->m_splineType = m_splineType; + GradingBSplineCurveRcPtr res; + res = copy; + return res; +} + +BSplineType GradingBSplineCurveImpl::getSplineType() const +{ + return m_splineType; +} + +void GradingBSplineCurveImpl::setSplineType(BSplineType splineType) +{ + m_splineType = splineType; +} + +size_t GradingBSplineCurveImpl::getNumControlPoints() const noexcept +{ + return m_controlPoints.size(); +} + +void GradingBSplineCurveImpl::setNumControlPoints(size_t size) +{ + m_controlPoints.resize(size); + m_slopesArray.resize(size, 0.f); +} + +void GradingBSplineCurveImpl::validateIndex(size_t index) const +{ + const size_t numPoints = m_controlPoints.size(); + if (index >= numPoints) + { + std::ostringstream oss; + oss << "There are '"<< numPoints << "' control points. '" << index << "' is out of bounds."; + throw Exception(oss.str().c_str()); + } +} + +const GradingControlPoint & GradingBSplineCurveImpl::getControlPoint(size_t index) const +{ + validateIndex(index); + return m_controlPoints[index]; +} + +GradingControlPoint & GradingBSplineCurveImpl::getControlPoint(size_t index) +{ + validateIndex(index); + return m_controlPoints[index]; +} + +float GradingBSplineCurveImpl::getSlope(size_t index) const +{ + validateIndex(index); + return m_slopesArray[index]; +} + +void GradingBSplineCurveImpl::setSlope(size_t index, float slope) +{ + validateIndex(index); + m_slopesArray[index] = slope; +} + +bool GradingBSplineCurveImpl::slopesAreDefault() const +{ + for (size_t i = 0; i < m_slopesArray.size(); ++i) + { + if (m_slopesArray[i] != 0.f) + { + return false; + } + } + return true; +} + +//------------------------------------------------------------------------------------------------ +// +void GradingBSplineCurveImpl::validate() const +{ + const size_t numPoints = m_controlPoints.size(); + if (numPoints < 2) + { + throw Exception("There must be at least 2 control points."); + } + if (numPoints != m_slopesArray.size()) + { + throw Exception("The slopes array must be the same length as the control points."); + } + + // Make sure the x-coordinates are non-decreasing. + float lastX = -std::numeric_limits::max(); + for (size_t i = 0; i < numPoints; ++i) + { + const float x = m_controlPoints[i].m_x; + if (x < lastX) + { + std::ostringstream oss; + oss << "Control point at index " << i << " has a x coordinate '" << x << "' that is "; + oss << "less than previous control point x coordinate '" << lastX << "'."; + throw Exception(oss.str().c_str()); + } + lastX = x; + } + + // The x-coordinates for a hue-hue spline must be in [0,1]. + if (m_splineType == HUE_HUE_B_SPLINE) + { + if (m_controlPoints[0].m_x < 0.f) + { + std::ostringstream oss; + oss << "The HUE-HUE spline may not have negative x coordinates."; + throw Exception(oss.str().c_str()); + } + else if (m_controlPoints[numPoints - 1].m_x > 1.f) + { + std::ostringstream oss; + oss << "The HUE-HUE spline may not have x coordinates greater than one."; + throw Exception(oss.str().c_str()); + } + } + + // Make sure the y-coordinates are non-decreasing, for diagonal spline types. + if ( m_splineType == B_SPLINE || + m_splineType == DIAGONAL_B_SPLINE || + m_splineType == HUE_HUE_B_SPLINE ) + { + float lastY = -std::numeric_limits::max(); + if (m_splineType == HUE_HUE_B_SPLINE) + { + // The curve is diagonal but continues in a periodic way, so wrap the last point + // around and ensure the first point would preserve monotonicity. + lastY = m_controlPoints[numPoints - 1].m_y - 1.f; + } + + for (size_t i = 0; i < numPoints; ++i) + { + const float y = m_controlPoints[i].m_y; + if (y < lastY) + { + std::ostringstream oss; + oss << "Control point at index " << i << " has a y coordinate '" << y << "' that is "; + oss << "less than previous control point y coordinate '" << lastY << "'."; + throw Exception(oss.str().c_str()); + } + lastY = y; + } + } + + // Don't allow only x values of 0 and 1 for periodic curves (since they are essentially only one point). + if (numPoints == 2) + { + // Periodic curve types only. + if ( m_splineType == PERIODIC_1_B_SPLINE || + m_splineType == PERIODIC_0_B_SPLINE || + m_splineType == HUE_HUE_B_SPLINE ) + { + const float del_x = m_controlPoints[1].m_x - m_controlPoints[0].m_x; + if (std::abs(1.f - del_x) < 1e-3) + { + throw Exception("The periodic spline x coordinates may not wrap to the same value."); + } + } + } +} + +//------------------------------------------------------------------------------------------------ +// +bool GradingBSplineCurveImpl::isIdentity() const +{ + bool isIdentity = true; + if( m_splineType == DIAGONAL_B_SPLINE || m_splineType == B_SPLINE || m_splineType == HUE_HUE_B_SPLINE ) + { + isIdentity = std::all_of(m_controlPoints.cbegin(), + m_controlPoints.cend(), + [](const GradingControlPoint& cp) { return cp.m_x == cp.m_y; }); + } + else if( m_splineType == PERIODIC_0_B_SPLINE ) + { + isIdentity = std::all_of(m_controlPoints.cbegin(), + m_controlPoints.cend(), + [](const GradingControlPoint& cp) { return cp.m_y == 0.f; }); + } + else if( m_splineType == HORIZONTAL1_B_SPLINE || m_splineType == PERIODIC_1_B_SPLINE ) + { + isIdentity = std::all_of(m_controlPoints.cbegin(), + m_controlPoints.cend(), + [](const GradingControlPoint& cp) { return cp.m_y == 1.f; }); + } else + { + throw Exception("Unknown curve type: Could not determine if curve is identity."); + } + + if (!isIdentity || !slopesAreDefault()) + { + return false; + } + return true; +} + +//------------------------------------------------------------------------------------------------ +// +bool IsGradingCurveIdentity(const ConstGradingBSplineCurveRcPtr & curve) +{ + auto curveImpl = dynamic_cast(curve.get()); + if (curveImpl) + { + return curveImpl->isIdentity(); + } + return false; +} + +//------------------------------------------------------------------------------------------------ +// +void GradingBSplineCurveImpl::computeKnotsAndCoefsForRGBCurve(KnotsCoefs & knotsCoefs, int curveIdx) const { // Skip invalid data and identity. if (m_controlPoints.size() < 2 || isIdentity()) @@ -331,6 +800,7 @@ void GradingBSplineCurveImpl::computeKnotsAndCoefs(KnotsCoefs & knotsCoefs, int knotsCoefs.m_knotsOffsetsArray[curveIdx * 2 + 1] = 0; knotsCoefs.m_coefsOffsetsArray[curveIdx * 2] = -1; knotsCoefs.m_coefsOffsetsArray[curveIdx * 2 + 1] = 0; + return; } else { @@ -348,23 +818,23 @@ void GradingBSplineCurveImpl::computeKnotsAndCoefs(KnotsCoefs & knotsCoefs, int else { // Otherwise, estimate slopes based on the control points. - EstimateSlopes(m_controlPoints, slopes); + EstimateRGBSlopes(m_controlPoints, slopes); } - FitSpline(m_controlPoints, slopes, knots, coefsA, coefsB, coefsC); + FitRGBSpline(m_controlPoints, slopes, knots, coefsA, coefsB, coefsC); - bool adjustment_done = AdjustSlopes(m_controlPoints, slopes, knots); + bool adjustment_done = AdjustRGBSlopes(m_controlPoints, slopes, knots); if (adjustment_done) { knots.clear(); coefsA.clear(); coefsB.clear(); coefsC.clear(); - FitSpline(m_controlPoints, slopes, knots, coefsA, coefsB, coefsC); + FitRGBSpline(m_controlPoints, slopes, knots, coefsA, coefsB, coefsC); } - const int numKnots = static_cast(knotsCoefs.m_knotsArray.size()); + const int numKnots = static_cast(knotsCoefs.m_numKnots); const int newKnots = static_cast(knots.size()); - const int numCoefs = static_cast(knotsCoefs.m_coefsArray.size()); + const int numCoefs = static_cast(knotsCoefs.m_numCoefs); const int newCoefs = static_cast(coefsA.size() * 3); if (numKnots + newKnots > KnotsCoefs::MAX_NUM_KNOTS || @@ -378,142 +848,412 @@ void GradingBSplineCurveImpl::computeKnotsAndCoefs(KnotsCoefs & knotsCoefs, int knotsCoefs.m_coefsOffsetsArray[curveIdx * 2] = numCoefs; knotsCoefs.m_coefsOffsetsArray[curveIdx * 2 + 1] = newCoefs; - knotsCoefs.m_knotsArray.insert(knotsCoefs.m_knotsArray.end(), knots.begin(), knots.end()); - knotsCoefs.m_coefsArray.insert(knotsCoefs.m_coefsArray.end(), coefsA.begin(), coefsA.end()); - knotsCoefs.m_coefsArray.insert(knotsCoefs.m_coefsArray.end(), coefsB.begin(), coefsB.end()); - knotsCoefs.m_coefsArray.insert(knotsCoefs.m_coefsArray.end(), coefsC.begin(), coefsC.end()); - } -} - -void GradingBSplineCurveImpl::AddShaderEval(GpuShaderText & st, - const std::string & knotsOffsets, - const std::string & coefsOffsets, - const std::string & knots, - const std::string & coefs, bool isInv) -{ - if (!isInv) // Forward - { - st.newLine() << "int knotsOffs = " << knotsOffsets << "[curveIdx * 2];"; - st.newLine() << "int knotsCnt = " << knotsOffsets << "[curveIdx * 2 + 1];"; - st.newLine() << "int coefsOffs = " << coefsOffsets << "[curveIdx * 2];"; - st.newLine() << "int coefsCnt = " << coefsOffsets << "[curveIdx * 2 + 1];"; - st.newLine() << "int coefsSets = coefsCnt / 3;"; - // If the curve has the default/identity values the coef data is empty, so return the input. - st.newLine() << "if (coefsSets == 0)"; - st.newLine() << "{"; - st.newLine() << " return x;"; - st.newLine() << "}"; - - st.newLine() << "float knStart = " << knots << "[knotsOffs];"; - st.newLine() << "float knEnd = " << knots << "[knotsOffs + knotsCnt - 1];"; - - st.newLine() << "if (x <= knStart)"; - st.newLine() << "{"; - st.newLine() << " float B = " << coefs << "[coefsOffs + coefsSets];"; - st.newLine() << " float C = " << coefs << "[coefsOffs + coefsSets * 2];"; - st.newLine() << " return (x - knStart) * B + C;"; - st.newLine() << "}"; - - st.newLine() << "else if (x >= knEnd)"; - st.newLine() << "{"; - st.newLine() << " float A = " << coefs << "[coefsOffs + coefsSets - 1];"; - st.newLine() << " float B = " << coefs << "[coefsOffs + coefsSets * 2 - 1];"; - st.newLine() << " float C = " << coefs << "[coefsOffs + coefsSets * 3 - 1];"; - st.newLine() << " float kn = " << knots << "[knotsOffs + knotsCnt - 2];"; - st.newLine() << " float t = knEnd - kn;"; - st.newLine() << " float slope = 2. * A * t + B;"; - st.newLine() << " float offs = ( A * t + B ) * t + C;"; - st.newLine() << " return (x - knEnd) * slope + offs;"; - st.newLine() << "}"; - - // else - st.newLine() << "int i = 0;"; - st.newLine() << "for (i = 0; i < knotsCnt - 2; ++i)"; - st.newLine() << "{"; - st.newLine() << " if (x < " << knots << "[knotsOffs + i + 1])"; - st.newLine() << " {"; - st.newLine() << " break;"; - st.newLine() << " }"; - st.newLine() << "}"; - - st.newLine() << "float A = " << coefs << "[coefsOffs + i];"; - st.newLine() << "float B = " << coefs << "[coefsOffs + coefsSets + i];"; - st.newLine() << "float C = " << coefs << "[coefsOffs + coefsSets * 2 + i];"; - st.newLine() << "float kn = " << knots << "[knotsOffs + i];"; - st.newLine() << "float t = x - kn;"; - st.newLine() << "return ( A * t + B ) * t + C;"; - } - else // Inverse - { - st.newLine() << "int knotsOffs = " << knotsOffsets << "[curveIdx * 2];"; - st.newLine() << "int knotsCnt = " << knotsOffsets << "[curveIdx * 2 + 1];"; - st.newLine() << "int coefsOffs = " << coefsOffsets << "[curveIdx * 2];"; - st.newLine() << "int coefsCnt = " << coefsOffsets << "[curveIdx * 2 + 1];"; - st.newLine() << "int coefsSets = coefsCnt / 3;"; - // If the curve has the default/identity values the coef data is empty, so return the input. - st.newLine() << "if (coefsSets == 0)"; - st.newLine() << "{"; - st.newLine() << " return x;"; - st.newLine() << "}"; - - st.newLine() << "float knStart = " << knots << "[knotsOffs];"; - st.newLine() << "float knEnd = " << knots << "[knotsOffs + knotsCnt - 1];"; - st.newLine() << "float knStartY = " << coefs << "[coefsOffs + coefsSets * 2];"; - st.newLine() << "float knEndY;"; - st.newLine() << "{"; - st.newLine() << " float A = " << coefs << "[coefsOffs + coefsSets - 1];"; - st.newLine() << " float B = " << coefs << "[coefsOffs + coefsSets * 2 - 1];"; - st.newLine() << " float C = " << coefs << "[coefsOffs + coefsSets * 3 - 1];"; - st.newLine() << " float kn = " << knots << "[knotsOffs + knotsCnt - 2];"; - st.newLine() << " float t = knEnd - kn;"; - st.newLine() << " knEndY = ( A * t + B ) * t + C;"; - st.newLine() << "}"; - - st.newLine() << "if (x <= knStartY)"; - st.newLine() << "{"; - st.newLine() << " float B = " << coefs << "[coefsOffs + coefsSets];"; - st.newLine() << " float C = " << coefs << "[coefsOffs + coefsSets * 2];"; - st.newLine() << " return abs(B) < 1e-5 ? knStart : (x - C) / B + knStart;"; - st.newLine() << "}"; - - st.newLine() << "else if (x >= knEndY)"; - st.newLine() << "{"; - st.newLine() << " float A = " << coefs << "[coefsOffs + coefsSets - 1];"; - st.newLine() << " float B = " << coefs << "[coefsOffs + coefsSets * 2 - 1];"; - st.newLine() << " float C = " << coefs << "[coefsOffs + coefsSets * 3 - 1];"; - st.newLine() << " float kn = " << knots << "[knotsOffs + knotsCnt - 2];"; - st.newLine() << " float t = knEnd - kn;"; - st.newLine() << " float slope = 2. * A * t + B;"; - st.newLine() << " float offs = ( A * t + B ) * t + C;"; - st.newLine() << " return abs(slope) < 1e-5 ? knEnd : (x - offs) / slope + knEnd;"; - st.newLine() << "}"; - - // else - st.newLine() << "int i = 0;"; - st.newLine() << "for (i = 0; i < knotsCnt - 2; ++i)"; - st.newLine() << "{"; - st.newLine() << " if (x < " << coefs << "[coefsOffs + coefsSets * 2 + i + 1])"; - st.newLine() << " {"; - st.newLine() << " break;"; - st.newLine() << " }"; - st.newLine() << "}"; - - st.newLine() << "float A = " << coefs << "[coefsOffs + i];"; - st.newLine() << "float B = " << coefs << "[coefsOffs + coefsSets + i];"; - st.newLine() << "float C = " << coefs << "[coefsOffs + coefsSets * 2 + i];"; - st.newLine() << "float kn = " << knots << "[knotsOffs + i];"; - st.newLine() << "float C0 = C - x;"; - st.newLine() << "float discrim = sqrt(B * B - 4. * A * C0);"; - st.newLine() << "return kn + (-2. * C0) / (discrim + B);"; - } -} - -float GradingBSplineCurveImpl::KnotsCoefs::evalCurve(int c, float x) const + const unsigned coefsSize = (unsigned) coefsA.size(); + std::copy(knots.begin(), knots.end(), knotsCoefs.m_knotsArray.begin() + numKnots); + std::copy(coefsA.begin(), coefsA.end(), knotsCoefs.m_coefsArray.begin() + numCoefs); + std::copy(coefsB.begin(), coefsB.end(), knotsCoefs.m_coefsArray.begin() + numCoefs + coefsSize); + std::copy(coefsC.begin(), coefsC.end(), knotsCoefs.m_coefsArray.begin() + numCoefs + coefsSize * 2); + + knotsCoefs.m_numKnots += newKnots; + knotsCoefs.m_numCoefs += newCoefs; + } +} + +//------------------------------------------------------------------------------------------------ +// +void GradingBSplineCurveImpl::computeKnotsAndCoefsForHueCurve(KnotsCoefs & knotsCoefs, + int curveIdx, + bool drawCurveOnly) const { + // Return 0 knots and coefficients when the curve is identity. + if (m_controlPoints.size() < 2 || isIdentity()) + { + if (!drawCurveOnly) + { + // In this case, do not add any knots or coefs. This will allow localBypass + // to be true if all the curves are identities. + + // Identity curve: offset is -1 and count is 0. + knotsCoefs.m_knotsOffsetsArray[curveIdx * 2] = -1; + knotsCoefs.m_knotsOffsetsArray[curveIdx * 2 + 1] = 0; + knotsCoefs.m_coefsOffsetsArray[curveIdx * 2] = -1; + knotsCoefs.m_coefsOffsetsArray[curveIdx * 2 + 1] = 0; + return; + } + else + { + // DrawCurveOnly is set when drawing the splines for a UI. In this mode, the + // spline is always set on the HueSat curve and the HueCurve eval only computes + // that one curve. But the curve/spline type are not known, so the polynomial + // must be set so that it returns the correct values, even if it is an identity. + // Note that the value returned for an identity varies among the spline types. + + // Set knots and coefficients that represent an identity curve. + + const int numKnots = static_cast(knotsCoefs.m_numKnots); + const int numCoefs = static_cast(knotsCoefs.m_numCoefs); + + const int N_IDENTITY_KNOTS = 2; + const int N_IDENTITY_COEFS = 3; + + knotsCoefs.m_knotsOffsetsArray[curveIdx * 2] = numKnots; + knotsCoefs.m_knotsOffsetsArray[curveIdx * 2 + 1] = N_IDENTITY_KNOTS; + knotsCoefs.m_coefsOffsetsArray[curveIdx * 2] = knotsCoefs.m_numCoefs; + knotsCoefs.m_coefsOffsetsArray[curveIdx * 2 + 1] = N_IDENTITY_COEFS; + + knotsCoefs.m_knotsArray.begin()[numKnots] = 0.f; + knotsCoefs.m_knotsArray.begin()[numKnots + 1] = 1.f; + + // Identity curve are linear or constant, so set the quadratic coefficient to zero. + knotsCoefs.m_coefsArray.begin()[numCoefs] = 0.f; + + // Set the linear coefficient to match the slope of the identity curve. + const float linearCoef = m_splineType == BSplineType::DIAGONAL_B_SPLINE || + m_splineType == BSplineType::HUE_HUE_B_SPLINE ? + 1.0f : 0.f; + + knotsCoefs.m_coefsArray.begin()[numCoefs + 1] = linearCoef; + + // Set the constant coefficient for an identity curve. + const float constantCoef = m_splineType == BSplineType::PERIODIC_1_B_SPLINE || + m_splineType == BSplineType::HORIZONTAL1_B_SPLINE ? + 1.0f : 0.f; + + knotsCoefs.m_coefsArray.begin()[numCoefs + 2] = constantCoef; + + knotsCoefs.m_numKnots += N_IDENTITY_KNOTS; + knotsCoefs.m_numCoefs += N_IDENTITY_COEFS; + + return; + } + } + + bool isPeriodic = false; + bool isHorizontal = true; + if (m_splineType == BSplineType::PERIODIC_1_B_SPLINE || + m_splineType == BSplineType::PERIODIC_0_B_SPLINE || + m_splineType == BSplineType::HUE_HUE_B_SPLINE) + { + isPeriodic = true; + } + if (m_splineType == BSplineType::DIAGONAL_B_SPLINE || + m_splineType == BSplineType::HUE_HUE_B_SPLINE) + { + isHorizontal = false; + } + + std::vector resultCtrlPnts; + PrepHueCurveData(m_controlPoints, resultCtrlPnts, isPeriodic, isHorizontal); + + // For the purposes of slope estimation, consider the hue-hue spline to be horizontal. + if (m_splineType == BSplineType::HUE_HUE_B_SPLINE) + { + isHorizontal = true; + } + + std::vector slopes; + if ( !slopesAreDefault() && (m_slopesArray.size() == m_controlPoints.size()) ) + { + // If the user-supplied slopes are non-zero, use those. + slopes = m_slopesArray; + + // We need to ensure equal number of slopes and control points for the spline fit. + if(isPeriodic) + { + float firstSlope = slopes.back(); + slopes.insert(slopes.begin(), firstSlope); + + float lastSlope = slopes.front(); + slopes.push_back(lastSlope); + } + } + else + { + EstimateHueSlopes(resultCtrlPnts, slopes, isPeriodic, isHorizontal); + } + + std::vector knots; + std::vector coefsA; + std::vector coefsB; + std::vector coefsC; + FitHueSpline(resultCtrlPnts, slopes, knots, coefsA, coefsB, coefsC); + + const int numKnots = static_cast(knotsCoefs.m_numKnots); + const int newKnots = static_cast(knots.size()); + const int numCoefs = static_cast(knotsCoefs.m_numCoefs); + const int newCoefs = static_cast(coefsA.size() * 3); + + if (numKnots + newKnots > KnotsCoefs::MAX_NUM_KNOTS || + numCoefs + newCoefs > KnotsCoefs::MAX_NUM_COEFS) + { + throw Exception("Hue curve: maximum number of control points reached."); + } + + knotsCoefs.m_knotsOffsetsArray[curveIdx * 2] = numKnots; + knotsCoefs.m_knotsOffsetsArray[curveIdx * 2 + 1] = newKnots; + knotsCoefs.m_coefsOffsetsArray[curveIdx * 2] = numCoefs; + knotsCoefs.m_coefsOffsetsArray[curveIdx * 2 + 1] = newCoefs; + + const unsigned coefsSize = (unsigned) coefsA.size(); + std::copy(knots.begin(), knots.end(), knotsCoefs.m_knotsArray.begin() + numKnots); + std::copy(coefsA.begin(), coefsA.end(), knotsCoefs.m_coefsArray.begin() + numCoefs); + std::copy(coefsB.begin(), coefsB.end(), knotsCoefs.m_coefsArray.begin() + numCoefs + coefsSize); + std::copy(coefsC.begin(), coefsC.end(), knotsCoefs.m_coefsArray.begin() + numCoefs + coefsSize * 2); + + knotsCoefs.m_numKnots += newKnots; + knotsCoefs.m_numCoefs += newCoefs; +} + +//------------------------------------------------------------------------------------------------ +// +void GradingBSplineCurveImpl::computeKnotsAndCoefs(KnotsCoefs & knotsCoefs, int curveIdx, bool drawCurveOnly) const +{ + if(m_splineType == BSplineType::B_SPLINE) + { + computeKnotsAndCoefsForRGBCurve(knotsCoefs, curveIdx); + } + else + { + computeKnotsAndCoefsForHueCurve(knotsCoefs, curveIdx, drawCurveOnly); + } +} + +//------------------------------------------------------------------------------------------------ +// +void GradingBSplineCurveImpl::AddShaderEvalFwd(GpuShaderText & st, + const std::string & knotsOffsets, + const std::string & coefsOffsets, + const std::string & knots, + const std::string & coefs) +{ + // See GradingHue/RGBCurveOpGPU.cpp:AddCurveEvalMethodTextToShaderProgram. + // The input arguments are: + // curveIdx -- The index of the curve being evaluated. + // x -- The input value. + // identity_x -- The desired output if there is no curve to evaluate. + + st.newLine() << "int knotsOffs = " << knotsOffsets << "[curveIdx * 2];"; + st.newLine() << "int knotsCnt = " << knotsOffsets << "[curveIdx * 2 + 1];"; + st.newLine() << "int coefsOffs = " << coefsOffsets << "[curveIdx * 2];"; + st.newLine() << "int coefsCnt = " << coefsOffsets << "[curveIdx * 2 + 1];"; + st.newLine() << "int coefsSets = coefsCnt / 3;"; + // If the curve has the default/identity values, the coef data is empty, return the identity. + st.newLine() << "if (coefsSets == 0)"; + st.newLine() << "{"; + st.newLine() << " return identity_x;"; + st.newLine() << "}"; + + st.newLine() << "float knStart = " << knots << "[knotsOffs];"; + st.newLine() << "float knEnd = " << knots << "[knotsOffs + knotsCnt - 1];"; + + st.newLine() << "if (x <= knStart)"; + st.newLine() << "{"; + st.newLine() << " float B = " << coefs << "[coefsOffs + coefsSets];"; + st.newLine() << " float C = " << coefs << "[coefsOffs + coefsSets * 2];"; + st.newLine() << " return (x - knStart) * B + C;"; + st.newLine() << "}"; + + st.newLine() << "else if (x >= knEnd)"; + st.newLine() << "{"; + st.newLine() << " float A = " << coefs << "[coefsOffs + coefsSets - 1];"; + st.newLine() << " float B = " << coefs << "[coefsOffs + coefsSets * 2 - 1];"; + st.newLine() << " float C = " << coefs << "[coefsOffs + coefsSets * 3 - 1];"; + st.newLine() << " float kn = " << knots << "[knotsOffs + knotsCnt - 2];"; + st.newLine() << " float t = knEnd - kn;"; + st.newLine() << " float slope = 2. * A * t + B;"; + st.newLine() << " float offs = ( A * t + B ) * t + C;"; + st.newLine() << " return (x - knEnd) * slope + offs;"; + st.newLine() << "}"; + + // else + st.newLine() << "int i = 0;"; + st.newLine() << "for (i = 0; i < knotsCnt - 2; ++i)"; + st.newLine() << "{"; + st.newLine() << " if (x < " << knots << "[knotsOffs + i + 1])"; + st.newLine() << " {"; + st.newLine() << " break;"; + st.newLine() << " }"; + st.newLine() << "}"; + + st.newLine() << "float A = " << coefs << "[coefsOffs + i];"; + st.newLine() << "float B = " << coefs << "[coefsOffs + coefsSets + i];"; + st.newLine() << "float C = " << coefs << "[coefsOffs + coefsSets * 2 + i];"; + st.newLine() << "float kn = " << knots << "[knotsOffs + i];"; + st.newLine() << "float t = x - kn;"; + st.newLine() << "return ( A * t + B ) * t + C;"; +} + +//------------------------------------------------------------------------------------------------ +// +void GradingBSplineCurveImpl::AddShaderEvalRev(GpuShaderText & st, + const std::string & knotsOffsets, + const std::string & coefsOffsets, + const std::string & knots, + const std::string & coefs) +{ + // See GradingHue/RGBCurveOpGPU.cpp:AddCurveEvalMethodTextToShaderProgram. + // The input arguments are: + // curveIdx -- The index of the curve being evaluated. + // x -- The input value. + + st.newLine() << "int knotsOffs = " << knotsOffsets << "[curveIdx * 2];"; + st.newLine() << "int knotsCnt = " << knotsOffsets << "[curveIdx * 2 + 1];"; + st.newLine() << "int coefsOffs = " << coefsOffsets << "[curveIdx * 2];"; + st.newLine() << "int coefsCnt = " << coefsOffsets << "[curveIdx * 2 + 1];"; + st.newLine() << "int coefsSets = coefsCnt / 3;"; + + st.newLine() << "if (coefsSets == 0)"; + st.newLine() << "{"; + st.newLine() << " return x;"; + st.newLine() << "}"; + + st.newLine() << "float knStart = " << knots << "[knotsOffs];"; + st.newLine() << "float knEnd = " << knots << "[knotsOffs + knotsCnt - 1];"; + st.newLine() << "float knStartY = " << coefs << "[coefsOffs + coefsSets * 2];"; + st.newLine() << "float knEndY;"; + st.newLine() << "{"; + st.newLine() << " float A = " << coefs << "[coefsOffs + coefsSets - 1];"; + st.newLine() << " float B = " << coefs << "[coefsOffs + coefsSets * 2 - 1];"; + st.newLine() << " float C = " << coefs << "[coefsOffs + coefsSets * 3 - 1];"; + st.newLine() << " float kn = " << knots << "[knotsOffs + knotsCnt - 2];"; + st.newLine() << " float t = knEnd - kn;"; + st.newLine() << " knEndY = ( A * t + B ) * t + C;"; + st.newLine() << "}"; + + st.newLine() << "if (x <= knStartY)"; + st.newLine() << "{"; + st.newLine() << " float B = " << coefs << "[coefsOffs + coefsSets];"; + st.newLine() << " float C = " << coefs << "[coefsOffs + coefsSets * 2];"; + st.newLine() << " return abs(B) < 1e-5 ? knStart : (x - C) / B + knStart;"; + st.newLine() << "}"; + + st.newLine() << "else if (x >= knEndY)"; + st.newLine() << "{"; + st.newLine() << " float A = " << coefs << "[coefsOffs + coefsSets - 1];"; + st.newLine() << " float B = " << coefs << "[coefsOffs + coefsSets * 2 - 1];"; + st.newLine() << " float C = " << coefs << "[coefsOffs + coefsSets * 3 - 1];"; + st.newLine() << " float kn = " << knots << "[knotsOffs + knotsCnt - 2];"; + st.newLine() << " float t = knEnd - kn;"; + st.newLine() << " float slope = 2. * A * t + B;"; + st.newLine() << " float offs = ( A * t + B ) * t + C;"; + st.newLine() << " return abs(slope) < 1e-5 ? knEnd : (x - offs) / slope + knEnd;"; + st.newLine() << "}"; + + // else + st.newLine() << "int i = 0;"; + st.newLine() << "for (i = 0; i < knotsCnt - 2; ++i)"; + st.newLine() << "{"; + st.newLine() << " if (x < " << coefs << "[coefsOffs + coefsSets * 2 + i + 1])"; + st.newLine() << " {"; + st.newLine() << " break;"; + st.newLine() << " }"; + st.newLine() << "}"; + + st.newLine() << "float A = " << coefs << "[coefsOffs + i];"; + st.newLine() << "float B = " << coefs << "[coefsOffs + coefsSets + i];"; + st.newLine() << "float C = " << coefs << "[coefsOffs + coefsSets * 2 + i];"; + st.newLine() << "float kn = " << knots << "[knotsOffs + i];"; + st.newLine() << "float C0 = C - x;"; + st.newLine() << "float discrim = sqrt(B * B - 4. * A * C0);"; + st.newLine() << "return kn + (-2. * C0) / (discrim + B);"; +} + +//------------------------------------------------------------------------------------------------ +// +void GradingBSplineCurveImpl::AddShaderEvalRevHue(GpuShaderText & st, + const std::string & knotsOffsets, + const std::string & coefsOffsets, + const std::string & knots, + const std::string & coefs) +{ + // See GradingHueCurveOpGPU.cpp:AddCurveEvalMethodTextToShaderProgram. + // The input arguments are: + // curveIdx -- The index of the curve being evaluated. + // x -- The input value. + + st.newLine() << "int knotsOffs = " << knotsOffsets << "[curveIdx * 2];"; + st.newLine() << "int knotsCnt = " << knotsOffsets << "[curveIdx * 2 + 1];"; + st.newLine() << "int coefsOffs = " << coefsOffsets << "[curveIdx * 2];"; + st.newLine() << "int coefsCnt = " << coefsOffsets << "[curveIdx * 2 + 1];"; + st.newLine() << "int coefsSets = coefsCnt / 3;"; + + st.newLine() << "if (coefsSets == 0)"; + st.newLine() << "{"; + st.newLine() << " return x;"; + st.newLine() << "}"; + + st.newLine() << "float knStart = " << knots << "[knotsOffs];"; + st.newLine() << "float knEnd = " << knots << "[knotsOffs + knotsCnt - 1];"; + st.newLine() << "float knStartY = " << coefs << "[coefsOffs + coefsSets * 2];"; + st.newLine() << "float knEndY;"; + st.newLine() << "{"; + st.newLine() << " float A = " << coefs << "[coefsOffs + coefsSets - 1];"; + st.newLine() << " float B = " << coefs << "[coefsOffs + coefsSets * 2 - 1];"; + st.newLine() << " float C = " << coefs << "[coefsOffs + coefsSets * 3 - 1];"; + st.newLine() << " float kn = " << knots << "[knotsOffs + knotsCnt - 2];"; + st.newLine() << " float t = knEnd - kn;"; + st.newLine() << " knEndY = ( A * t + B ) * t + C;"; + // The HUE-FX curve is index 7 and requires special handling. + st.newLine() << " knEndY = (curveIdx == 7) ? knEndY + knEnd : knEndY;"; + st.newLine() << "}"; + + st.newLine() << "if (x < knStartY)"; + st.newLine() << "{"; + st.newLine() << " x = x + ceil(knStartY - x);"; + st.newLine() << "}"; + + st.newLine() << "else if (x > knEndY)"; + st.newLine() << "{"; + st.newLine() << " x = x - ceil(x - knEndY);"; + st.newLine() << "}"; + + st.newLine() << "int i = 0;"; + st.newLine() << "for (i = 0; i < knotsCnt - 2; ++i)"; + st.newLine() << "{"; + st.newLine() << " float curve_x = " << coefs << "[coefsOffs + coefsSets * 2 + i + 1];"; + st.newLine() << " curve_x = (curveIdx == 7) ? curve_x + " << knots << "[knotsOffs + i + 1] : curve_x;"; + st.newLine() << " if (x < curve_x)"; + st.newLine() << " {"; + st.newLine() << " break;"; + st.newLine() << " }"; + st.newLine() << "}"; + + st.newLine() << "float A = " << coefs << "[coefsOffs + i];"; + st.newLine() << "float B = " << coefs << "[coefsOffs + coefsSets + i];"; + st.newLine() << "float C = " << coefs << "[coefsOffs + coefsSets * 2 + i];"; + st.newLine() << "float kn = " << knots << "[knotsOffs + i];"; + st.newLine() << "if (curveIdx == 7)"; + st.newLine() << "{"; + st.newLine() << " C = C + kn;"; + st.newLine() << " B = B + 1.;"; + st.newLine() << "}"; + st.newLine() << "float C0 = C - x;"; + st.newLine() << "float discrim = sqrt(B * B - 4. * A * C0);"; + st.newLine() << "return kn + (-2. * C0) / (discrim + B);"; +} + +//------------------------------------------------------------------------------------------------ +// +GradingBSplineCurveImpl::KnotsCoefs::KnotsCoefs(size_t numCurves) +{ + m_knotsOffsetsArray.resize(2 * numCurves); + m_coefsOffsetsArray.resize(2 * numCurves); + + m_coefsArray.resize(DynamicPropertyGradingRGBCurveImpl::GetMaxCoefs()); + m_knotsArray.resize(DynamicPropertyGradingRGBCurveImpl::GetMaxKnots()); +} + +//------------------------------------------------------------------------------------------------ +// +float GradingBSplineCurveImpl::KnotsCoefs::evalCurve(int c, float x, float identity_x) const +{ + // NB: When evaluating hue curves, x should be wrapped to [0,1) by the caller + // so there is no extrapolation. + const int coefsSets = m_coefsOffsetsArray[2 * c + 1] / 3; if (coefsSets == 0) { - return x; + return identity_x; } const int coefsOffs = m_coefsOffsetsArray[2 * c]; const int knotsCnt = m_knotsOffsetsArray[2 * c + 1]; @@ -556,8 +1296,15 @@ float GradingBSplineCurveImpl::KnotsCoefs::evalCurve(int c, float x) const } } +//------------------------------------------------------------------------------------------------ +// float GradingBSplineCurveImpl::KnotsCoefs::evalCurveRev(int c, float y) const { + // Note: This is only intended to invert the monotonic curve types. + // The horizontal curve types only need to be evaluated in the forward + // direction, even when inverting the hue curve transform. The exception + // is the HueFX curve but that has its own inversion function below. + const int coefsSets = m_coefsOffsetsArray[2 * c + 1] / 3; if (coefsSets == 0) { @@ -582,12 +1329,14 @@ float GradingBSplineCurveImpl::KnotsCoefs::evalCurveRev(int c, float y) const if (y <= knStartY) { + // Extrapolate low side. const float B = m_coefsArray[coefsOffs + coefsSets]; const float C = m_coefsArray[coefsOffs + coefsSets * 2]; return std::fabs(B) < 1e-5f ? knStart : (y - C) / B + knStart; } else if (y >= knEndY) { + // Extrapolate high side. const float A = m_coefsArray[coefsOffs + coefsSets - 1]; const float B = m_coefsArray[coefsOffs + coefsSets * 2 - 1]; const float C = m_coefsArray[coefsOffs + coefsSets * 3 - 1]; @@ -615,6 +1364,78 @@ float GradingBSplineCurveImpl::KnotsCoefs::evalCurveRev(int c, float y) const } } +//------------------------------------------------------------------------------------------------ +// +float GradingBSplineCurveImpl::KnotsCoefs::evalCurveRevHue(int c, float y) const +{ + // This function is specifically to invert the HueFX and Hue-Hue curve types. + // + // The output of the HueFX curve is a "delta hue" signal that is + // added on to the incoming hue: HueOut = HueIn + HueFX(HueIn). + // The input to this function should be the HueOut, in other words, + // the sum of the HueIn and delta hue. It returns HueIn. + + const int coefsSets = m_coefsOffsetsArray[2 * c + 1] / 3; + if (coefsSets == 0) + { + return y; + } + + const int coefsOffs = m_coefsOffsetsArray[2 * c]; + const int knotsCnt = m_knotsOffsetsArray[2 * c + 1]; + const int knotsOffs = m_knotsOffsetsArray[2 * c]; + + const float knStart = m_knotsArray[knotsOffs]; + const float knEnd = m_knotsArray[knotsOffs + knotsCnt - 1]; + float knStartY = m_coefsArray[coefsOffs + coefsSets * 2]; + const bool isHfx = c == 7; + knStartY = isHfx ? knStartY + knStart : knStartY; + float knEndY; + { + const float A = m_coefsArray[coefsOffs + coefsSets - 1]; + const float B = m_coefsArray[coefsOffs + coefsSets * 2 - 1]; + const float C = m_coefsArray[coefsOffs + coefsSets * 3 - 1]; + const float kn = m_knotsArray[knotsOffs + knotsCnt - 2]; + const float t = knEnd - kn; + knEndY = (A * t + B) * t + C; + knEndY = isHfx ? knEndY + knEnd : knEndY; + } + + if (y < knStartY) + { + // Wrap up into the valid hue range. + y += std::ceil(knStartY - y); + } + else if (y > knEndY) + { + // Wrap down into the valid hue range. + y -= std::ceil(y - knEndY); + } + + int i; + for (i = 0; i < knotsCnt - 2; ++i) + { + float curve_y = m_coefsArray[coefsOffs + coefsSets * 2 + i + 1]; + curve_y = isHfx ? curve_y + m_knotsArray[knotsOffs + i + 1] : curve_y; + + if (y < curve_y) + break; + } + const float A = m_coefsArray[coefsOffs + i]; + float B = m_coefsArray[coefsOffs + coefsSets + i]; + float C = m_coefsArray[coefsOffs + coefsSets * 2 + i]; + const float kn = m_knotsArray[knotsOffs + i]; + if (isHfx) + { + C += kn; // shift curve up so left edge is on the main diagonal + B += 1.f; // add diagonal line + } + const float C0 = C - y; + const float discrim = sqrt(B * B - 4.f * A * C0); + return kn + (-2.f * C0) / (discrim + B); + // Note: The caller should wrap to ensure the output is a hue on [0,1). +} + bool operator==(const GradingControlPoint & lhs, const GradingControlPoint & rhs) { return lhs.m_x == rhs.m_x && lhs.m_y == rhs.m_y; @@ -627,12 +1448,18 @@ bool operator!=(const GradingControlPoint & lhs, const GradingControlPoint & rhs bool operator==(const GradingBSplineCurve & lhs, const GradingBSplineCurve & rhs) { + if (lhs.getSplineType() != rhs.getSplineType()) + { + return false; + } + const auto num = lhs.getNumControlPoints(); if (num == rhs.getNumControlPoints()) { for (size_t i = 0; i < num; ++i) { - if (lhs.getControlPoint(i) != rhs.getControlPoint(i)) + if ( lhs.getControlPoint(i) != rhs.getControlPoint(i) || + lhs.getSlope(i) != rhs.getSlope(i) ) { return false; } diff --git a/src/OpenColorIO/ops/gradingrgbcurve/GradingBSplineCurve.h b/src/OpenColorIO/ops/gradingrgbcurve/GradingBSplineCurve.h index 9944564051..f990f67886 100644 --- a/src/OpenColorIO/ops/gradingrgbcurve/GradingBSplineCurve.h +++ b/src/OpenColorIO/ops/gradingrgbcurve/GradingBSplineCurve.h @@ -18,10 +18,14 @@ class GradingBSplineCurveImpl : public GradingBSplineCurve { public: explicit GradingBSplineCurveImpl(size_t size); + explicit GradingBSplineCurveImpl(size_t size, BSplineType splineType); GradingBSplineCurveImpl(const std::vector & controlPoints); + GradingBSplineCurveImpl(const std::vector & controlPoints, BSplineType splineType); ~GradingBSplineCurveImpl() = default; GradingBSplineCurveRcPtr createEditableCopy() const override; + BSplineType getSplineType() const override; + void setSplineType(BSplineType splineType) override; size_t getNumControlPoints() const noexcept override; void setNumControlPoints(size_t size) override; const GradingControlPoint & getControlPoint(size_t index) const override; @@ -57,11 +61,7 @@ class GradingBSplineCurveImpl : public GradingBSplineCurve { KnotsCoefs() = delete; - explicit KnotsCoefs(size_t numCurves) - { - m_knotsOffsetsArray.resize(2 * numCurves); - m_coefsOffsetsArray.resize(2 * numCurves); - } + explicit KnotsCoefs(size_t numCurves); // Pre-processing scalar values. @@ -88,40 +88,63 @@ class GradingBSplineCurveImpl : public GradingBSplineCurve // control points but the number of knots may be, at most, the number of control // points * 2 - 1. // - // There are 4 RGB curves (R, G, B, M) each represented by one RGBCurve. We want to keep - // the total for two curves well below the 200 knot, 600 coef limit. - // (TODO: 6 Hue curves (H/H, H/S, H/L, L/S, L/L, S/S)). + // There are four spline curves (R, G, B, M) in an RGBCurve and eight in a HueCurve. + // We want to keep the total for two instances well below the 200 knot, 600 coef limit. // // A value of 60 knots would allow about 30 control points spread across the 4 or 6 curves. // Note that the default RGB curves use 3 control points each and the hue curves may use as // many as 6 even for the default. However, there is an optimization below that does not // add knots for curves that are simply identity. // + // UPDATE: Hardware has improved since this was introduced. For OCIO 2.5, double the + // allowed knots/coefs from 60/180 to 120/360. + // // Maximum size of the knots array (for ALL curves). - static constexpr int MAX_NUM_KNOTS = 60; + static constexpr int MAX_NUM_KNOTS = 120; // Maximum size of the coefs array (for ALL curves). - static constexpr int MAX_NUM_COEFS = 180; + static constexpr int MAX_NUM_COEFS = 360; // Pre-processing arrays of length MAX_NUM_KNOTS and MAX_NUM_COEFS. std::vector m_coefsArray; // Contains packed coefs of ALL curves. std::vector m_knotsArray; // Contains packed knots of ALL curves. - float evalCurve(int curveIdx, float x) const; - float evalCurveRev(int curveIdx, float x) const; + int m_numCoefs = 0; + int m_numKnots = 0; + + // Forward evaluation of any spline type. + float evalCurve(int curveIdx, float x, float identity_x) const; + // Reverse evaluation of B_SPLINE or DIAGONAL_B_SPLINE. + float evalCurveRev(int curveIdx, float y) const; + // Reverse evaluation of HUE_HUE_B_SPLINE or HUE_FX curves using PERIODIC_0_B_SPLINE. + float evalCurveRevHue(int c, float y) const; }; // Compute knots and coefs for a curve and add result to knotsCoefs. It has to be called for // each curve using a given curve order. - void computeKnotsAndCoefs(KnotsCoefs & knotsCoefs, int curveIdx) const; - - static void AddShaderEval(GpuShaderText & st, - const std::string & knotsOffsets, const std::string & coefsOffsets, - const std::string & knots, const std::string & coefs, bool isInv); + void computeKnotsAndCoefs(KnotsCoefs & knotsCoefs, int curveIdx, bool drawCurveOnly) const; + + // Forward evaluation of any spline type. + static void AddShaderEvalFwd(GpuShaderText & st, + const std::string & knotsOffsets, const std::string & coefsOffsets, + const std::string & knots, const std::string & coefs); + // Reverse evaluation of B_SPLINE or DIAGONAL_B_SPLINE. + static void AddShaderEvalRev(GpuShaderText & st, + const std::string & knotsOffsets, const std::string & coefsOffsets, + const std::string & knots, const std::string & coefs); + // Reverse evaluation of HUE_HUE_B_SPLINE or HUE_FX curves using PERIODIC_0_B_SPLINE. + static void AddShaderEvalRevHue(GpuShaderText & st, + const std::string & knotsOffsets, const std::string & coefsOffsets, + const std::string & knots, const std::string & coefs); private: + void computeKnotsAndCoefsForRGBCurve(KnotsCoefs & knotsCoefs, int curveIdx) const; + void computeKnotsAndCoefsForHueCurve(KnotsCoefs & knotsCoefs, int curveIdx, bool drawCurveOnly) const; + void validateIndex(size_t index) const; std::vector m_controlPoints; std::vector m_slopesArray; // Optional slope values for the control points. + + BSplineType m_splineType = BSplineType::B_SPLINE; }; bool IsGradingCurveIdentity(const ConstGradingBSplineCurveRcPtr & curve); diff --git a/src/OpenColorIO/ops/gradingrgbcurve/GradingRGBCurve.cpp b/src/OpenColorIO/ops/gradingrgbcurve/GradingRGBCurve.cpp index 196988a8e7..d223c5315b 100644 --- a/src/OpenColorIO/ops/gradingrgbcurve/GradingRGBCurve.cpp +++ b/src/OpenColorIO/ops/gradingrgbcurve/GradingRGBCurve.cpp @@ -104,6 +104,15 @@ void GradingRGBCurveImpl::validate() const << "with: " << e.what(); throw Exception(oss.str().c_str()); } + + if (m_curves[c]->getSplineType() != B_SPLINE) + { + // TODO: Allow use of the hue curve diagonal spline types? + std::ostringstream oss; + oss << "GradingRGBCurve validation failed: '" << CurveType(c) << "' curve " + << "is of the wrong BSplineType."; + throw Exception(oss.str().c_str()); + } } } diff --git a/src/OpenColorIO/ops/gradingrgbcurve/GradingRGBCurveOpCPU.cpp b/src/OpenColorIO/ops/gradingrgbcurve/GradingRGBCurveOpCPU.cpp index bf8c73a4b9..914a9881a0 100644 --- a/src/OpenColorIO/ops/gradingrgbcurve/GradingRGBCurveOpCPU.cpp +++ b/src/OpenColorIO/ops/gradingrgbcurve/GradingRGBCurveOpCPU.cpp @@ -33,13 +33,13 @@ class GradingRGBCurveOpCPU : public OpCPU void eval(const GradingBSplineCurveImpl::KnotsCoefs & knotsCoefs, float * out, const float * in) const { - out[0] = knotsCoefs.evalCurve(static_cast(RGB_RED), in[0]); - out[1] = knotsCoefs.evalCurve(static_cast(RGB_GREEN), in[1]); - out[2] = knotsCoefs.evalCurve(static_cast(RGB_BLUE), in[2]); + out[0] = knotsCoefs.evalCurve(static_cast(RGB_RED), in[0], in[0]); + out[1] = knotsCoefs.evalCurve(static_cast(RGB_GREEN), in[1], in[1]); + out[2] = knotsCoefs.evalCurve(static_cast(RGB_BLUE), in[2], in[2]); // TODO: Add vectorized version for master curve. - out[0] = knotsCoefs.evalCurve(static_cast(RGB_MASTER), out[0]); - out[1] = knotsCoefs.evalCurve(static_cast(RGB_MASTER), out[1]); - out[2] = knotsCoefs.evalCurve(static_cast(RGB_MASTER), out[2]); + out[0] = knotsCoefs.evalCurve(static_cast(RGB_MASTER), out[0], out[0]); + out[1] = knotsCoefs.evalCurve(static_cast(RGB_MASTER), out[1], out[1]); + out[2] = knotsCoefs.evalCurve(static_cast(RGB_MASTER), out[2], out[2]); } void evalRev(const GradingBSplineCurveImpl::KnotsCoefs & knotsCoefs, float * out, const float * in) const diff --git a/src/OpenColorIO/ops/gradingrgbcurve/GradingRGBCurveOpGPU.cpp b/src/OpenColorIO/ops/gradingrgbcurve/GradingRGBCurveOpGPU.cpp index 6093026420..e2ae5d7b86 100644 --- a/src/OpenColorIO/ops/gradingrgbcurve/GradingRGBCurveOpGPU.cpp +++ b/src/OpenColorIO/ops/gradingrgbcurve/GradingRGBCurveOpGPU.cpp @@ -100,13 +100,16 @@ void AddUniform(GpuShaderCreatorRcPtr & shaderCreator, const GpuShaderCreator::VectorIntGetter & getVector, const std::string & name) { + // 4 curves x 2 values (count and offset). + static constexpr unsigned arrayLen = RGBCurveType::RGB_NUM_CURVES * 2; + // Add the uniform if it does not already exist. - if (shaderCreator->addUniform(name.c_str(), getSize, getVector, 8)) + if (shaderCreator->addUniform(name.c_str(), getSize, getVector, arrayLen)) { // Declare uniform. GpuShaderText stDecl(shaderCreator->getLanguage()); // Need 2 ints for each RGBM curve. - stDecl.declareUniformArrayInt(name, 8); + stDecl.declareUniformArrayInt(name, arrayLen); shaderCreator->addToParameterDeclareShaderCode(stDecl.string().c_str()); } } @@ -217,31 +220,37 @@ void AddCurveEvalMethodTextToShaderProgram(GpuShaderCreatorRcPtr & shaderCreator if (!dyn) { auto propGC = gcData->getDynamicPropertyInternal(); + const int numOffsets = propGC->GetNumOffsetValues(); // 2 ints for each curve. st.newLine() << ""; - st.declareIntArrayConst(props.m_knotsOffsets, 4 * 2, propGC->getKnotsOffsetsArray()); + st.declareIntArrayConst(props.m_knotsOffsets, numOffsets, propGC->getKnotsOffsetsArray()); st.declareFloatArrayConst(props.m_knots, propGC->getNumKnots(), propGC->getKnotsArray()); - st.declareIntArrayConst(props.m_coefsOffsets, 4 * 2, propGC->getCoefsOffsetsArray()); + st.declareIntArrayConst(props.m_coefsOffsets, numOffsets, propGC->getCoefsOffsetsArray()); st.declareFloatArrayConst(props.m_coefs, propGC->getNumCoefs(), propGC->getCoefsArray()); } st.newLine() << ""; if (shaderCreator->getLanguage() == LANGUAGE_OSL_1 || shaderCreator->getLanguage() == GPU_LANGUAGE_MSL_2_0) { - st.newLine() << st.floatKeyword() << " " << props.m_eval << "(int curveIdx, float x)"; + st.newLine() << st.floatKeyword() << " " << props.m_eval << "(int curveIdx, float x, float identity_x)"; } else { - st.newLine() << st.floatKeyword() << " " << props.m_eval << "(in int curveIdx, in float x)"; + st.newLine() << st.floatKeyword() << " " << props.m_eval << "(in int curveIdx, in float x, in float identity_x)"; } st.newLine() << "{"; st.indent(); - - const bool isInv = gcData->getDirection() == TRANSFORM_DIR_INVERSE; - GradingBSplineCurveImpl::AddShaderEval(st, props.m_knotsOffsets, props.m_coefsOffsets, - props.m_knots, props.m_coefs, isInv); - + if (gcData->getDirection() == TRANSFORM_DIR_INVERSE) + { + GradingBSplineCurveImpl::AddShaderEvalRev(st, props.m_knotsOffsets, props.m_coefsOffsets, + props.m_knots, props.m_coefs); + } + else + { + GradingBSplineCurveImpl::AddShaderEvalFwd(st, props.m_knotsOffsets, props.m_coefsOffsets, + props.m_knots, props.m_coefs); + } st.dedent(); st.newLine() << "}"; @@ -275,13 +284,13 @@ void AddGCForwardShader(GpuShaderCreatorRcPtr & shaderCreator, const std::string pix(shaderCreator->getPixelName()); // Call the curve evaluation method for each curve. - st.newLine() << pix << ".rgb.r = " << props.m_eval << "(0, " << pix << ".rgb.r);"; // RED - st.newLine() << pix << ".rgb.g = " << props.m_eval << "(1, " << pix << ".rgb.g);"; // GREEN - st.newLine() << pix << ".rgb.b = " << props.m_eval << "(2, " << pix << ".rgb.b);"; // BLUE + st.newLine() << pix << ".rgb.r = " << props.m_eval << "(0, " << pix << ".rgb.r, " << pix << ".rgb.r);"; // RED + st.newLine() << pix << ".rgb.g = " << props.m_eval << "(1, " << pix << ".rgb.g, " << pix << ".rgb.g);"; // GREEN + st.newLine() << pix << ".rgb.b = " << props.m_eval << "(2, " << pix << ".rgb.b, " << pix << ".rgb.b);"; // BLUE // TODO: vectorize master. - st.newLine() << pix << ".rgb.r = " << props.m_eval << "(3, " << pix << ".rgb.r);"; // MASTER - st.newLine() << pix << ".rgb.g = " << props.m_eval << "(3, " << pix << ".rgb.g);"; // MASTER - st.newLine() << pix << ".rgb.b = " << props.m_eval << "(3, " << pix << ".rgb.b);"; // MASTER + st.newLine() << pix << ".rgb.r = " << props.m_eval << "(3, " << pix << ".rgb.r, " << pix << ".rgb.r);"; // MASTER + st.newLine() << pix << ".rgb.g = " << props.m_eval << "(3, " << pix << ".rgb.g, " << pix << ".rgb.g);"; // MASTER + st.newLine() << pix << ".rgb.b = " << props.m_eval << "(3, " << pix << ".rgb.b, " << pix << ".rgb.b);"; // MASTER if (doLinToLog) { @@ -323,12 +332,12 @@ void AddGCInverseShader(GpuShaderCreatorRcPtr & shaderCreator, const std::string pix(shaderCreator->getPixelName()); // Call the curve evaluation method for each curve. - st.newLine() << pix << ".rgb.r = " << props.m_eval << "(3, " << pix << ".rgb.r);"; // MASTER - st.newLine() << pix << ".rgb.g = " << props.m_eval << "(3, " << pix << ".rgb.g);"; // MASTER - st.newLine() << pix << ".rgb.b = " << props.m_eval << "(3, " << pix << ".rgb.b);"; // MASTER - st.newLine() << pix << ".rgb.r = " << props.m_eval << "(0, " << pix << ".rgb.r);"; // RED - st.newLine() << pix << ".rgb.g = " << props.m_eval << "(1, " << pix << ".rgb.g);"; // GREEN - st.newLine() << pix << ".rgb.b = " << props.m_eval << "(2, " << pix << ".rgb.b);"; // BLUE + st.newLine() << pix << ".rgb.r = " << props.m_eval << "(3, " << pix << ".rgb.r, " << pix << ".rgb.r);"; // MASTER + st.newLine() << pix << ".rgb.g = " << props.m_eval << "(3, " << pix << ".rgb.g, " << pix << ".rgb.g);"; // MASTER + st.newLine() << pix << ".rgb.b = " << props.m_eval << "(3, " << pix << ".rgb.b, " << pix << ".rgb.b);"; // MASTER + st.newLine() << pix << ".rgb.r = " << props.m_eval << "(0, " << pix << ".rgb.r, " << pix << ".rgb.r);"; // RED + st.newLine() << pix << ".rgb.g = " << props.m_eval << "(1, " << pix << ".rgb.g, " << pix << ".rgb.g);"; // GREEN + st.newLine() << pix << ".rgb.b = " << props.m_eval << "(2, " << pix << ".rgb.b, " << pix << ".rgb.b);"; // BLUE if (doLinToLog) { diff --git a/src/OpenColorIO/transforms/CDLTransform.cpp b/src/OpenColorIO/transforms/CDLTransform.cpp index cee7ed963d..393bec4dbe 100755 --- a/src/OpenColorIO/transforms/CDLTransform.cpp +++ b/src/OpenColorIO/transforms/CDLTransform.cpp @@ -373,13 +373,25 @@ std::ostream & operator<< (std::ostream & os, const CDLTransform & t) os << ""; return os; diff --git a/src/OpenColorIO/transforms/ExponentTransform.cpp b/src/OpenColorIO/transforms/ExponentTransform.cpp index 719c9b44dc..1ceb576751 100755 --- a/src/OpenColorIO/transforms/ExponentTransform.cpp +++ b/src/OpenColorIO/transforms/ExponentTransform.cpp @@ -103,13 +103,13 @@ std::ostream & operator<< (std::ostream & os, const ExponentTransform & t) os << ""; return os; } diff --git a/src/OpenColorIO/transforms/ExponentWithLinearTransform.cpp b/src/OpenColorIO/transforms/ExponentWithLinearTransform.cpp index 53c4ca480b..5e93f0aec6 100644 --- a/src/OpenColorIO/transforms/ExponentWithLinearTransform.cpp +++ b/src/OpenColorIO/transforms/ExponentWithLinearTransform.cpp @@ -145,22 +145,22 @@ std::ostream & operator<< (std::ostream & os, const ExponentWithLinearTransform double gamma[4]; t.getGamma(gamma); - os << "gamma=" << gamma[0]; + os << "gamma=[" << gamma[0]; for (int i = 1; i < 4; ++i) { - os << " " << gamma[i]; + os << ", " << gamma[i]; } double offset[4]; t.getOffset(offset); - os << ", offset=" << offset[0]; + os << "], offset=[" << offset[0]; for (int i = 1; i < 4; ++i) { - os << " " << offset[i]; + os << ", " << offset[i]; } - os << ", style=" << NegativeStyleToString(t.getNegativeStyle()); + os << "], style=" << NegativeStyleToString(t.getNegativeStyle()); os << ">"; return os; } diff --git a/src/OpenColorIO/transforms/FixedFunctionTransform.cpp b/src/OpenColorIO/transforms/FixedFunctionTransform.cpp index 1449e09dba..07baef12a9 100644 --- a/src/OpenColorIO/transforms/FixedFunctionTransform.cpp +++ b/src/OpenColorIO/transforms/FixedFunctionTransform.cpp @@ -148,11 +148,12 @@ std::ostream & operator<< (std::ostream & os, const FixedFunctionTransform & t) FixedFunctionOpData::Params params(numParams, 0.); t.getParams(¶ms[0]); - os << ", params=" << params[0]; + os << ", params=[" << params[0]; for (size_t i = 1; i < numParams; ++i) { - os << " " << params[i]; + os << ", " << params[i]; } + os << "]"; } os << ">"; diff --git a/src/OpenColorIO/transforms/GradingHueCurveTransform.cpp b/src/OpenColorIO/transforms/GradingHueCurveTransform.cpp new file mode 100644 index 0000000000..da959e7b48 --- /dev/null +++ b/src/OpenColorIO/transforms/GradingHueCurveTransform.cpp @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + +#include + +#include + +#include "transforms/GradingHueCurveTransform.h" + +namespace OCIO_NAMESPACE +{ + +GradingHueCurveTransformRcPtr GradingHueCurveTransform::Create(GradingStyle style) +{ + return GradingHueCurveTransformRcPtr(new GradingHueCurveTransformImpl(style), + &GradingHueCurveTransformImpl::deleter); +} + +GradingHueCurveTransformImpl::GradingHueCurveTransformImpl(GradingStyle style) : + m_data(style) +{ +} + +void GradingHueCurveTransformImpl::deleter(GradingHueCurveTransform* t) +{ + delete static_cast(t); +} + +TransformRcPtr GradingHueCurveTransformImpl::createEditableCopy() const +{ + GradingHueCurveTransformRcPtr transform = GradingHueCurveTransform::Create(getStyle()); + dynamic_cast(transform.get())->data() = data(); + return transform; +} + +TransformDirection GradingHueCurveTransformImpl::getDirection() const noexcept +{ + return data().getDirection(); +} + +void GradingHueCurveTransformImpl::setDirection(TransformDirection dir) noexcept +{ + data().setDirection(dir); +} + +void GradingHueCurveTransformImpl::validate() const +{ + try + { + Transform::validate(); + data().validate(); + } + catch(Exception & ex) + { + std::string errMsg("GradingHueCurveTransform validation failed: "); + errMsg += ex.what(); + throw Exception(errMsg.c_str()); + } + + return; +} + +FormatMetadata & GradingHueCurveTransformImpl::getFormatMetadata() noexcept +{ + return data().getFormatMetadata(); +} + +const FormatMetadata & GradingHueCurveTransformImpl::getFormatMetadata() const noexcept +{ + return data().getFormatMetadata(); +} + +bool GradingHueCurveTransformImpl::equals(const GradingHueCurveTransform & other) const noexcept +{ + if (this == &other) return true; + return data() == dynamic_cast(&other)->data(); +} + +GradingStyle GradingHueCurveTransformImpl::getStyle() const noexcept +{ + return data().getStyle(); +} + +void GradingHueCurveTransformImpl::setStyle(GradingStyle style) noexcept +{ + data().setStyle(style); +} + +const ConstGradingHueCurveRcPtr GradingHueCurveTransformImpl::getValue() const +{ + return data().getValue(); +} + +void GradingHueCurveTransformImpl::setValue(const ConstGradingHueCurveRcPtr & values) +{ + data().setValue(values); +} + +float GradingHueCurveTransformImpl::getSlope(HueCurveType c, size_t index) const +{ + return data().getSlope(c, index); +} + +void GradingHueCurveTransformImpl::setSlope(HueCurveType c, size_t index, float slope) +{ + data().setSlope(c, index, slope); +} + +bool GradingHueCurveTransformImpl::slopesAreDefault(HueCurveType c) const +{ + return data().slopesAreDefault(c); +} + +HSYTransformStyle GradingHueCurveTransformImpl::getRGBToHSY() const noexcept +{ + return data().getRGBToHSY(); +} + +void GradingHueCurveTransformImpl::setRGBToHSY(HSYTransformStyle style) noexcept +{ + data().setRGBToHSY(style); +} + +bool GradingHueCurveTransformImpl::isDynamic() const noexcept +{ + return data().isDynamic(); +} + +void GradingHueCurveTransformImpl::makeDynamic() noexcept +{ + data().getDynamicPropertyInternal()->makeDynamic(); +} + +void GradingHueCurveTransformImpl::makeNonDynamic() noexcept +{ + data().getDynamicPropertyInternal()->makeNonDynamic(); +} + +std::ostream& operator<< (std::ostream & os, const GradingHueCurveTransform & t) noexcept +{ + os << ""; + return os; +} + +std::ostream & operator<<(std::ostream & os, const GradingHueCurve & hueCurve) +{ + os << ""; + + return os; +} + +} // namespace OCIO_NAMESPACE diff --git a/src/OpenColorIO/transforms/GradingHueCurveTransform.h b/src/OpenColorIO/transforms/GradingHueCurveTransform.h new file mode 100644 index 0000000000..6881232d50 --- /dev/null +++ b/src/OpenColorIO/transforms/GradingHueCurveTransform.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + + +#ifndef INCLUDED_OCIO_HUECURVETRANSFORM_H +#define INCLUDED_OCIO_HUECURVETRANSFORM_H + +#include + +#include "ops/gradinghuecurve/GradingHueCurveOpData.h" + + +namespace OCIO_NAMESPACE +{ + +class GradingHueCurveTransformImpl : public GradingHueCurveTransform +{ +public: + GradingHueCurveTransformImpl(GradingStyle style); + GradingHueCurveTransformImpl() = delete; + GradingHueCurveTransformImpl(const GradingHueCurveTransformImpl &) = delete; + GradingHueCurveTransformImpl& operator=(const GradingHueCurveTransformImpl &) = delete; + ~GradingHueCurveTransformImpl() override = default; + + TransformRcPtr createEditableCopy() const override; + + TransformDirection getDirection() const noexcept override; + void setDirection(TransformDirection dir) noexcept override; + + FormatMetadata & getFormatMetadata() noexcept override; + const FormatMetadata & getFormatMetadata() const noexcept override; + + void validate() const override; + + bool equals(const GradingHueCurveTransform & other) const noexcept override; + + GradingStyle getStyle() const noexcept override; + + void setStyle(GradingStyle style) noexcept override; + + const ConstGradingHueCurveRcPtr getValue() const override; + + void setValue(const ConstGradingHueCurveRcPtr & values) override; + + float getSlope(HueCurveType c, size_t index) const override; + void setSlope(HueCurveType c, size_t index, float slope) override; + bool slopesAreDefault(HueCurveType c) const override; + + HSYTransformStyle getRGBToHSY() const noexcept override; + void setRGBToHSY(HSYTransformStyle style) noexcept override; + + bool isDynamic() const noexcept override; + void makeDynamic() noexcept override; + void makeNonDynamic() noexcept override; + + GradingHueCurveOpData & data() noexcept { return m_data; } + const GradingHueCurveOpData & data() const noexcept { return m_data; } + + static void deleter(GradingHueCurveTransform* t); + +private: + GradingHueCurveOpData m_data; +}; + + +} // namespace OCIO_NAMESPACE + +#endif // INCLUDED_OCIO_HUECURVETRANSFORM_H diff --git a/src/OpenColorIO/transforms/GradingRGBCurveTransform.cpp b/src/OpenColorIO/transforms/GradingRGBCurveTransform.cpp index 77ace5ad4f..e9772e667e 100644 --- a/src/OpenColorIO/transforms/GradingRGBCurveTransform.cpp +++ b/src/OpenColorIO/transforms/GradingRGBCurveTransform.cpp @@ -140,6 +140,10 @@ std::ostream& operator<< (std::ostream & os, const GradingRGBCurveTransform & t) os << "direction=" << TransformDirectionToString(t.getDirection()); os << ", style=" << GradingStyleToString(t.getStyle()); os << ", values=" << *t.getValue(); + if (t.getBypassLinToLog()) + { + os << ", bypass_lintolog"; + } if (t.isDynamic()) { os << ", dynamic"; @@ -160,7 +164,15 @@ std::ostream & operator<<(std::ostream & os, const GradingBSplineCurve & bspline const auto numPoints = bspline.getNumControlPoints(); for (size_t i = 0; i < numPoints; ++i) { - os << bspline.getControlPoint(i); + if (bspline.slopesAreDefault()) + { + os << bspline.getControlPoint(i); + } + else + { + const GradingControlPoint cp = bspline.getControlPoint(i); + os << ""; + } } os << "]>"; return os; diff --git a/src/OpenColorIO/transforms/LogAffineTransform.cpp b/src/OpenColorIO/transforms/LogAffineTransform.cpp index d54de42d85..6711a75fea 100644 --- a/src/OpenColorIO/transforms/LogAffineTransform.cpp +++ b/src/OpenColorIO/transforms/LogAffineTransform.cpp @@ -126,13 +126,13 @@ std::ostream & operator<< (std::ostream & os, const LogAffineTransform & t) os << ", base=" << t.getBase(); double values[3]; t.getLogSideSlopeValue(values); - os << ", logSideSlope=" << values[0] << " " << values[1] << " " << values[2]; + os << ", logSideSlope=[" << values[0] << ", " << values[1] << ", " << values[2] << "]"; t.getLogSideOffsetValue(values); - os << ", logSideOffset=" << values[0] << " " << values[1] << " " << values[2]; + os << ", logSideOffset=[" << values[0] << ", " << values[1] << ", " << values[2] << "]"; t.getLinSideSlopeValue(values); - os << ", linSideSlope=" << values[0] << " " << values[1] << " " << values[2]; + os << ", linSideSlope=[" << values[0] << ", " << values[1] << ", " << values[2] << "]"; t.getLinSideOffsetValue(values); - os << ", linSideOffset=" << values[0] << " " << values[1] << " " << values[2]; + os << ", linSideOffset=[" << values[0] << ", " << values[1] << ", " << values[2] << "]"; os << ">"; return os; diff --git a/src/OpenColorIO/transforms/LogCameraTransform.cpp b/src/OpenColorIO/transforms/LogCameraTransform.cpp index ee7cb7276a..1f4188417a 100644 --- a/src/OpenColorIO/transforms/LogCameraTransform.cpp +++ b/src/OpenColorIO/transforms/LogCameraTransform.cpp @@ -158,18 +158,18 @@ std::ostream & operator<< (std::ostream & os, const LogCameraTransform & t) os << ", base=" << t.getBase(); double values[3]; t.getLogSideSlopeValue(values); - os << ", logSideSlope=" << values[0] << " " << values[1] << " " << values[2]; + os << ", logSideSlope=[" << values[0] << ", " << values[1] << ", " << values[2] << "]"; t.getLogSideOffsetValue(values); - os << ", logSideOffset=" << values[0] << " " << values[1] << " " << values[2]; + os << ", logSideOffset=[" << values[0] << ", " << values[1] << ", " << values[2] << "]"; t.getLinSideSlopeValue(values); - os << ", linSideSlope=" << values[0] << " " << values[1] << " " << values[2]; + os << ", linSideSlope=[" << values[0] << ", " << values[1] << ", " << values[2] << "]"; t.getLinSideOffsetValue(values); - os << ", linSideOffset=" << values[0] << " " << values[1] << " " << values[2]; + os << ", linSideOffset=[" << values[0] << ", " << values[1] << ", " << values[2] << "]"; t.getLinSideBreakValue(values); - os << ", linSideBreak=" << values[0] << " " << values[1] << " " << values[2]; + os << ", linSideBreak=[" << values[0] << ", " << values[1] << ", " << values[2] << "]"; if (t.getLinearSlopeValue(values)) { - os << ", linearSlope=" << values[0] << " " << values[1] << " " << values[2]; + os << ", linearSlope=[" << values[0] << ", " << values[1] << ", " << values[2] << "]"; } os << ">"; diff --git a/src/OpenColorIO/transforms/Lut1DTransform.cpp b/src/OpenColorIO/transforms/Lut1DTransform.cpp index 2e3f93a5fa..cc3a9f7f61 100644 --- a/src/OpenColorIO/transforms/Lut1DTransform.cpp +++ b/src/OpenColorIO/transforms/Lut1DTransform.cpp @@ -214,9 +214,9 @@ std::ostream & operator<< (std::ostream & os, const Lut1DTransform & t) bMax = std::max(bMax, b); } os << "minrgb=["; - os << rMin << " " << gMin << " " << bMin << "], "; + os << rMin << ", " << gMin << ", " << bMin << "], "; os << "maxrgb=["; - os << rMax << " " << gMax << " " << bMax << "]"; + os << rMax << ", " << gMax << ", " << bMax << "]"; } os << ">"; diff --git a/src/OpenColorIO/transforms/Lut3DTransform.cpp b/src/OpenColorIO/transforms/Lut3DTransform.cpp index 3d6630d93a..6e1a2babeb 100644 --- a/src/OpenColorIO/transforms/Lut3DTransform.cpp +++ b/src/OpenColorIO/transforms/Lut3DTransform.cpp @@ -208,9 +208,9 @@ std::ostream & operator<< (std::ostream & os, const Lut3DTransform & t) } } os << "minrgb=["; - os << rMin << " " << gMin << " " << bMin << "], "; + os << rMin << ", " << gMin << ", " << bMin << "], "; os << "maxrgb=["; - os << rMax << " " << gMax << " " << bMax << "]"; + os << rMax << ", " << gMax << ", " << bMax << "]"; } os << ">"; diff --git a/src/OpenColorIO/transforms/MatrixTransform.cpp b/src/OpenColorIO/transforms/MatrixTransform.cpp index d9991bd4e3..ff73b918a0 100755 --- a/src/OpenColorIO/transforms/MatrixTransform.cpp +++ b/src/OpenColorIO/transforms/MatrixTransform.cpp @@ -351,17 +351,17 @@ std::ostream& operator<< (std::ostream& os, const MatrixTransform& t) noexcept os << "direction=" << TransformDirectionToString(t.getDirection()); os << ", fileindepth=" << BitDepthToString(t.getFileInputBitDepth()); os << ", fileoutdepth=" << BitDepthToString(t.getFileOutputBitDepth()); - os << ", matrix=" << matrix[0]; + os << ", matrix=[" << matrix[0]; for (int i = 1; i < 16; ++i) { - os << " " << matrix[i]; + os << ", " << matrix[i]; } - os << ", offset=" << offset[0]; + os << "], offset=[" << offset[0]; for (int i = 1; i < 4; ++i) { - os << " " << offset[i]; + os << ", " << offset[i]; } - os << ">"; + os << "]>"; return os; } diff --git a/src/bindings/python/CMakeLists.txt b/src/bindings/python/CMakeLists.txt index 94af56302e..978b6253d1 100644 --- a/src/bindings/python/CMakeLists.txt +++ b/src/bindings/python/CMakeLists.txt @@ -68,6 +68,7 @@ set(SOURCES transforms/PyFixedFunctionTransform.cpp transforms/PyGradingPrimaryTransform.cpp transforms/PyGradingRGBCurveTransform.cpp + transforms/PyGradingHueCurveTransform.cpp transforms/PyGradingToneTransform.cpp transforms/PyGroupTransform.cpp transforms/PyLogAffineTransform.cpp diff --git a/src/bindings/python/PyDynamicProperty.cpp b/src/bindings/python/PyDynamicProperty.cpp index c163161eac..626a5883de 100644 --- a/src/bindings/python/PyDynamicProperty.cpp +++ b/src/bindings/python/PyDynamicProperty.cpp @@ -27,6 +27,10 @@ void bindPyDynamicProperty(py::module & m) DOC(DynamicPropertyValue, AsGradingRGBCurve)) .def("setGradingRGBCurve", &PyDynamicProperty::setGradingRGBCurve, "val"_a, DOC(DynamicPropertyValue, AsGradingRGBCurve)) + .def("getGradingHueCurve", &PyDynamicProperty::getGradingHueCurve, + DOC(DynamicPropertyValue, AsGradingHueCurve)) + .def("setGradingHueCurve", &PyDynamicProperty::setGradingHueCurve, "val"_a, + DOC(DynamicPropertyValue, AsGradingHueCurve)) .def("getGradingTone", &PyDynamicProperty::getGradingTone, DOC(DynamicPropertyValue, AsGradingTone)) .def("setGradingTone", &PyDynamicProperty::setGradingTone, "val"_a, diff --git a/src/bindings/python/PyDynamicProperty.h b/src/bindings/python/PyDynamicProperty.h index 3180ed6a54..2115cfaf4d 100644 --- a/src/bindings/python/PyDynamicProperty.h +++ b/src/bindings/python/PyDynamicProperty.h @@ -84,6 +84,26 @@ struct OCIOHIDDEN PyDynamicProperty throw OCIO::Exception("Invalid dynamic property type (doesn't accept a GradingRGBCurve)."); } + const ConstGradingHueCurveRcPtr & getGradingHueCurve() + { + auto propGC = DynamicPropertyValue::AsGradingHueCurve(m_prop); + if (propGC) + { + return propGC->getValue(); + } + throw OCIO::Exception("Invalid dynamic property type (doesn't hold a GradingHueCurve)."); + } + + void setGradingHueCurve(const ConstGradingHueCurveRcPtr & v) + { + auto propGC = DynamicPropertyValue::AsGradingHueCurve(m_prop); + if (propGC) + { + return propGC->setValue(v); + } + throw OCIO::Exception("Invalid dynamic property type (doesn't accept a GradingHueCurve)."); + } + const GradingTone & getGradingTone() { auto propGT = DynamicPropertyValue::AsGradingTone(m_prop); diff --git a/src/bindings/python/PyGradingData.cpp b/src/bindings/python/PyGradingData.cpp index 815da41140..a1a093d7b3 100644 --- a/src/bindings/python/PyGradingData.cpp +++ b/src/bindings/python/PyGradingData.cpp @@ -68,6 +68,10 @@ void bindPyGradingData(py::module & m) py::class_( m.attr("GradingRGBCurve")); + auto clsGradingHueCurve = + py::class_( + m.attr("GradingHueCurve")); + clsGradingRGBM .def(py::init<>(), DOC(GradingRGBM, GradingRGBM)) @@ -247,6 +251,22 @@ void bindPyGradingData(py::module & m) }), "size"_a, DOC(GradingBSplineCurve, Create)) + .def(py::init([](size_t size, + HueCurveType curveType) + { + return GradingBSplineCurve::Create(size, curveType); + }), + "size"_a, + "huecurvetype"_a, + DOC(GradingBSplineCurve, Create)) + .def(py::init([](size_t size, + BSplineType splineType) + { + return GradingBSplineCurve::Create(size, splineType); + }), + "size"_a, + "splinetype"_a, + DOC(GradingBSplineCurve, Create)) .def(py::init([](const std::vector & values) { const auto size = values.size(); @@ -269,6 +289,29 @@ void bindPyGradingData(py::module & m) return c; }), DOC(GradingBSplineCurve, Create, 2)) + .def(py::init([](const std::vector & values, + HueCurveType curveType) + { + const auto size = values.size(); + if (size < 4) + { + throw Exception("GradingBSpline needs at least 4 values."); + } + if (size % 2 != 0) + { + throw Exception("GradingBSpline needs an even number of values."); + } + auto numCtrlPts = size / 2; + + GradingBSplineCurveRcPtr c = GradingBSplineCurve::Create(numCtrlPts, curveType); + for (size_t p = 0; p < numCtrlPts; ++p) + { + c->getControlPoint(p).m_x = values[2 * p]; + c->getControlPoint(p).m_y = values[2 * p + 1]; + } + return c; + }), + DOC(GradingBSplineCurve, Create, 2)) .def("__eq__", [](const GradingBSplineCurve &self, const GradingBSplineCurve &other) { @@ -286,6 +329,36 @@ void bindPyGradingData(py::module & m) .def("getControlPoints", [](GradingBSplineCurveRcPtr & self) { return GradingControlPointIterator(self); + }) + .def("getSplineType", &GradingBSplineCurve::getSplineType, + DOC(GradingBSplineCurve, getSplineType)) + .def("setSplineType", &GradingBSplineCurve::setSplineType, "splinetype"_a, + DOC(GradingBSplineCurve, setSplineType)) + .def("slopesAreDefault", &GradingBSplineCurve::slopesAreDefault, + DOC(GradingBSplineCurve, slopesAreDefault)) + .def("getSlopes", [](GradingBSplineCurveRcPtr self) + { + std::vector slopes; + const size_t numPts = self->getNumControlPoints(); + slopes.resize(numPts); + for (size_t pt = 0; pt < numPts; ++pt) + { + slopes[pt] = self->getSlope(pt); + } + return slopes; + }) + .def("setSlopes", [](GradingBSplineCurveRcPtr self, const std::vector & slopes) + { + const size_t numPts = self->getNumControlPoints(); + const auto size = slopes.size(); + if (size != numPts) + { + throw Exception("Length of slopes vector must match number of control points."); + } + for (size_t pt = 0; pt < numPts; ++pt) + { + self->setSlope(pt, slopes[pt]); + } }); defRepr(clsGradingBSplineCurve); @@ -345,6 +418,11 @@ void bindPyGradingData(py::module & m) return self != other; }, py::is_operator()) + .def("validate", &GradingRGBCurve::validate, + DOC(GradingRGBCurve, validate)) + .def("isIdentity", &GradingRGBCurve::isIdentity, + DOC(GradingRGBCurve, isIdentity)) + .def_property("red", [](const GradingRGBCurveRcPtr & rgbCurve) { @@ -386,7 +464,129 @@ void bindPyGradingData(py::module & m) CopyGradingBSpline(rgbCurve->getCurve(RGB_MASTER), master); }); - defRepr(clsGradingRGBCurve); + defRepr(clsGradingHueCurve); + + clsGradingHueCurve + .def(py::init([](GradingStyle style) + { + return GradingHueCurve::Create(style); + }), + "style"_a, + DOC(GradingHueCurve, GradingHueCurve)) + .def(py::init([](const GradingBSplineCurveRcPtr & hue_hue, + const GradingBSplineCurveRcPtr & hue_sat, + const GradingBSplineCurveRcPtr & hue_lum, + const GradingBSplineCurveRcPtr & lum_sat, + const GradingBSplineCurveRcPtr & sat_sat, + const GradingBSplineCurveRcPtr & lum_lum, + const GradingBSplineCurveRcPtr & sat_lum, + const GradingBSplineCurveRcPtr & hue_fx) + { + return GradingHueCurve::Create(hue_hue, hue_sat, hue_lum, lum_sat, + sat_sat, lum_lum, sat_lum, hue_fx); + }), + DOC(GradingHueCurve, GradingHueCurve, 2)) + + .def("__eq__", [](const GradingHueCurve &self, const GradingHueCurve &other) + { + return self == other; + }, py::is_operator()) + .def("__ne__", [](const GradingHueCurve &self, const GradingHueCurve &other) + { + return self != other; + }, py::is_operator()) + + .def("validate", &GradingHueCurve::validate, + DOC(GradingHueCurve, validate)) + .def("isIdentity", &GradingHueCurve::isIdentity, + DOC(GradingHueCurve, isIdentity)) + .def("getDrawCurveOnly", &GradingHueCurve::getDrawCurveOnly, + DOC(GradingHueCurve, getDrawCurveOnly)) + .def("setDrawCurveOnly", &GradingHueCurve::setDrawCurveOnly, "drawcurveonly"_a, + DOC(GradingHueCurve, setDrawCurveOnly)) + + .def_property("hue_hue", + [](const GradingHueCurveRcPtr & hueCurve) + { + return hueCurve->getCurve(HUE_HUE); + }, + [](const GradingHueCurveRcPtr & hueCurve, + const GradingBSplineCurveRcPtr & hue_hue) + { + CopyGradingBSpline(hueCurve->getCurve(HUE_HUE), hue_hue); + }) + .def_property("hue_sat", + [](const GradingHueCurveRcPtr & hueCurve) + { + return hueCurve->getCurve(HUE_SAT); + }, + [](const GradingHueCurveRcPtr & hueCurve, + const GradingBSplineCurveRcPtr & hue_sat) + { + CopyGradingBSpline(hueCurve->getCurve(HUE_SAT), hue_sat); + }) + .def_property("hue_lum", + [](const GradingHueCurveRcPtr & hueCurve) + { + return hueCurve->getCurve(HUE_LUM); + }, + [](const GradingHueCurveRcPtr & hueCurve, + const GradingBSplineCurveRcPtr & hue_lum) + { + CopyGradingBSpline(hueCurve->getCurve(HUE_LUM), hue_lum); + }) + .def_property("lum_sat", + [](const GradingHueCurveRcPtr & hueCurve) + { + return hueCurve->getCurve(LUM_SAT); + }, + [](const GradingHueCurveRcPtr & hueCurve, + const GradingBSplineCurveRcPtr & lum_sat) + { + CopyGradingBSpline(hueCurve->getCurve(LUM_SAT), lum_sat); + }) + .def_property("sat_sat", + [](const GradingHueCurveRcPtr & hueCurve) + { + return hueCurve->getCurve(SAT_SAT); + }, + [](const GradingHueCurveRcPtr & hueCurve, + const GradingBSplineCurveRcPtr & sat_sat) + { + CopyGradingBSpline(hueCurve->getCurve(SAT_SAT), sat_sat); + }) + .def_property("lum_lum", + [](const GradingHueCurveRcPtr & hueCurve) + { + return hueCurve->getCurve(LUM_LUM); + }, + [](const GradingHueCurveRcPtr & hueCurve, + const GradingBSplineCurveRcPtr & lum_lum) + { + CopyGradingBSpline(hueCurve->getCurve(LUM_LUM), lum_lum); + }) + .def_property("sat_lum", + [](const GradingHueCurveRcPtr & hueCurve) + { + return hueCurve->getCurve(SAT_LUM); + }, + [](const GradingHueCurveRcPtr & hueCurve, + const GradingBSplineCurveRcPtr & sat_lum) + { + CopyGradingBSpline(hueCurve->getCurve(SAT_LUM), sat_lum); + }) + .def_property("hue_fx", + [](const GradingHueCurveRcPtr & hueCurve) + { + return hueCurve->getCurve(HUE_FX); + }, + [](const GradingHueCurveRcPtr & hueCurve, + const GradingBSplineCurveRcPtr & hue_fx) + { + CopyGradingBSpline(hueCurve->getCurve(HUE_FX), hue_fx); + }); + + defRepr(clsGradingHueCurve); } } // namespace OCIO_NAMESPACE diff --git a/src/bindings/python/PyOpenColorIO.h b/src/bindings/python/PyOpenColorIO.h index a803c540e3..298b05ff0f 100644 --- a/src/bindings/python/PyOpenColorIO.h +++ b/src/bindings/python/PyOpenColorIO.h @@ -115,7 +115,7 @@ struct polymorphic_type_hook { { type = &typeid(OCIO::AllocationTransform); } - if(dynamic_cast(src)) + else if(dynamic_cast(src)) { type = &typeid(OCIO::BuiltinTransform); } @@ -159,7 +159,11 @@ struct polymorphic_type_hook { { type = &typeid(OCIO::GradingRGBCurveTransform); } - if(dynamic_cast(src)) + else if (dynamic_cast(src)) + { + type = &typeid(OCIO::GradingHueCurveTransform); + } + else if(dynamic_cast(src)) { type = &typeid(OCIO::GradingToneTransform); } diff --git a/src/bindings/python/PyTransform.cpp b/src/bindings/python/PyTransform.cpp index 997bf984ec..40ea4fb4b4 100644 --- a/src/bindings/python/PyTransform.cpp +++ b/src/bindings/python/PyTransform.cpp @@ -43,6 +43,7 @@ void bindPyTransform(py::module & m) bindPyFixedFunctionTransform(m); bindPyGradingPrimaryTransform(m); bindPyGradingRGBCurveTransform(m); + bindPyGradingHueCurveTransform(m); bindPyGradingToneTransform(m); bindPyGroupTransform(m); bindPyLogAffineTransform(m); diff --git a/src/bindings/python/PyTransform.h b/src/bindings/python/PyTransform.h index 817c3a6b49..89fa49d20d 100644 --- a/src/bindings/python/PyTransform.h +++ b/src/bindings/python/PyTransform.h @@ -25,6 +25,7 @@ void bindPyFileTransform(py::module & m); void bindPyFixedFunctionTransform(py::module & m); void bindPyGradingPrimaryTransform(py::module & m); void bindPyGradingRGBCurveTransform(py::module & m); +void bindPyGradingHueCurveTransform(py::module & m); void bindPyGradingToneTransform(py::module & m); void bindPyGroupTransform(py::module & m); void bindPyLogAffineTransform(py::module & m); diff --git a/src/bindings/python/PyTypes.cpp b/src/bindings/python/PyTypes.cpp index 80110f0122..01eba8ef4e 100644 --- a/src/bindings/python/PyTypes.cpp +++ b/src/bindings/python/PyTypes.cpp @@ -139,6 +139,10 @@ void bindPyTypes(py::module & m) m, "GradingRGBCurve", DOC(GradingRGBCurve)); + py::class_( + m, "GradingHueCurve", + DOC(GradingHueCurve)); + py::class_( m, "Transform", DOC(Transform)); @@ -207,6 +211,12 @@ void bindPyTypes(py::module & m) m, "GradingRGBCurveTransform", DOC(GradingRGBCurveTransform)); + py::class_( + m, "GradingHueCurveTransform", + DOC(GradingHueCurveTransform)); + py::class_( @@ -401,6 +411,8 @@ void bindPyTypes(py::module & m) DOC(PyOpenColorIO, TransformType, TRANSFORM_TYPE_GRADING_PRIMARY)) .value("TRANSFORM_TYPE_GRADING_RGB_CURVE", TRANSFORM_TYPE_GRADING_RGB_CURVE, DOC(PyOpenColorIO, TransformType, TRANSFORM_TYPE_GRADING_RGB_CURVE)) + .value("TRANSFORM_TYPE_GRADING_HUE_CURVE", TRANSFORM_TYPE_GRADING_HUE_CURVE, + DOC(PyOpenColorIO, TransformType, TRANSFORM_TYPE_GRADING_HUE_CURVE)) .value("TRANSFORM_TYPE_GRADING_TONE", TRANSFORM_TYPE_GRADING_TONE, DOC(PyOpenColorIO, TransformType, TRANSFORM_TYPE_GRADING_TONE)) .value("TRANSFORM_TYPE_GROUP", TRANSFORM_TYPE_GROUP, @@ -599,6 +611,12 @@ void bindPyTypes(py::module & m) DOC(PyOpenColorIO, FixedFunctionStyle, FIXED_FUNCTION_ACES_TONESCALE_COMPRESS_20)) .value("FIXED_FUNCTION_ACES_GAMUT_COMPRESS_20", FIXED_FUNCTION_ACES_GAMUT_COMPRESS_20, DOC(PyOpenColorIO, FixedFunctionStyle, FIXED_FUNCTION_ACES_GAMUT_COMPRESS_20)) + .value("FIXED_FUNCTION_RGB_TO_HSY_LIN", FIXED_FUNCTION_RGB_TO_HSY_LIN, + DOC(PyOpenColorIO, FixedFunctionStyle, FIXED_FUNCTION_RGB_TO_HSY_LIN)) + .value("FIXED_FUNCTION_RGB_TO_HSY_LOG", FIXED_FUNCTION_RGB_TO_HSY_LOG, + DOC(PyOpenColorIO, FixedFunctionStyle, FIXED_FUNCTION_RGB_TO_HSY_LOG)) + .value("FIXED_FUNCTION_RGB_TO_HSY_VID", FIXED_FUNCTION_RGB_TO_HSY_VID, + DOC(PyOpenColorIO, FixedFunctionStyle, FIXED_FUNCTION_RGB_TO_HSY_VID)) .export_values(); py::enum_( @@ -665,6 +683,8 @@ void bindPyTypes(py::module & m) DOC(PyOpenColorIO, DynamicPropertyType, DYNAMIC_PROPERTY_GRADING_PRIMARY)) .value("DYNAMIC_PROPERTY_GRADING_RGBCURVE", DYNAMIC_PROPERTY_GRADING_RGBCURVE, DOC(PyOpenColorIO, DynamicPropertyType, DYNAMIC_PROPERTY_GRADING_RGBCURVE)) + .value("DYNAMIC_PROPERTY_GRADING_HUECURVE", DYNAMIC_PROPERTY_GRADING_HUECURVE, + DOC(PyOpenColorIO, DynamicPropertyType, DYNAMIC_PROPERTY_GRADING_HUECURVE)) .value("DYNAMIC_PROPERTY_GRADING_TONE", DYNAMIC_PROPERTY_GRADING_TONE, DOC(PyOpenColorIO, DynamicPropertyType, DYNAMIC_PROPERTY_GRADING_TONE)) .export_values(); @@ -685,6 +705,56 @@ void bindPyTypes(py::module & m) DOC(PyOpenColorIO, RGBCurveType, RGB_NUM_CURVES)) .export_values(); + py::enum_( + m, "HueCurveType", + DOC(PyOpenColorIO, HueCurveType)) + + .value("HUE_HUE", HUE_HUE, + DOC(PyOpenColorIO, HueCurveType, HUE_HUE)) + .value("HUE_SAT", HUE_SAT, + DOC(PyOpenColorIO, HueCurveType, HUE_SAT)) + .value("HUE_LUM", HUE_LUM, + DOC(PyOpenColorIO, HueCurveType, HUE_LUM)) + .value("LUM_SAT", LUM_SAT, + DOC(PyOpenColorIO, HueCurveType, LUM_SAT)) + .value("SAT_SAT", SAT_SAT, + DOC(PyOpenColorIO, HueCurveType, SAT_SAT)) + .value("LUM_LUM", LUM_LUM, + DOC(PyOpenColorIO, HueCurveType, LUM_LUM)) + .value("SAT_LUM", SAT_LUM, + DOC(PyOpenColorIO, HueCurveType, SAT_LUM)) + .value("HUE_FX", HUE_FX, + DOC(PyOpenColorIO, HueCurveType, HUE_FX)) + .export_values(); + + py::enum_( + m, "HSYTransformStyle", + DOC(PyOpenColorIO, HSYTransformStyle)) + + .value("HSY_TRANSFORM_NONE", HSY_TRANSFORM_NONE, + DOC(PyOpenColorIO, HSYTransformStyle, HSY_TRANSFORM_NONE)) + .value("HSY_TRANSFORM_1", HSY_TRANSFORM_1, + DOC(PyOpenColorIO, HSYTransformStyle, HSY_TRANSFORM_1)) + .export_values(); + + py::enum_( + m, "BSplineType", + DOC(PyOpenColorIO, BSplineType)) + + .value("B_SPLINE", B_SPLINE, + DOC(PyOpenColorIO, BSplineType, B_SPLINE)) + .value("DIAGONAL_B_SPLINE", DIAGONAL_B_SPLINE, + DOC(PyOpenColorIO, BSplineType, DIAGONAL_B_SPLINE)) + .value("HUE_HUE_B_SPLINE", HUE_HUE_B_SPLINE, + DOC(PyOpenColorIO, BSplineType, HUE_HUE_B_SPLINE)) + .value("PERIODIC_1_B_SPLINE", PERIODIC_1_B_SPLINE, + DOC(PyOpenColorIO, BSplineType, PERIODIC_1_B_SPLINE)) + .value("PERIODIC_0_B_SPLINE", PERIODIC_0_B_SPLINE, + DOC(PyOpenColorIO, BSplineType, PERIODIC_0_B_SPLINE)) + .value("HORIZONTAL1_B_SPLINE", HORIZONTAL1_B_SPLINE, + DOC(PyOpenColorIO, BSplineType, HORIZONTAL1_B_SPLINE)) + .export_values(); + py::enum_( m, "UniformDataType", DOC(PyOpenColorIO, UniformDataType)) diff --git a/src/bindings/python/transforms/PyGradingHueCurveTransform.cpp b/src/bindings/python/transforms/PyGradingHueCurveTransform.cpp new file mode 100644 index 0000000000..1f6c992d9a --- /dev/null +++ b/src/bindings/python/transforms/PyGradingHueCurveTransform.cpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + +#include "PyTransform.h" + +namespace OCIO_NAMESPACE +{ + +void bindPyGradingHueCurveTransform(py::module & m) +{ + GradingHueCurveTransformRcPtr DEFAULT = GradingHueCurveTransform::Create(GRADING_LOG); + + auto clsGradingHueCurveTransform = + py::class_( + m.attr("GradingHueCurveTransform")) + + .def(py::init([](const ConstGradingHueCurveRcPtr & values, + GradingStyle style, + bool dynamic, + TransformDirection dir) + { + GradingHueCurveTransformRcPtr p = GradingHueCurveTransform::Create(style); + p->setStyle(style); + p->setValue(values); + if (dynamic) { p->makeDynamic(); } + p->setDirection(dir); + p->validate(); + return p; + }), + "values"_a, + "style"_a = DEFAULT->getStyle(), + "dynamic"_a = DEFAULT->isDynamic(), + "dir"_a = DEFAULT->getDirection(), + DOC(GradingHueCurveTransform, Create)) + .def(py::init([](GradingStyle style, bool dynamic, TransformDirection dir) + { + GradingHueCurveTransformRcPtr p = GradingHueCurveTransform::Create(style); + p->setStyle(style); + if (dynamic) + { + p->makeDynamic(); + } + p->setDirection(dir); + p->validate(); + return p; + }), + "style"_a = DEFAULT->getStyle(), + "dynamic"_a = DEFAULT->isDynamic(), + "dir"_a = DEFAULT->getDirection(), + DOC(GradingHueCurveTransform, Create)) + + .def("getFormatMetadata", + (FormatMetadata & (GradingHueCurveTransform::*)()) + &GradingHueCurveTransform::getFormatMetadata, + py::return_value_policy::reference_internal, + DOC(GradingHueCurveTransform, getFormatMetadata)) + .def("getStyle", &GradingHueCurveTransform::getStyle, + DOC(GradingHueCurveTransform, getStyle)) + .def("setStyle", &GradingHueCurveTransform::setStyle, "style"_a, + DOC(GradingHueCurveTransform, setStyle)) + .def("getValue", &GradingHueCurveTransform::getValue, + DOC(GradingHueCurveTransform, getValue)) + .def("setValue", &GradingHueCurveTransform::setValue, "values"_a, + DOC(GradingHueCurveTransform, setValue)) + .def("getSlope", &GradingHueCurveTransform::getSlope, "curve"_a, "index"_a, + DOC(GradingHueCurveTransform, getSlope)) + .def("setSlope", &GradingHueCurveTransform::setSlope, "curve"_a, "index"_a, "slope"_a, + DOC(GradingHueCurveTransform, setSlope)) + .def("slopesAreDefault", &GradingHueCurveTransform::slopesAreDefault, "curve"_a, + DOC(GradingHueCurveTransform, slopesAreDefault)) +// TODO: The above follows what is in GradingRGBCurveTransform, but could add a more pythonic version? +// .def("getSlopes", [](GradingHueCurveTransformRcPtr self, HueCurveType curveType) +// { +// std::vector slopes; +// GradingHueCurveRcPtr curve = self->getValue(); +// const size_t numPts = self->getNumControlPoints(); +// slopes.resize(numPts); +// for (size_t pt = 0; pt < numPts; ++pt) +// { +// slopes[pt] = self->getSlope(curveType, pt); +// } +// return slopes; +// }, +// DOC(GradingHueCurveTransform, getSlopes)) +// .def("setSlopes", [](GradingHueCurveTransformRcPtr self, +// HueCurveType curveType, +// const std::vector & slopes) +// { +// const size_t numPts = self->getNumControlPoints(); +// const auto size = slopes.size(); +// if (size != numPts) +// { +// throw Exception("Length of slopes vector must match number of control points."); +// } +// for (size_t pt = 0; pt < numPts; ++pt) +// { +// self->setSlope(curveType, pt, slopes[pt]); +// } +// }, +// DOC(GradingHueCurveTransform, getSlopes)); + .def("getRGBToHSY", &GradingHueCurveTransform::getRGBToHSY, + DOC(GradingHueCurveTransform, getRGBToHSY)) + .def("setRGBToHSY", &GradingHueCurveTransform::setRGBToHSY, "style"_a, + DOC(GradingHueCurveTransform, setRGBToHSY)) + .def("isDynamic", &GradingHueCurveTransform::isDynamic, + DOC(GradingHueCurveTransform, isDynamic)) + .def("makeDynamic", &GradingHueCurveTransform::makeDynamic, + DOC(GradingHueCurveTransform, makeDynamic)) + .def("makeNonDynamic", &GradingHueCurveTransform::makeNonDynamic, + DOC(GradingHueCurveTransform, makeNonDynamic)); + + defRepr(clsGradingHueCurveTransform); +} + +} // namespace OCIO_NAMESPACE diff --git a/tests/cpu/CMakeLists.txt b/tests/cpu/CMakeLists.txt index 322f0f94bf..aaf12d5f60 100755 --- a/tests/cpu/CMakeLists.txt +++ b/tests/cpu/CMakeLists.txt @@ -151,6 +151,7 @@ set(SOURCES ops/fixedfunction/ACES2/Transform.cpp ops/fixedfunction/FixedFunctionOpGPU.cpp ops/gamma/GammaOpGPU.cpp + ops/gradinghuecurve/GradingHueCurveOpGPU.cpp ops/gradingprimary/GradingPrimaryOpGPU.cpp ops/gradingrgbcurve/GradingRGBCurveOpGPU.cpp ops/gradingtone/GradingToneOpGPU.cpp @@ -253,6 +254,10 @@ set(TESTS ops/gradingprimary/GradingPrimaryOpData_tests.cpp ops/gradingprimary/GradingPrimaryOp_tests.cpp ops/gradingrgbcurve/GradingBSplineCurve_tests.cpp + ops/gradinghuecurve/GradingHueCurve_tests.cpp + ops/gradinghuecurve/GradingHueCurveOpCPU_tests.cpp + ops/gradinghuecurve/GradingHueCurveOpData_tests.cpp + ops/gradinghuecurve/GradingHueCurveOp_tests.cpp ops/gradingrgbcurve/GradingRGBCurve_tests.cpp ops/gradingrgbcurve/GradingRGBCurveOpCPU_tests.cpp ops/gradingrgbcurve/GradingRGBCurveOpData_tests.cpp @@ -301,6 +306,7 @@ set(TESTS transforms/ExposureContrastTransform_tests.cpp transforms/FileTransform_tests.cpp transforms/FixedFunctionTransform_tests.cpp + transforms/GradingHueCurveTransform_tests.cpp transforms/GradingPrimaryTransform_tests.cpp transforms/GradingRGBCurveTransform_tests.cpp transforms/GradingToneTransform_tests.cpp diff --git a/tests/cpu/Config_tests.cpp b/tests/cpu/Config_tests.cpp index 163481c40a..b3d69c1688 100644 --- a/tests/cpu/Config_tests.cpp +++ b/tests/cpu/Config_tests.cpp @@ -4729,6 +4729,121 @@ OCIO_ADD_TEST(Config, grading_rgbcurve_serialization) } } +OCIO_ADD_TEST(Config, grading_huecurve_serialization) +{ + { + const std::string strEnd = + " from_scene_reference: !\n" + " children:\n" + " - ! {style: log}\n" + " - ! {style: log, direction: inverse}\n" + " - ! {style: linear}\n" + " - ! {style: linear, direction: inverse}\n" + " - ! {name: test, style: video}\n" + " - ! {style: video, direction: inverse}\n"; + + const std::string str = PROFILE_START_V<2, 5>() + strEnd; + + std::istringstream is; + is.str(str); + + OCIO::ConstConfigRcPtr config; + OCIO_CHECK_NO_THROW(config = OCIO::Config::CreateFromStream(is)); + OCIO_CHECK_NO_THROW(config->validate()); + + // Write the config. + + std::stringstream ss; + OCIO_CHECK_NO_THROW(ss << *config.get()); + OCIO_CHECK_EQUAL(ss.str(), str); + } + + { + const std::string strEnd = + " from_scene_reference: !\n" + " children:\n" + " - !\n" + " style: log\n" + " hue_hue: {control_points: [0, 0.15, 0.5, 0.5, 1, 1.123456]}\n" + " - !\n" + " style: log\n" + " hue_sat: {control_points: [0, 0, 0.5, 0.5, 1, 1.5]}\n" + " lum_lum: {control_points: [-1, -1, 0, 0.1, 0.5, 0.6, 1, 1.1]}\n" + " direction: inverse\n" + " - !\n" + " style: linear\n" + " hsy_transform: none\n" + " sat_sat: {control_points: [0, 0, 0.1, 0.2, 0.5, 0.5, 0.7, 0.6, 1, 1.5]}\n" + " lum_lum: {control_points: [-1, -1, 0, 0.1, 0.5, 0.6, 1, 1.1]}\n" + " - !\n" + " style: video\n" + " hue_hue: {control_points: [0.02, -0.1, 0.5, 0.5, 0.9, 0.8]}\n" + " hue_lum: {control_points: [0, 0, 0.2, 0.5, 1, 1.5]}\n" + " lum_sat: {control_points: [0, 0, 0.1, 0.5, 1, 1.5], slopes: [0, 1, 1.1]}\n" + " sat_lum: {control_points: [-1, -1, 0, 0.1, 0.5, 0.6, 1, 1.1]}\n" + " direction: inverse\n"; + + const std::string str = PROFILE_START_V<2, 5>() + strEnd; + + std::istringstream is; + is.str(str); + + OCIO::ConstConfigRcPtr config; + OCIO_CHECK_NO_THROW(config = OCIO::Config::CreateFromStream(is)); + OCIO_CHECK_NO_THROW(config->validate()); + + // Write the config. + + std::stringstream ss; + OCIO_CHECK_NO_THROW(ss << *config.get()); + OCIO_CHECK_EQUAL(ss.str(), str); + } + + { + const std::string strEnd = + " from_reference: !\n" + " children:\n" + " - ! {style: log}\n"; + const std::string str = PROFILE_START_V<2, 4>() + strEnd; + + std::istringstream is; + is.str(str); + + OCIO_CHECK_THROW_WHAT(OCIO::Config::CreateFromStream(is), OCIO::Exception, + "Only config version 2.5 (or higher) can have GradingHueCurveTransform"); + } + + { + const std::string strEnd = + " from_reference: !\n" + " children:\n" + " - !\n" + " style: log\n" + " sat_sat: {control_points: [0, 0, 0.1, 0.5, 1, 1.5], slopes: [0, 1, 1.1, 1]}\n"; + const std::string str = PROFILE_START_V<2, 5>() + strEnd; + + std::istringstream is; + is.str(str); + + OCIO_CHECK_THROW_WHAT(OCIO::Config::CreateFromStream(is), OCIO::Exception, + "Number of slopes must match number of control points"); + } + + { + const std::string strEnd = + " from_reference: !\n" + " children:\n" + " - ! {style: linear, hsy_transform: hsy1}\n"; + const std::string str = PROFILE_START_V<2, 5>() + strEnd; + + std::istringstream is; + is.str(str); + + OCIO_CHECK_THROW_WHAT(OCIO::Config::CreateFromStream(is), OCIO::Exception, + "Unknown hsy_transform value"); + } +} + OCIO_ADD_TEST(Config, grading_tone_serialization) { { @@ -5373,6 +5488,44 @@ R"([OpenColorIO Warning]: FixedFunction style is experimental and may be removed OCIO_CHECK_THROW_WHAT(config->validate(), OCIO::Exception, "FixedFunctionTransform validation failed: Parameter 100.5 (peak_luminance) cannot include any fractional component"); } + + { + const std::string strEnd = + " from_scene_reference: !\n" + " children:\n" + " - ! {style: RGB_TO_HSY_LOG}\n" + " - ! {style: RGB_TO_HSY_LOG, direction: inverse}\n" + " - ! {style: RGB_TO_HSY_LIN}\n" + " - ! {style: RGB_TO_HSY_LIN, direction: inverse}\n" + " - ! {style: RGB_TO_HSY_VID}\n" + " - ! {style: RGB_TO_HSY_VID, direction: inverse}\n"; + + { + const std::string str = PROFILE_START_V<2, 4>() + strEnd; + + std::istringstream is; + is.str(str); + + OCIO_CHECK_THROW_WHAT(OCIO::Config::CreateFromStream(is), OCIO::Exception, + "Only config version 2.5 (or higher) can have FixedFunctionTransform style 'RGB_TO_HSY_LOG'."); + } + + { + const std::string str = PROFILE_START_V<2, 5>() + strEnd; + + std::istringstream is; + is.str(str); + + OCIO::ConstConfigRcPtr config; + OCIO_CHECK_NO_THROW(config = OCIO::Config::CreateFromStream(is)); + OCIO_CHECK_NO_THROW(config->validate()); + + // Write the config. + std::stringstream ss; + OCIO_CHECK_NO_THROW(ss << *config.get()); + OCIO_CHECK_EQUAL(ss.str(), str); + } + } } OCIO_ADD_TEST(Config, exposure_contrast_serialization) diff --git a/tests/cpu/DynamicProperty_tests.cpp b/tests/cpu/DynamicProperty_tests.cpp index 9c1e26c9e3..ce00daed79 100644 --- a/tests/cpu/DynamicProperty_tests.cpp +++ b/tests/cpu/DynamicProperty_tests.cpp @@ -257,7 +257,56 @@ OCIO_ADD_TEST(DynamicPropertyImpl, equal_grading_rgb_curve) OCIO_CHECK_ASSERT(!(*dp0 == *dpImplDouble)); } -OCIO_ADD_TEST(DynamicPropertyImpl, grading_rgb_curve_knots) +OCIO_ADD_TEST(DynamicPropertyImpl, setter_validation) +{ + // Make an identity dynamic transform. + OCIO::GradingHueCurveTransformRcPtr gct = OCIO::GradingHueCurveTransform::Create(OCIO::GRADING_LOG); + gct->makeDynamic(); + + // Apply it on CPU. + OCIO::ConfigRcPtr config = OCIO::Config::Create(); + OCIO::ConstProcessorRcPtr processor = config->getProcessor(gct); + OCIO::ConstCPUProcessorRcPtr cpuProcessor = processor->getDefaultCPUProcessor(); + + float pixel[3] = { 0.4f, 0.3f, 0.2f }; + cpuProcessor->applyRGB(pixel); + + const float error = 1e-5f; + OCIO_CHECK_CLOSE(pixel[0], pixel[0], error); + OCIO_CHECK_CLOSE(pixel[1], pixel[1], error); + OCIO_CHECK_CLOSE(pixel[2], pixel[2], error); + + // Get a handle to the dynamic property. + OCIO::DynamicPropertyRcPtr dp; + OCIO_CHECK_NO_THROW(dp = cpuProcessor->getDynamicProperty(OCIO::DYNAMIC_PROPERTY_GRADING_HUECURVE)); + auto dpVal = OCIO::DynamicPropertyValue::AsGradingHueCurve(dp); + OCIO_REQUIRE_ASSERT(dpVal); + + // Set a non-identity value. + OCIO::GradingHueCurveRcPtr hueCurve = dpVal->getValue()->createEditableCopy(); + OCIO::GradingBSplineCurveRcPtr huehue = hueCurve->getCurve(OCIO::HUE_HUE); + huehue->setNumControlPoints(3); + huehue->getControlPoint(0) = OCIO::GradingControlPoint(0.f, -0.1f); + huehue->getControlPoint(1) = OCIO::GradingControlPoint(0.5f, 0.5f); + huehue->getControlPoint(2) = OCIO::GradingControlPoint(0.8f, 0.8f); + dpVal->setValue(hueCurve); + cpuProcessor->applyRGB(pixel); + + OCIO_CHECK_CLOSE(pixel[0], 0.4385873675f, error); + OCIO_CHECK_CLOSE(pixel[1], 0.2829087377f, error); + OCIO_CHECK_CLOSE(pixel[2], 0.2556785941f, error); + + // Ensure that validation of control points is happening as expected. Set the last point + // so that it is no longer monotonic with respect to the first point. Because it is periodic, + // the last point Y value becomes -0.05 when wrapped around to an X value of -0.2. + huehue->getControlPoint(2) = OCIO::GradingControlPoint(0.8f, 0.95f); + OCIO_CHECK_THROW_WHAT(dpVal->setValue(hueCurve), + OCIO::Exception, + "GradingHueCurve validation failed for 'hue_hue' curve with: Control point at index 0 " + "has a y coordinate '-0.1' that is less than previous control point y coordinate '-0.05'."); +} + +OCIO_ADD_TEST(DynamicPropertyImpl, grading_rgb_curve_knots_coefs) { auto curve11 = OCIO::GradingBSplineCurve::Create({ { 0.f, 10.f },{ 2.f, 10.f },{ 3.f, 10.f }, { 5.f, 10.f },{ 6.f, 10.f },{ 8.f, 10.f },{ 9.f, 10.5f },{ 11.f, 15.f },{ 12.f, 50.f }, @@ -399,6 +448,190 @@ OCIO_ADD_TEST(DynamicPropertyImpl, grading_rgb_curve_knots) OCIO_CHECK_EQUAL(dpPointer, dpPointerAfterSet); } +void checkKnotsAndCoefs( + OCIO::DynamicPropertyGradingHueCurveImplRcPtr & dp, + int set, + const float * true_knots, + const float * true_coefsA, + const float * true_coefsB, + const float * true_coefsC, + unsigned line) +{ + const int * knotsOffsets = dp->getKnotsOffsetsArray(); + const int * coefsOffsets = dp->getCoefsOffsetsArray(); + const float * knots = dp->getKnotsArray(); + const float * coefs = dp->getCoefsArray(); + + const int numKnots = knotsOffsets[set*2 + 1]; + for (int i = 0; i < numKnots; i++) + { + const int offset = knotsOffsets[set*2]; + OCIO_CHECK_CLOSE_FROM(knots[offset + i], true_knots[i], 1e-6, line); + } + const int numCoefSets = coefsOffsets[set*2 + 1] / 3; + for (int i = 0; i < numCoefSets; i++) + { + const int offset = coefsOffsets[set*2]; + OCIO_CHECK_CLOSE_FROM(coefs[offset + i], true_coefsA[i], 3e-4, line); + OCIO_CHECK_CLOSE_FROM(coefs[offset + numCoefSets + i], true_coefsB[i], 1e-5, line); + OCIO_CHECK_CLOSE_FROM(coefs[offset + 2*numCoefSets + i], true_coefsC[i], 1e-5, line); + } +} + +OCIO_ADD_TEST(DynamicPropertyImpl, grading_hue_curve_knots_coefs) +{ + auto hh = OCIO::GradingBSplineCurve::Create( + { {0.1f, 0.05f}, {0.2f, 0.3f}, {0.5f, 0.4f}, {0.8f, 0.7f}, {0.9f, 0.75f}, {1.0f, 0.9f} }, + OCIO::HUE_HUE); + auto hs = OCIO::GradingBSplineCurve::Create( + { {-0.15f, 1.25f}, {0.f, 0.8f}, {0.2f, 0.9f}, {0.4f, 1.8f}, {0.6f, 1.4f}, {0.8f, 1.3f}, {0.9f, 1.1f}, {1.1f, 0.7f} }, + OCIO::HUE_SAT); + auto hl = OCIO::GradingBSplineCurve::Create( + { {0.f, 0.f}, {0.22f, 0.077f}, {0.36f, 0.092f}, {0.51f, 0.27f}, {0.67f, 0.f}, {0.83f, 0.f} }, + OCIO::HUE_LUM); + // The rest are identities, but not the default curves. + auto ls = OCIO::GradingBSplineCurve::Create( + { {0.f, 1.f}, {1.f, 1.f} }, + OCIO::LUM_SAT); + auto ss = OCIO::GradingBSplineCurve::Create( + { {0.f, 0.f}, {0.25f, 0.25f}, {1.f, 1.f} }, + OCIO::SAT_SAT); + auto ll = OCIO::GradingBSplineCurve::Create( + { {0.f, 0.f}, {0.25f, 0.25f}, {0.5f, 0.5f}, {1.f, 1.f} }, + OCIO::LUM_LUM); + auto sl = OCIO::GradingBSplineCurve::Create( + { {0.f, 1.f}, {0.25f, 1.f}, {0.5f, 1.f}, {1.f, 1.f} }, + OCIO::SAT_LUM); + auto hfx = OCIO::GradingBSplineCurve::Create( + { {0.f, 0.f}, {0.1f, 0.f}, {0.2f, 0.f}, {0.4f, 0.f}, {0.6f, 0.f}, {0.8f, 0.f} }, + OCIO::HUE_FX); + + auto curves = OCIO::GradingHueCurve::Create(hh, hs, hl, ls, ss, ll, sl, hfx); + + { + // Fit the polynomials. + OCIO::DynamicPropertyGradingHueCurveImplRcPtr dp = + std::make_shared(curves, false); + + OCIO_CHECK_EQUAL(46, dp->getNumKnots()); + OCIO_CHECK_EQUAL(129, dp->getNumCoefs()); + + const int * coefsOffsets = dp->getCoefsOffsetsArray(); + const int * knotsOffsets = dp->getKnotsOffsetsArray(); + + // These are offset0, count0, offset1, count1, offset2, count2, ... + const int true_knotsOffsets[] = {0, 15, 15, 19, 34, 12, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0}; + const int true_coefsOffsets[] = {0, 42, 42, 54, 96, 33, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0}; + OCIO_CHECK_EQUAL(16, dp->GetNumOffsetValues()); + for (int i=0; i < dp->GetNumOffsetValues(); i++) + { + OCIO_CHECK_EQUAL(knotsOffsets[i], true_knotsOffsets[i]); + OCIO_CHECK_EQUAL(coefsOffsets[i], true_coefsOffsets[i]); + } + } + + // Repeat the test in DrawCurveOnly mode. This will yield identity knots and coefs for the + // curves that are identities. + + curves->setDrawCurveOnly(true); + + OCIO::DynamicPropertyGradingHueCurveImplRcPtr dp = + std::make_shared(curves, false); + + OCIO_CHECK_EQUAL(56, dp->getNumKnots()); + OCIO_CHECK_EQUAL(144, dp->getNumCoefs()); + + const int * coefsOffsets = dp->getCoefsOffsetsArray(); + const int * knotsOffsets = dp->getKnotsOffsetsArray(); + + const int true_knotsOffsets[] = {0, 15, 15, 19, 34, 12, 46, 2, 48, 2, 50, 2, 52, 2, 54, 2}; + const int true_coefsOffsets[] = {0, 42, 42, 54, 96, 33, 129, 3, 132, 3, 135, 3, 138, 3, 141, 3}; + for (int i=0; i < dp->GetNumOffsetValues(); i++) + { + OCIO_CHECK_EQUAL(knotsOffsets[i], true_knotsOffsets[i]); + OCIO_CHECK_EQUAL(coefsOffsets[i], true_coefsOffsets[i]); + } + + { + // Hue-Hue + const float true_knots[15] = {-0.1f, -0.06928571f, 0.0f, 0.05642857f, 0.1f, 0.17549634f, 0.2f, 0.33714286f, + 0.5f, 0.62499860f, 0.8f, 0.85261905f, 0.9f, 0.93071429f, 1.f }; + + // Quadratic coefs. + const float true_coefsA[14] = { 15.95930233f, -1.66237113f, -1.44778481f, 6.17827869f, 10.39930009f, + -58.70626575f, -1.54375789f, 1.03834397f, 3.7077401f, -2.12344738f, + -3.54260935f, 4.81365159f, 15.95930233f,-1.66237113f }; + // Linear coefs. + const float true_coefsB[14] = { 0.75f, 1.73035714f, 1.5f, 1.33660714f, 1.875f, 3.44521825f, 0.56818182f, + 0.14475108f, 0.48295455f, 1.40987919f, 0.66666667f, 0.29384921f, 0.75f, 1.73035714f }; + + // Constant coefs. + const float true_coefsC[14] = { -0.25f, -0.2119088f, -0.1f, -0.01996716f, 0.05f, 0.25082851f, 0.3f, + 0.34888683f, 0.4f, 0.51830078f, 0.7f, 0.72527072f, 0.75f, 0.7880912f }; + + checkKnotsAndCoefs(dp, 0, true_knots, true_coefsA, true_coefsB, true_coefsC, __LINE__); + } + { + // Hue-Sat + + const float true_knots[19] = { -0.1f, -0.03071429f, 0.f, 0.0625f, 0.1f, 0.13333333f, 0.2f, 0.34913793f, 0.4f, + 0.46896552f, 0.6f, 0.69f, 0.8f, 0.82770833f, 0.85f, 0.86535714f, 0.9f, 0.96928571f, 1.f }; + const float true_coefsA[18] = { -3.32474227f, 31.91860465f, 3.5f, 14.16666667f, 32.30769231f, 4.61538462f, + 13.9662072f, -68.17470665f, -25.2f, 10.21052632f, 2.92592593f, -1.78787879f, + -5.32581454f, -12.07165109f, -63.8372093f, 6.64948454f, -3.32474227f, 31.91860465f }; + const float true_coefsB[18] = { -3.f, -3.46071429f, -1.5f, -1.0625f, 0.f, 2.15384615f, 2.76923077f, 6.93501326f, 0.f, -3.47586207f, + -0.8f, -0.27333333f, -0.66666667f, -0.96180556f, -1.5f, -3.46071429f, -3.f, -3.46071429f }; + const float true_coefsC[18] = { 1.1f, 0.8761824f, 0.8f, 0.71992187f, 0.7f, 0.73589744f, 0.9f, 1.62363544f, 1.8f, + 1.68014269f, 1.4f, 1.3517f, 1.3f, 1.27743887f, 1.25f, 1.2119088f, 1.1f, 0.8761824f }; + + checkKnotsAndCoefs(dp, 1, true_knots, true_coefsA, true_coefsB, true_coefsC, __LINE__); + } + { + // Hue-Lum + // Test for the "Adjust slopes that are not shape-preserving" path in EstimateHueSlopes. + + const float true_knots[12] = { -0.17f, 0.f, 0.07049104f, 0.22f, 0.29691485f, 0.36f, 0.435f, 0.51f, 0.59f, 0.67f, 0.83f, 1.f }; + + const float true_coefsA[11] = { 0.f, 4.21997107f, -1.47264319f, -0.70657119f, 1.10402357f, 13.97025263f, + -15.20489902f, -21.09375f, 21.09375f, 0.f, 0.f }; + const float true_coefsB[11] = { 0.f, 0.f, 0.59494032f, 0.15459362f, 0.04590198f, 0.18519696f, 2.28073485f, + 0.f, -3.375f, 0.f, 0.f }; + const float true_coefsC[11] = { 0.f, 0.f, 0.02096898f, 0.077f, 0.08471054f, 0.092f, 0.18447244f, 0.27f, + 0.135f, 0.f, 0.f }; + + checkKnotsAndCoefs(dp, 2, true_knots, true_coefsA, true_coefsB, true_coefsC, __LINE__); + } + { + // Horizontal identities + + const float true_knots[2] = { 0.f, 1.f }; + const float true_coefsA[1] = { 0.f }; + const float true_coefsB[1] = { 0.f }; + const float true_coefsC[1] = { 1.f }; + const float true_coefsCfx[1] = { 0.f }; + + // Lum-Sat + checkKnotsAndCoefs(dp, 3, true_knots, true_coefsA, true_coefsB, true_coefsC, __LINE__); + // Sat-Lum + checkKnotsAndCoefs(dp, 6, true_knots, true_coefsA, true_coefsB, true_coefsC, __LINE__); + // Hue-Fx + checkKnotsAndCoefs(dp, 7, true_knots, true_coefsA, true_coefsB, true_coefsCfx, __LINE__); + } + { + // Diagonal identities + + const float true_knots[2] = { 0.f, 1.f }; + const float true_coefsA[1] = { 0.f }; + const float true_coefsB[1] = { 1.f }; + const float true_coefsC[1] = { 0.f }; + + // Sat-Sat + checkKnotsAndCoefs(dp, 4, true_knots, true_coefsA, true_coefsB, true_coefsC, __LINE__); + // Lum-Lum + checkKnotsAndCoefs(dp, 5, true_knots, true_coefsA, true_coefsB, true_coefsC, __LINE__); + } +} + OCIO_ADD_TEST(DynamicPropertyImpl, get_as) { OCIO::GradingPrimary gplog{ OCIO::GRADING_LOG }; diff --git a/tests/cpu/GpuShader_tests.cpp b/tests/cpu/GpuShader_tests.cpp index 610366a036..569358239b 100644 --- a/tests/cpu/GpuShader_tests.cpp +++ b/tests/cpu/GpuShader_tests.cpp @@ -1090,7 +1090,7 @@ constant constexpr static float ocio_grading_rgbcurve_knots_0[5] = {0., 0.333333 constant constexpr static int ocio_grading_rgbcurve_coefsOffsets_0[8] = {0, 12, -1, 0, -1, 0, -1, 0}; constant constexpr static float ocio_grading_rgbcurve_coefs_0[12] = {0.0982520878, 0.393008381, 0.347727984, 0.08693178, 0.934498608, 1., 1.13100278, 1.246912, 0., 0.322416425, 0.5, 0.698159397}; -float ocio_grading_rgbcurve_evalBSplineCurve_0(int curveIdx, float x) +float ocio_grading_rgbcurve_evalBSplineCurve_0(int curveIdx, float x, float identity_x) { int knotsOffs = ocio_grading_rgbcurve_knotsOffsets_0[curveIdx * 2]; int knotsCnt = ocio_grading_rgbcurve_knotsOffsets_0[curveIdx * 2 + 1]; @@ -1099,7 +1099,7 @@ float ocio_grading_rgbcurve_evalBSplineCurve_0(int curveIdx, float x) int coefsSets = coefsCnt / 3; if (coefsSets == 0) { - return x; + return identity_x; } float knStart = ocio_grading_rgbcurve_knots_0[knotsOffs]; float knEnd = ocio_grading_rgbcurve_knots_0[knotsOffs + knotsCnt - 1]; @@ -1145,12 +1145,12 @@ float4 OCIOMain(float4 inPixel) // Add GradingRGBCurve 'log' forward processing { - outColor.rgb.r = ocio_grading_rgbcurve_evalBSplineCurve_0(0, outColor.rgb.r); - outColor.rgb.g = ocio_grading_rgbcurve_evalBSplineCurve_0(1, outColor.rgb.g); - outColor.rgb.b = ocio_grading_rgbcurve_evalBSplineCurve_0(2, outColor.rgb.b); - outColor.rgb.r = ocio_grading_rgbcurve_evalBSplineCurve_0(3, outColor.rgb.r); - outColor.rgb.g = ocio_grading_rgbcurve_evalBSplineCurve_0(3, outColor.rgb.g); - outColor.rgb.b = ocio_grading_rgbcurve_evalBSplineCurve_0(3, outColor.rgb.b); + outColor.rgb.r = ocio_grading_rgbcurve_evalBSplineCurve_0(0, outColor.rgb.r, outColor.rgb.r); + outColor.rgb.g = ocio_grading_rgbcurve_evalBSplineCurve_0(1, outColor.rgb.g, outColor.rgb.g); + outColor.rgb.b = ocio_grading_rgbcurve_evalBSplineCurve_0(2, outColor.rgb.b, outColor.rgb.b); + outColor.rgb.r = ocio_grading_rgbcurve_evalBSplineCurve_0(3, outColor.rgb.r, outColor.rgb.r); + outColor.rgb.g = ocio_grading_rgbcurve_evalBSplineCurve_0(3, outColor.rgb.g, outColor.rgb.g); + outColor.rgb.b = ocio_grading_rgbcurve_evalBSplineCurve_0(3, outColor.rgb.b, outColor.rgb.b); } return outColor; @@ -1196,11 +1196,11 @@ struct ocioOCIOMain ocioOCIOMain( constant int ocio_grading_rgbcurve_knotsOffsets[8] , int ocio_grading_rgbcurve_knotsOffsets_count - , constant float ocio_grading_rgbcurve_knots[60] + , constant float ocio_grading_rgbcurve_knots[120] , int ocio_grading_rgbcurve_knots_count , constant int ocio_grading_rgbcurve_coefsOffsets[8] , int ocio_grading_rgbcurve_coefsOffsets_count - , constant float ocio_grading_rgbcurve_coefs[180] + , constant float ocio_grading_rgbcurve_coefs[360] , int ocio_grading_rgbcurve_coefs_count , bool ocio_grading_rgbcurve_localBypass ) @@ -1217,7 +1217,7 @@ ocioOCIOMain( { this->ocio_grading_rgbcurve_knots[i] = ocio_grading_rgbcurve_knots[i]; } - for(int i = ocio_grading_rgbcurve_knots_count; i < 60; ++i) + for(int i = ocio_grading_rgbcurve_knots_count; i < 120; ++i) { this->ocio_grading_rgbcurve_knots[i] = 0; } @@ -1233,7 +1233,7 @@ ocioOCIOMain( { this->ocio_grading_rgbcurve_coefs[i] = ocio_grading_rgbcurve_coefs[i]; } - for(int i = ocio_grading_rgbcurve_coefs_count; i < 180; ++i) + for(int i = ocio_grading_rgbcurve_coefs_count; i < 360; ++i) { this->ocio_grading_rgbcurve_coefs[i] = 0; } @@ -1244,16 +1244,16 @@ ocioOCIOMain( // Declaration of all variables int ocio_grading_rgbcurve_knotsOffsets[8]; -float ocio_grading_rgbcurve_knots[60]; +float ocio_grading_rgbcurve_knots[120]; int ocio_grading_rgbcurve_coefsOffsets[8]; -float ocio_grading_rgbcurve_coefs[180]; +float ocio_grading_rgbcurve_coefs[360]; bool ocio_grading_rgbcurve_localBypass; // Declaration of all helper methods -float ocio_grading_rgbcurve_evalBSplineCurve(int curveIdx, float x) +float ocio_grading_rgbcurve_evalBSplineCurve(int curveIdx, float x, float identity_x) { int knotsOffs = ocio_grading_rgbcurve_knotsOffsets[curveIdx * 2]; int knotsCnt = ocio_grading_rgbcurve_knotsOffsets[curveIdx * 2 + 1]; @@ -1262,7 +1262,7 @@ float ocio_grading_rgbcurve_evalBSplineCurve(int curveIdx, float x) int coefsSets = coefsCnt / 3; if (coefsSets == 0) { - return x; + return identity_x; } float knStart = ocio_grading_rgbcurve_knots[knotsOffs]; float knEnd = ocio_grading_rgbcurve_knots[knotsOffs + knotsCnt - 1]; @@ -1310,12 +1310,12 @@ float4 OCIOMain(float4 inPixel) { if (!ocio_grading_rgbcurve_localBypass) { - outColor.rgb.r = ocio_grading_rgbcurve_evalBSplineCurve(0, outColor.rgb.r); - outColor.rgb.g = ocio_grading_rgbcurve_evalBSplineCurve(1, outColor.rgb.g); - outColor.rgb.b = ocio_grading_rgbcurve_evalBSplineCurve(2, outColor.rgb.b); - outColor.rgb.r = ocio_grading_rgbcurve_evalBSplineCurve(3, outColor.rgb.r); - outColor.rgb.g = ocio_grading_rgbcurve_evalBSplineCurve(3, outColor.rgb.g); - outColor.rgb.b = ocio_grading_rgbcurve_evalBSplineCurve(3, outColor.rgb.b); + outColor.rgb.r = ocio_grading_rgbcurve_evalBSplineCurve(0, outColor.rgb.r, outColor.rgb.r); + outColor.rgb.g = ocio_grading_rgbcurve_evalBSplineCurve(1, outColor.rgb.g, outColor.rgb.g); + outColor.rgb.b = ocio_grading_rgbcurve_evalBSplineCurve(2, outColor.rgb.b, outColor.rgb.b); + outColor.rgb.r = ocio_grading_rgbcurve_evalBSplineCurve(3, outColor.rgb.r, outColor.rgb.r); + outColor.rgb.g = ocio_grading_rgbcurve_evalBSplineCurve(3, outColor.rgb.g, outColor.rgb.g); + outColor.rgb.b = ocio_grading_rgbcurve_evalBSplineCurve(3, outColor.rgb.b, outColor.rgb.b); } } @@ -1329,11 +1329,11 @@ float4 OCIOMain(float4 inPixel) float4 OCIOMain( constant int ocio_grading_rgbcurve_knotsOffsets[8] , int ocio_grading_rgbcurve_knotsOffsets_count - , constant float ocio_grading_rgbcurve_knots[60] + , constant float ocio_grading_rgbcurve_knots[120] , int ocio_grading_rgbcurve_knots_count , constant int ocio_grading_rgbcurve_coefsOffsets[8] , int ocio_grading_rgbcurve_coefsOffsets_count - , constant float ocio_grading_rgbcurve_coefs[180] + , constant float ocio_grading_rgbcurve_coefs[360] , int ocio_grading_rgbcurve_coefs_count , bool ocio_grading_rgbcurve_localBypass , float4 inPixel) diff --git a/tests/cpu/NamedTransform_tests.cpp b/tests/cpu/NamedTransform_tests.cpp index 2d2b5e810d..5a05cae41d 100644 --- a/tests/cpu/NamedTransform_tests.cpp +++ b/tests/cpu/NamedTransform_tests.cpp @@ -40,8 +40,8 @@ OCIO_ADD_TEST(NamedTransform, basic) OCIO_CHECK_NO_THROW(oss << *namedTransform); OCIO_CHECK_EQUAL(oss.str(), ">"); + "fileoutdepth=unknown, matrix=[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], " + "offset=[0, 0, 0, 0]>>"); // Test faulty cases. diff --git a/tests/cpu/fileformats/FileFormatCTF_tests.cpp b/tests/cpu/fileformats/FileFormatCTF_tests.cpp index 25cc87a4d2..4dc12578a0 100644 --- a/tests/cpu/fileformats/FileFormatCTF_tests.cpp +++ b/tests/cpu/fileformats/FileFormatCTF_tests.cpp @@ -6,6 +6,7 @@ #include "fileformats/FileFormatCTF.cpp" #include "ops/fixedfunction/FixedFunctionOp.h" #include "ops/gradingrgbcurve/GradingRGBCurve.h" +#include "ops/gradinghuecurve/GradingHueCurve.h" #include "testutils/UnitTest.h" #include "UnitTestLogUtils.h" #include "UnitTestUtils.h" @@ -3803,14 +3804,20 @@ void WriteGroupCLF(OCIO::ConstGroupTransformRcPtr group, std::ostringstream & ou group->write(cfg, OCIO::FILEFORMAT_CLF, outputTransform); } -void ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::Style style, - const std::string & vers, int lineNo) +void ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::Style style, + const std::string & vers, int lineNo, std::string params="") { // Validate the load & save for any FixedFunction style without parameters. std::ostringstream ffStr; ffStr << ""; + << OCIO::FixedFunctionOpData::ConvertStyleToString(style, false) << "\""; //>"; + + if (!params.empty()) + { + ffStr << " params=\"" << params << "\""; + } + ffStr << ">"; std::ostringstream strebuf; strebuf << "\n" @@ -3819,6 +3826,7 @@ void ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::Style style, << " \n" << "\n"; + // Test parsing. OCIO::LocalCachedFileRcPtr cachedFile; OCIO_CHECK_NO_THROW_FROM(cachedFile = ParseString(strebuf.str()), lineNo); OCIO::ConstOpDataVec & fileOps = cachedFile->m_transform->getOps(); @@ -3841,6 +3849,7 @@ void ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::Style style, OCIO_CHECK_NO_THROW_FROM(OCIO::CreateFixedFunctionTransform(group, constOp), lineNo); OCIO_REQUIRE_EQUAL_FROM(group->getNumTransforms(), 1, lineNo); + // Test serialization. std::ostringstream outputTransform; OCIO_CHECK_NO_THROW_FROM(WriteGroupCTF(group, outputTransform), lineNo); @@ -3859,26 +3868,62 @@ void ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::Style style, OCIO_ADD_TEST(FileFormatCTF, ff_load_save_ctf) { - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::ACES_RED_MOD_03_FWD, "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::ACES_RED_MOD_03_INV, "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::ACES_RED_MOD_10_FWD, "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::ACES_RED_MOD_10_INV, "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::ACES_GLOW_03_FWD , "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::ACES_GLOW_03_INV , "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::ACES_GLOW_10_FWD , "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::ACES_GLOW_10_INV , "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::ACES_DARK_TO_DIM_10_FWD, "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::ACES_DARK_TO_DIM_10_INV, "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::RGB_TO_HSV , "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::HSV_TO_RGB , "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::XYZ_TO_xyY , "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::xyY_TO_XYZ , "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::XYZ_TO_uvY , "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::uvY_TO_XYZ , "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::XYZ_TO_LUV , "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::LUV_TO_XYZ , "2", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::PQ_TO_LIN , "2.4", __LINE__); - ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::LIN_TO_PQ , "2.4", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_RED_MOD_03_FWD , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_RED_MOD_03_INV , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_RED_MOD_10_FWD , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_RED_MOD_10_INV , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_GLOW_03_FWD , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_GLOW_03_INV , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_GLOW_10_FWD , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_GLOW_10_INV , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_DARK_TO_DIM_10_FWD, "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_DARK_TO_DIM_10_INV, "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_GAMUT_COMP_13_FWD , "2.1", __LINE__, + "1.147 1.264 1.312 0.815 0.803 0.88 1.2"); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::REC2100_SURROUND_FWD , "2", __LINE__, + "1"); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::REC2100_SURROUND_INV , "2", __LINE__, + "1"); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::RGB_TO_HSV , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::HSV_TO_RGB , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::XYZ_TO_xyY , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::xyY_TO_XYZ , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::XYZ_TO_uvY , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::uvY_TO_XYZ , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::XYZ_TO_LUV , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::LUV_TO_XYZ , "2", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::LIN_TO_PQ , "2.4", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::PQ_TO_LIN , "2.4", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::LIN_TO_GAMMA_LOG , "2.4", __LINE__, + "0 0.25 0.5 1 0 2.718 0.17883277 0.807825590164 1 -0.07116723"); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::GAMMA_LOG_TO_LIN , "2.4", __LINE__, + "0 0.25 0.5 1 0 2.718 0.17883277 0.807825590164 1 -0.07116723"); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::LIN_TO_DOUBLE_LOG , "2.4", __LINE__, + "10 0.25 0.5 -1 0 -1 1.25 1 1 1 0.5 1 0"); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::DOUBLE_LOG_TO_LIN , "2.4", __LINE__, + "10 0.25 0.5 -1 0 -1 1.25 1 1 1 0.5 1 0"); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_OUTPUT_TRANSFORM_20_FWD , "2.4", __LINE__, + "1000 0.68 0.32 0.265 0.69 0.15 0.06 0.3127 0.329"); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_OUTPUT_TRANSFORM_20_INV , "2.4", __LINE__, + "1000 0.68 0.32 0.265 0.69 0.15 0.06 0.3127 0.329"); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_RGB_TO_JMh_20 , "2.4", __LINE__, + "0.68 0.32 0.265 0.69 0.15 0.06 0.3127 0.329"); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_JMh_TO_RGB_20 , "2.4", __LINE__, + "0.68 0.32 0.265 0.69 0.15 0.06 0.3127 0.329"); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_TONESCALE_COMPRESS_20_FWD, "2.4", __LINE__, + "1000"); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_TONESCALE_COMPRESS_20_INV, "2.4", __LINE__, + "1000"); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_GAMUT_COMPRESS_20_FWD , "2.4", __LINE__, + "1000 0.68 0.32 0.265 0.69 0.15 0.06 0.3127 0.329"); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::ACES_GAMUT_COMPRESS_20_INV , "2.4", __LINE__, + "1000 0.68 0.32 0.265 0.69 0.15 0.06 0.3127 0.329"); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::RGB_TO_HSY_LOG , "2.5", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::HSY_LOG_TO_RGB , "2.5", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::RGB_TO_HSY_LIN , "2.5", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::HSY_LIN_TO_RGB , "2.5", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::RGB_TO_HSY_VID , "2.5", __LINE__); + ValidateFixedFunctionStyle(OCIO::FixedFunctionOpData::HSY_VID_TO_RGB , "2.5", __LINE__); } OCIO_ADD_TEST(FileFormatCTF, load_ff_fail_version) @@ -4365,7 +4410,7 @@ OCIO_ADD_TEST(FileFormatCTF, load_grading_primary_errors) )"), OCIO::Exception, "Dynamic parameter 'CONTRAST' is not supported in 'GradingPrimary'"); } -OCIO_ADD_TEST(FileFormatCTF, load_grading_curves_lin) +OCIO_ADD_TEST(FileFormatCTF, load_grading_rgbcurves_lin) { std::istringstream ctfLin; ctfLin.str(R"( @@ -4454,7 +4499,7 @@ OCIO_ADD_TEST(FileFormatCTF, load_grading_curves_lin) OCIO_CHECK_EQUAL(master->getSlope(2), 1.1f); } -OCIO_ADD_TEST(FileFormatCTF, load_grading_curves_log) +OCIO_ADD_TEST(FileFormatCTF, load_grading_rgbcurves_log) { std::istringstream ctfLog; ctfLog.str(R"( @@ -4469,7 +4514,7 @@ OCIO_ADD_TEST(FileFormatCTF, load_grading_curves_log) - 0.015625 1 + 0.015625 0.1 2.5 0.5 3.5 1.5 @@ -4483,7 +4528,7 @@ OCIO_ADD_TEST(FileFormatCTF, load_grading_curves_log) - 11 11 12.5 10.5 13.5 0.5 26.5 -1.5 + 11 11 12.5 11.5 13.5 12.5 26.5 15 @@ -4522,11 +4567,118 @@ OCIO_ADD_TEST(FileFormatCTF, load_grading_curves_log) OCIO_CHECK_EQUAL(master->getControlPoint(0).m_x, 11.0f); OCIO_CHECK_EQUAL(master->getControlPoint(0).m_y, 11.0f); OCIO_CHECK_EQUAL(master->getControlPoint(1).m_x, 12.5f); - OCIO_CHECK_EQUAL(master->getControlPoint(1).m_y, 10.5f); + OCIO_CHECK_EQUAL(master->getControlPoint(1).m_y, 11.5f); OCIO_CHECK_EQUAL(master->getControlPoint(2).m_x, 13.5f); - OCIO_CHECK_EQUAL(master->getControlPoint(2).m_y, 0.5f); + OCIO_CHECK_EQUAL(master->getControlPoint(2).m_y, 12.5f); OCIO_CHECK_EQUAL(master->getControlPoint(3).m_x, 26.5f); - OCIO_CHECK_EQUAL(master->getControlPoint(3).m_y, -1.5f); + OCIO_CHECK_EQUAL(master->getControlPoint(3).m_y, 15.f); +} + +OCIO_ADD_TEST(FileFormatCTF, load_grading_huecurves_log) +{ + std::istringstream ctfLog; + ctfLog.str(R"( + + + + + 0.015625 0 + 0.5 0.6 + 0.9 0.8 + + + + + 0.015625 1 + 0.5 0.5 + 0.9 1.5 + + + + + 0.1, 1.5, 0.2, 0.7, 0.4, 1.4, 0.5, 0.8, 0.8, 0.5 + + + + + -0.1 1.0 + 0.5 1.5 + 1.0 0.9 + 1.1 1.2 + + + + + 0., 0.1, 0.5, 0.45, 1., 1.1 + + + + + 0. -0.0005 + 0.5 0.3 + 1. 0.9 + + + + + 0., 1.2, 0.6, 0.8, 0.9, 1.1 + + + + + 0.2, 0.05, .4, -0.09, .6, -0.2, .8, 0.05, 0.99, -0.02 + + + + +)"); + + // Load file. + std::string emptyString; + OCIO::LocalFileFormat tester; + OCIO::CachedFileRcPtr file; + OCIO_CHECK_NO_THROW(file = tester.read(ctfLog, emptyString, OCIO::INTERP_DEFAULT)); + OCIO::LocalCachedFileRcPtr cachedFile = OCIO_DYNAMIC_POINTER_CAST(file); + OCIO_REQUIRE_ASSERT(cachedFile); + const auto & fileOps = cachedFile->m_transform->getOps(); + + OCIO_REQUIRE_EQUAL(fileOps.size(), 1); + const auto op0 = fileOps[0]; + const auto gradingCurves0 = std::dynamic_pointer_cast(op0); + OCIO_REQUIRE_ASSERT(gradingCurves0); + OCIO_CHECK_EQUAL(gradingCurves0->getStyle(), OCIO::GRADING_LOG); + OCIO_CHECK_EQUAL(gradingCurves0->getDirection(), OCIO::TRANSFORM_DIR_INVERSE); + OCIO_CHECK_EQUAL(gradingCurves0->getRGBToHSY(), OCIO::HSY_TRANSFORM_NONE); + OCIO_CHECK_ASSERT(gradingCurves0->isDynamic()); + auto curves = gradingCurves0->getValue(); + auto hh = curves->getCurve(OCIO::HUE_HUE); + OCIO_CHECK_EQUAL(hh->getNumControlPoints(), 3); + OCIO_CHECK_EQUAL(hh->getControlPoint(0).m_x, 0.015625f); + OCIO_CHECK_EQUAL(hh->getControlPoint(0).m_y, 0.0f); + OCIO_CHECK_EQUAL(hh->getControlPoint(1).m_x, 0.5f); + OCIO_CHECK_EQUAL(hh->getControlPoint(1).m_y, 0.6f); + OCIO_CHECK_EQUAL(hh->getControlPoint(2).m_x, 0.9f); + OCIO_CHECK_EQUAL(hh->getControlPoint(2).m_y, 0.8f); + + auto ls = curves->getCurve(OCIO::LUM_SAT); + OCIO_CHECK_EQUAL(ls->getNumControlPoints(), 4); + OCIO_CHECK_EQUAL(ls->getControlPoint(0).m_x, -0.1f); + OCIO_CHECK_EQUAL(ls->getControlPoint(0).m_y, 1.0f); + OCIO_CHECK_EQUAL(ls->getControlPoint(1).m_x, 0.5f); + OCIO_CHECK_EQUAL(ls->getControlPoint(1).m_y, 1.5f); + OCIO_CHECK_EQUAL(ls->getControlPoint(2).m_x, 1.0f); + OCIO_CHECK_EQUAL(ls->getControlPoint(2).m_y, 0.9f); + OCIO_CHECK_EQUAL(ls->getControlPoint(3).m_x, 1.1f); + OCIO_CHECK_EQUAL(ls->getControlPoint(3).m_y, 1.2f); + + auto sl = curves->getCurve(OCIO::SAT_LUM); + OCIO_CHECK_EQUAL(sl->getNumControlPoints(), 3); + OCIO_CHECK_EQUAL(sl->getControlPoint(0).m_x, 0.0f); + OCIO_CHECK_EQUAL(sl->getControlPoint(0).m_y, 1.2f); + OCIO_CHECK_EQUAL(sl->getControlPoint(1).m_x, 0.6f); + OCIO_CHECK_EQUAL(sl->getControlPoint(1).m_y, 0.8f); + OCIO_CHECK_EQUAL(sl->getControlPoint(2).m_x, 0.9f); + OCIO_CHECK_EQUAL(sl->getControlPoint(2).m_y, 1.1f); } OCIO_ADD_TEST(FileFormatCTF, load_grading_curves_errors) @@ -4540,6 +4692,13 @@ OCIO_ADD_TEST(FileFormatCTF, load_grading_curves_errors) )"), OCIO::Exception, "CTF file version '1.8' does not support operator 'GradingRGBCurve'"); + OCIO_CHECK_THROW_WHAT(ParseString(R"( + + + + +)"), OCIO::Exception, "CTF file version '2' does not support operator 'GradingHueCurve'"); + // Missing style attribute. OCIO_CHECK_THROW_WHAT(ParseString(R"( @@ -4547,6 +4706,13 @@ OCIO_ADD_TEST(FileFormatCTF, load_grading_curves_errors) +)"), OCIO::Exception, "Required attribute 'style' is missing"); + + OCIO_CHECK_THROW_WHAT(ParseString(R"( + + + + )"), OCIO::Exception, "Required attribute 'style' is missing"); // Wrong dynamic property param. @@ -4595,8 +4761,8 @@ OCIO_ADD_TEST(FileFormatCTF, load_grading_curves_errors) -)"), OCIO::Exception, "Control point at index 2 has a x coordinate '-1' that is less from " - "previous control point x cooordinate '0'"); +)"), OCIO::Exception, "Control point at index 2 has a x coordinate '-1' that is less than " + "previous control point x coordinate '0'"); // Number of slopes matches control points. OCIO_CHECK_THROW_WHAT(ParseString(R"( @@ -4633,6 +4799,13 @@ OCIO_ADD_TEST(FileFormatCTF, load_grading_curves_errors) OCIO_CHECK_NE(std::string::npos, StringUtils::Find(parts[0], "Unrecognized element 'Grn'")); OCIO_CHECK_NE(std::string::npos, StringUtils::Find(parts[1], "Unrecognized element 'ControlPoints'")); + OCIO_CHECK_THROW_WHAT(ParseString(R"( + + + + + +)"), OCIO::Exception, "Unknown hsyTransform value: 'hsy1'"); } OCIO_ADD_TEST(CTFTransform, load_grading_tone) @@ -7112,6 +7285,100 @@ OCIO_ADD_TEST(CTFTransform, grading_rgbcurve_lin_ctf) } } +OCIO_ADD_TEST(CTFTransform, grading_huecurve_lin_ctf) +{ + auto gradingCurves = OCIO::GradingHueCurveTransform::Create(OCIO::GRADING_LIN); + + // Note: The default curves are not saved. + OCIO::GradingHueCurveRcPtr curves = OCIO::GradingHueCurve::Create(OCIO::GRADING_LIN); + curves->getCurve(OCIO::LUM_SAT)->getControlPoint(0).m_y = 1.1f; + curves->getCurve(OCIO::LUM_LUM)->setNumControlPoints(4); + curves->getCurve(OCIO::LUM_LUM)->getControlPoint(3).m_x = 16.f; + curves->getCurve(OCIO::LUM_LUM)->getControlPoint(3).m_y = 10.f; + curves->getCurve(OCIO::LUM_LUM)->setSlope(0, 1.f); + curves->getCurve(OCIO::LUM_LUM)->setSlope(1, 0.75f); + curves->getCurve(OCIO::LUM_LUM)->setSlope(2, 1.1f); + curves->getCurve(OCIO::LUM_LUM)->setSlope(3, 1.f); + gradingCurves->setValue(curves); + { + OCIO::GroupTransformRcPtr group = OCIO::GroupTransform::Create(); + group->getFormatMetadata().addAttribute(OCIO::METADATA_ID, "UIDGradingCurves"); + group->appendTransform(gradingCurves); + + std::ostringstream outputTransform; + OCIO_CHECK_NO_THROW(WriteGroupCTF(group, outputTransform)); + + const std::string expected{ R"( + + + + + -7 1.1 + 0 1 + 7 1 + + + + + -7 -7 + 0 0 + 7 7 + 16 10 + + + 1 0.75 1.1 1 + + + + +)" }; + + const StringUtils::StringVec osvec = StringUtils::SplitByLines(outputTransform.str()); + const StringUtils::StringVec resvec = StringUtils::SplitByLines(expected); + OCIO_CHECK_EQUAL(osvec.size(), resvec.size()); + for(unsigned int i = 0; i < resvec.size(); ++i) + { + if ( (i >= 5 && i <= 7) || + (i >= 12 && i <= 15) || + (i == 18)) + { + OCIO_CHECK_STR_FLOAT_VEC_CLOSE(osvec[i], resvec[i], 1e-5f); + } + else + { + OCIO_CHECK_EQUAL(osvec[i], resvec[i]); + } + + } + } + + // All curves are default curves, no curve is saved. + curves = OCIO::GradingHueCurve::Create(OCIO::GRADING_LIN); + gradingCurves->setValue(curves); + gradingCurves->setRGBToHSY(OCIO::HSY_TRANSFORM_NONE); + // Make it dynamic so it is not identity. + gradingCurves->makeDynamic(); + { + OCIO::GroupTransformRcPtr group = OCIO::GroupTransform::Create(); + group->getFormatMetadata().addAttribute(OCIO::METADATA_ID, "UIDGradingCurves"); + group->appendTransform(gradingCurves); + + std::ostringstream outputTransform; + OCIO_CHECK_NO_THROW(WriteGroupCTF(group, outputTransform)); + + const std::string expected{ R"( + + + + + +)" }; + + OCIO_CHECK_EQUAL(expected.size(), outputTransform.str().size()); + OCIO_CHECK_EQUAL(expected, outputTransform.str()); + } +} + OCIO_ADD_TEST(CTFTransform, grading_tone_log_ctf) { auto gradingTone = OCIO::GradingToneTransform::Create(OCIO::GRADING_LOG); diff --git a/tests/cpu/ops/fixedfunction/FixedFunctionOpCPU_tests.cpp b/tests/cpu/ops/fixedfunction/FixedFunctionOpCPU_tests.cpp index 008264582e..9593c9cd8e 100644 --- a/tests/cpu/ops/fixedfunction/FixedFunctionOpCPU_tests.cpp +++ b/tests/cpu/ops/fixedfunction/FixedFunctionOpCPU_tests.cpp @@ -1068,6 +1068,88 @@ OCIO_ADD_TEST(FixedFunctionOpCPU, RGB_TO_HSV) ApplyFixedFunction(&img[0], &rgbFrame[0], numHSV, dataFInv, 1e-6f, __LINE__); } +OCIO_ADD_TEST(FixedFunctionOpCPU, RGB_TO_HSY_LIN) +{ + const int numPixels = 8; + const std::vector hsyFrame { + 0.470554752f, 9.12594033f, 0.0326650218f, 0.f, // hsy alpha == 1 + 0.75f, 0.22196741f, 0.38596f, 0.f, + 0.08333333f, 0.12976444f, 0.034974f, 0.f, + 0.333333333333f, 0.606036032f, 0.0056680f, 1.f, // hsy mid alpha + 0.241666666667f, 0.8372990325f, 0.0034440f, 1.f, + 0.734693877551f, 0.752099600f, 0.0005572f, 0.f, // hsy alpha == 0 + 0.96296296f, 9.7034f, -0.1862f, 0.f, + 0.730158730159f, 0.811517000f, -0.0009310f, 0.f }; + + const std::vector rgbFrame { + -0.075290f, 0.078996f, -0.108397f, 0.f, + 0.3f, 0.4f, 0.5f, 0.f, + 0.05f, 0.03f, 0.04f, 0.f, + 0.01f, 0.01f, -0.05f, 1.f, + 0.05f, -0.005f, -0.05f, 1.f, + -0.048f, 0.01f, 0.05f, 0.f, + 0.3f, -0.4f, 0.5f, 0.f, + -0.055f, 0.01f, 0.05f, 0.f }; + + OCIO::ConstFixedFunctionOpDataRcPtr dataFwdLin + = std::make_shared(OCIO::FixedFunctionOpData::RGB_TO_HSY_LIN); + + std::vector img = rgbFrame; + ApplyFixedFunction(&img[0], &hsyFrame[0], numPixels, dataFwdLin, 1e-6f, __LINE__); + + OCIO::ConstFixedFunctionOpDataRcPtr dataFInvLin + = std::make_shared(OCIO::FixedFunctionOpData::HSY_LIN_TO_RGB); + + img = hsyFrame; + ApplyFixedFunction(&img[0], &rgbFrame[0], numPixels, dataFInvLin, 1e-6f, __LINE__); +} + +OCIO_ADD_TEST(FixedFunctionOpCPU, RGB_TO_HSY_LOG) +{ + const std::vector hsyFrame { + 14563.0f / 65535, 64392.0f / 65535, 20899.0f / 65535, 32007.0f / 65535, + 50061.0f / 65535, 64310.0f / 65535, 6328.0f / 65535, 65535.0f / 65535 }; + + const std::vector rgbFrame { + 1800.0f / 4095, 1200.0f / 4095, 900.0f / 4095, 2000.0f / 4095, + 40.0f / 4095, 440.0f / 4095, 1000.0f / 4095, 4095.0f / 4095 }; + + OCIO::ConstFixedFunctionOpDataRcPtr dataFwdLin + = std::make_shared(OCIO::FixedFunctionOpData::RGB_TO_HSY_LOG); + + std::vector img = rgbFrame; + ApplyFixedFunction(&img[0], &hsyFrame[0], 2, dataFwdLin, 1e-5f, __LINE__); + + OCIO::ConstFixedFunctionOpDataRcPtr dataFInvLin + = std::make_shared(OCIO::FixedFunctionOpData::HSY_LOG_TO_RGB); + + img = hsyFrame; + ApplyFixedFunction(&img[0], &rgbFrame[0], 2, dataFInvLin, 1e-5f, __LINE__); +} + +OCIO_ADD_TEST(FixedFunctionOpCPU, RGB_TO_HSY_VID) +{ + const std::vector hsyFrame { + 0.54190051555634f, 1.0851333141327f, 0.55111348628998f, 0.48840048909187f, + 0.54262113571167f, 1.4824789762497f, 1.1281162500381f, 1.f }; + + const std::vector rgbFrame { + 0.12152557820082f, 0.70731294155121f, 0.26879417896271f, 0.48840048909187f, + 0.53938156366348f, 1.3418402671814f, 0.74459171295166f, 1.f }; + + OCIO::ConstFixedFunctionOpDataRcPtr dataFwdLin + = std::make_shared(OCIO::FixedFunctionOpData::RGB_TO_HSY_VID); + + std::vector img = rgbFrame; + ApplyFixedFunction(&img[0], &hsyFrame[0], 2, dataFwdLin, 1e-6f, __LINE__); + + OCIO::ConstFixedFunctionOpDataRcPtr dataFInvLin + = std::make_shared(OCIO::FixedFunctionOpData::HSY_VID_TO_RGB); + + img = hsyFrame; + ApplyFixedFunction(&img[0], &rgbFrame[0], 2, dataFInvLin, 1e-6f, __LINE__); +} + OCIO_ADD_TEST(FixedFunctionOpCPU, XYZ_TO_xyY) { const std::vector inputFrame { diff --git a/tests/cpu/ops/fixedfunction/FixedFunctionOp_tests.cpp b/tests/cpu/ops/fixedfunction/FixedFunctionOp_tests.cpp index cdecbfda49..1f6e8cfc8b 100644 --- a/tests/cpu/ops/fixedfunction/FixedFunctionOp_tests.cpp +++ b/tests/cpu/ops/fixedfunction/FixedFunctionOp_tests.cpp @@ -302,6 +302,84 @@ OCIO_ADD_TEST(FixedFunctionOps, RGB_TO_HSV) OCIO_CHECK_NE(std::string::npos, StringUtils::Find(typeName, "Renderer_RGB_TO_HSV")); } +OCIO_ADD_TEST(FixedFunctionOps, RGB_TO_HSY_LIN) +{ + OCIO::OpRcPtrVec ops; + + OCIO_CHECK_NO_THROW(OCIO::CreateFixedFunctionOp(ops, OCIO::FixedFunctionOpData::RGB_TO_HSY_LIN, {})); + OCIO_CHECK_NO_THROW(OCIO::CreateFixedFunctionOp(ops, OCIO::FixedFunctionOpData::HSY_LIN_TO_RGB, {})); + + OCIO_CHECK_NO_THROW(ops.finalize()); + OCIO_REQUIRE_EQUAL(ops.size(), 2); + + OCIO::ConstOpRcPtr op0 = ops[0]; + OCIO::ConstOpRcPtr op1 = ops[1]; + + OCIO_CHECK_ASSERT(!op0->isIdentity()); + OCIO_CHECK_ASSERT(!op1->isIdentity()); + + OCIO_CHECK_ASSERT(op0->isSameType(op1)); + OCIO_CHECK_ASSERT(op0->isInverse(op1)); + OCIO_CHECK_ASSERT(op1->isInverse(op0)); + + OCIO::ConstOpCPURcPtr cpuOp = op0->getCPUOp(false); + const OCIO::OpCPU & c = *cpuOp; + const std::string typeName(typeid(c).name()); + OCIO_CHECK_NE(std::string::npos, StringUtils::Find(typeName, "Renderer_RGB_TO_HSY_LIN")); +} + +OCIO_ADD_TEST(FixedFunctionOps, RGB_TO_HSY_LOG) +{ + OCIO::OpRcPtrVec ops; + + OCIO_CHECK_NO_THROW(OCIO::CreateFixedFunctionOp(ops, OCIO::FixedFunctionOpData::RGB_TO_HSY_LOG, {})); + OCIO_CHECK_NO_THROW(OCIO::CreateFixedFunctionOp(ops, OCIO::FixedFunctionOpData::HSY_LOG_TO_RGB, {})); + + OCIO_CHECK_NO_THROW(ops.finalize()); + OCIO_REQUIRE_EQUAL(ops.size(), 2); + + OCIO::ConstOpRcPtr op0 = ops[0]; + OCIO::ConstOpRcPtr op1 = ops[1]; + + OCIO_CHECK_ASSERT(!op0->isIdentity()); + OCIO_CHECK_ASSERT(!op1->isIdentity()); + + OCIO_CHECK_ASSERT(op0->isSameType(op1)); + OCIO_CHECK_ASSERT(op0->isInverse(op1)); + OCIO_CHECK_ASSERT(op1->isInverse(op0)); + + OCIO::ConstOpCPURcPtr cpuOp = op0->getCPUOp(false); + const OCIO::OpCPU & c = *cpuOp; + const std::string typeName(typeid(c).name()); + OCIO_CHECK_NE(std::string::npos, StringUtils::Find(typeName, "Renderer_RGB_TO_HSY_LOG")); +} + +OCIO_ADD_TEST(FixedFunctionOps, RGB_TO_HSY_VID) +{ + OCIO::OpRcPtrVec ops; + + OCIO_CHECK_NO_THROW(OCIO::CreateFixedFunctionOp(ops, OCIO::FixedFunctionOpData::RGB_TO_HSY_VID, {})); + OCIO_CHECK_NO_THROW(OCIO::CreateFixedFunctionOp(ops, OCIO::FixedFunctionOpData::HSY_VID_TO_RGB, {})); + + OCIO_CHECK_NO_THROW(ops.finalize()); + OCIO_REQUIRE_EQUAL(ops.size(), 2); + + OCIO::ConstOpRcPtr op0 = ops[0]; + OCIO::ConstOpRcPtr op1 = ops[1]; + + OCIO_CHECK_ASSERT(!op0->isIdentity()); + OCIO_CHECK_ASSERT(!op1->isIdentity()); + + OCIO_CHECK_ASSERT(op0->isSameType(op1)); + OCIO_CHECK_ASSERT(op0->isInverse(op1)); + OCIO_CHECK_ASSERT(op1->isInverse(op0)); + + OCIO::ConstOpCPURcPtr cpuOp = op0->getCPUOp(false); + const OCIO::OpCPU & c = *cpuOp; + const std::string typeName(typeid(c).name()); + OCIO_CHECK_NE(std::string::npos, StringUtils::Find(typeName, "Renderer_RGB_TO_HSY_VID")); +} + OCIO_ADD_TEST(FixedFunctionOps, XYZ_TO_xyY) { OCIO::OpRcPtrVec ops; diff --git a/tests/cpu/ops/gradinghuecurve/GradingHueCurveOpCPU_tests.cpp b/tests/cpu/ops/gradinghuecurve/GradingHueCurveOpCPU_tests.cpp new file mode 100644 index 0000000000..5fe9ad4f97 --- /dev/null +++ b/tests/cpu/ops/gradinghuecurve/GradingHueCurveOpCPU_tests.cpp @@ -0,0 +1,434 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + + +#include "ops/gradinghuecurve/GradingHueCurveOpCPU.cpp" + +#include "testutils/UnitTest.h" +#include "utils/StringUtils.h" + +namespace OCIO = OCIO_NAMESPACE; + +namespace +{ +void ValidateImage(const float * expected, const float * res, long numPix, unsigned line) +{ + static constexpr float error = 2e-5f; + + for (long i = 0; i < numPix; ++i) + { + for (long j = 0; j < 4; ++j) + { + if (OCIO::IsNan(expected[i * 4 + j])) + { + OCIO_CHECK_ASSERT(OCIO::IsNan(res[i * 4 + j])); + } + else if (expected[i * 4 + j] != res[i * 4 + j]) + { + OCIO_CHECK_CLOSE_FROM(expected[i * 4 + j], res[i * 4 + j], error, line); + } + } + } +} +} // anon + +OCIO_ADD_TEST(GradingHueCurveOpCPU, identity) +{ + constexpr long numPixels = 9; + const float qnan = std::numeric_limits::quiet_NaN(); + const float inf = std::numeric_limits::infinity(); + const float image[4 * numPixels] = { -0.50f, -0.25f, 0.50f, 0.0f, + 0.75f, 1.00f, 1.25f, 1.0f, + 1.25f, 1.50f, 1.75f, 0.0f, + 0.0f, 0.0f, 0.0f, qnan, + 0.0f, 0.0f, 0.0f, inf, + 0.0f, 0.0f, 0.0f, -inf }; + + const float expected[4 * numPixels] = { -0.50f, -0.25f, 0.50f, 0.0f, + 0.75f, 1.00f, 1.25f, 1.0f, + 1.25f, 1.50f, 1.75f, 0.0f, + 0.0f, 0.0f, 0.0f, qnan, + 0.0f, 0.0f, 0.0f, inf, + 0.0f, 0.0f, 0.0f, -inf }; + + // TODO: Nan/Inf handling + + float res[4 * numPixels]{ 0.f }; + + auto gc = std::make_shared(OCIO::GRADING_LIN); + OCIO::ConstOpCPURcPtr op; + OCIO::ConstGradingHueCurveOpDataRcPtr gcc = gc; + OCIO_CHECK_NO_THROW(op = OCIO::GetGradingHueCurveCPURenderer(gcc)); + OCIO_CHECK_ASSERT(op); + + // Check that the right OpCPU is created. Check that class name contains CurveFwdOp. + { + const OCIO::OpCPU & c = *op; + std::string typeName = typeid(c).name(); + OCIO_CHECK_NE(std::string::npos, StringUtils::Find(typeName, "CurveFwdOp")); + } + OCIO_CHECK_NO_THROW(op->apply(image, res, numPixels)); + ValidateImage(expected, res, numPixels, __LINE__); + + gc->setDirection(OCIO::TRANSFORM_DIR_INVERSE); + OCIO_CHECK_NO_THROW(op = OCIO::GetGradingHueCurveCPURenderer(gcc)); + OCIO_CHECK_ASSERT(op); + + // Check that the right OpCPU is created. Check that class name contains CurveRevOp. + { + const OCIO::OpCPU & c = *op; + std::string typeName = typeid(c).name(); + OCIO_CHECK_NE(std::string::npos, StringUtils::Find(typeName, "CurveRevOp")); + } + OCIO_CHECK_NO_THROW(op->apply(image, res, numPixels)); + ValidateImage(expected, res, numPixels, __LINE__); +} + +OCIO_ADD_TEST(GradingHueCurveOpCPU, log_identity) +{ + // Identity curves (for log or video) that are different from the default curves. + auto hh = OCIO::GradingBSplineCurve::Create( + { {0.f, 0.f}, {0.1f, 0.1f}, {0.2f, 0.2f}, {0.4f, 0.4f}, {0.6f, 0.6f}, {0.8f, 0.8f} }, + OCIO::HUE_HUE); + auto hs = OCIO::GradingBSplineCurve::Create( + { {0.f, 1.f}, {0.1f, 1.f}, {0.2f, 1.f}, {0.4f, 1.f}, {0.6f, 1.f}, {0.8f, 1.f} }, + OCIO::HUE_SAT); + auto hl = OCIO::GradingBSplineCurve::Create( + { {0.f, 1.f}, {0.1f, 1.f}, {0.2f, 1.f}, {0.4f, 1.f}, {0.6f, 1.f}, {0.8f, 1.f} }, + OCIO::HUE_LUM); + auto ls = OCIO::GradingBSplineCurve::Create( + { {0.f, 1.f}, {1.f, 1.f} }, + OCIO::LUM_SAT); + auto ss = OCIO::GradingBSplineCurve::Create( + { {0.f, 0.f}, {0.25f, 0.25f}, {1.f, 1.f} }, + OCIO::SAT_SAT); + auto ll = OCIO::GradingBSplineCurve::Create( + { {0.f, 0.f}, {0.25f, 0.25f}, {0.5f, 0.5f}, {1.f, 1.f} }, + OCIO::LUM_LUM); + auto sl = OCIO::GradingBSplineCurve::Create( + { {0.f, 1.f}, {0.25f, 1.f}, {0.5f, 1.f}, {1.f, 1.f} }, + OCIO::SAT_LUM); + auto hfx = OCIO::GradingBSplineCurve::Create( + { {0.f, 0.f}, {0.1f, 0.f}, {0.2f, 0.f}, {0.4f, 0.f}, {0.6f, 0.f}, {0.8f, 0.f} }, + OCIO::HUE_FX); + + auto gc = std::make_shared(OCIO::GRADING_LOG, + hh, hs, hl, ls, ss, ll, sl, hfx); + OCIO::ConstOpCPURcPtr op; + OCIO::ConstGradingHueCurveOpDataRcPtr gcc = gc; + OCIO_CHECK_NO_THROW(op = OCIO::GetGradingHueCurveCPURenderer(gcc)); + OCIO_CHECK_ASSERT(op); + + constexpr long num_samples = 2; + float res[4 * num_samples]{ 0.f }; + + constexpr float input_32f[] = { + -0.2f, 0.2f, 0.5f, 0.0f, + 0.8f, 1.0f, 2.0f, 0.5f }; + + constexpr float expected_32f[] = { + -0.2f, 0.2f, 0.5f, 0.0f, + 0.8f, 1.0f, 2.0f, 0.5f }; + + // Test in forward direction. + + OCIO_CHECK_NO_THROW(op->apply(input_32f, res, num_samples)); + ValidateImage(expected_32f, res, num_samples, __LINE__); + + // Test in inverse direction. + + gc->setDirection(OCIO::TRANSFORM_DIR_INVERSE); + + OCIO_CHECK_NO_THROW(op = OCIO::GetGradingHueCurveCPURenderer(gcc)); + OCIO_CHECK_ASSERT(op); + OCIO_CHECK_NO_THROW(op->apply(expected_32f, res, num_samples)); + ValidateImage(input_32f, res, num_samples, __LINE__); +} + +OCIO_ADD_TEST(GradingHueCurveOpCPU, hh_hfx_curves) +{ + // Validate that the hue curves round-trip. + + // Identity curves. + auto hs = OCIO::GradingBSplineCurve::Create({ {0.f, 1.f}, {0.9f, 1.f} }, OCIO::HUE_SAT); + auto hl = OCIO::GradingBSplineCurve::Create({ {0.f, 1.f}, {0.9f, 1.f} }, OCIO::HUE_LUM); + auto ls = OCIO::GradingBSplineCurve::Create({ {0.f, 1.f}, {0.9f, 1.f} }, OCIO::LUM_SAT); + auto ss = OCIO::GradingBSplineCurve::Create({ {0.f, 0.f}, {0.9f, 0.9f} }, OCIO::SAT_SAT); + auto ll = OCIO::GradingBSplineCurve::Create({ {0.f, 0.f}, {0.9f, 0.9f} }, OCIO::LUM_LUM); + auto sl = OCIO::GradingBSplineCurve::Create({ {0.f, 1.f}, {0.9f, 1.f} }, OCIO::SAT_LUM); + + // Set hh and hfx to non-identities. + auto hh = OCIO::GradingBSplineCurve::Create( + { {0.05f, 0.15f}, {0.2f, 0.3f}, {0.35f, 0.4f}, {0.45f, 0.45f}, {0.6f, 0.7f}, {0.8f, 0.85f} }, + OCIO::HUE_HUE); + auto hfx = OCIO::GradingBSplineCurve::Create( + { {0.2f, 0.05f}, {0.4f, -0.09f}, {0.6f, -0.2f}, { 0.8f, 0.05f}, {0.99f, -0.02f} }, + OCIO::HUE_FX); + + auto gc = std::make_shared(OCIO::GRADING_LOG, + hh, hs, hl, ls, ss, ll, sl, hfx); + OCIO::ConstOpCPURcPtr op; + OCIO::ConstGradingHueCurveOpDataRcPtr gcc = gc; + OCIO_CHECK_NO_THROW(op = OCIO::GetGradingHueCurveCPURenderer(gcc)); + OCIO_CHECK_ASSERT(op); + + constexpr long num_samples = 4; + float res[4 * num_samples]{ 0.f }; + + const float input_32f[] = { + 0.1f, 0.5f, 0.7f, 0.0f, + 0.6f, 0.9f, 0.8f, 0.5f, + 0.4f, 0.35f, 0.3f, 0.f, + 0.0f, 0.0f, 0.0f, 1.0f + }; + + const float expected_32f[] = { + 0.3984785676f, 0.3790940642f, 1.0187726020f, 0.0f, + 0.6117081642f, 0.8883015513f, 0.8814064860f, 0.5f, + 0.3847683966f, 0.3567464352f, 0.2780219615f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }; + + // Test in forward direction. + + OCIO_CHECK_NO_THROW(op->apply(input_32f, res, num_samples)); + ValidateImage(expected_32f, res, num_samples, __LINE__); + + // Test in inverse direction. + + gc->setDirection(OCIO::TRANSFORM_DIR_INVERSE); + + OCIO_CHECK_NO_THROW(op = OCIO::GetGradingHueCurveCPURenderer(gcc)); + OCIO_CHECK_ASSERT(op); + OCIO_CHECK_NO_THROW(op->apply(res, res, num_samples)); + ValidateImage(input_32f, res, num_samples, __LINE__); +} + +OCIO_ADD_TEST(GradingHueCurveOpCPU_1, log_all_curves) +{ + // All curves are non-identities. + auto hh = OCIO::GradingBSplineCurve::Create( + { {0.05f, 0.15f}, {0.2f, 0.3f}, {0.35f, 0.4f}, {0.45f, 0.45f}, {0.6f, 0.7f}, {0.8f, 0.85f} }, + OCIO::HUE_HUE); + auto hs = OCIO::GradingBSplineCurve::Create( + { {-0.1f, 1.2f}, {0.2f, 0.7f}, {0.4f, 1.5f}, {0.5f, 0.5f}, {0.6f, 1.4f}, {0.8f, 0.7f} }, + OCIO::HUE_SAT); + auto hl = OCIO::GradingBSplineCurve::Create( + { {0.1f, 1.5f}, {0.2f, 0.7f}, {0.4f, 1.4f}, {0.5f, 0.8f}, {0.8f, 0.5f} }, + OCIO::HUE_LUM); + auto ls = OCIO::GradingBSplineCurve::Create( + { {0.05f, 1.5f}, {0.5f, 0.9f}, {1.1f, 1.4f} }, + OCIO::LUM_SAT); + auto ss = OCIO::GradingBSplineCurve::Create( + { {0.f, 0.1f}, {0.5f, 0.45f}, {1.f, 1.1f} }, + OCIO::SAT_SAT); + auto ll = OCIO::GradingBSplineCurve::Create( + { {-0.02f, -0.04f}, {0.2f, 0.1f}, {0.8f, 0.95f}, {1.1f, 1.2f} }, + OCIO::LUM_LUM); + auto sl = OCIO::GradingBSplineCurve::Create( + { {0.f, 1.2f}, {0.6f, 0.8f}, {0.9f, 1.1f} }, + OCIO::SAT_LUM); + auto hfx = OCIO::GradingBSplineCurve::Create( + { {0.2f, 0.05f}, {0.4f, -0.09f}, {0.6f, -0.2f}, { 0.8f, 0.05f}, {0.99f, -0.02f} }, + OCIO::HUE_FX); + + auto gc = std::make_shared(OCIO::GRADING_LOG, + hh, hs, hl, ls, ss, ll, sl, hfx); + OCIO::ConstOpCPURcPtr op; + OCIO::ConstGradingHueCurveOpDataRcPtr gcc = gc; + OCIO_CHECK_NO_THROW(op = OCIO::GetGradingHueCurveCPURenderer(gcc)); + OCIO_CHECK_ASSERT(op); + + constexpr long num_samples = 5; + float res[4 * num_samples]{ 0.f }; + + const float input_32f[] = { + 0.1f, 0.5f, 0.7f, 0.0f, + 0.6f, 0.9f, 0.8f, 0.5f, + 0.4f, 0.35f,0.3f, 0.f, + 0.4f,-0.2f,-0.05f, 0.f, + 0.0f, 0.0f, 0.0f, 1.0f + }; + + const float expected_32f[] = { + 0.651269494808f, 0.630018105394f, 1.331314732772f, 0.0f, + 0.787401154155f, 1.286561695129f, 1.274118545611f, 0.5f, + 0.317389674917f, 0.297787779440f, 0.242718507572f, 0.0f, + 0.830653473122f, 0.449246419743f, -0.173027078802f, 0.0f, + 0.004989255546f, -0.033773428950f, -0.019725339077f, 1.0f + }; + + // Test in forward direction. + + OCIO_CHECK_NO_THROW(op->apply(input_32f, res, num_samples)); + ValidateImage(expected_32f, res, num_samples, __LINE__); + + // Test in inverse direction. + + gc->setDirection(OCIO::TRANSFORM_DIR_INVERSE); + + OCIO_CHECK_NO_THROW(op = OCIO::GetGradingHueCurveCPURenderer(gcc)); + OCIO_CHECK_ASSERT(op); + OCIO_CHECK_NO_THROW(op->apply(res, res, num_samples)); + ValidateImage(input_32f, res, num_samples, __LINE__); +} + +OCIO_ADD_TEST(GradingHueCurveOpCPU, lin_all_curves) +{ + // All curves are non-identities. + auto hh = OCIO::GradingBSplineCurve::Create( + { {0.05f, 0.15f}, {0.2f, 0.3f}, {0.35f, 0.4f}, {0.45f, 0.45f}, {0.6f, 0.7f}, {0.8f, 0.85f} }, + OCIO::HUE_HUE); + auto hs = OCIO::GradingBSplineCurve::Create( + { {-0.1f, 1.2f}, {0.2f, 0.7f}, {0.4f, 1.5f}, {0.5f, 0.5f}, {0.6f, 1.4f}, {0.8f, 0.7f} }, + OCIO::HUE_SAT); + auto hl = OCIO::GradingBSplineCurve::Create( + { {0.1f, 1.5f}, {0.2f, 0.7f}, {0.4f, 1.4f}, {0.5f, 0.8f}, {0.8f, 0.5f} }, + OCIO::HUE_LUM); + auto ss = OCIO::GradingBSplineCurve::Create( + { {0.f, 0.1f}, {0.5f, 0.45f}, {1.f, 1.1f} }, + OCIO::SAT_SAT); + auto sl = OCIO::GradingBSplineCurve::Create( + { {0.f, 1.2f}, {0.6f, 0.8f}, {0.9f, 1.1f} }, + OCIO::SAT_LUM); + auto hfx = OCIO::GradingBSplineCurve::Create( + { {0.2f, 0.05f}, {0.4f, -0.09f}, {0.6f, -0.2f}, { 0.8f, 0.05f}, {0.99f, -0.02f} }, + OCIO::HUE_FX); + // Adjust these two, relative to previous test, to work in f-stops. + auto ls = OCIO::GradingBSplineCurve::Create( + { {-6.f, 0.9f}, {-3.f, 0.8f}, {0.f, 1.2f}, {2.f, 1.f}, {4.f, 0.6f}, {6.f, 0.55f} }, + OCIO::LUM_SAT); + auto ll = OCIO::GradingBSplineCurve::Create( + { {-8.f, -7.f}, {-2.f, -3.f}, {2.f, 3.5f}, {8.f, 7.f} }, + OCIO::LUM_LUM); + + auto gc = std::make_shared(OCIO::GRADING_LIN, + hh, hs, hl, ls, ss, ll, sl, hfx); + OCIO::ConstOpCPURcPtr op; + OCIO::ConstGradingHueCurveOpDataRcPtr gcc = gc; + OCIO_CHECK_NO_THROW(op = OCIO::GetGradingHueCurveCPURenderer(gcc)); + OCIO_CHECK_ASSERT(op); + + constexpr long num_samples = 5; + float res[4 * num_samples]{ 0.f }; + + const float input_32f[] = { + 0.1f, 0.5f, 0.7f, 0.0f, + 0.6f, 0.9f, 0.8f, 0.5f, + 2.4f, 2.35f, 2.3f, 0.f, + 0.4f, 0.2f, -0.05f, 0.f, + 0.0f, 0.0f, 0.0f, 1.0f + }; + + const float expected_32f[] = { + 0.527229344453f, 0.490778616791f, 1.693653961874f, 0.f, + 1.253512415394f, 2.240034381083f, 2.215442212203f, 0.5f, + 6.983003751281f, 6.772174817271f, 6.179875164501f, 0.f, + 0.527554073346f, 0.360480028655f, -0.135388205576f, 0.f, + 0.011308048228f, -0.001711436982f, 0.003006990049f, 1.f + }; + + // Test in forward direction. + + OCIO_CHECK_NO_THROW(op->apply(input_32f, res, num_samples)); + ValidateImage(expected_32f, res, num_samples, __LINE__); + + // Test in inverse direction. + + gc->setDirection(OCIO::TRANSFORM_DIR_INVERSE); + + OCIO_CHECK_NO_THROW(op = OCIO::GetGradingHueCurveCPURenderer(gcc)); + OCIO_CHECK_ASSERT(op); + OCIO_CHECK_NO_THROW(op->apply(res, res, num_samples)); + ValidateImage(input_32f, res, num_samples, __LINE__); +} + +OCIO_ADD_TEST(GradingHueCurveOpCPU, draw_curve_only) +{ + auto gc = std::make_shared(OCIO::GRADING_LOG); + + auto val = gc->getValue()->createEditableCopy(); + auto spline = val->getCurve(OCIO::HUE_SAT); + spline->getControlPoint(1) = OCIO::GradingControlPoint(0.15f, 1.4f); + OCIO_CHECK_ASSERT(!val->isIdentity()); + + // Enable drawCurveOnly mode. This should only evaluate the HUE-SAT spline for + // use in a user interface. + val->setDrawCurveOnly(true); + gc->setValue(val); + + OCIO::ConstGradingHueCurveOpDataRcPtr gcc = gc; + OCIO::ConstOpCPURcPtr op; + OCIO_CHECK_NO_THROW(op = OCIO::GetGradingHueCurveCPURenderer(gcc)); + OCIO_CHECK_ASSERT(op); + + constexpr long num_samples = 2; + float res[4 * num_samples]{ 0.f }; + + constexpr float input_32f[] = { + -0.2f, 0.15f, 0.15f, 0.0f, + 0.15f, 1.0f, 2.0f, 0.5f }; + + constexpr float expected_32f[] = { + 1.0f, 1.4f, 1.4f, 0.0f, + 1.4f, 1.0f, 1.0f, 0.5f }; + + // Test in forward direction. + + OCIO_CHECK_NO_THROW(op->apply(input_32f, res, num_samples)); + ValidateImage(expected_32f, res, num_samples, __LINE__); + + // Test in inverse direction, which should be the same as the forward direction + // since the direction is ignored for DrawCurveOnly mode. + + gc->setDirection(OCIO::TRANSFORM_DIR_INVERSE); + + OCIO_CHECK_NO_THROW(op = OCIO::GetGradingHueCurveCPURenderer(gcc)); + OCIO_CHECK_ASSERT(op); + OCIO_CHECK_NO_THROW(op->apply(input_32f, res, num_samples)); + ValidateImage(expected_32f, res, num_samples, __LINE__); +} + +OCIO_ADD_TEST(GradingHueCurveOpCPU, bypass_rgbtohsy) +{ + auto gc = std::make_shared(OCIO::GRADING_LOG); + + gc->setRGBToHSY(OCIO::HSY_TRANSFORM_NONE); + + auto val = gc->getValue()->createEditableCopy(); + auto spline = val->getCurve(OCIO::SAT_SAT); + spline->getControlPoint(1) = OCIO::GradingControlPoint(0.4f, 0.8f); + OCIO_CHECK_ASSERT(!val->isIdentity()); + gc->setValue(val); + + OCIO::ConstGradingHueCurveOpDataRcPtr gcc = gc; + OCIO::ConstOpCPURcPtr op; + OCIO_CHECK_NO_THROW(op = OCIO::GetGradingHueCurveCPURenderer(gcc)); + OCIO_CHECK_ASSERT(op); + + constexpr long num_samples = 2; + float res[4 * num_samples]{ 0.f }; + + constexpr float input_32f[] = { + 0.2f, 0.2f, -0.1f, 0.0f, + 0.1f, 0.4f, 2.0f, 0.5f }; + + // Only the green channel gets processed, using the sat-sat curve. + constexpr float expected_32f[] = { + 0.2f, 0.4475418f, -0.1f, 0.0f, + 0.1f, 0.8000000f, 2.0f, 0.5f }; + + // Test in forward direction. + + OCIO_CHECK_NO_THROW(op->apply(input_32f, res, num_samples)); + ValidateImage(expected_32f, res, num_samples, __LINE__); + + // Test in inverse direction. + + gc->setDirection(OCIO::TRANSFORM_DIR_INVERSE); + + OCIO_CHECK_NO_THROW(op = OCIO::GetGradingHueCurveCPURenderer(gcc)); + OCIO_CHECK_ASSERT(op); + OCIO_CHECK_NO_THROW(op->apply(res, res, num_samples)); + ValidateImage(input_32f, res, num_samples, __LINE__); +} diff --git a/tests/cpu/ops/gradinghuecurve/GradingHueCurveOpData_tests.cpp b/tests/cpu/ops/gradinghuecurve/GradingHueCurveOpData_tests.cpp new file mode 100644 index 0000000000..6aeabd0260 --- /dev/null +++ b/tests/cpu/ops/gradinghuecurve/GradingHueCurveOpData_tests.cpp @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + + +#include "ops/gradinghuecurve/GradingHueCurveOpData.cpp" + +#include "testutils/UnitTest.h" + +namespace OCIO = OCIO_NAMESPACE; + +OCIO_ADD_TEST(GradingHueCurveOpData, accessors) +{ + // Create GradingHueCurveOpData and check values. Change them and check. + + OCIO::GradingHueCurveOpData gc( OCIO::GRADING_LOG ); + + static constexpr char expected[]{ "log forward " + "]>, " + "hue_sat=]>, " + "hue_lum=]>, " + "lum_sat=]>, " + "sat_sat=]>, " + "lum_lum=]>, " + "sat_lum=]>, " + "hue_fx=]>>" }; + OCIO_CHECK_EQUAL(gc.getCacheID(), expected); + + OCIO_CHECK_EQUAL(gc.getStyle(), OCIO::GRADING_LOG); + auto curves = gc.getValue(); + OCIO_REQUIRE_ASSERT(curves); + OCIO_CHECK_ASSERT(curves->isIdentity()); + OCIO_CHECK_ASSERT(gc.isIdentity()); + OCIO_CHECK_ASSERT(gc.isNoOp()); + OCIO_CHECK_ASSERT(gc.hasChannelCrosstalk()); + OCIO_CHECK_EQUAL(gc.getRGBToHSY(), OCIO::HSY_TRANSFORM_1); + + gc.setStyle(OCIO::GRADING_LIN); + OCIO_CHECK_EQUAL(gc.getStyle(), OCIO::GRADING_LIN); + gc.setRGBToHSY(OCIO::HSY_TRANSFORM_NONE); + OCIO_CHECK_EQUAL(gc.getRGBToHSY(), OCIO::HSY_TRANSFORM_NONE); + + // Get dynamic property as a generic dynamic property and as a typed one and verify they are + // the same and can be made dynamic. + + OCIO_CHECK_ASSERT(!gc.isDynamic()); + auto dp = gc.getDynamicProperty(); + OCIO_REQUIRE_ASSERT(dp); + OCIO_CHECK_EQUAL(dp->getType(), OCIO::DYNAMIC_PROPERTY_GRADING_HUECURVE); + auto dpImpl = gc.getDynamicPropertyInternal(); + OCIO_REQUIRE_ASSERT(dpImpl); + OCIO_CHECK_ASSERT(dp == dpImpl); + OCIO_CHECK_ASSERT(!dpImpl->isDynamic()); + dpImpl->makeDynamic(); + OCIO_CHECK_ASSERT(gc.isDynamic()); + + OCIO_CHECK_EQUAL(gc.getDirection(), OCIO::TRANSFORM_DIR_FORWARD); + gc.setDirection(OCIO::TRANSFORM_DIR_INVERSE); + OCIO_CHECK_EQUAL(gc.getDirection(), OCIO::TRANSFORM_DIR_INVERSE); + + // Test operator==. + + OCIO::GradingHueCurveOpData gc1{ OCIO::GRADING_LIN }; + OCIO::GradingHueCurveOpData gc2{ OCIO::GRADING_LIN }; + + OCIO_CHECK_ASSERT(gc1 == gc2); + gc1.setDirection(OCIO::TRANSFORM_DIR_INVERSE); + OCIO_CHECK_ASSERT(!(gc1 == gc2)); + gc2.setDirection(OCIO::TRANSFORM_DIR_INVERSE); + OCIO_CHECK_ASSERT(gc1 == gc2); + + gc1.setStyle(OCIO::GRADING_LOG); + OCIO_CHECK_ASSERT(!(gc1 == gc2)); + gc2.setStyle(OCIO::GRADING_LOG); + OCIO_CHECK_ASSERT(gc1 == gc2); + + auto v1 = gc1.getValue()->createEditableCopy(); + auto hueHue = v1->getCurve(OCIO::HUE_HUE); + hueHue->setNumControlPoints(4); + hueHue->getControlPoint(3).m_x = hueHue->getControlPoint(2).m_x + 0.25f; + hueHue->getControlPoint(3).m_y = hueHue->getControlPoint(2).m_y + 0.5f; + gc1.setValue(v1); + OCIO_CHECK_ASSERT(!(gc1 == gc2)); + auto v2 = gc2.getValue()->createEditableCopy(); + auto hueHue2 = v2->getCurve(OCIO::HUE_HUE); + hueHue2->setNumControlPoints(4); + hueHue2->getControlPoint(3).m_x = hueHue2->getControlPoint(2).m_x + 0.25f; + hueHue2->getControlPoint(3).m_y = hueHue2->getControlPoint(2).m_y + 0.5f; + gc2.setValue(v2); + OCIO_CHECK_ASSERT(gc1 == gc2); + + gc1.setSlope(OCIO::HUE_SAT, 2, 0.9f); + OCIO_CHECK_ASSERT(!(gc1 == gc2)); + OCIO_CHECK_EQUAL(gc1.getSlope(OCIO::HUE_SAT, 2), 0.9f); + OCIO_CHECK_ASSERT(gc1.slopesAreDefault(OCIO::HUE_LUM)); + OCIO_CHECK_ASSERT(!gc1.slopesAreDefault(OCIO::HUE_SAT)); + + OCIO_CHECK_EQUAL(gc1.isIdentity(), false); + OCIO_CHECK_ASSERT(gc1.hasChannelCrosstalk()); + + // Check isInverse. + + // Make two equal non-identity ops, invert one. + OCIO::GradingHueCurveOpData gc3{ OCIO::GRADING_LIN }; + auto v3 = gc3.getValue()->createEditableCopy(); + auto spline = v3->getCurve(OCIO::HUE_LUM); + spline->setNumControlPoints(2); + spline->getControlPoint(0) = OCIO::GradingControlPoint(0.f, 2.f); + spline->getControlPoint(1) = OCIO::GradingControlPoint(0.9f, 2.f); + gc3.setValue(v3); + OCIO_CHECK_ASSERT(!gc3.isIdentity()); + // Need a shared pointer for the parameter. + OCIO::ConstGradingHueCurveOpDataRcPtr gcptr3 = gc3.clone(); + gc3.setDirection(OCIO::TRANSFORM_DIR_INVERSE); + // They start as inverses. + OCIO_CHECK_ASSERT(gc3.isInverse(gcptr3)); + + // Change value of one: no longer an inverse. + spline->getControlPoint(1).m_y += 0.25f; + gc3.setValue(v3); + OCIO_CHECK_ASSERT(!gc3.isInverse(gcptr3)); + // Restore value. + spline->getControlPoint(1).m_y -= 0.25f; + gc3.setValue(v3); + OCIO_CHECK_ASSERT(gc3.isInverse(gcptr3)); + + // Change slope of one: no longer an inverse. + gc3.setSlope(OCIO::HUE_SAT, 2, 0.9f); + OCIO_CHECK_ASSERT(!gc3.isInverse(gcptr3)); + // Restore value. + gc3.setSlope(OCIO::HUE_SAT, 2, 0.f); + OCIO_CHECK_ASSERT(gc3.isInverse(gcptr3)); + + // Change direction: no longer an inverse. + gc3.setDirection(OCIO::TRANSFORM_DIR_FORWARD); + OCIO_CHECK_ASSERT(!gc3.isInverse(gcptr3)); +} + +OCIO_ADD_TEST(GradingHueCurveOpData, validate) +{ + // Default is valid. + OCIO::GradingHueCurveOpData gc{ OCIO::GRADING_LOG }; + OCIO_CHECK_NO_THROW(gc.validate()); + + // Curves with a single control point are not valid. + auto curve = OCIO::GradingBSplineCurve::Create(1); + OCIO_CHECK_THROW_WHAT(auto curves = OCIO::GradingHueCurve::Create( + curve, curve, curve, curve, curve, curve, curve, curve), + OCIO::Exception, "There must be at least 2 control points."); + + // A periodic curve may not have only two control points that wrap to the same point. + curve = OCIO::GradingBSplineCurve::Create({ { 0.f,0.f },{ 1.f,0.f } }, OCIO::HUE_FX); + OCIO_CHECK_THROW_WHAT(auto curves = OCIO::GradingHueCurve::Create( + curve, curve, curve, curve, curve, curve, curve, curve), + OCIO::Exception, "The periodic spline x coordinates may not wrap to the same value."); + + // Curve x coordinates have to increase. + curve = OCIO::GradingBSplineCurve::Create({ { 0.f,0.f },{ 0.7f,0.3f }, + { 0.5f,0.7f },{ 1.f,1.f } }); + OCIO_CHECK_THROW_WHAT(auto curves = OCIO::GradingHueCurve::Create( + curve, curve, curve, curve, curve, curve, curve, curve), + OCIO::Exception, "has a x coordinate '0.5' that is less than previous control " + "point x coordinate '0.7'."); + + // A hue-hue curve must have x-coordinates in [0,1]. + curve = OCIO::GradingBSplineCurve::Create({ { 0.1f,0.05f },{ 1.1f,1.05f } }, OCIO::HUE_HUE); + OCIO_CHECK_THROW_WHAT(auto curves = OCIO::GradingHueCurve::Create( + curve, curve, curve, curve, curve, curve, curve, curve), + OCIO::Exception, "The HUE-HUE spline may not have x coordinates greater than one."); + + // Fix the curve x coordinate. + curve->getControlPoint(1).m_x = 1.f; + // But the curves are not all of the correct BSplineType. + OCIO_CHECK_THROW_WHAT(auto curves = OCIO::GradingHueCurve::Create( + curve, curve, curve, curve, curve, curve, curve, curve), + OCIO::Exception, "GradingHueCurve validation failed: 'hue_sat' curve is of the wrong BSplineType."); + + // Curve y coordinates have to increase. + curve = OCIO::GradingBSplineCurve::Create({ { 0.f,0.f },{ 0.3f,0.3f }, + { 0.5f,0.27f },{ 1.f,1.f } }); + OCIO_CHECK_THROW_WHAT(auto curves = OCIO::GradingHueCurve::Create( + curve, curve, curve, curve, curve, curve, curve, curve), + OCIO::Exception, "point at index 2 has a y coordinate '0.27' that is less than " + "previous control point y coordinate '0.3'."); + + // Curve y coordinates have to increase. For hue-hue this includes comparing the wrapped last + // point to the first point. In this case, 1.1 at the end is equivalent to 0.1 at the start. + curve = OCIO::GradingBSplineCurve::Create({ { 0.1f,0.05f },{ 1.f,1.1f } }, OCIO::HUE_HUE); + OCIO_CHECK_THROW_WHAT(auto curves = OCIO::GradingHueCurve::Create( + curve, curve, curve, curve, curve, curve, curve, curve), + OCIO::Exception, "Control point at index 0 has a y coordinate '0.05' that is less than " + "previous control point y coordinate '0.1'."); +} diff --git a/tests/cpu/ops/gradinghuecurve/GradingHueCurveOp_tests.cpp b/tests/cpu/ops/gradinghuecurve/GradingHueCurveOp_tests.cpp new file mode 100644 index 0000000000..1c6e56661d --- /dev/null +++ b/tests/cpu/ops/gradinghuecurve/GradingHueCurveOp_tests.cpp @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + + +#include "OpBuilders.h" + +#include "ops/gradinghuecurve/GradingHueCurveOp.cpp" + +#include "testutils/UnitTest.h" + +namespace OCIO = OCIO_NAMESPACE; + +OCIO_ADD_TEST(GradingHueCurveOp, create) +{ + OCIO::TransformDirection direction = OCIO::TRANSFORM_DIR_FORWARD; + OCIO::GradingHueCurveOpDataRcPtr data = + std::make_shared(OCIO::GRADING_LOG); + OCIO::OpRcPtrVec ops; + + OCIO_CHECK_NO_THROW(OCIO::CreateGradingHueCurveOp(ops, data, direction)); + OCIO_REQUIRE_EQUAL(ops.size(), 1); + OCIO_REQUIRE_ASSERT(ops[0]); + OCIO_CHECK_EQUAL(ops[0]->getInfo(), ""); + OCIO_CHECK_ASSERT(ops[0]->isIdentity()); + OCIO_CHECK_ASSERT(ops[0]->isNoOp()); + + data->getDynamicPropertyInternal()->makeDynamic(); + OCIO_CHECK_NO_THROW(OCIO::CreateGradingHueCurveOp(ops, data, direction)); + OCIO_REQUIRE_EQUAL(ops.size(), 2); + OCIO_REQUIRE_ASSERT(ops[1]); + OCIO_CHECK_EQUAL(ops[1]->getInfo(), ""); + OCIO_CHECK_ASSERT(!ops[1]->isIdentity()); + OCIO_CHECK_ASSERT(!ops[1]->isNoOp()); +} + +OCIO_ADD_TEST(GradingHueCurveOp, create_transform) +{ + OCIO::TransformDirection direction = OCIO::TRANSFORM_DIR_FORWARD; + OCIO::GradingHueCurveOpDataRcPtr data = + std::make_shared(OCIO::GRADING_LOG); + data->getDynamicPropertyInternal()->makeDynamic(); + OCIO::OpRcPtrVec ops; + + OCIO_CHECK_NO_THROW(OCIO::CreateGradingHueCurveOp(ops, data, direction)); + OCIO_REQUIRE_EQUAL(ops.size(), 1); + + OCIO::GroupTransformRcPtr group = OCIO::GroupTransform::Create(); + + OCIO::ConstOpRcPtr op(ops[0]); + + OCIO::CreateGradingHueCurveTransform(group, op); + OCIO_REQUIRE_EQUAL(group->getNumTransforms(), 1); + auto transform = group->getTransform(0); + OCIO_REQUIRE_ASSERT(transform); + auto gcTransform = OCIO_DYNAMIC_POINTER_CAST(transform); + OCIO_REQUIRE_ASSERT(gcTransform); + OCIO_CHECK_EQUAL(gcTransform->getStyle(), OCIO::GRADING_LOG); + OCIO_CHECK_ASSERT(gcTransform->isDynamic()); +} + +OCIO_ADD_TEST(GradingHueCurveOp, build_ops) +{ + OCIO::ConstConfigRcPtr config = OCIO::Config::CreateRaw(); + + auto gcTransform = OCIO::GradingHueCurveTransform::Create(OCIO::GRADING_LOG); + OCIO_CHECK_ASSERT(gcTransform.get()); + + // Identity does create an op. + OCIO::OpRcPtrVec ops; + OCIO::BuildOps(ops, *(config.get()), config->getCurrentContext(), gcTransform, + OCIO::TRANSFORM_DIR_FORWARD); + OCIO_REQUIRE_EQUAL(ops.size(), 1); + OCIO_CHECK_ASSERT(ops[0]->isIdentity()); + OCIO_CHECK_ASSERT(ops[0]->isNoOp()); + ops.clear(); + + // Make it dynamic and keep default values. + gcTransform->makeDynamic(); + + OCIO::BuildOps(ops, *(config.get()), config->getCurrentContext(), gcTransform, + OCIO::TRANSFORM_DIR_FORWARD); + + OCIO_REQUIRE_EQUAL(ops.size(), 1); + OCIO_REQUIRE_ASSERT(ops[0]); + OCIO_CHECK_EQUAL(ops[0]->getInfo(), ""); + OCIO::ConstGradingHueCurveOpRcPtr gco = OCIO_DYNAMIC_POINTER_CAST(ops[0]); + auto data = gco->data(); + OCIO_REQUIRE_ASSERT(data); + auto gcd = OCIO_DYNAMIC_POINTER_CAST(data); + OCIO_REQUIRE_ASSERT(gcd); + OCIO_CHECK_ASSERT(gcd->isDynamic()); + + auto valsOp = gcd->getValue(); + OCIO_CHECK_EQUAL(6, valsOp->getCurve(OCIO::HUE_HUE)->getNumControlPoints()); + + // Create processor with dynamic identity before changing the transform. + auto proc = config->getProcessor(gcTransform); + OCIO_CHECK_ASSERT(proc->hasDynamicProperty(OCIO::DYNAMIC_PROPERTY_GRADING_HUECURVE)); + OCIO_CHECK_ASSERT(!proc->hasDynamicProperty(OCIO::DYNAMIC_PROPERTY_EXPOSURE)); + + auto cpu = proc->getDefaultCPUProcessor(); + + // Create a non-identity curve. + auto hueCurve = OCIO::GradingHueCurve::Create(OCIO::GRADING_LOG); + OCIO::GradingBSplineCurveRcPtr spline = hueCurve->getCurve(OCIO::HUE_LUM); + // Add a constant offset to all hues. + spline->setNumControlPoints(2); + spline->getControlPoint(0) = OCIO::GradingControlPoint(0.f, 2.f); + spline->getControlPoint(1) = OCIO::GradingControlPoint(0.9f, 2.f); + OCIO_CHECK_ASSERT(!hueCurve->isIdentity()); + + // Sharing of dynamic properties is done through processor, changing the source transform + // does not change the op that was created from it. + gcTransform->setValue(hueCurve); + valsOp = gcd->getValue(); + // (Still has its original value.) + OCIO_CHECK_EQUAL(1.f, valsOp->getCurve(OCIO::HUE_LUM)->getControlPoint(0).m_y); + + // Get dynamic property from the CPU processor. + OCIO::DynamicPropertyRcPtr dp; + OCIO_CHECK_NO_THROW(dp = cpu->getDynamicProperty(OCIO::DYNAMIC_PROPERTY_GRADING_HUECURVE)); + OCIO_REQUIRE_ASSERT(dp); + // Get typed value accessor. + auto dpgc = OCIO_DYNAMIC_POINTER_CAST(dp); + OCIO_REQUIRE_ASSERT(dpgc); + + float pixel[]{ 0.0f, 0.2f, 2.0f }; + cpu->applyRGB(pixel); + // Default values are identity. + static constexpr float error = 1e-5f; + OCIO_CHECK_CLOSE(pixel[0], 0.0f, error); + OCIO_CHECK_CLOSE(pixel[1], 0.2f, error); + OCIO_CHECK_CLOSE(pixel[2], 2.0f, error); + + // Set the modified curve. + dpgc->setValue(hueCurve); + + // The pixel value is different. + cpu->applyRGB(pixel); + OCIO_CHECK_CLOSE(pixel[0], 0.1f, error); + OCIO_CHECK_CLOSE(pixel[1], 0.3f, error); + OCIO_CHECK_CLOSE(pixel[2], 2.1f, error); +} diff --git a/tests/cpu/ops/gradinghuecurve/GradingHueCurve_tests.cpp b/tests/cpu/ops/gradinghuecurve/GradingHueCurve_tests.cpp new file mode 100644 index 0000000000..621846cf6c --- /dev/null +++ b/tests/cpu/ops/gradinghuecurve/GradingHueCurve_tests.cpp @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + + +#include "DynamicProperty.h" +#include "ops/gradinghuecurve/GradingHueCurve.cpp" + +#include "testutils/UnitTest.h" + +namespace OCIO = OCIO_NAMESPACE; + +OCIO_ADD_TEST(GradingHueCurve, basic) +{ + auto curve = OCIO::GradingBSplineCurve::Create({ { 0.f,0.f },{ 0.2f,0.2f }, + { 0.5f,0.7f },{ 1.f,1.f } }, OCIO::HUE_HUE); + OCIO::ConstGradingBSplineCurveRcPtr curveHH = curve; + OCIO_CHECK_EQUAL(0.2f, curveHH->getControlPoint(1).m_y); + curve->getControlPoint(1).m_y = 0.3f; + OCIO_CHECK_EQUAL(0.3f, curveHH->getControlPoint(1).m_y); + OCIO::ConstGradingBSplineCurveRcPtr curveHS = OCIO::GradingBSplineCurve::Create(4, OCIO::HUE_SAT); + OCIO::ConstGradingBSplineCurveRcPtr curveHL = OCIO::GradingBSplineCurve::Create(3, OCIO::HUE_LUM); + OCIO::ConstGradingBSplineCurveRcPtr curveLS = OCIO::GradingBSplineCurve::Create(2, OCIO::LUM_SAT); + OCIO::ConstGradingBSplineCurveRcPtr curveSS = OCIO::GradingBSplineCurve::Create(2, OCIO::SAT_SAT); + OCIO::ConstGradingBSplineCurveRcPtr curveLL = OCIO::GradingBSplineCurve::Create(2, OCIO::LUM_LUM); + OCIO::ConstGradingBSplineCurveRcPtr curveSL = OCIO::GradingBSplineCurve::Create(2, OCIO::SAT_LUM); + OCIO::ConstGradingBSplineCurveRcPtr curveHFX = OCIO::GradingBSplineCurve::Create(2, OCIO::HUE_FX); + + // The Create function takes 8 pointers to curves and creates new curves that are copies of + // the 8 parameters. First try to create one using curveHH instead of curveSS. + OCIO_CHECK_THROW_WHAT(auto badHueCurve = OCIO::GradingHueCurve::Create(curveHH, curveHS, curveHL, curveLS, + curveHH, curveLL, curveSL, curveHFX), + OCIO::Exception, + "GradingHueCurve validation failed: 'sat_sat' curve is of the wrong BSplineType."); + + // Now create a valid one. + auto hueCurve = OCIO::GradingHueCurve::Create(curveHH, curveHS, curveHL, curveLS, + curveSS, curveLL, curveSL, curveHFX); + OCIO_REQUIRE_ASSERT(hueCurve); + OCIO_CHECK_NO_THROW(hueCurve->validate()); + + OCIO_REQUIRE_ASSERT(hueCurve->getCurve(OCIO::HUE_HUE)); + OCIO_REQUIRE_ASSERT(hueCurve->getCurve(OCIO::HUE_SAT)); + OCIO_REQUIRE_ASSERT(hueCurve->getCurve(OCIO::HUE_LUM)); + OCIO_REQUIRE_ASSERT(hueCurve->getCurve(OCIO::LUM_SAT)); + OCIO_REQUIRE_ASSERT(hueCurve->getCurve(OCIO::SAT_SAT)); + OCIO_REQUIRE_ASSERT(hueCurve->getCurve(OCIO::LUM_LUM)); + OCIO_REQUIRE_ASSERT(hueCurve->getCurve(OCIO::SAT_LUM)); + OCIO_REQUIRE_ASSERT(hueCurve->getCurve(OCIO::HUE_FX)); + OCIO_CHECK_THROW_WHAT(hueCurve->getCurve(OCIO::HUE_NUM_CURVES), OCIO::Exception, + "The HueCurveType provided is illegal"); + + // Validate that the Create function made copies of its curve arguments. + OCIO::GradingBSplineCurveRcPtr copiedCurve = hueCurve->getCurve(OCIO::HUE_HUE); + OCIO_CHECK_EQUAL(0.3f, copiedCurve->getControlPoint(1).m_y); + curve->getControlPoint(1).m_y = 0.4f; + OCIO_CHECK_EQUAL(0.4f, curve->getControlPoint(1).m_y); + OCIO_CHECK_EQUAL(0.3f, copiedCurve->getControlPoint(1).m_y); + + // Set the type to the wrong BSpline type and re-validate. + copiedCurve->setSplineType(OCIO::DIAGONAL_B_SPLINE); + OCIO_CHECK_THROW_WHAT(hueCurve->validate(), OCIO::Exception, + "GradingHueCurve validation failed: 'hue_hue' curve is of the wrong BSplineType."); + // Turn on drawCurveOnly mode and verify that any spline type is now allowed. + hueCurve->setDrawCurveOnly(true); + OCIO_CHECK_NO_THROW(hueCurve->validate()); + + // Test default curves. + auto hueCurveLin = OCIO::GradingHueCurve::Create(OCIO::GRADING_LIN); + auto hueCurveLog = OCIO::GradingHueCurve::Create(OCIO::GRADING_LOG); + auto hueCurveVideo = OCIO::GradingHueCurve::Create(OCIO::GRADING_VIDEO); + OCIO_REQUIRE_ASSERT(hueCurveLin && hueCurveLog && hueCurveVideo); + OCIO_CHECK_ASSERT(*hueCurveLog == *hueCurveVideo); + OCIO_CHECK_ASSERT(*hueCurveLog != *hueCurveLin); + + OCIO_CHECK_ASSERT(*hueCurveLog->getCurve(OCIO::HUE_LUM) == + *hueCurveLog->getCurve(OCIO::HUE_SAT)); + OCIO_CHECK_ASSERT(*hueCurveLog->getCurve(OCIO::SAT_SAT) == + *hueCurveLog->getCurve(OCIO::LUM_LUM)); + OCIO_CHECK_ASSERT(*hueCurveLog->getCurve(OCIO::LUM_SAT) == + *hueCurveLog->getCurve(OCIO::SAT_LUM)); + OCIO_CHECK_ASSERT(*hueCurveLog->getCurve(OCIO::HUE_HUE) != + *hueCurveLog->getCurve(OCIO::HUE_SAT)); + OCIO_CHECK_EQUAL(3, hueCurveLog->getCurve(OCIO::LUM_LUM)->getNumControlPoints()); + OCIO_CHECK_EQUAL(0.f, hueCurveLog->getCurve(OCIO::LUM_LUM)->getControlPoint(0).m_x); + OCIO_CHECK_EQUAL(0.f, hueCurveLog->getCurve(OCIO::LUM_LUM)->getControlPoint(0).m_y); + OCIO_CHECK_EQUAL(0.5f, hueCurveLog->getCurve(OCIO::LUM_LUM)->getControlPoint(1).m_x); + OCIO_CHECK_EQUAL(0.5f, hueCurveLog->getCurve(OCIO::LUM_LUM)->getControlPoint(1).m_y); + OCIO_CHECK_EQUAL(1.f, hueCurveLog->getCurve(OCIO::LUM_LUM)->getControlPoint(2).m_x); + OCIO_CHECK_EQUAL(1.f, hueCurveLog->getCurve(OCIO::LUM_LUM)->getControlPoint(2).m_y); + + OCIO_CHECK_ASSERT(*hueCurveLin->getCurve(OCIO::HUE_LUM) == + *hueCurveLin->getCurve(OCIO::HUE_SAT)); + OCIO_CHECK_ASSERT(*hueCurveLin->getCurve(OCIO::SAT_SAT) != + *hueCurveLin->getCurve(OCIO::LUM_LUM)); + OCIO_CHECK_ASSERT(*hueCurveLin->getCurve(OCIO::LUM_SAT) != + *hueCurveLin->getCurve(OCIO::SAT_LUM)); + OCIO_CHECK_ASSERT(*hueCurveLin->getCurve(OCIO::HUE_HUE) != + *hueCurveLin->getCurve(OCIO::HUE_SAT)); + OCIO_CHECK_EQUAL(3, hueCurveLin->getCurve(OCIO::LUM_LUM)->getNumControlPoints()); + OCIO_CHECK_EQUAL(-7.f, hueCurveLin->getCurve(OCIO::LUM_LUM)->getControlPoint(0).m_x); + OCIO_CHECK_EQUAL(-7.f, hueCurveLin->getCurve(OCIO::LUM_LUM)->getControlPoint(0).m_y); + OCIO_CHECK_EQUAL(0.f, hueCurveLin->getCurve(OCIO::LUM_LUM)->getControlPoint(1).m_x); + OCIO_CHECK_EQUAL(0.f, hueCurveLin->getCurve(OCIO::LUM_LUM)->getControlPoint(1).m_y); + OCIO_CHECK_EQUAL(7.f, hueCurveLin->getCurve(OCIO::LUM_LUM)->getControlPoint(2).m_x); + OCIO_CHECK_EQUAL(7.f, hueCurveLin->getCurve(OCIO::LUM_LUM)->getControlPoint(2).m_y); + + OCIO_CHECK_ASSERT(!hueCurveLin->getDrawCurveOnly()); + hueCurveLin->setDrawCurveOnly(true); + OCIO_CHECK_ASSERT(hueCurveLin->getDrawCurveOnly()); + + // Validate that the other Create function made copies of its arguments. + auto hueCurveLinCopy = OCIO::GradingHueCurve::Create(hueCurveLin); + OCIO_REQUIRE_ASSERT(hueCurveLinCopy); + OCIO_CHECK_ASSERT(hueCurveLin != hueCurveLinCopy); + // Use overloaded op== to compare the contents of the curves. + OCIO_CHECK_ASSERT(*hueCurveLin == *hueCurveLinCopy); + + OCIO_CHECK_ASSERT(hueCurveLinCopy->getDrawCurveOnly()); + + // Test createEditableCopy. + hueCurveLinCopy = hueCurveLin->createEditableCopy(); + OCIO_REQUIRE_ASSERT(hueCurveLinCopy); + OCIO_CHECK_ASSERT(hueCurveLin != hueCurveLinCopy); + OCIO_CHECK_ASSERT(*hueCurveLin == *hueCurveLinCopy); + + // Test op<<. + std::ostringstream oss; + oss << *hueCurveLin; + OCIO_CHECK_EQUAL(std::string( + "" + "]>, " + "hue_sat=" + "]>, " + "hue_lum=" + "]>, " + "lum_sat=]>, " + "sat_sat=]>, " + "lum_lum=]>, " + "sat_lum=]>, " + "hue_fx=" + "]>>"), + oss.str()); +} + +OCIO_ADD_TEST(GradingHueCurve, curves) +{ + auto curves = OCIO::GradingHueCurve::Create(OCIO::GRADING_VIDEO); + OCIO_CHECK_ASSERT(curves->isIdentity()); + // Use non const curve accessor to modify one of the spline curves. + OCIO::GradingBSplineCurveRcPtr spline = curves->getCurve(OCIO::HUE_SAT); + OCIO_CHECK_EQUAL(curves->getCurve(OCIO::HUE_SAT)->getNumControlPoints(), 6); + // For this spline type, all y values must be 1. to be an identity. + OCIO_CHECK_ASSERT(curves->isIdentity()); + spline->getControlPoint(3).m_x = 0.9f; + spline->getControlPoint(3).m_y = 1.1f; + OCIO_CHECK_ASSERT(!curves->isIdentity()); + spline->getControlPoint(3).m_y = 1.f; + OCIO_CHECK_ASSERT(curves->isIdentity()); + spline->setNumControlPoints(4); + OCIO_CHECK_EQUAL(4, spline->getNumControlPoints()); + + // Changing the pointer does not change the curves. + auto newSpline = OCIO::GradingBSplineCurve::Create({ { 0.f,0.f },{ 1.f,2.f } }); + spline = newSpline; + OCIO_CHECK_EQUAL(curves->getCurve(OCIO::HUE_SAT)->getNumControlPoints(), 4); +} + +OCIO_ADD_TEST(GradingHueCurve, max_ctrl_pnts) +{ + auto hueCurve = OCIO::GradingHueCurve::Create(OCIO::GRADING_VIDEO); + for (int c = 0; c < OCIO::HUE_NUM_CURVES; ++c) + { + // Use non const curve accessor to modify the curves. + OCIO::GradingBSplineCurveRcPtr spline = hueCurve->getCurve(static_cast(c)); + spline->setNumControlPoints(28); + } + + OCIO::DynamicPropertyGradingHueCurveImplRcPtr res; + OCIO_CHECK_THROW_WHAT(res = std::make_shared(hueCurve, false), + OCIO::Exception, "Hue curve: maximum number of control points reached"); +} diff --git a/tests/cpu/ops/gradingrgbcurve/GradingBSplineCurve_tests.cpp b/tests/cpu/ops/gradingrgbcurve/GradingBSplineCurve_tests.cpp index 79ed001733..b0f5a48c40 100644 --- a/tests/cpu/ops/gradingrgbcurve/GradingBSplineCurve_tests.cpp +++ b/tests/cpu/ops/gradingrgbcurve/GradingBSplineCurve_tests.cpp @@ -8,6 +8,10 @@ namespace OCIO = OCIO_NAMESPACE; +// +// Please see DynamicProperty_tests.cpp for tests of the actual spline fitting. +// + OCIO_ADD_TEST(GradingBSplineCurve, basic) { auto curve = OCIO::GradingBSplineCurve::Create(3); @@ -48,9 +52,9 @@ OCIO_ADD_TEST(GradingBSplineCurve, basic) OCIO_CHECK_EQUAL(1.f, curve->getControlPoint(3).m_y); OCIO_CHECK_THROW_WHAT(curve->getControlPoint(42), OCIO::Exception, - "There are '4' control points. '42' is invalid."); + "There are '4' control points. '42' is out of bounds."); OCIO_CHECK_THROW_WHAT(curve->setSlope(42, 0.2f), OCIO::Exception, - "There are '4' control points. '42' is invalid."); + "There are '4' control points. '42' is out of bounds."); std::ostringstream oss; oss << *curve; @@ -67,10 +71,33 @@ OCIO_ADD_TEST(GradingBSplineCurve, validate) curve = OCIO::GradingBSplineCurve::Create({ { 0.f,0.f },{ 0.7f,0.3f }, { 0.5f,0.7f },{ 1.f,1.f } }); OCIO_CHECK_THROW_WHAT(curve->validate(), OCIO::Exception, - "has a x coordinate '0.5' that is less from previous control " - "point x cooordinate '0.7'."); + "has a x coordinate '0.5' that is less than previous control " + "point x coordinate '0.7'."); curve->getControlPoint(1).m_x = 0.3f; OCIO_CHECK_NO_THROW(curve->validate()); } +OCIO_ADD_TEST(GradingBSplineCurve, equals) +{ + // Identical curves. + auto curve1 = OCIO::GradingBSplineCurve::Create({ {0.f,0.f},{0.2f,0.3f},{0.5f,0.7f},{1.f,1.f} }); + auto curve2 = OCIO::GradingBSplineCurve::Create({ {0.f,0.f},{0.2f,0.3f},{0.5f,0.7f},{1.f,1.f} }); + OCIO_CHECK_ASSERT(*curve1 == *curve2); + + // Curve has different spline type. + auto curve3 = OCIO::GradingBSplineCurve::Create({ {0.f,0.f},{0.2f,0.3f},{0.5f,0.7f},{1.f,1.f} }, + OCIO::DIAGONAL_B_SPLINE); + OCIO_CHECK_ASSERT(!(*curve1 == *curve3)); + + // Curve has different slopes. + curve2->setSlope(3, 0.9f); + OCIO_CHECK_NO_THROW(curve2->validate()); + OCIO_CHECK_ASSERT(!(*curve1 == *curve2)); + + // Curve has a different control point value. + auto curve4 = OCIO::GradingBSplineCurve::Create({ {0.f,0.f},{0.2f,0.3f},{0.5f,0.7f},{1.f,1.f} }); + OCIO_CHECK_ASSERT(*curve1 == *curve4); + curve4->getControlPoint(2).m_y = 0.9f; + OCIO_CHECK_ASSERT(!(*curve1 == *curve4)); +} diff --git a/tests/cpu/ops/gradingrgbcurve/GradingRGBCurveOpData_tests.cpp b/tests/cpu/ops/gradingrgbcurve/GradingRGBCurveOpData_tests.cpp index 2a373a5723..38431dc170 100644 --- a/tests/cpu/ops/gradingrgbcurve/GradingRGBCurveOpData_tests.cpp +++ b/tests/cpu/ops/gradingrgbcurve/GradingRGBCurveOpData_tests.cpp @@ -91,23 +91,40 @@ OCIO_ADD_TEST(GradingRGBCurveOpData, accessors) // Check isInverse. - // We have equal ops, inverse one. - gc1.setDirection(OCIO::TRANSFORM_DIR_FORWARD); + // Make two equal non-identity ops, invert one. + OCIO::GradingRGBCurveOpData gc3{ OCIO::GRADING_LIN }; + auto v3 = gc3.getValue()->createEditableCopy(); + auto spline = v3->getCurve(OCIO::RGB_RED); + spline->setNumControlPoints(2); + spline->getControlPoint(0) = OCIO::GradingControlPoint(0.f, 2.f); + spline->getControlPoint(1) = OCIO::GradingControlPoint(0.9f, 2.f); + gc3.setValue(v3); + OCIO_CHECK_ASSERT(!gc3.isIdentity()); // Need a shared pointer for the parameter. - OCIO::ConstGradingRGBCurveOpDataRcPtr gcptr2 = gc1.clone(); - gc1.setDirection(OCIO::TRANSFORM_DIR_INVERSE); - OCIO_CHECK_ASSERT(gc1.isInverse(gcptr2)); - // Change value of one: no longer inverse. - red->getControlPoint(3).m_y += 0.1f; - gc1.setValue(v1); - OCIO_CHECK_ASSERT(!gc1.isInverse(gcptr2)); + OCIO::ConstGradingRGBCurveOpDataRcPtr gcptr3 = gc3.clone(); + gc3.setDirection(OCIO::TRANSFORM_DIR_INVERSE); + // They start as inverses. + OCIO_CHECK_ASSERT(gc3.isInverse(gcptr3)); + + // Change value of one: no longer an inverse. + spline->getControlPoint(1).m_y += 0.25f; + gc3.setValue(v3); + OCIO_CHECK_ASSERT(!gc3.isInverse(gcptr3)); // Restore value. - red->getControlPoint(3).m_y -= 0.1f; - gc1.setValue(v1); - OCIO_CHECK_ASSERT(gc1.isInverse(gcptr2)); - // Change direction: no longer inverse. - gc1.setDirection(OCIO::TRANSFORM_DIR_FORWARD); - OCIO_CHECK_ASSERT(!gc1.isInverse(gcptr2)); + spline->getControlPoint(1).m_y -= 0.25f; + gc3.setValue(v3); + OCIO_CHECK_ASSERT(gc3.isInverse(gcptr3)); + + // Change slope of one: no longer an inverse. + gc3.setSlope(OCIO::RGB_BLUE, 2, 0.9f); + OCIO_CHECK_ASSERT(!gc3.isInverse(gcptr3)); + // Restore value. + gc3.setSlope(OCIO::RGB_BLUE, 2, 0.f); + OCIO_CHECK_ASSERT(gc3.isInverse(gcptr3)); + + // Change direction: no longer an inverse. + gc3.setDirection(OCIO::TRANSFORM_DIR_FORWARD); + OCIO_CHECK_ASSERT(!gc3.isInverse(gcptr3)); } OCIO_ADD_TEST(GradingRGBCurveOpData, validate) @@ -127,12 +144,26 @@ OCIO_ADD_TEST(GradingRGBCurveOpData, validate) { 0.5f,0.7f },{ 1.f,1.f } }); curves = OCIO::GradingRGBCurve::Create(curve, curve, curve, curve); OCIO_CHECK_THROW_WHAT(gc.setValue(curves), OCIO::Exception, - "has a x coordinate '0.5' that is less from previous control " - "point x cooordinate '0.7'."); + "has a x coordinate '0.5' that is less than previous control " + "point x coordinate '0.7'."); // Fix the curve x coordinate. curve->getControlPoint(1).m_x = 0.3f; curves = OCIO::GradingRGBCurve::Create(curve, curve, curve, curve); OCIO_CHECK_NO_THROW(gc.setValue(curves)); OCIO_CHECK_NO_THROW(gc.validate()); + + // Curve y coordinates have to increase. + curve = OCIO::GradingBSplineCurve::Create({ { 0.f,0.f },{ 0.3f,0.3f }, + { 0.5f,0.27f },{ 1.f,1.f } }); + curves = OCIO::GradingRGBCurve::Create(curve, curve, curve, curve); + OCIO_CHECK_THROW_WHAT(gc.setValue(curves), OCIO::Exception, + "point at index 2 has a y coordinate '0.27' that is less than " + "previous control point y coordinate '0.3'."); + + // Curve must use the proper spline type. + curve = OCIO::GradingBSplineCurve::Create({ { 0.f,0.f },{ 0.9f,0.f } }, OCIO::HUE_FX); + curves = OCIO::GradingRGBCurve::Create(curve, curve, curve, curve); + OCIO_CHECK_THROW_WHAT(gc.setValue(curves), OCIO::Exception, + "validation failed: 'red' curve is of the wrong BSplineType."); } diff --git a/tests/cpu/ops/gradingrgbcurve/GradingRGBCurveOp_tests.cpp b/tests/cpu/ops/gradingrgbcurve/GradingRGBCurveOp_tests.cpp index fbdd4a5b93..755a047f22 100644 --- a/tests/cpu/ops/gradingrgbcurve/GradingRGBCurveOp_tests.cpp +++ b/tests/cpu/ops/gradingrgbcurve/GradingRGBCurveOp_tests.cpp @@ -101,7 +101,7 @@ OCIO_ADD_TEST(GradingRGBCurveOp, build_ops) // Sharing of dynamic properties is done through processor, changing the source will not // change the op. - auto curve = OCIO::GradingBSplineCurve::Create({ { 0.f,1.f },{ 0.2f,0.3f }, + auto curve = OCIO::GradingBSplineCurve::Create({ { 0.f,0.1f },{ 0.2f,0.3f }, { 0.5f,0.8f },{ 2.f,1.5f } }); auto rgbCurve = OCIO::GradingRGBCurve::Create(curve, curve, curve, curve); gcTransform->setValue(rgbCurve); @@ -131,7 +131,7 @@ OCIO_ADD_TEST(GradingRGBCurveOp, build_ops) // Control point has moved. cpu->applyRGB(pixel); - OCIO_CHECK_CLOSE(pixel[0], 1.11148262f, error); - OCIO_CHECK_CLOSE(pixel[1], 0.04518771f, error); + OCIO_CHECK_CLOSE(pixel[0], 0.18597151f, error); + OCIO_CHECK_CLOSE(pixel[1], 0.47056902f, error); OCIO_CHECK_CLOSE(pixel[2], 1.32527864f, error); } diff --git a/tests/cpu/ops/gradingrgbcurve/GradingRGBCurve_tests.cpp b/tests/cpu/ops/gradingrgbcurve/GradingRGBCurve_tests.cpp index a4e37c8751..167463a2eb 100644 --- a/tests/cpu/ops/gradingrgbcurve/GradingRGBCurve_tests.cpp +++ b/tests/cpu/ops/gradingrgbcurve/GradingRGBCurve_tests.cpp @@ -110,16 +110,24 @@ OCIO_ADD_TEST(GradingRGBCurve, curves) OCIO_ADD_TEST(GradingRGBCurve, max_ctrl_pnts) { auto curveR = OCIO::GradingBSplineCurve::Create({ { 0.f, 10.f },{ 2.f, 10.f },{ 3.f, 10.f },{ 5.f, 10.f },{ 6.f, 10.f }, - { 8.f, 10.f },{ 9.f, 10.5f },{ 11.f, 15.f },{ 12.f, 50.f },{ 14.f, 60.f },{ 15.f, 85.f } }); + { 8.f, 10.f },{ 9.f, 10.5f },{ 11.f, 15.f },{ 12.f, 50.f },{ 14.f, 60.f },{ 15.f, 85.f }, { 16.f, 86.f }, { 17.f, 87.f }, + { 18.f, 88.f }, { 19.f, 89.f }, { 20.f, 90.f }, { 21.f, 91.f }, { 22.f, 92.f }, { 23.f, 93.f }, { 24.f, 94.f }, + { 25.f, 95.f }, { 26.f, 96.f }, { 27.f, 97.f }, { 28.f, 98.f }, { 29.f, 99.f }, { 30.f, 100.f }}); auto curveG = OCIO::GradingBSplineCurve::Create({ { 0.f, 10.f },{ 2.f, 10.f },{ 3.f, 10.f },{ 5.f, 10.f },{ 6.f, 10.f }, - { 8.f, 10.f },{ 9.f, 10.5f },{ 11.f, 15.f },{ 12.f, 50.f },{ 14.f, 60.f },{ 15.f, 85.f } }); + { 8.f, 10.f },{ 9.f, 10.5f },{ 11.f, 15.f },{ 12.f, 50.f },{ 14.f, 60.f },{ 15.f, 85.f }, { 16.f, 86.f }, { 17.f, 87.f }, + { 18.f, 88.f }, { 19.f, 89.f }, { 20.f, 90.f }, { 21.f, 91.f }, { 22.f, 92.f }, { 23.f, 93.f }, { 24.f, 94.f }, + { 25.f, 95.f }, { 26.f, 96.f }, { 27.f, 97.f }, { 28.f, 98.f }, { 29.f, 99.f }, { 30.f, 100.f } }); auto curveB = OCIO::GradingBSplineCurve::Create({ { 0.f, 10.f },{ 2.f, 10.f },{ 3.f, 10.f },{ 5.f, 10.f },{ 6.f, 10.f }, - { 8.f, 10.f },{ 9.f, 10.5f },{ 11.f, 15.f },{ 12.f, 50.f },{ 14.f, 60.f },{ 15.f, 85.f } }); + { 8.f, 10.f },{ 9.f, 10.5f },{ 11.f, 15.f },{ 12.f, 50.f },{ 14.f, 60.f },{ 15.f, 85.f }, { 16.f, 86.f }, { 17.f, 87.f }, + { 18.f, 88.f }, { 19.f, 89.f }, { 20.f, 90.f }, { 21.f, 91.f }, { 22.f, 92.f }, { 23.f, 93.f }, { 24.f, 94.f }, + { 25.f, 95.f }, { 26.f, 96.f }, { 27.f, 97.f }, { 28.f, 98.f }, { 29.f, 99.f }, { 30.f, 100.f } }); auto curveM = OCIO::GradingBSplineCurve::Create({ { 0.f, 10.f },{ 2.f, 10.f },{ 3.f, 10.f },{ 5.f, 10.f },{ 6.f, 10.f }, - { 8.f, 10.f },{ 9.f, 10.5f },{ 11.f, 15.f },{ 12.f, 50.f },{ 14.f, 60.f },{ 15.f, 85.f } }); + { 8.f, 10.f },{ 9.f, 10.5f },{ 11.f, 15.f },{ 12.f, 50.f },{ 14.f, 60.f },{ 15.f, 85.f }, { 16.f, 86.f }, { 17.f, 87.f }, + { 18.f, 88.f }, { 19.f, 89.f }, { 20.f, 90.f }, { 21.f, 91.f }, { 22.f, 92.f }, { 23.f, 93.f }, { 24.f, 94.f }, + { 25.f, 95.f }, { 26.f, 96.f }, { 27.f, 97.f }, { 28.f, 98.f }, { 29.f, 99.f }, { 30.f, 100.f } }); auto rgbCurve = OCIO::GradingRGBCurve::Create(curveR, curveG, curveB, curveM); OCIO_REQUIRE_ASSERT(rgbCurve); diff --git a/tests/cpu/transforms/GradingHueCurveTransform_tests.cpp b/tests/cpu/transforms/GradingHueCurveTransform_tests.cpp new file mode 100644 index 0000000000..26d5bba759 --- /dev/null +++ b/tests/cpu/transforms/GradingHueCurveTransform_tests.cpp @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + + +#include "ops/gradingrgbcurve/GradingBSplineCurve.h" +#include "transforms/GradingHueCurveTransform.cpp" + +#include "testutils/UnitTest.h" +#include "UnitTestLogUtils.h" + +namespace OCIO = OCIO_NAMESPACE; + +OCIO_ADD_TEST(GradingHueCurveTransform, basic) +{ + // Create transform and validate default values for all styles. + + auto gctLin = OCIO::GradingHueCurveTransform::Create(OCIO::GRADING_LIN); + OCIO_CHECK_EQUAL(gctLin->getStyle(), OCIO::GRADING_LIN); + OCIO_CHECK_EQUAL(gctLin->getDirection(), OCIO::TRANSFORM_DIR_FORWARD); + gctLin->setDirection(OCIO::TRANSFORM_DIR_INVERSE); + OCIO_CHECK_EQUAL(gctLin->getDirection(), OCIO::TRANSFORM_DIR_INVERSE); + OCIO_CHECK_EQUAL(gctLin->getRGBToHSY(), OCIO::HSY_TRANSFORM_1); + OCIO_CHECK_ASSERT(!gctLin->isDynamic()); + auto crv = gctLin->getValue()->getCurve(OCIO::LUM_SAT); + OCIO_CHECK_EQUAL(crv->getNumControlPoints(), 3); + OCIO_CHECK_EQUAL(crv->getControlPoint(0), OCIO::GradingControlPoint(-7.f, 1.f)); + OCIO_CHECK_EQUAL(crv->getControlPoint(1), OCIO::GradingControlPoint(0.f, 1.f)); + OCIO_CHECK_EQUAL(crv->getControlPoint(2), OCIO::GradingControlPoint(7.f, 1.f)); + crv = gctLin->getValue()->getCurve(OCIO::HUE_LUM); + OCIO_CHECK_EQUAL(*gctLin->getValue()->getCurve(OCIO::HUE_SAT), *crv); + OCIO_CHECK_ASSERT(gctLin.get()); + OCIO_CHECK_NO_THROW(gctLin->validate()); + + auto gctLog = OCIO::GradingHueCurveTransform::Create(OCIO::GRADING_LOG); + OCIO_CHECK_EQUAL(gctLog->getStyle(), OCIO::GRADING_LOG); + OCIO_CHECK_EQUAL(gctLog->getDirection(), OCIO::TRANSFORM_DIR_FORWARD); + OCIO_CHECK_EQUAL(gctLog->getRGBToHSY(), OCIO::HSY_TRANSFORM_1); + OCIO_CHECK_ASSERT(!gctLog->isDynamic()); + crv = gctLog->getValue()->getCurve(OCIO::LUM_SAT); + OCIO_CHECK_EQUAL(crv->getNumControlPoints(), 3); + OCIO_CHECK_EQUAL(crv->getControlPoint(0), OCIO::GradingControlPoint(0.f, 1.f)); + OCIO_CHECK_EQUAL(crv->getControlPoint(1), OCIO::GradingControlPoint(0.5f, 1.f)); + OCIO_CHECK_EQUAL(crv->getControlPoint(2), OCIO::GradingControlPoint(1.f, 1.f)); + crv = gctLog->getValue()->getCurve(OCIO::HUE_LUM); + OCIO_CHECK_EQUAL(*gctLog->getValue()->getCurve(OCIO::HUE_SAT), *crv); + crv = gctLog->getValue()->getCurve(OCIO::LUM_LUM); + OCIO_CHECK_EQUAL(*gctLog->getValue()->getCurve(OCIO::SAT_SAT), *crv); + OCIO_CHECK_NO_THROW(gctLog->validate()); + + auto gctVid = OCIO::GradingHueCurveTransform::Create(OCIO::GRADING_VIDEO); + OCIO_CHECK_EQUAL(gctVid->getStyle(), OCIO::GRADING_VIDEO); + OCIO_CHECK_EQUAL(gctVid->getDirection(), OCIO::TRANSFORM_DIR_FORWARD); + OCIO_CHECK_EQUAL(gctVid->getRGBToHSY(), OCIO::HSY_TRANSFORM_1); + OCIO_CHECK_ASSERT(!gctVid->isDynamic()); + crv = gctVid->getValue()->getCurve(OCIO::LUM_SAT); + OCIO_CHECK_EQUAL(crv->getNumControlPoints(), 3); + OCIO_CHECK_EQUAL(crv->getControlPoint(0), OCIO::GradingControlPoint(0.f, 1.f)); + OCIO_CHECK_EQUAL(crv->getControlPoint(1), OCIO::GradingControlPoint(0.5f, 1.f)); + OCIO_CHECK_EQUAL(crv->getControlPoint(2), OCIO::GradingControlPoint(1.f, 1.f)); + crv = gctLog->getValue()->getCurve(OCIO::HUE_LUM); + OCIO_CHECK_EQUAL(*gctLog->getValue()->getCurve(OCIO::HUE_SAT), *crv); + crv = gctLog->getValue()->getCurve(OCIO::LUM_LUM); + OCIO_CHECK_EQUAL(*gctLog->getValue()->getCurve(OCIO::SAT_SAT), *crv); + OCIO_CHECK_NO_THROW(gctVid->validate()); + + // Change values. + auto t = gctVid->createEditableCopy(); + auto gct = OCIO_DYNAMIC_POINTER_CAST(t); + gct->setStyle(OCIO::GRADING_LIN); + OCIO_CHECK_EQUAL(gct->getStyle(), OCIO::GRADING_LIN); + gct->setDirection(OCIO::TRANSFORM_DIR_INVERSE); + OCIO_CHECK_EQUAL(gct->getDirection(), OCIO::TRANSFORM_DIR_INVERSE); + gct->setRGBToHSY(OCIO::HSY_TRANSFORM_NONE); + OCIO_CHECK_EQUAL(gct->getRGBToHSY(), OCIO::HSY_TRANSFORM_NONE); + gct->makeDynamic(); + OCIO_CHECK_ASSERT(gct->isDynamic()); + gct->setValue(gctLin->getValue()); + crv = gct->getValue()->getCurve(OCIO::LUM_LUM); + OCIO_CHECK_EQUAL(crv->getControlPoint(0), OCIO::GradingControlPoint(-7.f, -7.f)); + OCIO_CHECK_NO_THROW(gct->validate()); + + // Access out of range point. + OCIO_CHECK_THROW_WHAT(crv->getControlPoint(4), OCIO::Exception, + "There are '3' control points. '4' is out of bounds."); + + // X-coordinate has to be increasing. + { + OCIO::GradingHueCurveTransformRcPtr hct = OCIO::GradingHueCurveTransform::Create(OCIO::GRADING_VIDEO); + OCIO::GradingHueCurveRcPtr hueCurve = hct->getValue()->createEditableCopy(); + OCIO::GradingBSplineCurveRcPtr lumsat = hueCurve->getCurve(OCIO::LUM_SAT); + OCIO::GradingControlPoint & cp = lumsat->getControlPoint(0); + cp = OCIO::GradingControlPoint(0.7f, 1.f); + OCIO_CHECK_THROW_WHAT(hct->setValue(hueCurve), OCIO::Exception, + "has a x coordinate '0.5' that is less than previous control " + "point x coordinate '0.7'."); + } + + // Y-coordinate has to be increasing, for diagonal curves. + { + OCIO::GradingHueCurveTransformRcPtr hct = OCIO::GradingHueCurveTransform::Create(OCIO::GRADING_VIDEO); + OCIO::GradingHueCurveRcPtr hueCurve = hct->getValue()->createEditableCopy(); + OCIO::GradingBSplineCurveRcPtr lumlum = hueCurve->getCurve(OCIO::LUM_LUM); + OCIO::GradingControlPoint & cp = lumlum->getControlPoint(0); + cp = OCIO::GradingControlPoint(0.f, 0.6f); + OCIO_CHECK_THROW_WHAT(hct->setValue(hueCurve), OCIO::Exception, + "has a y coordinate '0.5' that is less than previous control " + "point y coordinate '0.6'."); + } + + // Check slopes. + gct->setSlope(OCIO::LUM_LUM, 2, 0.9f); + OCIO_CHECK_NO_THROW(gct->validate()); + OCIO_CHECK_EQUAL(gct->getSlope(OCIO::LUM_LUM, 2), 0.9f); + OCIO_CHECK_THROW_WHAT(gct->setSlope(OCIO::LUM_LUM, 4, 2.f), OCIO::Exception, + "There are '3' control points. '4' is out of bounds."); + OCIO_CHECK_ASSERT(gct->slopesAreDefault(OCIO::LUM_SAT)); + OCIO_CHECK_ASSERT(!gct->slopesAreDefault(OCIO::LUM_LUM)); +} + +OCIO_ADD_TEST(GradingHueCurveTransform, processor_several_transforms) +{ + OCIO::GradingHueCurveTransformRcPtr gcta = OCIO::GradingHueCurveTransform::Create(OCIO::GRADING_LOG); + OCIO::GradingHueCurveRcPtr hueCurveA = gcta->getValue()->createEditableCopy(); + OCIO::GradingBSplineCurveRcPtr huefx = hueCurveA->getCurve(OCIO::HUE_FX); + // Shift all hues up by 0.1. + huefx->setNumControlPoints(2); + huefx->getControlPoint(0) = OCIO::GradingControlPoint(0.f, 0.1f); + huefx->getControlPoint(1) = OCIO::GradingControlPoint(0.9f, 0.1f); + gcta->setValue(hueCurveA); + + OCIO_CHECK_NO_THROW(gcta->validate()); + OCIO_CHECK_ASSERT(!hueCurveA->isIdentity()); + + OCIO::ConfigRcPtr config = OCIO::Config::Create(); + const float srcPixel[3] = { 0.2f, 0.3f, 0.4f }; + + // Will hold results for hueCurveA. + float pixel_a[3] = { srcPixel[0], srcPixel[1], srcPixel[2] }; + // Will hold results for hueCurveA applied twice. + float pixel_aa[3]; + { + OCIO::ConstProcessorRcPtr processor = config->getProcessor(gcta); + OCIO::ConstCPUProcessorRcPtr cpuProcessor = processor->getDefaultCPUProcessor(); + cpuProcessor->applyRGB(pixel_a); + + pixel_aa[0] = pixel_a[0]; + pixel_aa[1] = pixel_a[1]; + pixel_aa[2] = pixel_a[2]; + cpuProcessor->applyRGB(pixel_aa); + } + + // NB: This must be GRADING_LOG like above because the test will be changing the curves + // as dynamic parameters but that does not change the base style. + OCIO::GradingHueCurveTransformRcPtr gctb = OCIO::GradingHueCurveTransform::Create(OCIO::GRADING_LOG); + OCIO::GradingHueCurveRcPtr hueCurveB = gctb->getValue()->createEditableCopy(); + OCIO::GradingBSplineCurveRcPtr lumsat = hueCurveB->getCurve(OCIO::LUM_SAT); + // Increase sat at all luminances by 1.5. + lumsat->setNumControlPoints(2); + lumsat->getControlPoint(0) = OCIO::GradingControlPoint(0.f, 1.5f); + lumsat->getControlPoint(1) = OCIO::GradingControlPoint(1.f, 1.5f); + gctb->setValue(hueCurveB); + + OCIO_CHECK_ASSERT(!hueCurveB->isIdentity()); + + // Will hold results for hueCurveA applied then hueCurveB applied. + float pixel_ab[3] = { pixel_a[0], pixel_a[1], pixel_a[2] }; + { + OCIO::ConstProcessorRcPtr processor = config->getProcessor(gctb); + OCIO::ConstCPUProcessorRcPtr cpuProcessor = processor->getDefaultCPUProcessor(); + cpuProcessor->applyRGB(pixel_ab); + } + + // Make second transform dynamic. + gctb->makeDynamic(); + const float error = 1e-6f; + + // + // Test with two grading hue curve transforms where only the second one is dynamic. + // + + OCIO::GroupTransformRcPtr grp1 = OCIO::GroupTransform::Create(); + gctb->setValue(hueCurveA); + grp1->appendTransform(gcta); // gpta values are hueCurveA + grp1->appendTransform(gctb); // gptb values are hueCurveA + + { + OCIO::ConstProcessorRcPtr processor = config->getProcessor(grp1); + OCIO::ConstCPUProcessorRcPtr cpuProcessor = processor->getDefaultCPUProcessor(); + + // Second transform is dynamic. Value is still hueCurveA. + OCIO::DynamicPropertyRcPtr dp; + OCIO_CHECK_NO_THROW(dp = cpuProcessor->getDynamicProperty(OCIO::DYNAMIC_PROPERTY_GRADING_HUECURVE)); + auto dpVal = OCIO::DynamicPropertyValue::AsGradingHueCurve(dp); + OCIO_REQUIRE_ASSERT(dpVal); + + float pixel[3] = { srcPixel[0], srcPixel[1], srcPixel[2] }; + + // Apply hueCurveA then hueCurveA. + cpuProcessor->applyRGB(pixel); + + OCIO_CHECK_CLOSE(pixel[0], pixel_aa[0], error); + OCIO_CHECK_CLOSE(pixel[1], pixel_aa[1], error); + OCIO_CHECK_CLOSE(pixel[2], pixel_aa[2], error); + + // Change the 2nd values. + dpVal->setValue(hueCurveB); + + pixel[0] = srcPixel[0]; + pixel[1] = srcPixel[1]; + pixel[2] = srcPixel[2]; + + // Apply hueCurveA then hueCurveB. + cpuProcessor->applyRGB(pixel); + + OCIO_CHECK_CLOSE(pixel[0], pixel_ab[0], error); + OCIO_CHECK_CLOSE(pixel[1], pixel_ab[1], error); + OCIO_CHECK_CLOSE(pixel[2], pixel_ab[2], error); + } + + // + // Test two grading hue curve transforms can't be both dynamic. + // + + // Make first dynamic (second already is). + gcta->makeDynamic(); + + OCIO::GroupTransformRcPtr grp2 = OCIO::GroupTransform::Create(); + grp2->appendTransform(gcta); + grp2->appendTransform(gctb); + + { + OCIO::LogGuard log; + OCIO::SetLoggingLevel(OCIO::LOGGING_LEVEL_WARNING); + OCIO_CHECK_NO_THROW(config->getProcessor(grp2)); + OCIO_CHECK_EQUAL(log.output(), "[OpenColorIO Warning]: Grading hue curve dynamic property " + "can only be there once.\n"); + } +} + +OCIO_ADD_TEST(GradingHueCurveTransform, serialization) +{ + // Test the serialization of the transform. + + auto hh = OCIO::GradingBSplineCurve::Create( + { {0.1f, -0.05f}, {0.2f, 0.23f}, {0.5f, 0.25f}, {0.8f, 0.7f}, {0.85f, 0.8f}, {0.95f, 0.9f} }, + OCIO::HUE_HUE); + auto hs = OCIO::GradingBSplineCurve::Create( + { {0.f, 1.2f}, {0.1f, 1.2f}, {0.4f, 0.7f}, {0.6f, 0.3f}, {0.8f, 0.5f}, {0.9f, 0.8f} }, + OCIO::HUE_SAT); + auto hl = OCIO::GradingBSplineCurve::Create( + { {0.1f, 1.4f}, {0.2f, 1.4f}, {0.4f, 0.7f}, {0.6f, 0.5f}, {0.8f, 0.8f} }, + OCIO::HUE_LUM); + auto ls = OCIO::GradingBSplineCurve::Create( + { {0.0f, 1.0f}, {0.5f, 1.5f}, {1.0f, 0.9f}, {1.1f, 1.1f} }, + OCIO::LUM_SAT); + auto ss = OCIO::GradingBSplineCurve::Create( + { {0.f, 0.05f}, {0.5f, 0.8f}, {1.f, 1.05f} }, + OCIO::SAT_SAT); + auto ll = OCIO::GradingBSplineCurve::Create( + { {0.f, -0.0005f}, {0.5f, 0.3f}, {1.f, 0.9f} }, + OCIO::LUM_LUM); + auto sl = OCIO::GradingBSplineCurve::Create( + { {0.05f, 1.1f}, {0.3f, 1.f}, {1.2f, 0.9f} }, + OCIO::SAT_LUM); + auto hfx = OCIO::GradingBSplineCurve::Create( + { {-0.15f, 0.1f}, {0.f, -0.05f}, {0.2f, -0.1f}, {0.4f, 0.3f}, {0.6f, 0.25f}, { 0.8f, 0.2f}, {0.9f, 0.05f}, {1.1f, -0.07f} }, + OCIO::HUE_FX); + + ss->setSlope(0, 1.2f); + ss->setSlope(1, 0.8f); + ss->setSlope(2, 0.4f); + + auto data = OCIO::GradingHueCurve::Create(hh, hs, hl, ls, ss, ll, sl, hfx); + + auto curve = OCIO::GradingHueCurveTransform::Create(OCIO::GRADING_VIDEO); + OCIO_CHECK_ASSERT(curve.get()); + OCIO_CHECK_NO_THROW(curve->validate()); + + curve->setValue(data); + + static constexpr char CURVE_STR[] + = "" + "]>, hue_sat=" + "]>, hue_lum=]>, lum_sat=" + "]>, sat_sat=]>, lum_lum=]>, sat_lum=" + "]>, hue_fx=" + "]>>>"; + + { + std::ostringstream oss; + oss << *curve; + + OCIO_CHECK_EQUAL(oss.str(), CURVE_STR); + } + + OCIO::GroupTransformRcPtr grp = OCIO::GroupTransform::Create(); + grp->appendTransform(OCIO::DynamicPtrCast(curve)); + + { + std::ostringstream oss; + oss << *grp; + + std::string GROUP_STR("validate()); + + OCIO::ConstConfigRcPtr config = OCIO::Config::CreateRaw(); + + OCIO::ConstProcessorRcPtr proc = config->getProcessor(transform); + OCIO::ConstGPUProcessorRcPtr gpu = proc->getOptimizedGPUProcessor(OCIO::OPTIMIZATION_NONE); + + OCIO::GpuShaderDescRcPtr shaderDesc = OCIO::GpuShaderDesc::CreateShaderDesc(); + + OCIO_CHECK_NO_THROW(gpu->extractGpuShaderInfo(shaderDesc)); + + static const std::string gpuStr("\n" + "// Declaration of the OCIO shader function\n" + "\n" + "vec4 OCIOMain(vec4 inPixel)\n" + "{\n" + " vec4 outColor = inPixel;\n" + "\n" + " return outColor;\n" + "}\n"); + + OCIO_CHECK_EQUAL(gpuStr, std::string(shaderDesc->getShaderText())); +} diff --git a/tests/cpu/transforms/GradingRGBCurveTransform_tests.cpp b/tests/cpu/transforms/GradingRGBCurveTransform_tests.cpp index 3e1f1ec742..26b568f8f1 100644 --- a/tests/cpu/transforms/GradingRGBCurveTransform_tests.cpp +++ b/tests/cpu/transforms/GradingRGBCurveTransform_tests.cpp @@ -77,22 +77,22 @@ OCIO_ADD_TEST(GradingRGBCurveTransform, basic) // Access out of range point. OCIO_CHECK_THROW_WHAT(red->getControlPoint(4), OCIO::Exception, - "There are '3' control points. '4' is invalid."); + "There are '3' control points. '4' is out of bounds."); // X has to be increasing. auto invalidCurve = OCIO::GradingBSplineCurve::Create({ { 0.0f, 0.0f }, { 0.5f, 0.2f }, { 0.2f, 0.7f }, { 1.0f, 1.0f } }); auto newCurve = OCIO::GradingRGBCurve::Create(red, red, invalidCurve, red); OCIO_CHECK_THROW_WHAT(gct->setValue(newCurve), OCIO::Exception, - "has a x coordinate '0.2' that is less from previous control " - "point x cooordinate '0.5'."); + "has a x coordinate '0.2' that is less than previous control " + "point x coordinate '0.5'."); // Check slopes. gct->setSlope(OCIO::RGB_BLUE, 2, 0.9f); OCIO_CHECK_NO_THROW(gct->validate()); OCIO_CHECK_EQUAL(gct->getSlope(OCIO::RGB_BLUE, 2), 0.9f); OCIO_CHECK_THROW_WHAT(gct->setSlope(OCIO::RGB_BLUE, 4, 2.f), OCIO::Exception, - "There are '3' control points. '4' is invalid."); + "There are '3' control points. '4' is out of bounds."); OCIO_CHECK_ASSERT(gct->slopesAreDefault(OCIO::RGB_GREEN)); OCIO_CHECK_ASSERT(!gct->slopesAreDefault(OCIO::RGB_BLUE)); } diff --git a/tests/cpu/transforms/Lut1DTransform_tests.cpp b/tests/cpu/transforms/Lut1DTransform_tests.cpp index 6c8928cd50..f31e9dc0fc 100644 --- a/tests/cpu/transforms/Lut1DTransform_tests.cpp +++ b/tests/cpu/transforms/Lut1DTransform_tests.cpp @@ -99,7 +99,7 @@ OCIO_ADD_TEST(Lut1DTransform, basic) oss << *lut; OCIO_CHECK_EQUAL(oss.str(), ""); + " length=3, minrgb=[-0.2, 0.1, -0.3], maxrgb=[1.2, 1.3, 0.8]>"); auto lut2 = OCIO_DYNAMIC_POINTER_CAST(lut->createEditableCopy()); std::ostringstream oss2; diff --git a/tests/cpu/transforms/Lut3DTransform_tests.cpp b/tests/cpu/transforms/Lut3DTransform_tests.cpp index ad2c9834ff..db3296e7d2 100644 --- a/tests/cpu/transforms/Lut3DTransform_tests.cpp +++ b/tests/cpu/transforms/Lut3DTransform_tests.cpp @@ -99,7 +99,7 @@ OCIO_ADD_TEST(Lut3DTransform, basic) std::ostringstream oss; oss << *lut; OCIO_CHECK_EQUAL(oss.str(), ""); + " interpolation=default, gridSize=3, minrgb=[-0.2, -0.1, -0.3], maxrgb=[1.2, 1.3, 1.8]>"); } OCIO_ADD_TEST(Lut3DTransform, create_with_parameters) diff --git a/tests/data/files/grading_hue_curve.ctf b/tests/data/files/grading_hue_curve.ctf new file mode 100644 index 0000000000..ee73caf69b --- /dev/null +++ b/tests/data/files/grading_hue_curve.ctf @@ -0,0 +1,47 @@ + + + + + + +0.05, 0.15, 0.2, 0.3, 0.35, 0.4, 0.45, 0.45, 0.6, 0.7, 0.8, 0.85 + + + + +-0.1, 1.2, 0.2, 0.7, 0.4, 1.5, 0.5, 0.5, 0.6, 1.4, 0.8, 0.7 + + + + +0.1, 1.5, 0.2, 0.7, 0.4, 1.4, 0.5, 0.8, 0.8, 0.5 + + + + +0.05, 1.5, 0.5, 0.9, 1.1, 1.4 + + + + +0., 0.1, 0.5, 0.45, 1., 1.1 + + + + +-0.02, -0.04, 0.2, 0.1, 0.8, 0.95, 1.1, 1.2 + + + + +0., 1.2, 0.6, 0.8, 0.9, 1.1 + + + + +0.2, 0.05, .4, -0.09, .6, -0.2, .8, 0.05, 0.99, -0.02 + + + + + diff --git a/tests/gpu/CMakeLists.txt b/tests/gpu/CMakeLists.txt index 2c56f2948b..5f1c0379cf 100644 --- a/tests/gpu/CMakeLists.txt +++ b/tests/gpu/CMakeLists.txt @@ -15,6 +15,7 @@ set(SOURCES GPUUnitTest.cpp GradingPrimaryOp_test.cpp GradingRGBCurveOp_test.cpp + GradingHueCurveOp_test.cpp GradingToneOp_test.cpp LogOp_test.cpp Lut1DOp_test.cpp diff --git a/tests/gpu/FixedFunctionOp_test.cpp b/tests/gpu/FixedFunctionOp_test.cpp index 227da21235..b6a30c11ae 100644 --- a/tests/gpu/FixedFunctionOp_test.cpp +++ b/tests/gpu/FixedFunctionOp_test.cpp @@ -1242,6 +1242,144 @@ OCIO_ADD_GPU_TEST(FixedFunction, style_RGB_TO_HSV_inv_custom) test.setErrorThreshold(1e-6f); } +OCIO_ADD_GPU_TEST(FixedFunction, style_RGB_TO_HSY_LIN_fwd) +{ + OCIO::FixedFunctionTransformRcPtr func = + OCIO::FixedFunctionTransform::Create(OCIO::FIXED_FUNCTION_RGB_TO_HSY_LIN); + func->setDirection(OCIO::TRANSFORM_DIR_FORWARD); + + test.setProcessor(func); + + OCIOGPUTest::CustomValues values; + values.m_inputValues = { + -0.075290f, 0.078996f, -0.108397f, 0.f, + 0.3f, 0.4f, 0.5f, 0.f, + 0.05f, 0.03f, 0.04f, 0.f, + 0.01f, 0.01f, -0.05f, 1.f, + 0.05f, -0.005f, -0.05f, 1.f, + -0.048f, 0.01f, 0.05f, 0.f, + 0.3f, -0.4f, 0.5f, 0.f, + -0.055f, 0.01f, 0.05f, 0.f }; + test.setCustomValues(values); + + test.setErrorThreshold(1e-6f); +} + +OCIO_ADD_GPU_TEST(FixedFunction, style_RGB_TO_HSY_LIN_inv) +{ + OCIO::FixedFunctionTransformRcPtr func = + OCIO::FixedFunctionTransform::Create(OCIO::FIXED_FUNCTION_RGB_TO_HSY_LIN); + func->setDirection(OCIO::TRANSFORM_DIR_INVERSE); + + test.setProcessor(func); + + OCIOGPUTest::CustomValues values; + values.m_inputValues = { + 0.470554752f, 9.12594033f, 0.0326650218f, 0.f, // hsy alpha == 1 + 0.75f, 0.22196741f, 0.38596f, 0.f, + 0.08333333f, 0.12976444f, 0.034974f, 0.f, + 0.333333333333f, 0.606036032f, 0.0056680f, 1.f, // hsy mid alpha + 0.241666666667f, 0.8372990325f, 0.0034440f, 1.f, + 0.734693877551f, 0.752099600f, 0.0005572f, 0.f, // hsy alpha == 0 + 0.96296296f, 9.7034f, -0.1862f, 0.f, + 0.730158730159f, 0.811517000f, -0.0009310f, 0.f }; + test.setCustomValues(values); + + test.setErrorThreshold(1e-6f); +} + +OCIO_ADD_GPU_TEST(FixedFunction, style_RGB_TO_HSY_LOG_fwd) +{ + OCIO::FixedFunctionTransformRcPtr func = + OCIO::FixedFunctionTransform::Create(OCIO::FIXED_FUNCTION_RGB_TO_HSY_LOG); + func->setDirection(OCIO::TRANSFORM_DIR_FORWARD); + + test.setProcessor(func); + + OCIOGPUTest::CustomValues values; + values.m_inputValues = { + -0.075290f, 0.078996f, -0.108397f, 0.f, + 0.3f, 0.4f, 0.5f, 0.f, + 0.05f, 0.03f, 0.04f, 0.f, + 0.01f, 0.01f, -0.05f, 1.f, + 0.05f, -0.005f, -0.05f, 1.f, + -0.048f, 0.01f, 0.05f, 0.f, + 0.3f, -0.4f, 0.5f, 0.f, + -0.055f, 0.01f, 0.05f, 0.f }; + test.setCustomValues(values); + + test.setErrorThreshold(1e-6f); +} + +OCIO_ADD_GPU_TEST(FixedFunction, style_RGB_TO_HSY_LOG_inv) +{ + OCIO::FixedFunctionTransformRcPtr func = + OCIO::FixedFunctionTransform::Create(OCIO::FIXED_FUNCTION_RGB_TO_HSY_LOG); + func->setDirection(OCIO::TRANSFORM_DIR_INVERSE); + + test.setProcessor(func); + + OCIOGPUTest::CustomValues values; + values.m_inputValues = { + 0.470554752f, 9.12594033f, 0.0326650218f, 0.f, // hsy alpha == 1 + 0.75f, 0.22196741f, 0.38596f, 0.f, + 0.08333333f, 0.12976444f, 0.034974f, 0.f, + 0.333333333333f, 0.606036032f, 0.0056680f, 1.f, // hsy mid alpha + 0.241666666667f, 0.8372990325f, 0.0034440f, 1.f, + 0.734693877551f, 0.752099600f, 0.0005572f, 0.f, // hsy alpha == 0 + 0.96296296f, 9.7034f, -0.1862f, 0.f, + 0.730158730159f, 0.811517000f, -0.0009310f, 0.f }; + test.setCustomValues(values); + + test.setErrorThreshold(1e-6f); +} + +OCIO_ADD_GPU_TEST(FixedFunction, style_RGB_TO_HSY_VID_fwd) +{ + OCIO::FixedFunctionTransformRcPtr func = + OCIO::FixedFunctionTransform::Create(OCIO::FIXED_FUNCTION_RGB_TO_HSY_VID); + func->setDirection(OCIO::TRANSFORM_DIR_FORWARD); + + test.setProcessor(func); + + OCIOGPUTest::CustomValues values; + values.m_inputValues = { + -0.075290f, 0.078996f, -0.108397f, 0.f, + 0.3f, 0.4f, 0.5f, 0.f, + 0.05f, 0.03f, 0.04f, 0.f, + 0.01f, 0.01f, -0.05f, 1.f, + 0.05f, -0.005f, -0.05f, 1.f, + -0.048f, 0.01f, 0.05f, 0.f, + 0.3f, -0.4f, 0.5f, 0.f, + -0.055f, 0.01f, 0.05f, 0.f }; + test.setCustomValues(values); + + test.setErrorThreshold(1e-6f); +} + +OCIO_ADD_GPU_TEST(FixedFunction, style_RGB_TO_HSY_VID_inv) +{ + OCIO::FixedFunctionTransformRcPtr func = + OCIO::FixedFunctionTransform::Create(OCIO::FIXED_FUNCTION_RGB_TO_HSY_VID); + func->setDirection(OCIO::TRANSFORM_DIR_INVERSE); + + test.setProcessor(func); + + OCIOGPUTest::CustomValues values; + values.m_inputValues = { + 0.470554752f, 9.12594033f, 0.0326650218f, 0.f, // hsy alpha == 1 + 0.75f, 0.22196741f, 0.38596f, 0.f, + 0.08333333f, 0.12976444f, 0.034974f, 0.f, + 0.333333333333f, 0.606036032f, 0.0056680f, 1.f, // hsy mid alpha + 0.241666666667f, 0.8372990325f, 0.0034440f, 1.f, + 0.734693877551f, 0.752099600f, 0.0005572f, 0.f, // hsy alpha == 0 + 0.96296296f, 9.7034f, -0.1862f, 0.f, + 0.730158730159f, 0.811517000f, -0.0009310f, 0.f }; + test.setCustomValues(values); + + test.setErrorThreshold(1e-6f); +} + OCIO_ADD_GPU_TEST(FixedFunction, style_XYZ_TO_xyY_fwd) { OCIO::FixedFunctionTransformRcPtr func = diff --git a/tests/gpu/GPUUnitTest.cpp b/tests/gpu/GPUUnitTest.cpp index c85ff89af3..6508131218 100644 --- a/tests/gpu/GPUUnitTest.cpp +++ b/tests/gpu/GPUUnitTest.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include @@ -437,24 +436,34 @@ namespace size_t idxDiff = invalidIndex; size_t idxNan = invalidIndex; size_t idxInf = invalidIndex; + constexpr float huge = std::numeric_limits::max(); + float minVals[4] = {huge, huge, huge, huge}; + float maxVals[4] = {-huge, -huge, -huge, -huge}; const bool relativeTest = test->getRelativeComparison(); for(size_t idx=0; idx<(width*height); ++idx) { - DiffComponent(cpuImage, gpuImage, 4 * idx + 0, relativeTest, expectMinValue, - diff, idxDiff, idxInf, idxNan); - DiffComponent(cpuImage, gpuImage, 4 * idx + 1, relativeTest, expectMinValue, - diff, idxDiff, idxInf, idxNan); - DiffComponent(cpuImage, gpuImage, 4 * idx + 2, relativeTest, expectMinValue, - diff, idxDiff, idxInf, idxNan); - DiffComponent(cpuImage, gpuImage, 4 * idx + 3, relativeTest, expectMinValue, - diff, idxDiff, idxInf, idxNan); + for(size_t chan=0; chan<4; ++chan) + { + DiffComponent(cpuImage, gpuImage, 4 * idx + chan, relativeTest, expectMinValue, + diff, idxDiff, idxInf, idxNan); + minVals[chan] = std::min(minVals[chan], + std::isinf(gpuImage[4 * idx + chan]) ? huge: gpuImage[4 * idx + chan]); + maxVals[chan] = std::max(maxVals[chan], + std::isinf(gpuImage[4 * idx + chan]) ? -huge: gpuImage[4 * idx + chan]); + } } size_t componentIdx = idxDiff % 4; size_t pixelIdx = idxDiff / 4; - if (diff > epsilon || idxInf != invalidIndex || idxNan != invalidIndex) + if (diff > epsilon || idxInf != invalidIndex || idxNan != invalidIndex || test->isPrintMinMax()) { std::stringstream err; + err << std::setprecision(10); + err << "\n\nGPU max vals = {" + << maxVals[0] << ", " << maxVals[1] << ", " << maxVals[2] << ", " << maxVals[3] << "}\n" + << "GPU min vals = {" + << minVals[0] << ", " << minVals[1] << ", " << minVals[2] << ", " << minVals[3] << "}\n"; + err << std::setprecision(10) << "\nMaximum error: " << diff << " at pixel: " << pixelIdx << " on component " << componentIdx; diff --git a/tests/gpu/GPUUnitTest.h b/tests/gpu/GPUUnitTest.h index 967325c943..59d07183d2 100644 --- a/tests/gpu/GPUUnitTest.h +++ b/tests/gpu/GPUUnitTest.h @@ -8,6 +8,7 @@ #include #include #include +#include #include "CPUInfoConfig.h" @@ -90,10 +91,15 @@ class OCIOGPUTest inline float getExpectedMinimalValue() const { return m_expectedMinimalValue; } inline void setExpectedMinimalValue(float minValue) { m_expectedMinimalValue = minValue; } - // Dump or not the gpu shader program + // Dump or not the gpu shader program. inline void setVerbose(bool verbose) { m_verbose = verbose; } inline bool isVerbose() const { return m_verbose; } + // Fail the test and print the min and max values of the GPU output, excluding infinities. + // This is useful during development for assessing the range of values being tested. + inline void setPrintMinMax(bool print) { m_printMinMax = print; } + inline bool isPrintMinMax() const { return m_printMinMax; } + inline void setLegacyShader(bool legacy) { m_legacyShader = legacy; } inline bool isLegacyShader() const { return m_legacyShader; } @@ -159,6 +165,7 @@ class OCIOGPUTest bool m_testInfinity{ true }; bool m_performRelativeComparison{ false }; bool m_verbose{ false }; + bool m_printMinMax{ false }; bool m_enabled{ true }; bool m_legacyShader{ false }; unsigned m_legacyShaderLutEdge{ 32 }; diff --git a/tests/gpu/GradingHueCurveOp_test.cpp b/tests/gpu/GradingHueCurveOp_test.cpp new file mode 100644 index 0000000000..3eb31c0826 --- /dev/null +++ b/tests/gpu/GradingHueCurveOp_test.cpp @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + +#include + +#include "GPUUnitTest.h" + +namespace OCIO = OCIO_NAMESPACE; + +namespace +{ + +void GenerateIdentityLut3D(OCIOGPUTest::CustomValues & values, int edgeLen, float min, float max) +{ + const int numChannels = 4; + int num_samples = edgeLen * edgeLen * edgeLen; + std::vector img(num_samples * numChannels, 0.f); + + const float scale = max - min; + float c = 1.0f / ((float)edgeLen - 1.0f); + for (int i = 0; i < edgeLen*edgeLen*edgeLen; i++) + { + img[numChannels*i + 0] = scale * (float)(i%edgeLen) * c + min; + img[numChannels*i + 1] = scale * (float)((i / edgeLen) % edgeLen) * c + min; + img[numChannels*i + 2] = scale * (float)((i / edgeLen / edgeLen) % edgeLen) * c + min; + } + values.m_inputValues = img; +} + +} // anon. + +void GradingHueCurveLog(OCIOGPUTest & test, OCIO::TransformDirection dir, bool dynamic) +{ + // All curves are non-identities. + auto hh = OCIO::GradingBSplineCurve::Create( + { {0.05f, 0.15f}, {0.2f, 0.3f}, {0.35f, 0.4f}, {0.45f, 0.45f}, {0.6f, 0.7f}, {0.8f, 0.85f} }, + OCIO::HUE_HUE); + auto hs = OCIO::GradingBSplineCurve::Create( + { {-0.1f, 1.2f}, {0.2f, 0.7f}, {0.4f, 1.5f}, {0.5f, 0.5f}, {0.6f, 1.4f}, {0.8f, 0.7f} }, + OCIO::HUE_SAT); + auto hl = OCIO::GradingBSplineCurve::Create( + { {0.1f, 1.5f}, {0.2f, 0.7f}, {0.4f, 1.4f}, {0.5f, 0.8f}, {0.8f, 0.5f} }, + OCIO::HUE_LUM); + auto ls = OCIO::GradingBSplineCurve::Create( + { {0.05f, 1.5f}, {0.5f, 0.9f}, {1.1f, 1.4f} }, + OCIO::LUM_SAT); + auto ss = OCIO::GradingBSplineCurve::Create( + { {0.f, 0.1f}, {0.5f, 0.45f}, {1.f, 1.1f} }, + OCIO::SAT_SAT); + auto ll = OCIO::GradingBSplineCurve::Create( + { {-0.02f, -0.04f}, {0.2f, 0.1f}, {0.8f, 0.95f}, {1.1f, 1.2f} }, + OCIO::LUM_LUM); + auto sl = OCIO::GradingBSplineCurve::Create( + { {0.f, 1.2f}, {0.6f, 0.8f}, {0.9f, 1.1f} }, + OCIO::SAT_LUM); + auto hfx = OCIO::GradingBSplineCurve::Create( + { {0.2f, 0.05f}, {0.4f, -0.09f}, {0.6f, -0.2f}, { 0.8f, 0.05f}, {0.99f, -0.02f} }, + OCIO::HUE_FX); + + auto curve = OCIO::GradingHueCurve::Create(hh, hs, hl, ls, ss, ll, sl, hfx); + auto hc = OCIO::GradingHueCurveTransform::Create(OCIO::GRADING_LOG); + if(!hc.get()) + { + throw OCIO::Exception("Cannot create GradingHueCurveTransform."); + } + hc->setValue(curve); + hc->setDirection(dir); + if (dynamic) + { + hc->makeDynamic(); + } + + test.setProcessor(hc); + + // Set up a grid of RGBA custom values. + const int lut_size = 21; + OCIOGPUTest::CustomValues values; + // Choose values so that there is a grid point at 0. + GenerateIdentityLut3D(values, lut_size, -0.075f, 1.425f); + test.setCustomValues(values); + + test.setErrorThreshold(2e-5f); + test.setExpectedMinimalValue(1.0f); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, style_log_fwd) +{ + GradingHueCurveLog(test, OCIO::TRANSFORM_DIR_FORWARD, false); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, style_log_fwd_dynamic) +{ + GradingHueCurveLog(test, OCIO::TRANSFORM_DIR_FORWARD, true); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, style_log_rev) +{ + GradingHueCurveLog(test, OCIO::TRANSFORM_DIR_INVERSE, false); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, style_log_rev_dynamic) +{ + GradingHueCurveLog(test, OCIO::TRANSFORM_DIR_INVERSE, true); +} + +void HueCurveLin(OCIOGPUTest & test, OCIO::TransformDirection dir, bool dynamic) +{ + // All curves are non-identities. + auto hh = OCIO::GradingBSplineCurve::Create( + { {0.05f, 0.15f}, {0.2f, 0.3f}, {0.35f, 0.4f}, {0.45f, 0.45f}, {0.6f, 0.7f}, {0.8f, 0.85f} }, + OCIO::HUE_HUE); + auto hs = OCIO::GradingBSplineCurve::Create( + { {-0.1f, 1.2f}, {0.2f, 0.7f}, {0.4f, 1.5f}, {0.5f, 0.5f}, {0.6f, 1.4f}, {0.8f, 0.7f} }, + OCIO::HUE_SAT); + auto hl = OCIO::GradingBSplineCurve::Create( + { {0.1f, 1.5f}, {0.2f, 0.7f}, {0.4f, 1.4f}, {0.5f, 0.8f}, {0.8f, 0.5f} }, + OCIO::HUE_LUM); + auto ss = OCIO::GradingBSplineCurve::Create( + { {0.f, 0.1f}, {0.5f, 0.45f}, {1.f, 1.1f} }, + OCIO::SAT_SAT); + auto hfx = OCIO::GradingBSplineCurve::Create( + { {0.2f, 0.05f}, {0.4f, -0.09f}, {0.6f, -0.2f}, { 0.8f, 0.05f}, {0.99f, -0.02f} }, + OCIO::HUE_FX); + // Adjust these two, relative to previous test, to work in f-stops. + auto ll = OCIO::GradingBSplineCurve::Create( + { {-8.f, -7.f}, {-2.f, -3.f}, {2.f, 3.5f}, {8.f, 7.f} }, + OCIO::LUM_LUM); + auto ls = OCIO::GradingBSplineCurve::Create( + { {-6.f, 0.9f}, {-3.f, 0.95f}, {0.f, 1.1f}, {2.f, 1.f}, {4.f, 0.6f}, {6.f, 0.55f} }, + OCIO::LUM_SAT); + // Adjusted this one, relative to above, to avoid some artifacts due to high sat boost. + auto sl = OCIO::GradingBSplineCurve::Create( + { {0.f, 1.2f}, {0.6f, 0.8f}, {0.9f, 1.05f}, {1.f, 1.1f} }, + OCIO::SAT_LUM); + + auto curve = OCIO::GradingHueCurve::Create(hh, hs, hl, ls, ss, ll, sl, hfx); + auto hc = OCIO::GradingHueCurveTransform::Create(OCIO::GRADING_LIN); + if(!hc.get()) + { + throw OCIO::Exception("Cannot create GradingHueCurveTransform."); + } + hc->setValue(curve); + hc->setDirection(dir); + if (dynamic) + { + hc->makeDynamic(); + } + + test.setProcessor(hc); + + // Set up a grid of RGBA custom values. + const int lut_size = 21; + OCIOGPUTest::CustomValues values; + // Choose values so that there is a grid point at 0. + GenerateIdentityLut3D(values, lut_size, -0.075f, 1.425f); + test.setCustomValues(values); + + // This test produces some large linear values due to the sat boost, needs a large tolerance. + // Metal is worse than GLSL. + test.setErrorThreshold(4e-4f); + test.setExpectedMinimalValue(1.0f); + test.setRelativeComparison(true); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, style_lin_fwd) +{ + HueCurveLin(test, OCIO::TRANSFORM_DIR_FORWARD, false); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, style_lin_fwd_dynamic) +{ + HueCurveLin(test, OCIO::TRANSFORM_DIR_FORWARD, true); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, style_lin_rev) +{ + HueCurveLin(test, OCIO::TRANSFORM_DIR_INVERSE, false); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, style_lin_rev_dynamic) +{ + HueCurveLin(test, OCIO::TRANSFORM_DIR_INVERSE, true); +} + +void DrawCurve(OCIOGPUTest & test, OCIO::TransformDirection dir, bool dynamic) +{ + OCIO::GradingHueCurveTransformRcPtr hc = OCIO::GradingHueCurveTransform::Create(OCIO::GRADING_LIN); + OCIO::GradingHueCurveRcPtr hueCurve = hc->getValue()->createEditableCopy(); + OCIO::GradingBSplineCurveRcPtr huesat = hueCurve->getCurve(OCIO::HUE_SAT); + + // Enable drawCurveOnly mode. This should only evaluate the HUE-SAT spline for use + // in a user interface. + hueCurve->setDrawCurveOnly(true); + + huesat->setSplineType( OCIO::DIAGONAL_B_SPLINE ); + + huesat->setNumControlPoints(3); + huesat->getControlPoint(0) = OCIO::GradingControlPoint(0.0f, 0.0f); + huesat->getControlPoint(1) = OCIO::GradingControlPoint(0.5f, 0.7f); + huesat->getControlPoint(2) = OCIO::GradingControlPoint(1.0f, 1.0f); + + hc->setValue(hueCurve); + hc->setDirection(dir); + if (dynamic) + { + hc->makeDynamic(); + } + + test.setProcessor(hc); + + test.setErrorThreshold(1e-5f); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, draw_lin_fwd) +{ + DrawCurve(test, OCIO::TRANSFORM_DIR_FORWARD, false); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, draw_lin_fwd_dynamic) +{ + DrawCurve(test, OCIO::TRANSFORM_DIR_FORWARD, true); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, draw_lin_rev) +{ + DrawCurve(test, OCIO::TRANSFORM_DIR_INVERSE, false); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, draw_lin_rev_dynamic) +{ + DrawCurve(test, OCIO::TRANSFORM_DIR_INVERSE, true); +} + +void BypassRGBtoHSY(OCIOGPUTest & test, OCIO::TransformDirection dir, bool dynamic) +{ + OCIO::GradingHueCurveTransformRcPtr hc = OCIO::GradingHueCurveTransform::Create(OCIO::GRADING_LIN); + OCIO::GradingHueCurveRcPtr hueCurve = hc->getValue()->createEditableCopy(); + OCIO::GradingBSplineCurveRcPtr satsat = hueCurve->getCurve(OCIO::SAT_SAT); + satsat->getControlPoint(1) = OCIO::GradingControlPoint(0.4f, 0.6f); + + hc->setRGBToHSY(OCIO::HSY_TRANSFORM_NONE); + + hc->setValue(hueCurve); + hc->setDirection(dir); + if (dynamic) + { + hc->makeDynamic(); + } + + test.setProcessor(hc); + + test.setErrorThreshold(1e-5f); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, bypass_rgbtohsy_fwd) +{ + BypassRGBtoHSY(test, OCIO::TRANSFORM_DIR_FORWARD, false); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, bypass_rgbtohsy_fwd_dynamic) +{ + BypassRGBtoHSY(test, OCIO::TRANSFORM_DIR_FORWARD, true); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, bypass_rgbtohsy_rev) +{ + BypassRGBtoHSY(test, OCIO::TRANSFORM_DIR_INVERSE, false); +} + +OCIO_ADD_GPU_TEST(GradingHueCurve, bypass_rgbtohsy_rev_dynamic) +{ + BypassRGBtoHSY(test, OCIO::TRANSFORM_DIR_INVERSE, true); +} diff --git a/tests/osl/FixedFunctionOp_test.cpp b/tests/osl/FixedFunctionOp_test.cpp index c9d19b41cf..6e77c9546d 100644 --- a/tests/osl/FixedFunctionOp_test.cpp +++ b/tests/osl/FixedFunctionOp_test.cpp @@ -467,3 +467,5 @@ OCIO_OSL_TEST(FixedFunction, style_XYZ_TO_LUV_inv) m_data->m_threshold = 1e-5f; } + +// TODO: Add tests for: RGB_TO_HSY styles. diff --git a/tests/python/GradingDataTest.py b/tests/python/GradingDataTest.py index b015e53702..39cd8ba5cc 100644 --- a/tests/python/GradingDataTest.py +++ b/tests/python/GradingDataTest.py @@ -6,7 +6,9 @@ import sys import PyOpenColorIO as OCIO -from UnitTestUtils import assertEqualRGBM, assertEqualPrimary, assertEqualRGBMSW, assertEqualTone, assertEqualBSpline, assertEqualRGBCurve +from UnitTestUtils import assertEqualRGBM, assertEqualPrimary, assertEqualRGBMSW, \ + assertEqualTone, assertEqualBSpline, assertEqualRGBCurve, assertEqualHueCurve, \ + assertAlmostEqualVector class GradingDataTest(unittest.TestCase): @@ -148,6 +150,8 @@ def test_bspline(self): cpts[3] = OCIO.GradingControlPoint(0.6, 0.7) cpts[4] = OCIO.GradingControlPoint(1, 1) self.assertIsNone(bs.validate()) + self.assertEqual(bs.getSplineType(), OCIO.B_SPLINE) + self.assertEqual(bs.slopesAreDefault(), True) # Move point 4 before point 3 on the x axis so that the control points are not anymore # monotonic. Then, it must throw an exception. @@ -158,13 +162,32 @@ def test_bspline(self): # Restore valid data. cpts[4] = OCIO.GradingControlPoint(1, 1) + # Test constructing via splineType or curveType. + spl1 = OCIO.GradingBSplineCurve(5, OCIO.DIAGONAL_B_SPLINE) + self.assertEqual(spl1.getSplineType(), OCIO.DIAGONAL_B_SPLINE) + spl2 = OCIO.GradingBSplineCurve(5, OCIO.HUE_HUE) + self.assertEqual(spl2.getSplineType(), OCIO.HUE_HUE_B_SPLINE) + spl2.setSplineType(OCIO.PERIODIC_1_B_SPLINE) + self.assertEqual(spl2.getSplineType(), OCIO.PERIODIC_1_B_SPLINE) + # Create a similar bspline curve with alternate constructor. bs2 = OCIO.GradingBSplineCurve([0, 0, 0.1, 0.5, 0.4, 0.6, 0.6, 0.7, 1, 1]) cpts2 = bs2.getControlPoints() + assertEqualBSpline(self, bs, bs2) + self.assertEqual(bs2.getSplineType(), OCIO.B_SPLINE) + self.assertEqual(bs, bs2) + bs2 = OCIO.GradingBSplineCurve([0, 0, 0.1, 0.5, 0.4, 0.6, 0.6, 0.7, 1, 1], + OCIO.LUM_SAT) + cpts2 = bs2.getControlPoints() + # NB: This comparison function is only on the control points. assertEqualBSpline(self, bs, bs2) + self.assertEqual(bs2.getSplineType(), OCIO.HORIZONTAL1_B_SPLINE) + self.assertEqual(bs2.slopesAreDefault(), True) + # The class op== detects that the curve type is different. + self.assertNotEqual(bs, bs2) - # Curve with less control points. + # Curve with fewer control points. bs3 = OCIO.GradingBSplineCurve(4) cpts3 = bs3.getControlPoints() cpts3[1] = OCIO.GradingControlPoint(0.1, 0.5) @@ -186,7 +209,7 @@ def test_bspline(self): with self.assertRaises(AssertionError): assertEqualBSpline(self, bs, bs4) - # Curve with the same number of control points but point at index 2 differ. + # Curve with the same number of control points but point at index 2 differs. bs5 = OCIO.GradingBSplineCurve(5) cpts5 = bs5.getControlPoints() cpts5[1] = OCIO.GradingControlPoint(0.1, 0.5) @@ -215,11 +238,24 @@ def test_bspline(self): bs1.getControlPoints()[2] = OCIO.GradingControlPoint(0.1, 0.4) self.assertNotEqual(cpts1, cpts2) + # Slopes. + s = [0.9, 0.8, 0.7, 0.6, 0.5] + bs1.setSlopes(s) + s1 = bs1.getSlopes() + assertAlmostEqualVector(self, s, s1, delta=1e-6) + with self.assertRaises(OCIO.Exception): + # Length of slopes must match control points. + bs1.setSlopes(s[2:]) + # The comparison now fails since the slopes are not equal. + self.assertNotEqual(bs1, bs2) + def test_rgbcurve(self): """ Test the GradingRGBCurve, creation, default value, modification. """ + # Check default values. + rgbLin = OCIO.GradingRGBCurve(OCIO.GRADING_LIN) defLin = OCIO.GradingBSplineCurve(3) @@ -246,15 +282,91 @@ def test_rgbcurve(self): with self.assertRaises(AssertionError): assertEqualBSpline(self, rgbLog.master, defLin) - rgbVideo = OCIO.GradingRGBCurve(OCIO.GRADING_LOG) + rgbVideo = OCIO.GradingRGBCurve(OCIO.GRADING_VIDEO) assertEqualRGBCurve(self, rgbLog, rgbVideo) # Check comparison operators rgbc1 = OCIO.GradingRGBCurve(OCIO.GRADING_LIN) rgbc2 = OCIO.GradingRGBCurve(OCIO.GRADING_LIN) self.assertEqual(rgbc1, rgbc2) - rgbc1.red.getControlPoints()[1] = OCIO.GradingControlPoint(0.4, 0.4) + self.assertEqual(rgbc1.isIdentity(), True) + rgbc1.red.getControlPoints()[1] = OCIO.GradingControlPoint(0.4, 0.5) self.assertNotEqual(rgbc1, rgbc2) + self.assertEqual(rgbc1.isIdentity(), False) + rgbc1.validate() + + # Check full constructor. + bs1 = OCIO.GradingBSplineCurve([0, 0, 0.1, 0.5, 0.4, 0.6, 0.6, 0.7, 1, 1]) + bs2 = OCIO.GradingBSplineCurve([0.1, 0.5, 0.4, 0.6, 0.6, 0.7, 1, 1.1]) + rgbc1 = OCIO.GradingRGBCurve(bs1, bs2, bs1, bs2) + rgbc1.validate() + self.assertEqual(rgbc1.isIdentity(), False) + assertEqualBSpline(self, rgbc1.green, bs2) + self.assertEqual(rgbc1.green, bs2) + + def test_huecurve(self): + """ + Test the GradingHueCurve, creation, default value, modification. + """ + + # Check default values. + + hueLin = OCIO.GradingHueCurve(OCIO.GRADING_LIN) + + defLin = OCIO.GradingBSplineCurve(3) + cpts = defLin.getControlPoints() + cpts[0] = OCIO.GradingControlPoint(-7, -7) + cpts[1] = OCIO.GradingControlPoint(0, 0) + cpts[2] = OCIO.GradingControlPoint(7, 7) + assertEqualBSpline(self, hueLin.lum_lum, defLin) + + hueLog = OCIO.GradingHueCurve(OCIO.GRADING_LOG) + + defLog = OCIO.GradingBSplineCurve(3) + cpts = defLog.getControlPoints() + cpts[0] = OCIO.GradingControlPoint(0, 0) + cpts[1] = OCIO.GradingControlPoint(0.5, 0.5) + cpts[2] = OCIO.GradingControlPoint(1, 1) + assertEqualBSpline(self, hueLog.lum_lum, defLog) + with self.assertRaises(AssertionError): + assertEqualBSpline(self, hueLog.lum_lum, defLin) + self.assertEqual(hueLog.getDrawCurveOnly(), False) + + hueVideo = OCIO.GradingHueCurve(OCIO.GRADING_VIDEO) + assertEqualHueCurve(self, hueLog, hueVideo) + + # Check comparison operators + + huec1 = OCIO.GradingHueCurve(OCIO.GRADING_LIN) + huec2 = OCIO.GradingHueCurve(OCIO.GRADING_LIN) + self.assertEqual(huec1, huec2) + + huec1.setDrawCurveOnly(True) + self.assertNotEqual(huec1, huec2) + self.assertEqual(huec1.isIdentity(), True) + huec1.setDrawCurveOnly(False) + self.assertEqual(huec1, huec2) + + self.assertEqual(huec1.isIdentity(), True) + huec1.sat_lum.getControlPoints()[1] = OCIO.GradingControlPoint(0.4, 0.8) + self.assertNotEqual(huec1, huec2) + self.assertEqual(huec1.isIdentity(), False) + huec1.validate() + + # Check full constructor. + hh = OCIO.GradingBSplineCurve(5, OCIO.HUE_HUE) + hs = OCIO.GradingBSplineCurve(5, OCIO.HUE_SAT) + hl = OCIO.GradingBSplineCurve(5, OCIO.HUE_LUM) + ls = OCIO.GradingBSplineCurve([0.1, 0.8, 0.9, 0.7], OCIO.LUM_SAT) + ss = OCIO.GradingBSplineCurve(5, OCIO.SAT_SAT) + ll = OCIO.GradingBSplineCurve(5, OCIO.LUM_LUM) + sl = OCIO.GradingBSplineCurve(5, OCIO.SAT_LUM) + hfx = OCIO.GradingBSplineCurve(5, OCIO.HUE_FX) + hcrv = OCIO.GradingHueCurve(hh, hs, hl, ls, ss, ll, sl, hfx) + hcrv.validate() + self.assertEqual(hcrv.isIdentity(), False) + assertEqualBSpline(self, hcrv.lum_sat, ls) + self.assertEqual(hcrv.lum_sat, ls) def test_rgbmsw(self): """ diff --git a/tests/python/GradingHueCurveTransformTest.py b/tests/python/GradingHueCurveTransformTest.py new file mode 100644 index 0000000000..96bdc8e301 --- /dev/null +++ b/tests/python/GradingHueCurveTransformTest.py @@ -0,0 +1,166 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright Contributors to the OpenColorIO Project. + +import unittest +import os +import sys + +import PyOpenColorIO as OCIO +from UnitTestUtils import assertEqualHueCurve + +class GradingHueCurveTransformTest(unittest.TestCase): + + valDefaultLin = OCIO.GradingHueCurve(OCIO.GRADING_LIN) + valDefaultLog = OCIO.GradingHueCurve(OCIO.GRADING_LOG) + + def test_transform_type(self): + """ + Test the getTransformType() method. + """ + gct = OCIO.GradingHueCurveTransform() + self.assertEqual(gct.getTransformType(), OCIO.TRANSFORM_TYPE_GRADING_HUE_CURVE) + + def test_contructor(self): + """ + Test GradingHueCurveTransform constructor without and with keywords. + """ + + gct = OCIO.GradingHueCurveTransform() + self.assertEqual(gct.getStyle(), OCIO.GRADING_LOG) + assertEqualHueCurve(self, gct.getValue(), self.valDefaultLog) + self.assertEqual(gct.isDynamic(), False) + self.assertEqual(gct.getRGBToHSY(), OCIO.HSY_TRANSFORM_1) + self.assertEqual(gct.getDirection(), OCIO.TRANSFORM_DIR_FORWARD) + + gct = OCIO.GradingHueCurveTransform(OCIO.GRADING_LIN) + self.assertEqual(gct.getStyle(), OCIO.GRADING_LIN) + assertEqualHueCurve(self, gct.getValue(), self.valDefaultLin) + self.assertEqual(gct.isDynamic(), False) + self.assertEqual(gct.getRGBToHSY(), OCIO.HSY_TRANSFORM_1) + self.assertEqual(gct.getDirection(), OCIO.TRANSFORM_DIR_FORWARD) + + vals = OCIO.GradingHueCurve(OCIO.GRADING_LOG) + vals.sat_lum = OCIO.GradingBSplineCurve(4, OCIO.SAT_LUM) + cpts = vals.sat_lum.getControlPoints() + cpts[0] = OCIO.GradingControlPoint(0.0, 0.1) + cpts[1] = OCIO.GradingControlPoint(0.1, 0.5) + cpts[2] = OCIO.GradingControlPoint(0.4, 0.6) + cpts[3] = OCIO.GradingControlPoint(0.6, 0.7) + gct = OCIO.GradingHueCurveTransform(style=OCIO.GRADING_VIDEO, values=vals, + dynamic=True, dir=OCIO.TRANSFORM_DIR_INVERSE) + self.assertEqual(gct.getStyle(), OCIO.GRADING_VIDEO) + self.assertEqual(gct.isDynamic(), True) + self.assertEqual(gct.getDirection(), OCIO.TRANSFORM_DIR_INVERSE) + assertEqualHueCurve(self, gct.getValue(), vals) + + def test_style(self): + """ + Test setStyle() and getStyle(). + """ + + gct = OCIO.GradingHueCurveTransform(OCIO.GRADING_LOG) + for style in OCIO.GradingStyle.__members__.values(): + gct.setStyle(style) + self.assertEqual(gct.getStyle(), style) + + def test_misc(self): + """ + Test miscellaneous getters/setters. + """ + + gct = OCIO.GradingHueCurveTransform(OCIO.GRADING_LOG) + self.assertEqual(gct.getRGBToHSY(), OCIO.HSY_TRANSFORM_1) + gct.setRGBToHSY(OCIO.HSY_TRANSFORM_NONE) + self.assertEqual(gct.getRGBToHSY(), OCIO.HSY_TRANSFORM_NONE) + self.assertEqual(gct.getDirection(), OCIO.TRANSFORM_DIR_FORWARD) + gct.setDirection(OCIO.TRANSFORM_DIR_INVERSE) + self.assertEqual(gct.getDirection(), OCIO.TRANSFORM_DIR_INVERSE) + + def test_values(self): + """ + Test setValue() and getValue(). + """ + + gct = OCIO.GradingHueCurveTransform(OCIO.GRADING_LOG) + gct.setValue(self.valDefaultLin) + assertEqualHueCurve(self, gct.getValue(), self.valDefaultLin) + self.assertEqual(gct.getValue(), self.valDefaultLin) + + def test_slopes(self): + """ + Test slopes. + """ + + gct = OCIO.GradingHueCurveTransform(OCIO.GRADING_LOG) + self.assertEqual(gct.slopesAreDefault(OCIO.HUE_HUE), True) + + gct.setSlope(OCIO.HUE_HUE, 3, 1.1) + self.assertEqual(gct.slopesAreDefault(OCIO.HUE_HUE), False) + self.assertEqual(gct.slopesAreDefault(OCIO.HUE_SAT), True) + + s1 = gct.getSlope(OCIO.HUE_HUE, 3) + self.assertAlmostEqual(s1, 1.1, delta=1e-6) + with self.assertRaises(OCIO.Exception): + # Length of slopes must match control points. + gct.setSlope(OCIO.HUE_HUE, 6, 1.1) + + def test_dynamic(self): + """ + Test isDynamic() and makeDynamic(). + """ + + gct = OCIO.GradingHueCurveTransform(OCIO.GRADING_LOG) + self.assertEqual(gct.isDynamic(), False) + gct.makeDynamic() + self.assertEqual(gct.isDynamic(), True) + + def test_validation(self): + """ + Test validate() and setValue(). + """ + + gct = OCIO.GradingHueCurveTransform(OCIO.GRADING_LOG) + gct.validate() + + # 3rd control point x is lower than 2nd control point x. + vals = OCIO.GradingHueCurve(OCIO.GRADING_LOG) + vals.sat_lum = OCIO.GradingBSplineCurve([0, 0, 0.5, 0.2, 0.2, 0.5, 1, 1], OCIO.SAT_LUM) + + with self.assertRaises(OCIO.Exception): + gct.setValue(vals); + + def test_apply_inverse(self): + """ + Test applying transform with inversion. + """ + + gct = OCIO.GradingHueCurveTransform(OCIO.GRADING_LOG) + vals = OCIO.GradingHueCurve(OCIO.GRADING_LOG) + vals.hue_lum = OCIO.GradingBSplineCurve([0, 2., 0.9, 2.], OCIO.HUE_LUM) + gct.setValue(vals) + vals2 = gct.getValue() + self.assertEqual(vals, vals2) + + cfg = OCIO.Config().CreateRaw() + proc = cfg.getProcessor(gct) + cpu = proc.getDefaultCPUProcessor() + + # Apply the transform and keep the result. + pixel = [0.7, 0.5, 0.1] + rgb1 = cpu.applyRGB(pixel) + + # The processing did something. + self.assertAlmostEqual( 0.8, rgb1[0], delta=1e-5) + self.assertAlmostEqual( 0.6, rgb1[1], delta=1e-5) + self.assertAlmostEqual( 0.2, rgb1[2], delta=1e-5) + + # Invert. + gct.setDirection(OCIO.TRANSFORM_DIR_INVERSE) + proc = cfg.getProcessor(gct) + cpu = proc.getDefaultCPUProcessor() + pixel2 = cpu.applyRGB(rgb1) + + # Invert back to original value. + self.assertAlmostEqual(pixel[0], pixel2[0], delta=1e-5) + self.assertAlmostEqual(pixel[1], pixel2[1], delta=1e-5) + self.assertAlmostEqual(pixel[2], pixel2[2], delta=1e-5) diff --git a/tests/python/LegacyViewingPipelineTest.py b/tests/python/LegacyViewingPipelineTest.py index 8b4e3ea5e3..a4f9c025a2 100644 --- a/tests/python/LegacyViewingPipelineTest.py +++ b/tests/python/LegacyViewingPipelineTest.py @@ -197,7 +197,7 @@ def test_get_processor_errors(self): self.assertEqual(str(pipeline), ('DisplayViewTransform: , ' 'LinearCC: ')) + 'fileoutdepth=unknown, matrix=[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], offset=[0.1, 0.2, 0.3, 0]>')) def test_get_processor(self): """ diff --git a/tests/python/LogCameraTransformTest.py b/tests/python/LogCameraTransformTest.py index 5ef890d06e..21aebb0868 100644 --- a/tests/python/LogCameraTransformTest.py +++ b/tests/python/LogCameraTransformTest.py @@ -72,10 +72,10 @@ def test_constructor(self): self.assertEqual(lct.getDirection(), OCIO.TRANSFORM_DIR_INVERSE) self.assertEqual(str(lct), '') + 'base=2.5, logSideSlope=[1.1, 1.2, 1.3], ' + 'logSideOffset=[0.01, 0.02, 0.03], linSideSlope=[1.3, 1.2, 1.1], ' + 'linSideOffset=[0.02, 0.03, 0.01], linSideBreak=[0.1, 0.2, 0.3], ' + 'linearSlope=[0.9, 0.8, 0.7]>') LIN_SB = [0.15, 0.2, 0.3] BASE = 10 diff --git a/tests/python/OpenColorIOTestSuite.py b/tests/python/OpenColorIOTestSuite.py index d78963207b..05a6f52fca 100755 --- a/tests/python/OpenColorIOTestSuite.py +++ b/tests/python/OpenColorIOTestSuite.py @@ -73,6 +73,7 @@ import GpuShaderDescTest import GradingDataTest import GradingPrimaryTransformTest +import GradingHueCurveTransformTest import GradingRGBCurveTransformTest import GradingToneTransformTest import GroupTransformTest @@ -122,6 +123,7 @@ def get_test_modules(): ("GpuShaderDescTest", GpuShaderDescTest), ("GradingDataTest", GradingDataTest), ("GradingPrimaryTransformTest", GradingPrimaryTransformTest), + ("GradingHueCurveTransformTest", GradingHueCurveTransformTest), ("GradingRGBCurveTransformTest", GradingRGBCurveTransformTest), ("GradingToneTransformTest", GradingToneTransformTest), ("GroupTransformTest", GroupTransformTest), diff --git a/tests/python/UnitTestUtils.py b/tests/python/UnitTestUtils.py index dd41586547..20e54b966f 100644 --- a/tests/python/UnitTestUtils.py +++ b/tests/python/UnitTestUtils.py @@ -61,12 +61,28 @@ def assertEqualBSpline(testCase, first, second): else: raise AssertionError("Different number of control points") +def assertAlmostEqualVector(testCase, first, second, delta=1e-6): + if len(first) != len(second): + raise AssertionError("Different number of elements") + for pt1, pt2 in zip(first, second): + testCase.assertAlmostEqual(pt1, pt2, delta=delta) + def assertEqualRGBCurve(testCase, first, second): assertEqualBSpline(testCase, first.red, second.red) assertEqualBSpline(testCase, first.green, second.green) assertEqualBSpline(testCase, first.blue, second.blue) assertEqualBSpline(testCase, first.master, second.master) +def assertEqualHueCurve(testCase, first, second): + assertEqualBSpline(testCase, first.hue_hue, second.hue_hue) + assertEqualBSpline(testCase, first.hue_sat, second.hue_sat) + assertEqualBSpline(testCase, first.hue_lum, second.hue_lum) + assertEqualBSpline(testCase, first.lum_sat, second.lum_sat) + assertEqualBSpline(testCase, first.sat_sat, second.sat_sat) + assertEqualBSpline(testCase, first.lum_lum, second.lum_lum) + assertEqualBSpline(testCase, first.sat_lum, second.sat_lum) + assertEqualBSpline(testCase, first.hue_fx, second.hue_fx) + class MuteLogging: def __init__(self): self.previous_function = None