Repository: rasterio/rasterio

Issue:
Shapes() and sieve() describe their mask args incorrectly
These masks don't need to be numpy bools, they only need to evaluate to bool. GDAL style uint8 arrays where 0 means missing data and 255 means valid data are perfectly fine.



Discussion:
@sgillies are you already fixing this, or would you like me to jump on it?

@brendan-ward go for it.


Resolving patch:
diff --git a/rasterio/features.py b/rasterio/features.py
--- a/rasterio/features.py
+++ b/rasterio/features.py
@@ -34,8 +34,8 @@ def shapes(image, mask=None, connectivity=4, transform=IDENTITY):
         Data type must be one of rasterio.int16, rasterio.int32,
         rasterio.uint8, rasterio.uint16, or rasterio.float32.
     mask : numpy ndarray or rasterio Band object, optional
-        Values of False will be excluded from feature generation
-        Must be of type rasterio.bool_
+        Values of False or 0 will be excluded from feature generation
+        Must evaluate to bool (rasterio.bool_ or rasterio.uint8)
     connectivity : int, optional
         Use 4 or 8 pixel connectivity for grouping pixels into features
     transform : Affine transformation, optional
@@ -67,8 +67,8 @@ def shapes(image, mask=None, connectivity=4, transform=IDENTITY):
         raise ValueError('image dtype must be one of: %s'
                          % (', '.join(valid_dtypes)))
 
-    if mask is not None and np.dtype(mask.dtype) != np.dtype(rasterio.bool_):
-        raise ValueError("Mask must be dtype rasterio.bool_")
+    if mask is not None and np.dtype(mask.dtype).name not in ('bool', 'uint8'):
+        raise ValueError("Mask must be dtype rasterio.bool_ or rasterio.uint8")
 
     if connectivity not in (4, 8):
         raise ValueError("Connectivity Option must be 4 or 8")
@@ -99,8 +99,8 @@ def sieve(image, size, out=None, output=None, mask=None, connectivity=4):
     output : older alias for `out`, will be removed before 1.0.
     output : numpy ndarray, optional
     mask : numpy ndarray or rasterio Band object, optional
-        Values of False will be excluded from feature generation
-        Must be of type rasterio.bool_
+        Values of False or 0 will be excluded from feature generation
+        Must evaluate to bool (rasterio.bool_ or rasterio.uint8)
     connectivity : int, optional
         Use 4 or 8 pixel connectivity for grouping pixels into features
 
@@ -140,8 +140,9 @@ def sieve(image, size, out=None, output=None, mask=None, connectivity=4):
         raise ValueError('connectivity must be 4 or 8')
 
     if mask is not None:
-        if np.dtype(mask.dtype) != np.dtype(rasterio.bool_):
-            raise ValueError('Mask must be dtype rasterio.bool_')
+        if np.dtype(mask.dtype) not in ('bool', 'uint8'):
+            raise ValueError('Mask must be dtype rasterio.bool_ or '
+                             'rasterio.uint8')
         elif mask.shape != image.shape:
             raise ValueError('mask shape must be same as image shape')
 
@@ -161,9 +162,9 @@ def sieve(image, size, out=None, output=None, mask=None, connectivity=4):
             out = np.zeros_like(image)
     else:
         if np.dtype(image.dtype).name != np.dtype(out.dtype).name:
-            raise ValueError('output must match dtype of image')
+            raise ValueError('out raster must match dtype of image')
         elif out.shape != image.shape:
-            raise ValueError('mask shape must be same as image shape')
+            raise ValueError('out raster shape must be same as image shape')
 
     with rasterio.drivers():
         _sieve(image, size, out, mask, connectivity)


Test patch:
diff --git a/tests/test_features_shapes.py b/tests/test_features_shapes.py
--- a/tests/test_features_shapes.py
+++ b/tests/test_features_shapes.py
@@ -62,12 +62,21 @@ def test_shapes_connectivity():
     image[5:11, 5:11] = 1
     image[11, 11] = 1
 
+    shapes = ftrz.shapes(image, connectivity=4)
+    shape, val = next(shapes)
+    assert len(shape['coordinates'][0]) == 5
+
     shapes = ftrz.shapes(image, connectivity=8)
     shape, val = next(shapes)
     assert len(shape['coordinates'][0]) == 9
     # Note: geometry is not technically valid at this point, it has a self
     # intersection at 11,11
 
+    # Test invalid connectivity
+    with pytest.raises(ValueError):
+        shapes = ftrz.shapes(image, connectivity=12)
+        next(shapes)
+
 
 def test_shapes_dtype():
     """Test image data type handling"""
@@ -107,4 +116,29 @@ def test_shapes_dtype():
                 image = numpy.zeros((rows, cols), dtype=dtype)
                 image[2:5, 2:5] = test_value
                 shapes = ftrz.shapes(image)
-                shape, value = next(shapes)
+                next(shapes)
+
+        # Test mask types
+        image = numpy.zeros((rows, cols), dtype='uint8')
+        image.fill(255)
+        supported_mask_types = (
+            ('bool', 1),
+            ('uint8', 255)
+        )
+        for dtype, mask_value in supported_mask_types:
+            mask = numpy.zeros((rows, cols), dtype=dtype)
+            mask[2:5, 2:5] = mask_value
+            shapes = ftrz.shapes(image, mask=mask)
+            shape, value = next(shapes)
+            assert value == 255
+
+        unsupported_mask_types = (
+            ('int8', -127),
+            ('int16', -32768)
+        )
+        for dtype, mask_value in unsupported_mask_types:
+            with pytest.raises(ValueError):
+                mask = numpy.zeros((rows, cols), dtype=dtype)
+                mask[2:5, 2:5] = mask_value
+                shapes = ftrz.shapes(image, mask=mask)
+                next(shapes)
\ No newline at end of file
diff --git a/tests/test_features_sieve.py b/tests/test_features_sieve.py
--- a/tests/test_features_sieve.py
+++ b/tests/test_features_sieve.py
@@ -27,6 +27,11 @@ def test_sieve():
         sieved_image = ftrz.sieve(image, 101)
         assert not sieved_image.any()
 
+    # Invalid size value should fail
+    for invalid_size in (0, 45.1234, image.size + 1):
+        with pytest.raises(ValueError):
+            sieved_image = ftrz.sieve(image, invalid_size)
+
 
 def test_sieve_connectivity():
     """Test proper behavior of connectivity"""
@@ -46,6 +51,10 @@ def test_sieve_connectivity():
     sieved_image = ftrz.sieve(image, 54, connectivity=8)
     assert numpy.array_equal(sieved_image, image)
 
+    # Invalid connectivity value should fail
+    with pytest.raises(ValueError):
+        ftrz.sieve(image, 54, connectivity=12)
+
 
 def test_sieve_output():
     """Test proper behavior of output image, if passed into sieve"""
@@ -66,6 +75,11 @@ def test_sieve_output():
         with pytest.raises(ValueError):
             ftrz.sieve(image, 100, output)
 
+        # Output of a different shape should fail
+        output = numpy.zeros((21, 21), dtype=rasterio.ubyte)
+        with pytest.raises(ValueError):
+            ftrz.sieve(image, 100, output)
+
 
 def test_sieve_mask():
     """Test proper behavior of mask image, if passed int sieve"""
@@ -92,9 +106,8 @@ def test_sieve_mask():
         truth[7:10, 7:10] = 1
         assert numpy.array_equal(sieved_image, truth)
 
-        # mask of other type than rasterio.bool_ should fail
-        mask = numpy.zeros(shape, dtype=rasterio.uint8)
         with pytest.raises(ValueError):
+            mask = numpy.ones((21, 21), dtype=rasterio.bool_)
             ftrz.sieve(image, 100, mask=mask)
 
 
@@ -138,7 +151,31 @@ def test_dtypes():
             with pytest.raises(ValueError):
                 image = numpy.zeros((rows, cols), dtype=dtype)
                 image[2:5, 2:5] = test_value
-                sieved_image = ftrz.sieve(image, 2)
+                ftrz.sieve(image, 2)
+
+        # Test mask types
+        image = numpy.zeros((rows, cols), dtype='uint8')
+        image.fill(255)
+
+        supported_mask_types = (
+            ('bool', 1),
+            ('uint8', 255)
+        )
+        for dtype, mask_value in supported_mask_types:
+            mask = numpy.zeros((rows, cols), dtype=dtype)
+            mask[2:5, 2:5] = mask_value
+            sieved_image = ftrz.sieve(image, 2, mask=mask)
+            assert numpy.array_equal(image, sieved_image)
+
+        unsupported_mask_types = (
+            ('int8', -127),
+            ('int16', -32768)
+        )
+        for dtype, mask_value in unsupported_mask_types:
+            with pytest.raises(ValueError):
+                mask = numpy.zeros((rows, cols), dtype=dtype)
+                mask[2:5, 2:5] = mask_value
+                ftrz.sieve(image, 2, mask=mask)
 
 
 def test_sieve_shade():