Repository: scikit-image/scikit-image

Issue:
behaviour of rectangle and polygon of skimage.draw to float inputs
## Description
When `skimage.draw.rectangle` gets float inputs for `start` and `end`, the output is also float, which is unexpected as the docs say that the outputs should be array of ints. Documentation needs clarity as to whether float inputs are allowed and if so, what the expected behavior is.

`skimage.draw.polygon` returns array of ints when `r` or `c` inputs are floats. However the behavior is not clear from the docs.

## Way to reproduce
```python
from skimage.draw import rectangle, polygon
rr, cc = rectangle(start=(1.5, 1.5), end=(6.5, 6.5))
print(rr)
print(cc)
rr, cc = polygon(r=(1.5, 5.5, 3.5), c=(2, 2, 3))
print(rr)
print(cc)
```
Output:
```
[[1.5 2.5 3.5 4.5 5.5 6.5]
 [1.5 2.5 3.5 4.5 5.5 6.5]
 [1.5 2.5 3.5 4.5 5.5 6.5]
 [1.5 2.5 3.5 4.5 5.5 6.5]
 [1.5 2.5 3.5 4.5 5.5 6.5]
 [1.5 2.5 3.5 4.5 5.5 6.5]]
[[1.5 1.5 1.5 1.5 1.5 1.5]
 [2.5 2.5 2.5 2.5 2.5 2.5]
 [3.5 3.5 3.5 3.5 3.5 3.5]
 [4.5 4.5 4.5 4.5 4.5 4.5]
 [5.5 5.5 5.5 5.5 5.5 5.5]
 [6.5 6.5 6.5 6.5 6.5 6.5]]
[2 3 4 5]
[2 2 2 2]
```

## Version information
```python
# Paste the output of the following python commands
from __future__ import print_function
import sys; print(sys.version)
import platform; print(platform.platform())
import skimage; print("scikit-image version: {}".format(skimage.__version__))
import numpy; print("numpy version: {}".format(numpy.__version__))
```

```python
3.7.4 (default, Oct  4 2019, 06:57:26) 
[GCC 9.2.0]
Linux-5.3.6-arch1-1-ARCH-x86_64-with-arch
scikit-image version: 0.16.2
numpy version: 1.17.1
```




Discussion:
@ajkswamy You're absolutely right! I'm unsure yet whether we should change the documentation or the function's behaviour, but something should be fixed for sure. Thanks for the report!
Hi all,

I just noticed this exact problem (came here to report it too).

@jni I would say that this is a bug, and so should be fixed:
- It's counter to what I think is the intent of the function, which is to return _coordinates of pixels_; ie something you can pass as indicies into a numpy array.  This obviously doesn't work when it returns floats.
- The above is seen not just in the documentation for the description, 'Returns' _but also_ the Examples, where the "pass the coordinates into a numpy array" is the primary use case given in the example.  
- It's counter to what the behaviour is for the other draw functions (polygon, ellipse etc)

It looks like the somewhat easy work-around right now is to convert the output to int arrays (which will do an int floor, which will give you a 1 pixel error half the time, or do a rounding to nearest int conversion 
@richard-moss thank you for your input! It's extremely valuable to hear the perspective of our users. Would you be comfortable submitting a fix in a pull request? I think using `np.round(...).astype(int)` is the right approach, *before* clipping to make sure all the coordinates are inside the image shape.
@jni I'll try to get to it the next few days.

That approach does work fine (I'm using it in my code as the workaround), but I'll also check how polygon() etc are doing it right now (that do return int's when passed floats) to make sure it's in line with their implementation.


Hey @richard-moss @jni !

Happy to see this issue, as I stumbled on the same problem. Any news about the fix? I would be happy to help, if needed :)

Resolving patch:
diff --git a/skimage/draw/draw.py b/skimage/draw/draw.py
--- a/skimage/draw/draw.py
+++ b/skimage/draw/draw.py
@@ -925,6 +925,9 @@ def _rectangle_slice(start, end=None, extent=None):
     top_left = np.minimum(start, end)
     bottom_right = np.maximum(start, end)
 
+    top_left = np.round(top_left).astype(int)
+    bottom_right = np.round(bottom_right).astype(int)
+
     if extent is None:
         bottom_right += 1
 


Test patch:
diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py
--- a/skimage/draw/tests/test_draw.py
+++ b/skimage/draw/tests/test_draw.py
@@ -929,6 +929,37 @@ def test_rectangle_end():
     assert_array_equal(img, expected)
 
 
+def test_rectangle_float_input():
+    expected = np.array([[0, 1, 1, 1, 0],
+                         [0, 1, 1, 1, 0],
+                         [0, 1, 1, 1, 0],
+                         [0, 1, 1, 1, 0],
+                         [0, 0, 0, 0, 0]], dtype=np.uint8)
+    start = (0.2, 0.8)
+    end = (3.1, 2.9)
+    img = np.zeros((5, 5), dtype=np.uint8)
+    rr, cc = rectangle(start, end=end, shape=img.shape)
+    img[rr, cc] = 1
+    assert_array_equal(img, expected)
+
+    # Swap start and end
+    img = np.zeros((5, 5), dtype=np.uint8)
+    rr, cc = rectangle(end=start, start=end, shape=img.shape)
+    img[rr, cc] = 1
+    assert_array_equal(img, expected)
+
+    # Bottom left and top right
+    img = np.zeros((5, 5), dtype=np.uint8)
+    rr, cc = rectangle(start=(3.1, 0.8), end=(0.2, 2.9), shape=img.shape)
+    img[rr, cc] = 1
+    assert_array_equal(img, expected)
+
+    img = np.zeros((5, 5), dtype=np.uint8)
+    rr, cc = rectangle(end=(3.1, 0.8), start=(0.2, 2.9), shape=img.shape)
+    img[rr, cc] = 1
+    assert_array_equal(img, expected)
+
+
 def test_rectangle_extent():
     expected = np.array([[0, 0, 0, 0, 0],
                          [0, 1, 1, 1, 0],