perfect-cmaps 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. perfect_cmaps/__init__.py +0 -0
  2. perfect_cmaps/color_utils.py +377 -0
  3. perfect_cmaps/colormaps.py +237 -0
  4. perfect_cmaps/customize_cmap.py +223 -0
  5. perfect_cmaps/data/cielab_gamut_max_L.npy +0 -0
  6. perfect_cmaps/data/cielab_gamut_max_L_RGB.npy +0 -0
  7. perfect_cmaps/lab_control_points/README.md +5 -0
  8. perfect_cmaps/lab_control_points/__init__.py +0 -0
  9. perfect_cmaps/lab_control_points/__pycache__/__init__.cpython-38.pyc +0 -0
  10. perfect_cmaps/lab_control_points/cividish.json +209 -0
  11. perfect_cmaps/lab_control_points/diverging_hot_cool.json +89 -0
  12. perfect_cmaps/lab_control_points/mcyk.json +25 -0
  13. perfect_cmaps/lab_control_points/plasmoid.json +209 -0
  14. perfect_cmaps/lab_control_points/rgb_test.json +85 -0
  15. perfect_cmaps/lab_control_points/rgb_test2.json +85 -0
  16. perfect_cmaps/lab_control_points/salmiak.json +25 -0
  17. perfect_cmaps/lab_control_points/spectrum.json +209 -0
  18. perfect_cmaps/lab_control_points/viridish.json +89 -0
  19. perfect_cmaps/lab_control_points/viridish_2.json +209 -0
  20. perfect_cmaps/optimization.py +114 -0
  21. perfect_cmaps/storage.py +65 -0
  22. perfect_cmaps/test_images/README.md +6 -0
  23. perfect_cmaps/test_images/__init__.py +0 -0
  24. perfect_cmaps/test_images/__pycache__/__init__.cpython-38.pyc +0 -0
  25. perfect_cmaps/test_images/colourmaptest.tif +0 -0
  26. perfect_cmaps/test_images/sem/BSD-sandpaper.jpg +0 -0
  27. perfect_cmaps/test_images/sem/Lily-Pollen.png +0 -0
  28. perfect_cmaps/test_images/sem/PhenomPharos-Gallery_Pyrite.jpg +0 -0
  29. perfect_cmaps/test_images/sem/PhenomXLGallery_5.jpg +0 -0
  30. perfect_cmaps/test_images/sem/Steel-mesh.png +0 -0
  31. perfect_cmaps/test_images/sem/charcoal4_49721153188_o-scaled.jpg +0 -0
  32. perfect_cmaps/test_images/sem/graphite5_49722003087_o-scaled.jpg +0 -0
  33. perfect_cmaps/test_images/sem/melamine-foam_49722002717_o-scaled.jpg +0 -0
  34. perfect_cmaps-1.0.0.dist-info/LICENSE +201 -0
  35. perfect_cmaps-1.0.0.dist-info/METADATA +70 -0
  36. perfect_cmaps-1.0.0.dist-info/RECORD +38 -0
  37. perfect_cmaps-1.0.0.dist-info/WHEEL +5 -0
  38. perfect_cmaps-1.0.0.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,377 @@
1
+ import matplotlib.colors as mcolors
2
+ import numpy as np
3
+ from scipy.stats import beta
4
+ from scipy.interpolate import interp1d
5
+ from typing import Tuple, Union
6
+ from colour import sRGB_to_XYZ, XYZ_to_Lab, Lab_to_XYZ, XYZ_to_sRGB
7
+ from matplotlib import pyplot as plt
8
+ from matplotlib.colors import LinearSegmentedColormap
9
+
10
+ from perfect_cmaps.optimization import genetic_algorithm
11
+ from perfect_cmaps.storage import get_test_img_path
12
+
13
+
14
+ RGB_WEIGHT = np.array([0.2989, 0.5870, 0.1140])
15
+ SUPPORTED_L_PROFILES = [
16
+ "linear",
17
+ "diverging",
18
+ "diverging_inverted",
19
+ "diverging_sharper",
20
+ "diverging_inverted_sharper",
21
+ "flat"
22
+ ]
23
+
24
+
25
+ def diverging_envelope(x: np.ndarray, c: float = 4, x1: float = 0.25) -> np.ndarray:
26
+ result = np.zeros(x.shape)
27
+ m = x1 * (c - 2) / (1 - 2 * x1 + 1e-5)
28
+
29
+ below_x1 = x <= x1
30
+ result[below_x1] = c*x[below_x1]
31
+
32
+ before_middle = np.logical_and(x > x1, x <= 0.5)
33
+ result[before_middle] = m + 2*(1-m) * x[before_middle]
34
+
35
+ after_middle = np.logical_and(x > 0.5, x <= 1-x1)
36
+ result[after_middle] = 2 - m + 2*(m-1) * x[after_middle]
37
+
38
+ in_middle = np.logical_and(x > x1, x <= 1 - x1)
39
+ result[in_middle] = result.max()
40
+
41
+ after_x2 = x > 1-x1
42
+ result[after_x2] = c * (1 - x[after_x2])
43
+ return result
44
+
45
+
46
+ def dying_cos(x: np.ndarray, offset: float = 0.0):
47
+ result = np.zeros_like(x)
48
+ values = np.mod(np.round(x - offset), 2) == 0
49
+ result[values] = (1 + np.cos(2*np.pi*(x[values]-offset)))/2
50
+ return result
51
+
52
+
53
+ def unwrap_channels(ijk: Union[Tuple[int, int, int], None] = None) -> tuple:
54
+ if ijk is None:
55
+ ijk = (0, 1, 2)
56
+
57
+ return ijk
58
+
59
+
60
+ def linearize_rgba(rgba_colors: np.ndarray) -> np.ndarray:
61
+ lab_colors = XYZ_to_Lab(sRGB_to_XYZ(rgba_colors[:, :3]))
62
+ lab_colors[:, 0] = np.linspace(0, 100, lab_colors.shape[0])
63
+ rgb_colors_renormalized = Lab_to_sRGB(lab_colors)
64
+ rgb_colors_renormalized = np.concatenate(
65
+ (rgb_colors_renormalized, rgba_colors[:, 3:4]), axis=1
66
+ )
67
+ return rgb_colors_renormalized
68
+
69
+
70
+ def cold_blooded(x: np.ndarray, ijk: Union[Tuple[int, int, int], None] = None) -> np.ndarray:
71
+ i, j, k = unwrap_channels(ijk)
72
+
73
+ result = np.zeros([*x.shape, 4])
74
+ period = 2*np.pi
75
+ result[..., j] = x**2 - x/period * np.sin(period*x)
76
+ result[..., k] = ((1 - np.cos(period/2*x))/2)**2
77
+ result[..., i] = 3*x**2 - result[..., j] - result[..., k]
78
+ result[..., :3] = np.sqrt(result[..., :3])
79
+
80
+ result[..., :3] /= np.max(np.max(result))
81
+ result[..., 3] = 1
82
+
83
+ return result
84
+
85
+
86
+ def cold_blooded_l(x: np.ndarray, ijk: Union[Tuple[int, int, int], None] = None) -> np.ndarray:
87
+ normal_colors = cold_blooded(x, ijk)
88
+ return linearize_rgba(normal_colors)
89
+
90
+
91
+ def copper_salt(x: np.ndarray, ijk: Union[Tuple[int, int, int], None] = None) -> np.ndarray:
92
+ i, j, k = unwrap_channels(ijk)
93
+
94
+ result = np.zeros([*x.shape, 4])
95
+ dist = beta(2, 4)
96
+ pdf = dist.pdf
97
+ envelope = diverging_envelope(x, c=4, x1=0.5)
98
+ result[..., i] = pdf(x)
99
+ result[..., k] = pdf(1-x)
100
+ result[..., i] /= np.max(result[..., i])
101
+ result[..., k] /= np.max(result[..., k])
102
+ result[..., j] = np.sqrt(3 * envelope - result[..., i] ** 2 - result[..., k] ** 2)
103
+ weight = 1 / np.sqrt(RGB_WEIGHT)
104
+
105
+ result[..., :3] = np.einsum(
106
+ 'i...j,k...j->i...j',
107
+ result[..., :3],
108
+ weight.reshape(1, *weight.shape)
109
+ )
110
+ result[..., :3] /= np.max(result)
111
+ result[..., 3] = 1
112
+ return result
113
+
114
+
115
+ def get_lightness_profile(n_values: int, profile: str = "linear") -> np.ndarray:
116
+ if profile == "linear":
117
+ L_values = np.linspace(0, 100, n_values)
118
+ elif profile.startswith("diverging"):
119
+ base_envelope = np.linspace(-10, 10, n_values)
120
+
121
+ if profile == "diverging":
122
+ L_values = 100 - base_envelope ** 2
123
+
124
+ elif profile == "diverging_sharper":
125
+ L_values = 100 - (base_envelope ** 2 + np.abs(base_envelope * 10)) / 2
126
+
127
+ elif profile == "diverging_inverted":
128
+ L_values = base_envelope ** 2
129
+
130
+ elif profile == "diverging_inverted_sharper":
131
+ L_values = (base_envelope ** 2 + np.abs(base_envelope * 10)) / 2
132
+
133
+ elif profile == "flat":
134
+ # Use a flat lightness profile, e.g., L = 50
135
+ L_values = np.full(n_values, 50)
136
+ else:
137
+ # Default to linear
138
+ L_values = np.linspace(0, 100, n_values)
139
+
140
+ return L_values
141
+
142
+
143
+ def interpolate_lab(
144
+ control_points: np.ndarray,
145
+ num_values: int = 1000,
146
+ profile: str = "linear",
147
+ interpolation: str = "quadratic"
148
+ ) -> np.ndarray:
149
+ # Interpolate between the control points
150
+ lab_colors = np.zeros((num_values, 3))
151
+ space = np.linspace(0, 1, control_points.shape[0])
152
+ for i in range(2):
153
+ interpolator = interp1d(space, control_points[:, i], kind=interpolation)
154
+ lab_colors[:, i + 1] = interpolator(np.linspace(0, 1, num_values))
155
+
156
+ lab_colors[:, 0] = get_lightness_profile(num_values, profile)
157
+ return lab_colors
158
+
159
+
160
+ def rgb_to_grayscale(rgb: np.ndarray) -> np.ndarray:
161
+ """Convert RGB values to grayscale using luminosity values of RGB channels."""
162
+ return np.sqrt(np.dot(rgb[...,:3] ** 2, RGB_WEIGHT))
163
+
164
+
165
+ def rgb_to_grayscale_lightness(rgb: np.ndarray) -> np.ndarray:
166
+ """
167
+ Convert RGB values to grayscale values representing perceived lightness (L*).
168
+
169
+ Parameters
170
+ ----------
171
+ rgb : ndarray
172
+ An array of RGB values in sRGB color space, with values in the range [0, 1].
173
+
174
+ Returns
175
+ -------
176
+ L_normalized : ndarray
177
+ Grayscale values representing the L* component, normalized to [0, 1].
178
+ """
179
+ # Ensure RGB values are within [0, 1]
180
+ rgb = np.clip(rgb, 0, 1)
181
+
182
+ # Convert sRGB to XYZ
183
+ XYZ = sRGB_to_XYZ(rgb)
184
+
185
+ # Convert XYZ to CIELAB
186
+ Lab = XYZ_to_Lab(XYZ)
187
+
188
+ # Extract the L* component
189
+ L = Lab[..., 0] # L* ranges from 0 to 100
190
+
191
+ # Normalize L* to [0, 1]
192
+ L_normalized = L / 100.0
193
+
194
+ return L_normalized
195
+
196
+
197
+ def truncate_colormap(
198
+ cmap: mcolors.LinearSegmentedColormap,
199
+ minval: float = 0.0,
200
+ maxval: float = 1.0,
201
+ n: int = 1000
202
+ ) -> mcolors.LinearSegmentedColormap:
203
+
204
+ new_cmap = mcolors.LinearSegmentedColormap.from_list(
205
+ 'trunc({n},{a:.2f},{b:.2f})'.format(n=cmap.name, a=minval, b=maxval),
206
+ cmap(np.linspace(minval, maxval, n)))
207
+ return new_cmap
208
+
209
+
210
+ def Lab_to_sRGB(lab_colors: np.ndarray) -> np.ndarray:
211
+ """
212
+ Convert CIE-LAB colors to sRGB color space.
213
+
214
+ Parameters
215
+ ----------
216
+ lab_colors : ndarray
217
+ An array of Lab colors with shape (..., 3).
218
+
219
+ Returns
220
+ -------
221
+ rgb_colors : ndarray
222
+ An array of sRGB colors with values in [0, 1].
223
+ """
224
+ # Convert Lab to XYZ
225
+ XYZ = Lab_to_XYZ(lab_colors)
226
+
227
+ # Convert XYZ to sRGB
228
+ RGB = XYZ_to_sRGB(XYZ)
229
+
230
+ return RGB
231
+
232
+
233
+ def find_valid_L_range(a_star: float, b_star: float) -> Tuple[float, float]:
234
+ # Vectorized L* samples
235
+ L_samples = np.linspace(0, 100, 100)
236
+ # Create an array of Lab colors
237
+ lab = np.column_stack((L_samples, np.full_like(L_samples, a_star), np.full_like(L_samples, b_star)))
238
+
239
+ # Convert all Lab colors to RGB at once
240
+ rgb = Lab_to_sRGB(lab)
241
+
242
+ # Determine which colors are in gamut
243
+ in_gamut = np.all((rgb >= 0) & (rgb <= 1), axis=1)
244
+
245
+ if np.any(in_gamut):
246
+ L_in_gamut = L_samples[in_gamut]
247
+ L_min_valid = L_in_gamut.min()
248
+ L_max_valid = L_in_gamut.max()
249
+ else:
250
+ L_min_valid = None
251
+ L_max_valid = None
252
+
253
+ return L_min_valid, L_max_valid
254
+
255
+
256
+ def find_valid_L_range_coarse_optim(a_star: float, b_star: float) -> Tuple[float, float]:
257
+ # Coarse sampling
258
+ L_samples_coarse = np.linspace(0, 100, 50)
259
+ lab_coarse = np.column_stack((L_samples_coarse, np.full_like(L_samples_coarse, a_star), np.full_like(L_samples_coarse, b_star)))
260
+ rgb_coarse = Lab_to_sRGB(lab_coarse)
261
+ in_gamut_coarse = np.all((rgb_coarse >= 0) & (rgb_coarse <= 1), axis=1)
262
+
263
+ if np.any(in_gamut_coarse):
264
+ # Approximate L_min and L_max
265
+ L_in_gamut = L_samples_coarse[in_gamut_coarse]
266
+ L_min_approx = L_in_gamut.min()
267
+ L_max_approx = L_in_gamut.max()
268
+
269
+ # Fine sampling near L_min_approx
270
+ L_samples_min = np.linspace(max(L_min_approx - 1, 0), L_min_approx, 20)
271
+ lab_min = np.column_stack((L_samples_min, np.full_like(L_samples_min, a_star), np.full_like(L_samples_min, b_star)))
272
+ rgb_min = Lab_to_sRGB(lab_min)
273
+ in_gamut_min = np.all((rgb_min >= 0) & (rgb_min <= 1), axis=1)
274
+ L_min_valid = L_samples_min[in_gamut_min].min() if np.any(in_gamut_min) else L_min_approx
275
+
276
+ # Fine sampling near L_max_approx
277
+ L_samples_max = np.linspace(L_max_approx, min(L_max_approx + 1, 100), 20)
278
+ lab_max = np.column_stack((L_samples_max, np.full_like(L_samples_max, a_star), np.full_like(L_samples_max, b_star)))
279
+ rgb_max = Lab_to_sRGB(lab_max)
280
+ in_gamut_max = np.all((rgb_max >= 0) & (rgb_max <= 1), axis=1)
281
+ L_max_valid = L_samples_max[in_gamut_max].max() if np.any(in_gamut_max) else L_max_approx
282
+ else:
283
+ L_min_valid = None
284
+ L_max_valid = None
285
+
286
+ return L_min_valid, L_max_valid
287
+
288
+
289
+ def rgb_renormalized_lightness(
290
+ lab_colors: np.ndarray,
291
+ population_size: int = 100,
292
+ num_generations: int = 200,
293
+ verbose: bool = False,
294
+ ) -> np.ndarray:
295
+
296
+ # Compute L_min and L_max for each color
297
+ n_colors = lab_colors.shape[0]
298
+ L_min = np.zeros(n_colors)
299
+ L_max = L_min.copy()
300
+
301
+ for i in range(n_colors):
302
+ a_star = lab_colors[i, 1]
303
+ b_star = lab_colors[i, 2]
304
+ # Compute L_min[i] and L_max[i] for this (a*, b*) pair
305
+ L_min[i], L_max[i] = find_valid_L_range(a_star, b_star)
306
+
307
+ L_intended = lab_colors[:, 0]
308
+
309
+ # Optimize m and c
310
+ gene_limits = np.array([[0.0, 100.0], [-100.0, 100.0]])
311
+ genes, best_fitness = genetic_algorithm(population_size, num_generations, gene_limits, L_intended, L_min, L_max)
312
+
313
+ if best_fitness == 0.0:
314
+ print("Optimization failed for chosen lightness profile.")
315
+ print("Colormap is not perfectly perceptually uniform.")
316
+ return Lab_to_sRGB(lab_colors), 1.0, 0.0
317
+
318
+ if verbose:
319
+ print(f"Optimized colormap lightness range: {100 * best_fitness:.2f} %")
320
+
321
+ L_adjusted = L_intended * genes[0] + genes[1]
322
+
323
+ # Update L* values in lab_colors
324
+ lab_colors[:, 0] = L_adjusted
325
+
326
+ # Convert adjusted Lab colors to RGB
327
+ return Lab_to_sRGB(lab_colors)
328
+
329
+
330
+ def plot_colormap(colormap: LinearSegmentedColormap, num_points: int = 1000):
331
+ gradient = np.linspace(0, 1, num_points)
332
+ gradient = np.vstack((gradient, gradient))
333
+
334
+ # Get RGB values from colormap
335
+ gradient_rgb = colormap(gradient)
336
+
337
+ # Convert the RGB values to grayscale
338
+ lightness = rgb_to_grayscale_lightness(gradient_rgb[0][:, :3])
339
+ luminance = rgb_to_grayscale(gradient_rgb[0][:, :3])
340
+ gradient_gray = np.vstack((lightness, lightness))
341
+
342
+ # Plotting
343
+ fig = plt.figure(figsize=(20, 10), constrained_layout=True)
344
+ gs = fig.add_gridspec(3, 2)
345
+
346
+ # Define the axes
347
+ ax0 = fig.add_subplot(gs[0, 0]) # Top-left subplot
348
+ ax1 = fig.add_subplot(gs[1, 0]) # Mid-left subplot
349
+ ax2 = fig.add_subplot(gs[2, 0]) # Bottom-left subplot
350
+ ax3 = fig.add_subplot(gs[:, 1]) # Right subplot spanning all rows
351
+
352
+ # Plot RGB gradient on ax0
353
+ ax0.imshow(gradient_rgb, aspect="auto", vmin=0.0, vmax=1.0)
354
+ ax0.set_title("RGB gradient", fontsize=20)
355
+ ax0.axis("off")
356
+
357
+ # Plot Grayscale gradient on ax1
358
+ ax1.imshow(gradient_gray, cmap="gray", aspect="auto", vmin=0.0, vmax=1.0)
359
+ ax1.set_title("Lightness gradient", fontsize=20)
360
+ ax1.axis("off")
361
+
362
+ test_image_path = get_test_img_path()
363
+ colormap_test_img_path = test_image_path / "colourmaptest.tif"
364
+ colormap_test_image = plt.imread(colormap_test_img_path)
365
+ ax2.imshow(colormap_test_image, cmap=colormap)
366
+ ax2.set_title("Colormap test image", fontsize=20)
367
+ ax2.axis("off")
368
+
369
+ # Plot the color channels and luminance on ax2
370
+ for c, color_string in zip(range(3), ["red", "green", "blue"]):
371
+ ax3.plot(gradient_rgb[0, :, c], color=color_string, label=color_string, linewidth=4)
372
+
373
+ ax3.plot(luminance, color="black", linestyle="-.", label="luminance")
374
+ ax3.plot(lightness, color="grey", label="lightness", linewidth=4)
375
+ ax3.set_title("Color channel intensities", fontsize=20)
376
+ ax3.legend(loc=0, fontsize=20)
377
+ plt.show()
@@ -0,0 +1,237 @@
1
+ import matplotlib.colors as mcolors
2
+ from matplotlib import pyplot as plt
3
+ from matplotlib import colormaps
4
+
5
+ import os, sys
6
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
7
+
8
+ import numpy as np
9
+ from typing import Tuple, List
10
+ import argparse
11
+ import cv2
12
+ from scipy.ndimage import gaussian_filter1d
13
+ from pathlib import Path
14
+
15
+ from perfect_cmaps.color_utils import *
16
+ from perfect_cmaps.storage import load_data, get_test_img_path
17
+
18
+
19
+ def get_colormap(
20
+ cmap_name: str,
21
+ n: int = 100,
22
+ ijk: Tuple = None,
23
+ lightness: str = None,
24
+ interpolation: str = "quadratic",
25
+ smoothing: float = None
26
+ ) -> mcolors.LinearSegmentedColormap:
27
+
28
+ """Function for getting custom colormaps.
29
+ Two algorithmically generated colormaps are currently available:
30
+ - 'cold_blooded' a.k.a 'ectotherm'
31
+ - 'copper_salt'
32
+ The 'cold_blooded' map also has variants with completely linear lightness:
33
+ - 'cold_blooded_l', or 'ectotherm_l'.
34
+
35
+ Additionally, several control points in Lab format are also available in the "lab_control_points" folder.
36
+ These are custom generated through the script 'create_custom_cmap', and consist of a collection
37
+ points from 'a' and 'b' channels in Lab, along with a lightness profile.
38
+ These points are used in an optimization task, which optimizes the envelope for maximal expressiveness.
39
+
40
+ You can change the lightness profile in this script by choosing a lightness profile,
41
+ but note that the optimization task might not be feasible for certain lightness profiles.
42
+
43
+ Args:
44
+ cmap_name (str): The name of the colormap, either from the control points folder,
45
+ or the two algorithmically generated colormaps.
46
+ n (int): Number of points in the colormap sequence.
47
+ ijk (Tuple, optional): Order of the channels for the two algorithmic colormaps.
48
+ Typically you would choose a permutation of (0, 1, 2), such as (2, 1, 0).
49
+ Defaults to None, which amounts to the default permutation of the colormaps.
50
+ lightness (str, optional): Lightness profile - only applies for the Lab
51
+ control points. Defaults to None, which means the lightness profile
52
+ the chosen colormap was saved with.
53
+ interpolation (str): Interpolation method for L values. Defaults to 'quadratic.'
54
+ smoothing (float): Gaussian smoothing of colormap in Lab space,
55
+ applied after interpolation. Defaults to None.
56
+
57
+ Returns:
58
+ mcolors.LinearSegmentedColormap: Matplotlib-formatted colormap
59
+ """
60
+ space = np.linspace(0, 1, n)
61
+
62
+ if not cmap_name in CMAP_DICT.keys():
63
+ json_data = load_data(cmap_name)
64
+ control_points = np.array(json_data["points"])
65
+
66
+ if lightness is None:
67
+ lightness = json_data["lightness"]
68
+ else:
69
+ assert lightness in SUPPORTED_L_PROFILES, f"Lightness profile {lightness} not supported"
70
+
71
+ interpolated_values = interpolate_lab(control_points, n, lightness, interpolation)
72
+ if smoothing is not None and smoothing > 0.0:
73
+ sigma = smoothing * n
74
+ interpolated_values[:, 1:] = gaussian_filter1d(interpolated_values[:, 1:], sigma=sigma, axis=0)
75
+
76
+ rgb_values = rgb_renormalized_lightness(interpolated_values, 500, 500)
77
+
78
+ else:
79
+ color_function = CMAP_DICT[cmap_name]
80
+ rgb_values = color_function(space, ijk)
81
+ reoptimize_map = False
82
+
83
+ if color_function in (cold_blooded_l, cold_blooded) and lightness == "linear":
84
+ reoptimize_map = False
85
+
86
+ elif lightness is not None:
87
+ reoptimize_map = True
88
+
89
+ if reoptimize_map:
90
+ lab_values = XYZ_to_Lab(sRGB_to_XYZ(rgb_values[:, :3]))
91
+ lab_values[:, 0] = get_lightness_profile(n, lightness)
92
+ rgb_values, _, _ = rgb_renormalized_lightness(lab_values, 200, 500)
93
+
94
+ cdict = dict()
95
+
96
+ for num, col in enumerate(["red", "green", "blue"]):
97
+ col_list = [[space[i], rgb_values[i][num], rgb_values[i][num]] for i in range(n)]
98
+ cdict[col] = col_list
99
+ cmp = mcolors.LinearSegmentedColormap(cmap_name, segmentdata=cdict, N=n)
100
+ return cmp
101
+
102
+
103
+ def plot_images_with_colormap(image_paths: List[Path], colormap='viridis'):
104
+ # Create a figure with two rows, one for greyscale and one for colormap images
105
+ num_images = len(image_paths)
106
+
107
+ fig, axs = plt.subplots(2, num_images, figsize=(25, 10))
108
+ axs[0, 0].set_ylabel("Grayscale", fontsize=20)
109
+ axs[1, 0].set_ylabel("Custom colormap", fontsize=20)
110
+
111
+ for idx, img_path in enumerate(image_paths):
112
+ # Load the image in greyscale
113
+ img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
114
+ img = img[:int(img.shape[0] * 0.94)]
115
+
116
+ # Display the greyscale image
117
+ axs[0, idx].imshow(img, cmap='gray', vmin=0, vmax=255)
118
+
119
+ # Display the image with the specified colormap
120
+ axs[1, idx].imshow(img, cmap=colormap)
121
+ axs[0, idx].set_xticks([])
122
+ axs[0, idx].set_yticks([])
123
+ axs[1, idx].set_xticks([])
124
+ axs[1, idx].set_yticks([])
125
+
126
+ plt.tight_layout()
127
+ plt.show()
128
+
129
+
130
+ def parse_args():
131
+ arg_parser = argparse.ArgumentParser(add_help=False)
132
+ arg_parser.add_argument(
133
+ "--colormap",
134
+ "-c",
135
+ type=str,
136
+ default="ectotherm_l",
137
+ help="The name of the colormap to be loaded"
138
+ )
139
+ arg_parser.add_argument(
140
+ "--num_points",
141
+ "-n",
142
+ type=int,
143
+ default=1000,
144
+ help="Number of points in the colormap spectrum"
145
+ )
146
+ arg_parser.add_argument(
147
+ "--lightness",
148
+ "-l",
149
+ type=str,
150
+ default=None,
151
+ help="The lightness profile of the colormap. Allowed values are: " +
152
+ "linear, diverging, diverging_inverted, flat."
153
+ )
154
+ arg_parser.add_argument(
155
+ "--interpolation",
156
+ "-i",
157
+ type=str,
158
+ default="quadratic",
159
+ help="Interpolation method for Lab control points"
160
+ )
161
+ arg_parser.add_argument(
162
+ "--ijk",
163
+ type=int,
164
+ nargs='+',
165
+ default=(0, 1, 2),
166
+ help="Channel permutation for algorithmically generated colormaps"
167
+ )
168
+ arg_parser.add_argument(
169
+ "--smoothing",
170
+ "-s",
171
+ type=float,
172
+ default=None,
173
+ help="Gaussian smoothing of colormap in Lab space, applied after interpolation"
174
+ )
175
+ arg_parser.add_argument(
176
+ "--mpl_cmap",
177
+ "-mpl",
178
+ action="store_true",
179
+ default=False,
180
+ help="If specified, looks for a matplotlib colormap instead of one made with this library"
181
+ )
182
+ arg_parser.add_argument(
183
+ "--help",
184
+ "-h",
185
+ action="help",
186
+ default=argparse.SUPPRESS,
187
+ help="Python script for colormap handling with perceptually uniform colormaps. " +
188
+ "Run as a standalone script or import and use the function 'get_colormap' in your own code."
189
+ )
190
+ return arg_parser.parse_args()
191
+
192
+
193
+ CMAP_DICT = {
194
+ "cold_blooded": cold_blooded,
195
+ "cold_blooded_l": cold_blooded_l,
196
+ "ectotherm": cold_blooded,
197
+ "ectotherm_l": cold_blooded_l,
198
+ "copper_salt": copper_salt
199
+ }
200
+
201
+
202
+ """
203
+ If used as a main script, you can try colormaps out on test images,
204
+ and inspect the RGB channel profile of the colormap along with lightness and luminance.
205
+ """
206
+ def main():
207
+ args = parse_args()
208
+
209
+ if args.mpl_cmap:
210
+ cmap = colormaps[args.colormap]
211
+ else:
212
+ cmap = get_colormap(
213
+ args.colormap,
214
+ args.num_points,
215
+ ijk=args.ijk,
216
+ lightness=args.lightness,
217
+ interpolation=args.interpolation,
218
+ smoothing=args.smoothing
219
+ )
220
+
221
+ plot_colormap(cmap, args.num_points)
222
+
223
+ test_images = [
224
+ "charcoal4_49721153188_o-scaled.jpg",
225
+ "Lily-Pollen.png",
226
+ "melamine-foam_49722002717_o-scaled.jpg",
227
+ "PhenomPharos-Gallery_Pyrite.jpg",
228
+ "PhenomXLGallery_5.jpg",
229
+ ]
230
+
231
+ test_image_path = get_test_img_path()
232
+ test_images = [test_image_path / "sem" / img_name for img_name in test_images]
233
+ plot_images_with_colormap(test_images, cmap)
234
+
235
+
236
+ if __name__ == "__main__":
237
+ main()