diff --git a/docs/assets/screenshots/viz-colormaps-min_dist.gif b/docs/assets/screenshots/viz-colormaps-min_dist.gif new file mode 100644 index 0000000000000000000000000000000000000000..51879c2d577f6adaa1c18c65241d3f5a1e8ea63c Binary files /dev/null and b/docs/assets/screenshots/viz-colormaps-min_dist.gif differ diff --git a/docs/assets/screenshots/viz-colormaps-objects-all.png b/docs/assets/screenshots/viz-colormaps-objects-all.png new file mode 100644 index 0000000000000000000000000000000000000000..727744cfaef8fc3fb47d7bbcebb93a736939c0e3 Binary files /dev/null and b/docs/assets/screenshots/viz-colormaps-objects-all.png differ diff --git a/docs/assets/screenshots/viz-colormaps-objects.gif b/docs/assets/screenshots/viz-colormaps-objects.gif new file mode 100644 index 0000000000000000000000000000000000000000..2f8d666ebfdc2f2c7653449a0ab7194e188f721a Binary files /dev/null and b/docs/assets/screenshots/viz-colormaps-objects.gif differ diff --git a/docs/releases.md b/docs/releases.md index f35cc9244ed3b50aa0e831aec2dc6bc296eaf7ef..747f74de757bd6cdf460a2f6316595a07b2935ec 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -17,6 +17,7 @@ And remember to keep your pip installation [up to date](/qim3d/#upgrade) so that - Added option to pass `dim_order` to `vol/vgi` files - The 'Data Explorer' GUI now can load image sequences also - Warning in case user tries to load a file larger than available memory +- Objects colormap now can enforce a `min_dist` between neighbor colors ### v0.3.3 (11/04/2024) - Introduction of `qim3d.viz.slicer` (and also `qim3d.viz.orthogonal` ) 🎉 diff --git a/qim3d/viz/colormaps.py b/qim3d/viz/colormaps.py index fc0e54b8e1724984ff3f973b0eb19f0eb5a06985..d72c94d9ec1f104ec605f487b89dbbdf9b5cc609 100644 --- a/qim3d/viz/colormaps.py +++ b/qim3d/viz/colormaps.py @@ -5,11 +5,34 @@ This module provides a collection of colormaps useful for 3D visualization. import colorsys from typing import Union, Tuple import numpy as np +import math from matplotlib.colors import LinearSegmentedColormap from matplotlib import colormaps +from skimage import color from qim3d.io.logger import log +def rearrange_colors(randRGBcolors_old, min_dist = 0.5): + # Create new list for re-arranged colors + randRGBcolors_new = [randRGBcolors_old.pop(0)] + + while randRGBcolors_old: + previous_color = randRGBcolors_new[-1] + found = False + + # Find a next color that is at least min_dist away from previous color + for color in randRGBcolors_old: + if math.dist(color, previous_color) > min_dist: + randRGBcolors_new.append(color) + randRGBcolors_old.remove(color) + found = True + break + + # If no color was found, start over with the first color in the list + if not found: + randRGBcolors_new.append(randRGBcolors_old.pop(0)) + + return randRGBcolors_new def objects( nlabels: int, @@ -17,6 +40,7 @@ def objects( first_color_background: bool = True, last_color_background: bool = False, background_color: Union[Tuple[float, float, float], str] = (0.0, 0.0, 0.0), + min_dist: int = 0.5, seed: int = 19, ) -> LinearSegmentedColormap: """ @@ -24,10 +48,11 @@ def objects( Args: nlabels (int): Number of labels (size of colormap). - style (str, optional): 'bright' for strong colors, 'soft' for pastel colors. Defaults to 'bright'. + style (str, optional): 'bright' for strong colors, 'soft' for pastel colors, 'earth' for yellow/green/blue colors, 'ocean' for blue/purple/pink colors. Defaults to 'bright'. first_color_background (bool, optional): If True, the first color is used as background. Defaults to True. last_color_background (bool, optional): If True, the last color is used as background. Defaults to False. background_color (tuple or str, optional): RGB tuple or string for background color. Can be "black" or "white". Defaults to (0.0, 0.0, 0.0). + min_dist (int, optional): Minimum distance between neighboring colors. Defaults to 0.5. seed (int, optional): Seed for random number generator. Defaults to 19. Returns: @@ -38,16 +63,40 @@ def objects( ```python import qim3d - cmap = qim3d.viz.colormaps.objects(nlabels=100, first_color_background=True, background_color="black") - display(cmap) + cmap_bright = qim3d.viz.colormaps.objects(nlabels=100, style = 'bright', first_color_background=True, background_color="black", min_dist=0.7) + cmap_soft = qim3d.viz.colormaps.objects(nlabels=100, style = 'soft', first_color_background=True, background_color="black", min_dist=0.2) + cmap_earth = qim3d.viz.colormaps.objects(nlabels=100, style = 'earth', first_color_background=True, background_color="black", min_dist=0.8) + cmap_ocean = qim3d.viz.colormaps.objects(nlabels=100, style = 'ocean', first_color_background=True, background_color="black", min_dist=0.9) + + display(cmap_bright) + display(cmap_soft) + display(cmap_earth) + display(cmap_ocean) ``` -  +  + + ```python + import qim3d + + vol = qim3d.examples.cement_128x128x128 + binary = qim3d.processing.filters.gaussian(vol, 2) < 60 + labeled_volume, num_labels = qim3d.processing.operations.watershed(binary) + + cmap = qim3d.viz.colormaps.objects(num_labels, style = 'bright') + qim3d.viz.slicer(labeled_volume, axis = 1, cmap=cmap) + ``` +  + + Tip: + The `min_dist` parameter can be used to control the distance between neighboring colors. +  + """ # Check style - if style not in ("bright", "soft"): + if style not in ("bright", "soft", "earth", "ocean"): raise ValueError( - f'Please choose "bright" or "soft" for style in qim3dCmap not "{style}"' + f'Please choose "bright", "soft", "earth" or "ocean" for style in qim3dCmap not "{style}"' ) # Translate strings to background color @@ -97,6 +146,45 @@ def objects( for i in range(nlabels) ] + # Generate color map for earthy colors, based on LAB + if style == "earth": + randLABColors = [ + ( + rng.uniform(low=25, high=110), + rng.uniform(low=-120, high=70), + rng.uniform(low=-70, high=70), + ) + for i in range(nlabels) + ] + + # Convert LAB list to RGB + randRGBcolors = [] + for LabColor in randLABColors: + randRGBcolors.append( + color.lab2rgb([[LabColor]])[0][0].tolist() + ) + + # Generate color map for ocean colors, based on LAB + if style == "ocean": + randLABColors = [ + ( + rng.uniform(low=0, high=110), + rng.uniform(low=-128, high=160), + rng.uniform(low=-128, high=0), + ) + for i in range(nlabels) + ] + + # Convert LAB list to RGB + randRGBcolors = [] + for LabColor in randLABColors: + randRGBcolors.append( + color.lab2rgb([[LabColor]])[0][0].tolist() + ) + + # Re-arrange colors to have a minimum distance between neighboring colors + randRGBcolors = rearrange_colors(randRGBcolors, min_dist) + # Set first and last color to background if first_color_background: randRGBcolors[0] = background_color @@ -111,8 +199,6 @@ def objects( return objects - - qim = LinearSegmentedColormap.from_list('qim', [(0.6, 0.0, 0.0), #990000 (1.0, 0.6, 0.0), #ff9900