Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
279246e
adding new plot
joshqsumner Sep 8, 2025
10a5733
Merge branch 'color-correction-scatterplot' into color_chip_comparison
joshqsumner Sep 8, 2025
4c99f50
documentation
joshqsumner Sep 8, 2025
b92d000
export function
joshqsumner Sep 8, 2025
5e45a95
unnamed args
joshqsumner Sep 8, 2025
949ffe4
Merge branch 'color-correction-scatterplot' into color_chip_comparison
joshqsumner Sep 8, 2025
c08b12b
adding test
joshqsumner Sep 8, 2025
1efcec4
black linting
joshqsumner Sep 8, 2025
fe2354e
Merge branch 'main' into color_chip_comparison
joshqsumner Oct 20, 2025
e3c3c28
Merge branch 'main' into color_chip_comparison
joshqsumner May 7, 2026
e6bf2cb
Merge branch 'v5.0' into color_chip_comparison
joshqsumner May 13, 2026
1519fb3
Merge branch 'quick_color_check-to-qc' into color_chip_comparison
joshqsumner May 13, 2026
15bdcb9
move new visualization functions to qc
joshqsumner May 13, 2026
97f96af
whitespace
joshqsumner May 13, 2026
ebfe12c
add plot_deltaE function
joshqsumner May 13, 2026
2b5deb2
Merge branch 'deltaE' into delta_e_image_plot
joshqsumner May 13, 2026
2ba203d
docs for delta E plotting
joshqsumner May 13, 2026
563c2ad
tests for delta E plotting
joshqsumner May 13, 2026
b1a82ab
deepsource
joshqsumner May 13, 2026
eead4bc
fstring formatting
joshqsumner May 13, 2026
1d07c86
Merge branch 'quick_color_check-to-qc' into color_chip_comparison
joshqsumner May 27, 2026
8cc79c1
Merge branch 'color_chip_comparison' into delta_e_image_plot
joshqsumner May 27, 2026
edecbdf
Merge branch 'v5.0' into delta_e_image_plot
joshqsumner May 28, 2026
e901e2b
Merge branch 'v5.0' into color_chip_comparison
joshqsumner May 28, 2026
875d68f
Merge branch 'quick_color_check-to-qc' into color_chip_comparison
joshqsumner May 29, 2026
774e65c
Merge branch 'color_chip_comparison' into delta_e_image_plot
joshqsumner May 29, 2026
bed8a5e
Merge branch 'v5.0' into delta_e_image_plot
nfahlgren Jun 15, 2026
65edd74
Apply suggestions from code review
nfahlgren Jun 17, 2026
a943859
Remove function import
nfahlgren Jun 17, 2026
62194ce
Fix indentation
nfahlgren Jun 17, 2026
c764d41
Remove extra whitespace
nfahlgren Jun 17, 2026
044a10e
Add new functions to updating
nfahlgren Jun 17, 2026
087be45
updating docs images
joshqsumner Jun 17, 2026
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.
45 changes: 45 additions & 0 deletions docs/qc_color_chip_comparison.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## Color Chip Comparison

This function makes a plot comparing observed versus expected values from 1 or more color cards against a standard color card matrix via a "greenness rank". The greenness rank is useful in checking color card quality. The ninth (red) color chip is known to fade most quickly and the proportion of green light that it reflects can vary dramatically as the color card ages. The color of each bar is determined by the standard color matrix on the left side of the bar and by the observed color matrix on the right side of the bar. The order along the x axis is conserved from the order of `*args`.

**plantcv.qc.color_chip_comparison**(*std_matrix, \*args*)

**returns** plot, a altair.vegalite.v5.api.VConcatChart object

- **Parameters:**
- std_matrix - A numpy.ndarray as returned from [`pcv.transform.std_color_matrix`](std_color_matrix.md).
- \*args - Any number of numpy.ndarrays as returned from [`pcv.transform.get_color_matrix`](get_color_matrix.md)

- **Context:**
- The aim of this visualization is to help evaluate the condition of a color card or set of color cards.


- **Example use:**
- Below

**Dataset images:**

![Screenshot](img/documentation_images/qc_color_chip_comparison/input.png)

```python

from plantcv import plantcv as pcv

tgt_matrix = pcv.transform.std_color_matrix(pos=3)
_, cc1_matrix = pcv.transform.get_color_matrix(rgb_img=img, mask=cc_mask)
# ... masking more color cards for example
_, cc6_matrix = pcv.transform.get_color_matrix(rgb_img=img, mask=cc_mask6)

plot = pcv.qc.color_chip_comparison(tgt_matrix, cc1_matrix,
cc2_matrix, cc3_matrix,
cc4_matrix, cc5_matrix,
cc6_matrix)

```

**Color chip comparison visualizations:**

![Screenshot](img/documentation_images/qc_color_chip_comparison/output.png)


**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/master/plantcv/plantcv/qc/color_chip_comparison.py)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This function plots 4 panels of 2D scatter plot visualizations showing RGB and grayscale values of an input image, the expected color card, and optionally a color corrected image. The horizontal and vertical coordinates are defined by the intensity of the pixels in the specified channels. The color of each dot is given by the original RGB color of the image, ideal color card, or corrected image.

**plantcv.visualize.color_correction_plot**(*color_matrix, std_matrix, corrected_matrix=None*)
**plantcv.qc.color_correction_plot**(*color_matrix, std_matrix, corrected_matrix=None*)

**returns** fig, axs

Expand All @@ -20,19 +20,19 @@ This function plots 4 panels of 2D scatter plot visualizations showing RGB and g

**Dataset images:**

![Screenshot](img/documentation_images/visualize_color_correction_scatter/am003_sv_input.png)
![Screenshot](img/documentation_images/qc_color_correction_scatter/am003_sv_input.png)

```python

from plantcv import plantcv as pcv

fig, axs = pcv.visualize.color_correction_plot(colmat, stdmat, ccmat)
fig, axs = pcv.qc.color_correction_plot(colmat, stdmat, ccmat)

```

**Color correction scatter visualizations:**

![Screenshot](img/documentation_images/visualize_color_correction_scatter/am003_sv_ex.png)
![Screenshot](img/documentation_images/qc_color_correction_scatter/am003_sv_ex.png)


**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/master/plantcv/plantcv/visualize/color_correction_scatter.py)
**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/master/plantcv/plantcv/qc/color_correction_scatter.py)
41 changes: 41 additions & 0 deletions docs/qc_plot_delta_e.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## Plot Delta E

This function creates an interactive bar chart visualizing per-chip Delta E (color difference) values from a color checker card. Each bar is colored by a standard interpretation category: green (<1) indicates imperceptible differences, progressing through yellow (<2, <10) to orange and red (<49, >49) for increasingly noticeable differences. Reference lines at the category boundaries are drawn for quick visual assessment. Standard chip color swatches from the target color matrix are displayed below the x-axis to aid chip identification.

**plantcv.qc.plot_deltaE**(*deltaE_matrix*)

**returns** chart, an altair.vegalite.v5.api.LayerChart object

- **Parameters:**
- deltaE_matrix - numpy.ndarray of per-chip Delta E values, shaped to match the color card layout (e.g., (6, 4) for a 24-chip Macbeth card or (3, 5) for a 15-chip AstroBotany card), as returned from [`pcv.transform.deltaE`](https://github.com/danforthcenter/plantcv/blob/main/plantcv/plantcv/transform/detect_color_card.py).

- **Context:**
- Used to evaluate the quality of color calibration by visualizing how closely observed chip colors match their expected values. Lower Delta E values indicate better color fidelity. This function is best used during workflow development to interactively inspect calibration results before and after color correction or between different color correction methods.

- **Example use:**
- Below

**Dataset image:**

![Screenshot](img/documentation_images/qc_plot_delta_e/input.png)

```python

from plantcv import plantcv as pcv
from plantcv.plantcv.transform.detect_color_card import deltaE

# Calculate Delta E values for each chip relative to the standard color matrix
# Note this is
de_matrix = deltaE(rgb_img=img, color_chip_size="classic")
Comment thread
nfahlgren marked this conversation as resolved.
Outdated

# Plot the Delta E values
chart = pcv.qc.plot_deltaE(deltaE_matrix=de_matrix)

```

**Delta E bar chart:**

![Screenshot](img/documentation_images/qc_plot_delta_e/uncalibrated_output.png)


**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/main/plantcv/plantcv/qc/plot_delta_e.py)
5 changes: 5 additions & 0 deletions docs/updating.md
Original file line number Diff line number Diff line change
Expand Up @@ -1537,6 +1537,11 @@ pages for more details on the input and output variable types.
* pre v4.0: NA
* post v4.0: chart = **plantcv.visualize.chlorophyll_fluorescence**(*ps_da, labeled_mask, n_labels=1, label="object"*)

#### plantcv.visualize.color_chip_comparison

* pre v4.10: NA
* post v4.10: plot = plantcv.visualize.color_chip_comparison**(*std_matrix, \*args*)
Comment thread
nfahlgren marked this conversation as resolved.
Outdated

#### plantcv.visualize.color_correction_scatter

* pre v4.11: NA
Expand Down
3 changes: 2 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ nav:
- 'Print Image': print_image.md
- 'Plot Image': plot_image.md
- "Quality Control Tools":
- 'Color Correction Scatter Plot': qc_color_correction_scatter.md
- 'Color Chip Comparison Plot': qc_color_chip_comparison.md
Comment thread
nfahlgren marked this conversation as resolved.
- "Exposure": qc_exposure.md
- "Quick Color Check": quick_color_check.md
- 'Read Image': read_image.md
Comment thread
nfahlgren marked this conversation as resolved.
Expand Down Expand Up @@ -211,7 +213,6 @@ nav:
- 'Visualization Methods':
- 'Auto Threshold Methods': visualize_auto_threshold_methods.md
- 'Chlorophyll Fluorescence': visualize_chlorophyll_fluorescence.md
- 'Color Correction Scatter Plot': visualize_color_correction_scatter.md
- 'Colorize Label Image': visualize_colorize_label_img.md
- 'Colorize Masks': visualize_colorize_masks.md
- 'Colorspaces': visualize_colorspace.md
Expand Down
6 changes: 5 additions & 1 deletion plantcv/plantcv/qc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from plantcv.plantcv.qc.exposure import exposure
from plantcv.plantcv.qc.quick_color_check import quick_color_check
from plantcv.plantcv.qc.color_correction_scatter import color_correction_plot
from plantcv.plantcv.qc.color_chip_comparison import color_chip_comparison
from plantcv.plantcv.qc.plot_delta_e import plot_deltaE


__all__ = ["exposure", "quick_color_check"]
__all__ = ["exposure", "quick_color_check", "color_correction_plot", "color_chip_comparison",
"plot_deltaE"]
152 changes: 152 additions & 0 deletions plantcv/plantcv/qc/color_chip_comparison.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Visualize a scatter plot representation of color correction

import pandas as pd
import altair as alt


def color_chip_comparison(std_matrix, *args):
"""
Plot 4 panels showing the difference in observed vs expected colors and optionally
the calibrated colors in a color card.
The color of each dot is given by the RGB value either of the original image, known color card, or corrected image.

Comment thread
nfahlgren marked this conversation as resolved.
Outdated
Parameters
----------
std_matrix : numpy.ndarray
Output from pcv.transform.std_color_matrix
*args: list of numpy.ndarrays
Output from pcv.transform.get_color_matrix

Returns
-------
altair.vegalite.v5.api.VConcatChart of color chip greenness ranks between observed and expected values.
"""
# make standard color matrix into a rescaled dataframe
stddf = pd.DataFrame(std_matrix)
stddf.columns = ["chip", "R", "G", "B"]
stddf["card"] = "std"
stddf["std_R"] = stddf["R"] * 255
stddf["std_G"] = stddf["G"] * 255
stddf["std_B"] = stddf["B"] * 255
# initialize a list of like dataframes
df_list = [stddf]
# format and append all kwargs into list of dataframes
for i, mat in enumerate(args):
df = pd.DataFrame(mat)
df.columns = ["chip", "R", "G", "B"]
df["card"] = f"card {i + 1}"
df["std_R"] = stddf["std_R"]
df["std_G"] = stddf["std_G"]
df["std_B"] = stddf["std_B"]
df_list.append(df)
# rbind all dataframes from list
fulldf = pd.concat(*[df_list], ignore_index=True)
Comment thread
nfahlgren marked this conversation as resolved.
Outdated
# rescale rgb values to 0-255
fulldf["R"] = fulldf["R"] * 255
fulldf["G"] = fulldf["G"] * 255
fulldf["B"] = fulldf["B"] * 255
# calculate greenness, in the future maybe a named metric.
fulldf["greenness"] = fulldf["G"] / (fulldf["R"] + fulldf["G"] + fulldf["B"])
# rank greenness, chips should have same order in any "healthy" card
fulldf["greenness_rank"] = fulldf.groupby("card")["greenness"].rank(
method="first", ascending=False
)
# make standard greenness and rank it
fulldf["std_greenness"] = fulldf["std_G"] / (
fulldf["std_R"] + fulldf["std_G"] + fulldf["std_B"]
)
fulldf["std_greenness_rank"] = fulldf.groupby("card")["std_greenness"].rank(
method="first", ascending=False
)
# label chips 1 to 24
fulldf["chip"] = fulldf["chip"] / 10
Comment thread
nfahlgren marked this conversation as resolved.
Outdated
# initiate base of upper color chip chart
base = (
alt.Chart(fulldf)
.encode(
alt.X(
"card:O",
axis=alt.Axis(
grid=False, ticks=False, domain=False, labels=False, title=None
),
).scale(paddingInner=0),
alt.Y("greenness_rank:O", title="Greenness Rank").scale(paddingInner=0),
)
.properties(height=300, width=500)
)
# make rect layer of observed colors
tiles1 = base.mark_rect(width=alt.RelativeBandSize(0.6), align="left").encode(
color=alt.value(
alt.ExprRef(alt.expr.rgb(alt.datum.R, alt.datum.G, alt.datum.B))
),
)
# make rect layer of standard colors
tiles2 = base.mark_rect(width=alt.RelativeBandSize(0.3), align="right").encode(
color=alt.value(
alt.ExprRef(alt.expr.rgb(alt.datum.std_R, alt.datum.std_G, alt.datum.std_B))
),
)
# make text layer to label chip numbers
text = base.mark_text(baseline="middle", align="center").encode(
text="chip:Q", color=alt.value("white")
)
# combine rect and text layers
upper = tiles1 + tiles2 + text
# initialize list of margin plots
margin_plots = []
# for each kwarg matrix and std matrix make a margin plot of residual ranks
for i in range(0, len(args) + 1):
# select card
whichcard = f"card {i + 1}"
if i + 1 > len(args):
whichcard = "std"
sub1 = fulldf[fulldf["card"] == whichcard]
# initialize plot
subbase = (
alt.Chart(sub1)
.encode(alt.X("std_greenness_rank:Q"), alt.Y("std_greenness_rank:Q"))
.properties(
height=500 / (10 / 9 * len(args) + 1),
width=500 / (10 / 9 * len(args) + 1),
title=whichcard,
)
)
# make line+points layer of observed vs expected ranks
subpoints = subbase.mark_line(
point=True, strokeWidth=1.25, strokeDash=[5, 5]
).encode(
x=alt.X(
"greenness_rank:Q",
axis=alt.Axis(
grid=False, ticks=False, domain=False, labels=False, title=None
),
),
y=alt.Y(
"std_greenness_rank:Q",
axis=alt.Axis(
grid=False, ticks=False, domain=False, labels=False, title=None
),
),
)
# draw expected line with slope 1
sublinear = subbase.mark_line(color="black", strokeWidth=0.5).encode(
x=alt.X(
"std_greenness_rank:Q",
axis=alt.Axis(
grid=False, ticks=False, domain=False, labels=False, title=None
),
),
y=alt.Y(
"std_greenness_rank:Q",
axis=alt.Axis(
grid=False, ticks=False, domain=False, labels=False, title=None
),
),
)
# combine layers
iterchart = subpoints + sublinear
# add to list of margin plots for combination
margin_plots.append(iterchart)
# combine tile plot and margin plots
out = alt.vconcat(upper, alt.hconcat(*margin_plots, spacing=5))
return out
Loading