Skip to content

Commit ff26d6d

Browse files
authored
Merge pull request #1707 from danforthcenter/visualize_detect_color_card
Add `pcv.transform.mask_color_card` function
2 parents 89ea5ea + 5dde1f0 commit ff26d6d

9 files changed

Lines changed: 160 additions & 20 deletions

File tree

12.3 KB
Loading
86.2 KB
Loading
15.6 KB
Loading

docs/transform_mask_color_card.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
## Mask out Color Card
2+
3+
Automatically detects a color card and creates a bounding box mask.
4+
5+
**plantcv.transform.mask_color_card**(*rgb_img, \*\*kwargs*)
6+
7+
**returns** color_card_mask
8+
9+
- **Parameters**
10+
- rgb_img - Input RGB image data containing a color card.
11+
- **kwargs - Other keyword arguments passed to `cv2.adaptiveThreshold` and `cv2.circle`.
12+
- adaptive_method - Adaptive threhold method. 0 (mean) or 1 (Gaussian) (default = 1).
13+
- block_size - Size of a pixel neighborhood that is used to calculate a threshold value (default = 51). We suggest using 127 if using `adaptive_method=0`.
14+
- radius - Radius of circle to make the color card labeled mask (default = 20).
15+
- min_size - Minimum chip size for filtering objects after edge detection (default = 1000)
16+
- **Returns**
17+
- color_card_mask - Bounding box mask of all detected color card chips
18+
- **Context:**
19+
- This function can be used with [`pcv.image_subtract`](image_subtract.md) to clean up noise in a plant mask that was introduced by the color card. This is helpful in cases where the color card placement is not consistent enough for region filters to clean the color card chips from all images in a dataset.
20+
- **Example use:**
21+
- below
22+
23+
**RGB Image**
24+
25+
![Screenshot](img/documentation_images/transform_mask_color_card/seedhead-rgb.jpg)
26+
27+
**Seed head mask**
28+
29+
![Screenshot](img/documentation_images/transform_mask_color_card/seedhead-with-cc.png)
30+
31+
```python
32+
33+
from plantcv import plantcv as pcv
34+
35+
# Detect and mask the color card in the image
36+
cc_mask = pcv.transform.mask_color_card(rgb_img=img)
37+
38+
# Remove color card chips from the plant mask with image subtract
39+
cleaned_mask = pcv.image_subtract(gray_img1=plant_mask, gray_img2=cc_mask)
40+
41+
```
42+
43+
**Cleaned mask**
44+
45+
![Screenshot](img/documentation_images/transform_mask_color_card/seedhead-cleaned.png)
46+
47+
**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/main/plantcv/plantcv/transform/detect_color_card.py)

docs/updating.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,11 @@ pages for more details on the input and output variable types.
973973
* pre v4.2.1: NA
974974
* post v4.2.1: mtx, dist = **plantcv.transform.checkerboard_calib**(*img_path, col_corners, row_corners, out_dir*)
975975

976+
#### plantcv.transform.mask_color_card
977+
978+
* pre v4.8: NA
979+
* post v4.8: color_card_mask = **plantcv.transform.mask_color_card**(*rgb_img, \*\*kwargs*)
980+
976981
#### plantcv.transform.rotate
977982

978983
* post v3.12.0: rotated_img = **plantcv.transform.rotate**(*img, rotation_deg, crop*)
@@ -1223,7 +1228,7 @@ pages for more details on the input and output variable types.
12231228
#### plantcv.transform.auto_correct_color
12241229

12251230
* pre v4.6: NA
1226-
* post v4.6: corrected_img = **plantcv.transform.auto_correct_color**(*rgb_img, label=None, **kwargs*)
1231+
* post v4.6: corrected_img = **plantcv.transform.auto_correct_color**(*rgb_img, label=None, \*\*kwargs*)
12271232

12281233
#### plantcv.transform.correct_color
12291234

@@ -1238,7 +1243,7 @@ pages for more details on the input and output variable types.
12381243
#### plantcv.transform.detect_color_card
12391244

12401245
* pre v4.0.1: NA
1241-
* post v4.0.1: labeled_mask = **plantcv.transform.detect_color_card**(*rgb_img, label=None, **kwargs*)
1246+
* post v4.0.1: labeled_mask = **plantcv.transform.detect_color_card**(*rgb_img, label=None, \*\*kwargs*)
12421247

12431248
#### plantcv.transform.find_color_card
12441249

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ nav:
187187
- 'Affine Color Correction': transform_affine_color_correction.md
188188
- 'Standard Color Matrix': std_color_matrix.md
189189
- 'Gamma Correction': transform_gamma_correct.md
190+
- 'Mask Color Card': transform_mask_color_card.md
190191
- 'Merge Images': transform_merge_images.md
191192
- 'Nonuniform Illumination Correction': nonuniform_illumination.md
192193
- 'Perspective warp': transform_warp.md

plantcv/plantcv/transform/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@
2020
from plantcv.plantcv.transform.checkerboard_calib import checkerboard_calib, calibrate_camera
2121
from plantcv.plantcv.transform.merge_images import merge_images
2222
from plantcv.plantcv.transform.auto_correct_color import auto_correct_color
23+
from plantcv.plantcv.transform.detect_color_card import mask_color_card
2324

2425
__all__ = ["get_color_matrix", "get_matrix_m", "calc_transformation_matrix", "apply_transformation_matrix",
2526
"save_matrix", "load_matrix", "correct_color", "create_color_card_mask", "quick_color_check",
2627
"find_color_card", "std_color_matrix", "affine_color_correction", "rescale", "nonuniform_illumination", "resize",
2728
"resize_factor", "warp", "rotate", "warp", "warp_align", "gamma_correct", "detect_color_card", "checkerboard_calib",
28-
"calibrate_camera", "merge_images", "auto_correct_color"]
29+
"calibrate_camera", "merge_images", "auto_correct_color", "mask_color_card"]

plantcv/plantcv/transform/detect_color_card.py

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import numpy as np
99
from plantcv.plantcv import params, outputs, fatal_error, deprecation_warning
1010
from plantcv.plantcv._debug import _debug
11-
from plantcv.plantcv._helpers import _rgb2gray
11+
from plantcv.plantcv._helpers import _rgb2gray, _cv2_findcontours, _object_composition
1212

1313

1414
def _is_square(contour, min_size):
@@ -83,8 +83,8 @@ def _draw_color_chips(rgb_img, new_centers, radius):
8383
return labeled_mask, debug_img
8484

8585

86-
def detect_color_card(rgb_img, label=None, **kwargs):
87-
"""Automatically detect a color card.
86+
def _color_card_detection(rgb_img, **kwargs):
87+
"""Algorithm to automatically detect a color card.
8888
8989
Parameters
9090
----------
@@ -103,16 +103,9 @@ def detect_color_card(rgb_img, label=None, **kwargs):
103103
104104
Returns
105105
-------
106-
numpy.ndarray
107-
Labeled mask of chips.
106+
list
107+
Labeled mask of chips, debug img, detected chip areas, chip heights, chip widths, bounding box mask
108108
"""
109-
# Set lable to params.sample_label if None
110-
if label is None:
111-
label = params.sample_label
112-
deprecation_warning(
113-
"The 'label' parameter is no longer utilized, since color chip size is now metadata. "
114-
"It will be removed in PlantCV v5.0."
115-
)
116109
# Get keyword arguments and set defaults if not set
117110
min_size = kwargs.get("min_size", 1000) # Minimum size for _is_square chip filtering
118111
radius = kwargs.get("radius", 20) # Radius of circles to draw on the color chips
@@ -149,14 +142,15 @@ def detect_color_card(rgb_img, label=None, **kwargs):
149142
# Draw filtered contours on debug img
150143
debug_img = np.copy(rgb_img)
151144
cv2.drawContours(debug_img, filtered_contours, -1, color=(255, 50, 250), thickness=params.line_thickness)
145+
# Find the bounding box of the detected chips
146+
x, y, w, h = cv2.boundingRect(np.vstack(filtered_contours))
147+
148+
# Draw the bound box rectangle
149+
boundind_mask = cv2.rectangle(np.zeros(rgb_img.shape[0:2]), (x, y), (x + w, y + h), (255), -1).astype(np.uint8)
150+
152151
# Initialize chip shape lists
153152
marea, mwidth, mheight = _get_contour_sizes(filtered_contours)
154153

155-
# Create dataframe for easy summary stats
156-
chip_size = np.median(marea)
157-
chip_height = np.median(mheight)
158-
chip_width = np.median(mwidth)
159-
160154
# Concatenate all contours into one array and find the minimum area rectangle
161155
rect = np.concatenate([[np.array(cv2.minAreaRect(i)[0]).astype(int)] for i in filtered_contours])
162156
rect = cv2.minAreaRect(rect)
@@ -181,6 +175,84 @@ def detect_color_card(rgb_img, label=None, **kwargs):
181175
# Create labeled mask and debug image of color chips
182176
labeled_mask, debug_img = _draw_color_chips(debug_img, new_centers, radius)
183177

178+
return labeled_mask, debug_img, marea, mheight, mwidth, boundind_mask
179+
180+
181+
def mask_color_card(rgb_img, **kwargs):
182+
"""Automatically detect a color card and create bounding box mask of the chips detected.
183+
184+
Parameters
185+
----------
186+
rgb_img : numpy.ndarray
187+
Input RGB image data containing a color card.
188+
**kwargs
189+
Other keyword arguments passed to cv2.adaptiveThreshold and cv2.circle.
190+
191+
Valid keyword arguments:
192+
adaptive_method: 0 (mean) or 1 (Gaussian) (default = 1)
193+
block_size: int (default = 51)
194+
radius: int (default = 20)
195+
min_size: int (default = 1000)
196+
197+
Returns
198+
-------
199+
200+
numpy.ndarray
201+
Binary bounding box mask of the detected color card chips
202+
"""
203+
_, _, _, _, _, bounding_mask = _color_card_detection(rgb_img, **kwargs)
204+
205+
if params.debug is not None:
206+
# Find contours
207+
cnt, cnt_str = _cv2_findcontours(bin_img=bounding_mask)
208+
209+
# Consolidate contours
210+
obj = _object_composition(contours=cnt, hierarchy=cnt_str)
211+
bb_debug = cv2.drawContours(np.copy(rgb_img), [obj], -1, (255, 0, 255), params.line_thickness)
212+
213+
# Debug image handling
214+
_debug(visual=bb_debug, filename=os.path.join(params.debug_outdir, f'{params.device}_color_card.png'))
215+
216+
return bounding_mask
217+
218+
219+
def detect_color_card(rgb_img, label=None, **kwargs):
220+
"""Automatically detect a color card.
221+
222+
Parameters
223+
----------
224+
rgb_img : numpy.ndarray
225+
Input RGB image data containing a color card.
226+
label : str, optional
227+
modifies the variable name of observations recorded (default = pcv.params.sample_label).
228+
**kwargs
229+
Other keyword arguments passed to cv2.adaptiveThreshold and cv2.circle.
230+
231+
Valid keyword arguments:
232+
adaptive_method: 0 (mean) or 1 (Gaussian) (default = 1)
233+
block_size: int (default = 51)
234+
radius: int (default = 20)
235+
min_size: int (default = 1000)
236+
237+
Returns
238+
-------
239+
numpy.ndarray
240+
Labeled mask of chips.
241+
"""
242+
# Set lable to params.sample_label if None
243+
if label is None:
244+
label = params.sample_label
245+
deprecation_warning(
246+
"The 'label' parameter is no longer utilized, since color chip size is now metadata. "
247+
"It will be removed in PlantCV v5.0."
248+
)
249+
250+
labeled_mask, debug_img, marea, mheight, mwidth, _ = _color_card_detection(rgb_img, **kwargs)
251+
# Create dataframe for easy summary stats
252+
chip_size = np.median(marea)
253+
chip_height = np.median(mheight)
254+
chip_width = np.median(mwidth)
255+
184256
# Save out chip size for pixel to cm standardization
185257
outputs.add_metadata(term="median_color_chip_size", datatype=float, value=chip_size)
186258
outputs.add_metadata(term="median_color_chip_width", datatype=float, value=chip_width)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Tests for filters.color_card."""
2+
import cv2
3+
import numpy as np
4+
from plantcv.plantcv import params
5+
from plantcv.plantcv.transform.detect_color_card import mask_color_card
6+
7+
8+
def test_mask_color_card(transform_test_data):
9+
"""Test for PlantCV."""
10+
# Load rgb image
11+
params.debug = "plot"
12+
rgb_img = cv2.imread(transform_test_data.colorcard_img)
13+
cc_mask = mask_color_card(rgb_img=rgb_img)
14+
assert np.array_equal(np.unique(cc_mask), np.array([0, 255]))

0 commit comments

Comments
 (0)