Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e0ced55
add size scaling context to analyze_size.md
HaleySchuhl Jun 26, 2025
69fa3ff
add size scaling ability in both docs pages
HaleySchuhl Jun 26, 2025
8c9c664
add _set_size_scale_from_chip
HaleySchuhl Jun 26, 2025
33d27a6
Update test_detect_color_card.py
HaleySchuhl Jun 26, 2025
d94b062
deepsource issues whitespace and indentation of docstring
HaleySchuhl Jun 26, 2025
c6849b5
add bad card_type input unit test
HaleySchuhl Jun 26, 2025
e7ccdff
add option to explicitly set color card dimensions
HaleySchuhl Jul 7, 2025
5a440ca
change param to color_chip_size, add tests
HaleySchuhl Jul 8, 2025
158e3ed
add example imgs of supported color cards
HaleySchuhl Jul 8, 2025
950469d
update documentation pages
HaleySchuhl Jul 8, 2025
b57da78
deepsource fixes and simplify unit setting
HaleySchuhl Jul 8, 2025
a58fd8b
remaining deepsource clean up, and make docstring more specific about…
HaleySchuhl Jul 8, 2025
1135078
remove redundant None default value
HaleySchuhl Jul 8, 2025
92bee7b
silly american typo, update to millimeters
HaleySchuhl Jul 8, 2025
80391e7
Simplify logic in _set_size_scale_from_chip
k034b363 Jul 9, 2025
ca282d9
Fix deepsource and check for length in color_chip_size
k034b363 Jul 9, 2025
178c9eb
Merge branch 'main' into add_color_card_size_scaling
HaleySchuhl Jul 14, 2025
ba5d3b5
handle cases where color card dimension not tuple
HaleySchuhl Jul 14, 2025
5fff857
make color_chip_size a positional argument
HaleySchuhl Jul 21, 2025
b4128aa
Merge branch 'main' into add_color_card_size_scaling
HaleySchuhl Jul 21, 2025
ce830b8
update analyze.size documentation for size scaling clarification
HaleySchuhl Jul 21, 2025
91dab3b
Update auto_correct_color.py
HaleySchuhl Jul 21, 2025
1a9b685
remove whitespace
HaleySchuhl Jul 21, 2025
554b8e5
Merge branch 'main' into add_color_card_size_scaling
nfahlgren Jul 29, 2025
179328c
Fix docstring indentation
nfahlgren Jul 29, 2025
f3139dd
Specify Macbeth ColorChecker
nfahlgren Jul 29, 2025
7748560
Move color_chip_size to main parameter
nfahlgren Jul 29, 2025
b3b8f53
Note Macbeth ColorChecker only
nfahlgren Jul 29, 2025
82988ed
Update colorchecker product names
nfahlgren Jul 29, 2025
1cf6a44
Remove x-rite card image
nfahlgren Jul 29, 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
7 changes: 4 additions & 3 deletions docs/analysis_approach.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ of different ways in PlantCV.
##### Image Normalization

* [White balancing](white_balance.md) an image can help to reduce variation between images due to overall lighting changes. This may help
downstream image processing steps like thresholding to be the same between images. Normalizing color across a dataset using a reference
color card with [color correction](https://plantcv.org/tutorials/color-correction) is also recommended, especially when color analysis is one
of the analysis objectives.
downstream image processing steps like thresholding to be the same between images.
* Normalizing color across a dataset using a reference
color card with [color correction](https://plantcv.org/tutorials/color-correction) is highly recommended, especially when color analysis is one
of the analysis objectives. See the [detect color card documentation](transform_detect_color_card.md) for more detail.

#####Object Segmentation Approaches

Expand Down
2 changes: 1 addition & 1 deletion docs/analyze_size.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Size and shape analysis outputs numeric properties for individual plants, seeds,
- **Output data stored:** Data ('area', 'convex_hull_area', 'solidity', 'perimeter', 'width', 'height', 'longest_path',
'center_of_mass, 'convex_hull_vertices', 'object_in_frame', 'ellipse_center', 'ellipse_major_axis', 'ellipse_minor_axis',
'ellipse_angle', 'ellipse_eccentricity') automatically gets stored to the [`Outputs` class](outputs.md) when this function is
run. These data can be accessed during a workflow (example below). For more detail about data output see
run. These data can be accessed during a workflow (example below). Length and area type measurements can be scaled to real world units (e.g. mm and mm<sup>2</sup> using the `unit`, `px_height`, and `px_width` [parameters](params.md). For more detail about data output see
[Summary of Output Observations](output_measurements.md#summary-of-output-observations)

**Original image**
Expand Down
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.
14 changes: 6 additions & 8 deletions docs/transform_auto_correct_color.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@ in the RGB space after automatic detection of a color card within the image. A o
[plantcv.transform.detect_color_card](transform_detect_color_card.md), [plantcv.transform.std_color_matrix](std_color_matrix.md),
[plantcv.transform.get_color_matrix](get_color_matrix.md), and [plantcv.transform.affine_color_correction](transform_affine_color_correction.md).

**plantcv.transform.auto_correct_color**(*rgb_img, label=None, \*\*kwargs*)
**plantcv.transform.auto_correct_color**(*rgb_img, label=None, color_chip_size=None, \*\*kwargs*)

**returns** corrected_img

- **Parameters**
- rgb_img - Input RGB image data containing a color card.
- label - Optional label parameter, modifies the variable name of observations recorded. (default = `pcv.params.sample_label`)
- color_chip_size - Type of color card to be detected, (case insensitive, either "classic", "passport", or "cameratrax", by default `None`). Or provide `(width, height)` of your specific color card in millimeters. If set then size scalings parameters `pcv.params.unit`, `pcv.params.px_width`, and `pcv.params.px_height` are automatically set, and utilized throughout linear and area type measurements stored to `Outputs`.
- **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)
- min_size - Minimum chip size for filtering objects after edge detection (default = 1000)
- **Returns**
- corrected_img - Color corrected image

Expand All @@ -29,14 +30,11 @@ from plantcv import plantcv as pcv

rgb_img, imgpath, imgname = pcv.readimage(filename="top_view_plant.png")

corrected_rgb = pcv.transform.auto_correct_color(rgb_img=old_card)
corrected_rgb = pcv.transform.auto_correct_color(rgb_img=rgb_img, color_chip_size="Passport")

# Scale length & area Outputs collected downstream
# by updating size scaling parameters
pcv.params.unit = "mm"
# Or set `color_chip_size` can be defined explicitly
# E.G. Given a square color card chips, (11mm x 11mm) in size
pcv.params.px_width = 11 / pcv.outputs.metadata['median_color_chip_width']['value'][0]
pcv.params.px_height = 11 / pcv.outputs.metadata['median_color_chip_height']['value'][0]
corrected_rgb = pcv.transform.auto_correct_color(rgb_img=rgb_img, color_chip_size=(11, 11))
```

**Debug Image: automatically detected and masked the color card**
Expand Down
37 changes: 32 additions & 5 deletions docs/transform_detect_color_card.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
## Automatically Detect a Color Card

Automatically detects a color card and creates a labeled mask.
Automatically detects a Macbeth ColorChecker style color card and creates a labeled mask.

**plantcv.transform.detect_color_card**(*rgb_img, label=None, \*\*kwargs*)
**plantcv.transform.detect_color_card**(*rgb_img, label=None, color_chip_size=None, \*\*kwargs*)

**returns** labeled_mask

- **Parameters**
- rgb_img - Input RGB image data containing a color card.
- label - Optional label parameter, modifies the variable name of observations recorded. (default = `pcv.params.sample_label`)
- color_chip_size - Type of color card to be detected, ("classic", "passport", or "cameratrax", by default `None`) or a tuple of the `(width, height)` dimensions of the color card chips in millimeters. If set then size scalings parameters `pcv.params.unit`, `pcv.params.px_width`, and `pcv.params.px_height`
are automatically set, and utilized throughout linear and area type measurements stored to `Outputs`.
- **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`.
Expand All @@ -18,7 +20,9 @@ Automatically detects a color card and creates a labeled mask.
- labeled_mask - Labeled color card mask (useful downstream of this step in [`pcv.transform.get_color_matrix`](get_color_matrix.md) and [`pcv.transform.correct_color`](transform_correct_color.md) and [`pcv.transform.affine_color_correction`](transform_affine_color_correction.md)).

- **Context**
- This mask output will be consistent in chip order regardless of orientation, where the white chip is detected and labeled first with index=0. In the case of `affine_color_correction` one will make a target color matrix.
- If the goal is to color correct the image colorspace to the standard color card values, consider using [`pcv.transform.auto_correct_color`](transform_auto_correct_color.md) since this new function is a one-step wrapper of plantcv.transform.detect_color_card, [plantcv.transform.std_color_matrix](std_color_matrix.md),
and [plantcv.transform.affine_color_correction](transform_affine_color_correction.md).
- This mask output will be consistent in chip order regardless of orientation, where the white chip is detected and labeled first with index=0.
- **Example use:**
- [Color Correction Tutorial](https://plantcv.org/tutorials/color-correction) since this function is called during [`pcv.transform.auto_correct_color`](transform_auto_correct_color.md).

Expand All @@ -28,13 +32,16 @@ Automatically detects a color card and creates a labeled mask.
There are a few important assumptions that must be met in order to automatically detect color cards:

- There is only one color card in the image.
- Color card should be 4x6 (like an X-Rite ColorChecker Passport Photo).
- Color card should be 4x6 [Macbeth ColorChecker](https://en.wikipedia.org/wiki/ColorChecker) like one of the supported color cards described below.

```python

from plantcv import plantcv as pcv
rgb_img, path, filename = pcv.readimage("target_img.png")
cc_mask = pcv.transform.detect_color_card(rgb_img=rgb_img)
# Using a supported color card size will automatically set size scaling parameters
cc_mask = pcv.transform.detect_color_card(rgb_img=rgb_img, color_chip_size="passport")
# Or if using another Macbeth ColorChecker you can explicitly set the color chip size (in millimeters)
cc_mask = pcv.transform.detect_color_card(rgb_img=rgb_img, color_chip_size=(12, 12))

avg_chip_size = pcv.outputs.metadata['median_color_chip_size']['value'][0]
avg_chip_w = pcv.outputs.metadata['median_color_chip_width']['value'][0]
Expand All @@ -53,4 +60,24 @@ corrected_img = pcv.transform.affine_color_correction(rgb_img=rgb_img,

![Screenshot](img/documentation_images/correct_color_imgs/detect_color_card.png)

### Suppored Color Cards

**[Calibrite ColorChecker Passport](https://calibrite.com/us/product/colorchecker-passport-photo-2/)**

![Screenshot](img/documentation_images/correct_color_imgs/calibrite-passport.png)

Chip dimensions: 12mm x 12mm

**[Calibrite ColorChecker Classic](https://calibrite.com/us/product/colorchecker-classic/)**

![Screenshot](img/documentation_images/correct_color_imgs/classic.png)

Chip dimensions: 40mm x 40mm

**[CameraTrax 24ColorCard-2x3](https://www.cameratrax.com/cardorder.php)**

![Screenshot](img/documentation_images/correct_color_imgs/camera-trax.png)

Chip dimensions: 11mm x 11mm

**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/main/plantcv/plantcv/transform/detect_color_card.py)
2 changes: 2 additions & 0 deletions docs/updating.md
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,7 @@ pages for more details on the input and output variable types.

* pre v4.6: NA
* post v4.6: corrected_img = **plantcv.transform.auto_correct_color**(*rgb_img, label=None, \*\*kwargs*)
* post v4.9: corrected_img = **plantcv.transform.auto_correct_color**(*rgb_img, label=None, color_chip_size=None, \*\*kwargs*)

#### plantcv.transform.correct_color

Expand All @@ -1244,6 +1245,7 @@ pages for more details on the input and output variable types.

* pre v4.0.1: NA
* post v4.0.1: labeled_mask = **plantcv.transform.detect_color_card**(*rgb_img, label=None, \*\*kwargs*)
* post v4.9: labeled_mask = **plantcv.transform.detect_color_card**(*rgb_img, label=None, color_chip_size=None, \*\*kwargs*)

#### plantcv.transform.find_color_card

Expand Down
9 changes: 7 additions & 2 deletions plantcv/plantcv/transform/auto_correct_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
from plantcv.plantcv.transform.color_correction import get_color_matrix, std_color_matrix, affine_color_correction


def auto_correct_color(rgb_img, label=None, **kwargs):
def auto_correct_color(rgb_img, label=None, color_chip_size=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).
color_chip_size: str, tuple, optional
"passport", "classic", "cameratrax"; or tuple formatted (width, height)
in millimeters (default = None)
**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)
Expand All @@ -33,7 +37,8 @@ def auto_correct_color(rgb_img, label=None, **kwargs):
"It will be removed in PlantCV v5.0."
)
# Get keyword arguments and set defaults if not set
labeled_mask = detect_color_card(rgb_img=rgb_img, min_size=kwargs.get("min_size", 1000),
labeled_mask = detect_color_card(rgb_img=rgb_img, color_chip_size=color_chip_size,
min_size=kwargs.get("min_size", 1000),
radius=kwargs.get("radius", 20),
adaptive_method=kwargs.get("adaptive_method", 1),
block_size=kwargs.get("block_size", 51))
Expand Down
65 changes: 62 additions & 3 deletions plantcv/plantcv/transform/detect_color_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,58 @@ def _color_card_detection(rgb_img, **kwargs):
return labeled_mask, debug_img, marea, mheight, mwidth, boundind_mask


def _set_size_scale_from_chip(color_chip_width, color_chip_height, color_chip_size):
"""Set the size scaling factors in Params from the known size of a given color card target.

Parameters
----------
color_chip_width : float
Width in pixels of the detected color chips
color_chip_height : float
Height in pixels of the detected color chips
color_chip_size : str, tuple
Type of supported color card target ("classic", "passport", or "cameratrax"), or a tuple of
(width, height) of the color card chip real-world dimensions in milimeters.
"""
# Define known color chip dimensions, all in milimeters
card_types = {
"CLASSIC": {
"chip_width": 40,
"chip_height": 40
},
"PASSPORT": {
"chip_width": 12,
"chip_height": 12
},
"CAMERATRAX": {
"chip_width": 11,
"chip_height": 11
}
}

# Check if user provided a valid color card type
if type(color_chip_size) is str and color_chip_size.upper() in card_types:
# Set size scaling parameters
params.px_width = card_types[color_chip_size.upper()]["chip_width"] / color_chip_width
params.px_height = card_types[color_chip_size.upper()]["chip_height"] / color_chip_height
# If not, check to make sure custom dimensions provided are numeric
else:
try:
# Set size scaling parameters
params.px_width = float(color_chip_size[0]) / color_chip_width
params.px_height = float(color_chip_size[1]) / color_chip_height
# Fail if provided color_chip_size is not supported
except ValueError:
fatal_error(f"Invalid input '{color_chip_size}'. Choose from {list(card_types.keys())}\
or provide your color card chip dimensions explicitly")
# Fail if provided color_chip_size is integer rather than tuple
except TypeError:
fatal_error(f"Invalid input '{color_chip_size}'. Choose from {list(card_types.keys())}\
or provide your color card chip dimensions explicitly as a tuple e.g. color_chip_size=(10,10).")
# If size scaling successful, set units to millimeters
params.unit = "mm"


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

Expand Down Expand Up @@ -216,15 +268,18 @@ def mask_color_card(rgb_img, **kwargs):
return bounding_mask


def detect_color_card(rgb_img, label=None, **kwargs):
"""Automatically detect a color card.
def detect_color_card(rgb_img, label=None, color_chip_size=None, **kwargs):
"""Automatically detect a Macbeth ColorChecker style 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).
color_chip_size: str, tuple, optional
"passport", "classic", "cameratrax"; or tuple formatted (width, height)
in millimeters (default = None)
**kwargs
Other keyword arguments passed to cv2.adaptiveThreshold and cv2.circle.

Expand Down Expand Up @@ -253,11 +308,15 @@ def detect_color_card(rgb_img, label=None, **kwargs):
chip_height = np.median(mheight)
chip_width = np.median(mwidth)

# Save out chip size for pixel to cm standardization
# Save out chip size for pixel to mm 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)
outputs.add_metadata(term="median_color_chip_height", datatype=float, value=chip_height)

# Set size scaling factor if card type is provided
if color_chip_size:
_set_size_scale_from_chip(color_chip_height=chip_height, color_chip_width=chip_width, color_chip_size=color_chip_size)

# Debugging
_debug(visual=debug_img, filename=os.path.join(params.debug_outdir, f'{params.device}_color_card.png'))

Expand Down
26 changes: 25 additions & 1 deletion tests/plantcv/transform/test_detect_color_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ def test_detect_color_card(transform_test_data):
"""Test for PlantCV."""
# Load rgb image
rgb_img = cv2.imread(transform_test_data.colorcard_img)
labeled_mask = detect_color_card(rgb_img=rgb_img)
labeled_mask = detect_color_card(rgb_img=rgb_img, color_chip_size="classic")
assert len(np.unique(labeled_mask)) == 25


def test_detect_color_card_set_size(transform_test_data):
"""Test for PlantCV."""
# Load rgb image
rgb_img = cv2.imread(transform_test_data.colorcard_img)
labeled_mask = detect_color_card(rgb_img=rgb_img, color_chip_size=(40, 40))
assert len(np.unique(labeled_mask)) == 25


Expand All @@ -27,3 +35,19 @@ def test_detect_color_card_incorrect_block_size(transform_test_data):
rgb_img = cv2.imread(transform_test_data.colorcard_img)
with pytest.raises(RuntimeError):
_ = detect_color_card(rgb_img=rgb_img, block_size=2)


def test_detect_color_card_incorrect_card_size(transform_test_data):
"""Test for PlantCV."""
# Load rgb image
rgb_img = cv2.imread(transform_test_data.colorcard_img)
with pytest.raises(RuntimeError):
_ = detect_color_card(rgb_img=rgb_img, color_chip_size=100)


def test_detect_color_card_incorrect_card_type(transform_test_data):
"""Test for PlantCV."""
# Load rgb image
rgb_img = cv2.imread(transform_test_data.colorcard_img)
with pytest.raises(RuntimeError):
_ = detect_color_card(rgb_img=rgb_img, color_chip_size="pantone")