Skip to content

Commit

Permalink
Merge pull request #1616 from danforthcenter/1615-adding-houghcircles
Browse files Browse the repository at this point in the history
  • Loading branch information
nfahlgren authored Dec 16, 2024
2 parents 9e8e5bf + b799872 commit 723f2c2
Show file tree
Hide file tree
Showing 12 changed files with 263 additions and 2 deletions.
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.
44 changes: 44 additions & 0 deletions docs/roi_auto_wells.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## Autodetect Cicular Regions of Interest (ROI)

**plantcv.roi.auto_wells**(*gray_img, mindist, candec, accthresh, minradius, maxradius, nrows, ncols, radiusadjust=None*)

**returns** roi_objects

- **Parameters:**
- gray_img = Gray single channel image data
- mindist = minimum distance between detected circles
- candec = higher threshold of canny edge detector
- accthresh = accumulator threshold for the circl centers
- minradius = minimum circle radius
- maxradius = maximum circle radius
- nrows = expected number of rows
- ncols = expected number of columns
- radiusadjust = amount to adjust the average radius, this can be desirable if you want ROI to sit inside a well, for example (in that case you might set it to a negative value).
- **Context:**
- Uses a Hough Circle detector to find circular shapes, then uses a gaussian mixture model to sort found circular objects so they are ordered from
top left to bottom right. We assume that circles are of approximately equal size because we calculate an average radius of all of the found circles.
The average radius size can be adjusted with the radius adjust parameter, for example in the case that you'd like the ROI to sit inside of the well.

**Reference Image**

![Screenshot](img/documentation_images/roi_auto_wells/circle-wells.png)

```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"

# Detect Circular Shapes and Use as ROIs
rois1 = pcv.roi.auto_wells(gray_img=gray_img, mindist = 20, candec = 50,
accthresh = 30, minradius = 40, maxradius = 50, nrows=4, ncols=6, radiusadjust=-10)

```

**Grid of ROIs**

![Screenshot](img/documentation_images/roi_auto_wells/21_roi.png)

**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/main/plantcv/plantcv/roi/roi_methods.py)
5 changes: 5 additions & 0 deletions docs/updating.md
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ pages for more details on the input and output variable types.
* pre v4.0: NA
* post v4.0: roi_objects = **pcv.roi.auto_grid**(*mask, nrows, ncols, radius=None, img=None*)

#### plantcv.roi.auto_wells

* pre v4.6: NA
* post v4.6: roi_objects = **pcv.roi.auto_wells**(*gray_img, mindist, candec, accthresh, minradius, maxradius, nrows, ncols, radiusadjust=None*)

#### plantcv.roi.multi

* pre v3.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 ROI from Binary Image': roi_from_binary_image.md
- 'Create Multi ROIs': 'roi_multi.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
- 'Filter a mask by ROI (quickly)': roi_quick_filter.md
- 'Convert ROI to Mask': roi2mask.md
Expand Down
88 changes: 88 additions & 0 deletions plantcv/plantcv/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,94 @@
from plantcv.plantcv.logical_and import logical_and
from plantcv.plantcv import fatal_error, warn
from plantcv.plantcv import params
import pandas as pd


def _hough_circle(gray_img, mindist, candec, accthresh, minradius, maxradius, maxfound=None):
"""
Hough Circle Detection
Keyword inputs:
gray_img = gray image (np.ndarray)
mindist = minimum distance between detected circles
candec = higher threshold of canny edge detector
accthresh = accumulator threshold for the circl centers
minradius = minimum circle radius
maxradius = maximum circle radius
maxfound = maximum number of circles to find
:param gray_img: np.ndarray
:param mindist: int
:param candec: int
:param accthresh: int
:param minradius: int
:param maxradius: int
:param maxfound: None or int
:return dataframe: pandas dataframe
:return img: np.ndarray
"""
# Store debug
debug = params.debug
params.debug = None

circles = cv2.HoughCircles(gray_img, cv2.HOUGH_GRADIENT,
dp=1, minDist=mindist,
param1=candec, param2=accthresh,
minRadius=minradius, maxRadius=maxradius)

cimg = cv2.cvtColor(gray_img, cv2.COLOR_GRAY2BGR)
x = []
y = []
radius = []
if circles is None:
fatal_error('number of circles found is None with these parameters')
circles = np.uint16(np.around(circles))
if maxfound is not None:
if maxfound >= len(circles[0, :]):
for i in circles[0, :]:
# draw the outer circle
cv2.circle(cimg, (i[0], i[1]), i[2], (0, 255, 0),
params.line_thickness)
# draw the center of the circle
cv2.circle(cimg, (i[0], i[1]), 2, (0, 0, 255),
params.line_thickness)
x.append(i[0])
y.append(i[1])
radius.append(i[2])
else:
for n, i in enumerate(circles[0, :]):
if n <= (maxfound-1):
# draw the outer circle
cv2.circle(cimg, (i[0], i[1]), i[2], (0, 255, 0),
params.line_thickness)
# draw the center of the circle
cv2.circle(cimg, (i[0], i[1]), 2, (0, 0, 255),
params.line_thickness)
x.append(i[0])
y.append(i[1])
radius.append(i[2])
warn('Number of found circles is ' +
str(len(circles[0, :])) +
' Change Parameters. Only drawing first '+str(maxfound))
else:
for i in circles[0, :]:
# draw the outer circle
cv2.circle(cimg, (i[0], i[1]), i[2], (0, 255, 0),
params.line_thickness)
# draw the center of the circle
cv2.circle(cimg, (i[0], i[1]), 2, (0, 0, 255),
params.line_thickness)
x.append(i[0])
y.append(i[1])
radius.append(i[2])

data = {'x': x, 'y': y, 'radius': radius}
df = pd.DataFrame(data)

# Reset debug mode
params.debug = debug

return df, cimg


def _cv2_findcontours(bin_img):
Expand Down
4 changes: 3 additions & 1 deletion plantcv/plantcv/roi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
from plantcv.plantcv.roi.roi_methods import multi
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",
__all__ = ["circle", "ellipse", "from_binary_image", "rectangle", "auto_grid",
"multi", "custom", "auto_wells",
"filter", "roi2mask", "quick_filter"]
77 changes: 77 additions & 0 deletions plantcv/plantcv/roi/roi_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import os
import cv2
import numpy as np
import pandas as pd
from sklearn.mixture import GaussianMixture
from skimage.measure import label
from plantcv.plantcv._debug import _debug
from plantcv.plantcv import color_palette
from plantcv.plantcv._helpers import _cv2_findcontours
from plantcv.plantcv._helpers import _roi_filter
from plantcv.plantcv._helpers import _hough_circle
from plantcv.plantcv import fatal_error, warn, params, Objects


Expand Down Expand Up @@ -525,6 +527,81 @@ def multi(img, coord, radius=None, spacing=None, nrows=None, ncols=None):
return roi_objects


def auto_wells(gray_img, mindist, candec, accthresh, minradius, maxradius, nrows, ncols, radiusadjust=None):
"""Hough Circle Well Detection.
Keyword inputs:
gray_img = gray image (np.ndarray)
mindist = minimum distance between detected circles
candec = higher threshold of canny edge detector
accthresh = accumulator threshold for the circle centers
minradius = minimum circle radius
maxradius = maximum circle radius
nrows = expected number of rows
ncols = expected number of columns
radiusadjust = amount to adjust the average radius, this can be desirable
if you want ROI to sit inside a well, for example (in that case you might
set it to a negative value).
:param gray_img: np.ndarray
:param mindist: int
:param candec: int
:param accthresh: int
:param minradius: int
:param maxradius: int
:param nrows = int
:param ncols = int
:return roi: plantcv.plantcv.classes.Objects
"""
# Use hough circle helper function
maxfind = nrows * ncols
df, img = _hough_circle(gray_img, mindist, candec, accthresh, minradius,
maxradius, maxfind)

_debug(img, filename=os.path.join(params.debug_outdir, str(params.device) + '_roi_houghcircle.png'), cmap='gray')

xlist = []
centers_x = df['x'].values.reshape(-1, 1)
centers_y = df['y'].values.reshape(-1, 1)
gm_x = GaussianMixture(n_components=ncols, random_state=0).fit(centers_x)
gm_y = GaussianMixture(n_components=nrows, random_state=0).fit(centers_y)
clusters_x = gm_x.means_[:, 0]
clusters_y = gm_y.means_[:, 0]

sorted_indicesx = np.argsort(clusters_x)
sorted_x = np.sort(clusters_x)
xlist = list(range(len(clusters_x)))
clusterxdf = {'clusters_x': sorted_x, 'sorted': sorted_indicesx,
'xindex': [0]*len(clusters_x)}
xdf = pd.DataFrame(clusterxdf)
xdf = xdf.sort_values(by='clusters_x', ascending=True)
xdf['xindex'] = xlist

sorted_indicesy = np.argsort(clusters_y)
sorted_y = np.sort(clusters_y)
ylist = list(range(len(clusters_y)))
clusterydf = {'clusters_y': sorted_y, 'sorted': sorted_indicesy,
'yindex': [0]*len(clusters_y)}
ydf = pd.DataFrame(clusterydf)
ydf = ydf.sort_values(by='clusters_y', ascending=True)
ydf['yindex'] = ylist

df['column'] = gm_x.predict(centers_x)
df['row'] = gm_y.predict(centers_y)

df['row'] = df['row'].replace(list(ydf['sorted']), list(ydf['yindex']))
df['column'] = df['column'].replace(list(xdf['sorted']), list(xdf['xindex']))

df = df.sort_values(by=['row', 'column'], ascending=[True, True])
df['xy'] = list(zip(df.x, df.y))

radiusfinal = int(np.mean(list(df['radius'])))+radiusadjust

rois = multi(gray_img, list(df['xy']), radius=radiusfinal)

return rois


def custom(img, vertices):
"""Create an custom polygon ROI.
Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ def __init__(self):
self.cluster_names_too_many = os.path.join(self.datadir, "cluster_names_too_many.txt")
# Kmeans classifier directory
self.kmeans_classifier_dir = os.path.join(self.datadir, "kmeans_classifier_dir")
# Circle Wells for Hough Circle
self.hough_circle = os.path.join(self.datadir, "circle-wells.png")
# Kmeans classifier grayscale directory
self.kmeans_classifier_gray_dir = os.path.join(self.datadir, "kmeans_classifier_gray_dir")

Expand Down
9 changes: 8 additions & 1 deletion 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, custom, filter
from plantcv.plantcv.roi import from_binary_image, rectangle, circle, ellipse, auto_grid, multi, auto_wells, custom, filter


def test_from_binary_image(roi_test_data):
Expand Down Expand Up @@ -174,6 +174,13 @@ def test_multi(roi_test_data):
assert len(rois.hierarchy) == 4


def test_auto_wells(test_data):
"""Test for PlantCV."""
img = cv2.imread(test_data.hough_circle, -1)
rois = auto_wells(img, 20, 50, 30, 40, 50, 4, 6, -10)
assert len(rois.hierarchy) == 24


def test_multi_input_coords(roi_test_data):
"""Test for PlantCV."""
# Read in test RGB image
Expand Down
35 changes: 35 additions & 0 deletions tests/plantcv/test_helper_hough_circle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import cv2
import pytest
from plantcv.plantcv._helpers import _hough_circle


def test_hough_circle(test_data):
"""Test for PlantCV."""
img = cv2.imread(test_data.hough_circle, -1)
df, _ = _hough_circle(img, 20, 50, 30, 40, 50, 24)

assert df.shape == (24, 3)


def test_hough_circle_warn(test_data):
"""Test for PlantCV."""
img = cv2.imread(test_data.hough_circle, -1)
df, _ = _hough_circle(img, 20, 50, 30, 35, 50, 24)

assert df.shape == (24, 3)


def test_hough_circle_none(test_data):
"""Test for PlantCV."""
img = cv2.imread(test_data.hough_circle, -1)
df, _ = _hough_circle(img, 20, 50, 30, 40, 50, None)

assert df.shape == (24, 3)


def test_hough_no_circles(test_data):
"""Test for PlantCV."""
# Read in test data
img = cv2.imread(test_data.hough_circle, -1)
with pytest.raises(RuntimeError):
_, _ = _hough_circle(img, 20, 50, 30, 50, 50, None)
Binary file added tests/testdata/circle-wells.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 723f2c2

Please sign in to comment.