Repository: napari/napari

Issue:
Dask images are often posterized after initial load + autoscale
## 🚀 Feature
in a previous PR, we made it so that contrast limits aren't auto-calculated on dask images, so as to prevent extra compute.  (sidenote: we've since fixed up some things about dask... so the situation might not be as bad.)  However, if the contrast limits are a very small percent of the total bit depth, then you get posterization of the image after hitting autoscale (this is related to the way that we calculate contrast on the GPU, trying to not to create a new shader, ... and would probably also be solved by moving to float textures by default).  One has to then toggle visibility (reset) the layer in order to avoid the posterization...

we should generally fix that.

To reproduce. run this script, hit autoscale, then toggle the visibility of the layer.  It only looks correct after the final toggle.  it _should_ look correct after the initial autoscale, (and it would be nice to prevent needing that initial autoscale again)

```py
import dask.array as da
import napari

napari.view_image(da.arange(1024).reshape(32,32))
napari.run()
```





Discussion:

Resolving patch:
diff --git a/napari/layers/image/image.py b/napari/layers/image/image.py
--- a/napari/layers/image/image.py
+++ b/napari/layers/image/image.py
@@ -297,13 +297,16 @@ def __init__(
         self._experimental_slicing_plane = SlicingPlane(
             thickness=1, enabled=False
         )
+        # Whether to calculate clims on the next set_view_slice
+        self._should_calc_clims = False
         if contrast_limits is None:
             if not isinstance(data, np.ndarray):
-                dtype = getattr(data, 'dtype', None)
-                if np.issubdtype(normalize_dtype(dtype), np.integer):
+                dtype = normalize_dtype(getattr(data, 'dtype', None))
+                if np.issubdtype(dtype, np.integer):
                     self.contrast_limits_range = get_dtype_limits(dtype)
                 else:
                     self.contrast_limits_range = (0, 1)
+                self._should_calc_clims = dtype != np.uint8
             else:
                 self.contrast_limits_range = self._calc_data_range()
         else:
@@ -691,8 +694,9 @@ def _set_view_slice(self):
             self, image_indices, image, thumbnail_source
         )
         self._load_slice(data)
-        if self._keep_autoscale:
+        if self._keep_autoscale or self._should_calc_clims:
             self.reset_contrast_limits()
+            self._should_calc_clims = False
 
     @property
     def _SliceDataClass(self):


Test patch:
diff --git a/napari/layers/_tests/test_dask_layers.py b/napari/layers/_tests/test_dask_layers.py
--- a/napari/layers/_tests/test_dask_layers.py
+++ b/napari/layers/_tests/test_dask_layers.py
@@ -29,7 +29,9 @@ def get_plane(block_id):
     )
     layer = layers.Image(arr)
     assert FETCH_COUNT == 1
-    assert tuple(layer.contrast_limits) == (0, 1)
+    expected = (np.min(arr[0, 0, 0]), np.max(arr[0, 0, 0]))
+    assert tuple(layer.contrast_limits) != expected
+    FETCH_COUNT = 1  # because we just fetched one more time
 
     arr2 = da.map_blocks(
         get_plane,
@@ -38,7 +40,7 @@ def get_plane(block_id):
     )
     layer = layers.Image(arr2)
     assert FETCH_COUNT == 1
-    assert tuple(layer.contrast_limits) == (0, 2 ** 8 - 1)
+    assert tuple(layer.contrast_limits) == (0, 255)
 
 
 def test_dask_array_creates_cache():
diff --git a/napari/layers/image/_tests/test_multiscale.py b/napari/layers/image/_tests/test_multiscale.py
--- a/napari/layers/image/_tests/test_multiscale.py
+++ b/napari/layers/image/_tests/test_multiscale.py
@@ -272,7 +272,6 @@ def test_contrast_limits():
     assert layer.contrast_limits[0] >= 0
     assert layer.contrast_limits[1] <= 1
     assert layer.contrast_limits[0] < layer.contrast_limits[1]
-    assert layer.contrast_limits == layer._contrast_limits_range
 
     # Change contrast_limits property
     contrast_limits = [0, 2]