diff --git a/src/AIS/AIS_ColorScale.cxx b/src/AIS/AIS_ColorScale.cxx index ebc618b81d..afaba07c7f 100644 --- a/src/AIS/AIS_ColorScale.cxx +++ b/src/AIS/AIS_ColorScale.cxx @@ -289,6 +289,100 @@ void AIS_ColorScale::SetColors (const Aspect_SequenceOfColor& theSeq) } } +//======================================================================= +//function : MakeUniformColors +//purpose : +//======================================================================= +Aspect_SequenceOfColor AIS_ColorScale::MakeUniformColors (Standard_Integer theNbColors, + Standard_Real theLightness, + Standard_Real theHueFrom, + Standard_Real theHueTo) +{ + Aspect_SequenceOfColor aResult; + + // adjust range to be within (0, 360], with sign according to theHueFrom and theHueTo + Standard_Real aHueRange = std::fmod (theHueTo - theHueFrom, 360.); + const Standard_Real aHueEps = Precision::Angular() * 180. / M_PI; + if (Abs (aHueRange) <= aHueEps) + { + aHueRange = (aHueRange < 0 ? -360. : 360.); + } + + // treat limit cases + if (theNbColors < 1) + { + return aResult; + } + if (theNbColors == 1) + { + Standard_Real aHue = std::fmod (theHueFrom, 360.); + if (aHue < 0.) + { + aHue += 360.; + } + Quantity_Color aColor (theLightness, 130., aHue, Quantity_TOC_CIELch); + aResult.Append (aColor); + return aResult; + } + + // discretize the range with 1 degree step + const int NBCOLORS = 2 + (int)Abs (aHueRange / 1.); + Standard_Real aHueStep = aHueRange / (NBCOLORS - 1); + NCollection_Array1 aGrid (0, NBCOLORS - 1); + for (Standard_Integer i = 0; i < NBCOLORS; i++) + { + Standard_Real aHue = std::fmod (theHueFrom + i * aHueStep, 360.); + if (aHue < 0.) + { + aHue += 360.; + } + aGrid(i).SetValues (theLightness, 130., aHue, Quantity_TOC_CIELch); + } + + // and compute distances between each two colors in a grid + TColStd_Array1OfReal aMetric (0, NBCOLORS - 1); + Standard_Real aLength = 0.; + for (Standard_Integer i = 0, j = NBCOLORS - 1; i < NBCOLORS; j = i++) + { + aLength += (aMetric(i) = aGrid(i).DeltaE2000 (aGrid(j))); + } + + // determine desired step by distance; + // normally we aim to distribute colors from start to end + // of the range, but if distance between first and last points of the range + // is less than that step (e.g. range is full 360 deg), + // then distribute by the whole 360 deg scope to ensure that first + // and last colors are sufficiently distanced + Standard_Real aDStep = (aLength - aMetric.First()) / (theNbColors - 1); + if (aMetric.First() < aDStep) + { + aDStep = aLength / theNbColors; + } + + // generate sequence + aResult.Append(aGrid(0)); + Standard_Real aParam = 0., aPrev = 0., aTarget = aDStep; + for (int i = 1; i < NBCOLORS; i++) + { + aParam = aPrev + aMetric(i); + while (aTarget <= aParam) + { + float aCoefPrev = float((aParam - aTarget) / (aParam - aPrev)); + float aCoefCurr = float((aTarget - aPrev) / (aParam - aPrev)); + Quantity_Color aColor (aGrid(i).Rgb() * aCoefCurr + aGrid(i-1).Rgb() * aCoefPrev); + aResult.Append (aColor); + aTarget += aDStep; + } + aPrev = aParam; + } + if (aResult.Length() < theNbColors) + { + aResult.Append (aGrid.Last()); + } + Standard_ASSERT_VOID (aResult.Length() == theNbColors, "Failed to generate requested nb of colors"); + return aResult; +} + //======================================================================= //function : SizeHint //purpose : diff --git a/src/AIS/AIS_ColorScale.hxx b/src/AIS/AIS_ColorScale.hxx index a71f138fc8..f261759521 100644 --- a/src/AIS/AIS_ColorScale.hxx +++ b/src/AIS/AIS_ColorScale.hxx @@ -228,6 +228,36 @@ public: //! The length of the sequence should be equal to GetNumberOfIntervals(). Standard_EXPORT void SetColors (const Aspect_SequenceOfColor& theSeq); + //! Populates colors scale by colors of the same lightness value in CIE Lch + //! color space, distributed by hue, with perceptually uniform differences + //! between consequent colors. + //! See MakeUniformColors() for description of parameters. + void SetUniformColors (Standard_Real theLightness, + Standard_Real theHueFrom, Standard_Real theHueTo) + { + SetColors (MakeUniformColors (myNbIntervals, theLightness, theHueFrom, theHueTo)); + SetColorType (Aspect_TOCSD_USER); + } + + //! Generates sequence of colors of the same lightness value in CIE Lch + //! color space (see #Quantity_TOC_CIELch), with hue values in the specified range. + //! The colors are distributed across the range such as to have perceptually + //! same difference between neighbour colors. + //! For each color, maximal chroma value fitting in sRGB gamut is used. + //! + //! @param theNbColors - number of colors to generate + //! @param theLightness - lightness to be used (0 is black, 100 is white, 32 is + //! lightness of pure blue) + //! @param theHueFrom - hue value at the start of the scale + //! @param theHueTo - hue value defining the end of the scale + //! + //! Hue value can be out of the range [0, 360], interpreted as modulo 360. + //! The colors of the scale will be in the order of increasing hue if + //! theHueTo > theHueFrom, and decreasing otherwise. + Standard_EXPORT static Aspect_SequenceOfColor + MakeUniformColors (Standard_Integer theNbColors, Standard_Real theLightness, + Standard_Real theHueFrom, Standard_Real theHueTo); + //! Returns the position of labels concerning color filled rectangles, Aspect_TOCSP_RIGHT by default. Aspect_TypeOfColorScalePosition GetLabelPosition() const { return myLabelPos; } diff --git a/src/Quantity/Quantity_Color.cxx b/src/Quantity/Quantity_Color.cxx index d61a6097d1..cb8962e1af 100644 --- a/src/Quantity/Quantity_Color.cxx +++ b/src/Quantity/Quantity_Color.cxx @@ -40,6 +40,16 @@ static Standard_Real TheEpsilon = 0.0001; || theL < 0.0 || theL > 1.0 \ || theS < 0.0 || theS > 1.0) { throw Standard_OutOfRange("Color out"); } +// Throw exception if CIELab color values are out of range. +#define Quantity_ColorValidateLabRange(theL, thea, theb) \ + if (theL < 0. || theL > 100. || thea < -100. || thea > 100. || theb < -110. || theb > 100.) \ + { throw Standard_OutOfRange("Color out"); } + +// Throw exception if CIELch color values are out of range. +#define Quantity_ColorValidateLchRange(theL, thec, theh) \ + if (theL < 0. || theL > 100. || thec < 0. || thec > 135. || \ + theh < 0.0 || theh > 360.) { throw Standard_OutOfRange("Color out"); } + namespace { //! Raw color for defining list of standard color @@ -107,6 +117,8 @@ NCollection_Vec3 Quantity_Color::valuesOf (const Quantity_NameOfColor the case Quantity_TOC_RGB: return anRgb; case Quantity_TOC_sRGB: return Convert_LinearRGB_To_sRGB (anRgb); case Quantity_TOC_HLS: return Convert_LinearRGB_To_HLS (anRgb); + case Quantity_TOC_CIELab: return Convert_LinearRGB_To_Lab (anRgb); + case Quantity_TOC_CIELch: return Convert_Lab_To_Lch (Convert_LinearRGB_To_Lab (anRgb)); } throw Standard_ProgramError("Internal error"); } @@ -189,32 +201,10 @@ bool Quantity_Color::ColorFromHex (const Standard_CString theHexColorString, // function : Quantity_Color // purpose : // ======================================================================= -Quantity_Color::Quantity_Color (const Standard_Real theR1, const Standard_Real theR2, const Standard_Real theR3, +Quantity_Color::Quantity_Color (const Standard_Real theC1, const Standard_Real theC2, const Standard_Real theC3, const Quantity_TypeOfColor theType) { - switch (theType) - { - case Quantity_TOC_RGB: - { - Quantity_ColorValidateRgbRange(theR1, theR2, theR3); - myRgb.SetValues (float(theR1), float(theR2), float(theR3)); - break; - } - case Quantity_TOC_sRGB: - { - Quantity_ColorValidateRgbRange(theR1, theR2, theR3); - myRgb.SetValues ((float )Convert_sRGB_To_LinearRGB (theR1), - (float )Convert_sRGB_To_LinearRGB (theR2), - (float )Convert_sRGB_To_LinearRGB (theR3)); - break; - } - case Quantity_TOC_HLS: - { - Quantity_ColorValidateHlsRange(theR1, theR2, theR3); - myRgb = Convert_HLS_To_LinearRGB (NCollection_Vec3 (float(theR1), float(theR2), float(theR3))); - break; - } - } + SetValues (theC1, theC2, theC3, theType); } // ======================================================================= @@ -259,29 +249,41 @@ void Quantity_Color::ChangeIntensity (const Standard_Real theDelta) // function : SetValues // purpose : // ======================================================================= -void Quantity_Color::SetValues (const Standard_Real theR1, const Standard_Real theR2, const Standard_Real theR3, +void Quantity_Color::SetValues (const Standard_Real theC1, const Standard_Real theC2, const Standard_Real theC3, const Quantity_TypeOfColor theType) { switch (theType) { case Quantity_TOC_RGB: { - Quantity_ColorValidateRgbRange(theR1, theR2, theR3); - myRgb.SetValues (float(theR1), float(theR2), float(theR3)); + Quantity_ColorValidateRgbRange(theC1, theC2, theC3); + myRgb.SetValues (float(theC1), float(theC2), float(theC3)); break; } case Quantity_TOC_sRGB: { - Quantity_ColorValidateRgbRange(theR1, theR2, theR3); - myRgb.SetValues ((float )Convert_sRGB_To_LinearRGB (theR1), - (float )Convert_sRGB_To_LinearRGB (theR2), - (float )Convert_sRGB_To_LinearRGB (theR3)); + Quantity_ColorValidateRgbRange(theC1, theC2, theC3); + myRgb.SetValues ((float )Convert_sRGB_To_LinearRGB (theC1), + (float )Convert_sRGB_To_LinearRGB (theC2), + (float )Convert_sRGB_To_LinearRGB (theC3)); break; } case Quantity_TOC_HLS: { - Quantity_ColorValidateHlsRange(theR1, theR2, theR3); - myRgb = Convert_HLS_To_LinearRGB (NCollection_Vec3 (float(theR1), float(theR2), float(theR3))); + Quantity_ColorValidateHlsRange(theC1, theC2, theC3); + myRgb = Convert_HLS_To_LinearRGB (NCollection_Vec3 (float(theC1), float(theC2), float(theC3))); + break; + } + case Quantity_TOC_CIELab: + { + Quantity_ColorValidateLabRange(theC1, theC2, theC3); + myRgb = Convert_Lab_To_LinearRGB (NCollection_Vec3 (float(theC1), float(theC2), float(theC3))); + break; + } + case Quantity_TOC_CIELch: + { + Quantity_ColorValidateLchRange(theC1, theC2, theC3); + myRgb = Convert_Lab_To_LinearRGB (Convert_Lch_To_Lab (NCollection_Vec3 (float(theC1), float(theC2), float(theC3)))); break; } } @@ -301,6 +303,76 @@ void Quantity_Color::Delta (const Quantity_Color& theColor, theDI = Standard_Real (aHls1[1] - aHls2[1]); // light } +// ======================================================================= +// function : DeltaE2000 +// purpose : color difference according to CIE Delta E 2000 formula +// see http://brucelindbloom.com/index.html?Eqn_DeltaE_CIE2000.html +// ======================================================================= +Standard_Real Quantity_Color::DeltaE2000 (const Quantity_Color& theOther) const +{ + // get color components in CIE Lch space + Standard_Real aL1, aL2, aa1, aa2, ab1, ab2; + this ->Values (aL1, aa1, ab1, Quantity_TOC_CIELab); + theOther.Values (aL2, aa2, ab2, Quantity_TOC_CIELab); + + // mean L + Standard_Real aLx_mean = 0.5 * (aL1 + aL2); + + // mean C + Standard_Real aC1 = Sqrt (aa1 * aa1 + ab1 * ab1); + Standard_Real aC2 = Sqrt (aa2 * aa2 + ab2 * ab2); + Standard_Real aC_mean = 0.5 * (aC1 + aC2); + Standard_Real aC_mean_pow7 = Pow (aC_mean, 7); + static const double a25_pow7 = Pow (25., 7); + Standard_Real aG = 0.5 * (1. - Sqrt (aC_mean_pow7 / (aC_mean_pow7 + a25_pow7))); + Standard_Real aa1x = aa1 * (1. + aG); + Standard_Real aa2x = aa2 * (1. + aG); + Standard_Real aC1x = Sqrt (aa1x * aa1x + ab1 * ab1); + Standard_Real aC2x = Sqrt (aa2x * aa2x + ab2 * ab2); + Standard_Real aCx_mean = 0.5 * (aC1x + aC2x); + + // mean H + Standard_Real ah1x = (aC1x > TheEpsilon ? ATan2 (ab1, aa1x) * 180. / M_PI : 270.); + Standard_Real ah2x = (aC2x > TheEpsilon ? ATan2 (ab2, aa2x) * 180. / M_PI : 270.); + if (ah1x < 0.) ah1x += 360.; + if (ah2x < 0.) ah2x += 360.; + Standard_Real aHx_mean = 0.5 * (ah1x + ah2x); + Standard_Real aDeltahx = ah2x - ah1x; + if (Abs (aDeltahx) > 180.) + { + aHx_mean += (aHx_mean < 180. ? 180. : -180.); + aDeltahx += (ah1x >= ah2x ? 360. : -360.); + } + + // deltas + Standard_Real aDeltaLx = aL2 - aL1; + Standard_Real aDeltaCx = aC2x - aC1x; + Standard_Real aDeltaHx = 2. * Sqrt (aC1x * aC2x) * Sin (0.5 * aDeltahx * M_PI / 180.); + + // factors + Standard_Real aT = 1. - 0.17 * Cos (( aHx_mean - 30.) * M_PI / 180.) + + 0.24 * Cos ((2. * aHx_mean ) * M_PI / 180.) + + 0.32 * Cos ((3. * aHx_mean + 6.) * M_PI / 180.) - + 0.20 * Cos ((4. * aHx_mean - 63.) * M_PI / 180.); + + Standard_Real aLx_mean50_2 = (aLx_mean - 50.) * (aLx_mean - 50.); + Standard_Real aS_L = 1. + 0.015 * aLx_mean50_2 / Sqrt (20. + aLx_mean50_2); + Standard_Real aS_C = 1. + 0.045 * aCx_mean; + Standard_Real aS_H = 1. + 0.015 * aCx_mean * aT; + + Standard_Real aDelta_theta = 30. * Exp (-(aHx_mean - 275.) * (aHx_mean - 275.) / 625.); + Standard_Real aCx_mean_pow7 = Pow(aCx_mean, 7); + Standard_Real aR_C = 2. * Sqrt (aCx_mean_pow7 / (aCx_mean_pow7 + a25_pow7)); + Standard_Real aR_T = -aR_C * Sin (2. * aDelta_theta * M_PI / 180.); + + // finally, the difference + Standard_Real aDL = aDeltaLx / aS_L; + Standard_Real aDC = aDeltaCx / aS_C; + Standard_Real aDH = aDeltaHx / aS_H; + Standard_Real aDeltaE2000 = Sqrt (aDL * aDL + aDC * aDC + aDH * aDH + aR_T * aDC * aDH); + return aDeltaE2000; +} + // ======================================================================= // function : Name // purpose : @@ -359,6 +431,22 @@ void Quantity_Color::Values (Standard_Real& theR1, Standard_Real& theR2, Standar theR3 = aHls[2]; break; } + case Quantity_TOC_CIELab: + { + const NCollection_Vec3 aLab = Convert_LinearRGB_To_Lab (myRgb); + theR1 = aLab[0]; + theR2 = aLab[1]; + theR3 = aLab[2]; + break; + } + case Quantity_TOC_CIELch: + { + const NCollection_Vec3 aLch = Convert_Lab_To_Lch (Convert_LinearRGB_To_Lab (myRgb)); + theR1 = aLch[0]; + theR2 = aLch[1]; + theR3 = aLch[2]; + break; + } } } @@ -449,156 +537,135 @@ NCollection_Vec3 Quantity_Color::Convert_sRGB_To_HLS (const NCollection_V return NCollection_Vec3 (aHue, aMax, aSaturation); } -/////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////// TESTS //////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////// -static void TestOfColor() +// ======================================================================= +// function : CIELab_f +// purpose : non-linear function transforming XYZ coordinates to CIE Lab +// see http://www.brucelindbloom.com/index.html?Equations.html +// ======================================================================= +static inline double CIELab_f (double theValue) { - Standard_Real H, L, S; - Standard_Real R, G, B; - Standard_Real DC, DI; - Standard_Integer i; + return theValue > 0.008856451679035631 ? Pow (theValue, 1./3.) : (7.787037037037037 * theValue) + 16. / 116.; +} - std::cout << "definition color tests\n----------------------\n"; +// ======================================================================= +// function : CIELab_invertf +// purpose : inverse of non-linear function transforming XYZ coordinates to CIE Lab +// see http://www.brucelindbloom.com/index.html?Equations.html +// ======================================================================= +static inline double CIELab_invertf (double theValue) +{ + double aV3 = theValue * theValue * theValue; + return aV3 > 0.008856451679035631 ? aV3 : (theValue - 16. / 116.) / 7.787037037037037; +} - Quantity_Color C1; - Quantity_Color C2 (Quantity_NOC_ROYALBLUE2); - Quantity_Color C3 (Quantity_NOC_SANDYBROWN); +// ======================================================================= +// function : Convert_LinearRGB_To_Lab +// purpose : convert RGB color to CIE Lab color +// see https://www.easyrgb.com/en/math.php +// ======================================================================= +NCollection_Vec3 Quantity_Color::Convert_LinearRGB_To_Lab (const NCollection_Vec3& theRgb) +{ + double aR = theRgb[0]; + double aG = theRgb[1]; + double aB = theRgb[2]; - // An Introduction to Standard_Object-Oriented Programming and C++ p43 - // a comment for the "const char *const" declaration - const char *const cyan = "YELLOW"; - const char *const blue = "ROYALBLUE2"; - const char *const brown = "SANDYBROWN"; + // convert to XYZ normalized to D65 / 2 deg (CIE 1931) standard illuminant intensities + // see http://www.brucelindbloom.com/index.html?Equations.html + double aX = (aR * 0.4124564 + aG * 0.3575761 + aB * 0.1804375) * 100. / 95.047; + double aY = (aR * 0.2126729 + aG * 0.7151522 + aB * 0.0721750) * 100. / 100.000; + double aZ = (aR * 0.0193339 + aG * 0.1191920 + aB * 0.9503041) * 100. / 108.883; - Standard_Real RR, GG, BB; + // convert to Lab + double afX = CIELab_f (aX); + double afY = CIELab_f (aY); + double afZ = CIELab_f (aZ); - const Standard_Real DELTA = 1.0e-4; + double aL = 116. * afY - 16.; + double aa = 500. * (afX - afY); + double ab = 200. * (afY - afZ); - std::cout << "Get values and names of color tests\n-----------------------------------\n"; + return NCollection_Vec3 ((float)aL, (float)aa, (float)ab); +} - C1.Values (R, G, B, Quantity_TOC_RGB); - if ((R!=1.0) || (G!=1.0) || (B!=0.0)) +// ======================================================================= +// function : Convert_Lab_To_LinearRGB +// purpose : convert CIE Lab color to RGB +// see https://www.easyrgb.com/en/math.php +// ======================================================================= +NCollection_Vec3 Quantity_Color::Convert_Lab_To_LinearRGB (const NCollection_Vec3& theLab) +{ + double aL = theLab[0]; + double aa = theLab[1]; + double ab = theLab[2]; + + // conversion from Lab to RGB can yield point outside of RGB cube, + // in such case we will reduce a and b components gradually + // (by 0.1% at each step) until we fit into the range; + // NB: the procedure could be improved to get more precise + // result but this does not seem really crucial + const int NBSTEPS = 1000; + for (Standard_Integer aRate = NBSTEPS; ; aRate--) { - std::cout << "TEST_ERROR : Values () bad default color\n"; - std::cout << "R, G, B values: " << R << " " << G << " " << B << "\n"; - } - if ( (C1.Red ()!=1.0) || (C1.Green ()!=1.0) || (C1.Blue ()!=0.0) ) - { - std::cout << "TEST_ERROR : Values () bad default color\n"; - std::cout << "R, G, B values: " << C1.Red () << " " << C1.Green () << " " << C1.Blue () << "\n"; - } - if (strcmp (Quantity_Color::StringName (C1.Name()), cyan) != 0) - { - std::cout << "TEST_ERROR : StringName () " << Quantity_Color::StringName (C1.Name()) << " != YELLOW\n"; - } + double aC = aRate / (double)NBSTEPS; - RR=0.262745; GG=0.431373; BB=0.933333; - C1.SetValues (RR, GG, BB, Quantity_TOC_RGB); - C2.Values (R, G, B, Quantity_TOC_RGB); - if ((Abs (RR-R) > DELTA) - || (Abs (GG-G) > DELTA) - || (Abs (BB-B) > DELTA)) - { - std::cout << "TEST_ERROR : Values () bad default color\n"; - std::cout << "R, G, B values: " << R << " " << G << " " << B << "\n"; - } + // convert to XYZ for D65 / 2 deg (CIE 1931) standard illuminant + double afY = (aL + 16.) / 116.; + double afX = aC * aa / 500. + afY; + double afZ = afY - aC * ab / 200.; - if (C2 != C1) - { - std::cout << "TEST_ERROR : IsDifferent ()\n"; - } - if (C3 == C1) - { - std::cout << "TEST_ERROR : IsEqual ()\n"; - } + double aX = CIELab_invertf(afX) * 95.047; + double aY = CIELab_invertf(afY) * 100.000; + double aZ = CIELab_invertf(afZ) * 108.883; - std::cout << "Distance C1,C2 " << C1.Distance (C2) << "\n"; - std::cout << "Distance C1,C3 " << C1.Distance (C3) << "\n"; - std::cout << "Distance C2,C3 " << C2.Distance (C3) << "\n"; - std::cout << "SquareDistance C1,C2 " << C1.SquareDistance (C2) << "\n"; - std::cout << "SquareDistance C1,C3 " << C1.SquareDistance (C3) << "\n"; - std::cout << "SquareDistance C2,C3 " << C2.SquareDistance (C3) << "\n"; + // convert to RGB + // see http://www.brucelindbloom.com/index.html?Equations.html + double aR = (aX * 3.2404542 + aY * -1.5371385 + aZ * -0.4985314) / 100.; + double aG = (aX * -0.9692660 + aY * 1.8760108 + aZ * 0.0415560) / 100.; + double aB = (aX * 0.0556434 + aY * -0.2040259 + aZ * 1.0572252) / 100.; - if (strcmp (Quantity_Color::StringName (C2.Name()), blue) != 0) - { - std::cout << "TEST_ERROR : StringName () " << Quantity_Color::StringName (C2.Name()) << " != ROYALBLUE2\n"; - } - - std::cout << "conversion rgbhls tests\n-----------------------\n"; - Quantity_Color::RgbHls (R, G, B, H, L, S); - Quantity_Color::HlsRgb (H, L, S, R, G, B); - RR=0.262745; GG=0.431373; BB=0.933333; - if ((Abs (RR-R) > DELTA) - || (Abs (GG-G) > DELTA) - || (Abs (BB-B) > DELTA)) - { - std::cout << "TEST_ERROR : RgbHls or HlsRgb bad conversion\n"; - std::cout << "RGB init : " << RR << " " << GG << " " << BB << "\n"; - std::cout << "RGB values : " << R << " " << G << " " << B << "\n"; - std::cout << "Difference RGB : " << RR-R << " " << GG-G << " " << BB-B << "\n"; - } - - std::cout << "distance tests\n--------------\n"; - R = (float ) 0.9568631; G = (float ) 0.6431371; B = (float ) 0.3764711; - C2.SetValues (R, G, B, Quantity_TOC_RGB); - if (C2.Distance (C3) > DELTA) - { - std::cout << "TEST_ERROR : Distance () bad result\n"; - std::cout << "Distance C2 and C3 : " << C2.Distance (C3) << "\n"; - } - - C2.Delta (C3, DC, DI); - if (Abs (DC) > DELTA) - { - std::cout << "TEST_ERROR : Delta () bad result for DC\n"; - } - if (Abs (DI) > DELTA) - { - std::cout << "TEST_ERROR : Delta () bad result for DI\n"; - } - - std::cout << "name tests\n----------\n"; - R = (float ) 0.9568631; G = (float ) 0.6431371; B = (float ) 0.3764711; - C2.SetValues (R, G, B, Quantity_TOC_RGB); - if (strcmp (Quantity_Color::StringName (C2.Name()), brown) != 0) - { - std::cout << "TEST_ERROR : StringName () " << Quantity_Color::StringName (C2.Name()) << " != SANDYBROWN\n"; - } - - std::cout << "contrast change tests\n---------------------\n"; - for (i=1; i<=10; i++) - { - C2.ChangeContrast (10.); - C2.ChangeContrast (-9.09090909); - } - C2.Values (R, G, B, Quantity_TOC_RGB); - RR=0.956863; GG=0.6431371; BB=0.3764711; - if ((Abs (RR-R) > DELTA) - || (Abs (GG-G) > DELTA) - || (Abs (BB-B) > DELTA)) - { - std::cout << "TEST_ERROR : ChangeContrast () bad values\n"; - std::cout << "RGB init : " << RR << " " << GG << " " << BB << "\n"; - std::cout << "RGB values : " << R << " " << G << " " << B << "\n"; + // exit if we are in range or at zero C + if (aRate == 0 || + (aR >= 0. && aR <= 1. && aG >= 0. && aG <= 1. && aB >= 0. && aB <= 1.)) + { + return NCollection_Vec3((float)aR, (float)aG, (float)aB); + } } } // ======================================================================= -// function : Test -// purpose : +// function : Convert_Lab_To_Lch +// purpose : convert CIE Lab color to CIE Lch color +// see https://www.easyrgb.com/en/math.php // ======================================================================= -void Quantity_Color::Test() +NCollection_Vec3 Quantity_Color::Convert_Lab_To_Lch (const NCollection_Vec3& theLab) { - try - { - OCC_CATCH_SIGNALS - TestOfColor(); - } - catch (Standard_Failure const& anException) - { - std::cout << anException << std::endl; - } + double aa = theLab[1]; + double ab = theLab[2]; + + double aC = Sqrt (aa * aa + ab * ab); + double aH = (aC > TheEpsilon ? ATan2 (ab, aa) * 180. / M_PI : 0.); + + if (aH < 0.) aH += 360.; + + return NCollection_Vec3 (theLab[0], (float)aC, (float)aH); +} + +// ======================================================================= +// function : Convert_Lch_To_Lab +// purpose : convert CIE Lch color to CIE Lab color +// see https://www.easyrgb.com/en/math.php +// ======================================================================= +NCollection_Vec3 Quantity_Color::Convert_Lch_To_Lab (const NCollection_Vec3& theLch) +{ + double aC = theLch[1]; + double aH = theLch[2]; + + aH *= M_PI / 180.; + + double aa = aC * Cos (aH); + double ab = aC * Sin (aH); + + return NCollection_Vec3 (theLch[0], (float)aa, (float)ab); } //======================================================================= diff --git a/src/Quantity/Quantity_Color.hxx b/src/Quantity/Quantity_Color.hxx index a413e2f23d..82e3bdc9c8 100644 --- a/src/Quantity/Quantity_Color.hxx +++ b/src/Quantity/Quantity_Color.hxx @@ -46,12 +46,12 @@ public: //! Creates a color according to the definition system theType. //! Throws exception if values are out of range. - Standard_EXPORT Quantity_Color (const Standard_Real theR1, - const Standard_Real theR2, - const Standard_Real theR3, + Standard_EXPORT Quantity_Color (const Standard_Real theC1, + const Standard_Real theC2, + const Standard_Real theC3, const Quantity_TypeOfColor theType); - //! Define color from RGB values. + //! Define color from linear RGB values. Standard_EXPORT explicit Quantity_Color (const NCollection_Vec3& theRgb); //! Returns the name of the nearest color from the Quantity_NameOfColor enumeration. @@ -60,20 +60,24 @@ public: //! Updates the color from specified named color. void SetValues (const Quantity_NameOfColor theName) { myRgb = valuesOf (theName, Quantity_TOC_RGB); } + //! Return the color as vector of 3 float elements. + const NCollection_Vec3& Rgb () const { return myRgb; } + //! Return the color as vector of 3 float elements. operator const NCollection_Vec3&() const { return myRgb; } - //! Returns in theR1, theR2 and theR3 the components of this color according to the color system definition theType. - Standard_EXPORT void Values (Standard_Real& theR1, - Standard_Real& theR2, - Standard_Real& theR3, + //! Returns in theC1, theC2 and theC3 the components of this color + //! according to the color system definition theType. + Standard_EXPORT void Values (Standard_Real& theC1, + Standard_Real& theC2, + Standard_Real& theC3, const Quantity_TypeOfColor theType) const; //! Updates a color according to the mode specified by theType. //! Throws exception if values are out of range. - Standard_EXPORT void SetValues (const Standard_Real theR1, - const Standard_Real theR2, - const Standard_Real theR3, + Standard_EXPORT void SetValues (const Standard_Real theC1, + const Standard_Real theC2, + const Standard_Real theC3, const Quantity_TypeOfColor theType); //! Returns the Red component (quantity of red) of the color within range [0.0; 1.0]. @@ -138,6 +142,13 @@ public: Standard_EXPORT void Delta (const Quantity_Color& theColor, Standard_Real& DC, Standard_Real& DI) const; + //! Returns the value of the perceptual difference between this color + //! and @p theOther, computed using the CIEDE2000 formula. + //! The difference is in range [0, 100.], with 1 approximately corresponding + //! to the minimal percievable difference (usually difference 5 or greater is + //! needed for the difference to be recognizable in practice). + Standard_EXPORT Standard_Real DeltaE2000 (const Quantity_Color& theOther) const; + public: //! Returns the color from Quantity_NameOfColor enumeration nearest to specified RGB values. @@ -170,6 +181,9 @@ public: return true; } +public: + //!@name Routines converting colors between different encodings and color spaces + //! Parses the string as a hex color (like "#FF0" for short sRGB color, or "#FFFF00" for sRGB color) //! @param theHexColorString the string to be parsed //! @param theColor a color that is a result of parsing @@ -206,6 +220,19 @@ public: return Convert_sRGB_To_LinearRGB (Convert_HLS_To_sRGB (theHls)); } + //! Converts linear RGB components into CIE Lab ones. + Standard_EXPORT static NCollection_Vec3 Convert_LinearRGB_To_Lab (const NCollection_Vec3& theRgb); + + //! Converts CIE Lab components into CIE Lch ones. + Standard_EXPORT static NCollection_Vec3 Convert_Lab_To_Lch (const NCollection_Vec3& theLab); + + //! Converts CIE Lab components into linear RGB ones. + //! Note that the resulting values may be out of the valid range for RGB. + Standard_EXPORT static NCollection_Vec3 Convert_Lab_To_LinearRGB (const NCollection_Vec3& theLab); + + //! Converts CIE Lch components into CIE Lab ones. + Standard_EXPORT static NCollection_Vec3 Convert_Lch_To_Lab (const NCollection_Vec3& theLch); + //! Convert the color value to ARGB integer value, with alpha equals to 0. //! So the output is formatted as 0x00RRGGBB. //! Note that this unpacking does NOT involve non-linear sRGB -> linear RGB conversion, @@ -235,8 +262,6 @@ public: theColor.SetValues (aColor.r() / 255.0, aColor.g() / 255.0, aColor.b() / 255.0, Quantity_TOC_sRGB); } -public: - //! Convert linear RGB component into sRGB using OpenGL specs formula (double precision), also known as gamma correction. static Standard_Real Convert_LinearRGB_To_sRGB (Standard_Real theLinearValue) { @@ -287,8 +312,6 @@ public: Convert_sRGB_To_LinearRGB (theRGB.b())); } -public: - //! Convert linear RGB component into sRGB using approximated uniform gamma coefficient 2.2. static float Convert_LinearRGB_To_sRGB_approx22 (float theLinearValue) { return powf (theLinearValue, 2.2f); } @@ -311,14 +334,6 @@ public: Convert_sRGB_To_LinearRGB_approx22 (theRGB.b())); } -public: - - //! Returns the value used to compare two colors for equality; 0.0001 by default. - Standard_EXPORT static Standard_Real Epsilon(); - - //! Set the value used to compare two colors for equality. - Standard_EXPORT static void SetEpsilon (const Standard_Real theEpsilon); - //! Converts HLS components into sRGB ones. static void HlsRgb (const Standard_Real theH, const Standard_Real theL, const Standard_Real theS, Standard_Real& theR, Standard_Real& theG, Standard_Real& theB) @@ -339,8 +354,13 @@ public: theS = aHls[2]; } - //! Internal test - Standard_EXPORT static void Test(); +public: + + //! Returns the value used to compare two colors for equality; 0.0001 by default. + Standard_EXPORT static Standard_Real Epsilon(); + + //! Set the value used to compare two colors for equality. + Standard_EXPORT static void SetEpsilon (const Standard_Real theEpsilon); //! Dumps the content of me into the stream Standard_EXPORT void DumpJson (Standard_OStream& theOStream, Standard_Integer theDepth = -1) const; diff --git a/src/Quantity/Quantity_TypeOfColor.hxx b/src/Quantity/Quantity_TypeOfColor.hxx index ba70a48f1a..affa420a8f 100644 --- a/src/Quantity/Quantity_TypeOfColor.hxx +++ b/src/Quantity/Quantity_TypeOfColor.hxx @@ -20,13 +20,50 @@ //! Identifies color definition systems. enum Quantity_TypeOfColor { - Quantity_TOC_RGB, //!< normalized linear RGB (red, green, blue) values within range [0..1] for each component - Quantity_TOC_sRGB, //!< normalized non-linear gamma-shifted RGB (red, green, blue) values within range [0..1] for each component - Quantity_TOC_HLS, //!< hue + light + saturation components, where: - //! - First component is the Hue (H) angle in degrees within range [0.0; 360.0], 0.0 being Red; - //! value -1.0 is a special value reserved for grayscale color (S should be 0.0). - //! - Second component is the Lightness (L) within range [0.0; 1.0] - //! - Third component is the Saturation (S) within range [0.0; 1.0] + //! Normalized linear RGB (red, green, blue) values within range [0..1] for each component + Quantity_TOC_RGB, + + //! Normalized non-linear gamma-shifted RGB (red, green, blue) values within range [0..1] for each component + Quantity_TOC_sRGB, + + //! Hue + light + saturation components, where: + //! - First component is the Hue (H) angle in degrees within range [0.0; 360.0], 0.0 being Red; + //! value -1.0 is a special value reserved for grayscale color (S should be 0.0). + //! - Second component is the Lightness (L) within range [0.0; 1.0] + //! - Third component is the Saturation (S) within range [0.0; 1.0] + Quantity_TOC_HLS, + + //! CIE L*a*b* color space, constructed to be perceptually uniform for human eye. + //! The values are assumed to be with respect to D65 2° white point. + //! + //! The color is defined by: + //! - L: lightness in range [0, 100] (from black to white) + //! - a: green-to-red axis, approximately in range [-90, 100] + //! - b: blue-to-yellow axis, approximately in range [-110, 95] + //! + //! Note that not all combinations of L, a, and b values represent visible + //! colors, and RGB cube takes only part of visible color space. + //! + //! When Lab color is converted to RGB, a and b components may be reduced + //! (with the same proportion) to fit the result into the RGB range. + Quantity_TOC_CIELab, + + //! CIE L*c*h* color space, same as L*a*b* in cylindrical coordinates: + //! - L: lightness in range [0, 100] (from black to white) + //! - c: chroma, approximately in range [0, 135], 0 corresponds to greyscale + //! - h: hue angle, in range [0., 360.] + //! + //! The hue values of standard colors are approximately: + //! - red at 40, + //! - yellow at 103, + //! - green at 136, + //! - cyan at 196, + //! - blue at 306, + //! - magenta at 328. + //! + //! When Lch color is converted to RGB, chroma component may be reduced + //! to fit the color into the RGB range. + Quantity_TOC_CIELch }; #endif // _Quantity_TypeOfColor_HeaderFile diff --git a/src/ViewerTest/ViewerTest_ViewerCommands.cxx b/src/ViewerTest/ViewerTest_ViewerCommands.cxx index 11e690014e..c64f27916b 100644 --- a/src/ViewerTest/ViewerTest_ViewerCommands.cxx +++ b/src/ViewerTest/ViewerTest_ViewerCommands.cxx @@ -4831,6 +4831,14 @@ static int VColorScale (Draw_Interpretor& theDI, aColorScale->SetColors (aSeq); aColorScale->SetColorType (Aspect_TOCSD_USER); } + else if (aFlag == "-uniform") + { + const Standard_Real aLightness = Draw::Atof (theArgVec[++anArgIter]); + const Standard_Real aHueStart = Draw::Atof (theArgVec[++anArgIter]); + const Standard_Real aHueEnd = Draw::Atof (theArgVec[++anArgIter]); + aColorScale->SetUniformColors (aLightness, aHueStart, aHueEnd); + aColorScale->SetColorType (Aspect_TOCSD_USER); + } else if (aFlag == "-labels" || aFlag == "-freelabels") { @@ -13750,6 +13758,91 @@ static int VViewCube (Draw_Interpretor& , return 0; } +//=============================================================================================== +//function : VColorConvert +//purpose : +//=============================================================================================== +static int VColorConvert (Draw_Interpretor& theDI, Standard_Integer theNbArgs, const char** theArgVec) +{ + if (theNbArgs != 6) + { + std::cerr << "Error: command syntax is incorrect, see help" << std::endl; + return 1; + } + + Standard_Boolean convertFrom = (! strcasecmp (theArgVec[1], "from")); + if (! convertFrom && strcasecmp (theArgVec[1], "to")) + { + std::cerr << "Error: first argument must be either \"to\" or \"from\"" << std::endl; + return 1; + } + + const char* aTypeStr = theArgVec[2]; + Quantity_TypeOfColor aType = Quantity_TOC_RGB; + if (! strcasecmp (aTypeStr, "srgb")) + { + aType = Quantity_TOC_sRGB; + } + else if (! strcasecmp (aTypeStr, "hls")) + { + aType = Quantity_TOC_HLS; + } + else if (! strcasecmp (aTypeStr, "lab")) + { + aType = Quantity_TOC_CIELab; + } + else if (! strcasecmp (aTypeStr, "lch")) + { + aType = Quantity_TOC_CIELch; + } + else + { + std::cerr << "Error: unknown colorspace type: " << aTypeStr << std::endl; + return 1; + } + + double aC1 = Draw::Atof (theArgVec[3]); + double aC2 = Draw::Atof (theArgVec[4]); + double aC3 = Draw::Atof (theArgVec[5]); + + Quantity_Color aColor (aC1, aC2, aC3, convertFrom ? aType : Quantity_TOC_RGB); + aColor.Values (aC1, aC2, aC3, convertFrom ? Quantity_TOC_RGB : aType); + + // print values with 6 decimal digits + char buffer[1024]; + Sprintf (buffer, "%.6f %.6f %.6f", aC1, aC2, aC3); + theDI << buffer; + + return 0; +} + +//=============================================================================================== +//function : VColorDiff +//purpose : +//=============================================================================================== +static int VColorDiff (Draw_Interpretor& theDI, Standard_Integer theNbArgs, const char** theArgVec) +{ + if (theNbArgs != 7) + { + std::cerr << "Error: command syntax is incorrect, see help" << std::endl; + return 1; + } + + double aR1 = Draw::Atof (theArgVec[1]); + double aG1 = Draw::Atof (theArgVec[2]); + double aB1 = Draw::Atof (theArgVec[3]); + double aR2 = Draw::Atof (theArgVec[4]); + double aG2 = Draw::Atof (theArgVec[5]); + double aB2 = Draw::Atof (theArgVec[6]); + + Quantity_Color aColor1 (aR1, aG1, aB1, Quantity_TOC_RGB); + Quantity_Color aColor2 (aR2, aG2, aB2, Quantity_TOC_RGB); + + theDI << aColor1.DeltaE2000 (aColor2); + + return 0; +} + //======================================================================= //function : ViewerCommands //purpose : @@ -13992,9 +14085,11 @@ void ViewerTest::ViewerCommands(Draw_Interpretor& theCommands) "\n\t\t: [-labels Label1 Label2 ...] [-label Index Label]" "\n\t\t: [-freeLabels NbOfLabels Label1 Label2 ...]" "\n\t\t: [-xy Left=0 Bottom=0]" + "\n\t\t: [-uniform lightness hue_from hue_to]" "\n\t\t: -demo - displays a color scale with demonstratio values" "\n\t\t: -colors - set colors for all intervals" "\n\t\t: -color - set color for specific interval" + "\n\t\t: -uniform - generate colors with the same lightness" "\n\t\t: -textpos - horizontal label position relative to color scale bar" "\n\t\t: -labelAtBorder - vertical label position relative to color interval;" "\n\t\t: at border means the value inbetween neighbor intervals," @@ -14006,7 +14101,7 @@ void ViewerTest::ViewerCommands(Draw_Interpretor& theCommands) "\n\t\t: -title - set title" "\n\t\t: -reversed - setup smooth color transition between intervals" "\n\t\t: -smoothTransition - swap colorscale direction" - "\n\t\t: -hueRange - set hue angles corresponding to minimum and maximum values" + "\n\t\t: -hueRange - set hue angles corresponding to minimum and maximum values", __FILE__, VColorScale, group); theCommands.Add("vgraduatedtrihedron", "vgraduatedtrihedron : -on/-off [-xname Name] [-yname Name] [-zname Name] [-arrowlength Value]\n" @@ -14606,4 +14701,13 @@ void ViewerTest::ViewerCommands(Draw_Interpretor& theCommands) "\n\t\t: -duration Seconds animation duration in seconds", __FILE__, VViewCube, group); + theCommands.Add("vcolorconvert" , + "vcolorconvert {from|to} type C1 C2 C2" + "\n\t\t: vcolorconvert from type C1 C2 C2: Converts color from specified color space to linear RGB" + "\n\t\t: vcolorconvert to type R G B: Converts linear RGB color to specified color space" + "\n\t\t: type can be sRGB, HLS, Lab, or Lch", + __FILE__,VColorConvert,group); + theCommands.Add("vcolordiff" , + "vcolordiff R1 G1 B1 R2 G2 B2: returns CIEDE2000 color difference between two RGB colors", + __FILE__,VColorDiff,group); } diff --git a/tests/bugs/vis/bug22632 b/tests/bugs/vis/bug22632 index 8bd5918029..7ccfba7ffe 100644 --- a/tests/bugs/vis/bug22632 +++ b/tests/bugs/vis/bug22632 @@ -1,5 +1,5 @@ puts "============" -puts "OCC25632" +puts "OCC22632" puts "Display logarithmic colorscale." puts "============" puts "" diff --git a/tests/bugs/vis/bug31454 b/tests/bugs/vis/bug31454 new file mode 100644 index 0000000000..a108d5da86 --- /dev/null +++ b/tests/bugs/vis/bug31454 @@ -0,0 +1,23 @@ +puts "============" +puts "0031454: Visualization - perceptually uniform color scale" +puts "============" +puts "" + +vclear +vinit View1 -width 600 +#vsetcolorbg 1 1 1 +vaxo + +set nbcolors 10 + +# create default color scale with 20 steps +vcolorscale hsl -range 0 1 $nbcolors -xy 0 0 -title HSL + +# create color scales with uniform lightness +vcolorscale lch30 -range 0 1 $nbcolors -xy 100 0 -uniform 30 300 40 -title L=30 +vcolorscale lch40 -range 0 1 $nbcolors -xy 200 0 -uniform 40 300 40 -title L=40 +vcolorscale lch50 -range 0 1 $nbcolors -xy 300 0 -uniform 50 300 40 -title L=50 +vcolorscale lch60 -range 0 1 $nbcolors -xy 400 0 -uniform 60 300 40 -title L=60 +vcolorscale lch70 -range 0 1 $nbcolors -xy 500 0 -uniform 70 300 40 -title L=70 + +vdump ${imagedir}/${casename}.png diff --git a/tests/v3d/colors/begin b/tests/v3d/colors/begin new file mode 100644 index 0000000000..b8000f8739 --- /dev/null +++ b/tests/v3d/colors/begin @@ -0,0 +1,10 @@ +# Auxiliary procedure to compare triplet of numbers +# against reference values, with tolerance +proc check3reals {name value1 value2 value3 ref1 ref2 ref3 tol} { + checkreal "${name}, component 1" $value1 $ref1 $tol 1e-6 + checkreal "${name}, component 2" $value2 $ref2 $tol 1e-6 + checkreal "${name}, component 3" $value3 $ref3 $tol 1e-6 +} + +# weird way to disable unnecessary screen dumps +set to_dump_screen 0 \ No newline at end of file diff --git a/tests/v3d/colors/de2000 b/tests/v3d/colors/de2000 new file mode 100644 index 0000000000..efeb477b6c --- /dev/null +++ b/tests/v3d/colors/de2000 @@ -0,0 +1,58 @@ +# Check calculation of CIE Ddlta E 2000 color difference + +# Reference data are obtained using online calculator +# http://brucelindbloom.com/index.html?ColorDifferenceCalc.html +# or +# https://cielab.xyz/ +# Second one also shows color and reports if it is out of RGB gamut. +# +# Note that values out of RGB gamut would be amended during +# conversion to RGB and thus the result would be different! +# +# Samples aimed at testing discontinuity of CIEDE2000 +# formula are very sensitive to accuracy, we need higher tolerance +# because conversion is done via RGB floats and loses precision. +# +# Format: { {L1 a1 b1} {L2 a2 b2} expected_diff [tolerance] } +set lab_diff_samples { + { # synthetic color pairs } + { {0 0 0} {50 0 0} 36.519268 } + { {50 0 0} {100 0 0} 36.519268 } + { {0 0 0} {100 0 0} 100. } + { {20 10 10} {80 10 10} 60. } + { {50 0 0} {50 0 50} 23.529412 } + { {50 60 60} {50 60 0} 28.016927 } + { {30 30 40} {30 30 -60} 44.606253 } + + { # discontinuity of CIEDE2000 formula } + { {30 50.00 40} {20 -10 -8} 39.105394 0.001 } + { {30 50.01 40} {20 -10 -8} 43.53247 0.001 } + { {20 30.00 30.01} {60 -10 -10} 49.416742 0.05 } + { {20 30.01 30.00} {60 -10 -10} 52.448227 0.05 } + + { # randomly generated data } + { {73.4450 34.9839 -24.6753} {87.6216 -18.4863 57.8838} 62.402500 } + { {93.6166 -27.3677 29.3893} {46.9191 12.3400 -27.5948} 54.640034 } + { {53.9062 61.0929 -51.7583} {65.5157 26.3376 -37.0512} 15.679046 } + { {83.6996 9.3358 -24.5571} {93.2268 -3.8589 3.5217} 23.158692 } + { {64.8053 -27.3177 -8.9602} {65.8225 37.3192 -38.1465} 34.670514 } + { {94.7633 -19.7915 69.2787} {90.9238 -16.7535 4.1936} 26.093024 } + { {85.4699 5.6078 -11.1083} {67.9455 -28.4536 7.8808} 31.115070 } + { {83.5473 -15.7170 8.3546} {81.3193 -37.2851 57.7090} 19.696753 } + { {75.7406 -12.0785 -12.3505} {80.0810 -54.8591 52.1739} 35.834099 } + { {62.8209 32.1209 16.9113} {82.1106 25.0843 -7.9416} 21.178519 } +} + +foreach sample $lab_diff_samples { + set lab1 [lindex $sample 0] + if { $lab1 == "#" } continue + set lab2 [lindex $sample 1] + set dref [lindex $sample 2] + set diff [vcolordiff {*}[vcolorconvert from lab {*}$lab1] {*}[vcolorconvert from lab {*}$lab2]] + + # use tolerance 1e-4 except if other value is defined in sample data + set tol [lindex $sample 3] + if { $tol == "" } { set tol 1e-4 } + + checkreal "CIEDE2000 diff of Lab colors ($lab1) and ($lab2)" $diff $dref $tol 1e-6 +} diff --git a/tests/v3d/colors/de2000_sharma b/tests/v3d/colors/de2000_sharma new file mode 100644 index 0000000000..b399e225b4 --- /dev/null +++ b/tests/v3d/colors/de2000_sharma @@ -0,0 +1,68 @@ +# Check calculation of CIE Ddlta E 2000 color difference + +# Reference data taken from +# "The CIEDE2000 Color-Difference Formula: Implementation Notes, +# Supplementary Test Data, and Mathematical Observations", +# G. Sharma, W. Wu, E. N. Dalal, +# Color Research and Application, vol. 30. No. 1, pp. 21-30, February 2005 +# http://www2.ece.rochester.edu/~gsharma/ciede2000/ +# +# Note: samples 1 to 6 and 28 are commented because the colors +# are out of RGB gamut. +# Samples 10 and 15 are aimed at testing discontinuity of CIEDE2000 +# formula and so are very sensitive to accuracy, we need higher tolerance +# because conversion is done via RGB floats and loses precision. +# +# Format: L1 a1 b1 L2 a2 b2 expected_diff [tolerance] +set lab_diff_samples { +# 50.0000 2.6772 -79.7751 50.0000 0.0000 -82.7485 2.0425 +# 50.0000 3.1571 -77.2803 50.0000 0.0000 -82.7485 2.8615 +# 50.0000 2.8361 -74.0200 50.0000 0.0000 -82.7485 3.4412 +# 50.0000 -1.3802 -84.2814 50.0000 0.0000 -82.7485 1.0000 +# 50.0000 -1.1848 -84.8006 50.0000 0.0000 -82.7485 1.0000 +# 50.0000 -0.9009 -85.5211 50.0000 0.0000 -82.7485 1.0000 +50.0000 0.0000 0.0000 50.0000 -1.0000 2.0000 2.3669 +50.0000 -1.0000 2.0000 50.0000 0.0000 0.0000 2.3669 +50.0000 2.4900 -0.0010 50.0000 -2.4900 0.0009 7.1792 +50.0000 2.4900 -0.0010 50.0000 -2.4900 0.0010 7.1792 0.05 +50.0000 2.4900 -0.0010 50.0000 -2.4900 0.0011 7.2195 +50.0000 2.4900 -0.0010 50.0000 -2.4900 0.0012 7.2195 +50.0000 -0.0010 2.4900 50.0000 0.0009 -2.4900 4.8045 +50.0000 -0.0010 2.4900 50.0000 0.0010 -2.4900 4.8045 +50.0000 -0.0010 2.4900 50.0000 0.0011 -2.4900 4.7461 0.06 +50.0000 2.5000 0.0000 50.0000 0.0000 -2.5000 4.3065 +50.0000 2.5000 0.0000 73.0000 25.0000 -18.0000 27.1492 +50.0000 2.5000 0.0000 61.0000 -5.0000 29.0000 22.8977 +50.0000 2.5000 0.0000 56.0000 -27.0000 -3.0000 31.9030 +50.0000 2.5000 0.0000 58.0000 24.0000 15.0000 19.4535 +50.0000 2.5000 0.0000 50.0000 3.1736 0.5854 1.0000 +50.0000 2.5000 0.0000 50.0000 3.2972 0.0000 1.0000 +50.0000 2.5000 0.0000 50.0000 1.8634 0.5757 1.0000 +50.0000 2.5000 0.0000 50.0000 3.2592 0.3350 1.0000 +60.2574 -34.0099 36.2677 60.4626 -34.1751 39.4387 1.2644 +63.0109 -31.0961 -5.8663 62.8187 -29.7946 -4.0864 1.2630 +61.2901 3.7196 -5.3901 61.4292 2.2480 -4.9620 1.8731 +# 35.0831 -44.1164 3.7933 35.0232 -40.0716 1.5901 1.8645 +22.7233 20.0904 -46.6940 23.0331 14.9730 -42.5619 2.0373 +36.4612 47.8580 18.3852 36.2715 50.5065 21.2231 1.4146 +90.8027 -2.0831 1.4410 91.1528 -1.6435 0.0447 1.4441 +90.9257 -0.5406 -0.9208 88.6381 -0.8985 -0.7239 1.5381 +6.7747 -0.2908 -2.4247 5.8714 -0.0985 -2.2286 0.6377 +2.0776 0.0795 -1.1350 0.9033 -0.0636 -0.5514 0.9082 +} + +set index -1 +foreach sample [split $lab_diff_samples \n] { + incr index + set lab1 [lrange $sample 0 2] + if { [lindex $lab1 0] == "#" || $lab1 == "" } continue + set lab2 [lrange $sample 3 5] + set dref [lindex $sample 6] + set diff [vcolordiff {*}[vcolorconvert from lab {*}$lab1] {*}[vcolorconvert from lab {*}$lab2]] + + # use tolerance 1e-3 except if other value is defined in sample data + set tol [lindex $sample 7] + if { $tol == "" } { set tol 1e-3 } + + checkreal "Sample $index: Lab ($lab1) and ($lab2), diff" $diff $dref $tol 1e-6 +} diff --git a/tests/v3d/colors/rgb2lab b/tests/v3d/colors/rgb2lab new file mode 100644 index 0000000000..a0cd3a87d7 --- /dev/null +++ b/tests/v3d/colors/rgb2lab @@ -0,0 +1,37 @@ +# Check conversion of RGB colors to CIE Lab color space + +# Samples are obtained (with Ref. White D65, Gamma = 1 for linear RGB) using +# http://brucelindbloom.com/index.html?ColorCalculator.html +set rgb_to_lab_samples { + { # black, white, 50% gray } + { {0 0 0} {0 0 0} } + { {1 1 1} {100 0 0} } + { {0.5 0.5 0.5} {76.0693 0 0} } + + { # pure colors } + { {1 0 0} {53.2408 80.0925 67.2032} } + { {0 1 0} {87.7347 -86.1827 83.1793} } + { {0 0 1} {32.2970 79.1875 -107.8602} } + { {0 1 1} {91.1132 -48.0875 -14.1312} } + { {1 1 0} {97.1393 -21.5537 94.4780} } + { {1 0 1} {60.3242 98.2343 -60.8249} } + + { # shades of pure red } + { {0.1 0 0} {16.1387 37.1756 25.0600} } + { {0.3 0 0} {30.3521 53.6166 44.0349} } + { {0.5 0 0} {38.9565 63.5695 53.3392} } + { {0.7 0 0} {45.4792 71.1144 59.6700} } + { {0.9 0 0} {50.8512 77.3285 64.8840} } + + { # random colors } + { {0.3 0.5 0.9} {75.2228 0.7560 -31.8425} } +} + +foreach sample $rgb_to_lab_samples { + set rgb [lindex $sample 0] + if { $rgb == "#" } continue + + set ref [lindex $sample 1] + set lab [vcolorconvert to lab {*}$rgb] + check3reals "RGB ($rgb) to Lab" {*}$lab {*}$ref 1e-4 +} diff --git a/tests/v3d/colors/rgb2lch b/tests/v3d/colors/rgb2lch new file mode 100644 index 0000000000..16218671fd --- /dev/null +++ b/tests/v3d/colors/rgb2lch @@ -0,0 +1,38 @@ +# Check conversion of RGB colors to CIE Lch color space + +# Samples are obtained (with Ref. White D65, Gamma = 1 for linear RGB) using +# http://brucelindbloom.com/index.html?ColorCalculator.html +# Note that for c = 0 we have h = 0 (not 270 as in the above link) +set rgb_to_lch_samples { + { # black, white, 50% gray } + { {0 0 0} {0 0 0} } + { {1 1 1} {100 0 0} } + { {0.5 0.5 0.5} {76.0693 0 0} } + + { # pure colors } + { {1 0 0} {53.2408 104.5518 39.9990} } + { {0 1 0} {87.7347 119.7759 136.0160} } + { {0 0 1} {32.2970 133.8076 306.2849} } + { {0 1 1} {91.1132 50.1209 196.3762} } + { {1 1 0} {97.1393 96.9054 102.8512} } + { {1 0 1} {60.3242 115.5407 328.2350} } + + { # shades of pure red } + { {0.1 0 0} {16.1387 44.8334 33.9838} } + { {0.3 0 0} {30.3521 69.3816 39.3960} } + { {0.5 0 0} {38.9565 82.9828 39.9990} } + { {0.7 0 0} {45.4792 92.8320 39.9990} } + { {0.9 0 0} {50.8512 100.9436 39.9990} } + + { # random colors } + { {0.3 0.5 0.9} {75.2228 31.8514 271.3601} } +} + +foreach sample $rgb_to_lch_samples { + set rgb [lindex $sample 0] + if { $rgb == "#" } continue + + set ref [lindex $sample 1] + set lch [vcolorconvert to lch {*}$rgb] + check3reals "RGB ($rgb) to Lch" {*}$lch {*}$ref 1e-4 +} diff --git a/tests/v3d/colors/stability b/tests/v3d/colors/stability new file mode 100644 index 0000000000..e6c965f80f --- /dev/null +++ b/tests/v3d/colors/stability @@ -0,0 +1,16 @@ +# Check stability of conversion of RGB colors to CIE Lab and Lch +# color spaces and back on random colors + +# check color diff on random colors +for {set i 1} {$i < 1000} {incr i} { + set rgb "[expr rand()] [expr rand()] [expr rand()]" + + set lab [vcolorconvert to lab {*}$rgb] + set lch [vcolorconvert to lch {*}$rgb] + + set rgb_lab [vcolorconvert from lab {*}$lab] + set rgb_lch [vcolorconvert from lch {*}$lch] + + check3reals "RGB ($rgb) to Lab and back" {*}$rgb_lab {*}$rgb 1e-4 + check3reals "RGB ($rgb) to Lch and back" {*}$rgb_lch {*}$rgb 1e-4 +} diff --git a/tests/v3d/grids.list b/tests/v3d/grids.list index 39189a3242..9291a8fd76 100755 --- a/tests/v3d/grids.list +++ b/tests/v3d/grids.list @@ -20,3 +20,4 @@ 021 dimensions 022 transparency 023 viewcube +024 colors