Repository: napari/napari

Issue:
Black is still a color in the label colormap => Some labels are not displayed
## 🐛 Bug

<!-- A clear and concise description of what the bug is. -->
When displaying label images with a label of value 305, that label is not displayed in the viewer. When colors are shuffled, it appears (see images below). It also is still visible in the thumbnail. It looks like 305 is black in the default colormap. Black with translucency is not displayed. The same happens for 915, 1525, 1902 etc. in the standard colormap

## To Reproduce

Steps to reproduce the behavior:

1. Create a label image with a label of value 305
2. Display this image in napari by using `add_labels`

When originally displayed:
<img width="1200" alt="OriginalLabelMap" src="https://user-images.githubusercontent.com/18033446/74759054-7cb4d380-5278-11ea-85d8-fdf587583ccb.png">
After shuffle colors:
<img width="1200" alt="PostShuffleColors" src="https://user-images.githubusercontent.com/18033446/74759061-7e7e9700-5278-11ea-91c4-6dadb599c72f.png">

Here's a simpler case with an image generated in code:
```lbl = np.zeros((20, 20))
lbl[:, :10] = 304
lbl[:, 10:20] = 305
viewer = napari.Viewer()
viewer.add_labels(lbl)
```
![1](https://user-images.githubusercontent.com/18033446/74759553-3f047a80-5279-11ea-9eba-964ec72218a5.png)

Notice how the thumbnail still shows both labels, but the 305 label is not visible in the viewer until the colormap is shuffled.
After a color shuffle:
![2](https://user-images.githubusercontent.com/18033446/74759628-5f343980-5279-11ea-95fd-15c0968b947a.png)


## Expected behavior
All labels should be visible when used in the default mode, with translucent blending. Therefore, black should be removed from the label colormap except for value 0. Currently, they still occur at some pattern all over the colormap, relatively sparsely.


## Environment

 - Please copy and paste the information at napari info option in help menubar here:
napari: 0.2.11
Platform: Darwin-19.3.0-x86_64-i386-64bit
Python: 3.7.4 (default, Aug 13 2019, 15:17:50) [Clang 4.0.1 (tags/RELEASE_401/final)]
Qt: 5.13.2
PySide2: 5.13.2
VisPy: 0.6.4
NumPy: 1.17.4
SciPy: 1.3.3
scikit-image: 0.16.2
Dask: 2.9.0

GL version: 2.1 INTEL-14.4.23
MAX_TEXTURE_SIZE: 16384

We reproduced this on Mac & Windows, in napari 0.2.6, 0.2.10 & 0.2.11

(cc @HessMax who also helped with narrowing down this bug)


Discussion:
Opps, that's not so good, sorry about that. I could try and make a quick fix inside https://github.com/napari/napari/blob/56ab67d009b6f8a46a049a262aecf199d3d81cb0/napari/utils/colormaps/colormaps.py#L173, but I think I might use this as a driver to make a push on #713 (maybe @jni when I see you the week after next you can help me take that one over)
Thanks for the detailed report, @jluethi! Yep, definitely a bug in the way we "hacked" regular image display to display labels. Any value too close to zero after "hashing" from labels to floats in [0, 1] will map to 0.

@sofroniewn #713 is a bit of a mess, whereas I think this could be an easy fix: map the low-discrepancy sequence to 0.05-1 rather than 0-1. I'll try to submit a bug fix soon. Of course, that in no way precludes discussing #713 next week!
@sofroniewn fixing now. The fix is easy, it looks like we just need to tweak the values here:

https://github.com/napari/napari/blob/56ab67d009b6f8a46a049a262aecf199d3d81cb0/napari/utils/colormaps/colormaps.py#L102

The colormap we currently use has 256 values iirc, so it should be sufficient to clip at 1/128 rather than the current value of 10^-5.
Actually, I take it back, clipping is the wrong approach here: it'll make all the values in [0, 1/128] map to the same color. which is bad. Anyway, still an easy fix, just a bit different. =) Hold please!

Resolving patch:
diff --git a/napari/_qt/qt_viewer.py b/napari/_qt/qt_viewer.py
--- a/napari/_qt/qt_viewer.py
+++ b/napari/_qt/qt_viewer.py
@@ -315,13 +315,7 @@ def _save_screenshot(self):
             # double check that an appropriate extension has been added as the filter
             # filter option does not always add an extension on linux and windows
             # see https://bugreports.qt.io/browse/QTBUG-27186
-            image_extensions = (
-                '.bmp',
-                '.gif',
-                '.png',
-                '.tif',
-                '.tiff',
-            )
+            image_extensions = ('.bmp', '.gif', '.png', '.tif', '.tiff')
             if not filename.endswith(image_extensions):
                 filename = filename + '.png'
             self.screenshot(path=filename)
diff --git a/napari/utils/colormaps/colormaps.py b/napari/utils/colormaps/colormaps.py
--- a/napari/utils/colormaps/colormaps.py
+++ b/napari/utils/colormaps/colormaps.py
@@ -78,7 +78,7 @@ def _validate_rgb(colors, *, tolerance=0.0):
     return filtered_colors
 
 
-def _low_discrepancy_image(image, seed=0.5):
+def _low_discrepancy_image(image, seed=0.5, margin=1 / 256):
     """Generate a 1d low discrepancy sequence of coordinates.
 
     Parameters
@@ -87,6 +87,10 @@ def _low_discrepancy_image(image, seed=0.5):
         A set of labels or label image.
     seed : float
         The seed from which to start the quasirandom sequence.
+    margin : float
+        Values too close to 0 or 1 will get mapped to the edge of the colormap,
+        so we need to offset to a margin slightly inside those values. Since
+        the bin size is 1/256 by default, we offset by that amount.
 
     Returns
     -------
@@ -94,12 +98,13 @@ def _low_discrepancy_image(image, seed=0.5):
         The set of ``labels`` remapped to [0, 1] quasirandomly.
 
     """
-    phi = 1.6180339887498948482
-    image_out = (seed + image / phi) % 1
-    # Clipping slightly above 0 and below 1 is necessary to ensure that the
-    # labels do not get mapped to 0 which is represented by the background
-    # and is transparent
-    return np.clip(image_out, 0.00001, 1.0 - 0.00001)
+    phi_mod = 0.6180339887498948482
+    image_float = seed + image * phi_mod
+    # We now map the floats to the range [0 + margin, 1 - margin]
+    image_out = margin + (1 - 2 * margin) * (
+        image_float - np.floor(image_float)
+    )
+    return image_out
 
 
 def _low_discrepancy(dim, n, seed=0.5):


Test patch: