Repository: scikit-image/scikit-image

Issue:
deltaE_cmc can return NaN for valid input values
Simple to reproduce:

```
import numpy as np
from skimage import color

c = np.array([0., 1.59607713, 0.87755709])
print(color.deltaE_cmc(c, c))
```

Color difference: DeltaE cmc 0
Addresses #1908?



Discussion:
I suppose the docstrings need some improvement also.

This is because for values that should be equal to 0, `get_dH2` can return very small, but negative numbers, due to floating point error. I have an idea, PR to follow.

I'll take a look at the docstring as well, while I'm at it.

Sorry @soupault : what's wrong with the docstring?

@arokem I couldn't find any `skimage` conventions on color representation, so I though it would be helpful to specify valid intervals for input arguments.

What happens in cases where things are _almost_ equal, as you mentioned in #1908?

But sure enough, if: 

```
lab2 = np.array([0. + np.finfo(float).eps, 1.59607713, 0.87755709])
```

Then it fails... Any ideas? 

Doing this: 

```
if np.all(np.abs(lab1-lab2) < np.finfo(float).eps)
```

doesn't do the trick

@arokem `np.allclose`?

Yup. Thanks!

Just the final few comments to address, then we're good to go.

@arokem hi! do you have time to finish this?


Resolving patch:
diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py
--- a/skimage/color/delta_e.py
+++ b/skimage/color/delta_e.py
@@ -20,7 +20,7 @@
 
 import numpy as np
 
-from ..color.colorconv import lab2lch, _cart2polar_2pi
+from .colorconv import lab2lch, _cart2polar_2pi
 
 
 def deltaE_cie76(lab1, lab2):
@@ -116,7 +116,7 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015):
     dE2 = (dL / (kL * SL)) ** 2
     dE2 += (dC / (kC * SC)) ** 2
     dE2 += dH2 / (kH * SH) ** 2
-    return np.sqrt(dE2)
+    return np.sqrt(np.maximum(dE2, 0))
 
 
 def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1):
@@ -238,7 +238,7 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1):
     dE2 += C_term ** 2
     dE2 += H_term ** 2
     dE2 += R_term
-    ans = np.sqrt(dE2)
+    ans = np.sqrt(np.maximum(dE2, 0))
     if unroll:
         ans = ans[0]
     return ans
@@ -304,7 +304,8 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1):
     dE2 = (dL / (kL * SL)) ** 2
     dE2 += (dC / (kC * SC)) ** 2
     dE2 += dH2 / (SH ** 2)
-    return np.sqrt(dE2)
+
+    return np.sqrt(np.maximum(dE2, 0))
 
 
 def get_dH2(lab1, lab2):
@@ -327,6 +328,7 @@ def get_dH2(lab1, lab2):
     """
     lab1 = np.asarray(lab1)
     lab2 = np.asarray(lab2)
+
     a1, b1 = np.rollaxis(lab1, -1)[1:3]
     a2, b2 = np.rollaxis(lab2, -1)[1:3]
 


Test patch:
diff --git a/skimage/color/tests/test_delta_e.py b/skimage/color/tests/test_delta_e.py
--- a/skimage/color/tests/test_delta_e.py
+++ b/skimage/color/tests/test_delta_e.py
@@ -2,12 +2,11 @@
 from os.path import abspath, dirname, join as pjoin
 
 import numpy as np
-from skimage._shared.testing import assert_allclose
+from ..._shared.testing import (assert_allclose, assert_equal,
+                                assert_almost_equal)
 
-from skimage.color import (deltaE_cie76,
-                           deltaE_ciede94,
-                           deltaE_ciede2000,
-                           deltaE_cmc)
+from ..delta_e import (deltaE_cie76, deltaE_ciede94, deltaE_ciede2000,
+                       deltaE_cmc)
 
 
 def test_ciede2000_dE():
@@ -137,6 +136,23 @@ def test_cmc():
 
     assert_allclose(dE2, oracle, rtol=1.e-8)
 
+    # Equal or close colors make `delta_e.get_dH2` function to return
+    # negative values resulting in NaNs when passed to sqrt (see #1908
+    # issue on Github):
+    lab1 = lab2
+    expected = np.zeros_like(oracle)
+    assert_almost_equal(deltaE_cmc(lab1, lab2), expected, decimal=6)
+
+    lab2[0, 0] += np.finfo(float).eps
+    assert_almost_equal(deltaE_cmc(lab1, lab2), expected, decimal=6)
+
+    # Single item case:
+    lab1 = lab2 = np.array([0., 1.59607713, 0.87755709])
+    assert_equal(deltaE_cmc(lab1, lab2), 0)
+
+    lab2[0] += np.finfo(float).eps
+    assert_equal(deltaE_cmc(lab1, lab2), 0)
+
 
 def test_single_color_cie76():
     lab1 = (0.5, 0.5, 0.5)