Repository: rasterio/rasterio

Issue:
Error in docs?
The documentation for the `mask`  argument in `rasterio.features.shapes` is as follows:

>mask : numpy ndarray or rasterio Band object, optional
>Values of False or 0 will be excluded from feature generation
>Must evaluate to bool (rasterio.bool_ or rasterio.uint8)

This is highly confusing to me since from the `numpy` docs we have:

>When an element of the mask is False, the corresponding element of the associated array is valid and is said to be unmasked. When an element of the mask is True, the corresponding element of the associated array is said to be masked (invalid)

Therefore it is values which are `True` which are invalid, and the `rasterio.features.shapes` functionality should exclude _those_ from feature generation. It appears the documentation is incorrect?



Discussion:
@awa5114 thanks for pointing this out! It's our intent that the numpy semantics apply, but the text was copied from GDAL docs that have the opposite sense. I'll make sure this gets sorted out.
@sgillies thanks for your response. Interesting that the GDAL docs have the opposite sense. For the time being I am using something like this:
`inverse_ma = np.ma.masked_array(block.data, np.logical_not(block.mask))`

which inverts the mask and has polygonize working properly for this particular dataset. However it is a bit "inelegant" so to speak. It would be more intuitive to have it follow the `numpy` convention I guess...


Resolving patch:
diff --git a/rasterio/features.py b/rasterio/features.py
--- a/rasterio/features.py
+++ b/rasterio/features.py
@@ -78,8 +78,12 @@ def shapes(source, 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 or 0 will be excluded from feature generation
-        Must evaluate to bool (rasterio.bool_ or rasterio.uint8)
+        Must evaluate to bool (rasterio.bool_ or rasterio.uint8). Values
+        of False or 0 will be excluded from feature generation.  Note
+        well that this is the inverse sense from Numpy's, where a mask
+        value of True indicates invalid data in an array. If `source` is
+        a Numpy masked array and `mask` is None, the source's mask will
+        be inverted and used in place of `mask`.
     connectivity : int, optional
         Use 4 or 8 pixel connectivity for grouping pixels into features
     transform : Affine transformation, optional
@@ -104,6 +108,10 @@ def shapes(source, mask=None, connectivity=4, transform=IDENTITY):
     memory.
 
     """
+    if hasattr(source, 'mask') and mask is None:
+        mask = ~source.mask
+        source = source.data
+
     transform = guard_transform(transform)
     for s, v in _shapes(source, mask, connectivity, transform):
         yield s, v


Test patch:
diff --git a/tests/test_features.py b/tests/test_features.py
--- a/tests/test_features.py
+++ b/tests/test_features.py
@@ -845,6 +845,25 @@ def test_shapes_mask(basic_image):
     assert value == 1
 
 
+def test_shapes_masked_array(basic_image):
+    """Only pixels not masked out should be converted to features."""
+    mask = np.full(basic_image.shape, False, dtype=rasterio.bool_)
+    mask[4:5, 4:5] = True
+
+    results = list(shapes(np.ma.masked_array(basic_image, mask=mask)))
+
+    assert len(results) == 2
+
+    shape, value = results[0]
+    assert shape == {
+        'coordinates': [
+            [(2, 2), (2, 5), (4, 5), (4, 4), (5, 4), (5, 2), (2, 2)]
+        ],
+        'type': 'Polygon'
+    }
+    assert value == 1
+
+
 def test_shapes_blank_mask(basic_image):
     """Mask is blank so results should mask shapes without mask."""
     assert np.array_equal(