Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
869d979
add helper function for color card detection alg
HaleySchuhl Apr 21, 2025
20035ef
add docstring
HaleySchuhl Apr 21, 2025
ca35688
create visualize.color_card_detection
HaleySchuhl Apr 21, 2025
6a075c5
add visualize.color_card_detection to init
HaleySchuhl Apr 21, 2025
595b1e2
draw the convex hull as bin mask
HaleySchuhl Apr 21, 2025
9d989c0
update color card logic to use bounding box
HaleySchuhl May 6, 2025
a335de2
move function from visualize.color_card_detection to filters.color_card
HaleySchuhl May 6, 2025
0e03e8b
update subpackage inits
HaleySchuhl May 6, 2025
0c17d77
update debug image to show outline of cc detection
HaleySchuhl May 6, 2025
2b218dd
add longest_path_bugfix
HaleySchuhl May 15, 2025
d8acc58
cast mask to uint8
HaleySchuhl May 19, 2025
83b4d4e
write unit test for filters.color_card
HaleySchuhl May 19, 2025
0acfc69
create filters_color_card.md and add example images
HaleySchuhl May 19, 2025
07a003f
Update mkdocs.yml
HaleySchuhl May 19, 2025
2c1eac9
Update updating.md
HaleySchuhl May 19, 2025
5d6b408
whitespace fixes
HaleySchuhl May 19, 2025
0dc0446
undo update to longest_path in size
HaleySchuhl May 19, 2025
e7744fe
add missing line return
HaleySchuhl May 19, 2025
e5b5d57
move filters.color_card into transform.mask_color_card
HaleySchuhl May 23, 2025
29f5d9d
move unit test
HaleySchuhl May 23, 2025
58f6186
rename docs and doc images
HaleySchuhl May 23, 2025
910a2b3
Merge branch 'main' into visualize_detect_color_card
HaleySchuhl May 23, 2025
2b8ae6c
update function name in updating.md
HaleySchuhl May 23, 2025
b07adb6
fix doc location in mkdocs
HaleySchuhl May 23, 2025
316fc8c
remove changes to filters/conftest.py
HaleySchuhl May 27, 2025
5e7c3c4
move mask_color_card to detect_color_card.py
HaleySchuhl May 27, 2025
9099b9e
update docs and tests
HaleySchuhl May 27, 2025
251f7df
update assertion to mask being binary 0 255
HaleySchuhl May 27, 2025
f1c3aad
fix order of returns in mask_color_card
HaleySchuhl May 27, 2025
28c221a
turn plot on for test coverage
HaleySchuhl May 27, 2025
0b272a3
Merge branch 'main' into visualize_detect_color_card
nfahlgren Jun 2, 2025
5dde1f0
Escape kwargs stars
nfahlgren Jun 2, 2025
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
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions docs/transform_mask_color_card.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## Mask out Color Card

Automatically detects a color card and creates a bounding box mask.

**plantcv.transform.mask_color_card**(*rgb_img, \*\*kwargs*)

**returns** color_card_mask

- **Parameters**
- rgb_img - Input RGB image data containing a color card.
- **kwargs - Other keyword arguments passed to `cv2.adaptiveThreshold` and `cv2.circle`.
- adaptive_method - Adaptive threhold method. 0 (mean) or 1 (Gaussian) (default = 1).
- 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`.
- radius - Radius of circle to make the color card labeled mask (default = 20).
- min_size - Minimum chip size for filtering objects after edge detection (default = 1000)
- **Returns**
- color_card_mask - Bounding box mask of all detected color card chips
- **Context:**
- 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.
- **Example use:**
- below

**RGB Image**

![Screenshot](img/documentation_images/transform_mask_color_card/seedhead-rgb.jpg)

**Seed head mask**

![Screenshot](img/documentation_images/transform_mask_color_card/seedhead-with-cc.png)

```python

from plantcv import plantcv as pcv

# Detect and mask the color card in the image
cc_mask = pcv.transform.mask_color_card(rgb_img=img)

# Remove color card chips from the plant mask with image subtract
cleaned_mask = pcv.image_subtract(gray_img1=plant_mask, gray_img2=cc_mask)

```

**Cleaned mask**

![Screenshot](img/documentation_images/transform_mask_color_card/seedhead-cleaned.png)

**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/main/plantcv/plantcv/transform/detect_color_card.py)
5 changes: 5 additions & 0 deletions docs/updating.md
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,11 @@ pages for more details on the input and output variable types.
* pre v4.2.1: NA
* post v4.2.1: mtx, dist = **plantcv.transform.checkerboard_calib**(*img_path, col_corners, row_corners, out_dir*)

#### plantcv.transform.mask_color_card

* pre v4.8: NA
* post v4.8: color_card_mask = **plantcv.transform.mask_color_card**(*rgb_img, **kwargs*)

#### plantcv.transform.rotate

* post v3.12.0: rotated_img = **plantcv.transform.rotate**(*img, rotation_deg, crop*)
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ nav:
- 'Affine Color Correction': transform_affine_color_correction.md
- 'Standard Color Matrix': std_color_matrix.md
- 'Gamma Correction': transform_gamma_correct.md
- 'Mask Color Card': transform_mask_color_card.md
- 'Merge Images': transform_merge_images.md
- 'Nonuniform Illumination Correction': nonuniform_illumination.md
- 'Perspective warp': transform_warp.md
Expand Down
3 changes: 2 additions & 1 deletion plantcv/plantcv/transform/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
from plantcv.plantcv.transform.checkerboard_calib import checkerboard_calib, calibrate_camera
from plantcv.plantcv.transform.merge_images import merge_images
from plantcv.plantcv.transform.auto_correct_color import auto_correct_color
from plantcv.plantcv.transform.detect_color_card import mask_color_card

__all__ = ["get_color_matrix", "get_matrix_m", "calc_transformation_matrix", "apply_transformation_matrix",
"save_matrix", "load_matrix", "correct_color", "create_color_card_mask", "quick_color_check",
"find_color_card", "std_color_matrix", "affine_color_correction", "rescale", "nonuniform_illumination", "resize",
"resize_factor", "warp", "rotate", "warp", "warp_align", "gamma_correct", "detect_color_card", "checkerboard_calib",
"calibrate_camera", "merge_images", "auto_correct_color"]
"calibrate_camera", "merge_images", "auto_correct_color", "mask_color_card"]
106 changes: 89 additions & 17 deletions plantcv/plantcv/transform/detect_color_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import numpy as np
from plantcv.plantcv import params, outputs, fatal_error, deprecation_warning
from plantcv.plantcv._debug import _debug
from plantcv.plantcv._helpers import _rgb2gray
from plantcv.plantcv._helpers import _rgb2gray, _cv2_findcontours, _object_composition


def _is_square(contour, min_size):
Expand Down Expand Up @@ -83,8 +83,8 @@ def _draw_color_chips(rgb_img, new_centers, radius):
return labeled_mask, debug_img


def detect_color_card(rgb_img, label=None, **kwargs):
"""Automatically detect a color card.
def _color_card_detection(rgb_img, **kwargs):
"""Algorithm to automatically detect a color card.

Parameters
----------
Expand All @@ -103,16 +103,9 @@ def detect_color_card(rgb_img, label=None, **kwargs):

Returns
-------
numpy.ndarray
Labeled mask of chips.
list
Labeled mask of chips, debug img, detected chip areas, chip heights, chip widths, bounding box mask
"""
# Set lable to params.sample_label if None
if label is None:
label = params.sample_label
deprecation_warning(
"The 'label' parameter is no longer utilized, since color chip size is now metadata. "
"It will be removed in PlantCV v5.0."
)
# Get keyword arguments and set defaults if not set
min_size = kwargs.get("min_size", 1000) # Minimum size for _is_square chip filtering
radius = kwargs.get("radius", 20) # Radius of circles to draw on the color chips
Expand Down Expand Up @@ -149,14 +142,15 @@ def detect_color_card(rgb_img, label=None, **kwargs):
# Draw filtered contours on debug img
debug_img = np.copy(rgb_img)
cv2.drawContours(debug_img, filtered_contours, -1, color=(255, 50, 250), thickness=params.line_thickness)
# Find the bounding box of the detected chips
x, y, w, h = cv2.boundingRect(np.vstack(filtered_contours))

# Draw the bound box rectangle
boundind_mask = cv2.rectangle(np.zeros(rgb_img.shape[0:2]), (x, y), (x + w, y + h), (255), -1).astype(np.uint8)

# Initialize chip shape lists
marea, mwidth, mheight = _get_contour_sizes(filtered_contours)

# Create dataframe for easy summary stats
chip_size = np.median(marea)
chip_height = np.median(mheight)
chip_width = np.median(mwidth)

# Concatenate all contours into one array and find the minimum area rectangle
rect = np.concatenate([[np.array(cv2.minAreaRect(i)[0]).astype(int)] for i in filtered_contours])
rect = cv2.minAreaRect(rect)
Expand All @@ -181,6 +175,84 @@ def detect_color_card(rgb_img, label=None, **kwargs):
# Create labeled mask and debug image of color chips
labeled_mask, debug_img = _draw_color_chips(debug_img, new_centers, radius)

return labeled_mask, debug_img, marea, mheight, mwidth, boundind_mask


def mask_color_card(rgb_img, **kwargs):
"""Automatically detect a color card and create bounding box mask of the chips detected.

Parameters
----------
rgb_img : numpy.ndarray
Input RGB image data containing a color card.
**kwargs
Other keyword arguments passed to cv2.adaptiveThreshold and cv2.circle.

Valid keyword arguments:
adaptive_method: 0 (mean) or 1 (Gaussian) (default = 1)
block_size: int (default = 51)
radius: int (default = 20)
min_size: int (default = 1000)

Returns
-------

numpy.ndarray
Binary bounding box mask of the detected color card chips
"""
_, _, _, _, _, bounding_mask = _color_card_detection(rgb_img, **kwargs)

if params.debug is not None:
# Find contours
cnt, cnt_str = _cv2_findcontours(bin_img=bounding_mask)

# Consolidate contours
obj = _object_composition(contours=cnt, hierarchy=cnt_str)
bb_debug = cv2.drawContours(np.copy(rgb_img), [obj], -1, (255, 0, 255), params.line_thickness)

# Debug image handling
_debug(visual=bb_debug, filename=os.path.join(params.debug_outdir, f'{params.device}_color_card.png'))

return bounding_mask


def detect_color_card(rgb_img, label=None, **kwargs):
"""Automatically detect a color card.

Parameters
----------
rgb_img : numpy.ndarray
Input RGB image data containing a color card.
label : str, optional
modifies the variable name of observations recorded (default = pcv.params.sample_label).
**kwargs
Other keyword arguments passed to cv2.adaptiveThreshold and cv2.circle.

Valid keyword arguments:
adaptive_method: 0 (mean) or 1 (Gaussian) (default = 1)
block_size: int (default = 51)
radius: int (default = 20)
min_size: int (default = 1000)

Returns
-------
numpy.ndarray
Labeled mask of chips.
"""
# Set lable to params.sample_label if None
if label is None:
label = params.sample_label
deprecation_warning(
"The 'label' parameter is no longer utilized, since color chip size is now metadata. "
"It will be removed in PlantCV v5.0."
)

labeled_mask, debug_img, marea, mheight, mwidth, _ = _color_card_detection(rgb_img, **kwargs)
# Create dataframe for easy summary stats
chip_size = np.median(marea)
chip_height = np.median(mheight)
chip_width = np.median(mwidth)

# Save out chip size for pixel to cm standardization
outputs.add_metadata(term="median_color_chip_size", datatype=float, value=chip_size)
outputs.add_metadata(term="median_color_chip_width", datatype=float, value=chip_width)
Expand Down
14 changes: 14 additions & 0 deletions tests/plantcv/transform/test_mask_color_card.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Tests for filters.color_card."""
import cv2
import numpy as np
from plantcv.plantcv import params
from plantcv.plantcv.transform.detect_color_card import mask_color_card


def test_mask_color_card(transform_test_data):
"""Test for PlantCV."""
# Load rgb image
params.debug = "plot"
rgb_img = cv2.imread(transform_test_data.colorcard_img)
cc_mask = mask_color_card(rgb_img=rgb_img)
assert np.array_equal(np.unique(cc_mask), np.array([0, 255]))