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