Skip to content
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 72 additions & 0 deletions docs/roi_multi_rect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
## Create Multiple Rectangular Regions of Interest (ROI)

**plantcv.roi.multi_rect**(*img, coord, h=None, w=None, spacing=None, nrows=None, ncols=None*)

**returns** roi_objects

- **Parameters:**
- img = Input image data.
- coord = Two-element tuple of the center of the top left object.
- h = Int height of each rectangular ROI
- w = Int width of each rectangular ROI
- spacing = Two-element tuple of the horizontal and vertical spacing between ROIs.
- nrows = Number of rows in ROI layout.
- ncols = Number of columns in ROI layout.
- **Context:**
- Used to define multiple regions of interest in the same image. Users can either specify a
starting coordinate (`coord`), number of row and columns, and spacing to create a grid of ROIs,
or a custom list of coordinates that specify the centers of the ROIs. Providing a custom list
of coordinates (list of tuples) is useful for missing plants or any arrangement that isn't
a perfect grid. Returns an Objects instance that can be used in downstream steps. The analysis
image includes a number representing ROI order.

**Reference Image**

![Screenshot](img/documentation_images/multi/original_multi_image.jpg)

```python

from plantcv import plantcv as pcv

# Set global debug behavior to None (default), "print" (to file),
# or "plot" (Jupyter Notebooks or X11)
pcv.params.debug = "plot"

# Make a grid of ROIs
rois1 = pcv.roi.multi_rect(img, coord = (40, 90), h=50, w=50,
spacing=(70, 70), nrows=3, ncols=5)

# Specify a list of coordinates of desired ROIs
rois2 = pcv.roi.multi_rect(img, coord=[(40, 90), (105, 90), (170, 90)], h=50, w=50)

```

**Grid of ROIs**

![Screenshot](img/documentation_images/multi/grid_rect_roi.png)

**Custom list of ROIs**

![Screenshot](img/documentation_images/multi/custom_list_rect_roi.png)

### Next steps:

This function returns an Objects dataclass, which can be used with [create_labels](create_labels.md) to create a labeled
mask for use with analysis functions.

```python
lbl_mask, n_lbls = pcv.create_labels(mask=mask, rois=rois)

# Analyze the shape of each plant
shape_img = pcv.analyze.size(img=img1, labeled_mask=lbl_mask, n_labels=n_lbls, label="plant")

# Print out a text file with shape data for each plant in the image
pcv.outputs.save_results(filename=filename)

```

**Image with shape analysis characteristics on each plant**

![Screenshot](img/documentation_images/multi/multi_plants_shape.png)

**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/main/plantcv/plantcv/roi/roi_methods.py)
4 changes: 4 additions & 0 deletions docs/updating.md
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,10 @@ pages for more details on the input and output variable types.
* post v3.1: roi_contours, roi_hierarchies = **plantcv.roi.multi**(*img, coord, radius, spacing=None, nrows=None, ncols=None*)
* post v4.0: roi_objects = **plantcv.roi.multi**(*img, coord, radius=None, spacing=None, nrows=None, ncols=None*)

#### plantcv.roi.multi_rect
* pre v4.10: NA
* post v4.10: roi_objects = **plantcv.roi.multi_rect**(*img, coord, h, w, spacing=None, nrows=None, ncols=None*)

#### plantcv.roi.quick_filter

* pre v4.2.1: NA
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ nav:
- 'Create a Custom ROI': roi_custom.md
- 'Create ROI from Binary Image': roi_from_binary_image.md
- 'Create Multi ROIs': 'roi_multi.md'
- 'Create Multi Rectangular ROIs': 'roi_multi_rect.md'
- 'Create Grid of ROIs Automatically': roi_auto_grid.md
- 'Create Grid of Circular ROIs Automatically': roi_auto_wells.md
- 'Filter a mask by ROI': roi_filter.md
Expand Down
3 changes: 2 additions & 1 deletion plantcv/plantcv/roi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
from plantcv.plantcv.roi.roi_methods import rectangle
from plantcv.plantcv.roi.roi_methods import auto_grid
from plantcv.plantcv.roi.roi_methods import multi
from plantcv.plantcv.roi.roi_methods import multi_rect
from plantcv.plantcv.roi.roi_methods import custom
from plantcv.plantcv.roi.roi_methods import filter
from plantcv.plantcv.roi.roi_methods import auto_wells
from plantcv.plantcv.roi.roi2mask import roi2mask
from plantcv.plantcv.roi.quick_filter import quick_filter

__all__ = ["circle", "ellipse", "from_binary_image", "rectangle", "auto_grid",
"multi", "custom", "auto_wells",
"multi", "multi_rect", "custom", "auto_wells",
"filter", "roi2mask", "quick_filter"]
146 changes: 146 additions & 0 deletions plantcv/plantcv/roi/roi_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,46 @@ def _rois_from_coordinates(img, coord=None, radius=None):
return roi_objects, overlap_img


def _rect_rois_from_coordinates(img, h, w, coord=None):
"""Create multiple circular ROIs on a single image from a list of coordinates

Parameters
----------
img : numpy.ndarray
Input image data
coord : list, optional
List of tuples identifying the center of each roi [(x1,y1),(x2,y2)], by default None
h : int
Height of rectangular ROIs
w : int
Width of rectangular ROIs

Returns
-------
plantcv.plantcv.classes.Objects, numpy.ndarray
A dataclass with roi objects and hierarchies, A binary image with overlapping ROIs
"""
# Get the height and width of the reference image
height, width = np.shape(img)[:2]
overlap_img = np.zeros((height, width), dtype=np.uint8)
roi_objects = Objects()
for i in range(0, len(coord)):
y = int(coord[i][1])
x = int(coord[i][0])
# find corners of rectangle
pt1 = [x, y]
pt2 = [x, y + h - 1]
pt3 = [x + w - 1, y + h - 1]
pt4 = [x + w - 1, y]
# Make a list of contours and hierarchies
rc = [np.array([[pt1], [pt2], [pt3], [pt4]], dtype=np.int32)]
rh = np.array([[[-1, -1, -1, -1]]], dtype=np.int32)
# Keep track of each roi individually to check overlapping
overlap_img = cv2.drawContours(overlap_img, rc, -1, 255, -1)
roi_objects.append(rc, rh)
return roi_objects, overlap_img


def _grid_roi(img, nrows, ncols, coord=None, radius=None, spacing=None):
"""Create a grid of ROIs

Expand Down Expand Up @@ -438,6 +478,58 @@ def _grid_roi(img, nrows, ncols, coord=None, radius=None, spacing=None):
return roi_objects, overlap_img


def _grid_roi_rect(img, nrows, ncols, h, w, coord=None, spacing=None):
"""Create a grid of rectangular ROIs

Parameters
----------
img : numpy.ndarray
Input image data
nrows : int
Number of rows in ROI layout
ncols : int
Number of columns in ROI layout
coord : tuple, optional
Two-element tuple of the center of the top left object (x,y), by default None
h : int
Height of each rectangle
w : int
Width of each rectangle
spacing : tuple, optional
Two-element tuple of the horizontal and vertical spacing between ROIs, (x,y), by default None

Returns
-------
plantcv.plantcv.classes.Objects, numpy.ndarray
A dataclass with roi objects and hierarchies, A binary image with overlapping ROIs
"""
# Get the height and width of the reference image
height, width = np.shape(img)[:2]
overlap_img = np.zeros((height, width), dtype=np.uint8)
roi_objects = Objects()
# Loop over each row
for i in range(0, nrows):
# The upper left corner is the y starting coordinate + the ROI offset * the vertical spacing
y = coord[1] + i * spacing[1]
# Loop over each column
for j in range(0, ncols):
# The upper left corner is the x starting coordinate + the ROI offset * the
# horizontal spacing between chips
x = coord[0] + j * spacing[0]
# calculate corners
pt1 = [x, y]
pt2 = [x, y + h - 1]
pt3 = [x + w - 1, y + h - 1]
pt4 = [x + w - 1, y]
# Create the ROI contours and hierarchy
rc = [np.array([[pt1], [pt2], [pt3], [pt4]], dtype=np.int32)]
rh = np.array([[[-1, -1, -1, -1]]], dtype=np.int32)
# Draw the rectangle on the overall mask
overlap_img = cv2.drawContours(overlap_img, rc, -1, 255, -1)
roi_objects.append(rc, rh)
return roi_objects, overlap_img


def auto_grid(mask, nrows, ncols, radius=None, img=None):
"""Detect and create multiple circular ROIs on a single binary mask
Inputs
Expand Down Expand Up @@ -528,6 +620,60 @@ def multi(img, coord, radius=None, spacing=None, nrows=None, ncols=None):
return roi_objects


def multi_rect(img, coord, h=None, w=None, spacing=None, nrows=None, ncols=None):
"""Create multiple rectangular ROIs on a single image

Inputs
img = Input image data.
coord = Two-element tuple of the center of the top left object (x,y) or a list of tuples identifying
the top left corner of each roi [(x1,y1),(x2,y2), ...]
h = The height of each rectangular ROI
w = The width of each rectangular ROI
spacing = Two-element tuple of the horizontal and vertical spacing between ROIs, (x,y). Ignored if `coord`
is a list and `rows` and `cols` are None.
nrows = Number of rows in ROI layout. Should be missing or None if each center coordinate pair is listed.
ncols = Number of columns in ROI layout. Should be missing or None if each center coordinate pair is listed.

Returns
roi_objects = a dataclass with roi objects and hierarchies

:param img: numpy.ndarray
:param coord: tuple, list
:param h: int
:param w: int
:param spacing: tuple
:param nrows: int
:param ncols: int
:return roi_objects: plantcv.plantcv.classes.Objects
"""
# Grid of ROIs
num_rois = 0
if (isinstance(coord, tuple)) and ((nrows and ncols) is not None) and (isinstance(spacing, tuple)):
roi_objects, overlap_img = _grid_roi_rect(img, nrows, ncols, h, w, coord, spacing)
# The number of ROIs is the product of the number of rows and columns
num_rois = nrows * ncols
# User specified ROI centers
elif (isinstance(coord, list)) and ((nrows and ncols) is None) and (spacing is None):
roi_objects, overlap_img = _rect_rois_from_coordinates(img=img, h=h, w=w, coord=coord)
# The number of ROIs is the length of the list of coordinates
num_rois = len(coord)
else:
fatal_error("Function can either make a grid of ROIs (user must provide nrows, ncols, spacing, and coord) "
"or take custom ROI coordinates (user must provide only a list of tuples to 'coord' parameter). "
"For automatic detection of a grid layout from just nrows, ncols, and a binary mask, use auto_grid")

# Label the ROIs to check for overlap
_, num_labels = label(overlap_img, return_num=True)
# Check for overlapping ROIs where the number of labels is not equal to the number of expected ROIs
if num_labels != num_rois:
warn("Two or more of the user defined regions of interest overlap! "
"If you only see one ROI then they may overlap exactly.")

# Draw the ROIs if requested
_draw_roi(img=img, roi_contour=roi_objects)
return roi_objects


def auto_wells(gray_img, mindist, candec, accthresh, minradius, maxradius, nrows, ncols, radiusadjust=None, roi=None):
"""Hough Circle Well Detection.

Expand Down
39 changes: 34 additions & 5 deletions tests/plantcv/roi/test_roi.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import cv2
import numpy as np
from plantcv.plantcv import Objects
from plantcv.plantcv.roi import from_binary_image, rectangle, circle, ellipse, auto_grid, multi, auto_wells, custom, filter
from plantcv.plantcv.roi import from_binary_image, rectangle, circle, ellipse, auto_grid, multi, multi_rect, auto_wells, custom, filter


def test_from_binary_image(roi_test_data):
Expand Down Expand Up @@ -161,7 +161,7 @@ def test_auto_grid_multiple_cols_rows(roi_test_data):
# Read in test binary mask
mask = cv2.imread(roi_test_data.bin_grid_img, 0)
rois = auto_grid(mask=mask, nrows=2, ncols=2)
# Assert the contours has 2 ROIs
# Assert the contours has 4 ROIs
assert len(rois.contours) == 4


Expand All @@ -170,7 +170,17 @@ def test_multi(roi_test_data):
# Read in test RGB image
rgb_img = cv2.imread(roi_test_data.small_rgb_img)
rois = multi(rgb_img, coord=(10, 10), radius=10, spacing=(10, 10), nrows=2, ncols=2)
# Assert the contours has 18 ROIs
# Assert the contours has 4 ROIs
assert len(rois.hierarchy) == 4


def test_multi_rect(roi_test_data):
"""Test for PlantCV."""
# Read in test RGB image
rgb_img = cv2.imread(roi_test_data.small_rgb_img)
rois = multi_rect(rgb_img, coord=(10, 10), h=20, w=20,
spacing=(10, 10), nrows=2, ncols=2)
# Assert the contours has 4 ROIs
assert len(rois.hierarchy) == 4


Expand All @@ -189,14 +199,23 @@ def test_auto_wells_in_region(test_data):
rect = Objects(contours=[roi_cont], hierarchy=[roi_str])
rois = auto_wells(img, 20, 50, 30, 40, 50, 4, 6, -10, roi=rect)
assert len(rois.hierarchy) == 12



def test_multi_rect_input_coords(roi_test_data):
"""Test for PlantCV."""
# Read in test RGB image
rgb_img = cv2.imread(roi_test_data.small_rgb_img)
rois = multi_rect(rgb_img, coord=[(25, 120), (100, 100)], h=20, w=20)
# Assert the contours has 2 ROIs
assert len(rois.hierarchy) == 2


def test_multi_input_coords(roi_test_data):
"""Test for PlantCV."""
# Read in test RGB image
rgb_img = cv2.imread(roi_test_data.small_rgb_img)
rois = multi(rgb_img, coord=[(25, 120), (100, 100)], radius=20)
# Assert the contours has 18 ROIs
# Assert the contours has 2 ROIs
assert len(rois.hierarchy) == 2


Expand All @@ -209,6 +228,16 @@ def test_multi_bad_input(roi_test_data):
_ = multi(rgb_img, coord=[(25, 120), (100, 100)], radius=20, spacing=(10, 10), nrows=3, ncols=6)


def test_multi_rect_bad_input(roi_test_data):
"""Test for PlantCV."""
# Read in test RGB image
rgb_img = cv2.imread(roi_test_data.small_rgb_img)
# The user must input a list of custom coordinates OR inputs to make a grid. Not both
with pytest.raises(RuntimeError):
_ = multi_rect(rgb_img, coord=[(25, 120), (100, 100)],
h=10, w=10, spacing=(10, 10), nrows=3, ncols=6)


def test_multi_bad_input_no_radius(roi_test_data):
"""Test for PlantCV."""
# Read in test RGB image
Expand Down
Loading