Repository: napari/napari

Issue:
Wrong intensity values in status bar for automatically down-sampled images.
## 🐛 Bug

When displaying a 2D image larger than 'GL_MAX_TEXTURE_SIZE', it is automatically downsampled. While the coordinate values displayed in the status bar are correct, the intensity values that are shown don't take the downsampling into account.

## To Reproduce

Steps to reproduce the behavior:

1. Create a 2D image larger than 'GL_MAX_TEXTURE_SIZE'.
2. Hover over the image and notice that the intensity values displayed in the status bar correspond to a 'larger' image than the one displayed (i. e. are not taking the downsampling into account).

```Python
gl_max_texture_size = 16384 # insert hardware specific gl_max_texture_size
img = np.zeros((gl_max_texture_size*2, gl_max_texture_size*2), dtype=np.uint16) # create an empty image twice as large
img[img.shape[0]//2:, img.shape[1]//2:] = 1 # set the bottom right corner to 1

with napari.gui_qt():
    viewer = napari.Viewer()
    viewer.add_image(img, colormap='viridis')
```
## Expected behavior

Intensity values displayed in the status bar should correspond to the image on display.

## Environment

napari: 0.3.6
Platform: Windows-10-10.0.18362-SP0
Python: 3.7.6 | packaged by conda-forge | (default, Mar 23 2020, 22:22:21) [MSC v.1916 64 bit (AMD64)]
Qt: 5.14.2
PyQt5: 5.14.2
NumPy: 1.18.3
SciPy: 1.4.1
Dask: 2.15.0
VisPy: 0.6.4

GL version: 4.6.0 - Build 26.20.100.7812
MAX_TEXTURE_SIZE: 16384

Plugins:
- napari-plugin-engine: 0.1.5
- napari_bug
- svg: 0.1.3

## Additional context

While creating a reproducible example I noticed that if I use `dtype=np.uint8` with that same example there seems to be a display bug _in addition_ to the bug in the status bar (i. e. the whole image is displayed as 0).



Discussion:
Thanks for this report, this does sound like a bug! We will look into it promptly
Hi Nicholas,

I had some time to further look into the bug today. I found a simple fix by changing one line in `napari.layers.image.image.Image._get_value` :

```Python
coord = np.round(self.coordinates).astype(int) #old
coord = np.round(self._transforms(self.coordinates)).astype(int) #new
```

Piping the coordinates through the layer transforms seemed to do the trick. There might be similar bugs in other layers that override `_get_value`.

Cheers Maks
> Piping the coordinates through the layer transforms seemed to do the trick. There might be similar bugs in other layers that override _get_value.

That makes sense to me, thanks for digging in! We'd love a PR with a fix for this bug and a test to check it.

You could start with just one for the Image layer, but you might want to look at those other layers too.

Thanks!!

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
@@ -728,7 +728,15 @@ def _get_value(self):
         value : tuple
             Value of the data at the coord.
         """
-        coord = np.round(self.coordinates).astype(int)
+        if self.multiscale:
+            # for multiscale data map the coordinate from the data back to
+            # the tile
+            coord = self._transforms['tile2data'].inverse(self.coordinates)
+        else:
+            coord = self.coordinates
+
+        coord = np.round(coord).astype(int)
+
         raw = self._slice.image.raw
         if self.rgb:
             shape = raw.shape[:-1]


Test patch:
diff --git a/napari/_vispy/_tests/test_vispy_big_images.py b/napari/_vispy/_tests/test_vispy_big_images.py
--- a/napari/_vispy/_tests/test_vispy_big_images.py
+++ b/napari/_vispy/_tests/test_vispy_big_images.py
@@ -30,3 +30,28 @@ def test_big_3D_image(make_test_viewer):
     if visual.MAX_TEXTURE_SIZE_3D is not None:
         s = np.ceil(np.divide(shape, visual.MAX_TEXTURE_SIZE_3D)).astype(int)
         assert np.all(layer._transforms['tile2data'].scale == s)
+
+
+@pytest.mark.parametrize(
+    "shape", [(2, 4), (256, 4048), (4, 20_000), (20_000, 4)],
+)
+@pytest.mark.filterwarnings("ignore:data shape:UserWarning")
+def test_downsample_value(make_test_viewer, shape):
+    """Test getting correct value for downsampled data."""
+    viewer = make_test_viewer()
+
+    data = np.zeros(shape)
+    data[shape[0] // 2 :, shape[1] // 2 :] = 1
+    layer = viewer.add_image(data, multiscale=False)
+
+    test_points = [
+        (int(shape[0] * 0.25), int(shape[1] * 0.25)),
+        (int(shape[0] * 0.75), int(shape[1] * 0.25)),
+        (int(shape[0] * 0.25), int(shape[1] * 0.75)),
+        (int(shape[0] * 0.75), int(shape[1] * 0.75)),
+    ]
+    expected_values = [0.0, 0.0, 0.0, 1.0]
+
+    for test_point, expected_value in zip(test_points, expected_values):
+        layer.position = test_point
+        assert layer.get_value() == expected_value
diff --git a/napari/_vispy/_tests/test_vispy_multiscale.py b/napari/_vispy/_tests/test_vispy_multiscale.py
--- a/napari/_vispy/_tests/test_vispy_multiscale.py
+++ b/napari/_vispy/_tests/test_vispy_multiscale.py
@@ -33,12 +33,12 @@ def test_multiscale(make_test_viewer):
     np.testing.assert_allclose(value, (2, data[2][(0, 0)]))
 
     # Test value at bottom right corner of image
-    layer.position = (999, 749)
+    layer.position = (3995, 2995)
     value = layer.get_value()
     np.testing.assert_allclose(value, (2, data[2][(999, 749)]))
 
     # Test value outside image
-    layer.position = (1000, 750)
+    layer.position = (4000, 3000)
     value = layer.get_value()
     assert value[1] is None