-
Notifications
You must be signed in to change notification settings - Fork 289
Delta E #1905
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
+235
−10
Merged
Delta E #1905
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
4f12826
make delta e helper function
joshqsumner 995191b
use delta e helper in detection and in standalone function
joshqsumner 94dba00
docs update
joshqsumner 837fc5c
export deltaE function
joshqsumner e285234
test delta E function
joshqsumner a2820ed
preemptive linting
joshqsumner 6a90639
deepsource
joshqsumner 5263229
Merge branch 'v5.0' into deltaE
joshqsumner 6550df3
Merge branch 'v5.0' into deltaE
joshqsumner bd97cdd
Merge branch 'v5.0' into deltaE
joshqsumner e67e29c
Merge branch 'v5.0' into deltaE
nfahlgren c92d121
typo
joshqsumner 7f3f992
close figures
joshqsumner d5ea004
typo
joshqsumner 99811e6
docs namespace in example
joshqsumner f5da0bb
Merge branch 'v5.0' into deltaE
joshqsumner 2c0f6bb
copilot suggestions
joshqsumner c7bde2a
not using cv2
joshqsumner 8a6996b
Merge branch 'v5.0' into deltaE
nfahlgren File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| ## Calculate Delta E between observed and expected color cards | ||
|
|
||
| Calculates Delta E between a Macbeth ColorChecker or Astrobotany.com Calibration sticker style color card and the expected color values. | ||
|
|
||
| **plantcv.transform.deltaE**(*rgb_img, color_chip_size=None, roi=None, obs="calibrated", method="deltaE_ciede2000", \*\*kwargs*) | ||
|
|
||
| **returns** Delta E matrix | ||
|
|
||
| - **Parameters** | ||
| - rgb_img - Input RGB image data containing a color card. | ||
| - color_chip_size - Type of color card to be detected, ("classic", "passport", "nano", "mini", "cameratrax", or "astro", 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`. | ||
| - roi - Optional rectangular ROI as returned by [`pcv.roi.rectangle`](roi_rectangle.md) within which to look for the color card. (default = None) | ||
| - obs - label for metadata and debug images, typically "calibrated" or "uncalibrated" depending on whether the `rgb_img` has been color corrected. | ||
| - method - Function name from `skimage.color` as a string to use to calculate delta E. Currently `deltaE_cie76`, `deltaE_ciede2000`, `deltaE_ciede94`, and `deltaE_cmc` are supported | ||
| - **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) | ||
| - aspect_ratio - Optional aspect ratio (width / height) below which objects will get removed. Orientation agnostic since automatically set to the reciprocal if <1 (default = 1.27) | ||
| - solidity - Optional solidity (object area / convex hull area) filter (default = 0.8) | ||
|
|
||
| - **Returns** | ||
| - deltaE - Delta E values per each color chip as a matrix. | ||
|
|
||
| - **Context** | ||
| - Delta E is a perception-based metric for the difference between colors. 0 indicates no perceptual difference and higher values indicate more difference. Generally a delta E value less than 1 is imperceptibly different and values greater than 3.5 are clearly distinct. Whether a particular color chip's delta E value matters for your experimental goals depends on your hypothesis and analysis plan. | ||
|
|
||
|
|
||
| !!! note | ||
| Delta E is calculated when a color card is detected with `plantcv.transform.detect_color_card` by default | ||
| and a debug image is generated showing the differences in the color card against the expected colors. | ||
| This function will use `plantcv.transform.detect_color_card` to find the color card, potentially in an image that has already | ||
| been color-corrected so that the delta E values can be compared pre vs post calibration. | ||
|
|
||
| ```python | ||
| from plantcv import plantcv as pcv | ||
| rgb_img, path, filename = pcv.readimage("target_img.png") | ||
|
|
||
| pcv.params.debug = "plot" | ||
| # Delta E debug visualization shown below | ||
| cc_matrix = pcv.transform.detect_color_card(rgb_img=rgb_img) | ||
|
|
||
| # Next we may color correct the image | ||
| tgt_matrix = pcv.transform.std_color_matrix(pos=3) | ||
| corrected_img = pcv.transform.affine_color_correction(rgb_img=rgb_img, | ||
| source_matrix=cc_matrix, | ||
| target_matrix=tgt_matrix) | ||
|
|
||
| # Delta E on the corrected image | ||
| e_matrix = deltaE(corrected_img) | ||
| # outputs metadata will have min, mean, max, std dev of deltaE | ||
| print(pcv.outputs.metadata["max_deltaE_uncalibrated"]) # the uncalibrated metadata is added by detect_color_card | ||
| # [np.float64(49.5516548320252)] | ||
| print(pcv.outputs.metadata["max_deltaE_calibrated"]) # the calibrated metadata is added by deltaE | ||
| # [np.float64(15.730570870191682)] | ||
| ``` | ||
|
|
||
| Calling `plantcv.transform.detect_color_card` on the uncorrected image: | ||
|
|
||
|  | ||
|
|
||
| Using `plantcv.transform.deltaE` on the corrected image to check the new Delta E values: | ||
|
|
||
|  | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| """Calculate Delta E between color cards""" | ||
| import os | ||
| import cv2 | ||
| import numpy as np | ||
| from skimage import color | ||
| from matplotlib import pyplot as plt | ||
| from plantcv.plantcv._globals import params, outputs | ||
| from plantcv.plantcv.transform.color_correction import std_color_matrix, astro_color_matrix | ||
|
|
||
|
|
||
| def _delta_e(obs_rgb, card_type=None, obs="uncalibrated", method="deltaE_ciede2000"): | ||
| """Calculate summary of Delta E between two color cards | ||
|
|
||
| Parameters | ||
| ---------- | ||
| obs_rgb : numpy.ndarray | ||
| Observed RGB color chip values as returned from plantcv.transform.detect_color_card | ||
| card_type : str | ||
| either "macbeth" or "astro" for color card type, defaults to None for compatibility with detection. | ||
| obs : str | ||
| string describing what the obs_rgb data is, typically "uncalibrated" for an image input into color correction | ||
| or "calibrated" for an image that has been through color correction. | ||
| method : str | ||
| function name from skimage.color to calculate delta E. Currently deltaE_(cie76|ciede2000|ciede94|cmc) are | ||
| supported | ||
|
|
||
| Returns | ||
| ------- | ||
| delta_e_mat | ||
| numpy.ndarray, Delta E values between color chips. | ||
| """ | ||
| if card_type is None or isinstance(card_type, tuple): | ||
| card_type = "macbeth" | ||
| if card_type.upper() == "ASTRO": | ||
| std = astro_color_matrix() | ||
| obs_mat = (255 * np.delete(obs_rgb, 0, axis=1).reshape(3, 5, 3)).astype("uint8") | ||
| exp_mat = (255 * np.delete(std, 0, axis=1).reshape(3, 5, 3)).astype("uint8") | ||
| else: | ||
| std = std_color_matrix() | ||
| # format both rgb colors into 6x4 uint8 image | ||
| obs_mat = (255 * np.delete(obs_rgb, 0, axis=1).reshape(6, 4, 3)).astype("uint8") | ||
| exp_mat = (255 * np.rot90(np.delete(std, 0, axis=1).reshape(4, 6, 3), 3)).astype("uint8") | ||
| # convert to LAB for skimage color functions | ||
| obs_lab = cv2.cvtColor(obs_mat, cv2.COLOR_RGB2LAB) | ||
| exp_lab = cv2.cvtColor(exp_mat, cv2.COLOR_RGB2LAB) | ||
| # get function from skimage color | ||
| delta_e_fun = getattr(color, method) | ||
| # there are other parameters we could allow changes to but I don't think we need to yet. | ||
| delta_e_mat = delta_e_fun(obs_lab, exp_lab) | ||
|
joshqsumner marked this conversation as resolved.
Outdated
|
||
| # store metadata describing delta E | ||
| outputs.add_metadata(term="mean_deltaE_" + obs, datatype=float, value=np.mean(delta_e_mat)) | ||
| outputs.add_metadata(term="std_deltaE_" + obs, datatype=float, value=np.std(delta_e_mat)) | ||
| outputs.add_metadata(term="max_deltaE_" + obs, datatype=float, value=np.max(delta_e_mat)) | ||
| outputs.add_metadata(term="min_deltaE_" + obs, datatype=float, value=np.min(delta_e_mat)) | ||
| # make a debug plot | ||
| if params.debug: | ||
| params.device += 1 | ||
| _, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) | ||
| ax1.imshow(obs_mat) | ||
| ax1.set_title(obs.title() + ' Color Card') | ||
| ax2.imshow(exp_mat) | ||
| ax2.set_title('Reference Colors') | ||
| if params.debug == "print": | ||
| plt.savefig(fname=os.path.join(params.debug_outdir, f"{params.device}_{obs}_{method}.png")) | ||
| if params.debug == "plot": | ||
| plt.show() | ||
|
joshqsumner marked this conversation as resolved.
Outdated
|
||
|
|
||
| return delta_e_mat | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| """Tests for deltaE.""" | ||
| import os | ||
| import cv2 | ||
| import pytest | ||
| import numpy as np | ||
| from plantcv.plantcv._globals import outputs, params | ||
| from plantcv.plantcv.transform.detect_color_card import deltaE | ||
|
|
||
|
|
||
| def test_deltaE_macbeth(transform_test_data): | ||
| """Test for PlantCV.""" | ||
| outputs.clear() | ||
| rgb_img = cv2.imread(transform_test_data.colorcard_img) | ||
| de_matrix = deltaE(rgb_img=rgb_img, color_chip_size="classic") | ||
| assert np.shape(de_matrix) == (6, 4) | ||
| assert outputs.metadata["max_deltaE_calibrated"]["value"] == np.float64(40.10999900620317) | ||
|
joshqsumner marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| def test_deltaE_astro(transform_test_data): | ||
| """Test for PlantCV.""" | ||
| outputs.clear() | ||
| rgb_img = cv2.imread(transform_test_data.astrocard_img) | ||
| de_matrix = deltaE(rgb_img=rgb_img, color_chip_size="astro") | ||
| assert np.shape(de_matrix) == (3, 5) | ||
| assert outputs.metadata["max_deltaE_calibrated"]["value"] == np.float64(78.00042935949308) | ||
|
joshqsumner marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| @pytest.mark.parametrize("debug", ["print", "plot", None]) | ||
| def test_deltaE_plotting(debug, transform_test_data, tmpdir): | ||
| """Test for PlantCV.""" | ||
| cache = tmpdir.mkdir("cache") | ||
| debug_outdir = params.debug_outdir | ||
| params.debug_outdir = os.path.join(cache) | ||
| params.debug = debug | ||
| rgb_img = cv2.imread(transform_test_data.colorcard_img) | ||
| de_matrix = deltaE(rgb_img=rgb_img, color_chip_size="classic") | ||
| params.debug_outdir = debug_outdir | ||
|
joshqsumner marked this conversation as resolved.
|
||
| assert np.shape(de_matrix) == (6, 4) | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.