machinevision-toolbox-python 2.2.0__py3-none-any.whl → 2.4.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.
@@ -22,7 +22,7 @@ import numpy as np
22
22
 
23
23
  from machinevisiontoolbox.base.color import colorspace_convert, gamma_decode
24
24
  from machinevisiontoolbox.base.data import mvtb_path_to_datafile
25
- from machinevisiontoolbox.base.types import float_image, int_image
25
+ from machinevisiontoolbox.base.types import DTYPE_ALIASES, float_image, int_image
26
26
 
27
27
  try:
28
28
  import pyclip
@@ -456,470 +456,575 @@ def idisp(
456
456
  # if we are running in a Jupyter notebook, print to matplotlib,
457
457
  # otherwise print to opencv imshow/new window. This is done because
458
458
  # cv2.imshow does not play nicely with .ipynb
459
- if matplotlib: # _isnotebook() and
460
- ## display using matplotlib
461
- plt = _plt()
462
-
463
- # if flatten:
464
- # # either make new subplots for each channel
465
- # # or concatenate all into one large image and display
466
- # # TODO can we make axes as a list?
467
-
468
- # # for now, just concatenate:
469
- # # first check how many channels:
470
- # if im.ndim > 2:
471
- # # create list of image channels
472
- # imcl = [im[:, :, i] for i in range(im.shape[2])]
473
- # # stack horizontally
474
- # im = np.hstack(imcl)
475
- # # else just plot the regular image - only one channel
476
-
477
- if title is None:
478
- title = "Machine Vision Toolbox for Python"
479
-
480
- if len(plt.get_fignums()) == 0:
481
- # there are no figures, create one
482
- fig, ax = plt.subplots() # fig creates a new window
483
- else:
484
- # there are existing figures
485
-
486
- if reuse:
487
- # attempt to reuse an axes, saves all the setup overhead
488
- if ax is None:
489
- ax = plt.gca()
459
+ if matplotlib:
460
+ return _idisp_matplotlib(
461
+ im,
462
+ colororder,
463
+ block=block,
464
+ fps=fps,
465
+ fig=fig,
466
+ ax=ax,
467
+ reuse=reuse,
468
+ colormap=colormap,
469
+ ncolors=ncolors,
470
+ black=black,
471
+ darken=darken,
472
+ powernorm=powernorm,
473
+ gamma=gamma,
474
+ vrange=vrange,
475
+ badcolor=badcolor,
476
+ undercolor=undercolor,
477
+ overcolor=overcolor,
478
+ title=title,
479
+ grid=grid,
480
+ axes=axes,
481
+ axlabels=axlabels,
482
+ gui=gui,
483
+ frame=frame,
484
+ colorbar=colorbar,
485
+ square=square,
486
+ width=width,
487
+ height=height,
488
+ ynormal=ynormal,
489
+ extent=extent,
490
+ coordformat=coordformat,
491
+ savefigname=savefigname,
492
+ **kwargs,
493
+ )
494
+ else:
495
+ return _idisp_opencv(
496
+ im, colororder, title=title, block=block, fps=fps, reuse=reuse
497
+ )
490
498
 
491
- # look for an image in the axes to update, if there is one
492
- updated = False
493
- for c in ax.get_children():
494
- if isinstance(c, mpl.image.AxesImage):
495
- c.set_data(im)
496
- updated = True
497
- if updated:
498
- set_window_title(title)
499
499
 
500
- if fps is not None:
501
- # print("pausing", 1.0 / fps)
502
- plt.pause(1.0 / fps)
500
+ def _idisp_matplotlib(
501
+ im: np.ndarray,
502
+ colororder: str = "RGB",
503
+ block: bool | float | None = None,
504
+ fps: float | None = None,
505
+ fig: Any = None,
506
+ ax: Any = None,
507
+ reuse: bool = False,
508
+ colormap: str | Any = None,
509
+ ncolors: int | None = None,
510
+ black: int | float = 0,
511
+ darken: float | bool | None = None,
512
+ powernorm: bool = False,
513
+ gamma: float | None = None,
514
+ vrange: tuple[float, float] | list[float] | None = None,
515
+ badcolor: str | None = None,
516
+ undercolor: str | None = None,
517
+ overcolor: str | None = None,
518
+ title: str | None = None,
519
+ grid: bool = False,
520
+ axes: bool = True,
521
+ axlabels: tuple[str, str] | list[str] = ("u (pixels)", "v (pixels)"),
522
+ gui: bool = True,
523
+ frame: bool = True,
524
+ colorbar: bool | dict[str, Any] = False,
525
+ square: bool = True,
526
+ width: float | None = None,
527
+ height: float | None = None,
528
+ ynormal: bool = False,
529
+ extent: tuple[float, float, float, float] | list[float] | None = None,
530
+ coordformat: Callable[[float, float], str] | None = None,
531
+ savefigname: str | None = None,
532
+ **kwargs: Any,
533
+ ) -> Any:
534
+ """Matplotlib backend for :func:`idisp`.
503
535
 
504
- safe_plt_show(block=block)
536
+ Not part of the public API -- called from :func:`idisp` when
537
+ ``matplotlib=True``. Split out so each display backend's logic can be
538
+ read, tested and fixed independently of the other; see :func:`idisp`
539
+ for the full parameter documentation.
505
540
 
506
- return
541
+ :return: Matplotlib ``AxesImage`` handle
542
+ :rtype: matplotlib.image.AxesImage
543
+ """
544
+ ## display using matplotlib
545
+ plt = _plt()
507
546
 
508
- if fig is not None:
509
- # make this figure the current one
510
- plt.figure(fig)
547
+ # if flatten:
548
+ # # either make new subplots for each channel
549
+ # # or concatenate all into one large image and display
550
+ # # TODO can we make axes as a list?
551
+
552
+ # # for now, just concatenate:
553
+ # # first check how many channels:
554
+ # if im.ndim > 2:
555
+ # # create list of image channels
556
+ # imcl = [im[:, :, i] for i in range(im.shape[2])]
557
+ # # stack horizontally
558
+ # im = np.hstack(imcl)
559
+ # # else just plot the regular image - only one channel
560
+
561
+ if title is None:
562
+ title = "Machine Vision Toolbox for Python"
563
+
564
+ if len(plt.get_fignums()) == 0:
565
+ # there are no figures, create one
566
+ fig, ax = plt.subplots() # fig creates a new window
567
+ else:
568
+ # there are existing figures
511
569
 
570
+ if reuse:
571
+ # attempt to reuse an axes, saves all the setup overhead
512
572
  if ax is None:
513
- fig, ax = plt.subplots() # fig creates a new window
514
-
515
- if fig is None:
516
- fig = ax.figure
517
-
518
- set_window_title(title)
519
- # aspect ratio:
520
- if not square:
521
- mpl.rcParams["image.aspect"] = "auto"
522
-
523
- # hide interactive toolbar buttons (must be before figure creation)
524
- if not gui:
525
- mpl.rcParams["toolbar"] = "None"
526
-
527
- if darken is True:
528
- darken = 0.5
529
-
530
- # # experiment with addign buttons to the navigation bar
531
- # matplotlib.rcParams["toolbar"] = "toolmanager"
532
- # class LineTool(ToolToggleBase):
533
-
534
- # def trigger(self, *args, **kwargs):
535
- # print('hello from trigger')
536
-
537
- # tm = fig.canvas.manager.toolmanager
538
- # tm.add_tool('newtool', LineTool)
539
- # fig.canvas.manager.toolbar.add_tool(tm.get_tool("newtool"), "toolgroup")
540
-
541
- # get screen resolution:
542
- # swidth, sheight = pyautogui.size() # pixels TODO REPLACE THIS WITH STUFF FROM BDSIM
543
-
544
- # mpl_backend = mpl.get_backend()
545
-
546
- # if mpl_backend == 'Qt5Agg':
547
- # from PyQt5 import QtWidgets
548
- # app = QtWidgets.QApplication([])
549
- # screen = app.primaryScreen()
550
- # if screen.name is not None:
551
- # print(' Screen: %s' % screen.name())
552
- # size = screen.size()
553
- # print(' Size: %d x %d' % (size.width(), size.height()))
554
- # rect = screen.availableGeometry()
555
- # print(' Available: %d x %d' % (rect.width(), rect.height()))
556
- # sw = rect.width()
557
- # sh = rect.height()
558
- # #dpi = screen.physicalDotsPerInch()
559
- # dpiscale = screen.devicePixelRatio() # is 2.0 for Mac laptop screen
560
- # elif mpl_backend == 'TkAgg':
561
- # window = plt.get_current_fig_manager().window
562
- # sw = window.winfo_screenwidth()
563
- # sh = window.winfo_screenheight()
564
- # print(' Size: %d x %d' % (sw, sh))
565
- # else:
566
- # print('unknown backend, can't find width', mpl_backend)
567
-
568
- # dpi = None # can make this an input option
569
- # if dpi is None:
570
- # dpi = mpl.rcParams['figure.dpi'] # default is 100
571
-
572
- if width is not None:
573
- fig.set_figwidth(width / 25.4) # inches
574
-
575
- if height is not None:
576
- fig.set_figheight(height / 25.4) # inches
577
-
578
- ## Create the colormap and normalizer
579
- norm = None
580
- cmap = None
581
- if colormap == "invert":
582
- cmap = "Greys"
583
- elif colormap == "signed":
584
- # signed color map, red is negative, blue is positive, zero is white
585
- cmap = "bwr_r" # blue -> white -> red
586
- min = np.min(im)
587
- max = np.max(im)
588
-
589
- # ensure min/max are symmetric about zero, so that zero is white
590
- if abs(max) >= abs(min):
591
- min = -max # lgtm[py/multiple-definition]
592
- else:
593
- max = -min # lgtm[py/multiple-definition]
573
+ ax = plt.gca()
594
574
 
595
- if powernorm:
596
- norm = mpl.colors.PowerNorm(gamma=0.45)
597
- else:
598
- # if abs(min) > abs(max):
599
- # norm = mpl.colors.Normalize(vmin=min, vmax=abs(min / max) * max)
600
- # else:
601
- # norm = mpl.colors.Normalize(vmin=abs(max / min) * min, vmax=max)
602
- norm = mpl.colors.CenteredNorm()
603
- elif colormap == "invsigned":
604
- # inverse signed color map, red is negative, blue is positive, zero is black
605
- cdict = {
606
- "red": [(0, 1, 1), (0.5, 0, 0), (1, 0, 0)],
607
- "green": [(0, 0, 0), (1, 0, 0)],
608
- "blue": [(0, 0, 0), (0.5, 0, 0), (1, 1, 1)],
609
- }
610
- if ncolors is None:
611
- cmap = mpl.colors.LinearSegmentedColormap("signed", cdict)
612
- else:
613
- cmap = mpl.colors.LinearSegmentedColormap("signed", cdict, N=ncolors)
614
- min = np.min(im)
615
- max = np.max(im)
575
+ # look for an image in the axes to update, if there is one
576
+ updated = False
577
+ for c in ax.get_children():
578
+ if isinstance(c, mpl.image.AxesImage):
579
+ c.set_data(im)
580
+ updated = True
581
+ if updated:
582
+ set_window_title(title)
616
583
 
617
- # ensure min/max are symmetric about zero, so that zero is black
618
- if abs(max) >= abs(min):
619
- min = -max
620
- else:
621
- max = -min
584
+ if fps is not None:
585
+ # print("pausing", 1.0 / fps)
586
+ plt.pause(1.0 / fps)
622
587
 
623
- if powernorm:
624
- norm = mpl.colors.PowerNorm(gamma=0.45)
625
- else:
626
- if abs(min) > abs(max):
627
- norm = mpl.colors.Normalize(vmin=min, vmax=abs(min / max) * max)
628
- else:
629
- norm = mpl.colors.Normalize(vmin=abs(max / min) * min, vmax=max)
630
- elif colormap == "grey":
631
- cmap = "gray"
632
- elif colormap == "random":
633
- x = np.random.rand(256 if ncolors is None else ncolors, 3)
634
- cmap = mpl.colors.LinearSegmentedColormap.from_list("my_colormap", x)
588
+ safe_plt_show(block=block)
589
+
590
+ return
591
+
592
+ if fig is not None:
593
+ # make this figure the current one
594
+ plt.figure(fig)
595
+
596
+ if ax is None:
597
+ fig, ax = plt.subplots() # fig creates a new window
598
+
599
+ if fig is None:
600
+ fig = ax.figure
601
+
602
+ set_window_title(title)
603
+ # aspect ratio:
604
+ if not square:
605
+ mpl.rcParams["image.aspect"] = "auto"
606
+
607
+ # hide interactive toolbar buttons (must be before figure creation)
608
+ if not gui:
609
+ mpl.rcParams["toolbar"] = "None"
610
+
611
+ if darken is True:
612
+ darken = 0.5
613
+
614
+ # # experiment with addign buttons to the navigation bar
615
+ # matplotlib.rcParams["toolbar"] = "toolmanager"
616
+ # class LineTool(ToolToggleBase):
617
+
618
+ # def trigger(self, *args, **kwargs):
619
+ # print('hello from trigger')
620
+
621
+ # tm = fig.canvas.manager.toolmanager
622
+ # tm.add_tool('newtool', LineTool)
623
+ # fig.canvas.manager.toolbar.add_tool(tm.get_tool("newtool"), "toolgroup")
624
+
625
+ # get screen resolution:
626
+ # swidth, sheight = pyautogui.size() # pixels TODO REPLACE THIS WITH STUFF FROM BDSIM
627
+
628
+ # mpl_backend = mpl.get_backend()
629
+
630
+ # if mpl_backend == 'Qt5Agg':
631
+ # from PyQt5 import QtWidgets
632
+ # app = QtWidgets.QApplication([])
633
+ # screen = app.primaryScreen()
634
+ # if screen.name is not None:
635
+ # print(' Screen: %s' % screen.name())
636
+ # size = screen.size()
637
+ # print(' Size: %d x %d' % (size.width(), size.height()))
638
+ # rect = screen.availableGeometry()
639
+ # print(' Available: %d x %d' % (rect.width(), rect.height()))
640
+ # sw = rect.width()
641
+ # sh = rect.height()
642
+ # #dpi = screen.physicalDotsPerInch()
643
+ # dpiscale = screen.devicePixelRatio() # is 2.0 for Mac laptop screen
644
+ # elif mpl_backend == 'TkAgg':
645
+ # window = plt.get_current_fig_manager().window
646
+ # sw = window.winfo_screenwidth()
647
+ # sh = window.winfo_screenheight()
648
+ # print(' Size: %d x %d' % (sw, sh))
649
+ # else:
650
+ # print('unknown backend, can't find width', mpl_backend)
651
+
652
+ # dpi = None # can make this an input option
653
+ # if dpi is None:
654
+ # dpi = mpl.rcParams['figure.dpi'] # default is 100
655
+
656
+ if width is not None:
657
+ fig.set_figwidth(width / 25.4) # inches
658
+
659
+ if height is not None:
660
+ fig.set_figheight(height / 25.4) # inches
661
+
662
+ ## Create the colormap and normalizer
663
+ norm = None
664
+ cmap = None
665
+ if colormap == "invert":
666
+ cmap = "Greys"
667
+ elif colormap == "signed":
668
+ # signed color map, red is negative, blue is positive, zero is white
669
+ cmap = "bwr_r" # blue -> white -> red
670
+ min = np.min(im)
671
+ max = np.max(im)
672
+
673
+ # ensure min/max are symmetric about zero, so that zero is white
674
+ if abs(max) >= abs(min):
675
+ min = -max # lgtm[py/multiple-definition]
635
676
  else:
636
- cmap = colormap
637
-
638
- # choose default grey scale map for non-color image
639
- if cmap is None and len(im.shape) == 2:
640
- cmap = "gray"
641
-
642
- # TODO not sure why exclusion for color, nor why float conversion
643
- if im.ndim == 3 and darken is not None:
644
- im = float_image(im) / darken
645
-
646
- if isinstance(cmap, str):
647
- # cmap = cm.get_cmap(cmap, lut=ncolors)
648
- cmap = mpl.colormaps[cmap]
649
- if ncolors is not None:
650
- cmap = cmap.resampled(ncolors)
651
-
652
- # handle values outside of range
653
- #
654
- # - undercolor, below vmin
655
- # - overcolor, above vmax
656
- # - badcolor, nan, -inf, inf
657
- #
658
- # only works for greyscale image
659
- if im.ndim == 2:
660
- cmap = copy.copy(cmap)
661
- if undercolor is not None:
662
- cmap.set_under(color=undercolor)
663
- if overcolor is not None:
664
- cmap.set_over(color=overcolor)
665
- if badcolor is not None:
666
- cmap.set_bad(color=badcolor)
667
- # elif im.ndim == 3:
668
- # if badcolor is not None:
669
- # cmap.set_bad(color=badcolor)
670
-
671
- if black != 0:
672
- if np.issubdtype(im.dtype, np.floating):
673
- m = 1 - black
674
- c = black
675
- im = m * im + c
676
- norm = mpl.colors.Normalize(0, 1)
677
- elif np.issubdtype(im.dtype, bool):
678
- norm = mpl.colors.Normalize(0, 1)
679
- ncolors = 2
680
- else:
681
- max = np.iinfo(im.dtype).max
682
- black = black * max
683
- c = black
684
- m = (max - c) / max
685
- im = (m * im + c).astype(im.dtype)
686
- norm = mpl.colors.Normalize(0, max)
677
+ max = -min # lgtm[py/multiple-definition]
678
+
679
+ if powernorm:
680
+ norm = mpl.colors.PowerNorm(gamma=0.45)
681
+ else:
682
+ # if abs(min) > abs(max):
683
+ # norm = mpl.colors.Normalize(vmin=min, vmax=abs(min / max) * max)
687
684
  # else:
688
- # # lift the displayed intensity of black pixels.
689
- # # set the greyscale mapping [0,M] to [black,1]
690
- # M = np.max(im)
691
- # norm = mpl.colors.Normalize(-black * M / (1 - black), M)
692
- if darken:
693
- norm = mpl.colors.Normalize(np.min(im), np.max(im) / darken)
694
-
695
- if gamma:
696
- cmap.set_gamma(gamma)
697
-
698
- # print('Colormap is ', cmap)
699
-
700
- # build up options for imshow
701
- options = kwargs
702
- if ynormal:
703
- options["origin"] = "lower"
704
-
705
- if extent is not None:
706
- options["extent"] = extent
707
-
708
- # display the image
709
- if len(im.shape) == 3:
710
- # color image, display in RGB or RGBA order; imshow handles alpha
711
- # natively for both uint8 (0-255) and float (0.0-1.0) arrays
712
- target = "RGBA" if "A" in colororder else "RGB"
713
- idx = [colororder.find(c) for c in target if colororder.find(c) >= 0]
714
- im_rgb = im[:, :, idx] # reorder planes → RGB or RGBA
715
- h = ax.imshow(im_rgb, norm=norm, cmap=cmap, **options)
685
+ # norm = mpl.colors.Normalize(vmin=abs(max / min) * min, vmax=max)
686
+ norm = mpl.colors.CenteredNorm()
687
+ elif colormap == "invsigned":
688
+ # inverse signed color map, red is negative, blue is positive, zero is black
689
+ cdict = {
690
+ "red": [(0, 1, 1), (0.5, 0, 0), (1, 0, 0)],
691
+ "green": [(0, 0, 0), (1, 0, 0)],
692
+ "blue": [(0, 0, 0), (0.5, 0, 0), (1, 1, 1)],
693
+ }
694
+ if ncolors is None:
695
+ cmap = mpl.colors.LinearSegmentedColormap("signed", cdict)
716
696
  else:
717
- # monochrome image, display in indexed mode, where pixel value is mapped through the colormap
718
- if norm is None:
719
- # exclude NaN values
720
- if vrange is None:
721
- min = np.nanmin(im)
722
- max = np.nanmax(im)
723
- else:
724
- min, max = vrange
725
-
726
- if colorbar is not False and ncolors is not None:
727
- # colorbar requested with finite number of colors
728
- # adjust range so that ticks fall in middle of color segment
729
- min -= 0.5
730
- max += 0.5
731
- norm = mpl.colors.Normalize(vmin=min, vmax=max)
732
-
733
- h = ax.imshow(im, norm=norm, cmap=cmap, **options)
734
-
735
- # display the color bar
736
- if colorbar is not False:
737
- ticks = None
738
- labels = None
739
- cbargs = {}
740
- if ncolors:
741
- cbargs["ticks"] = range(ncolors + 1)
742
-
743
- if isinstance(colorbar, dict):
744
- # passed options have priority
745
- if "xticks" in colorbar:
746
- ticks, labels = colorbar["xticks"]
747
- del colorbar["xticks"]
748
- else:
749
- ticks = None
750
- labels = None
751
- cbargs = {**cbargs, **colorbar}
752
-
753
- cb = fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap), ax=ax, **cbargs)
754
- if ticks is not None:
755
- cb.set_ticks(ticks, labels=labels)
756
- # fig.suptitle(title) # slightly different positioning
757
- # ax.set_title(title)
758
-
759
- # hide image axes - by default also removes frame
760
- # back with ax.spines['top'].set_visible(True) ?
761
- if not axes:
762
- ax.axis("off")
763
-
764
- ax.set_xlabel(axlabels[0])
765
- ax.set_ylabel(axlabels[1])
766
-
767
- if grid is not False:
768
- # if grid is True:
769
- # ax.grid(True)
770
- # elif isinstance(grid, str):
771
- ax.grid(color="y", alpha=0.5, linewidth=0.5)
772
-
773
- # no frame:
774
- if not frame:
775
- # NOTE: for frame tweaking, see matplotlib.spines
776
- # https://matplotlib.org/3.3.2/api/spines_api.html
777
- # note: can set spines linewidth:
778
- # ax.spines['top'].set_linewidth(2.0)
779
- ax.spines["top"].set_visible(False)
780
- ax.spines["bottom"].set_visible(False)
781
- ax.spines["left"].set_visible(False)
782
- ax.spines["right"].set_visible(False)
783
-
784
- if savefigname is not None:
785
- # TODO check valid savefigname
786
- # set default save file format
787
- mpl.rcParams["savefig.format"] = "eps"
788
- plt.draw()
789
-
790
- # savefig must be called before plt.show
791
- # after plt.show(), a new fig is automatically created
792
- plt.savefig(savefigname)
793
-
794
- # format the pixel value display
795
- def format_coord(u: float, v: float) -> str:
796
- u = int(u + 0.5)
797
- v = int(v + 0.5)
798
-
799
- try:
800
- if im.ndim == 2:
801
- # monochrome image
802
- x = im[v, u]
803
- if isinstance(x, np.integer):
804
- val = f"{x:d}"
805
- elif isinstance(x, np.floating):
806
- val = f"{x:.3f}"
807
- elif isinstance(x, (np.bool_, bool)):
808
- val = f"{x}"
809
- else:
810
- print(f"unknown pixel type {type(x)}")
811
-
812
- return f"({u}, {v}): {val}"
697
+ cmap = mpl.colors.LinearSegmentedColormap("signed", cdict, N=ncolors)
698
+ min = np.min(im)
699
+ max = np.max(im)
700
+
701
+ # ensure min/max are symmetric about zero, so that zero is black
702
+ if abs(max) >= abs(min):
703
+ min = -max
704
+ else:
705
+ max = -min
706
+
707
+ if powernorm:
708
+ norm = mpl.colors.PowerNorm(gamma=0.45)
709
+ else:
710
+ if abs(min) > abs(max):
711
+ norm = mpl.colors.Normalize(vmin=min, vmax=abs(min / max) * max)
712
+ else:
713
+ norm = mpl.colors.Normalize(vmin=abs(max / min) * min, vmax=max)
714
+ elif colormap == "grey":
715
+ cmap = "gray"
716
+ elif colormap == "random":
717
+ x = np.random.rand(256 if ncolors is None else ncolors, 3)
718
+ cmap = mpl.colors.LinearSegmentedColormap.from_list("my_colormap", x)
719
+ else:
720
+ cmap = colormap
721
+
722
+ # choose default grey scale map for non-color image
723
+ if cmap is None and len(im.shape) == 2:
724
+ cmap = "gray"
725
+
726
+ # TODO not sure why exclusion for color, nor why float conversion
727
+ if im.ndim == 3 and darken is not None:
728
+ im = float_image(im) / darken
729
+
730
+ if isinstance(cmap, str):
731
+ # cmap = cm.get_cmap(cmap, lut=ncolors)
732
+ cmap = mpl.colormaps[cmap]
733
+ if ncolors is not None:
734
+ cmap = cmap.resampled(ncolors)
735
+
736
+ # handle values outside of range
737
+ #
738
+ # - undercolor, below vmin
739
+ # - overcolor, above vmax
740
+ # - badcolor, nan, -inf, inf
741
+ #
742
+ # only works for greyscale image
743
+ if im.ndim == 2:
744
+ cmap = copy.copy(cmap)
745
+ if undercolor is not None:
746
+ cmap.set_under(color=undercolor)
747
+ if overcolor is not None:
748
+ cmap.set_over(color=overcolor)
749
+ if badcolor is not None:
750
+ cmap.set_bad(color=badcolor)
751
+ # elif im.ndim == 3:
752
+ # if badcolor is not None:
753
+ # cmap.set_bad(color=badcolor)
754
+
755
+ if black != 0:
756
+ if np.issubdtype(im.dtype, np.floating):
757
+ m = 1 - black
758
+ c = black
759
+ im = m * im + c
760
+ norm = mpl.colors.Normalize(0, 1)
761
+ elif np.issubdtype(im.dtype, bool):
762
+ norm = mpl.colors.Normalize(0, 1)
763
+ ncolors = 2
764
+ else:
765
+ max = np.iinfo(im.dtype).max
766
+ black = black * max
767
+ c = black
768
+ m = (max - c) / max
769
+ im = (m * im + c).astype(im.dtype)
770
+ norm = mpl.colors.Normalize(0, max)
771
+ # else:
772
+ # # lift the displayed intensity of black pixels.
773
+ # # set the greyscale mapping [0,M] to [black,1]
774
+ # M = np.max(im)
775
+ # norm = mpl.colors.Normalize(-black * M / (1 - black), M)
776
+ if darken:
777
+ norm = mpl.colors.Normalize(np.min(im), np.max(im) / darken)
778
+
779
+ if gamma:
780
+ cmap.set_gamma(gamma)
781
+
782
+ # print('Colormap is ', cmap)
783
+
784
+ # build up options for imshow
785
+ options = kwargs
786
+ if ynormal:
787
+ options["origin"] = "lower"
788
+
789
+ if extent is not None:
790
+ options["extent"] = extent
791
+
792
+ # display the image
793
+ if len(im.shape) == 3:
794
+ # color image, display in RGB or RGBA order; imshow handles alpha
795
+ # natively for both uint8 (0-255) and float (0.0-1.0) arrays
796
+ target = "RGBA" if "A" in colororder else "RGB"
797
+ idx = [colororder.find(c) for c in target if colororder.find(c) >= 0]
798
+ im_rgb = im[:, :, idx] # reorder planes → RGB or RGBA
799
+ h = ax.imshow(im_rgb, norm=norm, cmap=cmap, **options)
800
+ else:
801
+ # monochrome image, display in indexed mode, where pixel value is mapped through the colormap
802
+ if norm is None:
803
+ # exclude NaN values
804
+ if vrange is None:
805
+ min = np.nanmin(im)
806
+ max = np.nanmax(im)
807
+ else:
808
+ min, max = vrange
809
+
810
+ if colorbar is not False and ncolors is not None:
811
+ # colorbar requested with finite number of colors
812
+ # adjust range so that ticks fall in middle of color segment
813
+ min -= 0.5
814
+ max += 0.5
815
+ norm = mpl.colors.Normalize(vmin=min, vmax=max)
816
+
817
+ h = ax.imshow(im, norm=norm, cmap=cmap, **options)
818
+
819
+ # display the color bar
820
+ if colorbar is not False:
821
+ ticks = None
822
+ labels = None
823
+ cbargs = {}
824
+ if ncolors:
825
+ cbargs["ticks"] = range(ncolors + 1)
826
+
827
+ if isinstance(colorbar, dict):
828
+ # passed options have priority
829
+ if "xticks" in colorbar:
830
+ ticks, labels = colorbar["xticks"]
831
+ del colorbar["xticks"]
832
+ else:
833
+ ticks = None
834
+ labels = None
835
+ cbargs = {**cbargs, **colorbar}
836
+
837
+ cb = fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap), ax=ax, **cbargs)
838
+ if ticks is not None:
839
+ cb.set_ticks(ticks, labels=labels)
840
+ # fig.suptitle(title) # slightly different positioning
841
+ # ax.set_title(title)
842
+
843
+ # hide image axes - by default also removes frame
844
+ # back with ax.spines['top'].set_visible(True) ?
845
+ if not axes:
846
+ ax.axis("off")
847
+
848
+ ax.set_xlabel(axlabels[0])
849
+ ax.set_ylabel(axlabels[1])
850
+
851
+ if grid is not False:
852
+ # if grid is True:
853
+ # ax.grid(True)
854
+ # elif isinstance(grid, str):
855
+ ax.grid(color="y", alpha=0.5, linewidth=0.5)
856
+
857
+ # no frame:
858
+ if not frame:
859
+ # NOTE: for frame tweaking, see matplotlib.spines
860
+ # https://matplotlib.org/3.3.2/api/spines_api.html
861
+ # note: can set spines linewidth:
862
+ # ax.spines['top'].set_linewidth(2.0)
863
+ ax.spines["top"].set_visible(False)
864
+ ax.spines["bottom"].set_visible(False)
865
+ ax.spines["left"].set_visible(False)
866
+ ax.spines["right"].set_visible(False)
867
+
868
+ if savefigname is not None:
869
+ # TODO check valid savefigname
870
+ # set default save file format
871
+ mpl.rcParams["savefig.format"] = "eps"
872
+ plt.draw()
873
+
874
+ # savefig must be called before plt.show
875
+ # after plt.show(), a new fig is automatically created
876
+ plt.savefig(savefigname)
877
+
878
+ # format the pixel value display
879
+ def format_coord(u: float, v: float) -> str:
880
+ u = int(u + 0.5)
881
+ v = int(v + 0.5)
882
+
883
+ try:
884
+ if im.ndim == 2:
885
+ # monochrome image
886
+ x = im[v, u]
887
+ if isinstance(x, np.integer):
888
+ val = f"{x:d}"
889
+ elif isinstance(x, np.floating):
890
+ val = f"{x:.3f}"
891
+ elif isinstance(x, (np.bool_, bool)):
892
+ val = f"{x}"
813
893
  else:
814
- # color image
815
- x = im[v, u, :] # in RGB order
816
- if colororder == "BGR":
817
- x = x[::-1]
818
- if np.issubdtype(x.dtype, np.integer):
819
- val = [f"{_:d}" for _ in x]
820
- elif np.issubdtype(x.dtype, np.floating):
821
- val = [f"{_:.3f}" for _ in x]
822
- else:
823
- val = [str(_) for _ in x]
824
- val = "[" + ", ".join(val) + "]"
825
-
826
- return f"({u}, {v}): {val} {colororder}, {x.dtype}"
827
-
828
- except IndexError:
829
- return ""
830
-
831
- def key_press(event: Any) -> None:
832
- if not _pyclip:
833
- return
834
- if pyclip is None:
835
- return
894
+ print(f"unknown pixel type {type(x)}")
836
895
 
837
- if event.inaxes is not None:
838
- u = int(event.xdata + 0.5)
839
- v = int(event.ydata + 0.5)
896
+ return f"({u}, {v}): {val}"
840
897
  else:
841
- return
898
+ # color image
899
+ x = im[v, u, :] # in RGB order
900
+ if colororder == "BGR":
901
+ x = x[::-1]
902
+ if np.issubdtype(x.dtype, np.integer):
903
+ val = [f"{_:d}" for _ in x]
904
+ elif np.issubdtype(x.dtype, np.floating):
905
+ val = [f"{_:.3f}" for _ in x]
906
+ else:
907
+ val = [str(_) for _ in x]
908
+ val = "[" + ", ".join(val) + "]"
842
909
 
843
- if event.key == "c":
844
- # print pixel value at mouse click
845
- pyclip.copy(f"{u},{v}")
846
- elif event.key == "C":
847
- # print pixel value at mouse click
848
- prev = pyclip.paste()
849
- if isinstance(prev, bytes):
850
- prev = prev.decode()
851
- pyclip.copy(f"{prev}\n{u},{v}")
852
- elif event.key == "x":
853
- pyclip.copy("")
854
- elif event.key == "v":
855
- val = im[v, u, ...]
856
- pyclip.copy(f"{u},{v},{tuple(val)}")
857
- elif event.key == "V":
858
- val = im[v, u, ...]
859
- prev = pyclip.paste()
860
- if isinstance(prev, bytes):
861
- prev = prev.decode()
862
- pyclip.copy(f"{prev}\n{u},{v},{tuple(val)}")
863
-
864
- fig.canvas.mpl_connect("key_press_event", key_press)
865
-
866
- if coordformat is None:
867
- ax.format_coord = format_coord
868
- else:
869
- ax.format_coord = coordformat
910
+ return f"({u}, {v}): {val} {colororder}, {x.dtype}"
870
911
 
871
- # don't display data
872
- h.format_cursor_data = lambda data: ""
912
+ except IndexError:
913
+ return ""
873
914
 
874
- if fps is not None:
875
- # print("pausing", 1.0 / fps)
876
- plt.pause(1.0 / fps)
915
+ def key_press(event: Any) -> None:
916
+ if not _pyclip:
917
+ return
918
+ if pyclip is None:
919
+ return
877
920
 
878
- safe_plt_show(block=block)
921
+ if event.inaxes is not None:
922
+ u = int(event.xdata + 0.5)
923
+ v = int(event.ydata + 0.5)
924
+ else:
925
+ return
879
926
 
880
- return h
927
+ if event.key == "c":
928
+ # print pixel value at mouse click
929
+ pyclip.copy(f"{u},{v}")
930
+ elif event.key == "C":
931
+ # print pixel value at mouse click
932
+ prev = pyclip.paste()
933
+ if isinstance(prev, bytes):
934
+ prev = prev.decode()
935
+ pyclip.copy(f"{prev}\n{u},{v}")
936
+ elif event.key == "x":
937
+ pyclip.copy("")
938
+ elif event.key == "v":
939
+ val = im[v, u, ...]
940
+ pyclip.copy(f"{u},{v},{tuple(val)}")
941
+ elif event.key == "V":
942
+ val = im[v, u, ...]
943
+ prev = pyclip.paste()
944
+ if isinstance(prev, bytes):
945
+ prev = prev.decode()
946
+ pyclip.copy(f"{prev}\n{u},{v},{tuple(val)}")
947
+
948
+ fig.canvas.mpl_connect("key_press_event", key_press)
949
+
950
+ if coordformat is None:
951
+ ax.format_coord = format_coord
881
952
  else:
882
- ## display using OpenCV
883
- global __last_window_number
953
+ ax.format_coord = coordformat
954
+
955
+ # don't display data
956
+ h.format_cursor_data = lambda data: ""
957
+
958
+ if fps is not None:
959
+ # print("pausing", 1.0 / fps)
960
+ plt.pause(1.0 / fps)
961
+
962
+ safe_plt_show(block=block)
963
+
964
+ return h
965
+
966
+
967
+ def _idisp_opencv(
968
+ im: np.ndarray,
969
+ colororder: str = "RGB",
970
+ title: str | None = None,
971
+ block: bool | float | None = None,
972
+ fps: float | None = None,
973
+ reuse: bool = False,
974
+ ) -> None:
975
+ """OpenCV (HighGUI) backend for :func:`idisp`.
884
976
 
885
- if not reuse and title is None:
977
+ Not part of the public API -- called from :func:`idisp` when
978
+ ``matplotlib=False``. Split out so each display backend's logic can be
979
+ read, tested and fixed independently of the other; see :func:`idisp`
980
+ for the full parameter documentation.
981
+ """
982
+ ## display using OpenCV
983
+ global __last_window_number
984
+
985
+ if title is None:
986
+ if reuse:
987
+ # fixed name (no counter) so repeated calls in an
988
+ # animation loop share the same window
989
+ title = "idisp"
990
+ else:
886
991
  # create a unique window name for each call
887
992
  title = "idisp." + str(__last_window_number)
888
993
  __last_window_number += 1
889
994
 
890
- # At this point title is guaranteed to be a string
891
- assert title is not None
892
- cv2.namedWindow(title, cv2.WINDOW_AUTOSIZE)
893
-
894
- if im.ndim == 3:
895
- # color image, display in BGR order, but allow user to specify color order of input image
896
- idx = []
897
- for c in "BGR":
898
- j = colororder.find(c)
899
- if j >= 0:
900
- idx.append(j)
901
- cv2.imshow(title, im[:, :, idx]) # convert → BGR order and display
902
- else:
903
- # monochrome image, display in indexed mode, where pixel value is mapped through the colormap
904
- cv2.imshow(title, im) # make sure BGR format image
995
+ # At this point title is guaranteed to be a string
996
+ assert title is not None
997
+ cv2.namedWindow(title, cv2.WINDOW_AUTOSIZE)
998
+
999
+ if im.ndim == 3:
1000
+ # color image, display in BGR order, but allow user to specify color order of input image
1001
+ idx = []
1002
+ for c in "BGR":
1003
+ j = colororder.find(c)
1004
+ if j >= 0:
1005
+ idx.append(j)
1006
+ cv2.imshow(title, im[:, :, idx]) # convert → BGR order and display
1007
+ else:
1008
+ # monochrome image, display in indexed mode, where pixel value is mapped through the colormap
1009
+ cv2.imshow(title, im) # make sure BGR format image
905
1010
 
906
- cv2.waitKey(1)
1011
+ cv2.waitKey(1)
907
1012
 
908
- if fps is not None:
909
- # wait one frame time
910
- cv2.waitKey(round(1000.0 / fps))
1013
+ if fps is not None:
1014
+ # wait one frame time
1015
+ cv2.waitKey(round(1000.0 / fps))
911
1016
 
912
- if block is True:
913
- while True:
914
- k = cv2.waitKey(delay=0) # wait forever for keystroke
915
- if k == ord("q"):
916
- assert title is not None
917
- cv2.destroyWindow(title)
918
- cv2.waitKey(1)
919
- break
1017
+ if block is True:
1018
+ while True:
1019
+ k = cv2.waitKey(delay=0) # wait forever for keystroke
1020
+ if k == ord("q"):
1021
+ assert title is not None
1022
+ cv2.destroyWindow(title)
1023
+ cv2.waitKey(1)
1024
+ break
920
1025
 
921
- # TODO fig, ax equivalent for OpenCV? how to print/plot to the same
922
- # window/set of axes?
1026
+ # TODO fig, ax equivalent for OpenCV? how to print/plot to the same
1027
+ # window/set of axes?
923
1028
 
924
1029
 
925
1030
  def set_window_title(title: str) -> None:
@@ -1176,7 +1281,7 @@ def convert(
1176
1281
  :type grey: bool or 'ITU601' [default] or 'ITU709'
1177
1282
  :param gray: synonym for ``grey``
1178
1283
  :param dtype: a NumPy dtype string such as ``"uint8"``, ``"int16"``, ``"float32"`` or
1179
- a NumPy type like ``np.uint8``.
1284
+ a NumPy type like ``np.uint8``. |dtype_aliases|
1180
1285
  :type dtype: str
1181
1286
  :param rgb: force color image to RGB order, otherwise BGR
1182
1287
  :type rgb: bool, optional
@@ -1250,17 +1355,10 @@ def convert(
1250
1355
  if mono and len(image.shape) == 3:
1251
1356
  image = colorspace_convert(image, colororder, "grey")
1252
1357
 
1253
- dtype_alias = {
1254
- "int": "uint8",
1255
- "float": "float32",
1256
- "double": "float64",
1257
- "half": "float16",
1258
- }
1259
-
1260
1358
  if dtype is not None:
1261
1359
  # default types
1262
1360
  if isinstance(dtype, str):
1263
- dtype = dtype_alias.get(dtype, dtype)
1361
+ dtype = DTYPE_ALIASES.get(dtype, dtype)
1264
1362
 
1265
1363
  if "int" in str(dtype):
1266
1364
  image = int_image(image, intclass=dtype, maxintval=maxintval)