Repository: scikit-image/scikit-image

Issue:
disk Inconsistency between `shape=None` and `shape=actual_shape`
## Description

The shape parameter in [disk documentation](https://scikit-image.org/docs/dev/api/skimage.draw.html#disk) (and maybe others using the same machinery) seems misleading to me. Based on the following phrasing, I would expect `shape=None` and the `shape=dimensions_of_my_disk` to lead to equal results. (but it doesn't)

```
Image shape which is used to determine the maximum extent of output pixel coordinates. This is useful for disks that exceed the image size. If None, the full extent of the disk is used. Must be at least length 2. Only the first two values are used to determine the extent of the input image.
```

## Way to reproduce
This MWE shows that using `shape=None` the negative indices are consumed by numpy without trouble and therefore make it a circular canvas where the disk can roll over (see figure below)
```python
from matplotlib import pyplot as plt
from skimage.draw import disk

fig, axes = plt.subplots(1, 2)
for ax, shape in zip(axes, [None, (10, 10)]):
    img = np.zeros((10, 10), dtype=np.uint8)
    rr, cc = disk((1, 1), 5, shape=shape)
    img[rr, cc] = 1
    ax.imshow(img)
```
![image](https://user-images.githubusercontent.com/7044835/97440429-bec60700-1927-11eb-820c-dfe2c04e3f34.png)


## Version information

```
3.8.5 | packaged by conda-forge | (default, Aug 29 2020, 00:43:28) [MSC v.1916 64 bit (AMD64)]
Windows-10-10.0.18362-SP0
scikit-image version: 0.17.2
numpy version: 1.19.1
```




Discussion:
Thanks @massich!

For reference, the original Zulip thread about this is here:

https://skimage.zulipchat.com/#narrow/stream/175598-general/topic/disk.20shape.20parameter.20doc/near/214162314

Excerpt of my responses there:

What's happening is that passing in shape= clips the coordinates to only those coordinates that fit in the shape, while not doing so returns all the coordinates of the disk, even those that are negative. Then, NumPy is totally happy to get negative coordinates and interpret those as indexing from the bottom/right of the array, so you get this wraparound behaviour.

To be honest, I don't know what the best thing to do is. Options:
- clarify in the documentation that not passing in the shape might result in negative coordinates and wraparound behaviour. (this is the easiest option)
- always clip negative coordinates even if the shape is not given
- don't clip but issue a warning.

Honestly, I think I prefer option 1, since some users might use the negative indices to index into some fancy array object having coordinates centered at (0, 0), for example. (e.g. xarray)

Resolving patch:
diff --git a/skimage/draw/draw.py b/skimage/draw/draw.py
--- a/skimage/draw/draw.py
+++ b/skimage/draw/draw.py
@@ -190,11 +190,11 @@ def disk(center, radius, *, shape=None):
     radius : double
         Radius of disk.
     shape : tuple, optional
-        Image shape which is used to determine the maximum extent of output
-        pixel coordinates. This is useful for disks that exceed the image
-        size. If None, the full extent of the disk is used.  Must be at least
-        length 2. Only the first two values are used to determine the extent of
-        the input image.
+        Image shape as a tuple of size 2. Determines the maximum
+        extent of output pixel coordinates. This is useful for disks that
+        exceed the image size. If None, the full extent of the disk is used.
+        The  shape might result in negative coordinates and wraparound
+        behaviour.
 
     Returns
     -------
@@ -205,7 +205,26 @@ def disk(center, radius, *, shape=None):
 
     Examples
     --------
+    >>> import numpy as np
     >>> from skimage.draw import disk
+    >>> shape = (4, 4)
+    >>> img = np.zeros(shape, dtype=np.uint8)
+    >>> rr, cc = disk((0, 0), 2, shape=shape)
+    >>> img[rr, cc] = 1
+    >>> img
+    array([[1, 1, 0, 0],
+           [1, 1, 0, 0],
+           [0, 0, 0, 0],
+           [0, 0, 0, 0]], dtype=uint8)
+    >>> img = np.zeros(shape, dtype=np.uint8)
+    >>> # Negative coordinates in rr and cc perform a wraparound
+    >>> rr, cc = disk((0, 0), 2, shape=None)
+    >>> img[rr, cc] = 1
+    >>> img
+    array([[1, 1, 0, 1],
+           [1, 1, 0, 1],
+           [0, 0, 0, 0],
+           [1, 1, 0, 1]], dtype=uint8)
     >>> img = np.zeros((10, 10), dtype=np.uint8)
     >>> rr, cc = disk((4, 4), 5)
     >>> img[rr, cc] = 1


Test patch: