Source code for napari_locan.widgets.widget_render_collection_features

"""
Render selected features of all SMLM datasets in a collection.

A QWidget plugin to represent collection features including centroid,
bounding box, oriented bounding box, convex hull and alpha shape.
The SMLM datasets must be kept in a Locdata collection (locdata.references).
"""

from __future__ import annotations

import logging

import locan as lc
import numpy as np
from napari.utils import progress
from napari.viewer import Viewer
from qtpy.QtWidgets import (
    QCheckBox,
    QHBoxLayout,
    QLabel,
    QMessageBox,
    QPushButton,
    QSpinBox,
    QVBoxLayout,
    QWidget,
)

from napari_locan import smlm_data
from napari_locan.data_model.smlm_data import SmlmData

logger = logging.getLogger(__name__)


[docs] class RenderCollectionFeaturesQWidget(QWidget): # type: ignore[misc] def __init__(self, napari_viewer: Viewer, smlm_data: SmlmData = smlm_data): super().__init__() self.viewer = napari_viewer self.smlm_data = smlm_data self._add_size_buttons() self._add_translation_selection() self._add_centroid_check_box() self._add_bounding_box_check_box() self._add_oriented_bounding_box_check_box() self._add_convex_hull_check_box() self._add_alpha_shape_check_box() self._add_render_buttons() self._set_layout() def _add_translation_selection(self) -> None: self._translation_label = QLabel("Translate to common origin:") self._translation_check_box = QCheckBox() self._translation_check_box.setToolTip( "Translate each dataset to centroid = (0, 0)." ) self._translation_selection_layout = QHBoxLayout() self._translation_selection_layout.addWidget(self._translation_label) self._translation_selection_layout.addWidget(self._translation_check_box) def _add_size_buttons(self) -> None: self._size_spin_box_label = QLabel("size:") self._size_spin_box = QSpinBox() self._size_spin_box.setToolTip("Size of rendered shapes.") self._size_spin_box.setRange(1, 2147483647) self._size_spin_box.setValue(100) self._edge_width_spin_box_label = QLabel("width:") self._edge_width_spin_box = QSpinBox() self._edge_width_spin_box.setToolTip("Edge width of rendered shapes.") self._edge_width_spin_box.setRange(1, 2147483647) self._edge_width_spin_box.setValue(100) self._size_buttons_layout = QHBoxLayout() self._size_buttons_layout.addWidget(self._size_spin_box_label) self._size_buttons_layout.addWidget(self._size_spin_box) self._size_buttons_layout.addWidget(self._edge_width_spin_box_label) self._size_buttons_layout.addWidget(self._edge_width_spin_box) def _add_centroid_check_box(self) -> None: self._centroid_label = QLabel("Centroid:") self._centroid_check_box = QCheckBox() self._centroid_check_box.setToolTip("Show centroid of all localizations.") self._centroid_check_box.setChecked(False) self._centroid_layout = QHBoxLayout() self._centroid_layout.addWidget(self._centroid_label) self._centroid_layout.addWidget(self._centroid_check_box) def _add_bounding_box_check_box(self) -> None: self._bounding_box_label = QLabel("Bounding box:") self._bounding_box_check_box = QCheckBox() self._bounding_box_check_box.setToolTip( "Show bounding box of all localizations." ) self._bounding_box_check_box.setChecked(False) self._bounding_box_layout = QHBoxLayout() self._bounding_box_layout.addWidget(self._bounding_box_label) self._bounding_box_layout.addWidget(self._bounding_box_check_box) def _add_oriented_bounding_box_check_box(self) -> None: self._oriented_bounding_box_label = QLabel("Oriented bounding box:") self._oriented_bounding_box_check_box = QCheckBox() self._oriented_bounding_box_check_box.setToolTip( "Show oriented bounding box of all localizations." ) self._oriented_bounding_box_check_box.setChecked(False) self._oriented_bounding_box_layout = QHBoxLayout() self._oriented_bounding_box_layout.addWidget(self._oriented_bounding_box_label) self._oriented_bounding_box_layout.addWidget( self._oriented_bounding_box_check_box ) def _add_convex_hull_check_box(self) -> None: self._convex_hull_label = QLabel("Convex hull:") self._convex_hull_check_box = QCheckBox() self._convex_hull_check_box.setToolTip("Show convex hull of all localizations.") self._convex_hull_check_box.setChecked(False) self._convex_hull_layout = QHBoxLayout() self._convex_hull_layout.addWidget(self._convex_hull_label) self._convex_hull_layout.addWidget(self._convex_hull_check_box) def _add_alpha_shape_check_box(self) -> None: self._alpha_shape_label = QLabel("Alpha shape:") self._alpha_shape_check_box = QCheckBox() self._alpha_shape_check_box.setToolTip("Show alpha shape of all localizations.") self._alpha_shape_check_box.setChecked(False) self._alpha_shape_spin_box_label = QLabel("alpha:") self._alpha_shape_spin_box = QSpinBox() self._alpha_shape_spin_box.setToolTip( "Alpha value to compute specific alpha shape." ) self._alpha_shape_spin_box.setRange(1, 2147483647) self._alpha_shape_spin_box.setValue(100) self._alpha_shape_layout = QHBoxLayout() self._alpha_shape_layout.addWidget(self._alpha_shape_label) self._alpha_shape_layout.addWidget(self._alpha_shape_check_box) self._alpha_shape_layout.addWidget(self._alpha_shape_spin_box_label) self._alpha_shape_layout.addWidget(self._alpha_shape_spin_box) def _add_render_buttons(self) -> None: self._render_button = QPushButton("Render") self._render_button.setToolTip( "Show the selected SMLM data features in new layers." ) self._render_button.clicked.connect(self._render_button_on_click) self._render_as_series_button = QPushButton("Render as series") self._render_as_series_button.setToolTip( "Show series of the selected features for SMLM data collection elements in new layers." ) self._render_as_series_button.clicked.connect( self._render_as_series_button_on_click ) self._render_buttons_layout = QVBoxLayout() self._render_buttons_layout.addWidget(self._render_button) self._render_buttons_layout.addWidget(self._render_as_series_button) def _set_layout(self) -> None: layout = QVBoxLayout() layout.addLayout(self._size_buttons_layout) layout.addLayout(self._translation_selection_layout) layout.addLayout(self._centroid_layout) layout.addLayout(self._bounding_box_layout) layout.addLayout(self._oriented_bounding_box_layout) layout.addLayout(self._convex_hull_layout) layout.addLayout(self._alpha_shape_layout) layout.addLayout(self._render_buttons_layout) self.setLayout(layout) def _render_button_on_click(self) -> None: with progress() as progress_bar: progress_bar.set_description("Rendering...") self._prepare_rendering(as_series=False) def _render_as_series_button_on_click(self) -> None: self._prepare_rendering(as_series=True) def _prepare_collection_for_rendering( self, ) -> lc.LocData: if self.smlm_data.locdata is None: raise ValueError("There is no SMLM data available.") elif bool(self.smlm_data.locdata) is False: raise ValueError("Locdata is empty.") elif self.smlm_data.locdata.references is None or isinstance( self.smlm_data.locdata.references, lc.LocData ): raise TypeError("SMLM data must be a LocData collection.") else: locdata = self.smlm_data.locdata # translation to centroid. if self._translation_check_box.isChecked(): locdata = lc.overlay( locdatas=self.smlm_data.locdata.references, centers="centroid", orientations=None, ) return locdata def _prepare_rendering(self, as_series: bool) -> None: collection = self._prepare_collection_for_rendering() assert collection.references is not None # type narrowing # noqa: S101 if self._centroid_check_box.isChecked(): reference_data = [locdata_.centroid for locdata_ in collection.references] # type: ignore if as_series: img_stack = [ np.insert(reference_, 0, i, axis=0) for i, reference_ in enumerate(reference_data) ] data = np.array(img_stack) else: data = reference_data # type: ignore self.viewer.add_points( data=data, name="centroid", symbol="x", size=self._size_spin_box.value(), ) if self._bounding_box_check_box.isChecked(): try: reference_data = [ locdata_.bounding_box.region.points # type: ignore for locdata_ in collection.references # type: ignore ] if as_series: shapes = [ np.insert(reference_, 0, i, axis=1) for i, reference_ in enumerate(reference_data) ] else: shapes = reference_data # type: ignore self.viewer.add_shapes( shapes, shape_type="polygon", name="bounding_box", edge_width=self._edge_width_spin_box.value(), edge_color="gray", face_color="", ) except NotImplementedError as exception: raise NotImplementedError( "Region not available for plotting." ) from exception if self._oriented_bounding_box_check_box.isChecked(): try: reference_data = [ locdata_.oriented_bounding_box.region.points # type: ignore for locdata_ in collection.references # type: ignore ] if as_series: shapes = [ np.insert(reference_, 0, i, axis=1) for i, reference_ in enumerate(reference_data) ] else: shapes = reference_data # type: ignore self.viewer.add_shapes( shapes, shape_type="polygon", name="oriented_bounding_box", edge_width=self._edge_width_spin_box.value(), edge_color="yellow", face_color="", ) except NotImplementedError as exception: raise NotImplementedError( "Region not available for plotting." ) from exception if self._convex_hull_check_box.isChecked(): try: reference_data = [ locdata_.convex_hull.region.points for locdata_ in collection.references # type: ignore ] if as_series: shapes = [ np.insert(reference_, 0, i, axis=1) for i, reference_ in enumerate(reference_data) ] else: shapes = reference_data # type: ignore self.viewer.add_shapes( shapes, shape_type="polygon", name="convex_hull", edge_width=self._edge_width_spin_box.value(), edge_color="white", face_color="", ) except NotImplementedError as exception: raise NotImplementedError( "Region not available for plotting." ) from exception if ( self._alpha_shape_check_box.isChecked() and self._get_message_feedback() is True ): with progress() as progress_bar: progress_bar.set_description("Processing alpha shape") alpha = self._alpha_shape_spin_box.value() for locdata_ in collection.references: # type: ignore locdata_.update_alpha_shape(alpha) try: reference_data = [ locdata_.alpha_shape.region.points # type: ignore for locdata_ in collection.references # type: ignore ] if as_series: shapes = [ np.insert(reference_, 0, i, axis=1) for i, reference_ in enumerate(reference_data) ] else: shapes = reference_data # type: ignore self.viewer.add_shapes( shapes, shape_type="polygon", name="alpha_shape", edge_width=self._edge_width_spin_box.value(), edge_color="blue", face_color="", ) except NotImplementedError as exception: raise NotImplementedError( "Region not available for plotting." ) from exception def _get_message_feedback(self) -> bool: n_localizations = len(self.smlm_data.locdata) # type: ignore if n_localizations < 10_000: run_computation = True else: msgBox = QMessageBox() msgBox.setText( f"There are {n_localizations} localizations. " f"The alpha shape computation will take some time." ) msgBox.setInformativeText("Do you want to run the computation?") msgBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) # type: ignore[attr-defined] msgBox.setDefaultButton(QMessageBox.Cancel) # type: ignore[attr-defined] return_value = msgBox.exec() run_computation = bool(return_value == QMessageBox.Ok) # type: ignore[attr-defined] return run_computation