Repository: azavea/raster-vision

Issue:
Output files not being written locally or to S3 when using `predict` on Model Zoo.
## ❓ Questions and Help

On using prediction on `Model Zoo`, the output `json` file is being written to some other location, not on the location fed through CLI.

**CLI Command:**
```python -m rastervision.pipeline.cli predict /opt/src/code/postdam.zip /opt/src/code/postdam.tif s3://byetgistestbucket/predictions```

It is able to read model bundle and .tif file as expected. When storing output to either S3 or locally, it gives the following error:
```
rastervision.pipeline.file_system.file_system.NotWritableError: Could not write s3://raster-vision/examples/0.13/output/isprs-potsdam-ss/predict/2_12/vector_output/polygons-0.json
```

I'm unclear on why is it trying to write the file to **s3://raster-vision/examples/0.13/output/isprs-potsdam-ss/predict/2_12/vector_output/polygons-0.json**, even though I'm entering a different location for output in the command.


Discussion:

Resolving patch:
diff --git a/rastervision_core/rastervision/core/data/label_store/semantic_segmentation_label_store_config.py b/rastervision_core/rastervision/core/data/label_store/semantic_segmentation_label_store_config.py
--- a/rastervision_core/rastervision/core/data/label_store/semantic_segmentation_label_store_config.py
+++ b/rastervision_core/rastervision/core/data/label_store/semantic_segmentation_label_store_config.py
@@ -1,10 +1,14 @@
-from typing import Optional, List
+from typing import TYPE_CHECKING, Optional, List
 from os.path import join
 
 from rastervision.core.data.label_store import (LabelStoreConfig,
                                                 SemanticSegmentationLabelStore)
 from rastervision.pipeline.config import register_config, Config, Field
 
+if TYPE_CHECKING:
+    from rastervision.core.data import SceneConfig  # noqa
+    from rastervision.core.rv_pipeline import RVPipelineConfig  # noqa
+
 
 @register_config('vector_output')
 class VectorOutputConfig(Config):
@@ -23,12 +27,17 @@ class VectorOutputConfig(Config):
         ('Radius of the structural element used to remove high-frequency signals from '
          'the image.'))
 
-    def update(self, pipeline=None, scene=None):
+    def update(self,
+               pipeline: Optional['RVPipelineConfig'] = None,
+               scene: Optional['SceneConfig'] = None,
+               uri_prefix: Optional[str] = None):
         if self.uri is None:
-            if pipeline and scene:
-                mode = self.get_mode()
-                class_id = self.class_id
-                filename = f'{mode}-{class_id}.json'
+            mode = self.get_mode()
+            class_id = self.class_id
+            filename = f'{mode}-{class_id}.json'
+            if uri_prefix is not None:
+                self.uri = join(uri_prefix, 'vector_output', filename)
+            elif pipeline and scene:
                 self.uri = join(pipeline.predict_uri, scene.id,
                                 'vector_output', filename)
 
@@ -127,7 +136,9 @@ def build(self, class_config, crs_transformer, extent, tmp_dir):
 
         return label_store
 
-    def update(self, pipeline=None, scene=None):
+    def update(self,
+               pipeline: Optional['RVPipelineConfig'] = None,
+               scene: Optional['SceneConfig'] = None):
         if pipeline is not None and scene is not None:
             if self.uri is None:
                 self.uri = join(pipeline.predict_uri, f'{scene.id}')
diff --git a/rastervision_core/rastervision/core/predictor.py b/rastervision_core/rastervision/core/predictor.py
--- a/rastervision_core/rastervision/core/predictor.py
+++ b/rastervision_core/rastervision/core/predictor.py
@@ -1,3 +1,4 @@
+from typing import TYPE_CHECKING, List, Optional
 from os.path import join
 import zipfile
 import logging
@@ -6,9 +7,15 @@
 from rastervision.pipeline.config import (build_config, upgrade_config)
 from rastervision.pipeline.file_system.utils import (download_if_needed,
                                                      make_dir, file_to_json)
-from rastervision.core.data.raster_source import ChannelOrderError
+from rastervision.core.data import (ChannelOrderError,
+                                    SemanticSegmentationLabelStoreConfig,
+                                    PolygonVectorOutputConfig)
 from rastervision.core.analyzer import StatsAnalyzerConfig
 
+if TYPE_CHECKING:
+    from rastervision.core.rv_pipeline import RVPipelineConfig  # noqa
+    from rastervision.core.data import SceneConfig  # noqa
+
 log = logging.getLogger(__name__)
 
 
@@ -16,10 +23,10 @@ class Predictor():
     """Class for making predictions based off of a model bundle."""
 
     def __init__(self,
-                 model_bundle_uri,
-                 tmp_dir,
-                 update_stats=False,
-                 channel_order=None):
+                 model_bundle_uri: str,
+                 tmp_dir: str,
+                 update_stats: bool = False,
+                 channel_order: Optional[List[int]] = None):
         """Creates a new Predictor.
 
         Args:
@@ -48,8 +55,8 @@ def __init__(self,
         rv_config.set_everett_config(
             config_overrides=config_dict.get('rv_config'))
         config_dict = upgrade_config(config_dict)
-        self.config = build_config(config_dict)
-        self.scene = self.config.dataset.validation_scenes[0]
+        self.config: 'RVPipelineConfig' = build_config(config_dict)
+        self.scene: 'SceneConfig' = self.config.dataset.validation_scenes[0]
 
         if not hasattr(self.scene.raster_source, 'uris'):
             raise Exception(
@@ -82,7 +89,7 @@ def __init__(self,
 
         self.pipeline = None
 
-    def predict(self, image_uris, label_uri):
+    def predict(self, image_uris: List[str], label_uri: str) -> None:
         """Generate predictions for the given image.
 
         Args:
@@ -98,9 +105,22 @@ def predict(self, image_uris, label_uri):
                 raise Exception(
                     'pipeline in model bundle must have predict method')
 
-        try:
-            self.scene.raster_source.uris = image_uris
+        self.scene.raster_source.uris = image_uris
+        self.scene.label_store.uri = label_uri
+
+        if isinstance(self.scene.label_store,
+                      SemanticSegmentationLabelStoreConfig):
+            # create vector outputs for each class without specifying URIs
+            self.scene.label_store.vector_output = [
+                PolygonVectorOutputConfig(class_id=i)
+                for i, _ in enumerate(self.config.dataset.class_config)
+            ]
+            # set URIs
             self.scene.label_store.uri = label_uri
+            for vo in self.scene.label_store.vector_output:
+                vo.update(self.config, self.scene, uri_prefix=label_uri)
+
+        try:
             if self.update_stats:
                 self.pipeline.analyze()
             self.pipeline.predict()


Test patch:
diff --git a/tests/core/data/label_store/test_semantic_segmentation_label_store.py b/tests/core/data/label_store/test_semantic_segmentation_label_store.py
--- a/tests/core/data/label_store/test_semantic_segmentation_label_store.py
+++ b/tests/core/data/label_store/test_semantic_segmentation_label_store.py
@@ -2,7 +2,7 @@
 from os.path import realpath
 
 from rastervision.core.data.label_store import (
-    VectorOutputConfig, PolygonVectorOutputConfig, BuildingVectorOutputConfig,
+    PolygonVectorOutputConfig, BuildingVectorOutputConfig,
     SemanticSegmentationLabelStoreConfig)
 
 
@@ -17,7 +17,11 @@ class MockSceneConfig():
 class TestSemanticSegmentationLabelStoreConfig(unittest.TestCase):
     def test_vector_output_config(self):
         # uri not updated if no pipeline and scene
-        cfg = VectorOutputConfig(class_id=0)
+        cfg = PolygonVectorOutputConfig(class_id=0)
+        cfg.update()
+        self.assertIsNone(cfg.uri)
+
+        cfg = BuildingVectorOutputConfig(class_id=0)
         cfg.update()
         self.assertIsNone(cfg.uri)