diff --git a/docs/color_palette.md b/docs/color_palette.md index 6effd8041..84fd67d67 100644 --- a/docs/color_palette.md +++ b/docs/color_palette.md @@ -1,31 +1,50 @@ ## Color Palette -Returns a random list of RGB color values, equally spaced across a rainbow color spectrum. -If one color is requested, a random color is returned. Otherwise, evenly spaced colors are returned. +Returns a list of RGB color values, equally spaced across a color map. The color map used is configurable and colors +can either be returned in sequential or randomized order. -**plantcv.color_palette(*num*)** +**plantcv.color_palette(*num, saved=False*)** **returns** colors - **Parameters:** - num - an integer number greater than or equal to 1 + - saved - True/False whether a previously saved color scale should be used. Default = False - **Context:** - - Used when a random set of colors is needed. + - Used when a set of colors is needed. + - See: [Multi-plant tutorial](multi-plant_tutorial.md), [Morphology tutorial](morphology_tutorial.md), + [Spatial clustering](spatial_clustering.md), and [Watershed segmentation](watershed.md). + - Visit the [Matplotlib](https://matplotlib.org/tutorials/colors/colormaps.html#sphx-glr-tutorials-colors-colormaps-py) website for a list of available colormaps. ```python from plantcv import plantcv as pcv -# Get one random color +# Get one color colors = pcv.color_palette(1) print(colors) -[(255, 16, 0)] +# [[255, 0, 40]] -# Get five random colors +# The color scale is saved for use by other functions +print(pcv.params.saved_color_scale) +# [[255, 0, 40]] + +# The color scale can be changed and the order can be changed from "sequential" to "random" +pcv.params.color_scale = "viridis" +pcv.params.color_sequence = "random" + +# Get five colors (note this will be a new color scale because saved = False by default) colors = pcv.color_palette(5) print(colors) -[(0, 0, 255), (0, 255, 205), (100, 255, 0), (255, 106, 0), (255, 0, 199)] +# [[68, 1, 84], [94, 201, 97], [58, 82, 139], [253, 231, 36], [32, 144, 140]] + +# To use a saved color scale (if it exists) +colors = pcv.color_palette(num=5, saved=True) +print(colors) +# [[68, 1, 84], [94, 201, 97], [58, 82, 139], [253, 231, 36], [32, 144, 140]] +# To explicitly remove the saved scale, set it to None +pcv.params.saved_color_scale = None ``` **Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/master/plantcv/plantcv/color_palette.py) diff --git a/docs/params.md b/docs/params.md index db0671dd0..50bed0acd 100644 --- a/docs/params.md +++ b/docs/params.md @@ -27,14 +27,23 @@ Attributes are accessed as plantcv.*attribute*. **dpi**: Dots per inch for plotting debugging images. -**text_size**: Size of the text for labels in debugging plots created by [segment_angle](segment_angle.md), [segment_curvature](segment_curvature.md), [segment_euclidean_length.md), +**text_size**: Size of the text for labels in debugging plots created by [segment_angle](segment_angle.md), [segment_curvature](segment_curvature.md), [segment_euclidean_length](segment_euclidean_length.md), [segment_id](segment_id.md), [segment_insertion_angle](segment_insertion_angle.md), [segment_path_length](segment_pathlength.md), and [segment_tangent_angle](segment_tangent_angle.md) from the morphology sub-package. -**text_thickness**: Thickness of the text for labels in debugging plots created by [segment_angle](segment_angle.md), [segment_curvature](segment_curvature.md), [segment_euclidean_length.md), +**text_thickness**: Thickness of the text for labels in debugging plots created by [segment_angle](segment_angle.md), [segment_curvature](segment_curvature.md), [segment_euclidean_length](segment_euclidean_length.md), [segment_id](segment_id.md), [segment_insertion_angle](segment_insertion_angle.md), [segment_path_length](segment_pathlength.md), and [segment_tangent_angle](segment_tangent_angle.md) from the morphology sub-package. +**color_scale**: The name of a color scale (a Matplot lib colormap) used by [segment_angle](segment_angle.md), [segment_curvature](segment_curvature.md), +[segment_euclidean_length](segment_euclidean_length.md), [segment_insertion_angle](segment_insertion_angle.md), [segment_path_length](segment_pathlength.md), and [segment_skeleton](segment_skeleton.md), +[segment_tangent_angle](segment_tangent_angle.md) from the morphology sub-package, and [cluster_contours](cluster_contours.md), [spatial_clustering](spatial_clustering.md), and +[watershed_segmentation](watershed.md) from the base package. Default = "gist_rainbow". See the [Matplotlib](https://matplotlib.org/tutorials/colors/colormaps.html#sphx-glr-tutorials-colors-colormaps-py) website for available options. + +**color_sequence**: Set the sequence of colors from the `color_scale` created by the `color_palette` function to "sequential" or "random" order. Default = "sequential". + +**saved_color_scale**: Using the `color_palette` function will save the color scale here for reuse in downstream functions. Set to `None` to remove. Default = `None`. + ### Example Updated PlantCV functions use `params` implicitly, so overriding the `params` defaults will alter the behavior of diff --git a/docs/updating.md b/docs/updating.md index 07f39cf28..705d9e40f 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -267,6 +267,7 @@ pages for more details on the input and output variable types. * pre v3.0: NA * post v3.0: colors = **plantcv.color_palette**(*num*) +* post v3.9: colors = **plantcv.color_palette**(*num, saved=False*) #### plantcv.crop_position_mask diff --git a/plantcv/plantcv/__init__.py b/plantcv/plantcv/__init__.py index e039a5f0e..3d902bf83 100644 --- a/plantcv/plantcv/__init__.py +++ b/plantcv/plantcv/__init__.py @@ -8,21 +8,35 @@ class Params: - """PlantCV parameters class - Keyword arguments/parameters: - device = device number. Used to count steps in the pipeline. (default: 0) - debug = None, print, or plot. Print = save to file, Plot = print to screen. (default: None) - debug_outdir = Debug images output directory. (default: .) - :param device: int - :param debug: str - :param debug_outdir: str - :param line_thickness: numeric - :param dpi: int - :param text_size: float - """ + """PlantCV parameters class.""" def __init__(self, device=0, debug=None, debug_outdir=".", line_thickness=5, dpi=100, text_size=0.55, - text_thickness=2): + text_thickness=2, color_scale="gist_rainbow", color_sequence="sequential", saved_color_scale=None): + """Initialize parameters. + + Keyword arguments/parameters: + device = Device number. Used to count steps in the pipeline. (default: 0) + debug = None, print, or plot. Print = save to file, Plot = print to screen. (default: None) + debug_outdir = Debug images output directory. (default: .) + line_thickness = Width of line drawings. (default: 5) + dpi = Figure plotting resolution, dots per inch. (default: 100) + text_size = Size of plotting text. (default: 0.55) + text_thickness = Thickness of plotting text. (default: 2) + color_scale = Name of plotting color scale (matplotlib colormap). (default: gist_rainbow) + color_sequence = Build color scales in "sequential" or "random" order. (default: sequential) + saved_color_scale = Saved color scale that will be applied next time color_palette is called. (default: None) + + :param device: int + :param debug: str + :param debug_outdir: str + :param line_thickness: numeric + :param dpi: int + :param text_size: float + :param text_thickness: int + :param color_scale: str + :param color_sequence: str + :param saved_color_scale: list + """ self.device = device self.debug = debug self.debug_outdir = debug_outdir @@ -30,6 +44,9 @@ def __init__(self, device=0, debug=None, debug_outdir=".", line_thickness=5, dpi self.dpi = dpi self.text_size = text_size self.text_thickness = text_thickness + self.color_scale = color_scale + self.color_sequence = color_sequence + self.saved_color_scale = saved_color_scale class Outputs: diff --git a/plantcv/plantcv/color_palette.py b/plantcv/plantcv/color_palette.py index 56b0da0ac..545419dd6 100755 --- a/plantcv/plantcv/color_palette.py +++ b/plantcv/plantcv/color_palette.py @@ -1,70 +1,36 @@ # Color palette returns an array of colors (rainbow) -from random import randrange +from matplotlib import pyplot as plt +import numpy as np +from plantcv.plantcv import params -def color_palette(num): +def color_palette(num, saved=False): """color_palette: Returns a list of colors length num Inputs: - num = number of colors to return. If num = 1 a random color is returned, - otherwise, evenly spaced colors are returned. + num = number of colors to return. + saved = use the previously stored color scale, if any (default = False). Returns: - colors = a list of color tuples (RGB values) + colors = a list of color lists (RGB values) :param num: int :return colors: list """ - # Rainbow color scheme (red->red) - rainbow = ( - (0, 0, 255), (0, 6, 255), (0, 12, 255), (0, 18, 255), (0, 24, 255), (0, 30, 255), (0, 36, 255), (0, 42, 255), - (0, 48, 255), (0, 54, 255), (0, 60, 255), (0, 66, 255), (0, 72, 255), (0, 78, 255), (0, 84, 255), (0, 90, 255), - (0, 96, 255), (0, 102, 255), (0, 108, 255), (0, 114, 255), (0, 120, 255), (0, 126, 255), (0, 131, 255), - (0, 137, 255), (0, 143, 255), (0, 149, 255), (0, 155, 255), (0, 161, 255), (0, 167, 255), (0, 173, 255), - (0, 179, 255), (0, 185, 255), (0, 191, 255), (0, 197, 255), (0, 203, 255), (0, 209, 255), (0, 215, 255), - (0, 221, 255), (0, 227, 255), (0, 233, 255), (0, 239, 255), (0, 245, 255), (0, 251, 255), (0, 255, 253), - (0, 255, 247), (0, 255, 241), (0, 255, 235), (0, 255, 229), (0, 255, 223), (0, 255, 217), (0, 255, 211), - (0, 255, 205), (0, 255, 199), (0, 255, 193), (0, 255, 187), (0, 255, 181), (0, 255, 175), (0, 255, 169), - (0, 255, 163), (0, 255, 157), (0, 255, 151), (0, 255, 145), (0, 255, 139), (0, 255, 133), (0, 255, 128), - (0, 255, 122), (0, 255, 116), (0, 255, 110), (0, 255, 104), (0, 255, 98), (0, 255, 92), (0, 255, 86), - (0, 255, 80), - (0, 255, 74), (0, 255, 68), (0, 255, 62), (0, 255, 56), (0, 255, 50), (0, 255, 44), (0, 255, 38), (0, 255, 32), - (0, 255, 26), (0, 255, 20), (0, 255, 14), (0, 255, 8), (0, 255, 2), (4, 255, 0), (10, 255, 0), (16, 255, 0), - (22, 255, 0), (28, 255, 0), (34, 255, 0), (40, 255, 0), (46, 255, 0), (52, 255, 0), (58, 255, 0), (64, 255, 0), - (70, 255, 0), (76, 255, 0), (82, 255, 0), (88, 255, 0), (94, 255, 0), (100, 255, 0), (106, 255, 0), - (112, 255, 0), - (118, 255, 0), (124, 255, 0), (129, 255, 0), (135, 255, 0), (141, 255, 0), (147, 255, 0), (153, 255, 0), - (159, 255, 0), (165, 255, 0), (171, 255, 0), (177, 255, 0), (183, 255, 0), (189, 255, 0), (195, 255, 0), - (201, 255, 0), (207, 255, 0), (213, 255, 0), (219, 255, 0), (225, 255, 0), (231, 255, 0), (237, 255, 0), - (243, 255, 0), (249, 255, 0), (255, 255, 0), (255, 249, 0), (255, 243, 0), (255, 237, 0), (255, 231, 0), - (255, 225, 0), (255, 219, 0), (255, 213, 0), (255, 207, 0), (255, 201, 0), (255, 195, 0), (255, 189, 0), - (255, 183, 0), (255, 177, 0), (255, 171, 0), (255, 165, 0), (255, 159, 0), (255, 153, 0), (255, 147, 0), - (255, 141, 0), (255, 135, 0), (255, 129, 0), (255, 124, 0), (255, 118, 0), (255, 112, 0), (255, 106, 0), - (255, 100, 0), (255, 94, 0), (255, 88, 0), (255, 82, 0), (255, 76, 0), (255, 70, 0), (255, 64, 0), (255, 58, 0), - (255, 52, 0), (255, 46, 0), (255, 40, 0), (255, 34, 0), (255, 28, 0), (255, 22, 0), (255, 16, 0), (255, 10, 0), - (255, 4, 0), (255, 0, 2), (255, 0, 8), (255, 0, 14), (255, 0, 20), (255, 0, 26), (255, 0, 32), (255, 0, 38), - (255, 0, 44), (255, 0, 50), (255, 0, 56), (255, 0, 62), (255, 0, 68), (255, 0, 74), (255, 0, 80), (255, 0, 86), - (255, 0, 92), (255, 0, 98), (255, 0, 104), (255, 0, 110), (255, 0, 116), (255, 0, 122), (255, 0, 128), - (255, 0, 133), (255, 0, 139), (255, 0, 145), (255, 0, 151), (255, 0, 157), (255, 0, 163), (255, 0, 169), - (255, 0, 175), (255, 0, 181), (255, 0, 187), (255, 0, 193), (255, 0, 199), (255, 0, 205), (255, 0, 211), - (255, 0, 217), (255, 0, 223), (255, 0, 229), (255, 0, 235), (255, 0, 241), (255, 0, 247), (255, 0, 253), - (251, 0, 255), (245, 0, 255), (239, 0, 255), (233, 0, 255), (227, 0, 255), (221, 0, 255), (215, 0, 255), - (209, 0, 255), (203, 0, 255), (197, 0, 255), (191, 0, 255), (185, 0, 255), (179, 0, 255), (173, 0, 255), - (167, 0, 255), (161, 0, 255), (155, 0, 255), (149, 0, 255), (143, 0, 255), (137, 0, 255), (131, 0, 255), - (126, 0, 255), (120, 0, 255), (114, 0, 255), (108, 0, 255), (102, 0, 255), (96, 0, 255), (90, 0, 255), - (84, 0, 255), - (78, 0, 255), (72, 0, 255), (66, 0, 255), (60, 0, 255), (54, 0, 255), (48, 0, 255), (42, 0, 255), (36, 0, 255), - (30, 0, 255), (24, 0, 255), (18, 0, 255), (12, 0, 255), (6, 0, 255)) - - if num == 1: - color = rainbow[randrange(0, 255)] - return [color] + # If a previous palette is saved and saved = True, return it + if params.saved_color_scale is not None and saved is True: + return params.saved_color_scale else: - dist = int(len(rainbow) / num) - colors = [] - index = 0 - for i in range(1, num + 1): - colors.append(rainbow[index]) - index += dist + # Retrieve the matplotlib colormap + cmap = plt.get_cmap(params.color_scale) + # Get num evenly spaced colors + colors = cmap(np.linspace(0, 1, num), bytes=True) + colors = colors[:, 0:3].tolist() + # colors are sequential, if params.color_sequence is random then shuffle the colors + if params.color_sequence == "random": + np.random.shuffle(colors) + # Save the color scale for further use + params.saved_color_scale = colors + return colors diff --git a/plantcv/plantcv/morphology/check_cycles.py b/plantcv/plantcv/morphology/check_cycles.py index ce19385ec..099b7e888 100644 --- a/plantcv/plantcv/morphology/check_cycles.py +++ b/plantcv/plantcv/morphology/check_cycles.py @@ -54,7 +54,8 @@ def check_cycles(skel_img): cycle_img = dilate(cycle_img, params.line_thickness, 1) cycle_img = cv2.cvtColor(cycle_img, cv2.COLOR_GRAY2RGB) if num_cycles > 0: - rand_color = color_palette(num_cycles) + # Get a new color scale + rand_color = color_palette(num=num_cycles, saved=False) for i, cnt in enumerate(cycle_objects): cv2.drawContours(cycle_img, cycle_objects, i, rand_color[i], params.line_thickness, lineType=8, hierarchy=cycle_hierarchies) diff --git a/plantcv/plantcv/morphology/segment_angle.py b/plantcv/plantcv/morphology/segment_angle.py index c311875b5..e8aac9cfa 100644 --- a/plantcv/plantcv/morphology/segment_angle.py +++ b/plantcv/plantcv/morphology/segment_angle.py @@ -33,7 +33,8 @@ def segment_angle(segmented_img, objects): labeled_img = segmented_img.copy() - rand_color = color_palette(len(objects)) + # Use a previously saved color scale if available + rand_color = color_palette(num=len(objects), saved=True) for i, cnt in enumerate(objects): # Find bounds for regression lines to get drawn diff --git a/plantcv/plantcv/morphology/segment_combine.py b/plantcv/plantcv/morphology/segment_combine.py index ecba4009f..e0ff864d4 100644 --- a/plantcv/plantcv/morphology/segment_combine.py +++ b/plantcv/plantcv/morphology/segment_combine.py @@ -78,8 +78,8 @@ def segment_combine(segment_list, objects, mask): labeled_img = mask.copy() labeled_img = cv2.cvtColor(labeled_img, cv2.COLOR_GRAY2RGB) - # Color each segment a different color - rand_color = color_palette(len(all_objects)) + # Color each segment a different color, use a previously saved scale if available + rand_color = color_palette(num=len(all_objects), saved=True) # Plot all segment contours for i, cnt in enumerate(all_objects): diff --git a/plantcv/plantcv/morphology/segment_curvature.py b/plantcv/plantcv/morphology/segment_curvature.py index c84cadc78..8d038ba6e 100644 --- a/plantcv/plantcv/morphology/segment_curvature.py +++ b/plantcv/plantcv/morphology/segment_curvature.py @@ -45,7 +45,8 @@ def segment_curvature(segmented_img, objects): eu_lengths = outputs.observations['segment_eu_length']['value'] path_lengths = outputs.observations['segment_path_length']['value'] curvature_measure = [float(x / y) for x, y in zip(path_lengths, eu_lengths)] - rand_color = color_palette(len(objects)) + # Create a color scale, use a previously stored scale if available + rand_color = color_palette(num=len(objects), saved=True) for i, cnt in enumerate(objects): # Store coordinates for labels diff --git a/plantcv/plantcv/morphology/segment_euclidean_length.py b/plantcv/plantcv/morphology/segment_euclidean_length.py index ae846ca38..6ccbb8edd 100644 --- a/plantcv/plantcv/morphology/segment_euclidean_length.py +++ b/plantcv/plantcv/morphology/segment_euclidean_length.py @@ -32,7 +32,8 @@ def segment_euclidean_length(segmented_img, objects): x_list = [] y_list = [] segment_lengths = [] - rand_color = color_palette(len(objects)) + # Create a color scale, use a previously stored scale if available + rand_color = color_palette(num=len(objects), saved=True) labeled_img = segmented_img.copy() # Store debug diff --git a/plantcv/plantcv/morphology/segment_id.py b/plantcv/plantcv/morphology/segment_id.py index d8d065c63..e2bff486b 100644 --- a/plantcv/plantcv/morphology/segment_id.py +++ b/plantcv/plantcv/morphology/segment_id.py @@ -36,8 +36,8 @@ def segment_id(skel_img, objects, mask=None): segmented_img = cv2.cvtColor(segmented_img, cv2.COLOR_GRAY2RGB) - # Color each segment a different color - rand_color = color_palette(len(objects)) + # Create a color scale, use a previously stored scale if available + rand_color = color_palette(num=len(objects), saved=True) # Plot all segment contours for i, cnt in enumerate(objects): diff --git a/plantcv/plantcv/morphology/segment_insertion_angle.py b/plantcv/plantcv/morphology/segment_insertion_angle.py index ca67be26a..62030e1ff 100644 --- a/plantcv/plantcv/morphology/segment_insertion_angle.py +++ b/plantcv/plantcv/morphology/segment_insertion_angle.py @@ -105,7 +105,8 @@ def segment_insertion_angle(skel_img, segmented_img, leaf_objects, stem_objects, label_coord_x.append(leaf_objects[i][0][0][0]) label_coord_y.append(leaf_objects[i][0][0][1]) - rand_color = color_palette(len(valid_segment)) + # Create a color scale, use a previously stored scale if available + rand_color = color_palette(num=len(valid_segment), saved=True) for i, cnt in enumerate(valid_segment): cv2.drawContours(labeled_img, valid_segment, i, rand_color[i], params.line_thickness, lineType=8) diff --git a/plantcv/plantcv/morphology/segment_skeleton.py b/plantcv/plantcv/morphology/segment_skeleton.py index 74e780e8b..5f3307923 100644 --- a/plantcv/plantcv/morphology/segment_skeleton.py +++ b/plantcv/plantcv/morphology/segment_skeleton.py @@ -16,19 +16,17 @@ def segment_skeleton(skel_img, mask=None): """ Segment a skeleton image into pieces Inputs: - skel_img = Skeletonized image - mask = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask. + skel_img = Skeletonized image + mask = (Optional) binary mask for debugging. If provided, debug image will be overlaid on the mask. Returns: segmented_img = Segmented debugging image segment_objects = list of contours - segment_hierarchies = contour hierarchy list :param skel_img: numpy.ndarray :param mask: numpy.ndarray :return segmented_img: numpy.ndarray :return segment_objects: list - "return segment_hierarchies: numpy.ndarray """ # Store debug @@ -48,8 +46,8 @@ def segment_skeleton(skel_img, mask=None): # Reset debug mode params.debug = debug - # Color each segment a different color - rand_color = color_palette(len(segment_objects)) + # Color each segment a different color, do not used a previously saved color scale + rand_color = color_palette(num=len(segment_objects), saved=False) if mask is None: segmented_img = skel_img.copy() diff --git a/plantcv/plantcv/morphology/segment_tangent_angle.py b/plantcv/plantcv/morphology/segment_tangent_angle.py index 1e3b88b58..89294cb84 100644 --- a/plantcv/plantcv/morphology/segment_tangent_angle.py +++ b/plantcv/plantcv/morphology/segment_tangent_angle.py @@ -58,7 +58,8 @@ def segment_tangent_angle(segmented_img, objects, size): label_coord_x = [] label_coord_y = [] - rand_color = color_palette(len(objects)) + # Create a color scale, use a previously stored scale if available + rand_color = color_palette(num=len(objects), saved=True) for i, cnt in enumerate(objects): find_tangents = np.zeros(segmented_img.shape[:2], np.uint8) diff --git a/tests/tests.py b/tests/tests.py index f66ba399c..d0f0193bd 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -556,7 +556,7 @@ def test_plantcv_parallel_process_results_invalid_json(): # #################################################################################################################### # ########################################### PLANTCV MAIN PACKAGE ################################################### -matplotlib.use('Template', warn=False) +matplotlib.use('Template') TEST_DATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") HYPERSPECTRAL_TEST_DATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "hyperspectral_data") @@ -1366,21 +1366,23 @@ def test_plantcv_cluster_contours_splitimg(): def test_plantcv_color_palette(): - # Collect assertions - truths = [] + # Return a color palette + colors = pcv.color_palette(num=10, saved=False) + assert np.shape(colors) == (10, 3) - # Return one random color - colors = pcv.color_palette(1) - # Colors should be a list of length 1, containing a tuple of length 3 - truths.append(len(colors) == 1) - truths.append(len(colors[0]) == 3) - # Return ten random colors - colors = pcv.color_palette(10) - # Colors should be a list of length 10 - truths.append(len(colors) == 10) - # All of these should be true for the function to pass testing. - assert (all(truths)) +def test_plantcv_color_palette_random(): + # Return a color palette in random order + pcv.params.color_sequence = "random" + colors = pcv.color_palette(num=10, saved=False) + assert np.shape(colors) == (10, 3) + + +def test_plantcv_color_palette_saved(): + # Return a color palette that was saved + pcv.params.saved_color_scale = [[0, 0, 0], [255, 255, 255]] + colors = pcv.color_palette(num=2, saved=True) + assert colors == [[0, 0, 0], [255, 255, 255]] def test_plantcv_crop(): @@ -5564,13 +5566,17 @@ def test_plantcv_visualize_clustered_contours(): cluster = [cluster_i[arr_n] for arr_n in cluster_i] # Test in print mode pcv.params.debug = "print" + # Reset the saved color scale (can be saved between tests) + pcv.params.saved_color_scale = None _ = pcv.visualize.clustered_contours(img=img, grouped_contour_indices=cluster, roi_objects=objs, roi_obj_hierarchy=obj_hierarchy, nrow=2, ncol=2) # Test in plot mode pcv.params.debug = "plot" + # Reset the saved color scale (can be saved between tests) + pcv.params.saved_color_scale = None cluster_img = pcv.visualize.clustered_contours(img=img1, grouped_contour_indices=cluster, roi_objects=objs, roi_obj_hierarchy=obj_hierarchy) - assert len(np.unique(cluster_img)) == 37 + assert len(np.unique(cluster_img.reshape(-1, cluster_img.shape[2]), axis=0)) == 37 def test_plantcv_visualize_colorspaces():