Repository: napari/napari

Issue:
Label editing and creation with scale keyword
## 🐛 Bug

When trying to edit the labels in Napari setting scale attribute in view_image prohibits editing and drawing of the labels.

## To Reproduce

Steps to reproduce the behavior:

1.  You can open an example image with its labels and then use this code 


<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->
`      
scaleZ = 1
scaleX = 0.16
scaleY = 0.16                  
with napari.gui_qt():

                            viewer = napari.view_image(image, name='Raw', rgb=False, scale=[scaleZ, scaleX, scaleY]) 
                            label_layer = viewer.add_labels(labels, name='Label', scale=[scaleZ, scaleX, scaleY]) `
## Expected behavior

Editing and drawing with scaled co ordinates.

## Environment

`
napari: 0.3.6
Platform: Darwin-18.7.0-x86_64-i386-64bit
Python: 3.7.3 (default, Mar 27 2019, 16:54:48)  [Clang 4.0.1 (tags/RELEASE_401/final)]
Qt: 5.14.2
PyQt5: 5.14.2
NumPy: 1.19.1
SciPy: 1.5.2
Dask: 2.21.0
VisPy: 0.6.4

GL version:  2.1 INTEL-12.10.12
MAX_TEXTURE_SIZE: 16384

Plugins:
  - napari-plugin-engine: 0.1.6
  - svg: 0.1.3
`





Discussion:
Thanks for flagging this, we will look in soon!
Hi @kapoorlab, sorry it's taken so long to get back to you on this one! I can confirm that this is still a bug, easy to reproduce with the following code:

```python
with napari.gui_qt():
    image = np.random.random((5, 128, 128))
    labels = np.zeros((5, 128, 128))
    scale = [1, 0.16, 0.16]
    viewer = napari.view_image(image, name='Raw', rgb=False, scale=scale) 
    label_layer = viewer.add_labels(labels, name='Label', scale=scale)
```

We're doing a lot right now to clean up our world coordinate system ahead of our `0.4.0` release, and I will get to this as soon as #854 has merged. We should have it fixed soon. Thanks for your patience!!

Resolving patch:
diff --git a/napari/layers/labels/labels.py b/napari/layers/labels/labels.py
--- a/napari/layers/labels/labels.py
+++ b/napari/layers/labels/labels.py
@@ -273,7 +273,12 @@ def brush_size(self):
     @brush_size.setter
     def brush_size(self, brush_size):
         self._brush_size = int(brush_size)
-        self.cursor_size = self._brush_size / self.scale_factor
+        data2world_scale = np.mean(
+            [self.scale[d] for d in self._dims.displayed]
+        )
+        self.cursor_size = (
+            self._brush_size / self.scale_factor * data2world_scale
+        )
         self.status = format_float(self.brush_size)
         self.events.brush_size()
 
@@ -507,7 +512,9 @@ def mode(self, mode: Union[str, Mode]):
             self.help = 'hold <space> to pan/zoom, click to pick a label'
             self.mouse_drag_callbacks.append(pick)
         elif mode == Mode.PAINT:
-            self.cursor_size = self.brush_size / self.scale_factor
+            # Note we have to reset the brush size to tigger an event
+            # This will be fixed in an upcomming PR
+            self.brush_size = self.brush_size
             self.cursor = self.brush_shape
             self.interactive = False
             self.help = (
@@ -524,7 +531,9 @@ def mode(self, mode: Union[str, Mode]):
             self.help = 'hold <space> to pan/zoom, click to fill a label'
             self.mouse_drag_callbacks.append(draw)
         elif mode == Mode.ERASE:
-            self.cursor_size = self.brush_size / self.scale_factor
+            # Note we have to reset the brush size to tigger an event
+            # This will be fixed in an upcomming PR
+            self.brush_size = self.brush_size
             self.cursor = self.brush_shape
             self.interactive = False
             self.help = 'hold <space> to pan/zoom, drag to erase a label'


Test patch:
diff --git a/napari/layers/labels/_tests/test_labels_mouse_bindings.py b/napari/layers/labels/_tests/test_labels_mouse_bindings.py
--- a/napari/layers/labels/_tests/test_labels_mouse_bindings.py
+++ b/napari/layers/labels/_tests/test_labels_mouse_bindings.py
@@ -33,6 +33,7 @@ def test_paint(Event, brush_shape, expected_sum):
     data = np.ones((20, 20))
     layer = Labels(data)
     layer.brush_size = 10
+    assert layer.cursor_size == 10
     layer.brush_shape = brush_shape
     layer.mode = 'paint'
     layer.selected_label = 3
@@ -61,6 +62,43 @@ def test_paint(Event, brush_shape, expected_sum):
     assert np.sum(layer.data == 3) == expected_sum
 
 
+@pytest.mark.parametrize(
+    "brush_shape, expected_sum", [("circle", 244), ("square", 274)]
+)
+def test_paint_scale(Event, brush_shape, expected_sum):
+    """Test painting labels with circle/square brush when scaled."""
+    data = np.ones((20, 20))
+    layer = Labels(data, scale=(2, 2))
+    layer.brush_size = 10
+    assert layer.cursor_size == 20
+    layer.brush_shape = brush_shape
+    layer.mode = 'paint'
+    layer.selected_label = 3
+    layer.position = (0, 0)
+
+    # Simulate click
+    event = ReadOnlyWrapper(Event(type='mouse_press', is_dragging=False))
+    mouse_press_callbacks(layer, event)
+
+    layer.position = (39, 39)
+
+    # Simulate drag
+    event = ReadOnlyWrapper(Event(type='mouse_move', is_dragging=True))
+    mouse_move_callbacks(layer, event)
+
+    # Simulate release
+    event = ReadOnlyWrapper(Event(type='mouse_release', is_dragging=False))
+    mouse_release_callbacks(layer, event)
+
+    # Painting goes from (0, 0) to (19, 19) with a brush size of 10, changing
+    # all pixels along that path, but none outside it.
+    assert np.unique(layer.data[:8, :8]) == 3
+    assert np.unique(layer.data[-8:, -8:]) == 3
+    assert np.unique(layer.data[:5, -5:]) == 1
+    assert np.unique(layer.data[-5:, :5]) == 1
+    assert np.sum(layer.data == 3) == expected_sum
+
+
 @pytest.mark.parametrize(
     "brush_shape, expected_sum", [("circle", 156), ("square", 126)]
 )