Skip to content

Embed ArrayViewer in a QWidget#

ndv can be embedded in an existing Qt application and enriched with additional elements in a custom layout. The following document shows some examples of such implementation.

Change the content of ArrayViewer via push buttons#

The following script shows an example on how to dynamically select a data set and load it in the ArrayViewer.

examples/cookbook/ndv_embedded.py
# /// script
# dependencies = [
#   "ndv[vispy,pyqt]",
#   "imageio[tifffile]",
# ]
# ///
"""An example on how to embed the `ArrayViewer` controller in a custom Qt widget."""

from qtpy import QtWidgets

from ndv import ArrayViewer, run_app
from ndv.data import astronaut, cat


class EmbeddingWidget(QtWidgets.QWidget):
    def __init__(self) -> None:
        super().__init__()
        self._viewer = ArrayViewer()

        self._cat_button = QtWidgets.QPushButton("Load cat image")
        self._cat_button.clicked.connect(self._load_cat)

        self._astronaut_button = QtWidgets.QPushButton("Load astronaut image")
        self._astronaut_button.clicked.connect(self._load_astronaut)

        btns = QtWidgets.QHBoxLayout()
        btns.addWidget(self._cat_button)
        btns.addWidget(self._astronaut_button)

        layout = QtWidgets.QVBoxLayout(self)
        # `ArrayViewer.widget()` returns the native Qt widget
        layout.addWidget(self._viewer.widget())
        layout.addLayout(btns)

        self._load_cat()

    def _load_cat(self) -> None:
        self._viewer.data = cat().mean(axis=-1)

    def _load_astronaut(self) -> None:
        self._viewer.data = astronaut().mean(axis=-1)


app = QtWidgets.QApplication([])
widget = EmbeddingWidget()
widget.show()
run_app()

Screenshot generated with ndv_embedded Screenshot generated with ndv_embedded

Use multiple ndv.ArrayViewer controllers in the same widget#

The following script shows an example on how to create multiple instances of the ArrayViewer controller in the same widget and load two different datasets in each one.

examples/cookbook/multi_ndv.py
# /// script
# dependencies = [
#   "ndv[vispy,pyqt]",
#   "imageio[tifffile]",
# ]
# ///
"""An example on how to embed multiple `ArrayViewer` controllers in a custom Qt widget.

It shows the `astronaut` and `cells3d` images side by side on two different viewers.
"""

from qtpy import QtWidgets

from ndv import ArrayViewer, run_app
from ndv.data import astronaut, cells3d


class MultiNDVWrapper(QtWidgets.QWidget):
    def __init__(self) -> None:
        super().__init__()

        self._astronaut_viewer = ArrayViewer(astronaut().mean(axis=-1))
        self._cells_virewer = ArrayViewer(cells3d(), current_index={0: 30, 1: 1})

        # get `ArrayViewer` widget and add it to the layout
        layout = QtWidgets.QHBoxLayout(self)
        layout.addWidget(self._astronaut_viewer.widget())
        layout.addWidget(self._cells_virewer.widget())


app = QtWidgets.QApplication([])
widget = MultiNDVWrapper()
widget.show()
run_app()

Screenshot generated with multi_ndv Screenshot generated with multi_ndv

A minimal microscope dashboard using openwfs#

You can use ndv to take an external image source (i.e. a widefield camera) and show its content in real-time in a custom widget embedding ArrayViewer. The script below uses openwfs to generate synthetic images of a sample and continuously update the view, and allows to move the field of view over the X and Y axis.

examples/cookbook/microscope_dashboard.py
# /// script
# dependencies = [
#   "ndv[vispy,pyqt]",
#   "openwfs",
# ]
# ///
"""A minimal microscope dashboard.

The `SyntheticManager` acts as an microscope simulator.
It generates images of a specimen and sends them to the `DashboardWidget`.

The `DashboardWidget` is a simple GUI that displays the images and allows the user to:
- starts/stops the simulation;
- move the stage in the x and y directions.
"""

from typing import Any, cast

import astropy.units as u
import numpy as np
from openwfs.simulation import Camera, Microscope, StaticSource
from qtpy import QtWidgets as QtW
from qtpy.QtCore import QObject, Qt, Signal

from ndv import ArrayViewer


class SyntheticManager(QObject):
    newFrame = Signal(np.ndarray)

    def __init__(self, parent: QObject | None = None) -> None:
        super().__init__(parent)

        specimen_resolution = (1024, 1024)  # (height, width) in pixels of the image
        specimen_pixel_size = 60 * u.nm  # resolution (pixel size) of the specimen image
        magnification = 40  # magnification from object plane to camera.
        numerical_aperture = 0.85  # numerical aperture of the microscope objective
        wavelength = 532.8 * u.nm  # wavelength of the light, for computing diffraction.
        camera_resolution = (256, 256)  # number of pixels on the camera

        img = np.random.randint(-10000, 10, specimen_resolution, dtype=np.int16)
        img = np.maximum(img, 0)
        src = StaticSource(img, pixel_size=specimen_pixel_size)

        self._microscope = Microscope(
            src,
            magnification=magnification,
            numerical_aperture=numerical_aperture,
            wavelength=wavelength,
        )
        self._camera = Camera(
            self._microscope,
            analog_max=None,
            shot_noise=True,
            digital_max=255,
            shape=camera_resolution,
        )

    def move_stage(self, axis: str, step: float) -> None:
        if axis == "x":
            self._microscope.xy_stage.x += step * u.um
        if axis == "y":
            self._microscope.xy_stage.y += step * u.um

    def toggle_simulation(self, start: bool) -> None:
        if start:
            self._timer_id = self.startTimer(100)
        elif hasattr(self, "_timer_id"):
            self.killTimer(self._timer_id)

    def timerEvent(self, e: Any) -> None:
        self.emit_frame()

    def emit_frame(self) -> None:
        self.newFrame.emit(self._camera.read())


class StageWidget(QtW.QGroupBox):
    stageMoved = Signal(str, int)

    def __init__(self, name: str, axes: list[str], parent: QtW.QWidget) -> None:
        super().__init__(name, parent)
        self._data_key = "data"

        def _make_button(txt: str, *data: Any) -> QtW.QPushButton:
            btn = QtW.QPushButton(txt)
            btn.setAutoRepeat(True)
            btn.setProperty(self._data_key, data)
            btn.clicked.connect(self._move_stage)
            return btn

        layout = QtW.QVBoxLayout(self)
        for ax in axes:
            # spinbox showing stage position
            spin = QtW.QDoubleSpinBox()
            spin.setMinimumWidth(80)
            spin.setAlignment(Qt.AlignmentFlag.AlignRight)
            spin.setSuffix(" µm")
            spin.setFocusPolicy(Qt.FocusPolicy.NoFocus)
            spin.setButtonSymbols(QtW.QAbstractSpinBox.ButtonSymbols.NoButtons)

            # buttons to move the stage
            down = _make_button("-", ax, spin)
            up = _make_button("+", ax, spin)

            row = QtW.QHBoxLayout()
            row.addWidget(QtW.QLabel(f"<strong>{ax}:</strong>"), 0)
            row.addWidget(spin, 0, Qt.AlignmentFlag.AlignRight)
            row.addWidget(down, 1)
            row.addWidget(up, 1)
            layout.addLayout(row)

    def _move_stage(self) -> None:
        button = cast(QtW.QPushButton, self.sender())
        ax, spin = button.property(self._data_key)
        step = 1 if button.text() == "+" else -1
        cast(QtW.QDoubleSpinBox, spin).stepBy(step)
        self.stageMoved.emit(ax, step)


class DashboardWidget(QtW.QWidget):
    simulationStarted = Signal(bool)
    stageMoved = Signal(str, int)

    def __init__(self) -> None:
        super().__init__()

        self._stage_widget = StageWidget("Stage", ["x", "y"], self)
        self._stage_widget.setEnabled(False)
        self._stage_widget.stageMoved.connect(self.stageMoved)

        self._viewer = ArrayViewer()
        self._viewer._async = False  # Disable async rendering for simplicity

        self._start_button = QtW.QPushButton("Start")
        self._start_button.setMinimumWidth(120)
        self._start_button.toggled.connect(self.start_simulation)
        self._start_button.setCheckable(True)

        bottom = QtW.QHBoxLayout()
        bottom.setContentsMargins(0, 0, 0, 0)
        bottom.addWidget(self._start_button)
        bottom.addSpacing(120)
        bottom.addWidget(self._stage_widget)

        layout = QtW.QVBoxLayout(self)
        layout.setSpacing(0)
        layout.addWidget(self._viewer.widget(), 1)
        layout.addLayout(bottom)

    def start_simulation(self, checked: bool) -> None:
        self._stage_widget.setEnabled(checked)
        self._start_button.setText("Stop" if checked else "Start")
        self.simulationStarted.emit(checked)

    def set_data(self, frame: np.ndarray) -> None:
        self._viewer.data = frame


app = QtW.QApplication([])
manager = SyntheticManager()
wrapper = DashboardWidget()

manager.newFrame.connect(wrapper.set_data)
wrapper.simulationStarted.connect(manager.toggle_simulation)
wrapper.stageMoved.connect(manager.move_stage)
manager.emit_frame()  # just to populate the viewer with an image
wrapper.show()
app.exec()

Screenshot generated with microscope_dashboard Screenshot generated with microscope_dashboard