Skip to content
Open
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
6 changes: 3 additions & 3 deletions docs/filters_obj_props.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## Filter on calculated object properties

Calculates properties of objects in a binary image.
Keeps objects that are either above or below a specified threshold for a specified property.
Keeps objects that are either above, below, within, or between a specified threshold for a specified property.
When debug set to "plot," also prints the min, max, and mean of the specified property.

**plantcv.filters.obj_props**(*bin_img, cut_side = "upper", thresh=0, regprop="area", roi=None*)
Expand All @@ -10,8 +10,8 @@ When debug set to "plot," also prints the min, max, and mean of the specified pr

- **Parameters:**
- bin_img - Binary image containing the connected regions to consider
- cut_side - "upper" or "lower", side to keep when objects are divided by the "thresh" value
- thresh - Threshold for keeping objects.
- cut_side - "upper", "lower", "in", or "out". "upper" and "lower" specify the side to keep when objects are divided by the "thresh" value, "in" and "out" specify to keep things in or out of a range of values, in which case thresh must be a tuple.
- thresh - Threshold for keeping objects as an int, float, or tuple. Floats or Ints are used for "upper" and "lower" cut_side, tuples are used for "in" and "out".
- regprop - Which object property to filter on
- roi - Optional rectangular ROI as returned by [`pcv.roi.rectangle`](roi_rectangle.md) within which to apply this function. (default = None, which uses the entire image)
- **Context:**
Expand Down
59 changes: 47 additions & 12 deletions plantcv/plantcv/filters/obj_props.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ def obj_props(bin_img, cut_side="upper", thresh=0, regprop="area", roi=None):
bin_img : numpy.ndarray
Binary image containing the objects to consider.
cut_side : str, default: "upper"
Side to keep when objects are divided by the "thresh" value.
thresh : int | float, default: 0
Region property threshold value.
Side to keep when objects are divided by the "thresh" value,
options are 'upper', 'lower', 'in' (within tuple of thresh range),
and 'out' (outside of tuple of thresh range)
thresh : int | float | tuple of int/float, default: 0
Region property threshold value. 'in' and 'out' cut_side require a tuple.
regprop : str, default: "area"
Region property to filter on. Can choose from "area" or other int and float properties calculated by
skimage.measure.regionprops.
Expand All @@ -34,8 +36,12 @@ def obj_props(bin_img, cut_side="upper", thresh=0, regprop="area", roi=None):
# Make cut_side all lowercase
cut_side = cut_side.lower()
# Check if cut_side is valid
if cut_side not in ("upper", "lower"):
fatal_error("Must specify either 'upper' or 'lower' for cut_side")
if cut_side.lower() not in ("upper", "lower", "in", "out"):
fatal_error("Must specify either 'upper', 'lower', 'in', or 'out' for cut_side")
if cut_side.lower() in ("in", "out") and not isinstance(thresh, tuple):
fatal_error("If cut_side is 'in' or 'out' then thresh must be a tuple")
if cut_side.lower() in ("upper", "lower") and isinstance(thresh, tuple):
fatal_error("If cut_side is 'upper' or 'lower' then thresh must be an int or float")
# subset binary image for ROI
sub_bin_img = _rect_filter(bin_img, roi=roi)
# Skip empty masks
Expand All @@ -59,13 +65,10 @@ def obj_props(bin_img, cut_side="upper", thresh=0, regprop="area", roi=None):
# Object color
gray_val = 255
# Store the value of the property for each object
valueslist.append(getattr(obj, regprop))
# If it is an upper threshold, keep the objects that are above the threshold
if cut_side == "upper":
gray_val = 255 if getattr(obj, regprop) > thresh else 0
# If it is a lower threshold, keep the objects that are below the threshold
elif cut_side == "lower":
gray_val = 255 if getattr(obj, regprop) < thresh else 0
val = getattr(obj, regprop)
valueslist.append(val)
# apply filter
gray_val = _apply_cut_side(cut_side, thresh, val)
# Add the object to the filtered mask (255 if it passes, 0 if it does not)
sub_filtered_mask += np.where(labeled_img == obj.label, gray_val, 0).astype(np.uint8)

Expand All @@ -82,3 +85,35 @@ def obj_props(bin_img, cut_side="upper", thresh=0, regprop="area", roi=None):
_debug(visual=filtered_mask, filename=os.path.join(params.debug_outdir,
f"{params.device}_filter_mask_{regprop}_{thresh}.png"))
return filtered_mask


def _apply_cut_side(cut_side, thresh, val):
"""Helper function to apply a filter based on the cut_side

Parameters
----------
cut_side = str,
direction of filter, one of 'upper', 'lower', 'in', or 'out'
thresh = int, float, or tuple of int/float
value above/below/between/within which to keep an object based on cut_side
val = int or float
The numeric property of an object

Returns
-------
gray_val = int,
255 or 0 depending on the logical evaluation of the cut side
"""
# If it is an upper threshold, keep the objects that are above the threshold
if cut_side == "upper":
gray_val = 255 if val > thresh else 0
# If it is a lower threshold, keep the objects that are below the threshold
elif cut_side == "lower":
gray_val = 255 if val < thresh else 0
# If it is 'in' threshold, keep the objects that are within the thresholds
elif cut_side == "in":
gray_val = 255 if val > min(thresh) or val < max(thresh) else 0
Comment on lines +114 to +115
# If it is 'out' threshold, keep the objects that are outside of the thresholds
elif cut_side == "out":
gray_val = 255 if val < min(thresh) or val > max(thresh) else 0
return gray_val
32 changes: 32 additions & 0 deletions tests/plantcv/filters/test_filter_objs.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@ def test_filter_objs_lower_thresh(filters_test_data):
assert nobjs == 11


def test_filter_objs_in_thresh(filters_test_data):
"""Test for PlantCV."""
# Read in test data
mask = cv2.imread(filters_test_data.barley_example)
filtered_mask = obj_props(bin_img=mask, cut_side="in", thresh=(0.1, 0.6), regprop="solidity")
_, nobjs = create_labels(mask=filtered_mask)
assert nobjs == 20
Comment on lines +34 to +36


def test_filter_objs_out_thresh(filters_test_data):
"""Test for PlantCV."""
# Read in test data
mask = cv2.imread(filters_test_data.barley_example)
filtered_mask = obj_props(bin_img=mask, cut_side="out", thresh=(0.6, 0.8), regprop="solidity")
_, nobjs = create_labels(mask=filtered_mask)
assert nobjs == 16


def test_filter_objs_lower_thresh_roi(filters_test_data):
"""Test for PlantCV."""
# Read in test data
Expand All @@ -46,6 +64,20 @@ def test_bad_params(filters_test_data):
_ = obj_props(bin_img=mask, cut_side="middle")


def test_bad_thresh_lower(filters_test_data):
"""PlantCV Test"""
mask = cv2.imread(filters_test_data.barley_example)
with pytest.raises(RuntimeError):
_ = obj_props(bin_img=mask, cut_side="lower", thresh=(1,2))


def test_bad_thresh_in(filters_test_data):
"""PlantCV Test"""
mask = cv2.imread(filters_test_data.barley_example)
with pytest.raises(RuntimeError):
_ = obj_props(bin_img=mask, cut_side="in", thresh=1)


def test_bad_property(filters_test_data):
"""PlantCV Test"""
mask = cv2.imread(filters_test_data.barley_example)
Expand Down