Repository: azavea/raster-vision

Issue:
Add support for multipolygons
Enable RV to handle geojson label stores that are encoded as multipolygons


Discussion:

Resolving patch:
diff --git a/src/rastervision/label_stores/object_detection_geojson_file.py b/src/rastervision/label_stores/object_detection_geojson_file.py
--- a/src/rastervision/label_stores/object_detection_geojson_file.py
+++ b/src/rastervision/label_stores/object_detection_geojson_file.py
@@ -32,9 +32,7 @@ def geojson_to_labels(geojson_dict, crs_transformer, extent=None):
     class_ids = []
     scores = []
 
-    for feature in features:
-        # Convert polygon to pixel coords and then convert to bounding box.
-        polygon = feature['geometry']['coordinates'][0]
+    def polygon_to_label(polygon, crs_transformer):
         polygon = [crs_transformer.map_to_pixel(p) for p in polygon]
         xmin, ymin = np.min(polygon, axis=0)
         xmax, ymax = np.max(polygon, axis=0)
@@ -44,6 +42,19 @@ def geojson_to_labels(geojson_dict, crs_transformer, extent=None):
         class_ids.append(properties['class_id'])
         scores.append(properties.get('score', 1.0))
 
+    for feature in features:
+        geom_type = feature['geometry']['type']
+        coordinates = feature['geometry']['coordinates']
+        if geom_type == 'MultiPolygon':
+            for polygon in coordinates:
+                polygon_to_label(polygon[0], crs_transformer)
+        elif geom_type == 'Polygon':
+            polygon_to_label(coordinates[0], crs_transformer)
+        else:
+            raise Exception(
+                "Geometries of type {} are not supported in object detection \
+                labels.".format(geom_type))
+
     boxes = np.array([box.npbox_format() for box in boxes], dtype=float)
     class_ids = np.array(class_ids)
     scores = np.array(scores)


Test patch:
diff --git a/src/rastervision/label_stores/object_detection_geojson_file_test.py b/src/rastervision/label_stores/object_detection_geojson_file_test.py
--- a/src/rastervision/label_stores/object_detection_geojson_file_test.py
+++ b/src/rastervision/label_stores/object_detection_geojson_file_test.py
@@ -7,7 +7,8 @@
 from moto import mock_s3
 
 from rastervision.label_stores.object_detection_geojson_file import (
-    ObjectDetectionGeoJSONFile)
+    ObjectDetectionGeoJSONFile, geojson_to_labels)
+from rastervision.label_stores.utils import add_classes_to_geojson
 from rastervision.labels.object_detection_labels import ObjectDetectionLabels
 from rastervision.core.crs_transformer import CRSTransformer
 from rastervision.core.box import Box
@@ -68,6 +69,53 @@ def setUp(self):
             }]
         }
 
+        self.multipolygon_geojson_dict = {
+            'type':
+            'FeatureCollection',
+            'features': [{
+                'type': 'Feature',
+                'geometry': {
+                    'type':
+                    'MultiPolygon',
+                    'coordinates': [[[[0., 0.], [0., 1.], [1., 1.], [1., 0.],
+                                      [0., 0.]]]]
+                },
+                'properties': {
+                    'class_name': 'car',
+                    'score': 0.9
+                }
+            }, {
+                'type': 'Feature',
+                'geometry': {
+                    'type':
+                    'MultiPolygon',
+                    'coordinates':
+                    [[[[1., 1.], [1., 2.], [2., 2.], [2., 1.], [1., 1.]]],
+                     [[[1., 0.], [1., 1.], [2., 1.], [2., 0.], [1., 0.]]]]
+                },
+                'properties': {
+                    'score': 0.9,
+                    'class_name': 'house'
+                }
+            }]
+        }
+
+        self.linestring_geojson_dict = {
+            "type":
+            "FeatureCollection",
+            "features": [{
+                "type": "Feature",
+                "properties": {
+                    'score': 0.9,
+                    'class_name': 'house'
+                },
+                "geometry": {
+                    "type": "LineString",
+                    "coordinates": [[0., 0.], [0., 1.]]
+                }
+            }]
+        }
+
         self.extent = Box.make_square(0, 0, 10)
         self.class_map = ClassMap([ClassItem(1, 'car'), ClassItem(2, 'house')])
 
@@ -79,6 +127,40 @@ def tearDown(self):
         self.mock_s3.stop()
         self.temp_dir.cleanup()
 
+    def test_multipolygon_geojson_to_labels(self):
+        geojson = add_classes_to_geojson(self.multipolygon_geojson_dict,
+                                         self.class_map)
+        labels = geojson_to_labels(geojson, self.crs_transformer)
+
+        # construct expected labels object
+        expected_npboxes = np.array([[0., 0., 2., 2.], [2., 2., 4., 4.],
+                                     [0., 2., 2., 4.]])
+        expected_class_ids = np.array([1, 2, 2])
+        expected_scores = np.array([0.9, 0.9, 0.9])
+        expected_labels = ObjectDetectionLabels(
+            expected_npboxes, expected_class_ids, expected_scores)
+
+        labels.assert_equal(expected_labels)
+
+    def test_polygon_geojson_to_labels(self):
+        geojson = add_classes_to_geojson(self.geojson_dict, self.class_map)
+        labels = geojson_to_labels(geojson, self.crs_transformer)
+
+        # construct expected labels object
+        expected_npboxes = np.array([[0., 0., 2., 2.], [2., 2., 4., 4.]])
+        expected_class_ids = np.array([1, 2])
+        expected_scores = np.array([0.9, 0.9])
+        expected_labels = ObjectDetectionLabels(
+            expected_npboxes, expected_class_ids, expected_scores)
+
+        labels.assert_equal(expected_labels)
+
+    def test_read_invalid_geometry_type(self):
+        with self.assertRaises(Exception):
+            geojson = add_classes_to_geojson(self.linestring_geojson_dict,
+                                             self.class_map)
+            geojson_to_labels(geojson, self.crs_transformer, extent=None)
+
     def test_read_invalid_uri_readable_false(self):
         try:
             invalid_uri = 's3://invalid_path/invalid.json'