Repository: azavea/raster-vision

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.


Discussion:
This blog post about xView describes how they make chips, which is even more extreme than the above approach. They just use a sliding window with no overlap (similar to chip classification) and just truncate bounding boxes that partially overlap. 

https://medium.com/@dariusl/object-detection-baselines-in-overhead-imagery-with-diux-xview-c39b1852f24f



Resolving patch:
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 @@ def add_classes_to_geojson(geojson, class_map):
                 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_subwindow(self, window, ioa_thresh=1.0):
     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 @@ def get_intersection(self, window):
         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 save_debug_image(im, labels, class_map, output_path):
 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
 


Test patch: