diff --git a/docs/02_interactive_usage/02_example_workflows/demo_fit_ellipsoid_interactive.md b/docs/02_interactive_usage/02_example_workflows/demo_fit_ellipsoid_interactive.md index 3e598e1f..ca739574 100644 --- a/docs/02_interactive_usage/02_example_workflows/demo_fit_ellipsoid_interactive.md +++ b/docs/02_interactive_usage/02_example_workflows/demo_fit_ellipsoid_interactive.md @@ -47,7 +47,7 @@ Lastly, you can measure mean curvature on the surface of this ellipse. To do so, ![](imgs/demo_fit_ellipsoid7.png) -You can also use the built-in [feature visualization widget](utility:visualize_features) to show, for instance, a hiostogram of the curvature on the surface. +You can also use [napari-matplotlib](https://napari-matplotlib.github.io/index.html) [feature visualization widget](point_and_click:visualize_features) to show, for instance, a hiostogram of the curvature on the surface. (fit_ellipsoid:normal_vectors)= ## Distances between pointcloud and approximation diff --git a/docs/02_interactive_usage/02_example_workflows/demo_visualize_features.md b/docs/02_interactive_usage/02_example_workflows/demo_visualize_features.md deleted file mode 100644 index 50005839..00000000 --- a/docs/02_interactive_usage/02_example_workflows/demo_visualize_features.md +++ /dev/null @@ -1,48 +0,0 @@ -(point_and_click:visualize_features)= -(utility:visualize_features)= -# Visualize measurements - -Napari-stress offers functionality to visualize measured data interactively in the napari viewer. This tutorial provides guidance on how to use these. - -* [Visualizing features](#visualize-features) -* [Export data](#export-data) - -## Sample data - -To get started, create a pointcloud according to the workflow suggestions in this repository or load the sample data from napari-stress (`File > Open Sample > napari-stress: Droplet pointcloud`). - -![](imgs/open_sample_droplet.png) - -![](imgs/open_sample_droplet1.png) - -Create a spherical harmonics expansion with `Tools > Points > Fit spherical harmonics (n-STRESS)`: - -![](imgs/demo_visualize_features1.png) - - -## Visualize features - -`Features` in the napari ecosystem are measurements that are assigned to a single Point (`Points` layer), label (`Labels` layer), or Surface vertex (`Surface` layer). In the context of napari-stress, such measurements include point-wise spherical harmonics expansion errors, curvature, etc. To do so for the created sample data, open the widget for this from `Tools > Utilities > Visualize pointcloud features (n-Stress)`. - -![](imgs/demo_visualize_features2.png) - -In the dropdown labelled `x axis key`, you'll see all available measurements for the selected layer - in this case, the feature to be visualized is called `error` and corresponds to the fit residue of the spherical harmonics expansion. By changing the number of bins (`n bins`) and clicking on `Update` you can change the number of bins of the histogram and apply the changes: - -![](imgs/demo_visualize_features3.png) - -You can additionally show the [cumulative distribution function ](https://en.wikipedia.org/wiki/Cumulative_distribution_function) (CDF) by clicking on the `CDF` button: - -![](imgs/demo_visualize_features4.png) - -Lastly, to explore the histogram/CDF distributions interactively, you can select parts of the histogram by drawing a rectangular selection on the plot: - -![](imgs/demo_visualize_features5.png) - -Note that the points pertaining to features in the respective range are highlighted in the viewer. You can also change the range of the selection by using the `Upper percentile` and `Lower percentile` spinboxes. - - -To plot a different feature, select it from the dropdown and click `Update` to apply. - -## Export data - -Lastly, you can export the displayed (histogram) data as a `.csv` file using the `Export plot as csv` button. \ No newline at end of file diff --git a/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_featureHistogram.png b/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_featureHistogram.png new file mode 100644 index 00000000..dabac853 Binary files /dev/null and b/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_featureHistogram.png differ diff --git a/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_featureHistogram2.png b/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_featureHistogram2.png new file mode 100644 index 00000000..857d492c Binary files /dev/null and b/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_featureHistogram2.png differ diff --git a/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_features1.png b/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_features1.png deleted file mode 100644 index 413a16fe..00000000 Binary files a/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_features1.png and /dev/null differ diff --git a/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_features2.png b/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_features2.png deleted file mode 100644 index 8ca7e425..00000000 Binary files a/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_features2.png and /dev/null differ diff --git a/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_features3.png b/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_features3.png deleted file mode 100644 index cf93c1c5..00000000 Binary files a/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_features3.png and /dev/null differ diff --git a/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_features4.png b/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_features4.png deleted file mode 100644 index c2b9423f..00000000 Binary files a/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_features4.png and /dev/null differ diff --git a/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_features5.png b/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_features5.png deleted file mode 100644 index 20b93c9c..00000000 Binary files a/docs/02_interactive_usage/02_example_workflows/imgs/demo_visualize_features5.png and /dev/null differ diff --git a/docs/02_interactive_usage/02_example_workflows/visualize_measurements_in_viewer.md b/docs/02_interactive_usage/02_example_workflows/visualize_measurements_in_viewer.md new file mode 100644 index 00000000..862ed305 --- /dev/null +++ b/docs/02_interactive_usage/02_example_workflows/visualize_measurements_in_viewer.md @@ -0,0 +1,12 @@ +(point_and_click:visualize_features)= +# Visualize measurements + +Napari-stress (and the [measurement toolbox](toolboxes:stress_toolbox:stress_toolbox_interactive), in partciular) generate a number of measurements that can be visualized using the [napari-matplotlib](https://napari-matplotlib.github.io/) plugin. It is automatically installed with napari-stress and can be activated from the plugins menu (`Plugins > napari Matplotlib`). + +In order to use it, check out the documentation on the [napari-matplotlib page](https://napari-matplotlib.github.io/). In the scope of napari-stress, what you will need, is the Features Histogram (`Plugins > napari Matplotlib > FeaturesHistogram`). + +![](./imgs/demo_visualize_featureHistogram.png) + +To display measurements, select a layer of interest and the dropdown in the newly created Histogram widget will show all possible features for you to highlight. + +![](./imgs/demo_visualize_featureHistogram2.png) \ No newline at end of file diff --git a/docs/_toc.yml b/docs/_toc.yml index 38903665..7231e634 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -27,6 +27,7 @@ parts: - file: 01_code_usage/02_example_workflows/demo_spherical_harmonics_code - file: 01_code_usage/02_example_workflows/demo_timelapse_processing - file: 01_code_usage/02_example_workflows/demo_measure_intensity_on_surface + - file: 01_code_usage/02_example_workflows/demo_measure_intensity_on_vectors - file: 02_interactive_usage/interactive_usage sections: @@ -41,7 +42,7 @@ parts: - file: 02_interactive_usage/02_example_workflows/demo_surface_reconstruction_interactive - file: 02_interactive_usage/02_example_workflows/demo_spherical_harmonics_interactive - file: 02_interactive_usage/02_example_workflows/demo_measure_curvature - - file: 02_interactive_usage/02_example_workflows/demo_visualize_features + - file: 02_interactive_usage/02_example_workflows/visualize_measurements_in_viewer - caption: Glossary chapters: diff --git a/setup.cfg b/setup.cfg index 50917df0..ff608532 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,10 +34,9 @@ install_requires = dask distributed joblib - matplotlib mpmath napari - napari-matplotlib==0.2.0 + napari-matplotlib>=1.1.0 napari-process-points-and-surfaces>=0.4.0 napari-segment-blobs-and-things-with-membranes napari-tools-menu>=0.1.15 diff --git a/src/napari_stress/__init__.py b/src/napari_stress/__init__.py index 7fcf1403..949ffe32 100644 --- a/src/napari_stress/__init__.py +++ b/src/napari_stress/__init__.py @@ -24,7 +24,5 @@ from ._sample_data.sample_data import get_droplet_point_cloud, get_droplet_point_cloud_4d, get_droplet_4d -from ._plotting.features_histogram import FeaturesHistogramWidget - from . import types from . import _vectors as vectors diff --git a/src/napari_stress/_plotting/features_histogram.py b/src/napari_stress/_plotting/features_histogram.py deleted file mode 100644 index 78b274cc..00000000 --- a/src/napari_stress/_plotting/features_histogram.py +++ /dev/null @@ -1,336 +0,0 @@ -import napari -from magicgui import magicgui -import numpy as np -import pandas as pd -from napari_matplotlib import HistogramWidget -from napari_matplotlib.util import Interval -from matplotlib.widgets import RectangleSelector -from matplotlib.patches import Rectangle -from magicgui.widgets import ComboBox -from typing import List, Optional, Tuple -from qtpy.QtWidgets import (QFileDialog, QHBoxLayout, - QPushButton, QDoubleSpinBox, - QSpacerItem, QSizePolicy, QGridLayout, - QLabel) -from scipy import stats -from napari_tools_menu import register_dock_widget - -@register_dock_widget(menu="Visualization > Visualize pointcloud features (n-Stress)") -class FeaturesHistogramWidget(HistogramWidget): - """Plot widget to display histogram of selected layer features.""" - - n_layers_input = Interval(1, 1) - # All layers that have a .features attributes - input_layer_types = ( - napari.layers.Labels, - napari.layers.Points, - napari.layers.Shapes, - napari.layers.Tracks, - napari.layers.Vectors, - ) - - def __init__(self, napari_viewer: napari.viewer.Viewer): - super().__init__(napari_viewer) - self._key_selection_widget = magicgui( - self._set_axis_keys, - x_axis_key={"choices": self._get_valid_axis_keys}, - n_bins={"value": 50, "widget_type": "SpinBox"}, - call_button="Update", - ) - self._export_button = magicgui( - self.export, - call_button='Export plot as csv' - ) - - # controllers for rectangle selector - self.left_edit = QDoubleSpinBox() - self.left_edit.setMaximum(1) - self.left_edit.setMinimum(0) - self.left_edit.setDecimals(2) - self.left_edit.setSingleStep(0.01) - self.right_edit = QDoubleSpinBox() - self.right_edit.setMaximum(1) - self.right_edit.setMinimum(0) - self.right_edit.setDecimals(2) - self.right_edit.setSingleStep(0.01) - container_edits = QGridLayout() - container_edits.addItem(QSpacerItem(100, 0, QSizePolicy.Expanding), 0, 0) - container_edits.addItem(QSpacerItem(100, 0, QSizePolicy.Expanding), 1, 0) - container_edits.addWidget(QLabel('Upper percentile'), 0, 1) - container_edits.addWidget(QLabel('Lower percentile'), 1, 1) - container_edits.addWidget(self.left_edit, 1, 2) - container_edits.addWidget(self.right_edit, 0, 2) - self.layout().addLayout(container_edits) - - self.left_edit.valueChanged.connect(self._on_percentile_select) - self.right_edit.valueChanged.connect(self._on_percentile_select) - - # buttons to switch between histogram/CDF - self.enable_histogram = QPushButton('Histogram') - self.enable_histogram.setCheckable(True) - self.enable_histogram.setChecked(True) - self.enable_cdf = QPushButton('CDF') - self.enable_cdf.setCheckable(True) - self.enable_histogram.clicked.connect(self._draw) - self.enable_cdf.clicked.connect(self._draw) - - container = QHBoxLayout() - container.addWidget(self.enable_histogram) - container.addWidget(self.enable_cdf) - self.layout().addLayout(container) - - self.layout().addWidget(self._key_selection_widget.native) - self.layout().addWidget(self._export_button.native) - - self.viewer = napari_viewer - - # create a second y-axis in the plot - self.axes2 = self.axes.twinx() # for cdf - self.axes3 = self.axes.twinx() # for selector rectangle - self.axes3.get_xaxis().set_visible(False) - self.axes3.get_yaxis().set_visible(False) - - # hook up rectangle selector - self.rectangle_selector = RectangleSelector( - self.axes3, - self._on_area_select, - useblit=True, - props=dict(edgecolor="white", fill=False), - minspanx=5, - minspany=5, - spancoords="pixels", - interactive=False, - ) - self.highlight_rectangle = None - self.highlight_layer = None - self.cdf_histogram = None - - @property - def x_axis_key(self) -> Optional[str]: - """Key to access x axis data from the FeaturesTable.""" - return self._x_axis_key - - @x_axis_key.setter - def x_axis_key(self, key: Optional[str]) -> None: - self._x_axis_key = key - self._draw() - - @property - def n_bins(self) -> Optional[str]: - """Key to access y axis data from the FeaturesTable.""" - return self._n_bins - - # @n_bins.setter - # def n_bins(self, key: Optional[str]) -> None: - # # self._y_axis_key = key - # self._draw() - - def _set_axis_keys(self, x_axis_key: str, n_bins: int) -> None: - """Set both axis keys and then redraw the plot.""" - self._x_axis_key = x_axis_key - self._n_bins = n_bins - self._draw() - - self.viewer.layers[self.layers[0].name].refresh_colors(True) - self.viewer.layers[self.layers[0].name].face_color = x_axis_key - - def _get_valid_axis_keys( - self, combo_widget: Optional[ComboBox] = None - ) -> List[str]: - """ - Get the valid axis keys from the layer FeatureTable. - - Returns - ------- - axis_keys : List[str] - The valid axis keys in the FeatureTable. If the table is empty - or there isn't a table, returns an empty list. - """ - if len(self.layers) == 0 or not (hasattr(self.layers[0], "features")): - return [] - else: - return self.layers[0].features.keys() - - def _get_data(self) -> Tuple[List[np.ndarray], str, int]: - """Get the plot data. - - Returns - ------- - data : List[np.ndarray] - List contains X and Y columns from the FeatureTable. Returns - an empty array if nothing to plot. - x_axis_name : str - The title to display on the x axis. Returns - an empty string if nothing to plot. - y_axis_name: int - The title to display on the y axis. Returns - an empty string if nothing to plot. - """ - if not hasattr(self.layers[0], "features"): - # if the selected layer doesn't have a featuretable, - # skip draw - return [], "", "" - - feature_table = self.layers[0].features - - if ( - (len(feature_table) == 0) - or (self.x_axis_key is None) - ): - return [], "", 0 - - data_x = feature_table[self.x_axis_key] - bins = np.linspace(np.min(data_x), np.max(data_x), self.n_bins+1) - # data_y = feature_table[self.y_axis_key] - data = [data_x, bins] - - x_axis_name = self.x_axis_key.replace("_", " ") - y_axis_name = 'Occurrences [#]' - - return data, x_axis_name, y_axis_name - - def _on_update_layers(self) -> None: - """This is called when the layer selection changes by - ``self.update_layers()``. - - """ - if hasattr(self, "_key_selection_widget"): - self._key_selection_widget.reset_choices() - - # reset the axis keys - self._x_axis_key = None - self._n_bins = None - - def _on_area_select(self, eclick, erelease): - """Triggered when user clicks within axes""" - # get click event coordinates - x1, _ = eclick.xdata, eclick.ydata - x2, _ = erelease.xdata, erelease.ydata - - y1 = 0 - y2 = self.axes.get_ylim()[1] - self._draw_highlight_rectangle(x1, x2, y1, y2) - - if self.cdf_histogram is not None: - percentile_left = self.cdf_histogram.cdf(min(x1, x2)) - percentile_right = self.cdf_histogram.cdf(max(x1, x2)) - self.left_edit.setValue(percentile_left) - self.right_edit.setValue(percentile_right) - - def _on_percentile_select(self): - """If values in percentile comboboxes are changed.""" - percentile_left = self.left_edit.value() - percentile_right = self.right_edit.value() - - if percentile_left > percentile_right: - percentile_left = percentile_right - self.left_edit.setValue(percentile_left) - - left = self.cdf_histogram.ppf(percentile_left) - right = self.cdf_histogram.ppf(percentile_right) - self._draw_highlight_rectangle(left, right, 0, self.axes.get_ylim()[1]) - - - - def _draw_highlight_rectangle(self, x1, x2, y1=0, y2=1): - """ - Draw the rectangle the highlights points in the viewer. - - Parameters - ---------- - x1 : float - left border - x2 : float - right border - y1 : float - lower border - y2 : float - upper border - - Returns - ------- - None. - - """ - # put highlight rectangle in histogram plot - if self.highlight_rectangle is None: - self.highlight_rectangle = Rectangle( - (min(x1,x2), 0), np.abs(x1-x2), np.abs(y1-y2), - alpha=0.35, facecolor='white') - self.axes3.add_patch(self.highlight_rectangle) - - else: - self.highlight_rectangle.set_x(x1) - self.highlight_rectangle.set_width(np.abs(x1-x2)) - - self.canvas.draw() - - # Get points that correspond to selected points in plot - left = min(x1, x2) - right = max(x1, x2) - shown_range = self.layers[0].features[self.x_axis_key] - shown_range = np.asarray((shown_range > left) * (shown_range < right)) - self.viewer.layers[self.layers[0].name].refresh_colors(True) - - # highlight in viewer - colors_highlight = np.ones((self.layers[0].data.shape[0], 4)) - colors_highlight[np.argwhere(shown_range == False), :3] = 0 - self.viewer.layers[self.layers[0].name].edge_color = colors_highlight - - def draw(self) -> None: - """Clear the axes and histogram the currently selected layer/slice.""" - data, x_axis_name, y_axis_name = self._get_data() - - if len(data) == 0: - # don't plot if there isn't data - return - - self.N, bins = np.histogram(data[0], data[1]) - - colormapping = self.layers[0].face_colormap - self.bins_norm = (bins - bins.min())/(bins.max() - bins.min()) - colors = colormapping.map(self.bins_norm) - - if self.enable_histogram.isChecked(): - self.N, bins, patches = self.axes.hist(data[0], - bins=data[1], - edgecolor='white', - linewidth=0.3, - label=self.layers[0].name) - # Set histogram style: - for idx, patch in enumerate(patches): - patch.set_facecolor(colors[idx]) - else: - self.axes.clear() - - if self.enable_cdf.isChecked(): - self.cdf_histogram = stats.rv_histogram((self.N, bins)) - self.axes2.step(bins, self.cdf_histogram.cdf(data[1]), color='black', - where='pre', zorder=1) - else: - self.axes2.clear() - - # set ax labels - self.axes.set_xlabel(x_axis_name) - self.axes.set_ylabel(y_axis_name) - self.axes2.set_ylabel('Cumulative density') - - # make sure that rectangle axes has correct x-range - self.axes3.set_xlim(bins[0], bins[-1]) - - self.canvas.draw() - - def export(self) -> None: - """Export plotted data as csv.""" - if not self.axes.has_data(): - print('No data plotted') - return - # Not including last bin because len(bins) = len(N) + 1 - # https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.hist.html - df_to_save = pd.DataFrame({self.axes.get_xlabel(): self.bins_norm[:-1], - self.axes.get_ylabel(): self.N}) - fname = QFileDialog.getSaveFileName(self, 'Save plotted data', - 'c:\\', - "Csv files (*.csv)") - df_to_save.to_csv(fname[0]) - return diff --git a/src/napari_stress/_tests/test_plotting.py b/src/napari_stress/_tests/test_plotting.py deleted file mode 100644 index a15d1fa1..00000000 --- a/src/napari_stress/_tests/test_plotting.py +++ /dev/null @@ -1,33 +0,0 @@ -def test_plotting(make_napari_viewer): - from napari_stress import FeaturesHistogramWidget - from napari_stress import get_droplet_point_cloud - from napari_stress._spherical_harmonics.spherical_harmonics_napari \ - import fit_spherical_harmonics - - # Load sample data - data = get_droplet_point_cloud()[0][0] - - # Calculate spherical harmonics - layer_data_tuple = fit_spherical_harmonics(data) - - viewer = make_napari_viewer() - # Add sample points to viewer - viewer.add_points(layer_data_tuple[0], **layer_data_tuple[1]) - # Add plot widget to viewer - plot_widget = FeaturesHistogramWidget(viewer) - viewer.window.add_dock_widget(plot_widget) - # Run plot widget - plot_widget._key_selection_widget() - # Check if plot has data - assert plot_widget.axes.has_data() - - plot_widget.enable_cdf.setChecked(True) - plot_widget._key_selection_widget() - - # check the data highlighting - plot_widget._draw_highlight_rectangle(0,1,0,1) - - -if __name__ == '__main__': - import napari - test_plotting(napari.Viewer) diff --git a/src/napari_stress/napari.yaml b/src/napari_stress/napari.yaml index 58e078c3..7203bdec 100644 --- a/src/napari_stress/napari.yaml +++ b/src/napari_stress/napari.yaml @@ -92,11 +92,6 @@ contributions: python_name: napari_stress._vectors:absolute_move_points_along_vector title: Move points along vector (absolute) - # Plotting - - id: napari-stress.features_histogram - python_name: napari_stress:FeaturesHistogramWidget - title: Visualize features (histogram) - - id: napari-stress.fit_lsq_ellipsoid python_name: napari_stress.approximation:least_squares_ellipsoid title: Fit least squares ellipsoid @@ -182,10 +177,6 @@ contributions: autogenerate: true display_name: Pairwise point distance - # Plotting - - command: napari-stress.features_histogram - display_name: Visualize features (histogram) - # Vectors - command: napari-stress.normal_vectors_on_pointcloud autogenerate: true