phasorpy 0.4__cp313-cp313-win_arm64.whl → 0.6__cp313-cp313-win_arm64.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.
@@ -0,0 +1,717 @@
1
+ """Higher level plot functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ 'plot_histograms',
7
+ 'plot_image',
8
+ 'plot_phasor',
9
+ 'plot_phasor_image',
10
+ 'plot_polar_frequency',
11
+ 'plot_signal_image',
12
+ ]
13
+
14
+ import warnings
15
+ from collections.abc import Sequence
16
+ from typing import TYPE_CHECKING
17
+
18
+ if TYPE_CHECKING:
19
+ from .._typing import Any, ArrayLike, NDArray, Literal
20
+
21
+ from matplotlib.axes import Axes
22
+ from matplotlib.image import AxesImage
23
+
24
+ import numpy
25
+ from matplotlib import pyplot
26
+ from matplotlib.gridspec import GridSpec
27
+
28
+ from .._utils import parse_kwargs, parse_signal_axis, update_kwargs
29
+ from ._phasorplot import PhasorPlot
30
+
31
+
32
+ def plot_phasor(
33
+ real: ArrayLike,
34
+ imag: ArrayLike,
35
+ /,
36
+ *,
37
+ style: Literal['plot', 'hist2d', 'contour'] | None = None,
38
+ allquadrants: bool | None = None,
39
+ frequency: float | None = None,
40
+ show: bool = True,
41
+ **kwargs: Any,
42
+ ) -> None:
43
+ """Plot phasor coordinates.
44
+
45
+ A simplified interface to the :py:class:`PhasorPlot` class.
46
+
47
+ Parameters
48
+ ----------
49
+ real : array_like
50
+ Real component of phasor coordinates.
51
+ imag : array_like
52
+ Imaginary component of phasor coordinates.
53
+ Must be of same shape as `real`.
54
+ style : {'plot', 'hist2d', 'contour'}, optional
55
+ Method used to plot phasor coordinates.
56
+ By default, if the number of coordinates are less than 65536
57
+ and the arrays are less than three-dimensional, `'plot'` style is used,
58
+ else `'hist2d'`.
59
+ allquadrants : bool, optional
60
+ Show all quadrants of phasor space.
61
+ By default, only the first quadrant is shown.
62
+ frequency : float, optional
63
+ Frequency of phasor plot.
64
+ If provided, the universal semicircle is labeled with reference
65
+ lifetimes.
66
+ show : bool, optional, default: True
67
+ Display figure.
68
+ **kwargs
69
+ Additional parguments passed to :py:class:`PhasorPlot`,
70
+ :py:meth:`PhasorPlot.plot`, :py:meth:`PhasorPlot.hist2d`, or
71
+ :py:meth:`PhasorPlot.contour` depending on `style`.
72
+
73
+ See Also
74
+ --------
75
+ phasorpy.plot.PhasorPlot
76
+ :ref:`sphx_glr_tutorials_api_phasorpy_phasorplot.py`
77
+
78
+ """
79
+ init_kwargs = parse_kwargs(
80
+ kwargs,
81
+ 'ax',
82
+ 'title',
83
+ 'xlabel',
84
+ 'ylabel',
85
+ 'xlim',
86
+ 'ylim',
87
+ 'xticks',
88
+ 'yticks',
89
+ 'grid',
90
+ )
91
+
92
+ real = numpy.asanyarray(real)
93
+ imag = numpy.asanyarray(imag)
94
+ plot = PhasorPlot(
95
+ frequency=frequency, allquadrants=allquadrants, **init_kwargs
96
+ )
97
+ if style is None:
98
+ style = 'plot' if real.size < 65536 and real.ndim < 3 else 'hist2d'
99
+ if style == 'plot':
100
+ plot.plot(real, imag, **kwargs)
101
+ elif style == 'hist2d':
102
+ plot.hist2d(real, imag, **kwargs)
103
+ elif style == 'contour':
104
+ plot.contour(real, imag, **kwargs)
105
+ else:
106
+ raise ValueError(f'invalid {style=}')
107
+ if show:
108
+ plot.show()
109
+
110
+
111
+ def plot_phasor_image(
112
+ mean: ArrayLike | None,
113
+ real: ArrayLike,
114
+ imag: ArrayLike,
115
+ *,
116
+ harmonics: int | None = None,
117
+ percentile: float | None = None,
118
+ title: str | None = None,
119
+ show: bool = True,
120
+ **kwargs: Any,
121
+ ) -> None:
122
+ """Plot phasor coordinates as images.
123
+
124
+ Preview phasor coordinates from time-resolved or hyperspectral
125
+ image stacks as returned by :py:func:`phasorpy.phasor.phasor_from_signal`.
126
+
127
+ The last two axes are assumed to be the image axes.
128
+ Harmonics, if any, are in the first axes of `real` and `imag`.
129
+ Other axes are averaged for display.
130
+
131
+ Parameters
132
+ ----------
133
+ mean : array_like
134
+ Image average. Must be two or more dimensional, or None.
135
+ real : array_like
136
+ Image of real component of phasor coordinates.
137
+ The last dimensions must match shape of `mean`.
138
+ imag : array_like
139
+ Image of imaginary component of phasor coordinates.
140
+ Must be same shape as `real`.
141
+ harmonics : int, optional
142
+ Number of harmonics to display.
143
+ If `mean` is None, a nonzero value indicates the presence of harmonics
144
+ in the first axes of `mean` and `real`. Else, the presence of harmonics
145
+ is determined from the shapes of `mean` and `real`.
146
+ By default, up to 4 harmonics are displayed.
147
+ percentile : float, optional
148
+ The (q, 100-q) percentiles of image data are covered by colormaps.
149
+ By default, the complete value range of `mean` is covered,
150
+ for `real` and `imag` the range [-1, 1].
151
+ title : str, optional
152
+ Figure title.
153
+ show : bool, optional, default: True
154
+ Display figure.
155
+ **kwargs
156
+ Additional arguments passed to :func:`matplotlib.pyplot.imshow`.
157
+
158
+ Raises
159
+ ------
160
+ ValueError
161
+ The shapes of `mean`, `real`, and `image` do not match.
162
+ Percentile is out of range.
163
+
164
+ """
165
+ update_kwargs(kwargs, interpolation='nearest')
166
+ cmap = kwargs.pop('cmap', None)
167
+ shape = None
168
+
169
+ if mean is not None:
170
+ mean = numpy.asarray(mean)
171
+ if mean.ndim < 2:
172
+ raise ValueError(f'not an image {mean.ndim=} < 2')
173
+ shape = mean.shape
174
+ mean = mean.reshape(-1, *mean.shape[-2:])
175
+ if mean.shape[0] == 1:
176
+ mean = mean[0]
177
+ else:
178
+ mean = numpy.nanmean(mean, axis=0)
179
+
180
+ real = numpy.asarray(real)
181
+ imag = numpy.asarray(imag)
182
+ if real.shape != imag.shape:
183
+ raise ValueError(f'{real.shape=} != {imag.shape=}')
184
+ if real.ndim < 2:
185
+ raise ValueError(f'not an image {real.ndim=} < 2')
186
+
187
+ if (shape is not None and real.shape[1:] == shape) or (
188
+ shape is None and harmonics
189
+ ):
190
+ # first image dimension contains harmonics
191
+ if real.ndim < 3:
192
+ raise ValueError(f'not a multi-harmonic image {real.shape=}')
193
+ nh = real.shape[0] # number harmonics
194
+ elif shape is None or shape == real.shape:
195
+ # single harmonic
196
+ nh = 1
197
+ else:
198
+ raise ValueError(f'shape mismatch {real.shape[1:]=} != {shape}')
199
+
200
+ real = real.reshape(nh, -1, *real.shape[-2:])
201
+ imag = imag.reshape(nh, -1, *imag.shape[-2:])
202
+ if real.shape[1] == 1:
203
+ real = real[:, 0]
204
+ imag = imag[:, 0]
205
+ else:
206
+ real = numpy.nanmean(real, axis=1)
207
+ imag = numpy.nanmean(imag, axis=1)
208
+
209
+ # for MyPy
210
+ assert isinstance(mean, numpy.ndarray) or mean is None
211
+ assert isinstance(real, numpy.ndarray)
212
+ assert isinstance(imag, numpy.ndarray)
213
+
214
+ # limit number of displayed harmonics
215
+ nh = min(4 if harmonics is None else harmonics, nh)
216
+
217
+ # create figure with size depending on image aspect and number of harmonics
218
+ fig = pyplot.figure(layout='constrained')
219
+ w, h = fig.get_size_inches()
220
+ aspect = min(1.0, max(0.5, real.shape[-2] / real.shape[-1]))
221
+ fig.set_size_inches(w, h * 0.4 * aspect * nh + h * 0.25 * aspect)
222
+ gs = GridSpec(nh, 2 if mean is None else 3, figure=fig)
223
+ if title:
224
+ fig.suptitle(title)
225
+
226
+ if mean is not None:
227
+ _imshow(
228
+ fig.add_subplot(gs[0, 0]),
229
+ mean,
230
+ percentile=percentile,
231
+ vmin=None,
232
+ vmax=None,
233
+ cmap=cmap,
234
+ axis=True,
235
+ title='mean',
236
+ **kwargs,
237
+ )
238
+
239
+ if percentile is None:
240
+ vmin = -1.0
241
+ vmax = 1.0
242
+ if cmap is None:
243
+ cmap = 'coolwarm_r'
244
+ else:
245
+ vmin = None
246
+ vmax = None
247
+
248
+ for h in range(nh):
249
+ axs = []
250
+ ax = fig.add_subplot(gs[h, -2])
251
+ axs.append(ax)
252
+ _imshow(
253
+ ax,
254
+ real[h],
255
+ percentile=percentile,
256
+ vmin=vmin,
257
+ vmax=vmax,
258
+ cmap=cmap,
259
+ axis=mean is None and h == 0,
260
+ colorbar=percentile is not None,
261
+ title=None if h else 'G, real',
262
+ **kwargs,
263
+ )
264
+
265
+ ax = fig.add_subplot(gs[h, -1])
266
+ axs.append(ax)
267
+ pos = _imshow(
268
+ ax,
269
+ imag[h],
270
+ percentile=percentile,
271
+ vmin=vmin,
272
+ vmax=vmax,
273
+ cmap=cmap,
274
+ axis=False,
275
+ colorbar=percentile is not None,
276
+ title=None if h else 'S, imag',
277
+ **kwargs,
278
+ )
279
+ if percentile is None and h == 0:
280
+ fig.colorbar(pos, ax=axs, shrink=0.4, location='bottom')
281
+
282
+ if show:
283
+ pyplot.show()
284
+
285
+
286
+ def plot_signal_image(
287
+ signal: ArrayLike,
288
+ /,
289
+ *,
290
+ axis: int | str | None = None,
291
+ percentile: float | Sequence[float] | None = None,
292
+ title: str | None = None,
293
+ xlabel: str | None = None,
294
+ show: bool = True,
295
+ **kwargs: Any,
296
+ ) -> None:
297
+ """Plot average image and signal along axis.
298
+
299
+ Preview time-resolved or hyperspectral image stacks to be anayzed with
300
+ :py:func:`phasorpy.phasor.phasor_from_signal`.
301
+
302
+ The last two axes, excluding `axis`, are assumed to be the image axes.
303
+ Other axes are averaged for image display.
304
+
305
+ Parameters
306
+ ----------
307
+ signal : array_like
308
+ Image stack. Must be three or more dimensional.
309
+ axis : int or str, optional
310
+ Axis over which phasor coordinates would be computed.
311
+ By default, the 'H' or 'C' axes if signal contains such dimension
312
+ names, else the last axis (-1).
313
+ percentile : float or [float, float], optional
314
+ The [q, 100-q] percentiles of image data are covered by colormaps.
315
+ By default, the complete value range of `mean` is covered,
316
+ for `real` and `imag` the range [-1, 1].
317
+ title : str, optional
318
+ Figure title.
319
+ xlabel : str, optional
320
+ Label of axis over which phasor coordinates would be computed.
321
+ show : bool, optional, default: True
322
+ Display figure.
323
+ **kwargs
324
+ Additional arguments passed to :func:`matplotlib.pyplot.imshow`.
325
+
326
+ Raises
327
+ ------
328
+ ValueError
329
+ Signal is not an image stack.
330
+ Percentile is out of range.
331
+
332
+ """
333
+ # TODO: add option to separate channels?
334
+ # TODO: add option to plot non-images?
335
+
336
+ axis, axis_label = parse_signal_axis(signal, axis)
337
+ if (
338
+ axis_label
339
+ and hasattr(signal, 'coords')
340
+ and axis_label in signal.coords
341
+ ):
342
+ axis_coords = signal.coords[axis_label]
343
+ else:
344
+ axis_coords = None
345
+
346
+ update_kwargs(kwargs, interpolation='nearest')
347
+ signal = numpy.asarray(signal)
348
+ if signal.ndim < 3:
349
+ raise ValueError(f'not an image stack {signal.ndim=} < 3')
350
+
351
+ axis %= signal.ndim
352
+
353
+ # for MyPy
354
+ assert isinstance(signal, numpy.ndarray)
355
+
356
+ fig = pyplot.figure(layout='constrained')
357
+ if title:
358
+ fig.suptitle(title)
359
+ w, h = fig.get_size_inches()
360
+ fig.set_size_inches(w, h * 0.7)
361
+ gs = GridSpec(1, 2, figure=fig, width_ratios=(1, 1))
362
+
363
+ # histogram
364
+ axes = list(range(signal.ndim))
365
+ del axes[axis]
366
+ ax = fig.add_subplot(gs[0, 1])
367
+
368
+ if axis_coords is not None:
369
+ ax.set_title(f'{axis=} {axis_label!r}')
370
+ ax.plot(axis_coords, numpy.nanmean(signal, axis=tuple(axes)))
371
+ else:
372
+ ax.set_title(f'{axis=}')
373
+ ax.plot(numpy.nanmean(signal, axis=tuple(axes)))
374
+
375
+ ax.set_ylim(kwargs.get('vmin', None), kwargs.get('vmax', None))
376
+
377
+ if xlabel is not None:
378
+ ax.set_xlabel(xlabel)
379
+
380
+ # image
381
+ axes = list(sorted(axes[:-2] + [axis]))
382
+ ax = fig.add_subplot(gs[0, 0])
383
+ _imshow(
384
+ ax,
385
+ numpy.nanmean(signal, axis=tuple(axes)),
386
+ percentile=percentile,
387
+ shrink=0.5,
388
+ title='mean',
389
+ **kwargs,
390
+ )
391
+
392
+ if show:
393
+ pyplot.show()
394
+
395
+
396
+ def plot_image(
397
+ *images: ArrayLike,
398
+ percentile: float | None = None,
399
+ columns: int | None = None,
400
+ title: str | None = None,
401
+ labels: Sequence[str | None] | None = None,
402
+ show: bool = True,
403
+ **kwargs: Any,
404
+ ) -> None:
405
+ """Plot images.
406
+
407
+ Parameters
408
+ ----------
409
+ *images : array_like
410
+ Images to be plotted. Must be two or more dimensional.
411
+ The last two axes are assumed to be the image axes.
412
+ Other axes are averaged for display.
413
+ Three-dimensional images with last axis size of three or four
414
+ are plotted as RGB(A) images.
415
+ percentile : float, optional
416
+ The (q, 100-q) percentiles of image data are covered by colormaps.
417
+ By default, the complete value range is covered.
418
+ Does not apply to RGB images.
419
+ columns : int, optional
420
+ Number of columns in figure.
421
+ By default, up to four columns are used.
422
+ title : str, optional
423
+ Figure title.
424
+ labels : sequence of str, optional
425
+ Labels for each image.
426
+ show : bool, optional, default: True
427
+ Display figure.
428
+ **kwargs
429
+ Additional arguments passed to :func:`matplotlib.pyplot.imshow`.
430
+
431
+ Raises
432
+ ------
433
+ ValueError
434
+ Percentile is out of range.
435
+
436
+ """
437
+ update_kwargs(
438
+ kwargs, interpolation='nearest', location='right', shrink=0.5
439
+ )
440
+ cmap = kwargs.pop('cmap', None)
441
+ figsize = kwargs.pop('figsize', None)
442
+ subplot_kw = kwargs.pop('subplot_kw', {})
443
+ location = kwargs['location']
444
+ allrgb = True
445
+
446
+ arrays = []
447
+ shape = [1, 1]
448
+ for image in images:
449
+ image = numpy.asarray(image)
450
+ if image.ndim < 2:
451
+ raise ValueError(f'not an image {image.ndim=} < 2')
452
+ if image.ndim == 3 and image.shape[2] in {3, 4}:
453
+ # RGB(A)
454
+ pass
455
+ else:
456
+ allrgb = False
457
+ image = image.reshape(-1, *image.shape[-2:])
458
+ if image.shape[0] == 1:
459
+ image = image[0]
460
+ else:
461
+ with warnings.catch_warnings():
462
+ warnings.filterwarnings('ignore', category=RuntimeWarning)
463
+ image = numpy.nanmean(image, axis=0)
464
+ assert isinstance(image, numpy.ndarray)
465
+ for i in (-1, -2):
466
+ if image.shape[i] > shape[i]:
467
+ shape[i] = image.shape[i]
468
+ arrays.append(image)
469
+
470
+ if columns is None:
471
+ n = len(arrays)
472
+ if n < 3:
473
+ columns = n
474
+ elif n < 5:
475
+ columns = 2
476
+ elif n < 7:
477
+ columns = 3
478
+ else:
479
+ columns = 4
480
+ rows = int(numpy.ceil(len(arrays) / columns))
481
+
482
+ vmin = None
483
+ vmax = None
484
+ if percentile is None:
485
+ vmin = kwargs.pop('vmin', None)
486
+ vmax = kwargs.pop('vmax', None)
487
+ if vmin is None:
488
+ vmin = numpy.inf
489
+ for image in images:
490
+ vmin = min(vmin, numpy.nanmin(image))
491
+ if vmin == numpy.inf:
492
+ vmin = None
493
+ if vmax is None:
494
+ vmax = -numpy.inf
495
+ for image in images:
496
+ vmax = max(vmax, numpy.nanmax(image))
497
+ if vmax == -numpy.inf:
498
+ vmax = None
499
+
500
+ # create figure with size depending on image aspect
501
+ fig = pyplot.figure(layout='constrained', figsize=figsize)
502
+ if figsize is None:
503
+ # TODO: find optimal figure height as a function of
504
+ # number of rows and columns, image shapes, labels, and colorbar
505
+ # presence and placements.
506
+ if allrgb:
507
+ hadd = 0.0
508
+ elif location == 'right':
509
+ hadd = 0.5
510
+ else:
511
+ hadd = 1.2
512
+ if labels is not None:
513
+ hadd += 0.3 * rows
514
+ w, h = fig.get_size_inches()
515
+ aspect = min(1.0, max(0.5, shape[0] / shape[1]))
516
+ fig.set_size_inches(
517
+ w, h * 0.9 / columns * aspect * rows + h * 0.1 * aspect + hadd
518
+ )
519
+ gs = GridSpec(rows, columns, figure=fig)
520
+ if title:
521
+ fig.suptitle(title)
522
+
523
+ axs = []
524
+ for i, image in enumerate(arrays):
525
+ ax = fig.add_subplot(gs[i // columns, i % columns], **subplot_kw)
526
+ ax.set_anchor('C')
527
+ axs.append(ax)
528
+ pos = _imshow(
529
+ ax,
530
+ image,
531
+ percentile=percentile,
532
+ vmin=vmin,
533
+ vmax=vmax,
534
+ cmap=cmap,
535
+ colorbar=percentile is not None,
536
+ axis=i == 0 and not subplot_kw,
537
+ title=None if labels is None else labels[i],
538
+ **kwargs,
539
+ )
540
+ if not allrgb and percentile is None:
541
+ fig.colorbar(pos, ax=axs, shrink=kwargs['shrink'], location=location)
542
+
543
+ if show:
544
+ pyplot.show()
545
+
546
+
547
+ def plot_polar_frequency(
548
+ frequency: ArrayLike,
549
+ phase: ArrayLike,
550
+ modulation: ArrayLike,
551
+ *,
552
+ ax: Axes | None = None,
553
+ title: str | None = None,
554
+ show: bool = True,
555
+ **kwargs: Any,
556
+ ) -> None:
557
+ """Plot phase and modulation verus frequency.
558
+
559
+ Parameters
560
+ ----------
561
+ frequency : array_like, shape (n, )
562
+ Laser pulse or modulation frequency in MHz.
563
+ phase : array_like
564
+ Angular component of polar coordinates in radians.
565
+ modulation : array_like
566
+ Radial component of polar coordinates.
567
+ ax : matplotlib axes, optional
568
+ Matplotlib axes used for plotting.
569
+ By default, a new subplot axes is created.
570
+ title : str, optional
571
+ Figure title. The default is "Multi-frequency plot".
572
+ show : bool, optional, default: True
573
+ Display figure.
574
+ **kwargs
575
+ Additional arguments passed to :py:func:`matplotlib.pyplot.plot`.
576
+
577
+ """
578
+ # TODO: make this customizable: labels, colors, ...
579
+ if ax is None:
580
+ ax = pyplot.subplots()[1]
581
+ if title is None:
582
+ title = 'Multi-frequency plot'
583
+ if title:
584
+ ax.set_title(title)
585
+ ax.set_xscale('log', base=10)
586
+ ax.set_xlabel('Frequency (MHz)')
587
+
588
+ phase = numpy.asarray(phase)
589
+ if phase.ndim < 2:
590
+ phase = phase.reshape(-1, 1)
591
+ modulation = numpy.asarray(modulation)
592
+ if modulation.ndim < 2:
593
+ modulation = modulation.reshape(-1, 1)
594
+
595
+ ax.set_ylabel('Phase (°)', color='tab:blue')
596
+ ax.set_yticks([0.0, 30.0, 60.0, 90.0])
597
+ for phi in phase.T:
598
+ ax.plot(frequency, numpy.rad2deg(phi), color='tab:blue', **kwargs)
599
+ ax = ax.twinx()
600
+
601
+ ax.set_ylabel('Modulation (%)', color='tab:red')
602
+ ax.set_yticks([0.0, 25.0, 50.0, 75.0, 100.0])
603
+ for mod in modulation.T:
604
+ ax.plot(frequency, mod * 100, color='tab:red', **kwargs)
605
+ if show:
606
+ pyplot.show()
607
+
608
+
609
+ def plot_histograms(
610
+ *data: ArrayLike,
611
+ title: str | None = None,
612
+ xlabel: str | None = None,
613
+ ylabel: str | None = None,
614
+ labels: Sequence[str] | None = None,
615
+ show: bool = True,
616
+ **kwargs: Any,
617
+ ) -> None:
618
+ """Plot histograms of flattened data arrays.
619
+
620
+ Parameters
621
+ ----------
622
+ data: array_like
623
+ Data arrays to be plotted as histograms.
624
+ title : str, optional
625
+ Figure title.
626
+ xlabel : str, optional
627
+ Label for x-axis.
628
+ ylabel : str, optional
629
+ Label for y-axis.
630
+ labels: sequence of str, optional
631
+ Labels for each data array.
632
+ show : bool, optional, default: True
633
+ Display figure.
634
+ **kwargs
635
+ Additional arguments passed to :func:`matplotlib.pyplot.hist`.
636
+
637
+ """
638
+ ax = pyplot.subplots()[1]
639
+ if kwargs.get('alpha') is None:
640
+ ax.hist(
641
+ [numpy.asarray(d).flatten() for d in data], label=labels, **kwargs
642
+ )
643
+ else:
644
+ for d, label in zip(
645
+ data, [None] * len(data) if labels is None else labels
646
+ ):
647
+ ax.hist(numpy.asarray(d).flatten(), label=label, **kwargs)
648
+ if title is not None:
649
+ ax.set_title(title)
650
+ if xlabel is not None:
651
+ ax.set_xlabel(xlabel)
652
+ if ylabel is not None:
653
+ ax.set_ylabel(ylabel)
654
+ if labels is not None:
655
+ ax.legend()
656
+ pyplot.tight_layout()
657
+ if show:
658
+ pyplot.show()
659
+
660
+
661
+ def _imshow(
662
+ ax: Axes,
663
+ image: NDArray[Any],
664
+ /,
665
+ *,
666
+ percentile: float | Sequence[float] | None = None,
667
+ vmin: float | None = None,
668
+ vmax: float | None = None,
669
+ colorbar: bool = True,
670
+ shrink: float | None = None,
671
+ axis: bool = True,
672
+ title: str | None = None,
673
+ **kwargs: Any,
674
+ ) -> AxesImage:
675
+ """Plot image array.
676
+
677
+ Convenience wrapper around :py:func:`matplotlib.pyplot.imshow`.
678
+
679
+ """
680
+ update_kwargs(kwargs, interpolation='none')
681
+ location = kwargs.pop('location', 'bottom')
682
+ if image.ndim == 3 and image.shape[2] in {3, 4}:
683
+ # RGB(A)
684
+ vmin = None
685
+ vmax = None
686
+ percentile = None
687
+ colorbar = False
688
+ if percentile is not None:
689
+ if isinstance(percentile, Sequence):
690
+ percentile = percentile[0], percentile[1]
691
+ else:
692
+ # percentile = max(0.0, min(50, percentile))
693
+ percentile = percentile, 100.0 - percentile
694
+ if (
695
+ percentile[0] >= percentile[1]
696
+ or percentile[0] < 0
697
+ or percentile[1] > 100
698
+ ):
699
+ raise ValueError(f'{percentile=} out of range')
700
+ vmin, vmax = numpy.nanpercentile(image, percentile)
701
+ pos = ax.imshow(image, vmin=vmin, vmax=vmax, **kwargs)
702
+ if colorbar:
703
+ if percentile is not None and vmin is not None and vmax is not None:
704
+ ticks = vmin, vmax
705
+ else:
706
+ ticks = None
707
+ fig = ax.get_figure()
708
+ if fig is not None:
709
+ if shrink is None:
710
+ shrink = 0.8
711
+ fig.colorbar(pos, shrink=shrink, location=location, ticks=ticks)
712
+ if title:
713
+ ax.set_title(title)
714
+ if not axis:
715
+ ax.set_axis_off()
716
+ # ax.set_anchor('C')
717
+ return pos