Repository: danforthcenter/plantcv
Issue: Very small objects breaks plantcv.roi_objects while using roi_type='largest'


## Very small objects breaks plantcv.roi_objects while using roi_type='largest'


### Description
* `plantcv.roi_objects` fails when objects inside the ROI are very small
* There is no built in functionality to ensure the function fails with a useful error code in the case where there are truly no contours partially overlapping with the ROI to keep the function from failing on an UnboundLocalError. 


### Details
 ```plantcv/plantcv/roi_objects.py in roi_objects(img, roi_contour, roi_hierarchy, object_contour, obj_hierarchy, roi_type)
    89
    90             # Store the largest contour as a list
---> 91             largest_cnt = [kept_cnt[index]]
    92
    93             # Store the hierarchy of the largest contour into a list

UnboundLocalError: local variable 'index' referenced before assignment
```
* `plantcv.roi_objects` uses `cv2.contourArea` while sorting contours when `roi_type='largest'` BUT with very small objects (like in the case of skeletonized masks) the cv2 area function will report the size of individual contours as "0.0" even though they are hundreds of pixels in length. 
* `cv2.contourArea` should be replaced with another method of quantifying contour size to allow the size filtering functionality to be more robust.


### Completion Criteria
- [ ] Update contour size algorithm to one that will work on very small objects
- [ ] Add useful fatal error for using `pcv.roi_objects` when there are no `object_contour`s within the `roi_contour` 
- [ ] Add test to fully cover the updated function
- [ ] Add to the package (pull request)


Changed files:
tests/tests.py, plantcv/plantcv/roi_objects.py

diff --git a/plantcv/plantcv/roi_objects.py b/plantcv/plantcv/roi_objects.py
index dc4abf84..ff73cd0b 100755
--- a/plantcv/plantcv/roi_objects.py
+++ b/plantcv/plantcv/roi_objects.py
@@ -60,20 +60,24 @@ def roi_objects(img, roi_contour, roi_hierarchy, object_contour, obj_hierarchy,
                 if int(pptest) != -1:
                     keep = True
             if keep:
-                # Color the "gap contours" white
+                # Color the "gap contours" black
                 if obj_hierarchy[0][c][3] > -1:
                     cv2.drawContours(mask, object_contour, c, (0), -1, lineType=8, hierarchy=obj_hierarchy)
                 else:
-                    # Color the plant contour parts black
+                    # Color the plant contour parts white
                     cv2.drawContours(mask, object_contour, c, (255), -1, lineType=8, hierarchy=obj_hierarchy)
             else:
-                # If the contour isn't overlapping with the ROI, color it white
+                # If the contour isn't overlapping with the ROI, color it black
                 cv2.drawContours(mask, object_contour, c, (0), -1, lineType=8, hierarchy=obj_hierarchy)
 
         # Find the kept contours and area
         kept_cnt, kept_hierarchy = cv2.findContours(np.copy(mask), cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)[-2:]
         obj_area = cv2.countNonZero(mask)
 
+        # If no objects were found partially within the ROI then fail
+        if obj_area == 0:
+            fatal_error("No objects found are within or overlap with the ROI")
+
         # Find the largest contour if roi_type is set to 'largest'
         if roi_type.upper() == 'LARGEST':
             # Print warning statement about this feature
@@ -81,8 +85,9 @@ def roi_objects(img, roi_contour, roi_hierarchy, object_contour, obj_hierarchy,
                   "subcontours will be dropped.")
             # Find the index of the largest contour in the list of contours
             largest_area = 0
+            index = 0
             for c, cnt in enumerate(kept_cnt):
-                area = cv2.contourArea(cnt)
+                area = len(cnt)
                 if area > largest_area:
                     largest_area = area
                     index = c
diff --git a/tests/tests.py b/tests/tests.py
index 09b1e29d..76ae078d 100755
--- a/tests/tests.py
+++ b/tests/tests.py
@@ -2612,6 +2612,20 @@ def test_plantcv_roi_objects_grayscale_input():
     assert len(kept_contours) == 1046
 
 
+def test_plantcv_roi_objects_no_objects():
+    # Read in test data
+    img = cv2.imread(os.path.join(TEST_DATA, TEST_INPUT_COLOR), 0)
+    roi_npz = np.load(os.path.join(TEST_DATA, TEST_INPUT_ROI), encoding="latin1")
+    roi_contour = [np.array([[[750, 750]],[[750, 849]],[[799, 849]],[[799, 750]]])]
+    roi_hierarchy = roi_npz['arr_1']
+    contours_npz = np.load(os.path.join(TEST_DATA, TEST_INPUT_CONTOURS1), encoding="latin1")
+    object_contours = contours_npz['arr_0']
+    object_hierarchy = contours_npz['arr_1']
+    with pytest.raises(RuntimeError):
+        _ = pcv.roi_objects(img=img, roi_type="partial", roi_contour=roi_contour, roi_hierarchy=roi_hierarchy,
+                            object_contour=object_contours, obj_hierarchy=object_hierarchy)
+
+
 def test_plantcv_rotate():
     # Test cache directory
     cache_dir = os.path.join(TEST_TMPDIR, "test_plantcv_rotate_img")