Repository: scikit-image/scikit-image

Issue:
separate_stains output
## Description

The transformed images of ImageJ color deconvolution plugin and skimage.color.separate_stains() are not the same, while transformation matrices are the same. In my case, after separate_stains(img, bex_from_rgb) eosin channel looks like inverted to ImageJ and all the output values are negative.
![Screenshot from 2019-04-24 11-02-34](https://user-images.githubusercontent.com/6372264/56642634-8e414500-6680-11e9-85bb-1d34b5e7e53a.png)

The following lines
```python
print(img_bex.min())
print(img_bex.max())
```
outputs
```
-0.550177897460211
-0.2117949505092822
```
So how to correctly convert values after transformation to uint8?

## Way to reproduce
```python
import numpy as np
from matplotlib import pyplot as plt
from skimage.color import separate_stains, bex_from_rgb
from matplotlib.colors import LinearSegmentedColormap
from skimage import img_as_float, img_as_ubyte
from skimage import data

import cv2

img = cv2.imread('./40x_col/Snap-450.png')
img = cv2.resize(img, (0, 0), fx=0.4, fy=0.4)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# Create an artificial color close to the original one
cmap_hema = LinearSegmentedColormap.from_list('mycmap', ['white', 'navy'])
# cmap_dab = LinearSegmentedColormap.from_list('mycmap', ['white',
#                                              'saddlebrown'])
cmap_eosin = LinearSegmentedColormap.from_list('mycmap', ['darkviolet',
                                               'white'])

img_bex = separate_stains(img, bex_from_rgb)



print(img_bex.min())
print(img_bex.max())

fig, axes = plt.subplots(2, 2, figsize=(7, 6), sharex=True, sharey=True)
ax = axes.ravel()

ax[0].imshow(img)
ax[0].set_title("Original image")

ax[1].imshow(img_bex[:, :, 0], cmap=cmap_hema)
ax[1].set_title("Hematoxylin")

ax[2].imshow(img_bex[:, :, 1], cmap=cmap_eosin)
ax[2].set_title("Eosin")

ax[3].imshow(img_bex[:, :, 2], cmap='gray')
ax[3].set_title("...")

for a in ax.ravel():
    a.axis('off')

fig.tight_layout()

plt.show()

```


## Version information
```
3.6.8 |Anaconda, Inc.| (default, Dec 30 2018, 01:22:34) 
[GCC 7.3.0]
Linux-4.15.0-47-generic-x86_64-with-debian-buster-sid
scikit-image version: 0.15.0
numpy version: 1.14.2
```
UPD
Snap-450.png
![Snap-450](https://user-images.githubusercontent.com/6372264/56709684-31e72f80-672b-11e9-9a4e-5cde668deddc.png)



Discussion:
Thank you for the detailed bug report, @Mnkach!  Are you able to share `Snap-450.png` as well?
I attached it to the header.
/cc @spotter
In line 595 of `Colour_Deconvolution.java`, there seems to be an offset calculation that is not present in our implementation.  Could that be the cause?

```java
double output = Math.exp(-((Rscaled + Gscaled + Bscaled) - 255.0) * log255 / 255.0);
					if(output>255) output=255;
					newpixels[i][j]=(byte)(0xff&(int)(Math.floor(output+.5)));
				}
			}
			 // add new values to output images
			outputstack[0].addSlice(label,newpixels[0]);
			outputstack[1].addSlice(label,newpixels[1]);
			outputstack[2].addSlice(label,newpixels[2]);
```

I also don't think the visualization method used here is the same as in the code, so the colors will be different.
Is this function operating correctly? I also get only negative values.
Hey @asmagen,
thank you for letting us know. Could you give us an example? Some code running would be great to investigate.
Thanks again!
See the following issue where I have placed an example [here](https://github.com/scikit-image/scikit-image/issues/4629)

Resolving patch:
diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py
--- a/skimage/color/colorconv.py
+++ b/skimage/color/colorconv.py
@@ -1445,9 +1445,17 @@ def separate_stains(rgb, conv_matrix):
     * ``ahx_from_rgb``: Alcian Blue + Hematoxylin
     * ``hpx_from_rgb``: Hematoxylin + PAS
 
+    This implementation borrows some ideas from DIPlib [2]_, e.g. the
+    compensation using a small value to avoid log artifacts when
+    calculating the Beer-Lambert law.
+
     References
     ----------
     .. [1] https://web.archive.org/web/20160624145052/http://www.mecourse.com/landinig/software/cdeconv/cdeconv.html
+    .. [2] https://github.com/DIPlib/diplib/
+    .. [3] A. C. Ruifrok and D. A. Johnston, “Quantification of histochemical
+           staining by color deconvolution,” Anal. Quant. Cytol. Histol., vol.
+           23, no. 4, pp. 291–299, Aug. 2001.
 
     Examples
     --------
@@ -1457,9 +1465,12 @@ def separate_stains(rgb, conv_matrix):
     >>> ihc_hdx = separate_stains(ihc, hdx_from_rgb)
     """
     rgb = _prepare_colorarray(rgb, force_copy=True)
-    rgb += 2
-    stains = np.reshape(-np.log10(rgb), (-1, 3)) @ conv_matrix
-    return np.reshape(stains, rgb.shape)
+    np.maximum(rgb, 1E-6, out=rgb)  # avoiding log artifacts
+    log_adjust = np.log(1E-6)  # used to compensate the sum above
+
+    stains = (np.log(rgb) / log_adjust) @ conv_matrix
+
+    return stains
 
 
 def combine_stains(stains, conv_matrix):
@@ -1503,6 +1514,9 @@ def combine_stains(stains, conv_matrix):
     References
     ----------
     .. [1] https://web.archive.org/web/20160624145052/http://www.mecourse.com/landinig/software/cdeconv/cdeconv.html
+    .. [2] A. C. Ruifrok and D. A. Johnston, “Quantification of histochemical
+           staining by color deconvolution,” Anal. Quant. Cytol. Histol., vol.
+           23, no. 4, pp. 291–299, Aug. 2001.
 
     Examples
     --------
@@ -1513,13 +1527,14 @@ def combine_stains(stains, conv_matrix):
     >>> ihc_hdx = separate_stains(ihc, hdx_from_rgb)
     >>> ihc_rgb = combine_stains(ihc_hdx, rgb_from_hdx)
     """
-    from ..exposure import rescale_intensity
-
     stains = _prepare_colorarray(stains)
-    logrgb2 = -np.reshape(stains, (-1, 3)) @ conv_matrix
-    rgb2 = np.power(10, logrgb2)
-    return rescale_intensity(np.reshape(rgb2 - 2, stains.shape),
-                             in_range=(-1, 1))
+
+    # log_adjust here is used to compensate the sum within separate_stains().
+    log_adjust = -np.log(1E-6)
+    log_rgb = -(stains * log_adjust) @ conv_matrix
+    rgb = np.exp(log_rgb)
+
+    return np.clip(rgb, a_min=0, a_max=1)
 
 
 def lab2lch(lab):


Test patch: