From 12ab67387d4effa25b83051f52e5b1bc071078f8 Mon Sep 17 00:00:00 2001 From: Josh Sumner <51797700+joshqsumner@users.noreply.github.com> Date: Tue, 12 May 2026 14:34:12 -0500 Subject: [PATCH 1/2] move quick_color_check to qc submodule --- docs/quick_color_check.md | 10 +-- docs/updating.md | 6 ++ mkdocs.yml | 2 +- plantcv/plantcv/qc/__init__.py | 4 +- plantcv/plantcv/qc/quick_color_check.py | 77 +++++++++++++++++++ plantcv/plantcv/transform/__init__.py | 3 +- plantcv/plantcv/transform/color_correction.py | 67 ---------------- tests/plantcv/qc/conftest.py | 29 +++++++ tests/plantcv/qc/test_quick_color_check.py | 11 +++ .../transform/test_color_correction.py | 12 +-- 10 files changed, 134 insertions(+), 87 deletions(-) create mode 100644 plantcv/plantcv/qc/quick_color_check.py create mode 100644 tests/plantcv/qc/conftest.py create mode 100644 tests/plantcv/qc/test_quick_color_check.py diff --git a/docs/quick_color_check.md b/docs/quick_color_check.md index 8f29e1b70..e19ab5c39 100644 --- a/docs/quick_color_check.md +++ b/docs/quick_color_check.md @@ -5,7 +5,7 @@ be better off excluded from analysis . A quick way to examine this is by plottin masked in the color card. -**plantcv.transform.quick_color_check**(*source_matrix, target_matrix, num_chips*) +**plantcv.qc.quick_color_check**(*source_matrix, target_matrix, num_chips*) **returns** Altair chart @@ -23,9 +23,9 @@ masked in the color card. from plantcv import plantcv as pcv -chart = pcv.transform.quick_color_check(source_matrix=s_matrix, - target_matrix=t_matrix, - num_chips=24) +chart = pcv.qc.quick_color_check(source_matrix=s_matrix, + target_matrix=t_matrix, + num_chips=24) ``` **Perfect Color Correlation** @@ -36,4 +36,4 @@ chart = pcv.transform.quick_color_check(source_matrix=s_matrix, ![Screenshot](img/documentation_images/quick_color_check/quick_color_plot2.png) -**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/main/plantcv/plantcv/transform/color_correction.py) +**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/main/plantcv/plantcv/qc/quick_color_check.py) diff --git a/docs/updating.md b/docs/updating.md index 5a8ac75d8..dc814a89d 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -994,6 +994,11 @@ pages for more details on the input and output variable types. * pre v4.3.1: NA * post v4.3.1: chart = **plantcv.qc.exposure**(*rgb_img, warning_threshold=0.05*) +#### plantcv.qc.quick_color_check + +* pre v5.0: NA, see `plantcv.transform.quick_color_check` +* post v5.0: chart = **plantcv.transform.quick_color_check**(*target_matrix, source_matrix, num_chips*) + #### plantcv.readbayer * pre v3.0: NA @@ -1497,6 +1502,7 @@ pages for more details on the input and output variable types. * pre v3.0: NA * post v3.0: **plantcv.transform.quick_color_check**(*target_matrix, source_matrix, num_chips*) * post v4.0: chart = **plantcv.transform.quick_color_check**(*target_matrix, source_matrix, num_chips*) +* post v5.0: NA, moved to `plantcv.qc.quick_color_check` #### plantcv.transform.save_matrix diff --git a/mkdocs.yml b/mkdocs.yml index d41c85b24..62f8997dc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -145,6 +145,7 @@ nav: - 'Plot Image': plot_image.md - "Quality Control Tools": - "Exposure": qc_exposure.md + - "Quick Color Check": quick_color_check.md - 'Read Image': read_image.md - 'Read Bayer Raw Image': read_bayer.md - 'Report Size Marker': report_size_marker.md @@ -187,7 +188,6 @@ nav: - 'Create Color Card Mask': create_color_card_mask.md - 'Convert Color Card to Matrix': get_color_matrix.md - 'Color Correction Workflow': transform_correct_color.md - - 'Quick Color Check': quick_color_check.md - 'Affine Color Correction': transform_affine_color_correction.md - 'Standard Color Matrix': std_color_matrix.md - 'Gamma Correction': transform_gamma_correct.md diff --git a/plantcv/plantcv/qc/__init__.py b/plantcv/plantcv/qc/__init__.py index 730532c8f..9607a4cbd 100644 --- a/plantcv/plantcv/qc/__init__.py +++ b/plantcv/plantcv/qc/__init__.py @@ -1,3 +1,5 @@ from plantcv.plantcv.qc.exposure import exposure +from plantcv.plantcv.qc.quick_color_check import quick_color_check -__all__ = ["exposure"] + +__all__ = ["exposure", "quick_color_check"] diff --git a/plantcv/plantcv/qc/quick_color_check.py b/plantcv/plantcv/qc/quick_color_check.py new file mode 100644 index 000000000..afa027800 --- /dev/null +++ b/plantcv/plantcv/qc/quick_color_check.py @@ -0,0 +1,77 @@ +# Plot the color values of a target and source color matrix +import os +import numpy as np +import pandas as pd +import altair as alt +from plantcv.plantcv._debug import _debug +from plantcv.plantcv._globals import params + + +def quick_color_check(target_matrix, source_matrix, num_chips): + """Plot the color values of a target and source color card matrix. + + Quickly plot target matrix values against source matrix values to determine + over saturated color chips or other issues. + + Inputs: + source_matrix = an nrowsXncols matrix containing the avg red, green, and blue values for each color chip + of the source image + target_matrix = an nrowsXncols matrix containing the avg red, green, and blue values for each color chip + of the target image + num_chips = number of color card chips included in th # Test data directory + self.datadir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "testdata")e matrices (integer) + + Returns: + p1 = an altair plot of the target and source color values + + :param source_matrix: numpy.ndarray + :param target_matrix: numpy.ndarray + :param num_chips: int + :return p1: altair.vegalite.v5.api.Chart + """ + # Scale matrices to 0-255 + target_matrix = 255*target_matrix + source_matrix = 255*source_matrix + + # Extract and organize matrix info + tr = target_matrix[:num_chips, 1:2] + tg = target_matrix[:num_chips, 2:3] + tb = target_matrix[:num_chips, 3:4] + sr = source_matrix[:num_chips, 1:2] + sg = source_matrix[:num_chips, 2:3] + sb = source_matrix[:num_chips, 3:4] + + # Create columns of color labels + red = ["red"] * num_chips + blue = ["blue"] * num_chips + green = ["green"] * num_chips + + # Make a column of chip numbers + chip = np.arange(0, num_chips).reshape((num_chips, 1)) + chips = np.vstack((chip, chip, chip)) + + # Combine info + color_data_r = np.column_stack((sr, tr, red)) + color_data_g = np.column_stack((sg, tg, green)) + color_data_b = np.column_stack((sb, tb, blue)) + all_color_data = np.vstack((color_data_b, color_data_g, color_data_r)) + + # Create a dataframe with headers + dataset = pd.DataFrame({'source': all_color_data[:, 0], 'target': all_color_data[:, 1], + 'color': all_color_data[:, 2]}) + + # Add chip numbers to the dataframe + dataset['chip'] = chips + dataset = dataset.astype({'color': str, 'chip': str, 'target': float, 'source': float}) + + # Make the plot + p1 = alt.Chart(dataset).mark_point(point=True).encode( + x="target", + y="source", + color=alt.Color("color").scale(range=["blue", "green", "red"]), + column="color" + ) + + _debug(visual=p1, filename=os.path.join(params.debug_outdir, 'color_quick_check.png')) + + return p1.interactive() diff --git a/plantcv/plantcv/transform/__init__.py b/plantcv/plantcv/transform/__init__.py index 911c90f21..ebe01fa72 100644 --- a/plantcv/plantcv/transform/__init__.py +++ b/plantcv/plantcv/transform/__init__.py @@ -6,7 +6,6 @@ from plantcv.plantcv.transform.color_correction import load_matrix from plantcv.plantcv.transform.color_correction import correct_color from plantcv.plantcv.transform.color_correction import create_color_card_mask -from plantcv.plantcv.transform.color_correction import quick_color_check from plantcv.plantcv.transform.color_correction import std_color_matrix from plantcv.plantcv.transform.color_correction import astro_color_matrix from plantcv.plantcv.transform.color_correction import affine_color_correction @@ -24,7 +23,7 @@ from plantcv.plantcv.transform.auto_correct_color import auto_correct_color_nonlinear __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", + "save_matrix", "load_matrix", "correct_color", "create_color_card_mask", "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", "mask_color_card", "auto_correct_color_nonlinear", diff --git a/plantcv/plantcv/transform/color_correction.py b/plantcv/plantcv/transform/color_correction.py index 0e6ee9f57..9eaba5006 100644 --- a/plantcv/plantcv/transform/color_correction.py +++ b/plantcv/plantcv/transform/color_correction.py @@ -599,70 +599,3 @@ def create_color_card_mask(rgb_img, radius, start_coord, spacing, nrows, ncols, return mask -def quick_color_check(target_matrix, source_matrix, num_chips): - """Plot the color values of a target and source color card matrix. - - Quickly plot target matrix values against source matrix values to determine - over saturated color chips or other issues. - - Inputs: - source_matrix = an nrowsXncols matrix containing the avg red, green, and blue values for each color chip - of the source image - target_matrix = an nrowsXncols matrix containing the avg red, green, and blue values for each color chip - of the target image - num_chips = number of color card chips included in the matrices (integer) - - Returns: - p1 = an altair plot of the target and source color values - - :param source_matrix: numpy.ndarray - :param target_matrix: numpy.ndarray - :param num_chips: int - :return p1: altair.vegalite.v5.api.Chart - """ - # Scale matrices to 0-255 - target_matrix = 255*target_matrix - source_matrix = 255*source_matrix - - # Extract and organize matrix info - tr = target_matrix[:num_chips, 1:2] - tg = target_matrix[:num_chips, 2:3] - tb = target_matrix[:num_chips, 3:4] - sr = source_matrix[:num_chips, 1:2] - sg = source_matrix[:num_chips, 2:3] - sb = source_matrix[:num_chips, 3:4] - - # Create columns of color labels - red = ["red"] * num_chips - blue = ["blue"] * num_chips - green = ["green"] * num_chips - - # Make a column of chip numbers - chip = np.arange(0, num_chips).reshape((num_chips, 1)) - chips = np.vstack((chip, chip, chip)) - - # Combine info - color_data_r = np.column_stack((sr, tr, red)) - color_data_g = np.column_stack((sg, tg, green)) - color_data_b = np.column_stack((sb, tb, blue)) - all_color_data = np.vstack((color_data_b, color_data_g, color_data_r)) - - # Create a dataframe with headers - dataset = pd.DataFrame({'source': all_color_data[:, 0], 'target': all_color_data[:, 1], - 'color': all_color_data[:, 2]}) - - # Add chip numbers to the dataframe - dataset['chip'] = chips - dataset = dataset.astype({'color': str, 'chip': str, 'target': float, 'source': float}) - - # Make the plot - p1 = alt.Chart(dataset).mark_point(point=True).encode( - x="target", - y="source", - color=alt.Color("color").scale(range=["blue", "green", "red"]), - column="color" - ) - - _debug(visual=p1, filename=os.path.join(params.debug_outdir, 'color_quick_check.png')) - - return p1.interactive() diff --git a/tests/plantcv/qc/conftest.py b/tests/plantcv/qc/conftest.py new file mode 100644 index 000000000..156486a5b --- /dev/null +++ b/tests/plantcv/qc/conftest.py @@ -0,0 +1,29 @@ +import pytest +import os +import numpy as np +import matplotlib + +# Disable plotting +matplotlib.use("Template") + + +class QCTestData: + def __init__(self): + """Initialize simple variables.""" + # Test data directory + self.datadir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "testdata") + # Target matrix file + self.target_matrix_file = os.path.join(self.datadir, "target_matrix.npz") + # Source 1 matrix file + self.source1_matrix_file = os.path.join(self.datadir, "source1_matrix.npz") + + @staticmethod + def load_npz(npz_file): + """Load data saved in a NumPy .npz file.""" + data = np.load(npz_file, encoding="latin1") + return data['arr_0'] + +@pytest.fixture(scope="session") +def qc_test_data(): + """Test data object for the PlantCV transform submodule.""" + return QCTestData() diff --git a/tests/plantcv/qc/test_quick_color_check.py b/tests/plantcv/qc/test_quick_color_check.py new file mode 100644 index 000000000..ecf39038f --- /dev/null +++ b/tests/plantcv/qc/test_quick_color_check.py @@ -0,0 +1,11 @@ +from altair.vegalite.v5.api import Chart +from plantcv.plantcv.qc.quick_color_check import quick_color_check + + +def test_quick_color_check(qc_test_data): + """Test for PlantCV.""" + # Load target image + target_matrix = qc_test_data.load_npz(qc_test_data.target_matrix_file) + source_matrix = qc_test_data.load_npz(qc_test_data.source1_matrix_file) + chart = quick_color_check(target_matrix, source_matrix, num_chips=22) + assert isinstance(chart, Chart) diff --git a/tests/plantcv/transform/test_color_correction.py b/tests/plantcv/transform/test_color_correction.py index 8d2b371cc..636592136 100644 --- a/tests/plantcv/transform/test_color_correction.py +++ b/tests/plantcv/transform/test_color_correction.py @@ -3,10 +3,9 @@ import os import cv2 import numpy as np -from altair.vegalite.v5.api import Chart from plantcv.plantcv.transform.color_correction import (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, std_color_matrix, + create_color_card_mask, std_color_matrix, astro_color_matrix, affine_color_correction) from plantcv.plantcv.transform.detect_color_card import detect_color_card @@ -303,15 +302,6 @@ def test_create_color_card_mask(transform_test_data): 220], dtype=np.uint8))) -def test_quick_color_check(transform_test_data): - """Test for PlantCV.""" - # Load target image - target_matrix = transform_test_data.load_npz(transform_test_data.target_matrix_file) - source_matrix = transform_test_data.load_npz(transform_test_data.source1_matrix_file) - chart = quick_color_check(target_matrix, source_matrix, num_chips=22) - assert isinstance(chart, Chart) - - def test_cameratrax_and_astro_consistent_color_calibration(transform_test_data): """Test for PlantCV.""" # Load rgb image From cd3200f28d5955fecdba02f7e31c316647b19bb8 Mon Sep 17 00:00:00 2001 From: Josh Sumner <51797700+joshqsumner@users.noreply.github.com> Date: Tue, 12 May 2026 15:14:34 -0500 Subject: [PATCH 2/2] linting --- plantcv/plantcv/transform/color_correction.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plantcv/plantcv/transform/color_correction.py b/plantcv/plantcv/transform/color_correction.py index 9eaba5006..14897d192 100644 --- a/plantcv/plantcv/transform/color_correction.py +++ b/plantcv/plantcv/transform/color_correction.py @@ -3,8 +3,6 @@ import math import cv2 import numpy as np -import altair as alt -import pandas as pd from plantcv.plantcv import params, fatal_error from plantcv.plantcv.roi import circle from plantcv.plantcv._debug import _debug @@ -597,5 +595,3 @@ def create_color_card_mask(rgb_img, radius, start_coord, spacing, nrows, ncols, _debug(visual=canvas, filename=os.path.join(params.debug_outdir, str(params.device) + '_color_card_mask_rois.png')) _debug(visual=mask, filename=os.path.join(params.debug_outdir, str(params.device) + '_color_card_mask.png')) return mask - -