Repository: rasterio/rasterio

Issue:
shapes from data types other than uint8
The companion to #136.



Discussion:
A few improvements pending in #155 will help this implementation.  Once that is approved, I can try and help with this.


Resolving patch:
diff --git a/rasterio/features.py b/rasterio/features.py
--- a/rasterio/features.py
+++ b/rasterio/features.py
@@ -22,17 +22,26 @@ def emit(self, record):
 
 def shapes(image, mask=None, connectivity=4, transform=IDENTITY):
     """Yields a (shape, image_value) pair for each feature in the image.
-    
-    The shapes are GeoJSON-like dicts and the image values are ints.
-    
+
+    The shapes are GeoJSON-like dicts and the image values are ints or floats
+    depending on the data type of the image.
+
     Features are found using a connected-component labeling algorithm.
 
-    The image must be of unsigned 8-bit integer (rasterio.byte or
-    numpy.uint8) data type. If a mask is provided, pixels for which the
-    mask is `False` will be excluded from feature generation.
+    The image must be one of int16, int32, uint8, uint16, float32 data types.
+    Note: due to floating point precision issues, the floating point values
+    returned from a floating point image may not exactly match the original
+    values.
+
+    If a mask is provided, pixels for which the mask is `False` will be
+    excluded from feature generation.
     """
-    if np.dtype(image.dtype) != np.dtype(rasterio.ubyte):
-        raise ValueError("Image must be dtype uint8/ubyte")
+
+    valid_dtypes = ('int16', 'int32', 'uint8', 'uint16', 'float32')
+
+    if np.dtype(image.dtype).name not in valid_dtypes:
+        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_")
@@ -52,7 +61,7 @@ def sieve(image, size, connectivity=4, output=None):
 
     Features smaller than the specified size have their pixel value
     replaced by that of the largest neighboring features.
-    
+
     The image must be of unsigned 8-bit integer (rasterio.byte or
     numpy.uint8) data type.
     """
@@ -68,7 +77,7 @@ def sieve(image, size, connectivity=4, output=None):
 
 
 def rasterize(
-        shapes, 
+        shapes,
         out_shape=None,
         fill=0,
         output=None,
@@ -96,7 +105,7 @@ def rasterize(
     :param fill: fill value for created image array
     :param output: alternatively, an existing image array
 
-    :param all_touched: if True, will rasterize all pixels touched, 
+    :param all_touched: if True, will rasterize all pixels touched,
     otherwise will use GDAL default method.
     :param default_value: value burned in for shapes if not provided as part
     of shapes.
@@ -178,7 +187,7 @@ def can_cast_dtype(values, dtype):
 
     if output is not None:
         if np.dtype(output.dtype).name not in valid_dtypes:
-            raise ValueError('Output image dtype must be one of: %s' 
+            raise ValueError('Output image dtype must be one of: %s'
                              % (', '.join(valid_dtypes)))
         if not can_cast_dtype(shape_values, output.dtype):
             raise ValueError('shape values cannot be cast to dtype of output '


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
@@ -7,10 +7,13 @@
 import rasterio
 import rasterio.features as ftrz
 
+
 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
 
+
 def test_shapes():
     """Access to shapes of labeled features"""
+
     image = numpy.zeros((20, 20), dtype=rasterio.ubyte)
     image[5:15,5:15] = 127
     with rasterio.drivers():
@@ -30,8 +33,10 @@ def test_shapes():
         else:
             assert False
 
+
 def test_shapes_band_shortcut():
     """Access to shapes of labeled features"""
+
     with rasterio.drivers():
         with rasterio.open('tests/data/shade.tif') as src:
             shapes = ftrz.shapes(rasterio.band(src, 1))
@@ -40,8 +45,10 @@ def test_shapes_band_shortcut():
             assert len(shape['coordinates']) == 1
             assert val == 255
 
+
 def test_shapes_internal_driver_manager():
     """Access to shapes of labeled features"""
+
     image = numpy.zeros((20, 20), dtype=rasterio.ubyte)
     image[5:15,5:15] = 127
     shapes = ftrz.shapes(image)
@@ -59,3 +66,44 @@ def test_shapes_connectivity():
     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
+
+
+def test_shapes_dtype():
+    """Test image dtype handling"""
+
+    rows = cols = 10
+    with rasterio.drivers():
+        supported_types = (
+            ('int16', -32768),
+            ('int32', -2147483648),
+            ('uint8', 255),
+            ('uint16', 65535),
+            ('float32', 1.434532)
+        )
+
+        for dtype, test_value in supported_types:
+            image = numpy.zeros((rows, cols), dtype=dtype)
+            image[2:5, 2:5] = test_value
+
+            shapes = ftrz.shapes(image)
+            shape, value = next(shapes)
+            if dtype == 'float32':
+                assert round(value, 6) == round(test_value, 6)
+            else:
+                assert value == test_value
+
+        # Unsupported types should all raise exceptions
+        unsupported_types = (
+            ('int8', -127),
+            ('uint32', 4294967295),
+            ('int64', 20439845334323),
+            ('float16', -9343.232),
+            ('float64', -98332.133422114)
+        )
+
+        for dtype, test_value in unsupported_types:
+            with pytest.raises(ValueError):
+                image = numpy.zeros((rows, cols), dtype=dtype)
+                image[2:5, 2:5] = test_value
+                shapes = ftrz.shapes(image)
+                shape, value = next(shapes)