0031454: Visualization - perceptually uniform color scale
authorabv <abv@opencascade.com>
Thu, 19 Mar 2020 07:03:40 +0000 (10:03 +0300)
committerbugmaster <bugmaster@opencascade.com>
Thu, 26 Mar 2020 16:53:27 +0000 (19:53 +0300)
Support of CIE Lab and Lch color spaces is introduced in Quantity_Color:
- Enumeration Quantity_TypeOfColor is extended by new values representing CIE Lab and Lch color spaces (with D65 2 deg illuminant).
- Conversion of RGB color to and from these color spaces is implemented in Quantity_Color class (within existing API).
- Color difference calculation using CIE Delta E 200 formula is implemented in method DeltaE2000().

New methods MakeUniformColors() and SetUniformColors() are added in class AIS_ColorScale, generating and setting color scale based on colors of the same lightness in CIE Lch color model.

DRAW commands vcolorconvert and vcolordiff are added to perform conversions and compute difference, respectively.
A new option -uniform is added in DRAW command vcolorscale, to set uniform color scale.

Added test grid v3d colors (color conversions and difference), test bugs vis bug31454 (uniform color scale)

15 files changed:
src/AIS/AIS_ColorScale.cxx
src/AIS/AIS_ColorScale.hxx
src/Quantity/Quantity_Color.cxx
src/Quantity/Quantity_Color.hxx
src/Quantity/Quantity_TypeOfColor.hxx
src/ViewerTest/ViewerTest_ViewerCommands.cxx
tests/bugs/vis/bug22632
tests/bugs/vis/bug31454 [new file with mode: 0644]
tests/v3d/colors/begin [new file with mode: 0644]
tests/v3d/colors/de2000 [new file with mode: 0644]
tests/v3d/colors/de2000_sharma [new file with mode: 0644]
tests/v3d/colors/rgb2lab [new file with mode: 0644]
tests/v3d/colors/rgb2lch [new file with mode: 0644]
tests/v3d/colors/stability [new file with mode: 0644]
tests/v3d/grids.list

index ebc618b..afaba07 100644 (file)
@@ -290,6 +290,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<Quantity_Color> 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  :
 //=======================================================================
index a71f138..f261759 100644 (file)
@@ -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; }
 
index d61a609..cb8962e 100644 (file)
@@ -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<float> 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> (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> (float(theR1), float(theR2), float(theR3)));
+      Quantity_ColorValidateHlsRange(theC1, theC2, theC3);
+      myRgb = Convert_HLS_To_LinearRGB (NCollection_Vec3<float> (float(theC1), float(theC2), float(theC3)));
+      break;
+    }
+    case Quantity_TOC_CIELab:
+    {
+      Quantity_ColorValidateLabRange(theC1, theC2, theC3);
+      myRgb = Convert_Lab_To_LinearRGB (NCollection_Vec3<float> (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> (float(theC1), float(theC2), float(theC3))));
       break;
     }
   }
@@ -302,6 +304,76 @@ void Quantity_Color::Delta (const Quantity_Color& theColor,
 }
 
 // =======================================================================
+// 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<float> aLab = Convert_LinearRGB_To_Lab (myRgb);
+      theR1 = aLab[0];
+      theR2 = aLab[1];
+      theR3 = aLab[2];
+      break;
+    }
+    case Quantity_TOC_CIELch:
+    {
+      const NCollection_Vec3<float> 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<float> Quantity_Color::Convert_sRGB_To_HLS (const NCollection_V
   return NCollection_Vec3<float> (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;
-
-  std::cout << "definition color tests\n----------------------\n";
-
-  Quantity_Color C1;
-  Quantity_Color C2 (Quantity_NOC_ROYALBLUE2);
-  Quantity_Color C3 (Quantity_NOC_SANDYBROWN);
-
-  // 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";
-
-  Standard_Real RR, GG, BB;
-
-  const Standard_Real DELTA = 1.0e-4;
-
-  std::cout << "Get values and names of color tests\n-----------------------------------\n";
-
-  C1.Values (R, G, B, Quantity_TOC_RGB);
-  if ((R!=1.0) || (G!=1.0) || (B!=0.0))
-  {
-    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";
-  }
-
-  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";
-  }
-
-  if (C2 != C1)
-  {
-    std::cout << "TEST_ERROR : IsDifferent ()\n";
-  }
-  if (C3 == C1)
-  {
-    std::cout << "TEST_ERROR : IsEqual ()\n";
-  }
+  return theValue > 0.008856451679035631 ? Pow (theValue, 1./3.) : (7.787037037037037 * theValue) + 16. / 116.;
+}
 
-  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";
+// =======================================================================
+// 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;
+}
 
-  if (strcmp (Quantity_Color::StringName (C2.Name()), blue) != 0)
-  {
-    std::cout << "TEST_ERROR : StringName () " << Quantity_Color::StringName (C2.Name()) <<  " != ROYALBLUE2\n";
-  }
+// =======================================================================
+// function : Convert_LinearRGB_To_Lab
+// purpose  : convert RGB color to CIE Lab color
+// see https://www.easyrgb.com/en/math.php
+// =======================================================================
+NCollection_Vec3<float> Quantity_Color::Convert_LinearRGB_To_Lab (const NCollection_Vec3<float>& theRgb)
+{
+  double aR = theRgb[0];
+  double aG = theRgb[1];
+  double aB = theRgb[2];
+
+  // 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;
+
+  // convert to Lab
+  double afX = CIELab_f (aX);
+  double afY = CIELab_f (aY);
+  double afZ = CIELab_f (aZ);
+
+  double aL = 116. * afY - 16.;
+  double aa = 500. * (afX - afY);
+  double ab = 200. * (afY - afZ);
+
+  return NCollection_Vec3<float> ((float)aL, (float)aa, (float)ab);
+}
 
-  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";
+// =======================================================================
+// function : Convert_Lab_To_LinearRGB
+// purpose  : convert CIE Lab color to RGB
+// see https://www.easyrgb.com/en/math.php
+// =======================================================================
+NCollection_Vec3<float> Quantity_Color::Convert_Lab_To_LinearRGB (const NCollection_Vec3<float>& 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--)
+  {
+    double aC = aRate / (double)NBSTEPS;
+
+    // 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.;
+
+    double aX = CIELab_invertf(afX) *  95.047;
+    double aY = CIELab_invertf(afY) * 100.000;
+    double aZ = CIELab_invertf(afZ) * 108.883;
+
+    // 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.;
+
+    // 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>((float)aR, (float)aG, (float)aB);
+    }
   }
+}
 
-  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";
-  }
+// =======================================================================
+// function : Convert_Lab_To_Lch
+// purpose  : convert CIE Lab color to CIE Lch color
+// see https://www.easyrgb.com/en/math.php
+// =======================================================================
+NCollection_Vec3<float> Quantity_Color::Convert_Lab_To_Lch (const NCollection_Vec3<float>& theLab)
+{
+  double aa = theLab[1];
+  double ab = theLab[2];
 
-  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";
-  }
+  double aC = Sqrt (aa * aa + ab * ab);
+  double aH = (aC > TheEpsilon ? ATan2 (ab, aa) * 180. / M_PI : 0.);
 
-  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";
-  }
+  if (aH < 0.) aH += 360.;
 
-  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";
-  }
+  return NCollection_Vec3<float> (theLab[0], (float)aC, (float)aH);
 }
 
 // =======================================================================
-// function : Test
-// purpose  :
+// function : Convert_Lch_To_Lab
+// purpose  : convert CIE Lch color to CIE Lab color
+// see https://www.easyrgb.com/en/math.php
 // =======================================================================
-void Quantity_Color::Test()
+NCollection_Vec3<float> Quantity_Color::Convert_Lch_To_Lab (const NCollection_Vec3<float>& theLch)
 {
-  try
-  {
-    OCC_CATCH_SIGNALS
-    TestOfColor();
-  }
-  catch (Standard_Failure const& anException)
-  {
-    std::cout << anException << std::endl;
-  }
+  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<float> (theLch[0], (float)aa, (float)ab);
 }
 
 //=======================================================================
index a413e2f..82e3bdc 100644 (file)
@@ -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<float>& theRgb);
 
   //! Returns the name of the nearest color from the Quantity_NameOfColor enumeration.
@@ -61,19 +61,23 @@ public:
   void SetValues (const Quantity_NameOfColor theName) { myRgb = valuesOf (theName, Quantity_TOC_RGB); }
 
   //! Return the color as vector of 3 float elements.
+  const NCollection_Vec3<float>& Rgb () const { return myRgb; }
+
+  //! Return the color as vector of 3 float elements.
   operator const NCollection_Vec3<float>&() 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<float> Convert_LinearRGB_To_Lab (const NCollection_Vec3<float>& theRgb);
+
+  //! Converts CIE Lab components into CIE Lch ones.
+  Standard_EXPORT static NCollection_Vec3<float> Convert_Lab_To_Lch (const NCollection_Vec3<float>& 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<float> Convert_Lab_To_LinearRGB (const NCollection_Vec3<float>& theLab);
+
+  //! Converts CIE Lch components into CIE Lab ones.
+  Standard_EXPORT static NCollection_Vec3<float> Convert_Lch_To_Lab (const NCollection_Vec3<float>& 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;
index ba70a48..affa420 100644 (file)
 //! 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&deg; 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
index 11e6900..c64f279 100644 (file)
@@ -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);
 }
index 8bd5918..7ccfba7 100644 (file)
@@ -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 (file)
index 0000000..a108d5d
--- /dev/null
@@ -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 (file)
index 0000000..b8000f8
--- /dev/null
@@ -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 (file)
index 0000000..efeb477
--- /dev/null
@@ -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 (file)
index 0000000..b399e22
--- /dev/null
@@ -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 (file)
index 0000000..a0cd3a8
--- /dev/null
@@ -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 (file)
index 0000000..1621867
--- /dev/null
@@ -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 (file)
index 0000000..e6c965f
--- /dev/null
@@ -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
+}
index 39189a3..9291a8f 100755 (executable)
@@ -20,3 +20,4 @@
 021 dimensions
 022 transparency
 023 viewcube
+024 colors