From 9e1c62e834a47c6485a37ce1cd29f66f3511d0d9 Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Wed, 19 Oct 2022 19:06:24 -0400 Subject: [PATCH 01/20] added tabs --- nwbwidgets/ophys.py | 141 ++++++++++++++++++++------------------------ 1 file changed, 63 insertions(+), 78 deletions(-) diff --git a/nwbwidgets/ophys.py b/nwbwidgets/ophys.py index d1b4c5f4..0d0a1974 100644 --- a/nwbwidgets/ophys.py +++ b/nwbwidgets/ophys.py @@ -19,7 +19,7 @@ from tifffile import imread, TiffFile from ndx_grayscalevolume import GrayscaleVolume -from .base import df_to_hover_text +from .base import df_to_hover_text, LazyTab from .timeseries import BaseGroupedTraceWidget from .utils.cmaps import linear_transfer_function from .utils.dynamictable import infer_categorical_columns @@ -33,6 +33,8 @@ class TwoPhotonSeriesWidget(widgets.VBox): def __init__(self, indexed_timeseries: TwoPhotonSeries, neurodata_vis_spec: dict): super().__init__() + self.figure = None + self.slider = None def _add_fig_trace(img_fig: go.Figure, index): if self.figure is None: @@ -51,71 +53,80 @@ def _add_fig_trace(img_fig: go.Figure, index): def update_figure(index=0): # Read first frame - img_fig = px.imshow( - imread(path_ext_file, key=int(index)), binary_string=True - ) + img_fig = px.imshow(imread(path_ext_file, key=int(index)), binary_string=True) _add_fig_trace(img_fig, index) - slider = widgets.IntSlider( - value=0, min=0, max=n_samples - 1, orientation="horizontal" - ) + self.slider = widgets.IntSlider(value=0, min=0, max=n_samples - 1, orientation="horizontal") + self.controls = dict(slider=self.slider) + self.slider.observe(lambda change: update_figure(index=change.new), names="value") + + update_figure() + self.children = [self.figure, self.slider] else: + self.slider = widgets.IntSlider( + value=0, min=0, max=indexed_timeseries.data.shape[0] - 1, orientation="horizontal" + ) + self.controls = dict(slider=self.slider) + if len(indexed_timeseries.data.shape) == 3: def update_figure(index=0): - img_fig = px.imshow( - indexed_timeseries.data[index].T, binary_string=True - ) + img_fig = px.imshow(indexed_timeseries.data[index].T, binary_string=True) _add_fig_trace(img_fig, index) + self.slider.observe(lambda change: update_figure(index=change.new), names="value") + update_figure() + self.children = [self.figure, self.slider] + elif len(indexed_timeseries.data.shape) == 4: - import ipyvolume.pylab as p3 - output = widgets.Output() + def update_volume_figure(index=0): + # import ipyvolume.pylab as p3 + # output = widgets.Output() + # p3.figure() + # p3.volshow( + # indexed_timeseries.data[index].transpose([1, 0, 2]) + # # tf=linear_transfer_function([0, 0, 0], max_opacity=0.3), + # ) + # output.clear_output(wait=True) + # self.figure = p3 + # with output: + # p3.show() + self.figure = widgets.HTML("Not currently working locally on Windows...") + + def plot_volume(indexed_timeseries: TwoPhotonSeries): + self.slider.observe(lambda change: update_volume_figure(index=change.new), names="value") + update_volume_figure() + return widgets.VBox(children=[self.figure, self.slider]) + + def update_plane_slice_figure(index=0): + img_fig = px.imshow(indexed_timeseries.data[index][:, :, -1].T, binary_string=True) + _add_fig_trace(img_fig, index) - def update_figure(index=0): - p3.figure() - p3.volshow( - indexed_timeseries.data[index].transpose([1, 0, 2]), - tf=linear_transfer_function([0, 0, 0], max_opacity=0.3), - ) - output.clear_output(wait=True) - self.figure = output - with output: - p3.show() + def plot_plane_slices(indexed_timeseries: TwoPhotonSeries): + self.slider.observe(lambda change: update_plane_slice_figure(index=change.new), names="value") + update_plane_slice_figure() + return widgets.VBox(children=[self.figure, self.slider]) + tab = LazyTab( + func_dict={"Planar Slice": plot_plane_slices, "3D Volume": plot_volume}, data=indexed_timeseries + ) + self.children = [tab] else: raise NotImplementedError - slider = widgets.IntSlider( - value=0, - min=0, - max=indexed_timeseries.data.shape[0] - 1, - orientation="horizontal", - ) - - slider.observe(lambda change: update_figure(change.new), names="value") - self.figure = None - self.controls = dict(slider=slider) - update_figure() - self.children = [self.figure, slider] - def show_df_over_f(df_over_f: DfOverF, neurodata_vis_spec: dict): if len(df_over_f.roi_response_series) == 1: title, data_input = list(df_over_f.roi_response_series.items())[0] - return neurodata_vis_spec[RoiResponseSeries]( - data_input, neurodata_vis_spec, title=title - ) + return neurodata_vis_spec[RoiResponseSeries](data_input, neurodata_vis_spec, title=title) else: return neurodata_vis_spec[NWBDataInterface](df_over_f, neurodata_vis_spec) def show_image_segmentation(img_seg: ImageSegmentation, neurodata_vis_spec: dict): if len(img_seg.plane_segmentations) == 1: - return route_plane_segmentation( - list(img_seg.plane_segmentations.values())[0], neurodata_vis_spec - ) + return route_plane_segmentation(list(img_seg.plane_segmentations.values())[0], neurodata_vis_spec) else: return neurodata_vis_spec[NWBDataInterface](img_seg, neurodata_vis_spec) @@ -177,19 +188,11 @@ def __init__(self, plane_seg: PlaneSegmentation, color_wheel=color_wheel, **kwar def on_button_click(self, b): if len(self.categorical_columns) == 1: - self.color_by = list(self.categorical_columns.keys())[ - 0 - ] # changing local variables to instance variables? - self.children += ( - self.show_plane_segmentation_2d(color_by=self.color_by, **self.kwargs), - ) + self.color_by = list(self.categorical_columns.keys())[0] # changing local variables to instance variables? + self.children += (self.show_plane_segmentation_2d(color_by=self.color_by, **self.kwargs),) elif len(self.categorical_columns) > 1: - self.cat_controller = widgets.Dropdown( - options=list(self.categorical_columns), description="color by" - ) - self.fig = self.show_plane_segmentation_2d( - color_by=self.cat_controller.value, **self.kwargs - ) + self.cat_controller = widgets.Dropdown(options=list(self.categorical_columns), description="color by") + self.fig = self.show_plane_segmentation_2d(color_by=self.cat_controller.value, **self.kwargs) def on_change(change): if change["new"] and isinstance(change["new"], dict): @@ -201,9 +204,7 @@ def on_change(change): self.cat_controller.observe(on_change) self.children += (self.cat_controller, self.fig) else: - self.children += ( - self.show_plane_segmentation_2d(color_by=None, **self.kwargs), - ) + self.children += (self.show_plane_segmentation_2d(color_by=None, **self.kwargs),) self.children = self.children[1:] def update_fig(self, color_by): @@ -211,9 +212,7 @@ def update_fig(self, color_by): legendgroups = [] with self.fig.batch_update(): for color_val, data in zip(self.plane_seg[color_by][:], self.fig.data): - color = self.color_wheel[ - np.where(cats == color_val)[0][0] - ] # store the color + color = self.color_wheel[np.where(cats == color_val)[0][0]] # store the color data.line.color = color # set the color data.legendgroup = str(color_val) # set the legend group to the color data.name = str(color_val) @@ -255,11 +254,7 @@ def show_plane_segmentation_2d( layout_kwargs = dict() if color_by: if color_by not in self.plane_seg: - raise ValueError( - "specified color_by parameter, {}, not in plane_seg object".format( - color_by - ) - ) + raise ValueError("specified color_by parameter, {}, not in plane_seg object".format(color_by)) cats = np.unique(self.plane_seg[color_by][:]) layout_kwargs.update(title=color_by) @@ -269,19 +264,13 @@ def show_plane_segmentation_2d( fig = go.FigureWidget() if ref_image is not None: - fig.add_trace( - go.Heatmap( - z=ref_image, hoverinfo="skip", showscale=False, colorscale="gray" - ) - ) + fig.add_trace(go.Heatmap(z=ref_image, hoverinfo="skip", showscale=False, colorscale="gray")) aux_leg = [] import pandas as pd plane_seg_hover_dict = { - key: self.plane_seg[key].data - for key in self.plane_seg.colnames - if key not in ["pixel_mask", "image_mask"] + key: self.plane_seg[key].data for key in self.plane_seg.colnames if key not in ["pixel_mask", "image_mask"] } plane_seg_hover_dict.update(id=self.plane_seg.id.data) plane_seg_hover_df = pd.DataFrame(plane_seg_hover_dict) @@ -337,9 +326,7 @@ def show_plane_segmentation_2d( @lru_cache(1000) def compute_outline(self, i, threshold): - x, y = zip( - *measure.find_contours(self.plane_seg["image_mask"][i], threshold)[0] - ) + x, y = zip(*measure.find_contours(self.plane_seg["image_mask"][i], threshold)[0]) return x, y @@ -361,7 +348,5 @@ def show_grayscale_volume(vol: GrayscaleVolume, neurodata_vis_spec: dict): class RoiResponseSeriesWidget(BaseGroupedTraceWidget): - def __init__( - self, roi_response_series: RoiResponseSeries, neurodata_vis_spec=None, **kwargs - ): + def __init__(self, roi_response_series: RoiResponseSeries, neurodata_vis_spec=None, **kwargs): super().__init__(roi_response_series, "rois", **kwargs) From 03349d5d6a7b06559c9a5ef2cd30ea4955276b35 Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Thu, 20 Oct 2022 16:03:28 -0400 Subject: [PATCH 02/20] remove stopping control attempts --- nwbwidgets/ophys.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/nwbwidgets/ophys.py b/nwbwidgets/ophys.py index 0d0a1974..0e3d841d 100644 --- a/nwbwidgets/ophys.py +++ b/nwbwidgets/ophys.py @@ -1,7 +1,7 @@ from functools import lru_cache - import numpy as np from skimage import measure +from multiprocessing import Process, Value import ipywidgets as widgets import plotly.graph_objects as go @@ -35,6 +35,9 @@ def __init__(self, indexed_timeseries: TwoPhotonSeries, neurodata_vis_spec: dict super().__init__() self.figure = None self.slider = None + self.stopper = None + self.main_subprocess = None + self.stopper_subprocess = None def _add_fig_trace(img_fig: go.Figure, index): if self.figure is None: @@ -80,24 +83,24 @@ def update_figure(index=0): elif len(indexed_timeseries.data.shape) == 4: - def update_volume_figure(index=0): - # import ipyvolume.pylab as p3 - # output = widgets.Output() - # p3.figure() - # p3.volshow( - # indexed_timeseries.data[index].transpose([1, 0, 2]) - # # tf=linear_transfer_function([0, 0, 0], max_opacity=0.3), - # ) - # output.clear_output(wait=True) - # self.figure = p3 - # with output: - # p3.show() - self.figure = widgets.HTML("Not currently working locally on Windows...") + self.figure2 = None + + def update_volume_figure(data): + import ipyvolume.pylab as p3 + + output = widgets.Output() + + p3.figure() + p3.volshow(data, tf=linear_transfer_function([0, 0, 0], max_opacity=0.3)) + output.clear_output(wait=True) + self.figure2 = output + with output: + p3.show() def plot_volume(indexed_timeseries: TwoPhotonSeries): self.slider.observe(lambda change: update_volume_figure(index=change.new), names="value") - update_volume_figure() - return widgets.VBox(children=[self.figure, self.slider]) + self.figure2 = widgets.HTML("Move the slider to render!") + return widgets.VBox(children=[self.figure2, self.slider]) def update_plane_slice_figure(index=0): img_fig = px.imshow(indexed_timeseries.data[index][:, :, -1].T, binary_string=True) From f057a3c042f1340ca12dbf14a1a961a682655d40 Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Thu, 20 Oct 2022 16:21:58 -0400 Subject: [PATCH 03/20] added render option button --- nwbwidgets/ophys.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/nwbwidgets/ophys.py b/nwbwidgets/ophys.py index 0e3d841d..8a13645d 100644 --- a/nwbwidgets/ophys.py +++ b/nwbwidgets/ophys.py @@ -85,21 +85,29 @@ def update_figure(index=0): self.figure2 = None - def update_volume_figure(data): + def update_volume_figure(index=0): import ipyvolume.pylab as p3 output = widgets.Output() - - p3.figure() - p3.volshow(data, tf=linear_transfer_function([0, 0, 0], max_opacity=0.3)) - output.clear_output(wait=True) self.figure2 = output + with output: + p3.figure() + p3.volshow( + indexed_timeseries.data[index].transpose([1, 0, 2]), + tf=linear_transfer_function([0, 0, 0], max_opacity=0.3), + ) + output.clear_output(wait=True) p3.show() - def plot_volume(indexed_timeseries: TwoPhotonSeries): + def first_volume_render(index=0): + update_volume_figure(index=self.slider.value) self.slider.observe(lambda change: update_volume_figure(index=change.new), names="value") - self.figure2 = widgets.HTML("Move the slider to render!") + + def plot_volume_init(indexed_timeseries: TwoPhotonSeries): + init_button = widgets.Button(description="Render") + init_button.on_click(first_volume_render) + self.figure2 = init_button # Have an activation button instead of initial render attempt return widgets.VBox(children=[self.figure2, self.slider]) def update_plane_slice_figure(index=0): @@ -112,7 +120,8 @@ def plot_plane_slices(indexed_timeseries: TwoPhotonSeries): return widgets.VBox(children=[self.figure, self.slider]) tab = LazyTab( - func_dict={"Planar Slice": plot_plane_slices, "3D Volume": plot_volume}, data=indexed_timeseries + func_dict={"Planar Slice": plot_plane_slices, "3D Volume": plot_volume_init}, + data=indexed_timeseries, ) self.children = [tab] else: From 571716b88f22987148ab66eba8c611bc9a2029e4 Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Thu, 20 Oct 2022 17:37:52 -0400 Subject: [PATCH 04/20] saving state --- nwbwidgets/ophys.py | 72 +++++++++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/nwbwidgets/ophys.py b/nwbwidgets/ophys.py index 8a13645d..c026acc0 100644 --- a/nwbwidgets/ophys.py +++ b/nwbwidgets/ophys.py @@ -35,16 +35,15 @@ def __init__(self, indexed_timeseries: TwoPhotonSeries, neurodata_vis_spec: dict super().__init__() self.figure = None self.slider = None - self.stopper = None - self.main_subprocess = None - self.stopper_subprocess = None + + series_name = indexed_timeseries.name + base_title = f"TwoPhotonSeries: {series_name}" def _add_fig_trace(img_fig: go.Figure, index): if self.figure is None: self.figure = go.FigureWidget(img_fig) else: self.figure.for_each_trace(lambda trace: trace.update(img_fig.data[0])) - self.figure.layout.title = f"Frame no: {index}" if indexed_timeseries.data is None: if indexed_timeseries.external_file is not None: @@ -59,17 +58,24 @@ def update_figure(index=0): img_fig = px.imshow(imread(path_ext_file, key=int(index)), binary_string=True) _add_fig_trace(img_fig, index) - self.slider = widgets.IntSlider(value=0, min=0, max=n_samples - 1, orientation="horizontal") + self.slider = widgets.IntSlider( + value=0, min=0, max=n_samples - 1, orientation="horizontal", description="TIFF index: " + ) self.controls = dict(slider=self.slider) self.slider.observe(lambda change: update_figure(index=change.new), names="value") update_figure() + self.figure.layout.title = f"{base_title} - read from first external file" self.children = [self.figure, self.slider] else: - self.slider = widgets.IntSlider( - value=0, min=0, max=indexed_timeseries.data.shape[0] - 1, orientation="horizontal" + self.frame_slider = widgets.IntSlider( + value=0, + min=0, + max=indexed_timeseries.data.shape[0] - 1, + orientation="horizontal", + description="Frame: ", ) - self.controls = dict(slider=self.slider) + self.controls = dict(slider=self.frame_slider) if len(indexed_timeseries.data.shape) == 3: @@ -77,14 +83,43 @@ def update_figure(index=0): img_fig = px.imshow(indexed_timeseries.data[index].T, binary_string=True) _add_fig_trace(img_fig, index) - self.slider.observe(lambda change: update_figure(index=change.new), names="value") + self.frame_slider.observe(lambda change: update_figure(index=change.new), names="value") + update_figure() - self.children = [self.figure, self.slider] + self.figure.layout.title = f"{base_title} - planar view" + self.children = [self.figure, self.frame_slider] elif len(indexed_timeseries.data.shape) == 4: self.figure2 = None + self.plane_slider = None + + # Planar Slice tab + def update_plane_slice_figure(frame_index=0, plane_index=0): + img_fig = px.imshow(indexed_timeseries.data[frame_index][:, :, plane_index].T, binary_string=True) + _add_fig_trace(img_fig, frame_index) + + def plot_plane_slices(indexed_timeseries: TwoPhotonSeries): + self.plane_slider = widgets.IntSlider( + value=0, + min=0, + max=indexed_timeseries.data.shape[-1] - 1, + orientation="horizontal", + description="Plane: ", + ) + + self.frame_slider.observe( + lambda change: update_plane_slice_figure(frame_index=change.new), names="value" + ) + self.plane_slider.observe( + lambda change: update_plane_slice_figure(plane_index=change.new), names="value" + ) + update_plane_slice_figure() + self.figure.layout.title = f"{base_title} - planar view of volume" + return widgets.VBox(children=[self.figure, self.frame_slider, self.plane_slider]) + + # Volume tab def update_volume_figure(index=0): import ipyvolume.pylab as p3 @@ -101,24 +136,17 @@ def update_volume_figure(index=0): p3.show() def first_volume_render(index=0): - update_volume_figure(index=self.slider.value) - self.slider.observe(lambda change: update_volume_figure(index=change.new), names="value") + update_volume_figure(index=self.frame_slider.value) + self.frame_slider.observe(lambda change: update_volume_figure(index=change.new), names="value") def plot_volume_init(indexed_timeseries: TwoPhotonSeries): init_button = widgets.Button(description="Render") init_button.on_click(first_volume_render) self.figure2 = init_button # Have an activation button instead of initial render attempt - return widgets.VBox(children=[self.figure2, self.slider]) - - def update_plane_slice_figure(index=0): - img_fig = px.imshow(indexed_timeseries.data[index][:, :, -1].T, binary_string=True) - _add_fig_trace(img_fig, index) - - def plot_plane_slices(indexed_timeseries: TwoPhotonSeries): - self.slider.observe(lambda change: update_plane_slice_figure(index=change.new), names="value") - update_plane_slice_figure() - return widgets.VBox(children=[self.figure, self.slider]) + self.figure2.layout.title = f"{base_title} - interactive volume" + return widgets.VBox(children=[self.figure2, self.frame_slider]) + # Main view tab = LazyTab( func_dict={"Planar Slice": plot_plane_slices, "3D Volume": plot_volume_init}, data=indexed_timeseries, From 335391cfd9c6a010e3fd29729464bfff767e3b96 Mon Sep 17 00:00:00 2001 From: CodyCBakerPhD Date: Fri, 21 Oct 2022 14:20:22 +0000 Subject: [PATCH 05/20] nwbwidgets --- nwbwidgets/ophys.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/nwbwidgets/ophys.py b/nwbwidgets/ophys.py index c026acc0..7b6de041 100644 --- a/nwbwidgets/ophys.py +++ b/nwbwidgets/ophys.py @@ -120,31 +120,31 @@ def plot_plane_slices(indexed_timeseries: TwoPhotonSeries): return widgets.VBox(children=[self.figure, self.frame_slider, self.plane_slider]) # Volume tab + output = widgets.Output() def update_volume_figure(index=0): import ipyvolume.pylab as p3 - output = widgets.Output() - self.figure2 = output - + p3.figure() + p3.volshow( + indexed_timeseries.data[index].transpose([1, 0, 2]), + tf=linear_transfer_function([0, 0, 0], max_opacity=0.3), + ) + output.clear_output(wait=True) with output: - p3.figure() - p3.volshow( - indexed_timeseries.data[index].transpose([1, 0, 2]), - tf=linear_transfer_function([0, 0, 0], max_opacity=0.3), - ) - output.clear_output(wait=True) p3.show() def first_volume_render(index=0): + self.figure2 = output update_volume_figure(index=self.frame_slider.value) self.frame_slider.observe(lambda change: update_volume_figure(index=change.new), names="value") def plot_volume_init(indexed_timeseries: TwoPhotonSeries): - init_button = widgets.Button(description="Render") - init_button.on_click(first_volume_render) - self.figure2 = init_button # Have an activation button instead of initial render attempt + self.init_button = widgets.Button(description="Render") + self.init_button.on_click(first_volume_render) + #self.figure2 = init_button # Have an activation button instead of initial render attempt + self.figure2 = output self.figure2.layout.title = f"{base_title} - interactive volume" - return widgets.VBox(children=[self.figure2, self.frame_slider]) + return widgets.VBox(children=[self.figure2, self.frame_slider, self.init_button]) # Main view tab = LazyTab( From a66b4761f05406640eba72f439030636d3ce9f14 Mon Sep 17 00:00:00 2001 From: CodyCBakerPhD Date: Tue, 25 Oct 2022 21:14:32 +0000 Subject: [PATCH 06/20] debugged --- nwbwidgets/ophys.py | 178 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 148 insertions(+), 30 deletions(-) diff --git a/nwbwidgets/ophys.py b/nwbwidgets/ophys.py index 7b6de041..9bb24fcb 100644 --- a/nwbwidgets/ophys.py +++ b/nwbwidgets/ophys.py @@ -2,7 +2,10 @@ import numpy as np from skimage import measure from multiprocessing import Process, Value +from typing import Tuple, Optional +from functools import lru_cache +import h5py import ipywidgets as widgets import plotly.graph_objects as go import plotly.express as px @@ -27,7 +30,146 @@ color_wheel = ["red", "blue", "green", "black", "magenta", "yellow"] +class TwoPhotonSeriesVolumetricPlaneSliceWidget(widgets.VBox): + """Sub-widget specifically for plane-wise views of a 4D TwoPhotonSeries.""" + def __init__(self, two_photon_series: TwoPhotonSeries): + num_dimensions = len(two_photon_series.data.shape) + if num_dimensions != 4: + raise ValueError(f"The TwoPhotonSeriesVolumetricPlaneSliceWidget is only appropriate for use on 4-dimensional TwoPhotonSeries! Detected dimension of {num_dimensions}.") + + super().__init__() + + self.two_photon_series = two_photon_series + + self.setup_figure() + self.setup_controllers() + self.setup_observers() + + self.children=[self.figure, self.controllers_box] + + def setup_figure(self): + """Basic setup for the figure layout.""" + self.current_data = self.two_photon_series.data[0, :, :, 0].T + + image = px.imshow(self.current_data, binary_string=True) + image.update_traces(hovertemplate=None, hoverinfo='skip') + self.figure = go.FigureWidget(image) + self.figure.layout.title = f"TwoPhotonSeries: {self.two_photon_series.name} - Planar slices of volume" + self.figure.update_xaxes(visible=False, showticklabels=False).update_yaxes(visible=False, showticklabels=False) + + def setup_controllers(self): + """Setup all controllers for the widget.""" + # Frame and plane controllers + self.frame_slider = widgets.IntSlider( + value=0, + min=0, + max=self.two_photon_series.data.shape[0] - 1, + orientation="horizontal", + description="Frame: ", + continuous_update=False, + ) + self.plane_slider = widgets.IntSlider( + value=0, + min=0, + max=self.two_photon_series.data.shape[-1] - 1, + orientation="horizontal", + description="Plane: ", + continuous_update=False, + ) + self.frame_and_plane_controller_box = widgets.VBox(children=[self.frame_slider, self.plane_slider]) + + # Contrast controllers + self.manual_contrast_checkbox = widgets.Checkbox(value=False, description="Enable Manual Contrast: ") + self.auto_contrast_method = widgets.Dropdown(options=["minmax", "infer"], description="Automatic Contrast Method: ") + initial_min = np.min(self.current_data) + initial_max = np.max(self.current_data) + self.contrast_slider = widgets.IntRangeSlider( + value=(initial_min, initial_max), + min=initial_min, + max=initial_max, + orientation="horizontal", + description="Manual Contrast Range: ", + continuous_update=False, + ) + self.contrast_controller_box = widgets.VBox(children=[self.manual_contrast_checkbox, self.auto_contrast_method]) + + self.controllers_box = widgets.HBox(children=[self.frame_and_plane_controller_box, self.contrast_controller_box]) + + def setup_observers(self): + """Given all of the controllers have been initialized and all the update routines have been defined, setup the observer rules for updates on each controller.""" + self.frame_slider.observe( + lambda change: self.update_plane_slice_figure(frame_index=change.new), names="value" + ) + self.plane_slider.observe( + lambda change: self.update_plane_slice_figure(plane_index=change.new), names="value" + ) + + self.manual_contrast_checkbox.observe( + lambda change: self.switch_contrast_modes(enable_manual_contrast=change.new), names="value" + ) + self.auto_contrast_method.observe( + lambda change: self.update_plane_slice_figure(contrast_rescaling=change.new), names="value" + ) + self.contrast_slider.observe( + lambda change: self.update_plane_slice_figure(contrast=change.new), names="value" + ) + + def switch_contrast_modes(self, enable_manual_contrast: bool): + """If the manual contrast checkbox is altered, adjust the manual vs. automatic disabling of the correpsonding controllers.""" + if enable_manual_contrast: + self.contrast_controller_box.children = [self.manual_contrast_checkbox, self.contrast_slider] + self.update_plane_slice_figure(contrast=self.contrast_slider.value) + else: + self.contrast_controller_box.children[1] = [self.manual_contrast_checkbox, self.contrast_slider] + self.update_plane_slice_figure(contrast_rescaling=self.auto_contrast_method.value) + + def update_contrast_range_per_frame_and_plane(self): + """ + If either of the frame or plane sliders are changed, be sure to update the valid range of the manual contrast. + + Applies even if current hidden, in case user wants to enable it. + """ + self.contrast_slider.min = np.min(self.current_data) + self.contrast_slider.max = np.max(self.current_data) + self.contrast_slider.value = (max(self.contrast_slider.value[0], self.contrast_slider.min), min(self.contrast_slider.value[1], self.contrast_slider.max)) + + @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: int, plane_index: int): + self.current_data = self._cache_data_read(dataset=self.two_photon_series.data, frame_index=frame_index, plane_index=plane_index) + self.update_contrast_range_per_frame_and_plane() + + def update_plane_slice_figure( + self, + frame_index:Optional[int]=None, + plane_index: Optional[int]=None, + contrast_rescaling: Optional[str] = None, + contrast: Optional[Tuple[int]]=None, + ): + """Primary update/generation method of the main figure.""" + update_data_region = True if frame_index is not None or plane_index is not None else False + + frame_index = frame_index or self.frame_slider.value + plane_index = plane_index or self.plane_slider.value + contrast_rescaling = contrast_rescaling or self.auto_contrast_method.value + contrast = contrast or self.contrast_slider.value + + if update_data_region: + self.update_data(frame_index=frame_index, plane_index=plane_index) + + img_fig_kwargs = dict(binary_string=True) + if self.manual_contrast_checkbox.value: # Manual contrast + img_fig_kwargs.update(zmin=contrast[0], zmax=contrast[1]) + else: + img_fig_kwargs.update(contrast_rescaling=contrast_rescaling) + + image = px.imshow(self.current_data, **img_fig_kwargs) + image.update_traces(hovertemplate=None, hoverinfo='skip') + self.figure.data[0].update(image.data[0]) + class TwoPhotonSeriesWidget(widgets.VBox): """Widget showing Image stack recorded over time from 2-photon microscope.""" @@ -91,33 +233,10 @@ def update_figure(index=0): elif len(indexed_timeseries.data.shape) == 4: - self.figure2 = None - self.plane_slider = None - - # Planar Slice tab - def update_plane_slice_figure(frame_index=0, plane_index=0): - img_fig = px.imshow(indexed_timeseries.data[frame_index][:, :, plane_index].T, binary_string=True) - _add_fig_trace(img_fig, frame_index) + self.volume_figure = None def plot_plane_slices(indexed_timeseries: TwoPhotonSeries): - self.plane_slider = widgets.IntSlider( - value=0, - min=0, - max=indexed_timeseries.data.shape[-1] - 1, - orientation="horizontal", - description="Plane: ", - ) - - self.frame_slider.observe( - lambda change: update_plane_slice_figure(frame_index=change.new), names="value" - ) - self.plane_slider.observe( - lambda change: update_plane_slice_figure(plane_index=change.new), names="value" - ) - - update_plane_slice_figure() - self.figure.layout.title = f"{base_title} - planar view of volume" - return widgets.VBox(children=[self.figure, self.frame_slider, self.plane_slider]) + return TwoPhotonSeriesVolumetricPlaneSliceWidget(two_photon_series=indexed_timeseries) # Volume tab output = widgets.Output() @@ -134,17 +253,16 @@ def update_volume_figure(index=0): p3.show() def first_volume_render(index=0): - self.figure2 = output + self.volume_figure = output update_volume_figure(index=self.frame_slider.value) self.frame_slider.observe(lambda change: update_volume_figure(index=change.new), names="value") def plot_volume_init(indexed_timeseries: TwoPhotonSeries): self.init_button = widgets.Button(description="Render") self.init_button.on_click(first_volume_render) - #self.figure2 = init_button # Have an activation button instead of initial render attempt - self.figure2 = output - self.figure2.layout.title = f"{base_title} - interactive volume" - return widgets.VBox(children=[self.figure2, self.frame_slider, self.init_button]) + self.volume_figure = output + self.volume_figure.layout.title = f"{base_title} - interactive volume" + return widgets.VBox(children=[self.volume_figure, self.frame_slider, self.init_button]) # Main view tab = LazyTab( From aa3405dd255436032b94aa6d4d1ada08d3a184f2 Mon Sep 17 00:00:00 2001 From: CodyCBakerPhD Date: Mon, 31 Oct 2022 15:16:25 +0000 Subject: [PATCH 07/20] Shorten names --- nwbwidgets/ophys.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nwbwidgets/ophys.py b/nwbwidgets/ophys.py index 9bb24fcb..de966444 100644 --- a/nwbwidgets/ophys.py +++ b/nwbwidgets/ophys.py @@ -80,7 +80,7 @@ def setup_controllers(self): # Contrast controllers self.manual_contrast_checkbox = widgets.Checkbox(value=False, description="Enable Manual Contrast: ") - self.auto_contrast_method = widgets.Dropdown(options=["minmax", "infer"], description="Automatic Contrast Method: ") + self.auto_contrast_method = widgets.Dropdown(options=["minmax", "infer"], description="Method: ") initial_min = np.min(self.current_data) initial_max = np.max(self.current_data) self.contrast_slider = widgets.IntRangeSlider( @@ -88,7 +88,7 @@ def setup_controllers(self): min=initial_min, max=initial_max, orientation="horizontal", - description="Manual Contrast Range: ", + description="Range: ", continuous_update=False, ) self.contrast_controller_box = widgets.VBox(children=[self.manual_contrast_checkbox, self.auto_contrast_method]) @@ -120,7 +120,7 @@ def switch_contrast_modes(self, enable_manual_contrast: bool): self.contrast_controller_box.children = [self.manual_contrast_checkbox, self.contrast_slider] self.update_plane_slice_figure(contrast=self.contrast_slider.value) else: - self.contrast_controller_box.children[1] = [self.manual_contrast_checkbox, self.contrast_slider] + self.contrast_controller_box.children = [self.manual_contrast_checkbox, self.auto_contrast_method] self.update_plane_slice_figure(contrast_rescaling=self.auto_contrast_method.value) def update_contrast_range_per_frame_and_plane(self): From 5e4027130857b33dc127c662156530ce282e85ec Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Fri, 2 Dec 2022 13:02:26 -0500 Subject: [PATCH 08/20] refactoring for new structure --- nwbwidgets/ophys.py | 281 ++++++++++++++++++++++++++++++-------------- 1 file changed, 193 insertions(+), 88 deletions(-) diff --git a/nwbwidgets/ophys.py b/nwbwidgets/ophys.py index 1bf757ec..dcde9f6d 100644 --- a/nwbwidgets/ophys.py +++ b/nwbwidgets/ophys.py @@ -1,3 +1,4 @@ +import math from functools import lru_cache import numpy as np from skimage import measure @@ -28,103 +29,129 @@ from .utils.dynamictable import infer_categorical_columns from .controllers import ProgressBar -import plotly.express as px - color_wheel = px.colors.qualitative.Dark24 -class TwoPhotonSeriesVolumetricPlaneSliceWidget(widgets.VBox): - """Sub-widget specifically for plane-wise views of a 4D TwoPhotonSeries.""" - def __init__(self, two_photon_series: TwoPhotonSeries): - num_dimensions = len(two_photon_series.data.shape) - if num_dimensions != 4: - raise ValueError(f"The TwoPhotonSeriesVolumetricPlaneSliceWidget is only appropriate for use on 4-dimensional TwoPhotonSeries! Detected dimension of {num_dimensions}.") - - super().__init__() - - self.two_photon_series = two_photon_series - - self.setup_figure() - self.setup_controllers() - self.setup_observers() - - self.children=[self.figure, self.controllers_box] - def setup_figure(self): - """Basic setup for the figure layout.""" - self.current_data = self.two_photon_series.data[0, :, :, 0].T +class FrameController(widgets.VBox): + controller_fields = "frame_slider" - image = px.imshow(self.current_data, binary_string=True) - image.update_traces(hovertemplate=None, hoverinfo='skip') - self.figure = go.FigureWidget(image) - self.figure.layout.title = f"TwoPhotonSeries: {self.two_photon_series.name} - Planar slices of volume" - self.figure.update_xaxes(visible=False, showticklabels=False).update_yaxes(visible=False, showticklabels=False) - - def setup_controllers(self): - """Setup all controllers for the widget.""" - # Frame and plane controllers + def __init__(self): self.frame_slider = widgets.IntSlider( value=0, min=0, - max=self.two_photon_series.data.shape[0] - 1, + max=1, orientation="horizontal", description="Frame: ", continuous_update=False, ) + + +class VolumetricDataController(widgets.VBox): + controller_fields = ("frame_slider", "plane_slider") + + def __init__(self): + self.frame_slider = FrameController() self.plane_slider = widgets.IntSlider( value=0, min=0, - max=self.two_photon_series.data.shape[-1] - 1, + max=1, orientation="horizontal", description="Plane: ", continuous_update=False, ) - self.frame_and_plane_controller_box = widgets.VBox(children=[self.frame_slider, self.plane_slider]) - # Contrast controllers - self.manual_contrast_checkbox = widgets.Checkbox(value=False, description="Enable Manual Contrast: ") - self.auto_contrast_method = widgets.Dropdown(options=["minmax", "infer"], description="Method: ") - initial_min = np.min(self.current_data) - initial_max = np.max(self.current_data) + self.children = [self.frame_slider, self.plane_slider] + + +class ImShowController(widgets.VBox): + """Controller specifically for handling various options for the plot.express.imshow function.""" + + controller_fields = ("frame_slider", "plane_slider") + + def __init__(self): + self.manual_contrast_toggle = widgets.ToggleButtons( + description="Constrast: ", options=[("Automatic", 0), ("Manual", 1)] + ) + self.auto_contrast_method = widgets.Dropdown(description="Method: ", options=["minmax", "infer"]) self.contrast_slider = widgets.IntRangeSlider( - value=(initial_min, initial_max), - min=initial_min, - max=initial_max, + value=(0, 1), # True value will depend on data selection + min=0, # True value will depend on data selection + max=1, # True value will depend on data selection orientation="horizontal", description="Range: ", continuous_update=False, ) - self.contrast_controller_box = widgets.VBox(children=[self.manual_contrast_checkbox, self.auto_contrast_method]) - - self.controllers_box = widgets.HBox(children=[self.frame_and_plane_controller_box, self.contrast_controller_box]) - def setup_observers(self): - """Given all of the controllers have been initialized and all the update routines have been defined, setup the observer rules for updates on each controller.""" - self.frame_slider.observe( - lambda change: self.update_plane_slice_figure(frame_index=change.new), names="value" - ) - self.plane_slider.observe( - lambda change: self.update_plane_slice_figure(plane_index=change.new), names="value" - ) + # Setup initial controller-specific layout + self.children[0] = self.auto_contrast_method + # self.children = [self.manual_contrast_toggle, self.auto_contrast_method] + + # Setup controller-specific observer events + self.setup_observers() - self.manual_contrast_checkbox.observe( + def setup_observers(self): + self.manual_contrast_toggle.observe( lambda change: self.switch_contrast_modes(enable_manual_contrast=change.new), names="value" ) - self.auto_contrast_method.observe( - lambda change: self.update_plane_slice_figure(contrast_rescaling=change.new), names="value" - ) - self.contrast_slider.observe( - lambda change: self.update_plane_slice_figure(contrast=change.new), names="value" - ) def switch_contrast_modes(self, enable_manual_contrast: bool): - """If the manual contrast checkbox is altered, adjust the manual vs. automatic disabling of the correpsonding controllers.""" - if enable_manual_contrast: - self.contrast_controller_box.children = [self.manual_contrast_checkbox, self.contrast_slider] - self.update_plane_slice_figure(contrast=self.contrast_slider.value) + """When the manual contrast toggle is altered, adjust the manual vs. automatic visibility of the components.""" + if self.manual_contrast_toggle: + self.children[1] = self.auto_contrast_method + # self.children = [self.manual_contrast_toggle, self.contrast_slider] else: - self.contrast_controller_box.children = [self.manual_contrast_checkbox, self.auto_contrast_method] - self.update_plane_slice_figure(contrast_rescaling=self.auto_contrast_method.value) + self.chdilren[1] = self.contrast_slider + # self.children = [self.manual_contrast_toggle, self.auto_contrast_method] + + +class MultiController: + def __init__(self, components: list): + self.controller_box = None + self.components = {component.__name__: component for component in components} + for component in self.components: + for field in component.controller_fields: + self.setattr(field, getattr(component, field)) + self.setup_observers() + + def setup_observers(self): + pass + + +class VolumetricPlaneSliceController(MultiController): + simplified_or_detailed_view = widgets.ToggleButtons(options=[("Simplified", 0), ("Detailed", 1)]) + + controllers = [simplified_or_detailed_view, VolumetricDataController(), ImShowController()] + + +class PlaneSliceVizualization(widgets.VBox): + """Sub-widget specifically for plane-wise views of a 4D TwoPhotonSeries.""" + + def __init__(self, two_photon_series: TwoPhotonSeries): + # num_dimensions = len(two_photon_series.data.shape) + # if num_dimensions != 4: + # raise ValueError( + # "The TwoPhotonSeriesVolumetricPlaneSliceWidget is only appropriate for " + # f"use on 4-dimensional TwoPhotonSeries! Detected dimension of {num_dimensions}." + # ) + + super().__init__() + self.two_photon_series = two_photon_series + + self.setup_data() + self.setup_data_to_plot() + + self.setup_controllers() + self.setup_canvas() + + self.setup_observers() + + # Setup layout of Canvas relative to Controllers + self.children = [self.Canvas, self.Controller.controller_box] + + @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_contrast_range_per_frame_and_plane(self): """ @@ -134,45 +161,122 @@ def update_contrast_range_per_frame_and_plane(self): """ self.contrast_slider.min = np.min(self.current_data) self.contrast_slider.max = np.max(self.current_data) - self.contrast_slider.value = (max(self.contrast_slider.value[0], self.contrast_slider.min), min(self.contrast_slider.value[1], self.contrast_slider.max)) - - @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 + self.contrast_slider.value = ( + max(self.contrast_slider.value[0], self.contrast_slider.min), + min(self.contrast_slider.value[1], self.contrast_slider.max), + ) def update_data(self, frame_index: int, plane_index: int): - self.current_data = self._cache_data_read(dataset=self.two_photon_series.data, frame_index=frame_index, plane_index=plane_index) + self.data = self._cache_data_read( + dataset=self.two_photon_series.data, frame_index=frame_index, plane_index=plane_index + ) self.update_contrast_range_per_frame_and_plane() - - def update_plane_slice_figure( + self.data = self.two_photon_series.data[frame_index, :, :, plane_index] + + 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 update_data_to_plot(self, frame_index: int, plane_index: int): + self.data_to_plot = self.data.T + + def setup_data_to_plot(self): + self.update_data_to_plot() + + def setup_controllers(self): + """Controller updates are handled through the defined Controller class.""" + self.Controller = VolumetricPlaneSliceController() + + # Setup layout of controllers relative to each other + self.Controller.controller_box = widgets.VBox( + [self.Controller.components["simplified_or_detailed_view"]], + widgets.HBox( + children=[ + self.Controller.components["VolumetricDataController"], + self.Controller.components["ImShowController"], + ] + ), + ) + + # Set some initial values based on neurodata object and initial data to plot + self.Controller.frame_slider.max = self.two_photon_series.data.shape[0] - 1 + self.Controller.plane_slider.max = self.two_photon_series.data.shape[-1] - 1 + self.Controller.contrast_slider.min = np.min(self.data_to_plot) + self.Controller.contrast_slider.min = np.max(self.data_to_plot) + + def update_figure( self, - frame_index:Optional[int]=None, - plane_index: Optional[int]=None, + frame_index: Optional[int] = None, + plane_index: Optional[int] = None, contrast_rescaling: Optional[str] = None, - contrast: Optional[Tuple[int]]=None, + contrast: Optional[Tuple[int]] = None, ): - """Primary update/generation method of the main figure.""" - update_data_region = True if frame_index is not None or plane_index is not None else False - frame_index = frame_index or self.frame_slider.value plane_index = plane_index or self.plane_slider.value contrast_rescaling = contrast_rescaling or self.auto_contrast_method.value contrast = contrast or self.contrast_slider.value - if update_data_region: - self.update_data(frame_index=frame_index, plane_index=plane_index) - img_fig_kwargs = dict(binary_string=True) - if self.manual_contrast_checkbox.value: # Manual contrast + if self.manual_contrast_toggle.value: # Manual contrast img_fig_kwargs.update(zmin=contrast[0], zmax=contrast[1]) else: img_fig_kwargs.update(contrast_rescaling=contrast_rescaling) - image = px.imshow(self.current_data, **img_fig_kwargs) - image.update_traces(hovertemplate=None, hoverinfo='skip') - self.figure.data[0].update(image.data[0]) + self.figure = px.imshow(self.current_data, **img_fig_kwargs) + self.figure.update_traces(hovertemplate=None, hoverinfo="skip") + + def update_canvas( + self, + frame_index: Optional[int] = None, + plane_index: Optional[int] = None, + contrast_rescaling: Optional[str] = None, + contrast: Optional[Tuple[int]] = None, + ): + self.update_figure( + frame_index=frame_index, plane_index=plane_index, contrast_rescaling=contrast_rescaling, contrast=contrast + ) + self.Canvas.data[0].update(self.figure.data[0]) + + def setup_canvas( + self, + frame_index: Optional[int] = None, + plane_index: Optional[int] = None, + contrast_rescaling: Optional[str] = None, + contrast: Optional[Tuple[int]] = None, + ): + self.update_figure() + self.Canvas = go.FigureWidget(self.figure) + self.Canvas.layout.title = f"TwoPhotonSeries: {self.two_photon_series.name} - Planar slices of volume" + self.Canvas.update_xaxes(visible=False, showticklabels=False).update_yaxes(visible=False, showticklabels=False) + + def setup_observers(self): + self.frame_slider.observe(lambda change: self.update_plane_slice_figure(frame_index=change.new), names="value") + self.plane_slider.observe(lambda change: self.update_plane_slice_figure(plane_index=change.new), names="value") + + self.manual_contrast_checkbox.observe(lambda change: self.update_canvas(contrast=change.new), names="value") + self.auto_contrast_method.observe( + lambda change: self.update_canvas(contrast_rescaling=change.new), names="value" + ) + self.contrast_slider.observe(lambda change: self.update_canvas(contrast=change.new), names="value") + - class TwoPhotonSeriesWidget(widgets.VBox): """Widget showing Image stack recorded over time from 2-photon microscope.""" @@ -239,10 +343,11 @@ def update_figure(index=0): self.volume_figure = None def plot_plane_slices(indexed_timeseries: TwoPhotonSeries): - return TwoPhotonSeriesVolumetricPlaneSliceWidget(two_photon_series=indexed_timeseries) + return PlaneSliceVizualization(two_photon_series=indexed_timeseries) # Volume tab output = widgets.Output() + def update_volume_figure(index=0): import ipyvolume.pylab as p3 From 20a52f30c1eb64d3d903d3139980c8ed7c9743dc Mon Sep 17 00:00:00 2001 From: CodyCBakerPhD Date: Fri, 2 Dec 2022 20:01:03 +0000 Subject: [PATCH 09/20] save state --- nwbwidgets/ophys.py | 178 ++++++++++++++++++++++++++++---------------- 1 file changed, 113 insertions(+), 65 deletions(-) diff --git a/nwbwidgets/ophys.py b/nwbwidgets/ophys.py index dcde9f6d..09432a40 100644 --- a/nwbwidgets/ophys.py +++ b/nwbwidgets/ophys.py @@ -3,7 +3,7 @@ import numpy as np from skimage import measure from multiprocessing import Process, Value -from typing import Tuple, Optional +from typing import Tuple, Optional, List, Dict from functools import lru_cache import h5py @@ -34,58 +34,65 @@ class FrameController(widgets.VBox): - controller_fields = "frame_slider" + controller_fields = ("frame_slider",) def __init__(self): + super().__init__() + self.frame_slider = widgets.IntSlider( - value=0, - min=0, - max=1, + 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 VolumetricDataController(widgets.VBox): - controller_fields = ("frame_slider", "plane_slider") +class PlaneController(widgets.VBox): + controller_fields = ("plane_slider",) def __init__(self): - self.frame_slider = FrameController() + super().__init__() + self.plane_slider = widgets.IntSlider( - value=0, - min=0, - max=1, + 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.frame_slider, self.plane_slider] + + self.children = (self.plane_slider,) + class ImShowController(widgets.VBox): """Controller specifically for handling various options for the plot.express.imshow function.""" - controller_fields = ("frame_slider", "plane_slider") + controller_fields = ("manual_contrast_toggle", "auto_contrast_method", "contrast_slider") def __init__(self): + super().__init__() + self.manual_contrast_toggle = widgets.ToggleButtons( - description="Constrast: ", options=[("Automatic", 0), ("Manual", 1)] + 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.contrast_slider = widgets.IntRangeSlider( - value=(0, 1), # True value will depend on data selection - min=0, # True value will depend on data selection - max=1, # True value will depend on data selection + 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[0] = self.auto_contrast_method - # self.children = [self.manual_contrast_toggle, self.auto_contrast_method] + self.children = (self.manual_contrast_toggle, self.auto_contrast_method) # Setup controller-specific observer events self.setup_observers() @@ -97,31 +104,58 @@ def setup_observers(self): 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.manual_contrast_toggle: - self.children[1] = self.auto_contrast_method - # self.children = [self.manual_contrast_toggle, self.contrast_slider] + if self.manual_contrast_toggle.value == "Manual": + self.children = (self.manual_contrast_toggle, self.contrast_slider) else: - self.chdilren[1] = self.contrast_slider - # self.children = [self.manual_contrast_toggle, self.auto_contrast_method] + self.children = (self.manual_contrast_toggle, self.auto_contrast_method) +class ViewTypeController(widgets.VBox): + controller_fields = ("view_type_toggle",) -class MultiController: + def __init__(self): + super().__init__() + + self.view_type_toggle = widgets.ToggleButtons( + options=[("Simplified", "Simplified"), ("Detailed", "Detailed")], # Values set to strings for external readability + ) + self.children = (self.view_type_toggle,) + + +class MultiController(widgets.VBox): + controller_fields: Tuple[str] = tuple() + components: Dict[str, widgets.VBox] = dict() + def __init__(self, components: list): - self.controller_box = None - self.components = {component.__name__: component for component in components} - for component in self.components: + 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: - self.setattr(field, getattr(component, field)) + controller_fields.append(field) + setattr(self, field, getattr(component, field)) + + # Default layout of children + if 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 - +class VolumetricDataController(MultiController): + def __init__(self): + super().__init__(components=[FrameController(), PlaneController()]) + class VolumetricPlaneSliceController(MultiController): - simplified_or_detailed_view = widgets.ToggleButtons(options=[("Simplified", 0), ("Detailed", 1)]) - - controllers = [simplified_or_detailed_view, VolumetricDataController(), ImShowController()] + def __init__(self): + super().__init__(components=[ViewTypeController(), VolumetricDataController(), ImShowController()]) class PlaneSliceVizualization(widgets.VBox): @@ -147,31 +181,37 @@ def __init__(self, two_photon_series: TwoPhotonSeries): self.setup_observers() # Setup layout of Canvas relative to Controllers - self.children = [self.Canvas, self.Controller.controller_box] + self.children = [self.Canvas, self.Controller] @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_contrast_range_per_frame_and_plane(self): + def update_contrast_range(self): """ If either of the frame or plane sliders are changed, be sure to update the valid range of the manual contrast. Applies even if current hidden, in case user wants to enable it. """ - self.contrast_slider.min = np.min(self.current_data) - self.contrast_slider.max = np.max(self.current_data) - self.contrast_slider.value = ( - max(self.contrast_slider.value[0], self.contrast_slider.min), - min(self.contrast_slider.value[1], self.contrast_slider.max), + self.Controller.contrast_slider.min = np.min(self.data) + self.Controller.contrast_slider.max = np.max(self.data) + self.Controller.contrast_slider.value = ( + max(self.Controller.contrast_slider.value[0], self.Controller.contrast_slider.min), + min(self.Controller.contrast_slider.value[1], self.Controller.contrast_slider.max), ) - def update_data(self, frame_index: int, plane_index: int): + 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.update_contrast_range_per_frame_and_plane() + self.data = self.two_photon_series.data[frame_index, :, :, plane_index] + + if self.Controller.manual_contrast_toggle.value == "Manual": + self.update_contrast_range() def setup_data(self, max_mb_treshold: float = 20.0): """ @@ -194,7 +234,7 @@ def setup_data(self, max_mb_treshold: float = 20.0): by_height = 2 self.data = self.two_photon_series.data[0, ::by_width, ::by_height, 0] - def update_data_to_plot(self, frame_index: int, plane_index: int): + def update_data_to_plot(self): self.data_to_plot = self.data.T def setup_data_to_plot(self): @@ -205,21 +245,25 @@ def setup_controllers(self): self.Controller = VolumetricPlaneSliceController() # Setup layout of controllers relative to each other - self.Controller.controller_box = widgets.VBox( - [self.Controller.components["simplified_or_detailed_view"]], - widgets.HBox( + self.Controller.children = [ + widgets.VBox( children=[ - self.Controller.components["VolumetricDataController"], - self.Controller.components["ImShowController"], + self.Controller.components["ViewTypeController"], + widgets.HBox( + children=[ + self.Controller.components["VolumetricDataController"], + self.Controller.components["ImShowController"], + ] + ), ] - ), - ) + ) + ] # Set some initial values based on neurodata object and initial data to plot self.Controller.frame_slider.max = self.two_photon_series.data.shape[0] - 1 self.Controller.plane_slider.max = self.two_photon_series.data.shape[-1] - 1 + self.Controller.contrast_slider.max = np.max(self.data_to_plot) self.Controller.contrast_slider.min = np.min(self.data_to_plot) - self.Controller.contrast_slider.min = np.max(self.data_to_plot) def update_figure( self, @@ -228,18 +272,22 @@ def update_figure( contrast_rescaling: Optional[str] = None, contrast: Optional[Tuple[int]] = None, ): - frame_index = frame_index or self.frame_slider.value - plane_index = plane_index or self.plane_slider.value - contrast_rescaling = contrast_rescaling or self.auto_contrast_method.value - contrast = contrast or self.contrast_slider.value - + if frame_index is not None or plane_index is not None: + self.update_data(frame_index=frame_index, plane_index=plane_index) + self.update_data_to_plot() + + frame_index = frame_index or self.Controller.frame_slider.value + plane_index = plane_index or self.Controller.plane_slider.value + contrast_rescaling = contrast_rescaling or self.Controller.auto_contrast_method.value + contrast = contrast or self.Controller.contrast_slider.value + img_fig_kwargs = dict(binary_string=True) - if self.manual_contrast_toggle.value: # Manual contrast + if self.Controller.manual_contrast_toggle.value == "Manual": img_fig_kwargs.update(zmin=contrast[0], zmax=contrast[1]) else: img_fig_kwargs.update(contrast_rescaling=contrast_rescaling) - self.figure = px.imshow(self.current_data, **img_fig_kwargs) + self.figure = px.imshow(self.data_to_plot, **img_fig_kwargs) self.figure.update_traces(hovertemplate=None, hoverinfo="skip") def update_canvas( @@ -267,14 +315,14 @@ def setup_canvas( self.Canvas.update_xaxes(visible=False, showticklabels=False).update_yaxes(visible=False, showticklabels=False) def setup_observers(self): - self.frame_slider.observe(lambda change: self.update_plane_slice_figure(frame_index=change.new), names="value") - self.plane_slider.observe(lambda change: self.update_plane_slice_figure(plane_index=change.new), names="value") + self.Controller.frame_slider.observe(lambda change: self.update_canvas(frame_index=change.new), names="value") + self.Controller.plane_slider.observe(lambda change: self.update_canvas(plane_index=change.new), names="value") - self.manual_contrast_checkbox.observe(lambda change: self.update_canvas(contrast=change.new), names="value") - self.auto_contrast_method.observe( + self.Controller.view_type_toggle.observe(lambda change: self.update_canvas(contrast=change.new), names="value") + self.Controller.auto_contrast_method.observe( lambda change: self.update_canvas(contrast_rescaling=change.new), names="value" ) - self.contrast_slider.observe(lambda change: self.update_canvas(contrast=change.new), names="value") + self.Controller.contrast_slider.observe(lambda change: self.update_canvas(contrast=change.new), names="value") class TwoPhotonSeriesWidget(widgets.VBox): From 100f2f24c107d4edb3ac12b2ef72b99bc5a25a3c Mon Sep 17 00:00:00 2001 From: CodyCBakerPhD Date: Fri, 2 Dec 2022 21:32:56 +0000 Subject: [PATCH 10/20] saving state --- nwbwidgets/ophys.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nwbwidgets/ophys.py b/nwbwidgets/ophys.py index 09432a40..7edd2762 100644 --- a/nwbwidgets/ophys.py +++ b/nwbwidgets/ophys.py @@ -106,7 +106,7 @@ 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.manual_contrast_toggle.value == "Manual": self.children = (self.manual_contrast_toggle, self.contrast_slider) - else: + elif self.manual_contrast_toggle.value == "Automatic": self.children = (self.manual_contrast_toggle, self.auto_contrast_method) class ViewTypeController(widgets.VBox): @@ -193,8 +193,8 @@ def update_contrast_range(self): Applies even if current hidden, in case user wants to enable it. """ - self.Controller.contrast_slider.min = np.min(self.data) self.Controller.contrast_slider.max = np.max(self.data) + self.Controller.contrast_slider.min = np.min(self.data) self.Controller.contrast_slider.value = ( max(self.Controller.contrast_slider.value[0], self.Controller.contrast_slider.min), min(self.Controller.contrast_slider.value[1], self.Controller.contrast_slider.max), @@ -264,6 +264,7 @@ def setup_controllers(self): self.Controller.plane_slider.max = self.two_photon_series.data.shape[-1] - 1 self.Controller.contrast_slider.max = np.max(self.data_to_plot) self.Controller.contrast_slider.min = np.min(self.data_to_plot) + self.Controller.contrast_slider.value = (self.Controller.contrast_slider.min, self.Controller.contrast_slider.max) def update_figure( self, @@ -284,7 +285,7 @@ def update_figure( img_fig_kwargs = dict(binary_string=True) if self.Controller.manual_contrast_toggle.value == "Manual": img_fig_kwargs.update(zmin=contrast[0], zmax=contrast[1]) - else: + elif self.Controller.manual_contrast_toggle.value == "Automatic": img_fig_kwargs.update(contrast_rescaling=contrast_rescaling) self.figure = px.imshow(self.data_to_plot, **img_fig_kwargs) @@ -318,7 +319,7 @@ def setup_observers(self): self.Controller.frame_slider.observe(lambda change: self.update_canvas(frame_index=change.new), names="value") self.Controller.plane_slider.observe(lambda change: self.update_canvas(plane_index=change.new), names="value") - self.Controller.view_type_toggle.observe(lambda change: self.update_canvas(contrast=change.new), names="value") + self.Controller.view_type_toggle.observe(lambda change: self.update_canvas(), names="value") self.Controller.auto_contrast_method.observe( lambda change: self.update_canvas(contrast_rescaling=change.new), names="value" ) From c1c2eab0a6b6ddda0ca2910d392a9be45ec0bb2d Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Fri, 2 Dec 2022 16:41:44 -0500 Subject: [PATCH 11/20] black; rename some things --- nwbwidgets/ophys.py | 90 ++++++++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/nwbwidgets/ophys.py b/nwbwidgets/ophys.py index 7edd2762..e03f34df 100644 --- a/nwbwidgets/ophys.py +++ b/nwbwidgets/ophys.py @@ -47,9 +47,10 @@ def __init__(self): description="Frame: ", continuous_update=False, ) - + self.children = (self.frame_slider,) + class PlaneController(widgets.VBox): controller_fields = ("plane_slider",) @@ -64,25 +65,27 @@ def __init__(self): description="Plane: ", continuous_update=False, ) - + self.children = (self.plane_slider,) - class ImShowController(widgets.VBox): """Controller specifically for handling various options for the plot.express.imshow function.""" - controller_fields = ("manual_contrast_toggle", "auto_contrast_method", "contrast_slider") + controller_fields = ("contrast_type_toggle", "auto_contrast_method", "manual_contrast_slider") def __init__(self): super().__init__() - - self.manual_contrast_toggle = widgets.ToggleButtons( + + self.contrast_type_toggle = widgets.ToggleButtons( description="Constrast: ", - options=[("Automatic", "Automatic"), ("Manual", "Manual")], # Values set to strings for external readability + options=[ + ("Automatic", "Automatic"), + ("Manual", "Manual"), + ], # Values set to strings for external readability ) self.auto_contrast_method = widgets.Dropdown(description="Method: ", options=["minmax", "infer"]) - self.contrast_slider = widgets.IntRangeSlider( + 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 @@ -92,22 +95,23 @@ def __init__(self): ) # Setup initial controller-specific layout - self.children = (self.manual_contrast_toggle, self.auto_contrast_method) + self.children = (self.contrast_type_toggle, self.auto_contrast_method) # Setup controller-specific observer events self.setup_observers() def setup_observers(self): - self.manual_contrast_toggle.observe( + 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.manual_contrast_toggle.value == "Manual": - self.children = (self.manual_contrast_toggle, self.contrast_slider) - elif self.manual_contrast_toggle.value == "Automatic": - self.children = (self.manual_contrast_toggle, self.auto_contrast_method) + 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) + class ViewTypeController(widgets.VBox): controller_fields = ("view_type_toggle",) @@ -116,18 +120,21 @@ def __init__(self): super().__init__() self.view_type_toggle = widgets.ToggleButtons( - options=[("Simplified", "Simplified"), ("Detailed", "Detailed")], # Values set to strings for external readability + options=[ + ("Simplified", "Simplified"), + ("Detailed", "Detailed"), + ], # Values set to strings for external readability ) self.children = (self.view_type_toggle,) - - + + 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} @@ -140,19 +147,21 @@ def __init__(self, components: list): # Default layout of children if 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 + class VolumetricDataController(MultiController): def __init__(self): super().__init__(components=[FrameController(), PlaneController()]) - + + class VolumetricPlaneSliceController(MultiController): def __init__(self): super().__init__(components=[ViewTypeController(), VolumetricDataController(), ImShowController()]) @@ -193,11 +202,11 @@ def update_contrast_range(self): Applies even if current hidden, in case user wants to enable it. """ - self.Controller.contrast_slider.max = np.max(self.data) - self.Controller.contrast_slider.min = np.min(self.data) - self.Controller.contrast_slider.value = ( - max(self.Controller.contrast_slider.value[0], self.Controller.contrast_slider.min), - min(self.Controller.contrast_slider.value[1], self.Controller.contrast_slider.max), + self.Controller.manual_contrast_slider.max = np.max(self.data) + self.Controller.manual_contrast_slider.min = np.min(self.data) + self.Controller.manual_contrast_slider.value = ( + max(self.Controller.manual_contrast_slider.value[0], self.Controller.manual_contrast_slider.min), + min(self.Controller.manual_contrast_slider.value[1], self.Controller.manual_contrast_slider.max), ) def update_data(self, frame_index: Optional[int] = None, plane_index: Optional[int] = None): @@ -209,8 +218,8 @@ def update_data(self, frame_index: Optional[int] = None, plane_index: Optional[i ) self.data = self.two_photon_series.data[frame_index, :, :, plane_index] - - if self.Controller.manual_contrast_toggle.value == "Manual": + + if self.Controller.contrast_type_toggle.value == "Manual": self.update_contrast_range() def setup_data(self, max_mb_treshold: float = 20.0): @@ -262,9 +271,12 @@ def setup_controllers(self): # Set some initial values based on neurodata object and initial data to plot self.Controller.frame_slider.max = self.two_photon_series.data.shape[0] - 1 self.Controller.plane_slider.max = self.two_photon_series.data.shape[-1] - 1 - self.Controller.contrast_slider.max = np.max(self.data_to_plot) - self.Controller.contrast_slider.min = np.min(self.data_to_plot) - self.Controller.contrast_slider.value = (self.Controller.contrast_slider.min, self.Controller.contrast_slider.max) + self.Controller.manual_contrast_slider.max = np.max(self.data_to_plot) + self.Controller.manual_contrast_slider.min = np.min(self.data_to_plot) + self.Controller.manual_contrast_slider.value = ( + self.Controller.manual_contrast_slider.min, + self.Controller.manual_contrast_slider.max, + ) def update_figure( self, @@ -276,16 +288,16 @@ def update_figure( if frame_index is not None or plane_index is not None: self.update_data(frame_index=frame_index, plane_index=plane_index) self.update_data_to_plot() - + frame_index = frame_index or self.Controller.frame_slider.value plane_index = plane_index or self.Controller.plane_slider.value contrast_rescaling = contrast_rescaling or self.Controller.auto_contrast_method.value - contrast = contrast or self.Controller.contrast_slider.value - + contrast = contrast or self.Controller.manual_contrast_slider.value + img_fig_kwargs = dict(binary_string=True) - if self.Controller.manual_contrast_toggle.value == "Manual": + if self.Controller.contrast_type_toggle.value == "Manual": img_fig_kwargs.update(zmin=contrast[0], zmax=contrast[1]) - elif self.Controller.manual_contrast_toggle.value == "Automatic": + elif self.Controller.contrast_type_toggle.value == "Automatic": img_fig_kwargs.update(contrast_rescaling=contrast_rescaling) self.figure = px.imshow(self.data_to_plot, **img_fig_kwargs) @@ -323,7 +335,9 @@ def setup_observers(self): self.Controller.auto_contrast_method.observe( lambda change: self.update_canvas(contrast_rescaling=change.new), names="value" ) - self.Controller.contrast_slider.observe(lambda change: self.update_canvas(contrast=change.new), names="value") + self.Controller.manual_contrast_slider.observe( + lambda change: self.update_canvas(contrast=change.new), names="value" + ) class TwoPhotonSeriesWidget(widgets.VBox): From 0aaefc4a59e59cee95bca9adc0f25472d53fe374 Mon Sep 17 00:00:00 2001 From: CodyCBakerPhD Date: Fri, 2 Dec 2022 23:11:02 +0000 Subject: [PATCH 12/20] saving state --- nwbwidgets/ophys.py | 94 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 16 deletions(-) diff --git a/nwbwidgets/ophys.py b/nwbwidgets/ophys.py index e03f34df..0203c90f 100644 --- a/nwbwidgets/ophys.py +++ b/nwbwidgets/ophys.py @@ -33,6 +33,31 @@ color_wheel = px.colors.qualitative.Dark24 +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 FrameController(widgets.VBox): controller_fields = ("frame_slider",) @@ -145,7 +170,7 @@ def __init__(self, components: list): setattr(self, field, getattr(component, field)) # Default layout of children - if not isinstance(component, MultiController): + if isinstance(component, widgets.Widget) and not isinstance(component, MultiController): children.append(component) self.children = tuple(children) @@ -159,13 +184,37 @@ def setup_observers(self): class VolumetricDataController(MultiController): def __init__(self): - super().__init__(components=[FrameController(), PlaneController()]) + super().__init__(components=[RotationController(), FrameController(), PlaneController()]) + # Align rotation buttons to center of sliders + self.layout.align_items = "center" class VolumetricPlaneSliceController(MultiController): def __init__(self): super().__init__(components=[ViewTypeController(), VolumetricDataController(), ImShowController()]) + 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()) + class PlaneSliceVizualization(widgets.VBox): """Sub-widget specifically for plane-wise views of a 4D TwoPhotonSeries.""" @@ -180,6 +229,7 @@ def __init__(self, two_photon_series: TwoPhotonSeries): super().__init__() self.two_photon_series = two_photon_series + self.rotation = 0 self.setup_data() self.setup_data_to_plot() @@ -242,9 +292,18 @@ def setup_data(self, max_mb_treshold: float = 20.0): by_width = 2 by_height = 2 self.data = self.two_photon_series.data[0, ::by_width, ::by_height, 0] - + def update_data_to_plot(self): - self.data_to_plot = self.data.T + rotation = self.Controller.components["VolumetricDataController"].components["RotationController"].rotation if hasattr(self, "Controller") else 0 + rotation_mod = rotation % 4 # Only supporting 90 degree increments + if rotation_mod == 0: + self.data_to_plot = self.data + elif rotation_mod == 1: + self.data_to_plot = self.data.T + elif rotation_mod == 2: + self.data_to_plot = np.flip(self.data) + elif rotation_mod == 3: + self.data_to_plot = np.flip(self.data.T) def setup_data_to_plot(self): self.update_data_to_plot() @@ -280,17 +339,18 @@ def setup_controllers(self): 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 frame_index is not None or plane_index is not None: + if rotation_changed is not None: + self.update_data_to_plot() + elif frame_index is not None or plane_index is not None: self.update_data(frame_index=frame_index, plane_index=plane_index) self.update_data_to_plot() - frame_index = frame_index or self.Controller.frame_slider.value - plane_index = plane_index or self.Controller.plane_slider.value contrast_rescaling = contrast_rescaling or self.Controller.auto_contrast_method.value contrast = contrast or self.Controller.manual_contrast_slider.value @@ -305,33 +365,35 @@ def update_figure( def update_canvas( 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, ): self.update_figure( - frame_index=frame_index, plane_index=plane_index, contrast_rescaling=contrast_rescaling, contrast=contrast + rotation_changed=rotation_changed, frame_index=frame_index, plane_index=plane_index, contrast_rescaling=contrast_rescaling, contrast=contrast ) self.Canvas.data[0].update(self.figure.data[0]) - def setup_canvas( - self, - frame_index: Optional[int] = None, - plane_index: Optional[int] = None, - contrast_rescaling: Optional[str] = None, - contrast: Optional[Tuple[int]] = None, - ): + def setup_canvas(self): + # Setup main figure area self.update_figure() self.Canvas = go.FigureWidget(self.figure) self.Canvas.layout.title = f"TwoPhotonSeries: {self.two_photon_series.name} - Planar slices of volume" self.Canvas.update_xaxes(visible=False, showticklabels=False).update_yaxes(visible=False, showticklabels=False) + + # Final vizualization-specific setup of controller positions + # Move the Simplified/Detailed switch to the right part of screen + self.Controller.components["ViewTypeController"].layout.align_items = "flex-end" def setup_observers(self): + self.Controller.rotate_right.on_click(lambda change: self.update_canvas(rotation_changed=True)) + self.Controller.rotate_left.on_click(lambda change: self.update_canvas(rotation_changed=True)) self.Controller.frame_slider.observe(lambda change: self.update_canvas(frame_index=change.new), names="value") self.Controller.plane_slider.observe(lambda change: self.update_canvas(plane_index=change.new), names="value") - self.Controller.view_type_toggle.observe(lambda change: self.update_canvas(), names="value") + self.Controller.contrast_type_toggle.observe(lambda change: self.update_canvas(), names="value") self.Controller.auto_contrast_method.observe( lambda change: self.update_canvas(contrast_rescaling=change.new), names="value" ) From 7ee38fb2ac281b89a9a836d2a12615408bb23da7 Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Sat, 3 Dec 2022 13:37:19 -0500 Subject: [PATCH 13/20] saving state --- nwbwidgets/controllers/__init__.py | 4 +- nwbwidgets/controllers/image_controllers.py | 70 ++ nwbwidgets/controllers/misc.py | 32 +- nwbwidgets/controllers/multicontroller.py | 32 + nwbwidgets/ophys.py | 744 -------------------- nwbwidgets/ophys/__init__.py | 3 + nwbwidgets/ophys/plane_slice.py | 277 ++++++++ nwbwidgets/ophys/segmentation.py | 241 +++++++ nwbwidgets/ophys/single_plane.py | 94 +++ nwbwidgets/ophys/two_photon_series.py | 62 ++ nwbwidgets/ophys/volume.py | 35 + nwbwidgets/view.py | 2 +- 12 files changed, 837 insertions(+), 759 deletions(-) create mode 100644 nwbwidgets/controllers/image_controllers.py create mode 100644 nwbwidgets/controllers/multicontroller.py delete mode 100644 nwbwidgets/ophys.py create mode 100644 nwbwidgets/ophys/__init__.py create mode 100644 nwbwidgets/ophys/plane_slice.py create mode 100644 nwbwidgets/ophys/segmentation.py create mode 100644 nwbwidgets/ophys/single_plane.py create mode 100644 nwbwidgets/ophys/two_photon_series.py create mode 100644 nwbwidgets/ophys/volume.py diff --git a/nwbwidgets/controllers/__init__.py b/nwbwidgets/controllers/__init__.py index a689f9f5..2c7d5ca2 100644 --- a/nwbwidgets/controllers/__init__.py +++ b/nwbwidgets/controllers/__init__.py @@ -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 diff --git a/nwbwidgets/controllers/image_controllers.py b/nwbwidgets/controllers/image_controllers.py new file mode 100644 index 00000000..a74f8c21 --- /dev/null +++ b/nwbwidgets/controllers/image_controllers.py @@ -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) diff --git a/nwbwidgets/controllers/misc.py b/nwbwidgets/controllers/misc.py index 3e2401e0..27409cb5 100644 --- a/nwbwidgets/controllers/misc.py +++ b/nwbwidgets/controllers/misc.py @@ -11,15 +11,28 @@ def __init__(self, *arg, **kwargs): # self.container.children[0].layout = Layout(width="80%") +class ViewTypeController(widgets.VBox): + controller_fields = ("view_type_toggle",) + + def __init__(self): + super().__init__() + + self.view_type_toggle = widgets.ToggleButtons( + options=[ + ("Simplified", "Simplified"), + ("Detailed", "Detailed"), + ], # Values set to strings for external readability + ) + self.children = (self.view_type_toggle,) + + def make_trial_event_controller(trials, layout=None, multiple=False): - """Controller for which reference to use (e.g. start_time) when making time-aligned averages""" + """Controller for which reference to use (e.g. start_time) when making time-aligned averages.""" trial_events = ["start_time"] if not np.all(np.isnan(trials["stop_time"].data)): trial_events.append("stop_time") trial_events += [ - x.name - for x in trials.columns - if (("_time" in x.name) and (x.name not in ("start_time", "stop_time"))) + x.name for x in trials.columns if (("_time" in x.name) and (x.name not in ("start_time", "stop_time"))) ] kwargs = {} if layout is not None: @@ -27,17 +40,10 @@ def make_trial_event_controller(trials, layout=None, multiple=False): if multiple: trial_event_controller = widgets.SelectMultiple( - options=trial_events, - value=["start_time"], - description='align to:', - disabled=False, - **kwargs + options=trial_events, value=["start_time"], description="align to:", disabled=False, **kwargs ) else: trial_event_controller = widgets.Dropdown( - options=trial_events, - value="start_time", - description="align to: ", - **kwargs + options=trial_events, value="start_time", description="align to: ", **kwargs ) return trial_event_controller diff --git a/nwbwidgets/controllers/multicontroller.py b/nwbwidgets/controllers/multicontroller.py new file mode 100644 index 00000000..84a941b6 --- /dev/null +++ b/nwbwidgets/controllers/multicontroller.py @@ -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 diff --git a/nwbwidgets/ophys.py b/nwbwidgets/ophys.py deleted file mode 100644 index 0203c90f..00000000 --- a/nwbwidgets/ophys.py +++ /dev/null @@ -1,744 +0,0 @@ -import math -from functools import lru_cache -import numpy as np -from skimage import measure -from multiprocessing import Process, Value -from typing import Tuple, Optional, List, Dict -from functools import lru_cache - -import h5py -import ipywidgets as widgets -import plotly.graph_objects as go -import plotly.express as px - -from pynwb.base import NWBDataInterface -from pynwb.ophys import ( - RoiResponseSeries, - DfOverF, - PlaneSegmentation, - TwoPhotonSeries, - ImageSegmentation, -) - -from tifffile import imread, TiffFile -from ndx_grayscalevolume import GrayscaleVolume - -from .base import df_to_hover_text, LazyTab -from .timeseries import BaseGroupedTraceWidget -from .utils.cmaps import linear_transfer_function -from .utils.dynamictable import infer_categorical_columns -from .controllers import ProgressBar - - -color_wheel = px.colors.qualitative.Dark24 - - -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 FrameController(widgets.VBox): - 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): - 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 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) - - -class ViewTypeController(widgets.VBox): - controller_fields = ("view_type_toggle",) - - def __init__(self): - super().__init__() - - self.view_type_toggle = widgets.ToggleButtons( - options=[ - ("Simplified", "Simplified"), - ("Detailed", "Detailed"), - ], # Values set to strings for external readability - ) - self.children = (self.view_type_toggle,) - - -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 - - -class VolumetricDataController(MultiController): - def __init__(self): - super().__init__(components=[RotationController(), FrameController(), PlaneController()]) - - # Align rotation buttons to center of sliders - self.layout.align_items = "center" - -class VolumetricPlaneSliceController(MultiController): - def __init__(self): - super().__init__(components=[ViewTypeController(), VolumetricDataController(), ImShowController()]) - - 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()) - - -class PlaneSliceVizualization(widgets.VBox): - """Sub-widget specifically for plane-wise views of a 4D TwoPhotonSeries.""" - - def __init__(self, two_photon_series: TwoPhotonSeries): - # num_dimensions = len(two_photon_series.data.shape) - # if num_dimensions != 4: - # raise ValueError( - # "The TwoPhotonSeriesVolumetricPlaneSliceWidget is only appropriate for " - # f"use on 4-dimensional TwoPhotonSeries! Detected dimension of {num_dimensions}." - # ) - - super().__init__() - self.two_photon_series = two_photon_series - self.rotation = 0 - - self.setup_data() - self.setup_data_to_plot() - - self.setup_controllers() - self.setup_canvas() - - self.setup_observers() - - # Setup layout of Canvas relative to Controllers - self.children = [self.Canvas, self.Controller] - - @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_contrast_range(self): - """ - If either of the frame or plane sliders are changed, be sure to update the valid range of the manual contrast. - - Applies even if current hidden, in case user wants to enable it. - """ - self.Controller.manual_contrast_slider.max = np.max(self.data) - self.Controller.manual_contrast_slider.min = np.min(self.data) - self.Controller.manual_contrast_slider.value = ( - max(self.Controller.manual_contrast_slider.value[0], self.Controller.manual_contrast_slider.min), - min(self.Controller.manual_contrast_slider.value[1], self.Controller.manual_contrast_slider.max), - ) - - 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 update_data_to_plot(self): - rotation = self.Controller.components["VolumetricDataController"].components["RotationController"].rotation if hasattr(self, "Controller") else 0 - rotation_mod = rotation % 4 # Only supporting 90 degree increments - if rotation_mod == 0: - self.data_to_plot = self.data - elif rotation_mod == 1: - self.data_to_plot = self.data.T - elif rotation_mod == 2: - self.data_to_plot = np.flip(self.data) - elif rotation_mod == 3: - self.data_to_plot = np.flip(self.data.T) - - def setup_data_to_plot(self): - self.update_data_to_plot() - - def setup_controllers(self): - """Controller updates are handled through the defined Controller class.""" - self.Controller = VolumetricPlaneSliceController() - - # Setup layout of controllers relative to each other - self.Controller.children = [ - widgets.VBox( - children=[ - self.Controller.components["ViewTypeController"], - widgets.HBox( - children=[ - self.Controller.components["VolumetricDataController"], - self.Controller.components["ImShowController"], - ] - ), - ] - ) - ] - - # Set some initial values based on neurodata object and initial data to plot - self.Controller.frame_slider.max = self.two_photon_series.data.shape[0] - 1 - self.Controller.plane_slider.max = self.two_photon_series.data.shape[-1] - 1 - self.Controller.manual_contrast_slider.max = np.max(self.data_to_plot) - self.Controller.manual_contrast_slider.min = np.min(self.data_to_plot) - self.Controller.manual_contrast_slider.value = ( - self.Controller.manual_contrast_slider.min, - self.Controller.manual_contrast_slider.max, - ) - - 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 rotation_changed is not None: - self.update_data_to_plot() - elif frame_index is not None or plane_index is not None: - self.update_data(frame_index=frame_index, plane_index=plane_index) - self.update_data_to_plot() - - contrast_rescaling = contrast_rescaling or self.Controller.auto_contrast_method.value - contrast = contrast or self.Controller.manual_contrast_slider.value - - img_fig_kwargs = dict(binary_string=True) - if self.Controller.contrast_type_toggle.value == "Manual": - img_fig_kwargs.update(zmin=contrast[0], zmax=contrast[1]) - elif self.Controller.contrast_type_toggle.value == "Automatic": - img_fig_kwargs.update(contrast_rescaling=contrast_rescaling) - - self.figure = px.imshow(self.data_to_plot, **img_fig_kwargs) - self.figure.update_traces(hovertemplate=None, hoverinfo="skip") - - def update_canvas( - 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, - ): - self.update_figure( - rotation_changed=rotation_changed, frame_index=frame_index, plane_index=plane_index, contrast_rescaling=contrast_rescaling, contrast=contrast - ) - self.Canvas.data[0].update(self.figure.data[0]) - - def setup_canvas(self): - # Setup main figure area - self.update_figure() - self.Canvas = go.FigureWidget(self.figure) - self.Canvas.layout.title = f"TwoPhotonSeries: {self.two_photon_series.name} - Planar slices of volume" - self.Canvas.update_xaxes(visible=False, showticklabels=False).update_yaxes(visible=False, showticklabels=False) - - # Final vizualization-specific setup of controller positions - # Move the Simplified/Detailed switch to the right part of screen - self.Controller.components["ViewTypeController"].layout.align_items = "flex-end" - - def setup_observers(self): - self.Controller.rotate_right.on_click(lambda change: self.update_canvas(rotation_changed=True)) - self.Controller.rotate_left.on_click(lambda change: self.update_canvas(rotation_changed=True)) - self.Controller.frame_slider.observe(lambda change: self.update_canvas(frame_index=change.new), names="value") - self.Controller.plane_slider.observe(lambda change: self.update_canvas(plane_index=change.new), names="value") - - self.Controller.contrast_type_toggle.observe(lambda change: self.update_canvas(), names="value") - self.Controller.auto_contrast_method.observe( - lambda change: self.update_canvas(contrast_rescaling=change.new), names="value" - ) - self.Controller.manual_contrast_slider.observe( - lambda change: self.update_canvas(contrast=change.new), names="value" - ) - - -class TwoPhotonSeriesWidget(widgets.VBox): - """Widget showing Image stack recorded over time from 2-photon microscope.""" - - def __init__(self, indexed_timeseries: TwoPhotonSeries, neurodata_vis_spec: dict): - super().__init__() - self.figure = None - self.slider = None - - series_name = indexed_timeseries.name - base_title = f"TwoPhotonSeries: {series_name}" - - def _add_fig_trace(img_fig: go.Figure, index): - if self.figure is None: - self.figure = go.FigureWidget(img_fig) - else: - self.figure.for_each_trace(lambda trace: trace.update(img_fig.data[0])) - - if indexed_timeseries.data is None: - if indexed_timeseries.external_file is not None: - path_ext_file = indexed_timeseries.external_file[0] - # Get Frames dimensions - tif = TiffFile(path_ext_file) - n_samples = len(tif.pages) - page = tif.pages[0] - - def update_figure(index=0): - # Read first frame - img_fig = px.imshow(imread(path_ext_file, key=int(index)), binary_string=True) - _add_fig_trace(img_fig, index) - - self.slider = widgets.IntSlider( - value=0, min=0, max=n_samples - 1, orientation="horizontal", description="TIFF index: " - ) - self.controls = dict(slider=self.slider) - self.slider.observe(lambda change: update_figure(index=change.new), names="value") - - update_figure() - self.figure.layout.title = f"{base_title} - read from first external file" - self.children = [self.figure, self.slider] - else: - self.frame_slider = widgets.IntSlider( - value=0, - min=0, - max=indexed_timeseries.data.shape[0] - 1, - orientation="horizontal", - description="Frame: ", - ) - self.controls = dict(slider=self.frame_slider) - - if len(indexed_timeseries.data.shape) == 3: - - def update_figure(index=0): - img_fig = px.imshow(indexed_timeseries.data[index].T, binary_string=True) - _add_fig_trace(img_fig, index) - - self.frame_slider.observe(lambda change: update_figure(index=change.new), names="value") - - update_figure() - self.figure.layout.title = f"{base_title} - planar view" - self.children = [self.figure, self.frame_slider] - - elif len(indexed_timeseries.data.shape) == 4: - - self.volume_figure = None - - def plot_plane_slices(indexed_timeseries: TwoPhotonSeries): - return PlaneSliceVizualization(two_photon_series=indexed_timeseries) - - # Volume tab - output = widgets.Output() - - def update_volume_figure(index=0): - import ipyvolume.pylab as p3 - - p3.figure() - p3.volshow( - indexed_timeseries.data[index].transpose([1, 0, 2]), - tf=linear_transfer_function([0, 0, 0], max_opacity=0.3), - ) - output.clear_output(wait=True) - with output: - p3.show() - - def first_volume_render(index=0): - self.volume_figure = output - update_volume_figure(index=self.frame_slider.value) - self.frame_slider.observe(lambda change: update_volume_figure(index=change.new), names="value") - - def plot_volume_init(indexed_timeseries: TwoPhotonSeries): - self.init_button = widgets.Button(description="Render") - self.init_button.on_click(first_volume_render) - self.volume_figure = output - self.volume_figure.layout.title = f"{base_title} - interactive volume" - return widgets.VBox(children=[self.volume_figure, self.frame_slider, self.init_button]) - - # Main view - tab = LazyTab( - func_dict={"Planar Slice": plot_plane_slices, "3D Volume": plot_volume_init}, - data=indexed_timeseries, - ) - self.children = [tab] - else: - raise NotImplementedError - - -def show_df_over_f(df_over_f: DfOverF, neurodata_vis_spec: dict): - if len(df_over_f.roi_response_series) == 1: - title, data_input = list(df_over_f.roi_response_series.items())[0] - return neurodata_vis_spec[RoiResponseSeries](data_input, neurodata_vis_spec, title=title) - else: - return neurodata_vis_spec[NWBDataInterface](df_over_f, neurodata_vis_spec) - - -def show_image_segmentation(img_seg: ImageSegmentation, neurodata_vis_spec: dict): - if len(img_seg.plane_segmentations) == 1: - return route_plane_segmentation(list(img_seg.plane_segmentations.values())[0], neurodata_vis_spec) - else: - return neurodata_vis_spec[NWBDataInterface](img_seg, neurodata_vis_spec) - - -def show_plane_segmentation_3d_voxel(plane_seg: PlaneSegmentation): - import ipyvolume.pylab as p3 - - nrois = len(plane_seg) - - voxel_mask = plane_seg["voxel_mask"] - - mx, my, mz = 0, 0, 0 - for voxel in voxel_mask: - for x, y, z, _ in voxel: - mx = max(mx, x) - my = max(my, y) - mz = max(mz, z) - - fig = p3.figure() - for icolor, color in enumerate(color_wheel): - vol = np.zeros((mx + 1, my + 1, mz + 1)) - sel = np.arange(icolor, nrois, len(color_wheel)) - for isel in sel: - dat = voxel_mask[isel] - for x, y, z, value in dat: - vol[x, y, z] = value - p3.volshow(vol, tf=linear_transfer_function(color, max_opacity=0.3)) - return fig - - -def show_plane_segmentation_3d_mask(plane_seg: PlaneSegmentation): - import ipyvolume.pylab as p3 - - nrois = len(plane_seg) - - image_masks = plane_seg["image_mask"] - - fig = p3.figure() - for icolor, color in enumerate(color_wheel): - vol = np.zeros(image_masks.shape[1:]) - sel = np.arange(icolor, nrois, len(color_wheel)) - for isel in sel: - vol += plane_seg["image_mask"][isel] - p3.volshow(vol, tf=linear_transfer_function(color, max_opacity=0.3)) - return fig - - -class PlaneSegmentation2DWidget(widgets.VBox): - def __init__(self, plane_seg: PlaneSegmentation, color_wheel=color_wheel, **kwargs): - super().__init__() - self.categorical_columns = infer_categorical_columns(plane_seg) - self.plane_seg = plane_seg - self.color_wheel = color_wheel - self.progress_bar = ProgressBar() - self.button = widgets.Button(description="Display ROIs") - self.children = [widgets.HBox([self.button, self.progress_bar.container])] - self.button.on_click(self.on_button_click) - self.kwargs = kwargs - - def on_button_click(self, b): - if len(self.categorical_columns) == 1: - self.color_by = list(self.categorical_columns.keys())[0] # changing local variables to instance variables? - self.children += (self.show_plane_segmentation_2d(color_by=self.color_by, **self.kwargs),) - elif len(self.categorical_columns) > 1: - self.cat_controller = widgets.Dropdown(options=list(self.categorical_columns), description="color by") - self.fig = self.show_plane_segmentation_2d(color_by=self.cat_controller.value, **self.kwargs) - - def on_change(change): - if change["new"] and isinstance(change["new"], dict): - ind = change["new"]["index"] - if isinstance(ind, int): - color_by = change["owner"].options[ind] - self.update_fig(color_by) - - self.cat_controller.observe(on_change) - self.children += (self.cat_controller, self.fig) - else: - self.children += (self.show_plane_segmentation_2d(color_by=None, **self.kwargs),) - self.children = self.children[1:] - - def update_fig(self, color_by): - cats = np.unique(self.plane_seg[color_by][:]) - legendgroups = [] - with self.fig.batch_update(): - for color_val, data in zip(self.plane_seg[color_by][:], self.fig.data): - color = self.color_wheel[np.where(cats == color_val)[0][0]] # store the color - data.line.color = color # set the color - data.legendgroup = str(color_val) # set the legend group to the color - data.name = str(color_val) - for color_val, data in zip(self.plane_seg[color_by][:], self.fig.data): - if color_val not in legendgroups: - data.showlegend = True - legendgroups.append(color_val) - else: - data.showlegend = False - - def show_plane_segmentation_2d( - self, - color_wheel: list = color_wheel, - color_by: str = None, - threshold: float = 0.01, - fig: go.Figure = None, - width: int = 600, - ref_image=None, - ): - """ - - Parameters - ---------- - plane_seg: PlaneSegmentation - color_wheel: list, optional - color_by: str, optional - threshold: float, optional - fig: plotly.graph_objects.Figure, optional - width: int, optional - width of image in pixels. Height is automatically determined - to be proportional - ref_image: image, optional - - - Returns - ------- - - """ - layout_kwargs = dict() - if color_by: - if color_by not in self.plane_seg: - raise ValueError("specified color_by parameter, {}, not in plane_seg object".format(color_by)) - cats = np.unique(self.plane_seg[color_by][:]) - layout_kwargs.update(title=color_by) - - data = self.plane_seg["image_mask"].data - n_units = len(data) - if fig is None: - fig = go.FigureWidget() - - if ref_image is not None: - fig.add_trace(go.Heatmap(z=ref_image, hoverinfo="skip", showscale=False, colorscale="gray")) - - aux_leg = [] - import pandas as pd - - plane_seg_hover_dict = { - key: self.plane_seg[key].data for key in self.plane_seg.colnames if key not in ["pixel_mask", "image_mask"] - } - plane_seg_hover_dict.update(id=self.plane_seg.id.data) - plane_seg_hover_df = pd.DataFrame(plane_seg_hover_dict) - all_hover = df_to_hover_text(plane_seg_hover_df) - self.progress_bar.reset(total=n_units) - self.progress_bar.set_description("Loading Image Masks") - for i in range(n_units): - kwargs = dict(showlegend=False) - if color_by is not None: - if plane_seg_hover_df[color_by][i] not in aux_leg: - kwargs.update(showlegend=True) - aux_leg.append(plane_seg_hover_df[color_by][i]) - index = np.where(cats == plane_seg_hover_df[color_by][i])[0][0] - c = color_wheel[index % len(color_wheel)] - kwargs.update( - line_color=c, - name=str(plane_seg_hover_df[color_by][i]), - legendgroup=str(plane_seg_hover_df[color_by][i]), - ) - - x, y = self.compute_outline(i, threshold) - fig.add_trace( - go.Scatter( - x=x, - y=y, - fill="toself", - mode="lines", - text=all_hover[i], - hovertext="text", - line=dict(width=0.5), - **kwargs, - ) - ) - self.progress_bar.update() - # self.progress_bar.close() - fig.update_layout( - width=width, - yaxis=dict( - mirror=True, - scaleanchor="x", - scaleratio=1, - range=[0, self.plane_seg["image_mask"].shape[2]], - constrain="domain", - ), - xaxis=dict( - mirror=True, - range=[0, self.plane_seg["image_mask"].shape[1]], - constrain="domain", - ), - margin=dict(t=30, b=10), - **layout_kwargs, - ) - return fig - - @lru_cache(1000) - def compute_outline(self, i, threshold): - x, y = zip(*measure.find_contours(self.plane_seg["image_mask"][i], threshold)[0]) - return x, y - - -def route_plane_segmentation(plane_seg: PlaneSegmentation, neurodata_vis_spec: dict): - if "voxel_mask" in plane_seg: - return show_plane_segmentation_3d_voxel(plane_seg) - elif "image_mask" in plane_seg and len(plane_seg.image_mask.shape) == 4: - raise NotImplementedError("3d image mask vis not implemented yet") - elif "image_mask" in plane_seg: - return PlaneSegmentation2DWidget(plane_seg) - - -def show_grayscale_volume(vol: GrayscaleVolume, neurodata_vis_spec: dict): - import ipyvolume.pylab as p3 - - fig = p3.figure() - p3.volshow(vol.data, tf=linear_transfer_function([0, 0, 0], max_opacity=0.1)) - return fig - - -class RoiResponseSeriesWidget(BaseGroupedTraceWidget): - def __init__(self, roi_response_series: RoiResponseSeries, neurodata_vis_spec=None, **kwargs): - super().__init__(roi_response_series, "rois", **kwargs) diff --git a/nwbwidgets/ophys/__init__.py b/nwbwidgets/ophys/__init__.py new file mode 100644 index 00000000..41d76364 --- /dev/null +++ b/nwbwidgets/ophys/__init__.py @@ -0,0 +1,3 @@ +from .plane_slice_visualization import PlaneSliceVisualization +from .two_photon_series import TwoPhotonSeriesVizualization +from .segmentation import PlaneSegmentation2DWidget, RoiResponseSeries \ No newline at end of file diff --git a/nwbwidgets/ophys/plane_slice.py b/nwbwidgets/ophys/plane_slice.py new file mode 100644 index 00000000..ebd179a6 --- /dev/null +++ b/nwbwidgets/ophys/plane_slice.py @@ -0,0 +1,277 @@ +import math +from functools import lru_cache +from typing import Tuple, Optional + +import numpy as np +import h5py +import ipywidgets as widgets +import plotly.graph_objects as go +import plotly.express as px +from pynwb.ophys import TwoPhotonSeries + +from ..controllers import RotationController, ImShowController, ViewTypeController, MultiController + + +class FrameController(widgets.VBox): + 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): + 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 VolumetricDataController(MultiController): + def __init__(self): + super().__init__(components=[RotationController(), FrameController(), PlaneController()]) + + # Align rotation buttons to center of sliders + self.layout.align_items = "center" + + +class VolumetricPlaneSliceController(MultiController): + def __init__(self): + super().__init__(components=[ViewTypeController(), VolumetricDataController(), ImShowController()]) + + 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()) + + +class PlaneSliceVisualization(widgets.VBox): + """Sub-widget specifically for plane-wise views of a 4D TwoPhotonSeries.""" + + def __init__(self, two_photon_series: TwoPhotonSeries): + num_dimensions = len(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}." + ) + + super().__init__() + self.two_photon_series = two_photon_series + self.rotation = 0 + + self.setup_data() + self.setup_data_to_plot() + + self.setup_controllers() + self.setup_canvas() + + self.setup_observers() + + # Setup layout of Canvas relative to Controllers + self.children = [self.Canvas, self.Controller] + + @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_contrast_range(self): + """ + If either of the frame or plane sliders are changed, be sure to update the valid range of the manual contrast. + + Applies even if current hidden, in case user wants to enable it. + """ + self.Controller.manual_contrast_slider.max = np.max(self.data) + self.Controller.manual_contrast_slider.min = np.min(self.data) + self.Controller.manual_contrast_slider.value = ( + max(self.Controller.manual_contrast_slider.value[0], self.Controller.manual_contrast_slider.min), + min(self.Controller.manual_contrast_slider.value[1], self.Controller.manual_contrast_slider.max), + ) + + 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 update_data_to_plot(self): + rotation = ( + self.Controller.components["VolumetricDataController"].components["RotationController"].rotation + if hasattr(self, "Controller") + else 0 + ) + rotation_mod = rotation % 4 # Only supporting 90 degree increments + if rotation_mod == 0: + self.data_to_plot = self.data + elif rotation_mod == 1: + self.data_to_plot = self.data.T + elif rotation_mod == 2: + self.data_to_plot = np.flip(self.data) + elif rotation_mod == 3: + self.data_to_plot = np.flip(self.data.T) + + def setup_data_to_plot(self): + self.update_data_to_plot() + + def setup_controllers(self): + """Controller updates are handled through the defined Controller class.""" + self.Controller = VolumetricPlaneSliceController() + + # Setup layout of controllers relative to each other + self.Controller.children = [ + widgets.VBox( + children=[ + self.Controller.components["ViewTypeController"], + widgets.HBox( + children=[ + self.Controller.components["VolumetricDataController"], + self.Controller.components["ImShowController"], + ] + ), + ] + ) + ] + + # Set some initial values based on neurodata object and initial data to plot + self.Controller.frame_slider.max = self.two_photon_series.data.shape[0] - 1 + self.Controller.plane_slider.max = self.two_photon_series.data.shape[-1] - 1 + self.Controller.manual_contrast_slider.max = np.max(self.data_to_plot) + self.Controller.manual_contrast_slider.min = np.min(self.data_to_plot) + self.Controller.manual_contrast_slider.value = ( + self.Controller.manual_contrast_slider.min, + self.Controller.manual_contrast_slider.max, + ) + + 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 rotation_changed is not None: + self.update_data_to_plot() + elif frame_index is not None or plane_index is not None: + self.update_data(frame_index=frame_index, plane_index=plane_index) + self.update_data_to_plot() + + contrast_rescaling = contrast_rescaling or self.Controller.auto_contrast_method.value + contrast = contrast or self.Controller.manual_contrast_slider.value + + img_fig_kwargs = dict(binary_string=True) + if self.Controller.contrast_type_toggle.value == "Manual": + img_fig_kwargs.update(zmin=contrast[0], zmax=contrast[1]) + elif self.Controller.contrast_type_toggle.value == "Automatic": + img_fig_kwargs.update(contrast_rescaling=contrast_rescaling) + + self.figure = px.imshow(self.data_to_plot, **img_fig_kwargs) + self.figure.update_traces(hovertemplate=None, hoverinfo="skip") + + def update_canvas( + 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, + ): + self.update_figure( + rotation_changed=rotation_changed, + frame_index=frame_index, + plane_index=plane_index, + contrast_rescaling=contrast_rescaling, + contrast=contrast, + ) + self.Canvas.data[0].update(self.figure.data[0]) + + def setup_canvas(self): + # Setup main figure area + self.update_figure() + self.Canvas = go.FigureWidget(self.figure) + self.Canvas.layout.title = f"TwoPhotonSeries: {self.two_photon_series.name} - Planar slices of volume" + self.Canvas.update_xaxes(visible=False, showticklabels=False).update_yaxes(visible=False, showticklabels=False) + + # Final vizualization-specific setup of controller positions + # Move the Simplified/Detailed switch to the right part of screen + self.Controller.components["ViewTypeController"].layout.align_items = "flex-end" + + def setup_observers(self): + self.Controller.rotate_right.on_click(lambda change: self.update_canvas(rotation_changed=True)) + self.Controller.rotate_left.on_click(lambda change: self.update_canvas(rotation_changed=True)) + self.Controller.frame_slider.observe(lambda change: self.update_canvas(frame_index=change.new), names="value") + self.Controller.plane_slider.observe(lambda change: self.update_canvas(plane_index=change.new), names="value") + + self.Controller.contrast_type_toggle.observe(lambda change: self.update_canvas(), names="value") + self.Controller.auto_contrast_method.observe( + lambda change: self.update_canvas(contrast_rescaling=change.new), names="value" + ) + self.Controller.manual_contrast_slider.observe( + lambda change: self.update_canvas(contrast=change.new), names="value" + ) diff --git a/nwbwidgets/ophys/segmentation.py b/nwbwidgets/ophys/segmentation.py new file mode 100644 index 00000000..98943080 --- /dev/null +++ b/nwbwidgets/ophys/segmentation.py @@ -0,0 +1,241 @@ +from functools import lru_cache + +import numpy as np +from skimage import measure +import ipywidgets as widgets +import plotly.graph_objects as go +import plotly.express as px +from pynwb.base import NWBDataInterface +from pynwb.ophys import RoiResponseSeries, DfOverF, PlaneSegmentation, ImageSegmentation + +from .base import df_to_hover_text +from .timeseries import BaseGroupedTraceWidget +from .utils.cmaps import linear_transfer_function +from .utils.dynamictable import infer_categorical_columns +from .controllers import ProgressBar + + +color_wheel = px.colors.qualitative.Dark24 + + +def show_df_over_f(df_over_f: DfOverF, neurodata_vis_spec: dict): + if len(df_over_f.roi_response_series) == 1: + title, data_input = list(df_over_f.roi_response_series.items())[0] + return neurodata_vis_spec[RoiResponseSeries](data_input, neurodata_vis_spec, title=title) + else: + return neurodata_vis_spec[NWBDataInterface](df_over_f, neurodata_vis_spec) + + +def show_image_segmentation(img_seg: ImageSegmentation, neurodata_vis_spec: dict): + if len(img_seg.plane_segmentations) == 1: + return route_plane_segmentation(list(img_seg.plane_segmentations.values())[0], neurodata_vis_spec) + else: + return neurodata_vis_spec[NWBDataInterface](img_seg, neurodata_vis_spec) + + +def show_plane_segmentation_3d_voxel(plane_seg: PlaneSegmentation): + import ipyvolume.pylab as p3 + + nrois = len(plane_seg) + + voxel_mask = plane_seg["voxel_mask"] + + mx, my, mz = 0, 0, 0 + for voxel in voxel_mask: + for x, y, z, _ in voxel: + mx = max(mx, x) + my = max(my, y) + mz = max(mz, z) + + fig = p3.figure() + for icolor, color in enumerate(color_wheel): + vol = np.zeros((mx + 1, my + 1, mz + 1)) + sel = np.arange(icolor, nrois, len(color_wheel)) + for isel in sel: + dat = voxel_mask[isel] + for x, y, z, value in dat: + vol[x, y, z] = value + p3.volshow(vol, tf=linear_transfer_function(color, max_opacity=0.3)) + return fig + + +def show_plane_segmentation_3d_mask(plane_seg: PlaneSegmentation): + import ipyvolume.pylab as p3 + + nrois = len(plane_seg) + + image_masks = plane_seg["image_mask"] + + fig = p3.figure() + for icolor, color in enumerate(color_wheel): + vol = np.zeros(image_masks.shape[1:]) + sel = np.arange(icolor, nrois, len(color_wheel)) + for isel in sel: + vol += plane_seg["image_mask"][isel] + p3.volshow(vol, tf=linear_transfer_function(color, max_opacity=0.3)) + return fig + + +class PlaneSegmentation2DWidget(widgets.VBox): + def __init__(self, plane_seg: PlaneSegmentation, color_wheel=color_wheel, **kwargs): + super().__init__() + self.categorical_columns = infer_categorical_columns(plane_seg) + self.plane_seg = plane_seg + self.color_wheel = color_wheel + self.progress_bar = ProgressBar() + self.button = widgets.Button(description="Display ROIs") + self.children = [widgets.HBox([self.button, self.progress_bar.container])] + self.button.on_click(self.on_button_click) + self.kwargs = kwargs + + def on_button_click(self, b): + if len(self.categorical_columns) == 1: + self.color_by = list(self.categorical_columns.keys())[0] # changing local variables to instance variables? + self.children += (self.show_plane_segmentation_2d(color_by=self.color_by, **self.kwargs),) + elif len(self.categorical_columns) > 1: + self.cat_controller = widgets.Dropdown(options=list(self.categorical_columns), description="color by") + self.fig = self.show_plane_segmentation_2d(color_by=self.cat_controller.value, **self.kwargs) + + def on_change(change): + if change["new"] and isinstance(change["new"], dict): + ind = change["new"]["index"] + if isinstance(ind, int): + color_by = change["owner"].options[ind] + self.update_fig(color_by) + + self.cat_controller.observe(on_change) + self.children += (self.cat_controller, self.fig) + else: + self.children += (self.show_plane_segmentation_2d(color_by=None, **self.kwargs),) + self.children = self.children[1:] + + def update_fig(self, color_by): + cats = np.unique(self.plane_seg[color_by][:]) + legendgroups = [] + with self.fig.batch_update(): + for color_val, data in zip(self.plane_seg[color_by][:], self.fig.data): + color = self.color_wheel[np.where(cats == color_val)[0][0]] # store the color + data.line.color = color # set the color + data.legendgroup = str(color_val) # set the legend group to the color + data.name = str(color_val) + for color_val, data in zip(self.plane_seg[color_by][:], self.fig.data): + if color_val not in legendgroups: + data.showlegend = True + legendgroups.append(color_val) + else: + data.showlegend = False + + def show_plane_segmentation_2d( + self, + color_wheel: list = color_wheel, + color_by: str = None, + threshold: float = 0.01, + fig: go.Figure = None, + width: int = 600, + ref_image=None, + ): + """ + Parameters + ---------- + plane_seg: PlaneSegmentation + color_wheel: list, optional + color_by: str, optional + threshold: float, optional + fig: plotly.graph_objects.Figure, optional + width: int, optional + width of image in pixels. Height is automatically determined + to be proportional + ref_image: image, optional + """ + layout_kwargs = dict() + if color_by: + if color_by not in self.plane_seg: + raise ValueError("specified color_by parameter, {}, not in plane_seg object".format(color_by)) + cats = np.unique(self.plane_seg[color_by][:]) + layout_kwargs.update(title=color_by) + + data = self.plane_seg["image_mask"].data + n_units = len(data) + if fig is None: + fig = go.FigureWidget() + + if ref_image is not None: + fig.add_trace(go.Heatmap(z=ref_image, hoverinfo="skip", showscale=False, colorscale="gray")) + + aux_leg = [] + import pandas as pd + + plane_seg_hover_dict = { + key: self.plane_seg[key].data for key in self.plane_seg.colnames if key not in ["pixel_mask", "image_mask"] + } + plane_seg_hover_dict.update(id=self.plane_seg.id.data) + plane_seg_hover_df = pd.DataFrame(plane_seg_hover_dict) + all_hover = df_to_hover_text(plane_seg_hover_df) + self.progress_bar.reset(total=n_units) + self.progress_bar.set_description("Loading Image Masks") + for i in range(n_units): + kwargs = dict(showlegend=False) + if color_by is not None: + if plane_seg_hover_df[color_by][i] not in aux_leg: + kwargs.update(showlegend=True) + aux_leg.append(plane_seg_hover_df[color_by][i]) + index = np.where(cats == plane_seg_hover_df[color_by][i])[0][0] + c = color_wheel[index % len(color_wheel)] + kwargs.update( + line_color=c, + name=str(plane_seg_hover_df[color_by][i]), + legendgroup=str(plane_seg_hover_df[color_by][i]), + ) + + x, y = self.compute_outline(i, threshold) + fig.add_trace( + go.Scatter( + x=x, + y=y, + fill="toself", + mode="lines", + text=all_hover[i], + hovertext="text", + line=dict(width=0.5), + **kwargs, + ) + ) + self.progress_bar.update() + # self.progress_bar.close() + fig.update_layout( + width=width, + yaxis=dict( + mirror=True, + scaleanchor="x", + scaleratio=1, + range=[0, self.plane_seg["image_mask"].shape[2]], + constrain="domain", + ), + xaxis=dict( + mirror=True, + range=[0, self.plane_seg["image_mask"].shape[1]], + constrain="domain", + ), + margin=dict(t=30, b=10), + **layout_kwargs, + ) + return fig + + @lru_cache(1000) + def compute_outline(self, i, threshold): + x, y = zip(*measure.find_contours(self.plane_seg["image_mask"][i], threshold)[0]) + return x, y + + +def route_plane_segmentation(plane_seg: PlaneSegmentation, neurodata_vis_spec: dict): + if "voxel_mask" in plane_seg: + return show_plane_segmentation_3d_voxel(plane_seg) + elif "image_mask" in plane_seg and len(plane_seg.image_mask.shape) == 4: + raise NotImplementedError("3d image mask vis not implemented yet") + elif "image_mask" in plane_seg: + return PlaneSegmentation2DWidget(plane_seg) + + +class RoiResponseSeriesWidget(BaseGroupedTraceWidget): + def __init__(self, roi_response_series: RoiResponseSeries, neurodata_vis_spec=None, **kwargs): + super().__init__(roi_response_series, "rois", **kwargs) diff --git a/nwbwidgets/ophys/single_plane.py b/nwbwidgets/ophys/single_plane.py new file mode 100644 index 00000000..8cb38c20 --- /dev/null +++ b/nwbwidgets/ophys/single_plane.py @@ -0,0 +1,94 @@ +import math +from functools import lru_cache +from typing import Tuple, Optional + +import h5py +import numpy as np +import plotly.express as px +from pynwb.ophys import TwoPhotonSeries + +from .plane_slice import PlaneSliceVisualization + + +class SinglePlaneVisualization(PlaneSliceVisualization): + """Sub-widget specifically for plane views of a 3D TwoPhotonSeries.""" + + def __init__(self, two_photon_series: TwoPhotonSeries): + num_dimensions = len(two_photon_series.data.shape) + if num_dimensions != 3: + raise ValueError( + "The SinglePlaneVisualization is only appropriate for " + f"use on 3-dimensional TwoPhotonSeries! Detected dimension of {num_dimensions}." + ) + + super().__init__() + + # Only major difference with the parent volumetric visualization is the ability to specify the plane index + # Could remove it altogether, but that's more work than just hiding the component and not using it's value + self.Controller.plane_slider.layout.visibility = "hidden" + self.Canvas.layout.title = f"TwoPhotonSeries: {self.two_photon_series.name}" + + @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) -> np.ndarray: + return dataset[frame_index, :, :].T + + def update_data(self, frame_index: Optional[int] = None): + frame_index = frame_index or self.Controller.frame_slider.value + + self.data = self._cache_data_read(dataset=self.two_photon_series.data, frame_index=frame_index) + + self.data = self.two_photon_series.data[frame_index, :, :] + + if self.Controller.contrast_type_toggle.value == "Manual": + self.update_contrast_range() + + def setup_data(self, max_mb_treshold: float = 20.0): + 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] + + def update_figure( + self, + rotation_changed: Optional[bool] = None, + frame_index: Optional[int] = None, + contrast_rescaling: Optional[str] = None, + contrast: Optional[Tuple[int]] = None, + ): + if rotation_changed is not None: + self.update_data_to_plot() + elif frame_index is not None: + self.update_data(frame_index=frame_index) + self.update_data_to_plot() + + contrast_rescaling = contrast_rescaling or self.Controller.auto_contrast_method.value + contrast = contrast or self.Controller.manual_contrast_slider.value + + img_fig_kwargs = dict(binary_string=True) + if self.Controller.contrast_type_toggle.value == "Manual": + img_fig_kwargs.update(zmin=contrast[0], zmax=contrast[1]) + elif self.Controller.contrast_type_toggle.value == "Automatic": + img_fig_kwargs.update(contrast_rescaling=contrast_rescaling) + + self.figure = px.imshow(self.data_to_plot, **img_fig_kwargs) + self.figure.update_traces(hovertemplate=None, hoverinfo="skip") + + def update_canvas( + self, + rotation_changed: Optional[bool] = None, + frame_index: Optional[int] = None, + contrast_rescaling: Optional[str] = None, + contrast: Optional[Tuple[int]] = None, + ): + self.update_figure( + rotation_changed=rotation_changed, + frame_index=frame_index, + contrast_rescaling=contrast_rescaling, + contrast=contrast, + ) + self.Canvas.data[0].update(self.figure.data[0]) diff --git a/nwbwidgets/ophys/two_photon_series.py b/nwbwidgets/ophys/two_photon_series.py new file mode 100644 index 00000000..d672cc5c --- /dev/null +++ b/nwbwidgets/ophys/two_photon_series.py @@ -0,0 +1,62 @@ +import ipywidgets as widgets +import plotly.graph_objects as go +import plotly.express as px +from pynwb.ophys import TwoPhotonSeries +from tifffile import imread, TiffFile + +from .single_plane import SinglePlaneVisualization +from .plane_slice import PlaneSliceVisualization +from .volume import VolumeVisualization +from ..base import LazyTab + + +class TwoPhotonSeriesVisualization(widgets.VBox): + """Widget showing Image stack recorded over time from 2-photon microscope.""" + + def __init__(self, indexed_timeseries: TwoPhotonSeries, neurodata_vis_spec: dict): + super().__init__() + + if indexed_timeseries.data is None: + if indexed_timeseries.external_file is not None: + path_ext_file = indexed_timeseries.external_file[0] + # Get Frames dimensions + tif = TiffFile(path_ext_file) + n_samples = len(tif.pages) + page = tif.pages[0] + + def _add_fig_trace(img_fig: go.Figure, index): + if self.figure is None: + self.figure = go.FigureWidget(img_fig) + else: + self.figure.for_each_trace(lambda trace: trace.update(img_fig.data[0])) + + def update_figure(index=0): + # Read first frame + img_fig = px.imshow(imread(path_ext_file, key=int(index)), binary_string=True) + _add_fig_trace(img_fig, index) + + self.slider = widgets.IntSlider( + value=0, min=0, max=n_samples - 1, orientation="horizontal", description="TIFF index: " + ) + self.controls = dict(slider=self.slider) + self.slider.observe(lambda change: update_figure(index=change.new), names="value") + + update_figure() + + series_name = indexed_timeseries.name + base_title = f"TwoPhotonSeries: {series_name}" + self.figure.layout.title = f"{base_title} - read from first external file" + + self.children = [self.figure, self.slider] + else: + if len(indexed_timeseries.data.shape) == 3: + self.children = (SinglePlaneVisualization(two_photon_series=indexed_timeseries),) + + elif len(indexed_timeseries.data.shape) == 4: + tab = LazyTab( + func_dict={"Planar Slice": PlaneSliceVisualization, "3D Volume": VolumeVisualization}, + data=indexed_timeseries, + ) + self.children = (tab,) + else: + raise NotImplementedError diff --git a/nwbwidgets/ophys/volume.py b/nwbwidgets/ophys/volume.py new file mode 100644 index 00000000..22c78e2c --- /dev/null +++ b/nwbwidgets/ophys/volume.py @@ -0,0 +1,35 @@ +import ipywidgets as widgets +from pynwb.ophys import TwoPhotonSeries + +from ..utils.cmaps import linear_transfer_function + + +class VolumeVisualization(widgets.VBox): + def __init__(self, two_photon_series: TwoPhotonSeries): + self.two_photon_series = two_photon_series + + self.volume_figure = widgets.Button(description="Render") + self.children = (self.volume_figure,) + + def update_volume_figure(self, index=0): + import ipyvolume.pylab as p3 + + p3.figure() + p3.volshow( + self.two_photon_series.data[index].transpose([1, 0, 2]), + tf=linear_transfer_function([0, 0, 0], max_opacity=0.3), + ) + self.volume_figure.clear_output(wait=True) + with self.volume_figure: + p3.show() + + def first_volume_render(self, index=0): + self.volume_figure = widgets.Output() + self.update_volume_figure(index=self.frame_slider.value) + self.frame_slider.observe(lambda change: self.update_volume_figure(index=change.new), names="value") + + def plot_volume_init(self, two_photon_series: TwoPhotonSeries): + self.init_button = widgets.Button(description="Render") + self.init_button.on_click(self.first_volume_render) + + self.volume_figure.layout.title = f"TwoPhotonSeries: {self.two_photon_series.name} - Interactive Volume" diff --git a/nwbwidgets/view.py b/nwbwidgets/view.py index 54f04699..4003013c 100644 --- a/nwbwidgets/view.py +++ b/nwbwidgets/view.py @@ -53,7 +53,7 @@ def show_dynamic_table(node, **kwargs) -> widgets.Widget: pynwb.file.Subject: base.show_fields, pynwb.ecephys.SpikeEventSeries: ecephys.show_spike_event_series, pynwb.ophys.ImageSegmentation: ophys.show_image_segmentation, - pynwb.ophys.TwoPhotonSeries: ophys.TwoPhotonSeriesWidget, + pynwb.ophys.TwoPhotonSeries: ophys.TwoPhotonSeriesVisualization, ndx_grayscalevolume.GrayscaleVolume: ophys.show_grayscale_volume, pynwb.ophys.PlaneSegmentation: ophys.route_plane_segmentation, pynwb.ophys.DfOverF: ophys.show_df_over_f, From 86718d61955f415296b3fdc433459bb3bcd6fb5f Mon Sep 17 00:00:00 2001 From: CodyCBakerPhD Date: Sat, 3 Dec 2022 22:31:51 +0000 Subject: [PATCH 14/20] debugging --- nwbwidgets/ophys/__init__.py | 6 +++--- nwbwidgets/ophys/segmentation.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/nwbwidgets/ophys/__init__.py b/nwbwidgets/ophys/__init__.py index 41d76364..edeebf00 100644 --- a/nwbwidgets/ophys/__init__.py +++ b/nwbwidgets/ophys/__init__.py @@ -1,3 +1,3 @@ -from .plane_slice_visualization import PlaneSliceVisualization -from .two_photon_series import TwoPhotonSeriesVizualization -from .segmentation import PlaneSegmentation2DWidget, RoiResponseSeries \ No newline at end of file +from .plane_slice import PlaneSliceVisualization +from .two_photon_series import TwoPhotonSeriesVisualization +from .segmentation import PlaneSegmentation2DWidget, RoiResponseSeries, show_image_segmentation \ No newline at end of file diff --git a/nwbwidgets/ophys/segmentation.py b/nwbwidgets/ophys/segmentation.py index 98943080..20f25ea3 100644 --- a/nwbwidgets/ophys/segmentation.py +++ b/nwbwidgets/ophys/segmentation.py @@ -8,11 +8,11 @@ from pynwb.base import NWBDataInterface from pynwb.ophys import RoiResponseSeries, DfOverF, PlaneSegmentation, ImageSegmentation -from .base import df_to_hover_text -from .timeseries import BaseGroupedTraceWidget -from .utils.cmaps import linear_transfer_function -from .utils.dynamictable import infer_categorical_columns -from .controllers import ProgressBar +from ..base import df_to_hover_text +from ..timeseries import BaseGroupedTraceWidget +from ..utils.cmaps import linear_transfer_function +from ..utils.dynamictable import infer_categorical_columns +from ..controllers import ProgressBar color_wheel = px.colors.qualitative.Dark24 From 32356ac84723917cdb892758171edff0df6c93b2 Mon Sep 17 00:00:00 2001 From: CodyCBakerPhD Date: Sun, 4 Dec 2022 04:16:11 +0000 Subject: [PATCH 15/20] debug --- nwbwidgets/ophys/__init__.py | 1 + nwbwidgets/view.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/nwbwidgets/ophys/__init__.py b/nwbwidgets/ophys/__init__.py index edeebf00..84338c66 100644 --- a/nwbwidgets/ophys/__init__.py +++ b/nwbwidgets/ophys/__init__.py @@ -1,3 +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 \ No newline at end of file diff --git a/nwbwidgets/view.py b/nwbwidgets/view.py index 4003013c..8366e1e8 100644 --- a/nwbwidgets/view.py +++ b/nwbwidgets/view.py @@ -54,7 +54,7 @@ def show_dynamic_table(node, **kwargs) -> widgets.Widget: pynwb.ecephys.SpikeEventSeries: ecephys.show_spike_event_series, pynwb.ophys.ImageSegmentation: ophys.show_image_segmentation, pynwb.ophys.TwoPhotonSeries: ophys.TwoPhotonSeriesVisualization, - ndx_grayscalevolume.GrayscaleVolume: ophys.show_grayscale_volume, + ndx_grayscalevolume.GrayscaleVolume: ophys.VolumeVisualization, pynwb.ophys.PlaneSegmentation: ophys.route_plane_segmentation, pynwb.ophys.DfOverF: ophys.show_df_over_f, pynwb.ophys.RoiResponseSeries: ophys.RoiResponseSeriesWidget, From ed008676438acf5c0e384c35f455e9238ec6193b Mon Sep 17 00:00:00 2001 From: CodyCBakerPhD Date: Sun, 4 Dec 2022 04:20:35 +0000 Subject: [PATCH 16/20] debugging --- nwbwidgets/ophys/__init__.py | 2 +- nwbwidgets/ophys/volume.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/nwbwidgets/ophys/__init__.py b/nwbwidgets/ophys/__init__.py index 84338c66..58747f99 100644 --- a/nwbwidgets/ophys/__init__.py +++ b/nwbwidgets/ophys/__init__.py @@ -1,4 +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 \ No newline at end of file +from .segmentation import PlaneSegmentation2DWidget, RoiResponseSeries, show_image_segmentation, route_plane_segmentation, show_df_over_f, RoiResponseSeriesWidget \ No newline at end of file diff --git a/nwbwidgets/ophys/volume.py b/nwbwidgets/ophys/volume.py index 22c78e2c..67719281 100644 --- a/nwbwidgets/ophys/volume.py +++ b/nwbwidgets/ophys/volume.py @@ -6,6 +6,7 @@ class VolumeVisualization(widgets.VBox): def __init__(self, two_photon_series: TwoPhotonSeries): + super().__init__() self.two_photon_series = two_photon_series self.volume_figure = widgets.Button(description="Render") From b3b8880dd915c20c4ee3d5da500ee292312c013b Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Sun, 4 Dec 2022 01:50:35 -0500 Subject: [PATCH 17/20] splinter ophys controllers; debug volume --- nwbwidgets/ophys/ophys_controllers.py | 74 +++++++++++++++++++++++++++ nwbwidgets/ophys/plane_slice.py | 73 +------------------------- nwbwidgets/ophys/volume.py | 59 +++++++++++++++------ 3 files changed, 119 insertions(+), 87 deletions(-) create mode 100644 nwbwidgets/ophys/ophys_controllers.py diff --git a/nwbwidgets/ophys/ophys_controllers.py b/nwbwidgets/ophys/ophys_controllers.py new file mode 100644 index 00000000..c4c46716 --- /dev/null +++ b/nwbwidgets/ophys/ophys_controllers.py @@ -0,0 +1,74 @@ +import ipywidgets as widgets + +from ..controllers import RotationController, ImShowController, ViewTypeController, MultiController + + +class FrameController(widgets.VBox): + 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): + 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 VolumetricDataController(MultiController): + def __init__(self): + super().__init__(components=[RotationController(), FrameController(), PlaneController()]) + + # Align rotation buttons to center of sliders + self.layout.align_items = "center" + + +class VolumetricPlaneSliceController(MultiController): + def __init__(self): + super().__init__(components=[ViewTypeController(), VolumetricDataController(), ImShowController()]) + + 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()) diff --git a/nwbwidgets/ophys/plane_slice.py b/nwbwidgets/ophys/plane_slice.py index ebd179a6..5d675a68 100644 --- a/nwbwidgets/ophys/plane_slice.py +++ b/nwbwidgets/ophys/plane_slice.py @@ -9,78 +9,7 @@ import plotly.express as px from pynwb.ophys import TwoPhotonSeries -from ..controllers import RotationController, ImShowController, ViewTypeController, MultiController - - -class FrameController(widgets.VBox): - 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): - 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 VolumetricDataController(MultiController): - def __init__(self): - super().__init__(components=[RotationController(), FrameController(), PlaneController()]) - - # Align rotation buttons to center of sliders - self.layout.align_items = "center" - - -class VolumetricPlaneSliceController(MultiController): - def __init__(self): - super().__init__(components=[ViewTypeController(), VolumetricDataController(), ImShowController()]) - - 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()) +from .ophys_controllers import VolumetricPlaneSliceController class PlaneSliceVisualization(widgets.VBox): diff --git a/nwbwidgets/ophys/volume.py b/nwbwidgets/ophys/volume.py index 67719281..1286325d 100644 --- a/nwbwidgets/ophys/volume.py +++ b/nwbwidgets/ophys/volume.py @@ -1,36 +1,65 @@ import ipywidgets as widgets from pynwb.ophys import TwoPhotonSeries +from .ophys_controllers import FrameController from ..utils.cmaps import linear_transfer_function class VolumeVisualization(widgets.VBox): + def _first_volume_render(self): + self.Canvas = widgets.Output() + self.update_canvas(index=self.frame_slider.value) + def __init__(self, two_photon_series: TwoPhotonSeries): super().__init__() self.two_photon_series = two_photon_series + self.canvas_title = f"TwoPhotonSeries: {self.two_photon_series.name} - Interactive Volume" + + self.Canvas = widgets.Button(description="Render") + self.Canvas.on_click(self._first_volume_render) + self.Canvas.layout.title = self.canvas_title + + self.setup_controllers() + self.setup_observers() + + self.children = (self.Canvas, self.Controller) + + def update_data(self, frame_index=0): + self.data = self.two_photon_series.data[frame_index, ...] - self.volume_figure = widgets.Button(description="Render") - self.children = (self.volume_figure,) + def setup_data(self): + """Initial data setup could be a lot (> 250 MB), but the true 'setup' of the figure is the 'Render' button.""" + self.update_data(frame_index=0) - def update_volume_figure(self, index=0): + def update_data_to_plot(self): + self.data_to_plot = self.data.transpose([1, 0, 2]) + + def setup_data_to_plot(self): + self.update_data_to_plot() + + def setup_controllers(self): + self.Controller = FrameController() + + def update_canvas(self, frame_index: int = 0): import ipyvolume.pylab as p3 p3.figure() - p3.volshow( - self.two_photon_series.data[index].transpose([1, 0, 2]), - tf=linear_transfer_function([0, 0, 0], max_opacity=0.3), - ) - self.volume_figure.clear_output(wait=True) - with self.volume_figure: + p3.volshow(self.data_to_plot, tf=linear_transfer_function([0, 0, 0], max_opacity=0.3)) + self.Canvas.clear_output(wait=True) + with self.Canvas: p3.show() - def first_volume_render(self, index=0): - self.volume_figure = widgets.Output() - self.update_volume_figure(index=self.frame_slider.value) - self.frame_slider.observe(lambda change: self.update_volume_figure(index=change.new), names="value") + def setup_canvas(self): + self.Canvas = widgets.Output() + self.Canvas.layout.title = self.canvas_title + + self.setup_data() + self.setup_data_to_plot() + self.update_canvas() + + def setup_observers(self): + self.Controller.frame_slider.observe(lambda change: self.update_canvas(frame_index=change.new), names="value") def plot_volume_init(self, two_photon_series: TwoPhotonSeries): - self.init_button = widgets.Button(description="Render") - self.init_button.on_click(self.first_volume_render) self.volume_figure.layout.title = f"TwoPhotonSeries: {self.two_photon_series.name} - Interactive Volume" From 9f94400a7937fc73b30c19c204a554b1e424879b Mon Sep 17 00:00:00 2001 From: Cody Baker Date: Sun, 4 Dec 2022 07:17:01 -0500 Subject: [PATCH 18/20] reorder inheritance --- nwbwidgets/ophys/ophys_controllers.py | 33 +++++- nwbwidgets/ophys/plane_slice.py | 145 +++-------------------- nwbwidgets/ophys/single_plane.py | 159 ++++++++++++++++++++++---- nwbwidgets/ophys/two_photon_series.py | 8 +- 4 files changed, 181 insertions(+), 164 deletions(-) diff --git a/nwbwidgets/ophys/ophys_controllers.py b/nwbwidgets/ophys/ophys_controllers.py index c4c46716..e2d0f944 100644 --- a/nwbwidgets/ophys/ophys_controllers.py +++ b/nwbwidgets/ophys/ophys_controllers.py @@ -1,3 +1,5 @@ +from typing import Optional + import ipywidgets as widgets from ..controllers import RotationController, ImShowController, ViewTypeController, MultiController @@ -39,17 +41,28 @@ def __init__(self): self.children = (self.plane_slider,) -class VolumetricDataController(MultiController): - def __init__(self): - super().__init__(components=[RotationController(), FrameController(), PlaneController()]) +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 VolumetricPlaneSliceController(MultiController): +class VolumetricDataController(SinglePlaneDataController): def __init__(self): - super().__init__(components=[ViewTypeController(), VolumetricDataController(), ImShowController()]) + 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() @@ -72,3 +85,13 @@ def setup_visibility(self): def setup_observers(self): self.view_type_toggle.observe(lambda change: self.update_visibility()) + + +class SinglePlaneSliceController(BasePlaneSliceController): + def __init__(self): + super().__init__(components=[SinglePlaneDataController()]) + + +class VolumetricPlaneSliceController(BasePlaneSliceController): + def __init__(self): + super().__init__(components=[VolumetricDataController()]) diff --git a/nwbwidgets/ophys/plane_slice.py b/nwbwidgets/ophys/plane_slice.py index 5d675a68..93469a95 100644 --- a/nwbwidgets/ophys/plane_slice.py +++ b/nwbwidgets/ophys/plane_slice.py @@ -4,57 +4,26 @@ import numpy as np import h5py -import ipywidgets as widgets -import plotly.graph_objects as go -import plotly.express as px -from pynwb.ophys import TwoPhotonSeries +from .single_plane_2 import SinglePlaneVisualization2 from .ophys_controllers import VolumetricPlaneSliceController -class PlaneSliceVisualization(widgets.VBox): +class PlaneSliceVisualization(SinglePlaneVisualization2): """Sub-widget specifically for plane-wise views of a 4D TwoPhotonSeries.""" - def __init__(self, two_photon_series: TwoPhotonSeries): - num_dimensions = len(two_photon_series.data.shape) + 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}." ) - super().__init__() - self.two_photon_series = two_photon_series - self.rotation = 0 - - self.setup_data() - self.setup_data_to_plot() - - self.setup_controllers() - self.setup_canvas() - - self.setup_observers() - - # Setup layout of Canvas relative to Controllers - self.children = [self.Canvas, self.Controller] - @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_contrast_range(self): - """ - If either of the frame or plane sliders are changed, be sure to update the valid range of the manual contrast. - - Applies even if current hidden, in case user wants to enable it. - """ - self.Controller.manual_contrast_slider.max = np.max(self.data) - self.Controller.manual_contrast_slider.min = np.min(self.data) - self.Controller.manual_contrast_slider.value = ( - max(self.Controller.manual_contrast_slider.value[0], self.Controller.manual_contrast_slider.min), - min(self.Controller.manual_contrast_slider.value[1], self.Controller.manual_contrast_slider.max), - ) - 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 @@ -89,53 +58,15 @@ def setup_data(self, max_mb_treshold: float = 20.0): by_height = 2 self.data = self.two_photon_series.data[0, ::by_width, ::by_height, 0] - def update_data_to_plot(self): - rotation = ( - self.Controller.components["VolumetricDataController"].components["RotationController"].rotation - if hasattr(self, "Controller") - else 0 - ) - rotation_mod = rotation % 4 # Only supporting 90 degree increments - if rotation_mod == 0: - self.data_to_plot = self.data - elif rotation_mod == 1: - self.data_to_plot = self.data.T - elif rotation_mod == 2: - self.data_to_plot = np.flip(self.data) - elif rotation_mod == 3: - self.data_to_plot = np.flip(self.data.T) - - def setup_data_to_plot(self): - self.update_data_to_plot() + 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.""" - self.Controller = VolumetricPlaneSliceController() + super().setup_controllers() - # Setup layout of controllers relative to each other - self.Controller.children = [ - widgets.VBox( - children=[ - self.Controller.components["ViewTypeController"], - widgets.HBox( - children=[ - self.Controller.components["VolumetricDataController"], - self.Controller.components["ImShowController"], - ] - ), - ] - ) - ] - - # Set some initial values based on neurodata object and initial data to plot - self.Controller.frame_slider.max = self.two_photon_series.data.shape[0] - 1 self.Controller.plane_slider.max = self.two_photon_series.data.shape[-1] - 1 - self.Controller.manual_contrast_slider.max = np.max(self.data_to_plot) - self.Controller.manual_contrast_slider.min = np.min(self.data_to_plot) - self.Controller.manual_contrast_slider.value = ( - self.Controller.manual_contrast_slider.min, - self.Controller.manual_contrast_slider.max, - ) def update_figure( self, @@ -145,62 +76,16 @@ def update_figure( contrast_rescaling: Optional[str] = None, contrast: Optional[Tuple[int]] = None, ): - if rotation_changed is not None: - self.update_data_to_plot() - elif frame_index is not None or plane_index is not None: - self.update_data(frame_index=frame_index, plane_index=plane_index) + if plane_index is not None: + self.update_data(plane_index=plane_index) self.update_data_to_plot() - contrast_rescaling = contrast_rescaling or self.Controller.auto_contrast_method.value - contrast = contrast or self.Controller.manual_contrast_slider.value + super().update_figure() - img_fig_kwargs = dict(binary_string=True) - if self.Controller.contrast_type_toggle.value == "Manual": - img_fig_kwargs.update(zmin=contrast[0], zmax=contrast[1]) - elif self.Controller.contrast_type_toggle.value == "Automatic": - img_fig_kwargs.update(contrast_rescaling=contrast_rescaling) - - self.figure = px.imshow(self.data_to_plot, **img_fig_kwargs) - self.figure.update_traces(hovertemplate=None, hoverinfo="skip") - - def update_canvas( - 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, - ): - self.update_figure( - rotation_changed=rotation_changed, - frame_index=frame_index, - plane_index=plane_index, - contrast_rescaling=contrast_rescaling, - contrast=contrast, - ) - self.Canvas.data[0].update(self.figure.data[0]) - - def setup_canvas(self): - # Setup main figure area - self.update_figure() - self.Canvas = go.FigureWidget(self.figure) - self.Canvas.layout.title = f"TwoPhotonSeries: {self.two_photon_series.name} - Planar slices of volume" - self.Canvas.update_xaxes(visible=False, showticklabels=False).update_yaxes(visible=False, showticklabels=False) - - # Final vizualization-specific setup of controller positions - # Move the Simplified/Detailed switch to the right part of screen - self.Controller.components["ViewTypeController"].layout.align_items = "flex-end" + def set_canvas_title(self): + self.canvas_title = f"TwoPhotonSeries: {self.two_photon_series.name}" def setup_observers(self): - self.Controller.rotate_right.on_click(lambda change: self.update_canvas(rotation_changed=True)) - self.Controller.rotate_left.on_click(lambda change: self.update_canvas(rotation_changed=True)) - self.Controller.frame_slider.observe(lambda change: self.update_canvas(frame_index=change.new), names="value") - self.Controller.plane_slider.observe(lambda change: self.update_canvas(plane_index=change.new), names="value") + super().setup_observers() - self.Controller.contrast_type_toggle.observe(lambda change: self.update_canvas(), names="value") - self.Controller.auto_contrast_method.observe( - lambda change: self.update_canvas(contrast_rescaling=change.new), names="value" - ) - self.Controller.manual_contrast_slider.observe( - lambda change: self.update_canvas(contrast=change.new), names="value" - ) + self.Controller.plane_slider.observe(lambda change: self.update_canvas(plane_index=change.new), names="value") diff --git a/nwbwidgets/ophys/single_plane.py b/nwbwidgets/ophys/single_plane.py index 8cb38c20..47e343d7 100644 --- a/nwbwidgets/ophys/single_plane.py +++ b/nwbwidgets/ophys/single_plane.py @@ -4,33 +4,59 @@ import h5py import numpy as np +import ipywidgets as widgets import plotly.express as px +import plotly.graph_objects as go from pynwb.ophys import TwoPhotonSeries -from .plane_slice import PlaneSliceVisualization +from .ophys_controllers import SinglePlaneSliceController -class SinglePlaneVisualization(PlaneSliceVisualization): +class SinglePlaneVisualization(widgets.VBox): """Sub-widget specifically for plane views of a 3D TwoPhotonSeries.""" - def __init__(self, two_photon_series: TwoPhotonSeries): - num_dimensions = len(two_photon_series.data.shape) + def _dimension_check(self): + num_dimensions = len(self.two_photon_series.data.shape) if num_dimensions != 3: raise ValueError( - "The SinglePlaneVisualization is only appropriate for " + "The SinglePlaneVisualization2 is only appropriate for " f"use on 3-dimensional TwoPhotonSeries! Detected dimension of {num_dimensions}." ) + def __init__(self, two_photon_series: TwoPhotonSeries): + self.two_photon_series = two_photon_series + self._dimension_check(two_photon_series=two_photon_series) + super().__init__() - # Only major difference with the parent volumetric visualization is the ability to specify the plane index - # Could remove it altogether, but that's more work than just hiding the component and not using it's value - self.Controller.plane_slider.layout.visibility = "hidden" - self.Canvas.layout.title = f"TwoPhotonSeries: {self.two_photon_series.name}" + self.setup_data() + self.setup_data_to_plot() + + self.setup_controllers() + self.canvas_title = f"TwoPhotonSeries: {self.two_photon_series.name} - Planar slices of volume" + self.setup_canvas() + + self.setup_observers() + + # Setup layout of Canvas relative to Controllers + self.children = [self.Canvas, self.Controller] @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) -> np.ndarray: - return dataset[frame_index, :, :].T + 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_contrast_range(self): + """ + If either of the frame or plane sliders are changed, be sure to update the valid range of the manual contrast. + + Applies even if current hidden, in case user wants to enable it. + """ + self.Controller.manual_contrast_slider.max = np.max(self.data) + self.Controller.manual_contrast_slider.min = np.min(self.data) + self.Controller.manual_contrast_slider.value = ( + max(self.Controller.manual_contrast_slider.value[0], self.Controller.manual_contrast_slider.min), + min(self.Controller.manual_contrast_slider.value[1], self.Controller.manual_contrast_slider.max), + ) def update_data(self, frame_index: Optional[int] = None): frame_index = frame_index or self.Controller.frame_slider.value @@ -43,16 +69,80 @@ def update_data(self, frame_index: Optional[int] = None): 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) + self.update_data(frame_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] + def get_rotation(self) -> int: + """The rotation attribute of the SinglePlaneDataController cannot be attached in a modifiable state.""" + if not hasattr(self, "Controller"): # First time this is called + return 0 + return self.Controller.components[self.data_controller_name].components["RotationController"].rotation + + def update_data_to_plot(self): + rotation = self.get_rotation() + rotation_mod = rotation % 4 # Only supporting 90 degree increments + if rotation_mod == 0: + self.data_to_plot = self.data + elif rotation_mod == 1: + self.data_to_plot = self.data.T + elif rotation_mod == 2: + self.data_to_plot = np.flip(self.data) + elif rotation_mod == 3: + self.data_to_plot = np.flip(self.data.T) + + def setup_data_to_plot(self): + self.update_data_to_plot() + + def pre_setup_controllers(self): + """This can change in child classes.""" + self.Controller = SinglePlaneSliceController() + self.data_controller_name = "SinglePlaneDataController" + + def setup_controllers(self): + """Controller updates are handled through the defined Controller class.""" + self.pre_setup_controllers() + + # Setup layout of controllers relative to each other + self.Controller.children = [ + widgets.VBox( + children=[ + self.Controller.components["ViewTypeController"], + widgets.HBox( + children=[ + self.Controller.components[self.data_controller_name], # Can change in child classes + self.Controller.components["ImShowController"], + ] + ), + ] + ) + ] + + # Set some initial values based on neurodata object and initial data to plot + self.Controller.frame_slider.max = self.two_photon_series.data.shape[0] - 1 + self.Controller.manual_contrast_slider.max = np.max(self.data_to_plot) + self.Controller.manual_contrast_slider.min = np.min(self.data_to_plot) + self.Controller.manual_contrast_slider.value = ( + self.Controller.manual_contrast_slider.min, + self.Controller.manual_contrast_slider.max, + ) + def update_figure( self, rotation_changed: Optional[bool] = None, @@ -78,17 +168,36 @@ def update_figure( self.figure = px.imshow(self.data_to_plot, **img_fig_kwargs) self.figure.update_traces(hovertemplate=None, hoverinfo="skip") - def update_canvas( - self, - rotation_changed: Optional[bool] = None, - frame_index: Optional[int] = None, - contrast_rescaling: Optional[str] = None, - contrast: Optional[Tuple[int]] = None, - ): - self.update_figure( - rotation_changed=rotation_changed, - frame_index=frame_index, - contrast_rescaling=contrast_rescaling, - contrast=contrast, - ) + def update_canvas(self, **update_figure_kwargs): + self.update_figure(update_figure_kwargs) self.Canvas.data[0].update(self.figure.data[0]) + + def set_canvas_title(self): + """This can change in child classes.""" + self.canvas_title = f"TwoPhotonSeries: {self.two_photon_series.name} - Planar slices of volume" + + def setup_canvas(self): + # Setup main figure area + self.update_figure() + self.Canvas = go.FigureWidget(self.figure) + self.set_canvas_title() + self.Canvas.layout.title = self.canvas_title + self.Canvas.update_xaxes(visible=False, showticklabels=False).update_yaxes(visible=False, showticklabels=False) + + # Final vizualization-specific setup of controller positions + # Move the Simplified/Detailed switch to the right part of screen + self.Controller.components["ViewTypeController"].layout.align_items = "flex-end" + + def setup_observers(self): + self.Controller.rotate_right.on_click(lambda change: self.update_canvas(rotation_changed=True)) + self.Controller.rotate_left.on_click(lambda change: self.update_canvas(rotation_changed=True)) + self.Controller.frame_slider.observe(lambda change: self.update_canvas(frame_index=change.new), names="value") + self.Controller.plane_slider.observe(lambda change: self.update_canvas(plane_index=change.new), names="value") + + self.Controller.contrast_type_toggle.observe(lambda change: self.update_canvas(), names="value") + self.Controller.auto_contrast_method.observe( + lambda change: self.update_canvas(contrast_rescaling=change.new), names="value" + ) + self.Controller.manual_contrast_slider.observe( + lambda change: self.update_canvas(contrast=change.new), names="value" + ) diff --git a/nwbwidgets/ophys/two_photon_series.py b/nwbwidgets/ophys/two_photon_series.py index d672cc5c..d06e7a1d 100644 --- a/nwbwidgets/ophys/two_photon_series.py +++ b/nwbwidgets/ophys/two_photon_series.py @@ -4,8 +4,8 @@ from pynwb.ophys import TwoPhotonSeries from tifffile import imread, TiffFile -from .single_plane import SinglePlaneVisualization -from .plane_slice import PlaneSliceVisualization +from .single_plane_2 import SinglePlaneVisualization2 +from .plane_slice_2 import PlaneSliceVisualization2 from .volume import VolumeVisualization from ..base import LazyTab @@ -50,11 +50,11 @@ def update_figure(index=0): self.children = [self.figure, self.slider] else: if len(indexed_timeseries.data.shape) == 3: - self.children = (SinglePlaneVisualization(two_photon_series=indexed_timeseries),) + self.children = (SinglePlaneVisualization2(two_photon_series=indexed_timeseries),) elif len(indexed_timeseries.data.shape) == 4: tab = LazyTab( - func_dict={"Planar Slice": PlaneSliceVisualization, "3D Volume": VolumeVisualization}, + func_dict={"Planar Slice": PlaneSliceVisualization2, "3D Volume": VolumeVisualization}, data=indexed_timeseries, ) self.children = (tab,) From ccd4b26380af0d1fd211ede792adc5a63469688b Mon Sep 17 00:00:00 2001 From: CodyCBakerPhD Date: Sun, 4 Dec 2022 12:40:20 +0000 Subject: [PATCH 19/20] debugging --- nwbwidgets/ophys/ophys_controllers.py | 2 +- nwbwidgets/ophys/plane_slice.py | 8 ++++---- nwbwidgets/ophys/single_plane.py | 7 +++---- nwbwidgets/ophys/two_photon_series.py | 6 +++--- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/nwbwidgets/ophys/ophys_controllers.py b/nwbwidgets/ophys/ophys_controllers.py index e2d0f944..9fc5f81e 100644 --- a/nwbwidgets/ophys/ophys_controllers.py +++ b/nwbwidgets/ophys/ophys_controllers.py @@ -84,7 +84,7 @@ def setup_visibility(self): self.set_detailed_visibility(visibile=False) def setup_observers(self): - self.view_type_toggle.observe(lambda change: self.update_visibility()) + self.view_type_toggle.observe(lambda change: self.update_visibility(), names="value") class SinglePlaneSliceController(BasePlaneSliceController): diff --git a/nwbwidgets/ophys/plane_slice.py b/nwbwidgets/ophys/plane_slice.py index 93469a95..9a31fda2 100644 --- a/nwbwidgets/ophys/plane_slice.py +++ b/nwbwidgets/ophys/plane_slice.py @@ -5,11 +5,11 @@ import numpy as np import h5py -from .single_plane_2 import SinglePlaneVisualization2 +from .single_plane import SinglePlaneVisualization from .ophys_controllers import VolumetricPlaneSliceController -class PlaneSliceVisualization(SinglePlaneVisualization2): +class PlaneSliceVisualization(SinglePlaneVisualization): """Sub-widget specifically for plane-wise views of a 4D TwoPhotonSeries.""" def _dimension_check(self): @@ -80,10 +80,10 @@ def update_figure( self.update_data(plane_index=plane_index) self.update_data_to_plot() - super().update_figure() + 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}" + self.canvas_title = f"TwoPhotonSeries: {self.two_photon_series.name} - Planar slices of volume" def setup_observers(self): super().setup_observers() diff --git a/nwbwidgets/ophys/single_plane.py b/nwbwidgets/ophys/single_plane.py index 47e343d7..7116ed21 100644 --- a/nwbwidgets/ophys/single_plane.py +++ b/nwbwidgets/ophys/single_plane.py @@ -25,7 +25,7 @@ def _dimension_check(self): def __init__(self, two_photon_series: TwoPhotonSeries): self.two_photon_series = two_photon_series - self._dimension_check(two_photon_series=two_photon_series) + self._dimension_check() super().__init__() @@ -169,12 +169,12 @@ def update_figure( self.figure.update_traces(hovertemplate=None, hoverinfo="skip") def update_canvas(self, **update_figure_kwargs): - self.update_figure(update_figure_kwargs) + self.update_figure(**update_figure_kwargs) self.Canvas.data[0].update(self.figure.data[0]) def set_canvas_title(self): """This can change in child classes.""" - self.canvas_title = f"TwoPhotonSeries: {self.two_photon_series.name} - Planar slices of volume" + self.canvas_title = f"TwoPhotonSeries: {self.two_photon_series.name}" def setup_canvas(self): # Setup main figure area @@ -192,7 +192,6 @@ def setup_observers(self): self.Controller.rotate_right.on_click(lambda change: self.update_canvas(rotation_changed=True)) self.Controller.rotate_left.on_click(lambda change: self.update_canvas(rotation_changed=True)) self.Controller.frame_slider.observe(lambda change: self.update_canvas(frame_index=change.new), names="value") - self.Controller.plane_slider.observe(lambda change: self.update_canvas(plane_index=change.new), names="value") self.Controller.contrast_type_toggle.observe(lambda change: self.update_canvas(), names="value") self.Controller.auto_contrast_method.observe( diff --git a/nwbwidgets/ophys/two_photon_series.py b/nwbwidgets/ophys/two_photon_series.py index d06e7a1d..529203c5 100644 --- a/nwbwidgets/ophys/two_photon_series.py +++ b/nwbwidgets/ophys/two_photon_series.py @@ -4,8 +4,8 @@ from pynwb.ophys import TwoPhotonSeries from tifffile import imread, TiffFile -from .single_plane_2 import SinglePlaneVisualization2 -from .plane_slice_2 import PlaneSliceVisualization2 +from .single_plane import SinglePlaneVisualization +from .plane_slice import PlaneSliceVisualization from .volume import VolumeVisualization from ..base import LazyTab @@ -54,7 +54,7 @@ def update_figure(index=0): elif len(indexed_timeseries.data.shape) == 4: tab = LazyTab( - func_dict={"Planar Slice": PlaneSliceVisualization2, "3D Volume": VolumeVisualization}, + func_dict={"Planar Slice": PlaneSliceVisualization, "3D Volume": VolumeVisualization}, data=indexed_timeseries, ) self.children = (tab,) From b296f8f7b02a6f8eaf13446fc3a6aa0225d5deca Mon Sep 17 00:00:00 2001 From: CodyCBakerPhD Date: Sun, 4 Dec 2022 13:22:15 +0000 Subject: [PATCH 20/20] debugging --- nwbwidgets/ophys/volume.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/nwbwidgets/ophys/volume.py b/nwbwidgets/ophys/volume.py index 1286325d..4327e9ab 100644 --- a/nwbwidgets/ophys/volume.py +++ b/nwbwidgets/ophys/volume.py @@ -1,3 +1,5 @@ +from typing import Optional + import ipywidgets as widgets from pynwb.ophys import TwoPhotonSeries @@ -7,16 +9,24 @@ class VolumeVisualization(widgets.VBox): def _first_volume_render(self): + self.Canvas.description = "Loading..." + + self.setup_data() + self.update_data_to_plot() + self.Canvas = widgets.Output() - self.update_canvas(index=self.frame_slider.value) + self.Canvas.layout.title = self.canvas_title + self.update_canvas(frame_index=self.Controller.frame_slider.value) + + self.children = (self.Canvas, self.Controller) def __init__(self, two_photon_series: TwoPhotonSeries): super().__init__() self.two_photon_series = two_photon_series self.canvas_title = f"TwoPhotonSeries: {self.two_photon_series.name} - Interactive Volume" - self.Canvas = widgets.Button(description="Render") - self.Canvas.on_click(self._first_volume_render) + self.Canvas = widgets.ToggleButton(description="Render") + self.Canvas.observe(lambda change: self._first_volume_render(), names="value") self.Canvas.layout.title = self.canvas_title self.setup_controllers() @@ -24,7 +34,9 @@ def __init__(self, two_photon_series: TwoPhotonSeries): self.children = (self.Canvas, self.Controller) - def update_data(self, frame_index=0): + def update_data(self, frame_index: Optional[int] = None): + frame_index = frame_index or self.Controller.frame_slider.value + self.data = self.two_photon_series.data[frame_index, ...] def setup_data(self): @@ -34,9 +46,6 @@ def setup_data(self): def update_data_to_plot(self): self.data_to_plot = self.data.transpose([1, 0, 2]) - def setup_data_to_plot(self): - self.update_data_to_plot() - def setup_controllers(self): self.Controller = FrameController() @@ -49,14 +58,6 @@ def update_canvas(self, frame_index: int = 0): with self.Canvas: p3.show() - def setup_canvas(self): - self.Canvas = widgets.Output() - self.Canvas.layout.title = self.canvas_title - - self.setup_data() - self.setup_data_to_plot() - self.update_canvas() - def setup_observers(self): self.Controller.frame_slider.observe(lambda change: self.update_canvas(frame_index=change.new), names="value")