We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
More efficient chipping strategy
In `make_training_chips` for object detection, we create a chip for each object. This means that in a crowded area (like a parking lot), we create many chips that only vary slightly for that area. This is good from a data augmentation standpoint, since it's effectively adding random translations. But, we are also wasting time when training since so many of the images are so similar. This might explain why it takes us so much longer to train an object detection model than a classification model. 

We can avoid this by using the same strategy except that we keep track of which objects have appeared entirely in a chip. If an object has already appeared in a chip, then we don't need to create a chip for it. That way, the parking lot (or similar) only gets into a few chips instead of 50.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/rastervision/label_stores/utils.py`
Content:
```
1 import copy
2 
3 
4 def add_classes_to_geojson(geojson, class_map):
5     """Add missing class_names and class_ids from label GeoJSON."""
6     geojson = copy.deepcopy(geojson)
7     features = geojson['features']
8 
9     for feature in features:
10         properties = feature.get('properties', {})
11         if 'class_id' not in properties:
12             if 'class_name' in properties:
13                 properties['class_id'] = \
14                     class_map.get_by_name(properties['class_name']).id
15             elif 'label' in properties:
16                 # label is considered a synonym of class_name for now in order
17                 # to interface with Raster Foundry.
18                 properties['class_id'] = \
19                     class_map.get_by_name(properties['label']).id
20                 properties['class_name'] = properties['label']
21             else:
22                 # if no class_id, class_name, or label, then just assume
23                 # everything corresponds to class_id = 1.
24                 class_id = 1
25                 class_name = class_map.get_by_id(class_id).name
26                 properties['class_id'] = class_id
27                 properties['class_name'] = class_name
28 
29     return geojson
30
```

Path: `src/rastervision/ml_tasks/object_detection.py`
Content:
```
1 import numpy as np
2 
3 from object_detection.utils import visualization_utils as vis_util
4 
5 from rastervision.core.ml_task import MLTask
6 from rastervision.evaluations.object_detection_evaluation import (
7     ObjectDetectionEvaluation)
8 from rastervision.utils.misc import save_img
9 
10 
11 def save_debug_image(im, labels, class_map, output_path):
12     npboxes = labels.get_npboxes()
13     class_ids = labels.get_class_ids()
14     scores = labels.get_scores()
15     if scores is None:
16         scores = [1.0] * len(labels)
17 
18     vis_util.visualize_boxes_and_labels_on_image_array(
19         im, npboxes, class_ids, scores,
20         class_map.get_category_index(), use_normalized_coordinates=True,
21         line_thickness=2, max_boxes_to_draw=None)
22     save_img(im, output_path)
23 
24 
25 def _make_chip_pos_windows(image_extent, label_store, options):
26     chip_size = options.chip_size
27     pos_windows = []
28     for box in label_store.get_all_labels().get_boxes():
29         window = box.make_random_square_container(
30             image_extent.get_width(), image_extent.get_height(), chip_size)
31         pos_windows.append(window)
32 
33     return pos_windows
34 
35 
36 def _make_label_pos_windows(image_extent, label_store, options):
37     label_buffer = options.object_detection_options.label_buffer
38     pos_windows = []
39     for box in label_store.get_all_labels().get_boxes():
40         window = box.make_buffer(label_buffer, image_extent)
41         pos_windows.append(window)
42 
43     return pos_windows
44 
45 
46 def make_pos_windows(image_extent, label_store, options):
47     window_method = options.object_detection_options.window_method
48 
49     if window_method == 'label':
50         return _make_label_pos_windows(image_extent, label_store, options)
51     elif window_method == 'image':
52         return [image_extent.make_copy()]
53     else:
54         return _make_chip_pos_windows(image_extent, label_store, options)
55 
56 
57 def make_neg_windows(raster_source, label_store, chip_size, nb_windows,
58                      max_attempts):
59     extent = raster_source.get_extent()
60     neg_windows = []
61     for _ in range(max_attempts):
62         window = extent.make_random_square(chip_size)
63         chip = raster_source.get_chip(window)
64         labels = label_store.get_labels(
65             window, ioa_thresh=0.2)
66 
67         # If no labels and not blank, append the chip
68         if len(labels) == 0 and np.sum(chip.ravel()) > 0:
69             neg_windows.append(window)
70 
71         if len(neg_windows) == nb_windows:
72             break
73 
74     return neg_windows
75 
76 
77 class ObjectDetection(MLTask):
78     def get_train_windows(self, scene, options):
79         raster_source = scene.raster_source
80         label_store = scene.ground_truth_label_store
81         # Make positive windows which contain labels.
82         pos_windows = make_pos_windows(
83             raster_source.get_extent(), label_store, options)
84         nb_pos_windows = len(pos_windows)
85 
86         # Make negative windows which do not contain labels.
87         # Generate randow windows and save the ones that don't contain
88         # any labels. It may take many attempts to generate a single
89         # negative window, and could get into an infinite loop in some cases,
90         # so we cap the number of attempts.
91         if nb_pos_windows:
92             nb_neg_windows = round(
93                 options.object_detection_options.neg_ratio * nb_pos_windows)
94         else:
95             nb_neg_windows = 100  # just make some
96         max_attempts = 100 * nb_neg_windows
97         neg_windows = make_neg_windows(
98             raster_source, label_store, options.chip_size,
99             nb_neg_windows, max_attempts)
100 
101         return pos_windows + neg_windows
102 
103     def get_train_labels(self, window, scene, options):
104         return scene.ground_truth_label_store.get_labels(
105             window, ioa_thresh=options.object_detection_options.ioa_thresh)
106 
107     def get_predict_windows(self, extent, options):
108         chip_size = options.chip_size
109         stride = chip_size // 2
110         return extent.get_windows(chip_size, stride)
111 
112     def get_evaluation(self):
113         return ObjectDetectionEvaluation()
114 
115     def save_debug_predict_image(self, scene, debug_dir_uri):
116         # TODO implement this
117         pass
118
```

Path: `src/rastervision/labels/object_detection_labels.py`
Content:
```
1 import numpy as np
2 
3 from object_detection.utils.np_box_list import BoxList
4 from object_detection.utils.np_box_list_ops import (
5     prune_non_overlapping_boxes, clip_to_window, change_coordinate_frame,
6     concatenate, scale, multi_class_non_max_suppression, _copy_extra_fields)
7 
8 from rastervision.core.box import Box
9 from rastervision.core.labels import Labels
10 from rastervision.labels.utils import boxes_to_geojson
11 
12 
13 def geojson_to_labels(geojson, crs_transformer, extent):
14     """Extract boxes and related info from GeoJSON file."""
15     features = geojson['features']
16     boxes = []
17     class_ids = []
18     scores = []
19 
20     for feature in features:
21         # Convert polygon to pixel coords and then convert to bounding box.
22         polygon = feature['geometry']['coordinates'][0]
23         polygon = [crs_transformer.web_to_pixel(p) for p in polygon]
24         xmin, ymin = np.min(polygon, axis=0)
25         xmax, ymax = np.max(polygon, axis=0)
26         boxes.append(Box(ymin, xmin, ymax, xmax))
27 
28         properties = feature['properties']
29         class_ids.append(properties['class_id'])
30         scores.append(properties.get('score', 1.0))
31 
32     boxes = np.array([box.npbox_format() for box in boxes], dtype=float)
33     class_ids = np.array(class_ids)
34     scores = np.array(scores)
35     labels = ObjectDetectionLabels(boxes, class_ids, scores=scores)
36     labels = labels.get_intersection(extent)
37     return labels
38 
39 
40 def inverse_change_coordinate_frame(boxlist, window):
41     scaled_boxlist = scale(boxlist, window.get_height(), window.get_width())
42     npboxes = np.round(scaled_boxlist.get())
43     npboxes += [window.ymin, window.xmin, window.ymin, window.xmin]
44     boxlist_new = BoxList(npboxes)
45     _copy_extra_fields(boxlist_new, boxlist)
46     return boxlist_new
47 
48 
49 class ObjectDetectionLabels(Labels):
50     def __init__(self, npboxes, class_ids, scores=None):
51         self.boxlist = BoxList(npboxes)
52         # This field name actually needs to be 'classes' to be able to use
53         # certain utility functions in the TF Object Detection API.
54         self.boxlist.add_field('classes', class_ids)
55         if scores is not None:
56             self.boxlist.add_field('scores', scores)
57 
58     @staticmethod
59     def from_boxlist(boxlist):
60         scores = boxlist.get_field('scores') \
61                  if boxlist.has_field('scores') else None
62         return ObjectDetectionLabels(
63             boxlist.get(), boxlist.get_field('classes'), scores)
64 
65     @staticmethod
66     def from_geojson(geojson, crs_transformer, extent):
67         return geojson_to_labels(geojson, crs_transformer, extent)
68 
69     @staticmethod
70     def make_empty():
71         npboxes = np.empty((0, 4))
72         labels = np.empty((0,))
73         scores = np.empty((0,))
74         return ObjectDetectionLabels(npboxes, labels, scores)
75 
76     def get_subwindow(self, window, ioa_thresh=1.0):
77         """Returns boxes relative to window.
78 
79         This returns the boxes that overlap enough with window, clipped to
80         the window and in relative coordinates that lie between 0 and 1.
81         """
82         window_npbox = window.npbox_format()
83         window_boxlist = BoxList(np.expand_dims(window_npbox, axis=0))
84         boxlist = prune_non_overlapping_boxes(
85             self.boxlist, window_boxlist, minoverlap=ioa_thresh)
86         boxlist = clip_to_window(boxlist, window_npbox)
87         boxlist = change_coordinate_frame(boxlist, window_npbox)
88         return ObjectDetectionLabels.from_boxlist(boxlist)
89 
90     def get_boxes(self):
91         return [Box.from_npbox(npbox) for npbox in self.boxlist.get()]
92 
93     def get_intersection(self, window):
94         """Returns list of boxes that intersect with window.
95 
96         Does not clip or perform coordinate transform.
97         """
98         window_npbox = window.npbox_format()
99         window_boxlist = BoxList(np.expand_dims(window_npbox, axis=0))
100         boxlist = prune_non_overlapping_boxes(
101             self.boxlist, window_boxlist, minoverlap=0.000001)
102         return ObjectDetectionLabels.from_boxlist(boxlist)
103 
104     def get_coordinates(self):
105         return self.boxlist.get_coordinates()
106 
107     def get_npboxes(self):
108         return self.boxlist.get()
109 
110     def get_scores(self):
111         if self.boxlist.has_field('scores'):
112             return self.boxlist.get_field('scores')
113         return None
114 
115     def get_class_ids(self):
116         return self.boxlist.get_field('classes')
117 
118     def __len__(self):
119         return self.boxlist.get().shape[0]
120 
121     def __str__(self):
122         return str(self.boxlist.get())
123 
124     def concatenate(self, window, labels):
125         boxlist_new = concatenate([
126             self.boxlist,
127             inverse_change_coordinate_frame(labels.boxlist, window)])
128         return ObjectDetectionLabels.from_boxlist(boxlist_new)
129 
130     def prune_duplicates(self, score_thresh, merge_thresh):
131         max_output_size = 1000000
132 
133         # Create a copy of self.boxlist that has a 2D scores
134         # field with a column for each class which is required
135         # by the multi_class_non_max_suppression function. It's
136         # suprising that the scores field has to be in this form since
137         # I haven't seen other functions require that.
138         boxlist = BoxList(self.boxlist.get())
139         classes = self.boxlist.get_field('classes').astype(np.int32)
140         nb_boxes = classes.shape[0]
141         nb_classes = np.max(classes)
142         class_inds = classes - 1
143         scores_1d = self.boxlist.get_field('scores')
144         scores_2d = np.zeros((nb_boxes, nb_classes))
145         # Not sure how to vectorize this so just do for loop :(
146         for box_ind in range(nb_boxes):
147             scores_2d[box_ind, class_inds[box_ind]] = scores_1d[box_ind]
148         boxlist.add_field('scores', scores_2d)
149 
150         pruned_boxlist = multi_class_non_max_suppression(
151             boxlist, score_thresh, merge_thresh, max_output_size)
152         # Add one because multi_class_nms outputs labels that start at zero
153         # instead of one like in the rest of the system.
154         class_ids = pruned_boxlist.get_field('classes')
155         class_ids += 1
156         return ObjectDetectionLabels.from_boxlist(pruned_boxlist)
157 
158     def to_geojson(self, crs_transformer, class_map):
159         boxes = self.get_boxes()
160         class_ids = self.get_class_ids().tolist()
161         scores = self.get_scores().tolist()
162 
163         return boxes_to_geojson(boxes, class_ids, crs_transformer, class_map,
164                                 scores=scores)
165
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/rastervision/label_stores/utils.py b/src/rastervision/label_stores/utils.py
--- a/src/rastervision/label_stores/utils.py
+++ b/src/rastervision/label_stores/utils.py
@@ -26,4 +26,6 @@
                 properties['class_id'] = class_id
                 properties['class_name'] = class_name
 
+        feature['properties'] = properties
+
     return geojson
diff --git a/src/rastervision/labels/object_detection_labels.py b/src/rastervision/labels/object_detection_labels.py
--- a/src/rastervision/labels/object_detection_labels.py
+++ b/src/rastervision/labels/object_detection_labels.py
@@ -90,7 +90,7 @@
     def get_boxes(self):
         return [Box.from_npbox(npbox) for npbox in self.boxlist.get()]
 
-    def get_intersection(self, window):
+    def get_intersection(self, window, min_ioa=0.000001):
         """Returns list of boxes that intersect with window.
 
         Does not clip or perform coordinate transform.
@@ -98,7 +98,7 @@
         window_npbox = window.npbox_format()
         window_boxlist = BoxList(np.expand_dims(window_npbox, axis=0))
         boxlist = prune_non_overlapping_boxes(
-            self.boxlist, window_boxlist, minoverlap=0.000001)
+            self.boxlist, window_boxlist, minoverlap=min_ioa)
         return ObjectDetectionLabels.from_boxlist(boxlist)
 
     def get_coordinates(self):
diff --git a/src/rastervision/ml_tasks/object_detection.py b/src/rastervision/ml_tasks/object_detection.py
--- a/src/rastervision/ml_tasks/object_detection.py
+++ b/src/rastervision/ml_tasks/object_detection.py
@@ -25,10 +25,22 @@
 def _make_chip_pos_windows(image_extent, label_store, options):
     chip_size = options.chip_size
     pos_windows = []
-    for box in label_store.get_all_labels().get_boxes():
-        window = box.make_random_square_container(
-            image_extent.get_width(), image_extent.get_height(), chip_size)
-        pos_windows.append(window)
+    boxes = label_store.get_all_labels().get_boxes()
+    done_boxes = set()
+
+    # Get a random window around each box. If a box was previously included
+    # in a window, then it is skipped.
+    for box in boxes:
+        if box.tuple_format() not in done_boxes:
+            window = box.make_random_square_container(
+                image_extent.get_width(), image_extent.get_height(), chip_size)
+            pos_windows.append(window)
+
+            # Get boxes that lie completely within window
+            window_boxes = label_store.get_all_labels().get_intersection(
+                window, min_ioa=1.0).get_boxes()
+            window_boxes = [box.tuple_format() for box in window_boxes]
+            done_boxes.update(window_boxes)
 
     return pos_windows

{"golden_diff": "diff --git a/src/rastervision/label_stores/utils.py b/src/rastervision/label_stores/utils.py\n--- a/src/rastervision/label_stores/utils.py\n+++ b/src/rastervision/label_stores/utils.py\n@@ -26,4 +26,6 @@\n                 properties['class_id'] = class_id\n                 properties['class_name'] = class_name\n \n+        feature['properties'] = properties\n+\n     return geojson\ndiff --git a/src/rastervision/labels/object_detection_labels.py b/src/rastervision/labels/object_detection_labels.py\n--- a/src/rastervision/labels/object_detection_labels.py\n+++ b/src/rastervision/labels/object_detection_labels.py\n@@ -90,7 +90,7 @@\n     def get_boxes(self):\n         return [Box.from_npbox(npbox) for npbox in self.boxlist.get()]\n \n-    def get_intersection(self, window):\n+    def get_intersection(self, window, min_ioa=0.000001):\n         \"\"\"Returns list of boxes that intersect with window.\n \n         Does not clip or perform coordinate transform.\n@@ -98,7 +98,7 @@\n         window_npbox = window.npbox_format()\n         window_boxlist = BoxList(np.expand_dims(window_npbox, axis=0))\n         boxlist = prune_non_overlapping_boxes(\n-            self.boxlist, window_boxlist, minoverlap=0.000001)\n+            self.boxlist, window_boxlist, minoverlap=min_ioa)\n         return ObjectDetectionLabels.from_boxlist(boxlist)\n \n     def get_coordinates(self):\ndiff --git a/src/rastervision/ml_tasks/object_detection.py b/src/rastervision/ml_tasks/object_detection.py\n--- a/src/rastervision/ml_tasks/object_detection.py\n+++ b/src/rastervision/ml_tasks/object_detection.py\n@@ -25,10 +25,22 @@\n def _make_chip_pos_windows(image_extent, label_store, options):\n     chip_size = options.chip_size\n     pos_windows = []\n-    for box in label_store.get_all_labels().get_boxes():\n-        window = box.make_random_square_container(\n-            image_extent.get_width(), image_extent.get_height(), chip_size)\n-        pos_windows.append(window)\n+    boxes = label_store.get_all_labels().get_boxes()\n+    done_boxes = set()\n+\n+    # Get a random window around each box. If a box was previously included\n+    # in a window, then it is skipped.\n+    for box in boxes:\n+        if box.tuple_format() not in done_boxes:\n+            window = box.make_random_square_container(\n+                image_extent.get_width(), image_extent.get_height(), chip_size)\n+            pos_windows.append(window)\n+\n+            # Get boxes that lie completely within window\n+            window_boxes = label_store.get_all_labels().get_intersection(\n+                window, min_ioa=1.0).get_boxes()\n+            window_boxes = [box.tuple_format() for box in window_boxes]\n+            done_boxes.update(window_boxes)\n \n     return pos_windows\n", "issue": "More efficient chipping strategy\nIn `make_training_chips` for object detection, we create a chip for each object. This means that in a crowded area (like a parking lot), we create many chips that only vary slightly for that area. This is good from a data augmentation standpoint, since it's effectively adding random translations. But, we are also wasting time when training since so many of the images are so similar. This might explain why it takes us so much longer to train an object detection model than a classification model. \r\n\r\nWe can avoid this by using the same strategy except that we keep track of which objects have appeared entirely in a chip. If an object has already appeared in a chip, then we don't need to create a chip for it. That way, the parking lot (or similar) only gets into a few chips instead of 50.\n", "before_files": [{"content": "import copy\n\n\ndef add_classes_to_geojson(geojson, class_map):\n    \"\"\"Add missing class_names and class_ids from label GeoJSON.\"\"\"\n    geojson = copy.deepcopy(geojson)\n    features = geojson['features']\n\n    for feature in features:\n        properties = feature.get('properties', {})\n        if 'class_id' not in properties:\n            if 'class_name' in properties:\n                properties['class_id'] = \\\n                    class_map.get_by_name(properties['class_name']).id\n            elif 'label' in properties:\n                # label is considered a synonym of class_name for now in order\n                # to interface with Raster Foundry.\n                properties['class_id'] = \\\n                    class_map.get_by_name(properties['label']).id\n                properties['class_name'] = properties['label']\n            else:\n                # if no class_id, class_name, or label, then just assume\n                # everything corresponds to class_id = 1.\n                class_id = 1\n                class_name = class_map.get_by_id(class_id).name\n                properties['class_id'] = class_id\n                properties['class_name'] = class_name\n\n    return geojson\n", "path": "src/rastervision/label_stores/utils.py"}, {"content": "import numpy as np\n\nfrom object_detection.utils import visualization_utils as vis_util\n\nfrom rastervision.core.ml_task import MLTask\nfrom rastervision.evaluations.object_detection_evaluation import (\n    ObjectDetectionEvaluation)\nfrom rastervision.utils.misc import save_img\n\n\ndef save_debug_image(im, labels, class_map, output_path):\n    npboxes = labels.get_npboxes()\n    class_ids = labels.get_class_ids()\n    scores = labels.get_scores()\n    if scores is None:\n        scores = [1.0] * len(labels)\n\n    vis_util.visualize_boxes_and_labels_on_image_array(\n        im, npboxes, class_ids, scores,\n        class_map.get_category_index(), use_normalized_coordinates=True,\n        line_thickness=2, max_boxes_to_draw=None)\n    save_img(im, output_path)\n\n\ndef _make_chip_pos_windows(image_extent, label_store, options):\n    chip_size = options.chip_size\n    pos_windows = []\n    for box in label_store.get_all_labels().get_boxes():\n        window = box.make_random_square_container(\n            image_extent.get_width(), image_extent.get_height(), chip_size)\n        pos_windows.append(window)\n\n    return pos_windows\n\n\ndef _make_label_pos_windows(image_extent, label_store, options):\n    label_buffer = options.object_detection_options.label_buffer\n    pos_windows = []\n    for box in label_store.get_all_labels().get_boxes():\n        window = box.make_buffer(label_buffer, image_extent)\n        pos_windows.append(window)\n\n    return pos_windows\n\n\ndef make_pos_windows(image_extent, label_store, options):\n    window_method = options.object_detection_options.window_method\n\n    if window_method == 'label':\n        return _make_label_pos_windows(image_extent, label_store, options)\n    elif window_method == 'image':\n        return [image_extent.make_copy()]\n    else:\n        return _make_chip_pos_windows(image_extent, label_store, options)\n\n\ndef make_neg_windows(raster_source, label_store, chip_size, nb_windows,\n                     max_attempts):\n    extent = raster_source.get_extent()\n    neg_windows = []\n    for _ in range(max_attempts):\n        window = extent.make_random_square(chip_size)\n        chip = raster_source.get_chip(window)\n        labels = label_store.get_labels(\n            window, ioa_thresh=0.2)\n\n        # If no labels and not blank, append the chip\n        if len(labels) == 0 and np.sum(chip.ravel()) > 0:\n            neg_windows.append(window)\n\n        if len(neg_windows) == nb_windows:\n            break\n\n    return neg_windows\n\n\nclass ObjectDetection(MLTask):\n    def get_train_windows(self, scene, options):\n        raster_source = scene.raster_source\n        label_store = scene.ground_truth_label_store\n        # Make positive windows which contain labels.\n        pos_windows = make_pos_windows(\n            raster_source.get_extent(), label_store, options)\n        nb_pos_windows = len(pos_windows)\n\n        # Make negative windows which do not contain labels.\n        # Generate randow windows and save the ones that don't contain\n        # any labels. It may take many attempts to generate a single\n        # negative window, and could get into an infinite loop in some cases,\n        # so we cap the number of attempts.\n        if nb_pos_windows:\n            nb_neg_windows = round(\n                options.object_detection_options.neg_ratio * nb_pos_windows)\n        else:\n            nb_neg_windows = 100  # just make some\n        max_attempts = 100 * nb_neg_windows\n        neg_windows = make_neg_windows(\n            raster_source, label_store, options.chip_size,\n            nb_neg_windows, max_attempts)\n\n        return pos_windows + neg_windows\n\n    def get_train_labels(self, window, scene, options):\n        return scene.ground_truth_label_store.get_labels(\n            window, ioa_thresh=options.object_detection_options.ioa_thresh)\n\n    def get_predict_windows(self, extent, options):\n        chip_size = options.chip_size\n        stride = chip_size // 2\n        return extent.get_windows(chip_size, stride)\n\n    def get_evaluation(self):\n        return ObjectDetectionEvaluation()\n\n    def save_debug_predict_image(self, scene, debug_dir_uri):\n        # TODO implement this\n        pass\n", "path": "src/rastervision/ml_tasks/object_detection.py"}, {"content": "import numpy as np\n\nfrom object_detection.utils.np_box_list import BoxList\nfrom object_detection.utils.np_box_list_ops import (\n    prune_non_overlapping_boxes, clip_to_window, change_coordinate_frame,\n    concatenate, scale, multi_class_non_max_suppression, _copy_extra_fields)\n\nfrom rastervision.core.box import Box\nfrom rastervision.core.labels import Labels\nfrom rastervision.labels.utils import boxes_to_geojson\n\n\ndef geojson_to_labels(geojson, crs_transformer, extent):\n    \"\"\"Extract boxes and related info from GeoJSON file.\"\"\"\n    features = geojson['features']\n    boxes = []\n    class_ids = []\n    scores = []\n\n    for feature in features:\n        # Convert polygon to pixel coords and then convert to bounding box.\n        polygon = feature['geometry']['coordinates'][0]\n        polygon = [crs_transformer.web_to_pixel(p) for p in polygon]\n        xmin, ymin = np.min(polygon, axis=0)\n        xmax, ymax = np.max(polygon, axis=0)\n        boxes.append(Box(ymin, xmin, ymax, xmax))\n\n        properties = feature['properties']\n        class_ids.append(properties['class_id'])\n        scores.append(properties.get('score', 1.0))\n\n    boxes = np.array([box.npbox_format() for box in boxes], dtype=float)\n    class_ids = np.array(class_ids)\n    scores = np.array(scores)\n    labels = ObjectDetectionLabels(boxes, class_ids, scores=scores)\n    labels = labels.get_intersection(extent)\n    return labels\n\n\ndef inverse_change_coordinate_frame(boxlist, window):\n    scaled_boxlist = scale(boxlist, window.get_height(), window.get_width())\n    npboxes = np.round(scaled_boxlist.get())\n    npboxes += [window.ymin, window.xmin, window.ymin, window.xmin]\n    boxlist_new = BoxList(npboxes)\n    _copy_extra_fields(boxlist_new, boxlist)\n    return boxlist_new\n\n\nclass ObjectDetectionLabels(Labels):\n    def __init__(self, npboxes, class_ids, scores=None):\n        self.boxlist = BoxList(npboxes)\n        # This field name actually needs to be 'classes' to be able to use\n        # certain utility functions in the TF Object Detection API.\n        self.boxlist.add_field('classes', class_ids)\n        if scores is not None:\n            self.boxlist.add_field('scores', scores)\n\n    @staticmethod\n    def from_boxlist(boxlist):\n        scores = boxlist.get_field('scores') \\\n                 if boxlist.has_field('scores') else None\n        return ObjectDetectionLabels(\n            boxlist.get(), boxlist.get_field('classes'), scores)\n\n    @staticmethod\n    def from_geojson(geojson, crs_transformer, extent):\n        return geojson_to_labels(geojson, crs_transformer, extent)\n\n    @staticmethod\n    def make_empty():\n        npboxes = np.empty((0, 4))\n        labels = np.empty((0,))\n        scores = np.empty((0,))\n        return ObjectDetectionLabels(npboxes, labels, scores)\n\n    def get_subwindow(self, window, ioa_thresh=1.0):\n        \"\"\"Returns boxes relative to window.\n\n        This returns the boxes that overlap enough with window, clipped to\n        the window and in relative coordinates that lie between 0 and 1.\n        \"\"\"\n        window_npbox = window.npbox_format()\n        window_boxlist = BoxList(np.expand_dims(window_npbox, axis=0))\n        boxlist = prune_non_overlapping_boxes(\n            self.boxlist, window_boxlist, minoverlap=ioa_thresh)\n        boxlist = clip_to_window(boxlist, window_npbox)\n        boxlist = change_coordinate_frame(boxlist, window_npbox)\n        return ObjectDetectionLabels.from_boxlist(boxlist)\n\n    def get_boxes(self):\n        return [Box.from_npbox(npbox) for npbox in self.boxlist.get()]\n\n    def get_intersection(self, window):\n        \"\"\"Returns list of boxes that intersect with window.\n\n        Does not clip or perform coordinate transform.\n        \"\"\"\n        window_npbox = window.npbox_format()\n        window_boxlist = BoxList(np.expand_dims(window_npbox, axis=0))\n        boxlist = prune_non_overlapping_boxes(\n            self.boxlist, window_boxlist, minoverlap=0.000001)\n        return ObjectDetectionLabels.from_boxlist(boxlist)\n\n    def get_coordinates(self):\n        return self.boxlist.get_coordinates()\n\n    def get_npboxes(self):\n        return self.boxlist.get()\n\n    def get_scores(self):\n        if self.boxlist.has_field('scores'):\n            return self.boxlist.get_field('scores')\n        return None\n\n    def get_class_ids(self):\n        return self.boxlist.get_field('classes')\n\n    def __len__(self):\n        return self.boxlist.get().shape[0]\n\n    def __str__(self):\n        return str(self.boxlist.get())\n\n    def concatenate(self, window, labels):\n        boxlist_new = concatenate([\n            self.boxlist,\n            inverse_change_coordinate_frame(labels.boxlist, window)])\n        return ObjectDetectionLabels.from_boxlist(boxlist_new)\n\n    def prune_duplicates(self, score_thresh, merge_thresh):\n        max_output_size = 1000000\n\n        # Create a copy of self.boxlist that has a 2D scores\n        # field with a column for each class which is required\n        # by the multi_class_non_max_suppression function. It's\n        # suprising that the scores field has to be in this form since\n        # I haven't seen other functions require that.\n        boxlist = BoxList(self.boxlist.get())\n        classes = self.boxlist.get_field('classes').astype(np.int32)\n        nb_boxes = classes.shape[0]\n        nb_classes = np.max(classes)\n        class_inds = classes - 1\n        scores_1d = self.boxlist.get_field('scores')\n        scores_2d = np.zeros((nb_boxes, nb_classes))\n        # Not sure how to vectorize this so just do for loop :(\n        for box_ind in range(nb_boxes):\n            scores_2d[box_ind, class_inds[box_ind]] = scores_1d[box_ind]\n        boxlist.add_field('scores', scores_2d)\n\n        pruned_boxlist = multi_class_non_max_suppression(\n            boxlist, score_thresh, merge_thresh, max_output_size)\n        # Add one because multi_class_nms outputs labels that start at zero\n        # instead of one like in the rest of the system.\n        class_ids = pruned_boxlist.get_field('classes')\n        class_ids += 1\n        return ObjectDetectionLabels.from_boxlist(pruned_boxlist)\n\n    def to_geojson(self, crs_transformer, class_map):\n        boxes = self.get_boxes()\n        class_ids = self.get_class_ids().tolist()\n        scores = self.get_scores().tolist()\n\n        return boxes_to_geojson(boxes, class_ids, crs_transformer, class_map,\n                                scores=scores)\n", "path": "src/rastervision/labels/object_detection_labels.py"}], "after_files": [{"content": "import copy\n\n\ndef add_classes_to_geojson(geojson, class_map):\n    \"\"\"Add missing class_names and class_ids from label GeoJSON.\"\"\"\n    geojson = copy.deepcopy(geojson)\n    features = geojson['features']\n\n    for feature in features:\n        properties = feature.get('properties', {})\n        if 'class_id' not in properties:\n            if 'class_name' in properties:\n                properties['class_id'] = \\\n                    class_map.get_by_name(properties['class_name']).id\n            elif 'label' in properties:\n                # label is considered a synonym of class_name for now in order\n                # to interface with Raster Foundry.\n                properties['class_id'] = \\\n                    class_map.get_by_name(properties['label']).id\n                properties['class_name'] = properties['label']\n            else:\n                # if no class_id, class_name, or label, then just assume\n                # everything corresponds to class_id = 1.\n                class_id = 1\n                class_name = class_map.get_by_id(class_id).name\n                properties['class_id'] = class_id\n                properties['class_name'] = class_name\n\n        feature['properties'] = properties\n\n    return geojson\n", "path": "src/rastervision/label_stores/utils.py"}, {"content": "import numpy as np\n\nfrom object_detection.utils import visualization_utils as vis_util\n\nfrom rastervision.core.ml_task import MLTask\nfrom rastervision.evaluations.object_detection_evaluation import (\n    ObjectDetectionEvaluation)\nfrom rastervision.utils.misc import save_img\n\n\ndef save_debug_image(im, labels, class_map, output_path):\n    npboxes = labels.get_npboxes()\n    class_ids = labels.get_class_ids()\n    scores = labels.get_scores()\n    if scores is None:\n        scores = [1.0] * len(labels)\n\n    vis_util.visualize_boxes_and_labels_on_image_array(\n        im, npboxes, class_ids, scores,\n        class_map.get_category_index(), use_normalized_coordinates=True,\n        line_thickness=2, max_boxes_to_draw=None)\n    save_img(im, output_path)\n\n\ndef _make_chip_pos_windows(image_extent, label_store, options):\n    chip_size = options.chip_size\n    pos_windows = []\n    boxes = label_store.get_all_labels().get_boxes()\n    done_boxes = set()\n\n    # Get a random window around each box. If a box was previously included\n    # in a window, then it is skipped.\n    for box in boxes:\n        if box.tuple_format() not in done_boxes:\n            window = box.make_random_square_container(\n                image_extent.get_width(), image_extent.get_height(), chip_size)\n            pos_windows.append(window)\n\n            # Get boxes that lie completely within window\n            window_boxes = label_store.get_all_labels().get_intersection(\n                window, min_ioa=1.0).get_boxes()\n            window_boxes = [box.tuple_format() for box in window_boxes]\n            done_boxes.update(window_boxes)\n\n    return pos_windows\n\n\ndef _make_label_pos_windows(image_extent, label_store, options):\n    label_buffer = options.object_detection_options.label_buffer\n    pos_windows = []\n    for box in label_store.get_all_labels().get_boxes():\n        window = box.make_buffer(label_buffer, image_extent)\n        pos_windows.append(window)\n\n    return pos_windows\n\n\ndef make_pos_windows(image_extent, label_store, options):\n    window_method = options.object_detection_options.window_method\n\n    if window_method == 'label':\n        return _make_label_pos_windows(image_extent, label_store, options)\n    elif window_method == 'image':\n        return [image_extent.make_copy()]\n    else:\n        return _make_chip_pos_windows(image_extent, label_store, options)\n\n\ndef make_neg_windows(raster_source, label_store, chip_size, nb_windows,\n                     max_attempts):\n    extent = raster_source.get_extent()\n    neg_windows = []\n    for _ in range(max_attempts):\n        window = extent.make_random_square(chip_size)\n        chip = raster_source.get_chip(window)\n        labels = label_store.get_labels(\n            window, ioa_thresh=0.2)\n\n        # If no labels and not blank, append the chip\n        if len(labels) == 0 and np.sum(chip.ravel()) > 0:\n            neg_windows.append(window)\n\n        if len(neg_windows) == nb_windows:\n            break\n\n    return neg_windows\n\n\nclass ObjectDetection(MLTask):\n    def get_train_windows(self, scene, options):\n        raster_source = scene.raster_source\n        label_store = scene.ground_truth_label_store\n        # Make positive windows which contain labels.\n        pos_windows = make_pos_windows(\n            raster_source.get_extent(), label_store, options)\n        nb_pos_windows = len(pos_windows)\n\n        # Make negative windows which do not contain labels.\n        # Generate randow windows and save the ones that don't contain\n        # any labels. It may take many attempts to generate a single\n        # negative window, and could get into an infinite loop in some cases,\n        # so we cap the number of attempts.\n        if nb_pos_windows:\n            nb_neg_windows = round(\n                options.object_detection_options.neg_ratio * nb_pos_windows)\n        else:\n            nb_neg_windows = 100  # just make some\n        max_attempts = 100 * nb_neg_windows\n        neg_windows = make_neg_windows(\n            raster_source, label_store, options.chip_size,\n            nb_neg_windows, max_attempts)\n\n        return pos_windows + neg_windows\n\n    def get_train_labels(self, window, scene, options):\n        return scene.ground_truth_label_store.get_labels(\n            window, ioa_thresh=options.object_detection_options.ioa_thresh)\n\n    def get_predict_windows(self, extent, options):\n        chip_size = options.chip_size\n        stride = chip_size // 2\n        return extent.get_windows(chip_size, stride)\n\n    def get_evaluation(self):\n        return ObjectDetectionEvaluation()\n\n    def save_debug_predict_image(self, scene, debug_dir_uri):\n        # TODO implement this\n        pass\n", "path": "src/rastervision/ml_tasks/object_detection.py"}, {"content": "import numpy as np\n\nfrom object_detection.utils.np_box_list import BoxList\nfrom object_detection.utils.np_box_list_ops import (\n    prune_non_overlapping_boxes, clip_to_window, change_coordinate_frame,\n    concatenate, scale, multi_class_non_max_suppression, _copy_extra_fields)\n\nfrom rastervision.core.box import Box\nfrom rastervision.core.labels import Labels\nfrom rastervision.labels.utils import boxes_to_geojson\n\n\ndef geojson_to_labels(geojson, crs_transformer, extent):\n    \"\"\"Extract boxes and related info from GeoJSON file.\"\"\"\n    features = geojson['features']\n    boxes = []\n    class_ids = []\n    scores = []\n\n    for feature in features:\n        # Convert polygon to pixel coords and then convert to bounding box.\n        polygon = feature['geometry']['coordinates'][0]\n        polygon = [crs_transformer.web_to_pixel(p) for p in polygon]\n        xmin, ymin = np.min(polygon, axis=0)\n        xmax, ymax = np.max(polygon, axis=0)\n        boxes.append(Box(ymin, xmin, ymax, xmax))\n\n        properties = feature['properties']\n        class_ids.append(properties['class_id'])\n        scores.append(properties.get('score', 1.0))\n\n    boxes = np.array([box.npbox_format() for box in boxes], dtype=float)\n    class_ids = np.array(class_ids)\n    scores = np.array(scores)\n    labels = ObjectDetectionLabels(boxes, class_ids, scores=scores)\n    labels = labels.get_intersection(extent)\n    return labels\n\n\ndef inverse_change_coordinate_frame(boxlist, window):\n    scaled_boxlist = scale(boxlist, window.get_height(), window.get_width())\n    npboxes = np.round(scaled_boxlist.get())\n    npboxes += [window.ymin, window.xmin, window.ymin, window.xmin]\n    boxlist_new = BoxList(npboxes)\n    _copy_extra_fields(boxlist_new, boxlist)\n    return boxlist_new\n\n\nclass ObjectDetectionLabels(Labels):\n    def __init__(self, npboxes, class_ids, scores=None):\n        self.boxlist = BoxList(npboxes)\n        # This field name actually needs to be 'classes' to be able to use\n        # certain utility functions in the TF Object Detection API.\n        self.boxlist.add_field('classes', class_ids)\n        if scores is not None:\n            self.boxlist.add_field('scores', scores)\n\n    @staticmethod\n    def from_boxlist(boxlist):\n        scores = boxlist.get_field('scores') \\\n                 if boxlist.has_field('scores') else None\n        return ObjectDetectionLabels(\n            boxlist.get(), boxlist.get_field('classes'), scores)\n\n    @staticmethod\n    def from_geojson(geojson, crs_transformer, extent):\n        return geojson_to_labels(geojson, crs_transformer, extent)\n\n    @staticmethod\n    def make_empty():\n        npboxes = np.empty((0, 4))\n        labels = np.empty((0,))\n        scores = np.empty((0,))\n        return ObjectDetectionLabels(npboxes, labels, scores)\n\n    def get_subwindow(self, window, ioa_thresh=1.0):\n        \"\"\"Returns boxes relative to window.\n\n        This returns the boxes that overlap enough with window, clipped to\n        the window and in relative coordinates that lie between 0 and 1.\n        \"\"\"\n        window_npbox = window.npbox_format()\n        window_boxlist = BoxList(np.expand_dims(window_npbox, axis=0))\n        boxlist = prune_non_overlapping_boxes(\n            self.boxlist, window_boxlist, minoverlap=ioa_thresh)\n        boxlist = clip_to_window(boxlist, window_npbox)\n        boxlist = change_coordinate_frame(boxlist, window_npbox)\n        return ObjectDetectionLabels.from_boxlist(boxlist)\n\n    def get_boxes(self):\n        return [Box.from_npbox(npbox) for npbox in self.boxlist.get()]\n\n    def get_intersection(self, window, min_ioa=0.000001):\n        \"\"\"Returns list of boxes that intersect with window.\n\n        Does not clip or perform coordinate transform.\n        \"\"\"\n        window_npbox = window.npbox_format()\n        window_boxlist = BoxList(np.expand_dims(window_npbox, axis=0))\n        boxlist = prune_non_overlapping_boxes(\n            self.boxlist, window_boxlist, minoverlap=min_ioa)\n        return ObjectDetectionLabels.from_boxlist(boxlist)\n\n    def get_coordinates(self):\n        return self.boxlist.get_coordinates()\n\n    def get_npboxes(self):\n        return self.boxlist.get()\n\n    def get_scores(self):\n        if self.boxlist.has_field('scores'):\n            return self.boxlist.get_field('scores')\n        return None\n\n    def get_class_ids(self):\n        return self.boxlist.get_field('classes')\n\n    def __len__(self):\n        return self.boxlist.get().shape[0]\n\n    def __str__(self):\n        return str(self.boxlist.get())\n\n    def concatenate(self, window, labels):\n        boxlist_new = concatenate([\n            self.boxlist,\n            inverse_change_coordinate_frame(labels.boxlist, window)])\n        return ObjectDetectionLabels.from_boxlist(boxlist_new)\n\n    def prune_duplicates(self, score_thresh, merge_thresh):\n        max_output_size = 1000000\n\n        # Create a copy of self.boxlist that has a 2D scores\n        # field with a column for each class which is required\n        # by the multi_class_non_max_suppression function. It's\n        # suprising that the scores field has to be in this form since\n        # I haven't seen other functions require that.\n        boxlist = BoxList(self.boxlist.get())\n        classes = self.boxlist.get_field('classes').astype(np.int32)\n        nb_boxes = classes.shape[0]\n        nb_classes = np.max(classes)\n        class_inds = classes - 1\n        scores_1d = self.boxlist.get_field('scores')\n        scores_2d = np.zeros((nb_boxes, nb_classes))\n        # Not sure how to vectorize this so just do for loop :(\n        for box_ind in range(nb_boxes):\n            scores_2d[box_ind, class_inds[box_ind]] = scores_1d[box_ind]\n        boxlist.add_field('scores', scores_2d)\n\n        pruned_boxlist = multi_class_non_max_suppression(\n            boxlist, score_thresh, merge_thresh, max_output_size)\n        # Add one because multi_class_nms outputs labels that start at zero\n        # instead of one like in the rest of the system.\n        class_ids = pruned_boxlist.get_field('classes')\n        class_ids += 1\n        return ObjectDetectionLabels.from_boxlist(pruned_boxlist)\n\n    def to_geojson(self, crs_transformer, class_map):\n        boxes = self.get_boxes()\n        class_ids = self.get_class_ids().tolist()\n        scores = self.get_scores().tolist()\n\n        return boxes_to_geojson(boxes, class_ids, crs_transformer, class_map,\n                                scores=scores)\n", "path": "src/rastervision/labels/object_detection_labels.py"}]}