Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/roi_filter.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ completely within the image.
- **Parameters:**
- mask = binary image data to be filtered
- roi = region of interest, an instance of the Objects class, output from one of the pcv.roi subpackage functions
- roi_type = 'partial' (for partially inside, default), 'cutto', or 'largest' (keep only the largest contour)
- roi_type = 'partial' (for partially inside, default), 'cutto' (cut objects to the inside of the ROI),
'within' (keep only objects fully inside ROI) or 'largest' (keep only the largest contour)

- **Context:**
- Used to filter objects within a region of interest and decide which ones to keep.
Expand Down
83 changes: 60 additions & 23 deletions plantcv/plantcv/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,30 +440,44 @@ def _cv2_findcontours(bin_img):

def _roi_filter(img, roi, obj, hierarchy, roi_type="partial"):
"""
Helper function to filter contours using a single ROI
Helper function to filter contours using a single ROI.

Find objects partially inside a region of interest or cut objects to the ROI.
Finds objects partially inside a region of interest or cuts objects to the ROI.

Inputs:
img = RGB, binary, or grayscale image data for shape
roi = region of interest, an instance of the Object class output from a roi function
obj = contours of objects, output from "_cv2_findcontours" function
hierarchy = hierarchy of objects, output from "_cv2_findcontours" function
roi_type = 'cutto', 'partial' (for partially inside, default), or 'largest' (keep only the largest contour)
Parameters
----------
img : numpy.ndarray
RGB, binary, or grayscale image data for shape.
roi : plantcv.plantcv.classes.Objects
Region of interest, an instance of the Object class output from a ROI function.
obj : list
Contours of objects, output from "_cv2_findcontours" function.
hierarchy : numpy.ndarray
Hierarchy of objects, output from "_cv2_findcontours" function.
roi_type : str, optional
Type of ROI filtering. Options are:
- 'partial': Find objects partially inside the ROI (default).
- 'cutto': Cut objects to the ROI.
- 'largest': Keep only the largest contour.
- 'within': Keep only objects fully within the ROI.

Returns:
kept_cnt = kept contours
kept_hier = kept hierarchy
mask = mask image

:param img: numpy.ndarray
:param roi: plantcv.plantcv.classes.Objects
:param obj: list
:param hierarchy: np.array
:param roi_type: str
:return kept_cnt: list
:return kept_hier: np.array
:return mask: numpy.ndarray
Returns
-------
kept_cnt : list
List of kept contours after filtering.
kept_hier : numpy.ndarray
Hierarchy of kept contours.
mask : numpy.ndarray
Mask image showing the filtered contours.

Raises
------
RuntimeError
If an invalid `roi_type` is provided.

Notes
-----
If a multi-ROI is provided, only the first ROI will be used. For multi-ROI processing, consider using a for loop.
"""
# Store debug
debug = params.debug
Expand Down Expand Up @@ -507,18 +521,41 @@ def _roi_filter(img, roi, obj, hierarchy, roi_type="partial"):
kept_cnt, kept_hierarchy = _cv2_findcontours(bin_img=mask)

# Allows user to cut objects to the ROI (all objects completely outside ROI will not be kept)
elif roi_type.upper() == 'CUTTO':
elif roi_type.upper() in ('CUTTO', 'WITHIN'):
background1 = np.zeros(np.shape(img)[:2], dtype=np.uint8)
background2 = np.zeros(np.shape(img)[:2], dtype=np.uint8)
cv2.drawContours(background1, object_contour, -1, (255), -1, lineType=8, hierarchy=obj_hierarchy)
roi_points = np.vstack(roi_contour[0])
cv2.fillPoly(background2, [roi_points], (255))
mask = cv2.multiply(background1, background2)
kept_cnt, kept_hierarchy = _cv2_findcontours(bin_img=mask)

# Filter out contours that touch the edge if roi_type is 'within'
if roi_type.upper() == 'WITHIN' and kept_cnt:
# make a mask with the outline of the ROI
roi_outline_mask = np.zeros(np.shape(img)[:2], dtype=np.uint8)
cv2.drawContours(image=roi_outline_mask, contours=roi_contour, contourIdx=-1,
color=255, thickness=1)
# make empty mask to append to
within_mask = np.zeros(np.shape(img)[:2], dtype=np.uint8)
for c, _ in enumerate(kept_cnt):
# for each contour make a mask with that contour filled
filtering_mask = np.zeros(np.shape(img)[:2], dtype=np.uint8)
cv2.fillPoly(filtering_mask, [np.vstack(kept_cnt[c])], (255))
# check overlap with traced ROI
overlap_img = _logical_operation(filtering_mask, roi_outline_mask, 'and')
# check color in original mask, ie don't keep gaps that are 0s.
# append contours fully within ROI to the within_mask
if not overlap_img.any() and kept_hierarchy[0][c][3] == -1:
cv2.drawContours(within_mask, kept_cnt, c,
int(img[kept_cnt[c][0][0][1], kept_cnt[c][0][0][0]]),
-1, lineType=8, hierarchy=kept_hierarchy)
mask = within_mask
kept_cnt, kept_hierarchy = _cv2_findcontours(bin_img=mask)
else:
# Reset debug mode
params.debug = debug
fatal_error('ROI Type ' + str(roi_type) + ' is not "cutto", "largest", or "partial"!')
fatal_error('ROI Type ' + str(roi_type) + ' is not "cutto", "largest", "within" or "partial"!')

# Reset debug mode
params.debug = debug
Expand Down
27 changes: 16 additions & 11 deletions plantcv/plantcv/roi/roi_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -799,20 +799,25 @@ def custom(img, vertices):

# Filter a mask based on a region of interest
def filter(mask, roi, roi_type="partial"):
"""Filter a mask using a region of interest. Connected regions of non-zero pixels outside the ROI turn to zero
"""
Filter a mask using a region of interest.

Inputs:
mask = binary image data to be filtered
roi = region of interest, an instance of the Object class output from a roi function
roi_type = 'cutto', 'partial' (for partially inside, default), or 'largest' (keep only the largest contour)
Connected regions of non-zero pixels outside the ROI are set to zero.

Returns:
filtered_mask = mask image
Parameters
----------
mask : numpy.ndarray
Binary image data to be filtered.
roi : plantcv.plantcv.classes.Objects
Region of interest, an instance of the Object class output from a ROI function.
roi_type : str, optional
Type of ROI filtering: 'cutto', 'partial' (default, for partially inside),
'largest' (keep only the largest contour), or 'within'.

:param mask: numpy.ndarray
:param roi: plantcv.plantcv.classes.Objects
:param roi_type: str
:return filtered_mask: numpy.ndarray
Returns
-------
filtered_mask : numpy.ndarray
Mask image after ROI filtering.
"""
found_obj, found_hier = _cv2_findcontours(bin_img=mask)

Expand Down
18 changes: 17 additions & 1 deletion tests/plantcv/roi/test_roi.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ def test_custom_bad_input(roi_test_data):
_ = custom(img=img, vertices=[[226, -1], [3130, 1848], [2404, 2029], [2205, 2298], [1617, 1761]])


@pytest.mark.parametrize("mode,exp", [["largest", 221], ["cutto", 152], ["partial", 221]])
@pytest.mark.parametrize("mode,exp", [["largest", 221], ["cutto", 152], ["partial", 221], ["within", 0]])
def test_filter(mode, exp, test_data):
"""Test for PlantCV."""
# Read in test data
Expand All @@ -310,6 +310,22 @@ def test_filter(mode, exp, test_data):
assert area == exp


def test_within_filter(test_data):
"""Test for PlantCV."""
# Read in test data
img = cv2.imread(test_data.small_rgb_img)
mask = np.zeros(np.shape(img)[:2], dtype=np.uint8)
cnt, cnt_str = test_data.load_contours(test_data.small_contours_file)
cv2.drawContours(mask, cnt, -1, (255), -1, lineType=8, hierarchy=cnt_str)
roi = [np.array([[[100, 100]], [[100, 224]], [[249, 224]], [[249, 100]]], dtype=np.int32)]
roi_str = np.array([[[-1, -1, -1, -1]]], dtype=np.int32)
roi_Obj = Objects(contours=[roi], hierarchy=[roi_str])
filtered_mask = filter(mask=mask, roi=roi_Obj, roi_type="within")
area = cv2.countNonZero(filtered_mask)
# Assert that the contours were filtered as expected
assert area == 221


def test_filter_multi(test_data):
"""Test for PlantCV."""
# Read in test data
Expand Down