-
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
Refactor TwoPhotonSeries #241
Changes from all commits
9e1c62e
03349d5
f057a3c
571716b
335391c
a66b476
bb5cf0c
aa3405d
70452cd
98e664f
5e40271
20a52f3
100f2f2
c1c2eab
0aaefc4
7ee38fb
86718d6
32356ac
ed00867
b3b8880
9f94400
ccd4b26
b296f8f
b990b96
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,3 +1,5 @@ | ||
from .time_window_controllers import StartAndDurationController, RangeController | ||
from .group_and_sort_controllers import GroupAndSortController | ||
from .misc import ProgressBar, make_trial_event_controller | ||
from .image_controllers import RotationController, ImShowController | ||
from .misc import ProgressBar, ViewTypeController, make_trial_event_controller | ||
from .multicontroller import MultiController |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import ipywidgets as widgets | ||
|
||
|
||
class RotationController(widgets.HBox): | ||
controller_fields = ("rotation", "rotate_left", "rotate_right") | ||
|
||
def __init__(self): | ||
super().__init__() | ||
|
||
self.rotation = 0 # A hidden non-widget value counter to keep relative track of progression | ||
self.rotate_left = widgets.Button(icon="rotate-left", layout=widgets.Layout(width="35px")) | ||
self.rotate_right = widgets.Button(icon="rotate-right", layout=widgets.Layout(width="35px")) | ||
|
||
self.set_observers() | ||
|
||
self.children = (self.rotate_left, self.rotate_right) | ||
|
||
def set_observers(self): | ||
def _rotate_right(change): | ||
self.rotation += 1 | ||
|
||
def _rotate_left(change): | ||
self.rotation -= 1 | ||
|
||
self.rotate_right.on_click(_rotate_right) | ||
self.rotate_left.on_click(_rotate_left) | ||
|
||
|
||
class ImShowController(widgets.VBox): | ||
"""Controller specifically for handling various options for the plot.express.imshow function.""" | ||
|
||
controller_fields = ("contrast_type_toggle", "auto_contrast_method", "manual_contrast_slider") | ||
|
||
def __init__(self): | ||
super().__init__() | ||
|
||
self.contrast_type_toggle = widgets.ToggleButtons( | ||
description="Constrast: ", | ||
options=[ | ||
("Automatic", "Automatic"), | ||
("Manual", "Manual"), | ||
], # Values set to strings for external readability | ||
) | ||
self.auto_contrast_method = widgets.Dropdown(description="Method: ", options=["minmax", "infer"]) | ||
self.manual_contrast_slider = widgets.IntRangeSlider( | ||
value=(0, 1), # Actual value will depend on data selection | ||
min=0, # Actual value will depend on data selection | ||
max=1, # Actual value will depend on data selection | ||
orientation="horizontal", | ||
description="Range: ", | ||
continuous_update=False, | ||
) | ||
|
||
# Setup initial controller-specific layout | ||
self.children = (self.contrast_type_toggle, self.auto_contrast_method) | ||
|
||
# Setup controller-specific observer events | ||
self.setup_observers() | ||
|
||
def setup_observers(self): | ||
self.contrast_type_toggle.observe( | ||
lambda change: self.switch_contrast_modes(enable_manual_contrast=change.new), names="value" | ||
) | ||
|
||
def switch_contrast_modes(self, enable_manual_contrast: bool): | ||
"""When the manual contrast toggle is altered, adjust the manual vs. automatic visibility of the components.""" | ||
if self.contrast_type_toggle.value == "Manual": | ||
self.children = (self.contrast_type_toggle, self.manual_contrast_slider) | ||
elif self.contrast_type_toggle.value == "Automatic": | ||
self.children = (self.contrast_type_toggle, self.auto_contrast_method) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
from typing import Tuple, Dict | ||
|
||
import ipywidgets as widgets | ||
|
||
|
||
class MultiController(widgets.VBox): | ||
controller_fields: Tuple[str] = tuple() | ||
components: Dict[str, widgets.VBox] = dict() | ||
|
||
def __init__(self, components: list): | ||
super().__init__() | ||
|
||
children = list() | ||
controller_fields = list() | ||
self.components = {component.__class__.__name__: component for component in components} | ||
for component in self.components.values(): | ||
# Set attributes at outermost level | ||
for field in component.controller_fields: | ||
controller_fields.append(field) | ||
setattr(self, field, getattr(component, field)) | ||
|
||
# Default layout of children | ||
if isinstance(component, widgets.Widget) and not isinstance(component, MultiController): | ||
children.append(component) | ||
|
||
self.children = tuple(children) | ||
self.controller_fields = tuple(controller_fields) | ||
|
||
self.setup_observers() | ||
|
||
def setup_observers(self): | ||
pass | ||
Comment on lines
+6
to
+32
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 don't like the distinction between Controllers and MultiControllers. I think a widget that contains multiple controllers should just be another controller. 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. It's fine to have a MultiController class that automates combination of Controllers. But this class should also be a Controller. 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. Yeah, I just need to utilize a base Controller class, which a MultiController itself is as well in every respect |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from .plane_slice import PlaneSliceVisualization | ||
from .two_photon_series import TwoPhotonSeriesVisualization | ||
from .volume import VolumeVisualization | ||
from .segmentation import PlaneSegmentation2DWidget, RoiResponseSeries, show_image_segmentation, route_plane_segmentation, show_df_over_f, RoiResponseSeriesWidget |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
from typing import Optional | ||
|
||
import ipywidgets as widgets | ||
|
||
from ..controllers import RotationController, ImShowController, ViewTypeController, MultiController | ||
|
||
|
||
class FrameController(widgets.VBox): | ||
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. There should be a |
||
controller_fields = ("frame_slider",) | ||
|
||
def __init__(self): | ||
super().__init__() | ||
|
||
self.frame_slider = widgets.IntSlider( | ||
value=0, # Actual value will depend on data selection | ||
min=0, # Actual value will depend on data selection | ||
max=1, # Actual value will depend on data selection | ||
orientation="horizontal", | ||
description="Frame: ", | ||
continuous_update=False, | ||
) | ||
|
||
self.children = (self.frame_slider,) | ||
|
||
|
||
class PlaneController(widgets.VBox): | ||
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 defining controllers that contain a single widget is a bit overkill. In my mind, the point of controllers is collections of widgets (or collections of collections of widgets) in a system that is reusable. |
||
controller_fields = ("plane_slider",) | ||
|
||
def __init__(self): | ||
super().__init__() | ||
|
||
self.plane_slider = widgets.IntSlider( | ||
value=0, # Actual value will depend on data selection | ||
min=0, # Actual value will depend on data selection | ||
max=1, # Actual value will depend on data selection | ||
orientation="horizontal", | ||
description="Plane: ", | ||
continuous_update=False, | ||
) | ||
|
||
self.children = (self.plane_slider,) | ||
|
||
|
||
class SinglePlaneDataController(MultiController): | ||
def __init__(self, components: Optional[list] = None): | ||
default_components = [RotationController(), FrameController()] | ||
if components is not None: | ||
default_components.extend(components) | ||
super().__init__(components=default_components) | ||
|
||
# Align rotation buttons to center of sliders | ||
self.layout.align_items = "center" | ||
|
||
|
||
class VolumetricDataController(SinglePlaneDataController): | ||
def __init__(self): | ||
super().__init__(components=[PlaneController()]) | ||
|
||
|
||
class BasePlaneSliceController(MultiController): | ||
def __init__(self, components: Optional[list] = None): | ||
default_components = [ViewTypeController(), ImShowController()] | ||
if components is not None: | ||
default_components.extend(components) | ||
super().__init__(components=default_components) | ||
|
||
self.setup_visibility() | ||
self.setup_observers() | ||
|
||
def set_detailed_visibility(self, visibile: bool): | ||
widget_visibility_type = "visible" if visibile else "hidden" | ||
|
||
self.contrast_type_toggle.layout.visibility = widget_visibility_type | ||
self.manual_contrast_slider.layout.visibility = widget_visibility_type | ||
self.auto_contrast_method.layout.visibility = widget_visibility_type | ||
|
||
def update_visibility(self): | ||
if self.view_type_toggle.value == "Simplified": | ||
self.set_detailed_visibility(visibile=False) | ||
elif self.view_type_toggle.value == "Detailed": | ||
self.set_detailed_visibility(visibile=True) | ||
|
||
def setup_visibility(self): | ||
self.set_detailed_visibility(visibile=False) | ||
|
||
def setup_observers(self): | ||
self.view_type_toggle.observe(lambda change: self.update_visibility(), names="value") | ||
|
||
|
||
class SinglePlaneSliceController(BasePlaneSliceController): | ||
def __init__(self): | ||
super().__init__(components=[SinglePlaneDataController()]) | ||
|
||
|
||
class VolumetricPlaneSliceController(BasePlaneSliceController): | ||
def __init__(self): | ||
super().__init__(components=[VolumetricDataController()]) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import math | ||
from functools import lru_cache | ||
from typing import Tuple, Optional | ||
|
||
import numpy as np | ||
import h5py | ||
|
||
from .single_plane import SinglePlaneVisualization | ||
from .ophys_controllers import VolumetricPlaneSliceController | ||
|
||
|
||
class PlaneSliceVisualization(SinglePlaneVisualization): | ||
"""Sub-widget specifically for plane-wise views of a 4D TwoPhotonSeries.""" | ||
|
||
def _dimension_check(self): | ||
num_dimensions = len(self.two_photon_series.data.shape) | ||
if num_dimensions != 4: | ||
raise ValueError( | ||
"The PlaneSliceVisualization is only appropriate for " | ||
f"use on 4-dimensional TwoPhotonSeries! Detected dimension of {num_dimensions}." | ||
) | ||
|
||
@lru_cache # default size of 128 items ought to be enough to create a 1GB cache on large images | ||
def _cache_data_read(self, dataset: h5py.Dataset, frame_index: int, plane_index: int) -> np.ndarray: | ||
return dataset[frame_index, :, :, plane_index].T | ||
|
||
def update_data(self, frame_index: Optional[int] = None, plane_index: Optional[int] = None): | ||
frame_index = frame_index or self.Controller.frame_slider.value | ||
plane_index = plane_index or self.Controller.plane_slider.value | ||
|
||
self.data = self._cache_data_read( | ||
dataset=self.two_photon_series.data, frame_index=frame_index, plane_index=plane_index | ||
) | ||
|
||
self.data = self.two_photon_series.data[frame_index, :, :, plane_index] | ||
|
||
if self.Controller.contrast_type_toggle.value == "Manual": | ||
self.update_contrast_range() | ||
|
||
def setup_data(self, max_mb_treshold: float = 20.0): | ||
""" | ||
Start by loading only a single frame of a single plane. | ||
|
||
If the image size relative to data type is too large, relative to max_mb_treshold (indicating the load | ||
operation for initial setup would take a noticeable amount of time), then sample the image with a `by`. | ||
|
||
Note this may not actually provide a speedup when streaming; need to think of way around that. Maybe set | ||
a global flag for if streaming mode is enabled on the file, and if so make full use of data within contiguous | ||
HDF5 chunks? | ||
""" | ||
itemsize = self.two_photon_series.data.dtype.itemsize | ||
nbytes_per_image = math.prod(self.two_photon_series.data.shape) * itemsize | ||
if nbytes_per_image <= max_mb_treshold: | ||
self.update_data(frame_index=0, plane_index=0) | ||
else: | ||
# TOD: Figure out formula for calculating by in one-shot | ||
by_width = 2 | ||
by_height = 2 | ||
self.data = self.two_photon_series.data[0, ::by_width, ::by_height, 0] | ||
|
||
def pre_setup_controllers(self): | ||
self.Controller = VolumetricPlaneSliceController() | ||
self.data_controller_name = "VolumetricDataController" | ||
|
||
def setup_controllers(self): | ||
"""Controller updates are handled through the defined Controller class.""" | ||
super().setup_controllers() | ||
|
||
self.Controller.plane_slider.max = self.two_photon_series.data.shape[-1] - 1 | ||
|
||
def update_figure( | ||
self, | ||
rotation_changed: Optional[bool] = None, | ||
frame_index: Optional[int] = None, | ||
plane_index: Optional[int] = None, | ||
contrast_rescaling: Optional[str] = None, | ||
contrast: Optional[Tuple[int]] = None, | ||
): | ||
if plane_index is not None: | ||
self.update_data(plane_index=plane_index) | ||
self.update_data_to_plot() | ||
|
||
super().update_figure(rotation_changed=rotation_changed, frame_index=frame_index, contrast_rescaling=contrast_rescaling, contrast=contrast) | ||
|
||
def set_canvas_title(self): | ||
self.canvas_title = f"TwoPhotonSeries: {self.two_photon_series.name} - Planar slices of volume" | ||
|
||
def setup_observers(self): | ||
super().setup_observers() | ||
|
||
self.Controller.plane_slider.observe(lambda change: self.update_canvas(plane_index=change.new), names="value") |
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.
Can you make this inherit from
widgets.Box
so components can be either horizontal or vertical? Seehttps://ipywidgets.readthedocs.io/en/7.x/examples/Widget%20Styling.html#The-VBox-and-HBox-helpers
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.
Yes I started running into this on another PR as well