-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add widgets for setting histogram bins #242
Changes from 20 commits
a7b09d4
96b2f0c
b8623ed
4e4fb84
5ac1f71
fab2906
5553174
e86d4f6
c5e0886
127d325
b8ffdb7
88760cf
d56942b
11590b2
08a5086
65e84f8
37d33ae
8a7d9e4
fbd929e
7c4cdc8
c6b5d8e
6261f4c
457bc1b
426a0f5
67a8641
208af97
8fe7c7f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,17 @@ | ||
from typing import Any, Optional, cast | ||
from typing import Any, Optional, Union, cast | ||
|
||
import napari | ||
import numpy as np | ||
import numpy.typing as npt | ||
from matplotlib.container import BarContainer | ||
from qtpy.QtWidgets import ( | ||
QAbstractSpinBox, | ||
QComboBox, | ||
QDoubleSpinBox, | ||
QFormLayout, | ||
QGroupBox, | ||
QLabel, | ||
QSpinBox, | ||
QVBoxLayout, | ||
QWidget, | ||
) | ||
|
@@ -26,7 +31,7 @@ def _get_bins(data: npt.NDArray[Any]) -> npt.NDArray[Any]: | |
step = np.ceil(np.ptp(data) / 100) | ||
return np.arange(np.min(data), np.max(data) + step, step) | ||
else: | ||
# For other data types, just have 128 evenly spaced bins | ||
# For other data types, just have 99 evenly spaced bins | ||
return np.linspace(np.min(data), np.max(data), 100) | ||
|
||
|
||
|
@@ -44,6 +49,55 @@ def __init__( | |
parent: Optional[QWidget] = None, | ||
): | ||
super().__init__(napari_viewer, parent=parent) | ||
|
||
# Create widgets for setting bin parameters | ||
bins_start = QDoubleSpinBox() | ||
bins_start.setStepType(QAbstractSpinBox.AdaptiveDecimalStepType) | ||
bins_start.setRange(-1e10, 1e10) | ||
bins_start.setValue(0) | ||
bins_start.setWrapping(False) | ||
bins_start.setKeyboardTracking(False) | ||
bins_start.setDecimals(2) | ||
|
||
bins_stop = QDoubleSpinBox() | ||
bins_stop.setStepType(QAbstractSpinBox.AdaptiveDecimalStepType) | ||
bins_stop.setRange(-1e10, 1e10) | ||
bins_stop.setValue(100) | ||
bins_start.setWrapping(False) | ||
bins_stop.setKeyboardTracking(False) | ||
bins_stop.setDecimals(2) | ||
|
||
bins_num = QSpinBox() | ||
bins_num.setRange(1, 100_000) | ||
bins_num.setValue(101) | ||
bins_num.setWrapping(False) | ||
bins_num.setKeyboardTracking(False) | ||
|
||
# Set bins widget layout | ||
bins_selection_layout = QFormLayout() | ||
bins_selection_layout.addRow("start", bins_start) | ||
bins_selection_layout.addRow("stop", bins_stop) | ||
bins_selection_layout.addRow("num", bins_num) | ||
|
||
# Group the widgets and add to main layout | ||
bins_widget_group = QGroupBox("Bins") | ||
bins_widget_group_layout = QVBoxLayout() | ||
bins_widget_group_layout.addLayout(bins_selection_layout) | ||
bins_widget_group.setLayout(bins_widget_group_layout) | ||
self.layout().addWidget(bins_widget_group) | ||
|
||
# Add callbacks | ||
bins_start.valueChanged.connect(self._draw) | ||
bins_stop.valueChanged.connect(self._draw) | ||
bins_num.valueChanged.connect(self._draw) | ||
|
||
# Store widgets for later usage | ||
self._bin_widgets = { | ||
"start": bins_start, | ||
"stop": bins_stop, | ||
"num": bins_num, | ||
} | ||
|
||
self._update_layers(None) | ||
self.viewer.events.theme.connect(self._on_napari_theme_changed) | ||
|
||
|
@@ -55,6 +109,31 @@ def on_update_layers(self) -> None: | |
for layer in self.viewer.layers: | ||
layer.events.contrast_limits.connect(self._update_contrast_lims) | ||
|
||
if not self.layers: | ||
return | ||
|
||
# Reset to bin start, stop and step | ||
layer_data = self._get_layer_data(self.layers[0]) | ||
self.autoset_widget_bins(data=layer_data) | ||
|
||
# Only allow integer bins for integer data | ||
# And only allow values greater than 0 for unsigned integers | ||
n_decimals = 0 if np.issubdtype(layer_data.dtype, np.integer) else 2 | ||
is_unsigned = layer_data.dtype.kind == "u" | ||
minimum_value = 0 if is_unsigned else -1e10 | ||
|
||
# Disable callbacks whilst widget values might change | ||
for widget in self._bin_widgets.values(): | ||
widget.blockSignals(True) | ||
|
||
self._bin_widgets["start"].setDecimals(n_decimals) | ||
self._bin_widgets["stop"].setDecimals(n_decimals) | ||
self._bin_widgets["start"].setMinimum(minimum_value) | ||
self._bin_widgets["stop"].setMinimum(minimum_value) | ||
|
||
for widget in self._bin_widgets.values(): | ||
widget.blockSignals(False) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. temporarily disable the callbacks otherwise the plot is re-drawn each time the widget value changes |
||
def _update_contrast_lims(self) -> None: | ||
for lim, line in zip( | ||
self.layers[0].contrast_limits, self._contrast_lines | ||
|
@@ -63,24 +142,83 @@ def _update_contrast_lims(self) -> None: | |
|
||
self.figure.canvas.draw() | ||
|
||
def draw(self) -> None: | ||
""" | ||
Clear the axes and histogram the currently selected layer/slice. | ||
""" | ||
layer = self.layers[0] | ||
def autoset_widget_bins(self, data: npt.NDArray[Any]) -> None: | ||
"""Update widgets with bins determined from the image data""" | ||
bins = _get_bins(data) | ||
|
||
# Disable callbacks whilst setting widget values | ||
for widget in self._bin_widgets.values(): | ||
widget.blockSignals(True) | ||
|
||
self.bins_start = bins[0] | ||
self.bins_stop = bins[-1] | ||
self.bins_num = bins.size - 1 | ||
|
||
for widget in self._bin_widgets.values(): | ||
widget.blockSignals(False) | ||
|
||
@property | ||
def bins_start(self) -> float: | ||
"""Minimum bin edge""" | ||
return self._bin_widgets["start"].value() | ||
|
||
@bins_start.setter | ||
def bins_start(self, start: Union[int, float]) -> None: | ||
"""Set the minimum bin edge""" | ||
self._bin_widgets["start"].setValue(start) | ||
|
||
@property | ||
def bins_stop(self) -> float: | ||
"""Maximum bin edge""" | ||
return self._bin_widgets["stop"].value() | ||
|
||
@bins_stop.setter | ||
def bins_stop(self, stop: Union[int, float]) -> None: | ||
"""Set the maximum bin edge""" | ||
self._bin_widgets["stop"].setValue(stop) | ||
|
||
@property | ||
def bins_num(self) -> int: | ||
"""Number of bins to use""" | ||
return self._bin_widgets["num"].value() | ||
|
||
@bins_num.setter | ||
def bins_num(self, num: int) -> None: | ||
"""Set the number of bins to use""" | ||
self._bin_widgets["num"].setValue(num) | ||
|
||
def _get_layer_data(self, layer: napari.layers.Layer) -> npt.NDArray[Any]: | ||
"""Get the data associated with a given layer""" | ||
if layer.data.ndim - layer.rgb == 3: | ||
# 3D data, can be single channel or RGB | ||
data = layer.data[self.current_z] | ||
self.axes.set_title(f"z={self.current_z}") | ||
else: | ||
data = layer.data | ||
|
||
# Read data into memory if it's a dask array | ||
data = np.asarray(data) | ||
|
||
return data | ||
|
||
def draw(self) -> None: | ||
""" | ||
Clear the axes and histogram the currently selected layer/slice. | ||
""" | ||
layer = self.layers[0] | ||
data = self._get_layer_data(layer) | ||
|
||
# Important to calculate bins after slicing 3D data, to avoid reading | ||
# whole cube into memory. | ||
bins = _get_bins(data) | ||
if data.dtype.kind in {"i", "u"}: | ||
# Make sure integer data types have integer sized bins | ||
step = abs(self.bins_stop - self.bins_start) // (self.bins_num) | ||
step = max(1, step) | ||
bins = np.arange(self.bins_start, self.bins_stop + step, step) | ||
else: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be good to show a notification when the actual number of bins used differs from that requested in the widgets, but perhaps this could be a different PR? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added the notification now, but happy to remove it and add separately if it's easier |
||
bins = np.linspace( | ||
self.bins_start, self.bins_stop, self.bins_num + 1 | ||
) | ||
|
||
p-j-smith marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if layer.rgb: | ||
# Histogram RGB channels independently | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't sure what a reasonable range would be (rather than the default of 0 to 100)?