Source code for monalisa.display._show_image

"""Python counterpart of src/imDisp/bmImage.m (2D to 5D image viewer).

Not a line-by-line port: MATLAB's bmImage2..5 figures with their keyboard shortcuts are replaced
by napari (https://napari.org), which offers the same functions (slice and frame sliders, views
along x, y or z, contrast, points) plus 3D rendering. In a Jupyter notebook, a light viewer with
sliders is used instead (no window needed).
"""

from __future__ import annotations

import warnings

import numpy as np

from monalisa._mat import iscell, squeeze
from monalisa.array._cell_to_array import cell_to_array
from monalisa.display._qt import NoDisplayError, check_display, in_jupyter

AXIS_LABELS = ("x", "y", "z", "t", "u")


def image_data(argImage) -> np.ndarray:
    """The array displayed by show_image: cells are stacked (one frame per cell, ``cell_to_array``
    then ``squeeze``), complex values become magnitudes and logical values numbers."""
    if iscell(argImage):
        argImage = squeeze(cell_to_array(argImage))
    x = np.asarray(argImage)
    if np.iscomplexobj(x):
        x = np.abs(x)
    if x.dtype == bool:
        x = x.astype(np.float64)
    if x.ndim == 1:
        x = x[:, None]
    while x.ndim > 2 and x.shape[-1] == 1:  # MATLAB drops trailing singleton dimensions
        x = x[..., 0]
    if x.ndim > 5:
        raise ValueError(f"show_image: {x.ndim}-D arrays are not supported (2 to 5)")
    return x


def view_order(ndim: int) -> tuple:
    """napari dimension order showing ``x(:, :, k, ...)`` as MATLAB does: rows x, columns y,
    sliders for z, t, ... (the data keep the MATLAB axis order, so napari coordinates are
    MATLAB indices minus one)."""
    return tuple(range(ndim - 1, 1, -1)) + (0, 1)


VIEWS = ("inline", "window", "3d", "ortho")


[docs] def show_image(argImage, view: str | None = None, *, name: str | None = None, contrast=None, block: bool = False, viewer=None, **old_names): """Show a 2D, 3D, 4D or 5D image (or a cell / list of equally sized frames). Axes keep the MATLAB layout ``(x, y, z, t, ...)``: the slice ``x(:, :, k)`` is shown with x down the rows and y along the columns, as in MATLAB, and z, t have sliders. Complex images are shown as magnitudes. Parameters ---------- argImage : the image, or a list / cell of frames of the same size (stacked along t). view : how to show it: - ``"inline"``: sliders and the slice below the cell (Jupyter only, no window); - ``"window"``: a napari window with one slice view (views along x, y or z, contrast, points; the cube button at its bottom left switches to 3D); - ``"3d"``: the same window, opened in 3D (volume rendering, rotate with the mouse); - ``"ortho"``: one window laid out like ITK-SNAP: the slices along z, x and y and a 3D view, linked by a cursor (click in a slice view to move it). Default: ``"inline"`` in Jupyter, ``"window"`` otherwise. Windows also open from Jupyter, next to the notebook (they need a display). In Jupyter without a display (Docker on macOS or Windows, a remote server), the window views fall back to ``"inline"``, with a warning. name : title of the window and name of the image. contrast : ``(low, high)`` display range; default: the data range. block : in a script, wait until the window is closed. Windows opened from a script close when it ends: pass ``block=True`` on the last call (like ``plt.show()``). Not needed in Jupyter. viewer : add the image to this napari ``Viewer`` (returned by an earlier ``show_image`` call) instead of opening a new window, to compare reconstructions: the images are shown side by side in one row, in the order they were added, with shared sliders (napari grid mode; ``viewer.canvas.grid.shape = (2, 2)`` for another arrangement). The grid button at the bottom left of the window stacks them instead; hide the top one with the eye icon of the layer list to see the one below. Returns ------- The viewer, to keep working with it from code: a napari ``Viewer`` (``"window"``, ``"3d"``; its layers hold the data and any points placed), an ``OrthoViewer`` (``"ortho"``: ``.point``, ``.set_point``) or the notebook viewer (``"inline"``). """ view, contrast = _old_names(view, contrast, old_names) x = image_data(argImage) if view is None: view = "inline" if in_jupyter() and viewer is None else "window" if view not in VIEWS: raise ValueError(f"show_image: unknown view {view!r} (one of {', '.join(VIEWS)})") if viewer is not None and view not in ("window", "3d"): raise ValueError(f"show_image: viewer= adds to a napari window (view 'window' or '3d', not {view!r})") if view in ("3d", "ortho") and x.ndim < 3: raise ValueError(f"show_image: view {view!r} needs a 3D (or 4D, 5D) image, got {x.ndim}D") if view == "inline": return _inline(x, name, contrast) try: check_display("In Jupyter, use view='inline', or save the image and view it elsewhere.") except NoDisplayError as e: # In a notebook without a display (Docker on macOS or Windows, a remote server), show the # image in the notebook instead of failing: the cell still shows something. if not in_jupyter() or viewer is not None: raise reason = str(e).split(". ")[0] warnings.warn(f"show_image: view={view!r} needs a window, which cannot open here ({reason}); " "the image is shown in the notebook instead (view='inline').", stacklevel=2) return _inline(x, name, contrast) if view == "ortho": from monalisa.display._ortho import OrthoViewer return OrthoViewer(x, name=name, contrast_limits=contrast).show(block=block) import napari # creates the Qt application and, in IPython / Jupyter, starts its Qt event loop added = viewer is not None and len(viewer.layers) > 0 if viewer is None: viewer = napari.Viewer(title=name or "monalisa") kw = {} if contrast is None else {"contrast_limits": tuple(contrast)} viewer.add_image(x, name=name or "image", colormap="gray", **kw) if viewer.dims.ndim == x.ndim: viewer.dims.axis_labels = AXIS_LABELS[: x.ndim] viewer.dims.order = view_order(x.ndim) if view == "3d": viewer.dims.ndisplay = 3 if added: # side by side: stacked layers hide each other grid = _grid(viewer) grid.enabled = True grid.shape = (1, -1) # one row, one column per image viewer.title = " | ".join(layer.name for layer in viewer.layers) # left to right if block: napari.run() return viewer
def _inline(x, name, contrast): from monalisa.display._notebook import NotebookViewer w = NotebookViewer(x, name=name, contrast_limits=contrast) w.show() return w def _grid(viewer): """napari's grid settings (``viewer.canvas.grid`` since napari 0.9, ``viewer.grid`` before).""" canvas = getattr(viewer, "canvas", None) return canvas.grid if hasattr(canvas, "grid") else viewer.grid def _old_names(view, contrast, old: dict): """Earlier keywords (backend, render3d, layout, contrast_limits) still work, with a warning.""" if not old: return view, contrast import warnings unknown = set(old) - {"backend", "render3d", "layout", "contrast_limits"} if unknown: raise TypeError(f"show_image() got unexpected keyword arguments: {', '.join(sorted(unknown))}") if old.get("layout") == "orthogonal": new = "ortho" elif old.get("render3d"): new = "3d" else: new = {"notebook": "inline", "napari": "window"}.get(old.get("backend")) if "contrast_limits" in old: contrast = old["contrast_limits"] hint = (f"view={new!r}" if new else "") + (", " if new and "contrast_limits" in old else "") + ( "contrast=(low, high)" if "contrast_limits" in old else "") warnings.warn(f"show_image: {', '.join(sorted(old))} are replaced by: {hint}", FutureWarning, stacklevel=3) return view or new, contrast