mergechannels 0.5.8__cp39-cp39-musllinux_1_2_x86_64.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.
- mergechannels/__init__.py +21 -0
- mergechannels/_blending.py +3 -0
- mergechannels/_internal.py +600 -0
- mergechannels/_luts.py +175 -0
- mergechannels/mergechannels.cpython-39-x86_64-linux-gnu.so +0 -0
- mergechannels/py.typed +0 -0
- mergechannels-0.5.8.dist-info/METADATA +575 -0
- mergechannels-0.5.8.dist-info/RECORD +11 -0
- mergechannels-0.5.8.dist-info/WHEEL +4 -0
- mergechannels-0.5.8.dist-info/licenses/LICENSE +21 -0
- mergechannels.libs/libgcc_s-6d2d9dc8.so.1 +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from ._internal import (
|
|
2
|
+
apply_color_map,
|
|
3
|
+
get_cmap_array,
|
|
4
|
+
get_mpl_cmap,
|
|
5
|
+
merge,
|
|
6
|
+
)
|
|
7
|
+
from ._luts import COLORMAPS
|
|
8
|
+
from .mergechannels import (
|
|
9
|
+
dispatch_multi_channel,
|
|
10
|
+
dispatch_single_channel,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'dispatch_single_channel',
|
|
15
|
+
'dispatch_multi_channel',
|
|
16
|
+
'merge',
|
|
17
|
+
'apply_color_map',
|
|
18
|
+
'get_cmap_array',
|
|
19
|
+
'get_mpl_cmap',
|
|
20
|
+
'COLORMAPS',
|
|
21
|
+
]
|
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import (
|
|
5
|
+
TYPE_CHECKING,
|
|
6
|
+
Sequence,
|
|
7
|
+
Tuple,
|
|
8
|
+
Union,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from ._blending import BLENDING_OPTIONS
|
|
14
|
+
from ._luts import COLORMAPS
|
|
15
|
+
from .mergechannels import ( # type: ignore
|
|
16
|
+
dispatch_multi_channel,
|
|
17
|
+
dispatch_single_channel,
|
|
18
|
+
)
|
|
19
|
+
from .mergechannels import (
|
|
20
|
+
get_cmap_array as _get_cmap_array, # just aliasing this to inject a docstring
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from cmap import Colormap as CmapColormap
|
|
25
|
+
from matplotlib.colors import Colormap as MatplotlibColormap
|
|
26
|
+
from matplotlib.colors import ListedColormap
|
|
27
|
+
from nptyping import (
|
|
28
|
+
NDArray,
|
|
29
|
+
Shape,
|
|
30
|
+
UInt8,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Type alias for mask color specification
|
|
34
|
+
MaskColor = Union[COLORMAPS, Tuple[int, int, int], Sequence[int]]
|
|
35
|
+
|
|
36
|
+
# Default mask color (purple) and alpha
|
|
37
|
+
DEFAULT_MASK_COLOR: Tuple[int, int, int] = (128, 0, 128)
|
|
38
|
+
DEFAULT_MASK_ALPHA: float = 0.5
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_cmap_arguments(
|
|
42
|
+
color: Union[
|
|
43
|
+
COLORMAPS,
|
|
44
|
+
NDArray[Shape['256, 3'], UInt8],
|
|
45
|
+
MatplotlibColormap,
|
|
46
|
+
CmapColormap,
|
|
47
|
+
],
|
|
48
|
+
) -> Tuple[Union[COLORMAPS, None], Union[NDArray[Shape['256, 3'], UInt8], None]]:
|
|
49
|
+
"""
|
|
50
|
+
Parse the color argument and return the corresponding cmap name and cmap values
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
color: a user-specified argument which may be the name of mergechannels colormap,
|
|
54
|
+
a ndarray lookup table, and matplotlib colormap, or a cmap colormap.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
A tuple specifying the corresponding mergechannels colormap name (or None if N/A),
|
|
58
|
+
and an array of the lookup table (or None if N/A)
|
|
59
|
+
"""
|
|
60
|
+
if isinstance(color, str):
|
|
61
|
+
return color, None
|
|
62
|
+
else:
|
|
63
|
+
try: # try to convert from a matplotlib colormap
|
|
64
|
+
if not color._isinit: # type: ignore
|
|
65
|
+
color._init() # type: ignore
|
|
66
|
+
cmap_values = (color._lut[: color.N, :3] * 255).astype('uint8') # type: ignore
|
|
67
|
+
except AttributeError: # try to convert from a cmaps ColorMap
|
|
68
|
+
try:
|
|
69
|
+
cmap_values = (np.asarray(color.lut()[:, :3]) * 255).astype('uint8') # type: ignore
|
|
70
|
+
except AttributeError: # must be a list of lists or an array castable to u8 (256, 3)
|
|
71
|
+
cmap_values = np.asarray(color).astype('uint8') # type: ignore
|
|
72
|
+
|
|
73
|
+
if not (
|
|
74
|
+
isinstance(cmap_values, np.ndarray)
|
|
75
|
+
and cmap_values.shape == (256, 3)
|
|
76
|
+
and cmap_values.dtype == np.uint8
|
|
77
|
+
):
|
|
78
|
+
raise ValueError(
|
|
79
|
+
'Expected a matplotlib colormap, a cmaps colormap, or an object directly castable to '
|
|
80
|
+
f'an 8-bit array of shape (256, 3), got {type(cmap_values)}: {color}'
|
|
81
|
+
)
|
|
82
|
+
return None, cmap_values
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
|
|
86
|
+
"""
|
|
87
|
+
Convert a hex color string to RGB tuple.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
hex_color: Hex color string like '#FF00FF', 'FF00FF', '#f0f', or 'f0f'
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Tuple of (R, G, B) values in range 0-255
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ValueError: If the hex string is invalid
|
|
97
|
+
"""
|
|
98
|
+
# Remove leading '#' if present
|
|
99
|
+
hex_color = hex_color.lstrip('#')
|
|
100
|
+
|
|
101
|
+
# Handle shorthand hex (e.g., 'f0f' -> 'ff00ff')
|
|
102
|
+
if len(hex_color) == 3:
|
|
103
|
+
hex_color = ''.join(c * 2 for c in hex_color)
|
|
104
|
+
|
|
105
|
+
if len(hex_color) != 6:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"Invalid hex color '{hex_color}': expected 3 or 6 hex digits"
|
|
108
|
+
" (with optional '#' prefix)"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Validate hex characters
|
|
112
|
+
if not re.match(r'^[0-9a-fA-F]{6}$', hex_color):
|
|
113
|
+
raise ValueError(f"Invalid hex color '{hex_color}': contains non-hexadecimal characters")
|
|
114
|
+
|
|
115
|
+
r = int(hex_color[0:2], 16)
|
|
116
|
+
g = int(hex_color[2:4], 16)
|
|
117
|
+
b = int(hex_color[4:6], 16)
|
|
118
|
+
return (r, g, b)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _parse_mask_color(color: MaskColor | None) -> Tuple[int, int, int]:
|
|
122
|
+
"""
|
|
123
|
+
Parse a mask color specification into an RGB tuple.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
color: Can be:
|
|
127
|
+
- None: returns default purple (128, 0, 128)
|
|
128
|
+
- A colormap name (str): uses the color at index 255 of that colormap
|
|
129
|
+
- A hex string: '#FF00FF', 'FF00FF', '#f0f', 'f0f'
|
|
130
|
+
- An RGB tuple/sequence: (R, G, B) with values 0-255
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Tuple of (R, G, B) values in range 0-255
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
ValueError: If the color specification is invalid
|
|
137
|
+
"""
|
|
138
|
+
if color is None:
|
|
139
|
+
return DEFAULT_MASK_COLOR
|
|
140
|
+
|
|
141
|
+
if isinstance(color, str):
|
|
142
|
+
# Check if it's a hex color (starts with # or is all hex digits)
|
|
143
|
+
if color.startswith('#') or re.match(r'^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$', color):
|
|
144
|
+
return _hex_to_rgb(color)
|
|
145
|
+
|
|
146
|
+
# Otherwise, treat as a colormap name
|
|
147
|
+
try:
|
|
148
|
+
cmap_array = _get_cmap_array(color)
|
|
149
|
+
# Use the color at index 255 (brightest value in the colormap)
|
|
150
|
+
return tuple(cmap_array[255]) # type: ignore
|
|
151
|
+
except ValueError as e:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f"Invalid mask color '{color}': not a valid hex color or colormap name. "
|
|
154
|
+
f"Hex colors should be like '#FF00FF' or 'f0f'. "
|
|
155
|
+
f'Available colormaps can be found in mergechannels.COLORMAPS.'
|
|
156
|
+
) from e
|
|
157
|
+
|
|
158
|
+
# Must be a sequence of RGB values
|
|
159
|
+
try:
|
|
160
|
+
rgb = tuple(color) # type: ignore
|
|
161
|
+
if len(rgb) != 3:
|
|
162
|
+
raise ValueError(f'Invalid mask color: expected 3 RGB values, got {len(rgb)}')
|
|
163
|
+
r, g, b = rgb
|
|
164
|
+
# Validate range
|
|
165
|
+
for val, name in [(r, 'R'), (g, 'G'), (b, 'B')]:
|
|
166
|
+
if not isinstance(val, (int, np.integer)):
|
|
167
|
+
raise ValueError(
|
|
168
|
+
f'Invalid mask color: {name} value must be an integer, got {type(val).__name__}'
|
|
169
|
+
)
|
|
170
|
+
if not 0 <= val <= 255:
|
|
171
|
+
raise ValueError(f'Invalid mask color: {name} value {val} out of range [0, 255]')
|
|
172
|
+
return (int(r), int(g), int(b))
|
|
173
|
+
except TypeError:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f'Invalid mask color type: expected str, tuple, or sequence, got {type(color).__name__}'
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _validate_mask(
|
|
180
|
+
mask: np.ndarray,
|
|
181
|
+
expected_shape: tuple,
|
|
182
|
+
mask_index: int | None = None,
|
|
183
|
+
) -> None:
|
|
184
|
+
"""
|
|
185
|
+
Validate a mask array.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
mask: The mask array to validate
|
|
189
|
+
expected_shape: Expected shape of the mask (should match the data array shape)
|
|
190
|
+
mask_index: Optional index for error messages when validating multiple masks
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
TypeError: If mask is not a numpy array
|
|
194
|
+
ValueError: If mask shape doesn't match or dtype is invalid
|
|
195
|
+
"""
|
|
196
|
+
idx_str = f' at index {mask_index}' if mask_index is not None else ''
|
|
197
|
+
|
|
198
|
+
if not isinstance(mask, np.ndarray):
|
|
199
|
+
raise TypeError(f'Mask{idx_str} must be a numpy array, got {type(mask).__name__}')
|
|
200
|
+
|
|
201
|
+
if mask.shape != expected_shape:
|
|
202
|
+
raise ValueError(
|
|
203
|
+
f'Mask{idx_str} shape {mask.shape} does not match array shape {expected_shape}'
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if mask.dtype not in (np.bool_, np.int32):
|
|
207
|
+
raise ValueError(f'Mask{idx_str} dtype must be bool or int32, got {mask.dtype}')
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _parse_mask_arguments(
|
|
211
|
+
masks: Sequence[np.ndarray] | np.ndarray | None,
|
|
212
|
+
mask_colors: Sequence[MaskColor] | MaskColor | None,
|
|
213
|
+
mask_alphas: Sequence[float] | float | None,
|
|
214
|
+
expected_shape: tuple,
|
|
215
|
+
) -> Tuple[list[np.ndarray] | None, list[Tuple[int, int, int]] | None, list[float] | None]:
|
|
216
|
+
"""
|
|
217
|
+
Parse and validate mask arguments, handling single values and sequences.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
masks: Single mask array, sequence of masks, or None
|
|
221
|
+
mask_colors: Single color, sequence of colors, or None
|
|
222
|
+
mask_alphas: Single alpha, sequence of alphas, or None
|
|
223
|
+
expected_shape: Expected shape for all masks (should match data array shape)
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Tuple of (masks_list, colors_list, alphas_list) or (None, None, None) if no masks
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
ValueError: If arguments are inconsistent or invalid
|
|
230
|
+
"""
|
|
231
|
+
if masks is None:
|
|
232
|
+
return None, None, None
|
|
233
|
+
|
|
234
|
+
# Normalize masks to a list
|
|
235
|
+
if isinstance(masks, np.ndarray):
|
|
236
|
+
masks_list = [masks]
|
|
237
|
+
else:
|
|
238
|
+
masks_list = list(masks)
|
|
239
|
+
|
|
240
|
+
if len(masks_list) == 0:
|
|
241
|
+
return None, None, None
|
|
242
|
+
|
|
243
|
+
# Validate all masks
|
|
244
|
+
for i, mask in enumerate(masks_list):
|
|
245
|
+
_validate_mask(mask, expected_shape, mask_index=i if len(masks_list) > 1 else None)
|
|
246
|
+
|
|
247
|
+
n_masks = len(masks_list)
|
|
248
|
+
|
|
249
|
+
# Parse colors
|
|
250
|
+
if mask_colors is None:
|
|
251
|
+
colors_list = [DEFAULT_MASK_COLOR] * n_masks
|
|
252
|
+
elif isinstance(mask_colors, (str, tuple)) or (
|
|
253
|
+
isinstance(mask_colors, Sequence)
|
|
254
|
+
and len(mask_colors) == 3
|
|
255
|
+
and isinstance(mask_colors[0], (int, np.integer))
|
|
256
|
+
):
|
|
257
|
+
# Single color specification - apply to all masks
|
|
258
|
+
parsed_color = _parse_mask_color(mask_colors) # type: ignore
|
|
259
|
+
colors_list = [parsed_color] * n_masks
|
|
260
|
+
else:
|
|
261
|
+
# Sequence of colors
|
|
262
|
+
colors_list = [_parse_mask_color(c) for c in mask_colors] # type: ignore
|
|
263
|
+
if len(colors_list) != n_masks:
|
|
264
|
+
raise ValueError(
|
|
265
|
+
f'Number of mask colors ({len(colors_list)}) does not match '
|
|
266
|
+
f'number of masks ({n_masks})'
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Parse alphas
|
|
270
|
+
if mask_alphas is None:
|
|
271
|
+
alphas_list = [DEFAULT_MASK_ALPHA] * n_masks
|
|
272
|
+
elif isinstance(mask_alphas, (int, float)):
|
|
273
|
+
# Single alpha - apply to all masks
|
|
274
|
+
alpha = float(mask_alphas)
|
|
275
|
+
if not 0.0 <= alpha <= 1.0:
|
|
276
|
+
raise ValueError(f'Mask alpha {alpha} out of range [0.0, 1.0]')
|
|
277
|
+
alphas_list = [alpha] * n_masks
|
|
278
|
+
else:
|
|
279
|
+
# Sequence of alphas
|
|
280
|
+
alphas_list = []
|
|
281
|
+
for i, a in enumerate(mask_alphas):
|
|
282
|
+
alpha = float(a)
|
|
283
|
+
if not 0.0 <= alpha <= 1.0:
|
|
284
|
+
raise ValueError(f'Mask alpha at index {i} ({alpha}) out of range [0.0, 1.0]')
|
|
285
|
+
alphas_list.append(alpha)
|
|
286
|
+
if len(alphas_list) != n_masks:
|
|
287
|
+
raise ValueError(
|
|
288
|
+
f'Number of mask alphas ({len(alphas_list)}) does not match '
|
|
289
|
+
f'number of masks ({n_masks})'
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return masks_list, colors_list, alphas_list
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def apply_color_map(
|
|
296
|
+
arr: np.ndarray,
|
|
297
|
+
color: Union[
|
|
298
|
+
COLORMAPS,
|
|
299
|
+
NDArray[Shape['256, 3'], UInt8],
|
|
300
|
+
MatplotlibColormap,
|
|
301
|
+
CmapColormap,
|
|
302
|
+
],
|
|
303
|
+
percentiles: Union[tuple[float, float], None] = None,
|
|
304
|
+
saturation_limits: Union[tuple[float, float], None] = None,
|
|
305
|
+
masks: Sequence[np.ndarray] | np.ndarray | None = None,
|
|
306
|
+
mask_colors: Sequence[MaskColor] | MaskColor | None = None,
|
|
307
|
+
mask_alphas: Sequence[float] | float | None = None,
|
|
308
|
+
parallel: bool = True,
|
|
309
|
+
) -> np.ndarray:
|
|
310
|
+
"""
|
|
311
|
+
Apply a colormap to a grayscale array.
|
|
312
|
+
|
|
313
|
+
Parameters
|
|
314
|
+
----------
|
|
315
|
+
arr : np.ndarray
|
|
316
|
+
Input array of shape (H, W) or (Z, H, W) with dtype uint8 or uint16.
|
|
317
|
+
color : COLORMAPS or NDArray or MatplotlibColormap or CmapColormap
|
|
318
|
+
The colormap to apply. Can be:
|
|
319
|
+
- A built-in colormap name (see mergechannels.COLORMAPS)
|
|
320
|
+
- A (256, 3) uint8 numpy array
|
|
321
|
+
- A matplotlib Colormap object
|
|
322
|
+
- A cmap Colormap object
|
|
323
|
+
percentiles : tuple[float, float] | None, optional
|
|
324
|
+
Percentile values (low, high) for auto-scaling intensity. Ignored if saturation_limits is
|
|
325
|
+
provided. Default is (1.1, 99.9).
|
|
326
|
+
saturation_limits : tuple[float, float] | None, optional
|
|
327
|
+
Explicit intensity limits (low, high) to set the black and white points.
|
|
328
|
+
masks : Sequence[np.ndarray] | np.ndarray | None, optional
|
|
329
|
+
Mask array(s) to overlay on the result. Each mask must have the same shape as the input
|
|
330
|
+
array and dtype of bool or int32. For bool masks, True pixels are overlaid. For int32
|
|
331
|
+
masks, any non-zero value is overlaid.
|
|
332
|
+
mask_colors : Sequence[MaskColor] | MaskColor | None, optional
|
|
333
|
+
Color(s) for the mask overlay. Can be:
|
|
334
|
+
- A colormap name (uses the color at index 255)
|
|
335
|
+
- A hex string ('#FF00FF', 'f0f')
|
|
336
|
+
- An RGB tuple (R, G, B) with values 0-255
|
|
337
|
+
If a single color is provided, it applies to all masks.
|
|
338
|
+
Default is purple (128, 0, 128).
|
|
339
|
+
mask_alphas : Sequence[float] | float | None, optional
|
|
340
|
+
Alpha value(s) for mask blending (0.0-1.0). If a single value is provided, it applies
|
|
341
|
+
to all masks. Default is 0.5.
|
|
342
|
+
parallel : bool, optional
|
|
343
|
+
Whether to use a Rayon threadpool on the Rust side for parallel processing. Default is True.
|
|
344
|
+
|
|
345
|
+
Returns
|
|
346
|
+
-------
|
|
347
|
+
np.ndarray
|
|
348
|
+
RGB array with shape (..., 3) and dtype uint8.
|
|
349
|
+
|
|
350
|
+
Raises
|
|
351
|
+
------
|
|
352
|
+
ValueError
|
|
353
|
+
If the colormap name is not found, color format is invalid, or mask arguments are invalid.
|
|
354
|
+
TypeError
|
|
355
|
+
If masks are not numpy arrays.
|
|
356
|
+
|
|
357
|
+
Examples
|
|
358
|
+
--------
|
|
359
|
+
>>> import mergechannels as mc
|
|
360
|
+
>>> import numpy as np
|
|
361
|
+
>>> arr = np.random.randint(0, 256, (512, 512), dtype=np.uint8)
|
|
362
|
+
>>> rgb = mc.apply_color_map(arr, 'betterBlue', saturation_limits=(0, 255))
|
|
363
|
+
>>> rgb.shape
|
|
364
|
+
(512, 512, 3)
|
|
365
|
+
|
|
366
|
+
With a mask overlay:
|
|
367
|
+
|
|
368
|
+
>>> mask = arr > 200 # Highlight bright pixels
|
|
369
|
+
>>> rgb = mc.apply_color_map(
|
|
370
|
+
... arr, 'Grays', saturation_limits=(0, 255),
|
|
371
|
+
... masks=[mask], mask_colors=['#FF0000'], mask_alphas=[0.5]
|
|
372
|
+
... )
|
|
373
|
+
"""
|
|
374
|
+
if saturation_limits is None:
|
|
375
|
+
if percentiles is None:
|
|
376
|
+
percentiles = (1.1, 99.9)
|
|
377
|
+
low, high = np.percentile(arr, percentiles)
|
|
378
|
+
saturation_limits = (low, high)
|
|
379
|
+
|
|
380
|
+
cmap_name, cmap_values = _parse_cmap_arguments(color)
|
|
381
|
+
|
|
382
|
+
# Parse mask arguments
|
|
383
|
+
masks_list, colors_list, alphas_list = _parse_mask_arguments(
|
|
384
|
+
masks, mask_colors, mask_alphas, expected_shape=arr.shape
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
return dispatch_single_channel(
|
|
388
|
+
array_reference=arr,
|
|
389
|
+
cmap_name=cmap_name,
|
|
390
|
+
cmap_values=cmap_values,
|
|
391
|
+
limits=saturation_limits,
|
|
392
|
+
parallel=parallel,
|
|
393
|
+
mask_arrays=masks_list,
|
|
394
|
+
mask_colors=colors_list,
|
|
395
|
+
mask_alphas=alphas_list,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def merge(
|
|
400
|
+
arrs: Sequence[np.ndarray],
|
|
401
|
+
colors: Sequence[COLORMAPS],
|
|
402
|
+
blending: BLENDING_OPTIONS = 'max',
|
|
403
|
+
percentiles: Sequence[tuple[float, float]] | None = None,
|
|
404
|
+
saturation_limits: Sequence[tuple[float, float]] | None = None,
|
|
405
|
+
masks: Sequence[np.ndarray] | np.ndarray | None = None,
|
|
406
|
+
mask_colors: Sequence[MaskColor] | MaskColor | None = None,
|
|
407
|
+
mask_alphas: Sequence[float] | float | None = None,
|
|
408
|
+
parallel: bool = True,
|
|
409
|
+
) -> np.ndarray:
|
|
410
|
+
"""
|
|
411
|
+
Apply colormaps to multiple arrays and blend them into a single RGB image.
|
|
412
|
+
|
|
413
|
+
Parameters
|
|
414
|
+
----------
|
|
415
|
+
arrs : Sequence[np.ndarray]
|
|
416
|
+
Sequence of input arrays, each with shape (H, W) or (Z, H, W).
|
|
417
|
+
All arrays must have the same shape and dtype (uint8 or uint16).
|
|
418
|
+
colors : Sequence[COLORMAPS]
|
|
419
|
+
Sequence of colormap names or colormap objects, one per input array. Can be built-in names
|
|
420
|
+
(see mergechannels.COLORMAPS), (256, 3) uint8 arrays, matplotlib Colormap objects, or cmap
|
|
421
|
+
Colormap objects.
|
|
422
|
+
blending : BLENDING_OPTIONS, optional
|
|
423
|
+
Blending mode for combining colored channels. One of:
|
|
424
|
+
- 'max': Maximum intensity projection (default)
|
|
425
|
+
- 'sum': Additive blending (clamped to 255)
|
|
426
|
+
- 'min': Minimum intensity projection
|
|
427
|
+
- 'mean': Average of all channels
|
|
428
|
+
percentiles : Sequence[tuple[float, float]] | None, optional
|
|
429
|
+
Per-channel percentile values (low, high) for auto-scaling. Ignored if saturation_limits is
|
|
430
|
+
provided. Default is (1.1, 99.9) for each.
|
|
431
|
+
saturation_limits : Sequence[tuple[float, float]] | None, optional
|
|
432
|
+
Per-channel explicit intensity limits (low, high) for scaling.
|
|
433
|
+
masks : Sequence[np.ndarray] | np.ndarray | None, optional
|
|
434
|
+
Mask array(s) to overlay on the blended result. Each mask must have the same shape as the
|
|
435
|
+
input arrays and dtype of bool or int32. For bool masks, True pixels are overlaid. For int32
|
|
436
|
+
masks, any non-zero value is overlaid.
|
|
437
|
+
mask_colors : Sequence[MaskColor] | MaskColor | None, optional
|
|
438
|
+
Color(s) for the mask overlay. Can be:
|
|
439
|
+
- A colormap name (uses the color at index 255)
|
|
440
|
+
- A hex string ('#FF00FF', 'f0f')
|
|
441
|
+
- An RGB tuple (R, G, B) with values 0-255
|
|
442
|
+
If a single color is provided, it applies to all masks.
|
|
443
|
+
Default is purple (128, 0, 128).
|
|
444
|
+
mask_alphas : Sequence[float] | float | None, optional
|
|
445
|
+
Alpha value(s) for mask blending (0.0-1.0). If a single value is provided, it applies
|
|
446
|
+
to all masks. Default is 0.5.
|
|
447
|
+
parallel : bool, optional
|
|
448
|
+
Whether to use a Rayon threadpool on the Rust side for parallel processing. Default is True.
|
|
449
|
+
|
|
450
|
+
Returns
|
|
451
|
+
-------
|
|
452
|
+
np.ndarray
|
|
453
|
+
Blended RGB array with shape (..., 3) and dtype uint8.
|
|
454
|
+
|
|
455
|
+
Raises
|
|
456
|
+
------
|
|
457
|
+
ValueError
|
|
458
|
+
If a colormap name is not found, color format is invalid, or mask arguments are invalid.
|
|
459
|
+
TypeError
|
|
460
|
+
If masks are not numpy arrays.
|
|
461
|
+
|
|
462
|
+
Examples
|
|
463
|
+
--------
|
|
464
|
+
>>> import mergechannels as mc
|
|
465
|
+
>>> import numpy as np
|
|
466
|
+
>>> ch1 = np.random.randint(0, 256, (512, 512), dtype=np.uint8)
|
|
467
|
+
>>> ch2 = np.random.randint(0, 256, (512, 512), dtype=np.uint8)
|
|
468
|
+
>>> rgb = mc.merge(
|
|
469
|
+
... [ch1, ch2],
|
|
470
|
+
... ['betterBlue', 'betterOrange'],
|
|
471
|
+
... blending='max',
|
|
472
|
+
... saturation_limits=[(0, 255), (0, 255)],
|
|
473
|
+
... )
|
|
474
|
+
>>> rgb.shape
|
|
475
|
+
(512, 512, 3)
|
|
476
|
+
|
|
477
|
+
With a mask overlay:
|
|
478
|
+
|
|
479
|
+
>>> mask = ch1 > 200 # Highlight bright pixels from channel 1
|
|
480
|
+
>>> rgb = mc.merge(
|
|
481
|
+
... [ch1, ch2],
|
|
482
|
+
... ['betterBlue', 'betterOrange'],
|
|
483
|
+
... saturation_limits=[(0, 255), (0, 255)],
|
|
484
|
+
... masks=[mask], mask_colors=[(255, 0, 0)], mask_alphas=[0.5]
|
|
485
|
+
... )
|
|
486
|
+
"""
|
|
487
|
+
cmap_names, cmap_values = zip(*[_parse_cmap_arguments(color) for color in colors])
|
|
488
|
+
if saturation_limits is None:
|
|
489
|
+
if percentiles is None:
|
|
490
|
+
percentiles = [(1.1, 99.9)] * len(arrs)
|
|
491
|
+
saturation_limits = tuple(
|
|
492
|
+
np.percentile(arr, ch_percentiles)
|
|
493
|
+
for arr, ch_percentiles in zip(arrs, percentiles) # type: ignore
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Get expected shape from first array
|
|
497
|
+
expected_shape = arrs[0].shape
|
|
498
|
+
for a in arrs[1:]:
|
|
499
|
+
if not a.shape == expected_shape:
|
|
500
|
+
raise ValueError(
|
|
501
|
+
f'Expected all input arrays to have the same shape, {a.shape} != {expected_shape}'
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
# Parse mask arguments
|
|
505
|
+
masks_list, colors_list, alphas_list = _parse_mask_arguments(
|
|
506
|
+
masks, mask_colors, mask_alphas, expected_shape=expected_shape
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
return dispatch_multi_channel(
|
|
510
|
+
array_references=arrs,
|
|
511
|
+
cmap_names=cmap_names,
|
|
512
|
+
cmap_values=cmap_values,
|
|
513
|
+
blending=blending,
|
|
514
|
+
limits=saturation_limits, # type: ignore
|
|
515
|
+
parallel=parallel,
|
|
516
|
+
mask_arrays=masks_list,
|
|
517
|
+
mask_colors=colors_list,
|
|
518
|
+
mask_alphas=alphas_list,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def get_cmap_array(name: COLORMAPS) -> np.ndarray:
|
|
523
|
+
"""
|
|
524
|
+
Get the RGB values for a built-in colormap.
|
|
525
|
+
|
|
526
|
+
Parameters
|
|
527
|
+
----------
|
|
528
|
+
name : COLORMAPS
|
|
529
|
+
The name of the colormap to retrieve. Use mergechannels.COLORMAPS
|
|
530
|
+
to see available colormap names.
|
|
531
|
+
|
|
532
|
+
Returns
|
|
533
|
+
-------
|
|
534
|
+
np.ndarray
|
|
535
|
+
A (256, 3) uint8 array of RGB values, where each row represents
|
|
536
|
+
the RGB color for that intensity level (0-255).
|
|
537
|
+
|
|
538
|
+
Raises
|
|
539
|
+
------
|
|
540
|
+
ValueError
|
|
541
|
+
If the colormap name is not found.
|
|
542
|
+
|
|
543
|
+
Examples
|
|
544
|
+
--------
|
|
545
|
+
>>> import mergechannels as mc
|
|
546
|
+
>>> cmap = mc.get_cmap_array('betterBlue')
|
|
547
|
+
>>> cmap.shape
|
|
548
|
+
(256, 3)
|
|
549
|
+
>>> cmap.dtype
|
|
550
|
+
dtype('uint8')
|
|
551
|
+
"""
|
|
552
|
+
return _get_cmap_array(name)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def get_mpl_cmap(name: COLORMAPS) -> ListedColormap:
|
|
556
|
+
"""
|
|
557
|
+
Get a built-in colormap as a matplotlib ListedColormap.
|
|
558
|
+
|
|
559
|
+
Parameters
|
|
560
|
+
----------
|
|
561
|
+
name : COLORMAPS
|
|
562
|
+
The name of the colormap to retrieve. Use mergechannels.COLORMAPS
|
|
563
|
+
to see available colormap names.
|
|
564
|
+
|
|
565
|
+
Returns
|
|
566
|
+
-------
|
|
567
|
+
matplotlib.colors.ListedColormap
|
|
568
|
+
A matplotlib ListedColormap object that can be used with matplotlib
|
|
569
|
+
plotting functions.
|
|
570
|
+
|
|
571
|
+
Raises
|
|
572
|
+
------
|
|
573
|
+
ImportError
|
|
574
|
+
If matplotlib is not installed. Install it with:
|
|
575
|
+
``uv pip install matplotlib`` or
|
|
576
|
+
``uv pip install "mergechannels[matplotlib]>=0.5.5"``
|
|
577
|
+
ValueError
|
|
578
|
+
If the colormap name is not found.
|
|
579
|
+
|
|
580
|
+
Examples
|
|
581
|
+
--------
|
|
582
|
+
>>> import mergechannels as mc
|
|
583
|
+
>>> cmap = mc.get_mpl_cmap('betterBlue')
|
|
584
|
+
>>> cmap.name
|
|
585
|
+
'betterBlue'
|
|
586
|
+
>>> import matplotlib.pyplot as plt
|
|
587
|
+
>>> plt.imshow(data, cmap=cmap) # doctest: +SKIP
|
|
588
|
+
"""
|
|
589
|
+
try:
|
|
590
|
+
from matplotlib.colors import ListedColormap
|
|
591
|
+
except ImportError as e:
|
|
592
|
+
raise ImportError(
|
|
593
|
+
'matplotlib is required for get_mpl_cmap(). '
|
|
594
|
+
'Install it with: uv pip install matplotlib '
|
|
595
|
+
'or uv pip install "mergechannels[matplotlib]>=0.5.5"'
|
|
596
|
+
) from e
|
|
597
|
+
|
|
598
|
+
cmap_array = get_cmap_array(name)
|
|
599
|
+
colors = cmap_array / 255.0 # Convert from uint8 (0-255) to float (0-1) for matplotlib
|
|
600
|
+
return ListedColormap(colors, name=name)
|