Repository: scikit-image/scikit-image

Issue:
colorconv.separate_stains should use np.log10() instead of np.log()?
This method is used for stain deconvolution. It implements the method defined in this paper: http://www.math-info.univ-paris5.fr/~lomn/Data/2017/Color/Quantification_of_histochemical_staining.pdf
which is the original paper on color deconvolution. The algorithm is defined on page 6 of the paper:

ODC = -log10(IC/I0,C) = A*cC . 

Shouldn't line 1130: 
stains = np.dot(np.reshape(-np.log(rgb), (-1, 3)), conv_matrix)

really be:
stains = np.dot(np.reshape(-np.log10(rgb), (-1, 3)), conv_matrix)
?

Right now it uses ln instead of log10. 


Discussion:
I'm not sure this matters very much. One is a constant factor from the other, and neither appears to be guaranteed to be in the range of values recommended for skimage datatypes. Do you know whether absolute values make sense in this scale?

CC @cespenel @spotter 
You seem to be right that it doesn't change the distribution, although I'm not sure why that's the case. However absolute values do matter. OD = A*C, where C is the absorption constant for a stain and A is the amount of stain present. As long as you determine your C with ln() in mind I guess it would be okay, but it doesn't follow the standard convention.

I'm pretty sure it's a bug and someone mistook np.log() for np.log10().

Wikipedia article for the same function:
https://en.wikipedia.org/wiki/Absorbance#Mathematical_definitions
(Wikipedia defines optical density as absorbance because reasons, but it's the same thing- see the intro)
@markemus you're probably right, can you please submit a PR about this ?

The discussion also shows that this function is very domain specific, maybe a bit too much for scikit-image.
Here is the a link to what seems like the paper: 

https://pdfs.semanticscholar.org/e0a0/dc42714dd8add295cb43c15110d86af95cf9.pdf
@markemus, I’m not a specialist but If I understood well the matter, I believe you are right. In optic the Transmission (IC/I0,C) is always a percentage (or a decimal <1) and the Transmission is the anti-log of the negative of OD. So I believe the function should be log10 not ln. (cespenel)

Resolving patch:
diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py
--- a/skimage/color/colorconv.py
+++ b/skimage/color/colorconv.py
@@ -1413,7 +1413,7 @@ def separate_stains(rgb, conv_matrix):
     """
     rgb = dtype.img_as_float(rgb, force_copy=True)
     rgb += 2
-    stains = np.reshape(-np.log(rgb), (-1, 3)) @ conv_matrix
+    stains = np.reshape(-np.log10(rgb), (-1, 3)) @ conv_matrix
     return np.reshape(stains, rgb.shape)
 
 
@@ -1474,7 +1474,7 @@ def combine_stains(stains, conv_matrix):
 
     stains = dtype.img_as_float(stains)
     logrgb2 = -np.reshape(stains, (-1, 3)) @ conv_matrix
-    rgb2 = np.exp(logrgb2)
+    rgb2 = np.power(10, logrgb2)
     return rescale_intensity(np.reshape(rgb2 - 2, stains.shape),
                              in_range=(-1, 1))
 


Test patch:
diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py
--- a/skimage/color/tests/test_colorconv.py
+++ b/skimage/color/tests/test_colorconv.py
@@ -192,10 +192,11 @@ def test_hdx_rgb_roundtrip(self):
         img_rgb = self.img_rgb
         conv = combine_stains(separate_stains(img_rgb, hdx_from_rgb),
                               rgb_from_hdx)
-        assert_equal(img_as_ubyte(conv), img_rgb)
+        with expected_warnings('precision loss'):
+            assert_equal(img_as_ubyte(conv), img_rgb)
 
-    # RGB<->HDX roundtrip with ubyte image
-    def test_hdx_rgb_roundtrip(self):
+    # RGB<->HDX roundtrip with float image
+    def test_hdx_rgb_roundtrip_float(self):
         from skimage.color.colorconv import hdx_from_rgb, rgb_from_hdx
         img_rgb = img_as_float(self.img_rgb)
         conv = combine_stains(separate_stains(img_rgb, hdx_from_rgb),