Repository: napari/napari

Issue:
viewer.layers[...].to_labels() not working 
## 🐛 Bug

Hi all, 
Following @sofroniewn's [tutorial](https://www.youtube.com/watch?v=VgvDSq5aCDQ), I have been trying to transform a shape layer to labels. Surprisingly, although not throwing any kind of error message, using ```to_labels()``` leads to random behavior. The different shapes are not transformed into labels. After some investigations, it seems that only the shape closer to the origin is transformed as label. 

Thanks!


## To Reproduce

Steps to reproduce the behavior:

1. Load image (here ```skimage.data.coins()```)
2. Create a shape layer and delineate some coins ```viewer.add_shapes(name='coins', face_color='red')```
3. Use ```to_labels()```to transform: ```labels=viewer.layers['coins'].to_labels()```
4. Add the new layer: ```viewer.add_labels(labels)```

## Expected behavior

See below the example of the different layers ... 
<img width="1574" alt="Screenshot" src="https://user-images.githubusercontent.com/14175231/134194902-2aaaa965-9188-4fee-86bf-eb1826b8f890.png">

I tried with various pictures leading to the same issue. Only part of the shapes are transformed to labels. 
In this example, when looking at the array obtained after ```to_labels()``` with ```np.unique```, I get ```array([0, 4])```. Label 4 is coherent with the fact that the shape transformed into a label was the forth. 

## Environment

napari: 0.4.11
Platform: macOS-11.5.2-x86_64-i386-64bit
System: MacOS 11.5.2
Python: 3.9.5 | packaged by conda-forge | (default, Jun 19 2021, 00:27:35) [Clang 11.1.0 ]
Qt: 5.15.2
PyQt5: 5.15.4
NumPy: 1.21.1
SciPy: 1.7.0
Dask: 2021.08.1
VisPy: 0.6.6

OpenGL:
- GL version: 2.1 INTEL-16.5.2
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 3200x1800, scale 2.0

Plugins:
- console: 0.0.4
- napari-allencell-segmenter: 1.0.3
- ome-types: 0.2.7
- scikit-image: 0.4.11
- svg: 0.1.5




Discussion:
Hmmm, very strange @MathieuBo, I am seeing similar but not exactly the same behaviour:

![to_labels](https://user-images.githubusercontent.com/492549/134351833-a7f47318-f4dd-4329-8c61-5e94e3bf4fde.png)

it looks like the logic that determines the default shape of the labels layer is faulty. Have a look at `viewer.layers['lab'].shape`: it will be much smaller than the full extent of the shapes.

If you pass in `to_labels(labels_shape=viewer.layers['coins'].data.shape)`, then it works as expected:

![image](https://user-images.githubusercontent.com/492549/134352495-e86fbcb5-7e57-43d7-91a8-8ab59494bec4.png)

We'll investigate, but hopefully that workaround gets you going in the meantime. :blush: 
Thanks a lot @jni for looking into that! The workaround works like a charm :)
BTW, what you observed is similar to what I got sometimes. The behaviour is quite erratic 
I'm guessing the problem is that the Shapes Layer doesn't know the underlying size of the Image layer (i.e. is it 512x512, or 1024x640) and so if a `labels_shape` is not provided it just looks at the full extent of the data it currently has here

https://github.com/napari/napari/blob/95a5d06fe36f10fb26f5798f1327a3ea082d581f/napari/layers/shapes/shapes.py#L2941-L2944

and guesses what it should be.

It also might get tripped up by shapes that appear at "negative" coordinates. We could take those into account though. I think we might clip them now.

So I guess in short-term providing a `labels_shape` is definitely good. We could in fact force one to be provided and not even try and guess, or at least improve the documentation so everything i said above is capture. in the longer term we can also try and improve, but i'm not sure what best approach will be.

thanks for the report though, definitely always good to know about quirks like this!!
> it just looks at the full extent of the data it currently has here

Oh I see it now: it subtracts the smallest coordinate. Clearly that's wrong — it should start at 0, right? Alternatively, *all* the coordinates should get offset with `- self._extent_data[0]`.
> Oh I see it now: it subtracts the smallest coordinate. Clearly that's wrong — it should start at 0, right? Alternatively, all the coordinates should get offset with - self._extent_data[0].

Yeah, should be one or the other. i'd probably expect most folks want to start at `0`. There could be an `origin` kwarg if needed

Resolving patch:
diff --git a/napari/layers/shapes/shapes.py b/napari/layers/shapes/shapes.py
--- a/napari/layers/shapes/shapes.py
+++ b/napari/layers/shapes/shapes.py
@@ -2915,7 +2915,11 @@ def to_masks(self, mask_shape=None):
             Array where there is one binary mask for each shape
         """
         if mask_shape is None:
-            mask_shape = self._extent_data[1] - self._extent_data[0]
+            # See https://github.com/napari/napari/issues/2778
+            # Point coordinates land on pixel centers. We want to find the
+            # smallest shape that will hold the largest point in the data,
+            # using rounding.
+            mask_shape = np.round(self._extent_data[1]) + 1
 
         mask_shape = np.ceil(mask_shape).astype('int')
         masks = self._data_view.to_masks(mask_shape=mask_shape)
@@ -2939,7 +2943,11 @@ def to_labels(self, labels_shape=None):
             For overlapping shapes z-ordering will be respected.
         """
         if labels_shape is None:
-            labels_shape = self._extent_data[1] - self._extent_data[0]
+            # See https://github.com/napari/napari/issues/2778
+            # Point coordinates land on pixel centers. We want to find the
+            # smallest shape that will hold the largest point in the data,
+            # using rounding.
+            labels_shape = np.round(self._extent_data[1]) + 1
 
         labels_shape = np.ceil(labels_shape).astype('int')
         labels = self._data_view.to_labels(labels_shape=labels_shape)


Test patch:
diff --git a/napari/layers/shapes/_tests/test_shapes.py b/napari/layers/shapes/_tests/test_shapes.py
--- a/napari/layers/shapes/_tests/test_shapes.py
+++ b/napari/layers/shapes/_tests/test_shapes.py
@@ -2023,6 +2023,21 @@ def test_to_masks():
     assert masks.shape == (shape[0], 20, 20)
 
 
+def test_to_masks_default_shape():
+    """Test that labels data generation preserves origin at (0, 0).
+
+    See https://github.com/napari/napari/issues/3401
+    """
+    shape = (10, 4, 2)
+    np.random.seed(0)
+    data = 20 * np.random.random(shape) + [50, 100]
+    layer = Shapes(data)
+    masks = layer.to_masks()
+    assert len(masks) == 10
+    assert 50 <= masks[0].shape[0] <= 71
+    assert 100 <= masks[0].shape[1] <= 121
+
+
 def test_to_labels():
     """Test the labels generation."""
     shape = (10, 4, 2)
@@ -2038,6 +2053,22 @@ def test_to_labels():
     assert len(np.unique(labels)) <= 11
 
 
+def test_to_labels_default_shape():
+    """Test that labels data generation preserves origin at (0, 0).
+
+    See https://github.com/napari/napari/issues/3401
+    """
+    shape = (10, 4, 2)
+    np.random.seed(0)
+    data = 20 * np.random.random(shape) + [50, 100]
+    layer = Shapes(data)
+    labels = layer.to_labels()
+    assert labels.ndim == 2
+    assert 1 < len(np.unique(labels)) <= 11
+    assert 50 <= labels.shape[0] <= 71
+    assert 100 <= labels.shape[1] <= 121
+
+
 def test_to_labels_3D():
     """Test label generation for 3D data"""
     data = [