Source code for nwbwidgets.ophys

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,
    TwoPhotonSeries,
    ImageSegmentation,
)

from tifffile import imread, TiffFile
from ndx_grayscalevolume import GrayscaleVolume

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 = ["red", "blue", "green", "black", "magenta", "yellow"]


[docs]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__() 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: 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) slider = widgets.IntSlider( value=0, min=0, max=n_samples - 1, orientation="horizontal" ) else: 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) elif len(indexed_timeseries.data.shape) == 4: import ipyvolume.pylab as p3 output = widgets.Output() 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() 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]
[docs]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)
[docs]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)
[docs]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
[docs]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
[docs]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
[docs] 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:]
[docs] 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
[docs] 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]) c = color_wheel[np.where(cats == plane_seg_hover_df[color_by][i])[0][0]] 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
[docs] @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
[docs]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)
[docs]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
[docs]class RoiResponseSeriesWidget(BaseGroupedTraceWidget): def __init__( self, roi_response_series: RoiResponseSeries, neurodata_vis_spec=None, **kwargs ): super().__init__(roi_response_series, "rois", **kwargs)