Repository: napari/napari

Issue:
Black line on ellipse mask
## 🐛 Bug

Hi napari developers!

We noticed this behavior when using napari-crop plugin and it seems to me it is related to the `to_masks()` function in napari. Could anyone take a look to see if it is really a bug?

When using the `to_masks()` function of an ellipse shape from [napari.layers.shapes module](https://napari.org/docs/0.3.7/api/napari.layers.shapes.html), a half black line appears around the center of the mask.

## To Reproduce

Steps to reproduce the behavior:

1. Draw an ellipse as a shape layer;
2. Convert it to mask with `to_masks()`;
3. Plot it;

Here is a MWE:

    import napari
    import numpy as np
    import matplotlib.pyplot as plt
    
    image = np.arange(144).reshape(12,12)
    viewer = napari.Viewer()
    image_layer = viewer.add_image(image, colormap = 'gist_earth')
    ellipse = np.array([[0, 0],
                  [0, 10],
                  [10, 0],
                  [10,10]])
    shapes_layer = viewer.add_shapes(ellipse, shape_type='ellipse', edge_width=0)

    print(viewer.layers[1].to_masks())
    plt.imshow(viewer.layers[1].to_masks()[0])

### Output from MWE

[[[False False False False False False False False False False False]
  [False False False  True  True  True  True  True False False False]
  [False False  True  True  True  True  True  True  True False False]
  [False  True  True  True  True  True  True  True  True  True False]
  [False  True  True  True  True  True  True  True  True  True False]
  [False  True  True  True  True  True  True  True False False False]
  [False  True  True  True  True  True  True  True  True  True False]
  [False  True  True  True  True  True  True  True  True  True False]
  [False False  True  True  True  True  True  True  True False False]
  [False False False  True  True  True  True  True False False False]
  [False False False False False False False False False False False]]]
![image](https://user-images.githubusercontent.com/26173597/153145767-1be3add3-15b1-4439-a770-70fcad8f6937.png)


## Expected behavior

I would expect a fully filled mask.

## Environment

napari: 0.4.14
Platform: Windows-10-10.0.17763-SP0
Python: 3.9.10 | packaged by conda-forge | (main, Feb 1 2022, 21:22:07) [MSC v.1929 64 bit (AMD64)]
Qt: 5.12.9
PyQt5: 5.12.3
NumPy: 1.22.2
SciPy: 1.8.0
Dask: 2022.01.1
VisPy: 0.9.6

OpenGL:
- GL version: 4.6.0 - Build 27.20.100.8729
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1280x720, scale 3.0

Plugins:
- console: 0.0.4
- scikit-image: 0.4.14
- svg: 0.1.6

## Additional context

[napari-crop](https://github.com/biapol/napari-crop) plugin uses this function. This was also referred in a [post](https://forum.image.sc/t/napari-how-can-i-see-the-code-of-what-i-do-in-gui/62885/8?u=zoccoler) at image.sc forum.

Black line on ellipse mask
## 🐛 Bug

Hi napari developers!

We noticed this behavior when using napari-crop plugin and it seems to me it is related to the `to_masks()` function in napari. Could anyone take a look to see if it is really a bug?

When using the `to_masks()` function of an ellipse shape from [napari.layers.shapes module](https://napari.org/docs/0.3.7/api/napari.layers.shapes.html), a half black line appears around the center of the mask.

## To Reproduce

Steps to reproduce the behavior:

1. Draw an ellipse as a shape layer;
2. Convert it to mask with `to_masks()`;
3. Plot it;

Here is a MWE:

    import napari
    import numpy as np
    import matplotlib.pyplot as plt
    
    image = np.arange(144).reshape(12,12)
    viewer = napari.Viewer()
    image_layer = viewer.add_image(image, colormap = 'gist_earth')
    ellipse = np.array([[0, 0],
                  [0, 10],
                  [10, 0],
                  [10,10]])
    shapes_layer = viewer.add_shapes(ellipse, shape_type='ellipse', edge_width=0)

    print(viewer.layers[1].to_masks())
    plt.imshow(viewer.layers[1].to_masks()[0])

### Output from MWE

[[[False False False False False False False False False False False]
  [False False False  True  True  True  True  True False False False]
  [False False  True  True  True  True  True  True  True False False]
  [False  True  True  True  True  True  True  True  True  True False]
  [False  True  True  True  True  True  True  True  True  True False]
  [False  True  True  True  True  True  True  True False False False]
  [False  True  True  True  True  True  True  True  True  True False]
  [False  True  True  True  True  True  True  True  True  True False]
  [False False  True  True  True  True  True  True  True False False]
  [False False False  True  True  True  True  True False False False]
  [False False False False False False False False False False False]]]
![image](https://user-images.githubusercontent.com/26173597/153145767-1be3add3-15b1-4439-a770-70fcad8f6937.png)


## Expected behavior

I would expect a fully filled mask.

## Environment

napari: 0.4.14
Platform: Windows-10-10.0.17763-SP0
Python: 3.9.10 | packaged by conda-forge | (main, Feb 1 2022, 21:22:07) [MSC v.1929 64 bit (AMD64)]
Qt: 5.12.9
PyQt5: 5.12.3
NumPy: 1.22.2
SciPy: 1.8.0
Dask: 2022.01.1
VisPy: 0.9.6

OpenGL:
- GL version: 4.6.0 - Build 27.20.100.8729
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1280x720, scale 3.0

Plugins:
- console: 0.0.4
- scikit-image: 0.4.14
- svg: 0.1.6

## Additional context

[napari-crop](https://github.com/biapol/napari-crop) plugin uses this function. This was also referred in a [post](https://forum.image.sc/t/napari-how-can-i-see-the-code-of-what-i-do-in-gui/62885/8?u=zoccoler) at image.sc forum.



Discussion:

Resolving patch:
diff --git a/napari/layers/shapes/_shapes_utils.py b/napari/layers/shapes/_shapes_utils.py
--- a/napari/layers/shapes/_shapes_utils.py
+++ b/napari/layers/shapes/_shapes_utils.py
@@ -942,6 +942,9 @@ def points_in_poly(points, vertices):
         )
         cond_3 = np.logical_or(cond_1, cond_2)
         d = vertices[j] - vertices[i]
+        # Prevents floating point imprecision from generating false positives
+        tolerance = 1e-12
+        d = np.where(abs(d) < tolerance, 0, d)
         if d[1] == 0:
             # If y vertices are aligned avoid division by zero
             cond_4 = 0 < d[0] * (points[:, 1] - vertices[i, 1])


Test patch: