Repository: azavea/raster-vision

Issue:
Handle multiple AOI URIs
It is convenient to have multiple AOIs associated with a scene stored in different files. This way the AOIs used for training / testing could be set by the experiment configuration script. However, it's not easy to do this at the moment because you can only set a single AOI file using `with_aoi_uri`. If we had a `with_aoi_uris` method, then we could handle this.


Discussion:

Resolving patch:
diff --git a/rastervision/data/scene_config.py b/rastervision/data/scene_config.py
--- a/rastervision/data/scene_config.py
+++ b/rastervision/data/scene_config.py
@@ -19,12 +19,12 @@ def __init__(self,
                  raster_source,
                  label_source=None,
                  label_store=None,
-                 aoi_uri=None):
+                 aoi_uris=None):
         self.id = id
         self.raster_source = raster_source
         self.label_source = label_source
         self.label_store = label_store
-        self.aoi_uri = aoi_uri
+        self.aoi_uris = aoi_uris
 
     def create_scene(self, task_config: TaskConfig, tmp_dir: str) -> Scene:
         """Create this scene.
@@ -46,10 +46,14 @@ def create_scene(self, task_config: TaskConfig, tmp_dir: str) -> Scene:
         if self.label_store:
             label_store = self.label_store.create_store(
                 task_config, extent, crs_transformer, tmp_dir)
+
         aoi_polygons = None
-        if self.aoi_uri:
-            aoi_js = json.loads(file_to_str(self.aoi_uri))
-            aoi_polygons = aoi_json_to_shapely(aoi_js, crs_transformer)
+        if self.aoi_uris:
+            aoi_polygons = []
+            for uri in self.aoi_uris:
+                aoi_js = json.loads(file_to_str(uri))
+                aoi_polygons.extend(
+                    aoi_json_to_shapely(aoi_js, crs_transformer))
 
         return Scene(self.id, raster_source, label_source, label_store,
                      aoi_polygons)
@@ -58,7 +62,7 @@ def to_proto(self):
         msg = SceneConfigMsg(
             id=self.id,
             raster_source=self.raster_source.to_proto(),
-            aoi_uri=self.aoi_uri)
+            aoi_uris=self.aoi_uris)
 
         if self.label_source:
             msg.ground_truth_label_source.CopyFrom(
@@ -128,8 +132,9 @@ def update_for_command(self,
             self.label_store.update_for_command(
                 command_type, experiment_config, context, io_def)
 
-        if self.aoi_uri:
-            io_def.add_input(self.aoi_uri)
+        if self.aoi_uris:
+            for uri in self.aoi_uris:
+                io_def.add_input(uri)
 
         return io_def
 
@@ -153,7 +158,7 @@ def __init__(self, prev=None):
                 'raster_source': prev.raster_source,
                 'label_source': prev.label_source,
                 'label_store': prev.label_store,
-                'aoi_uri': prev.aoi_uri
+                'aoi_uris': prev.aoi_uris
             }
         super().__init__(SceneConfig, config)
         self.task = None
@@ -167,9 +172,14 @@ def from_proto(self, msg):
         if msg.HasField('prediction_label_store'):
             b = b.with_label_store(
                 LabelStoreConfig.from_proto(msg.prediction_label_store))
+
+        # Here for backward compatibility.
         if msg.HasField('aoi_uri'):
             b = b.with_aoi_uri(msg.aoi_uri)
 
+        if len(msg.aoi_uris):
+            b = b.with_aoi_uris(msg.aoi_uris)
+
         return b
 
     def with_task(self, task):
@@ -296,10 +306,19 @@ def with_aoi_uri(self, uri):
         """Sets the Area of Interest for the scene.
 
             Args:
-                uri: The URI points to the AoI (nominally a GeoJSON
-                    polygon).
+                uri: a URI of a GeoJSON file with polygons.
 
         """
         b = deepcopy(self)
-        b.config['aoi_uri'] = uri
+        b.config['aoi_uris'] = [uri]
+        return b
+
+    def with_aoi_uris(self, uris):
+        """Sets the areas of interest for the scene.
+
+            Args:
+                uris: List of URIs each to a GeoJSON file with polygons.
+        """
+        b = deepcopy(self)
+        b.config['aoi_uris'] = uris
         return b
diff --git a/rastervision/protos/scene_pb2.py b/rastervision/protos/scene_pb2.py
--- a/rastervision/protos/scene_pb2.py
+++ b/rastervision/protos/scene_pb2.py
@@ -22,7 +22,7 @@
   name='rastervision/protos/scene.proto',
   package='rv.protos',
   syntax='proto2',
-  serialized_pb=_b('\n\x1frastervision/protos/scene.proto\x12\trv.protos\x1a\'rastervision/protos/raster_source.proto\x1a&rastervision/protos/label_source.proto\x1a%rastervision/protos/label_store.proto\"\xde\x01\n\x0bSceneConfig\x12\n\n\x02id\x18\x01 \x02(\t\x12\x34\n\rraster_source\x18\x02 \x02(\x0b\x32\x1d.rv.protos.RasterSourceConfig\x12?\n\x19ground_truth_label_source\x18\x03 \x01(\x0b\x32\x1c.rv.protos.LabelSourceConfig\x12;\n\x16prediction_label_store\x18\x04 \x01(\x0b\x32\x1b.rv.protos.LabelStoreConfig\x12\x0f\n\x07\x61oi_uri\x18\x05 \x01(\t')
+  serialized_pb=_b('\n\x1frastervision/protos/scene.proto\x12\trv.protos\x1a\'rastervision/protos/raster_source.proto\x1a&rastervision/protos/label_source.proto\x1a%rastervision/protos/label_store.proto\"\xf0\x01\n\x0bSceneConfig\x12\n\n\x02id\x18\x01 \x02(\t\x12\x34\n\rraster_source\x18\x02 \x02(\x0b\x32\x1d.rv.protos.RasterSourceConfig\x12?\n\x19ground_truth_label_source\x18\x03 \x01(\x0b\x32\x1c.rv.protos.LabelSourceConfig\x12;\n\x16prediction_label_store\x18\x04 \x01(\x0b\x32\x1b.rv.protos.LabelStoreConfig\x12\x0f\n\x07\x61oi_uri\x18\x05 \x01(\t\x12\x10\n\x08\x61oi_uris\x18\x06 \x03(\t')
   ,
   dependencies=[rastervision_dot_protos_dot_raster__source__pb2.DESCRIPTOR,rastervision_dot_protos_dot_label__source__pb2.DESCRIPTOR,rastervision_dot_protos_dot_label__store__pb2.DESCRIPTOR,])
 _sym_db.RegisterFileDescriptor(DESCRIPTOR)
@@ -72,6 +72,13 @@
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
       options=None),
+    _descriptor.FieldDescriptor(
+      name='aoi_uris', full_name='rv.protos.SceneConfig.aoi_uris', index=5,
+      number=6, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
   ],
   extensions=[
   ],
@@ -85,7 +92,7 @@
   oneofs=[
   ],
   serialized_start=167,
-  serialized_end=389,
+  serialized_end=407,
 )
 
 _SCENECONFIG.fields_by_name['raster_source'].message_type = rastervision_dot_protos_dot_raster__source__pb2._RASTERSOURCECONFIG


Test patch:
diff --git a/tests/data/test_scene.py b/tests/data/test_scene.py
new file mode 100644
--- /dev/null
+++ b/tests/data/test_scene.py
@@ -0,0 +1,55 @@
+import unittest
+
+import rastervision as rv
+from rastervision.rv_config import RVConfig
+
+import tests.mock as mk
+from tests import data_file_path
+
+
+class TestScene(unittest.TestCase):
+    def setUp(self):
+        config = {'PLUGINS_modules': '["{}"]'.format('tests.mock')}
+        rv._registry.initialize_config(config_overrides=config)
+
+        self.temp_dir = RVConfig.get_tmp_dir()
+
+    def tearDown(self):
+        rv._registry.initialize_config()
+        self.temp_dir.cleanup()
+
+    def test_with_aois(self):
+        aoi_uri = data_file_path('evaluator/cc-label-aoi.json')
+        aoi_uris = [aoi_uri, aoi_uri]
+        task_config = rv.TaskConfig.builder(mk.MOCK_TASK).build()
+
+        scene_config = mk.create_mock_scene()
+        scene_config = scene_config.to_builder().with_aoi_uris(
+            aoi_uris).build()
+        scene = scene_config.create_scene(task_config, self.temp_dir.name)
+        self.assertEqual(2, len(scene.aoi_polygons))
+
+    def test_with_aoi(self):
+        aoi_uri = data_file_path('evaluator/cc-label-aoi.json')
+        task_config = rv.TaskConfig.builder(mk.MOCK_TASK).build()
+
+        scene_config = mk.create_mock_scene()
+        scene_config = scene_config.to_builder().with_aoi_uri(aoi_uri).build()
+        scene = scene_config.create_scene(task_config, self.temp_dir.name)
+        self.assertEqual(1, len(scene.aoi_polygons))
+
+    def test_with_aoi_back_compat(self):
+        aoi_uri = data_file_path('evaluator/cc-label-aoi.json')
+        task_config = rv.TaskConfig.builder(mk.MOCK_TASK).build()
+        scene_config = mk.create_mock_scene()
+        scene_msg = scene_config.to_proto()
+        # Use deprecated aoi_uri field.
+        del scene_msg.aoi_uris[:]
+        scene_msg.aoi_uri = aoi_uri
+        scene_config = rv.SceneConfig.from_proto(scene_msg)
+        scene = scene_config.create_scene(task_config, self.temp_dir.name)
+        self.assertEqual(1, len(scene.aoi_polygons))
+
+
+if __name__ == '__main__':
+    unittest.main()