Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ImageSeriesWidget fixes #196

Open
wants to merge 56 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
0606860
move tests outside the nwbwidgets package
bendichter Nov 9, 2021
eaa77b8
Merge remote-tracking branch 'origin/master'
bendichter Nov 9, 2021
8e38592
improve plane segmentation coverage
bendichter Nov 9, 2021
22396a5
Merge branch 'master' into improve_plane_seg_coverage
bendichter Nov 9, 2021
3603e17
improve plane segmentation coverage
bendichter Nov 9, 2021
68a4e80
Merge remote-tracking branch 'origin/improve_plane_seg_coverage' into…
bendichter Nov 9, 2021
4940ee7
test show_timeseries_mpl
bendichter Nov 9, 2021
3a6a197
Merge branch 'master' into improve_plane_seg_coverage
bendichter Nov 15, 2021
f5e21cb
Merge branch 'master' into improve_plane_seg_coverage
bendichter Dec 31, 2021
b438b7c
extenral files existance check fix
Saksham20 Jan 4, 2022
234f37d
merging with 2photonseries widget implementation to make parent class
Saksham20 Jan 4, 2022
e5024ad
fixes
Saksham20 Jan 4, 2022
6a45abd
fixes
Saksham20 Jan 5, 2022
45b2098
remove time window controller
Saksham20 Jan 5, 2022
b91e39a
inherit from ImageSeries
Saksham20 Jan 5, 2022
5211089
reformat
Saksham20 Jan 5, 2022
85f71d8
Merge branch 'improve_plane_seg_coverage' into two_photon_series_fixes
Saksham20 Jan 5, 2022
45b1f7c
fix 2pseries tests init arg
Saksham20 Jan 5, 2022
b6bcaad
bug fix
Saksham20 Jan 5, 2022
1095759
propagate neurodata_vis_spec
Saksham20 Jan 5, 2022
bf5fa4d
propagate neurodata_vis_spec
Saksham20 Jan 5, 2022
1136fac
Revert "fix 2pseries tests init arg"
Saksham20 Jan 5, 2022
2d02901
restructure
Saksham20 Jan 6, 2022
a7c06e7
using go.image
Saksham20 Jan 6, 2022
34a053c
Update nwbwidgets/ophys.py
Saksham20 Jan 6, 2022
8906b88
black
Saksham20 Jan 7, 2022
9782709
add imageseries.py
Saksham20 Jan 7, 2022
17d9495
using custom context manager without inheritance
Saksham20 Jan 7, 2022
d2b7d59
remove print st
Saksham20 Jan 7, 2022
22a34ff
tests setup
Saksham20 Jan 8, 2022
0288883
cv2 dependency
Saksham20 Jan 8, 2022
4d785c2
cv2 dependency
Saksham20 Jan 8, 2022
7dda8db
neurodataviz spec not required
Saksham20 Jan 8, 2022
f31e312
neurodataviz spec not required
Saksham20 Jan 8, 2022
f73fde3
return variable novie frames number
Saksham20 Jan 9, 2022
798a528
indentation
Saksham20 Jan 9, 2022
c8a622f
add tests for ImageSeries
Saksham20 Jan 9, 2022
aa04d9f
varible movie frames
Saksham20 Jan 9, 2022
d29ea9b
import specific fixtures
Saksham20 Jan 9, 2022
5509487
kwargs update for videowriter
Saksham20 Jan 10, 2022
b5af95c
fix video writer
Saksham20 Jan 10, 2022
0213ccc
movie frames loop bug fix
Saksham20 Jan 10, 2022
9737047
rename fixtures.py to fixtures.py
Saksham20 Jan 10, 2022
29b003e
Merge branch 'master' into two_photon_series_fixes
Saksham20 Jan 10, 2022
8e72ae9
add all callbacks as class methods
Saksham20 Jan 11, 2022
9bb4cf0
time slider spans frames if video file selected is other than default
Saksham20 Jan 11, 2022
3add69a
bug fix
Saksham20 Jan 11, 2022
ae8e9cc
linking foreign time slider to timeslider
Saksham20 Jan 12, 2022
c2e2be0
using px for faster plotting
Saksham20 Jan 12, 2022
fe7d182
black
Saksham20 Jan 12, 2022
85a0d0e
use self.get_frame
Saksham20 Jan 13, 2022
3ea7f58
use video start_times, visible time slider
Saksham20 Jan 13, 2022
329d702
get fps of video and construct external file frame index
Saksham20 Jan 13, 2022
6541993
fps constant across external video files
Saksham20 Jan 13, 2022
089ed60
movie fps fixes
Saksham20 Jan 13, 2022
1d131bf
external files list, indexing fixes
Saksham20 Jan 13, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 133 additions & 54 deletions nwbwidgets/image.py
Original file line number Diff line number Diff line change
@@ -1,94 +1,173 @@
from pathlib import Path, PureWindowsPath
from pathlib import Path
from typing import Union

import matplotlib.pyplot as plt
import numpy as np
import plotly.graph_objects as go
import pynwb
from ipywidgets import widgets, fixed, Layout
from ipywidgets import widgets, Layout
from pynwb.image import GrayscaleImage, ImageSeries, RGBImage
from tifffile import imread, TiffFile

from .base import fig2widget
from .controllers import StartAndDurationController
from .utils.cmaps import linear_transfer_function
from .utils.imageseries import get_frame_count, get_frame
from .utils.timeseries import (
get_timeseries_maxt,
get_timeseries_mint,
timeseries_time_to_ind,
)

PathType = Union[str, Path]


class ImageSeriesWidget(widgets.VBox):
"""Widget showing ImageSeries."""

def __init__(
self,
imageseries: ImageSeries,
foreign_time_window_controller: StartAndDurationController = None,
**kwargs
foreign_time_slider: widgets.FloatSlider = None,
neurodata_vis_spec: dict = None,
):
super().__init__()
self.imageseries = imageseries
self.controls = {}
self.out_fig = None
self.figure = None
self.time_slider = foreign_time_slider

# Set controller
if foreign_time_window_controller is None:
tmin = get_timeseries_mint(imageseries)
if imageseries.external_file and imageseries.rate:
tif = TiffFile(imageseries.external_file[0])
tmax = imageseries.starting_time + len(tif.pages) / imageseries.rate
else:
tmax = get_timeseries_maxt(imageseries)
self.time_window_controller = StartAndDurationController(tmax, tmin)
if imageseries.external_file is not None:

# set time slider:
tmax = (
imageseries.starting_time
+ get_frame_count(imageseries.external_file[0]) / imageseries.rate
)
if self.time_slider is None:
self.time_slider = widgets.FloatSlider(
min=imageseries.starting_time,
max=tmax,
orientation="horizontal",
description="time(s)",
)
external_file = imageseries.external_file[0]
Saksham20 marked this conversation as resolved.
Show resolved Hide resolved
self.file_selector = None
# set file selector:
if len(imageseries.external_file) > 1:
self.file_selector = widgets.Dropdown(options=imageseries.external_file)
external_file = self.file_selector.value

def update_time_slider(value):
Saksham20 marked this conversation as resolved.
Show resolved Hide resolved
path_ext_file = value["new"]
# Read first frame
nonlocal external_file
external_file = path_ext_file
tmax = (
imageseries.starting_time
+ get_frame_count(path_ext_file) / imageseries.rate
)
tmin = 0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why reset tmin to 0 every update?

Copy link
Contributor Author

@Saksham20 Saksham20 Jan 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

knowing the starting_time for video files other than the first one is not possible. Unless we assume the starting time to the end time for the last video in the sequence. But I don't think this would be a good approximation, so I have made the time_slider widget to span the video frames (0, num_frames) instead, only when the user requests video other than the first one. What do you think of this approach?
Check here https://github.com/NeurodataWithoutBorders/nwb-jupyter-widgets/blob/3add69ade68d62a7790abeac45f538e11c7ea7dc/nwbwidgets/image.py#L114

self.time_slider.max = tmax
self.time_slider.min = tmin
self._set_figure_external(tmin, external_file, tmin)

self.file_selector.observe(update_time_slider, names="value")

# set time slider callbacks:
def change_fig(change):
time = change["new"]
starting_time = change["owner"].min
self._set_figure_external(time, external_file, starting_time)

self.time_slider.observe(change_fig, names="value")
self._set_figure_external(
imageseries.starting_time, external_file, imageseries.starting_time
)
# set children:
self.children = self.get_children(self.file_selector)
else:
self.time_window_controller = foreign_time_window_controller
self.set_controls(**kwargs)
if len(imageseries.data.shape) == 3:
self._set_figure_2d(0)

# Make widget figure
self.set_out_fig()
def time_slider_callback(change):
frame_number = self.time_to_index(change["new"])
self._set_figure_2d(frame_number)

self.children = [self.out_fig, self.time_window_controller]
elif len(imageseries.data.shape) == 4:
self._set_figure_3d(0)

def time_to_index(self, time):
if self.imageseries.external_file and self.imageseries.rate:
return int((time - self.imageseries.starting_time) * self.imageseries.rate)
else:
return timeseries_time_to_ind(self.imageseries, time)
def time_slider_callback(change):
frame_number = self.time_to_index(change["new"])
self._set_figure_3d(frame_number)

def set_controls(self, **kwargs):
self.controls.update(
timeseries=fixed(self.imageseries), time_window=self.time_window_controller
else:
raise NotImplementedError

# creat time window controller:
tmin = get_timeseries_mint(imageseries)
tmax = get_timeseries_maxt(imageseries)
if self.time_slider is None:
self.time_slider = widgets.FloatSlider(
value=tmin,
min=tmin,
max=tmax,
orientation="horizontal",
description="time(s)",
)
self.time_slider.observe(time_slider_callback, names="value")
self.children = self.get_children()

def _set_figure_3d(self, frame_number):
import ipyvolume.pylab as p3

output = widgets.Output()
p3.figure()
p3.volshow(
self.imageseries.data[frame_number].transpose([1, 0, 2]),
tf=linear_transfer_function([0, 0, 0], max_opacity=0.3),
)
self.controls.update({key: widgets.fixed(val) for key, val in kwargs.items()})
output.clear_output(wait=True)
self.figure = output
with output:
p3.show()

def _set_figure_2d(self, frame_number):
data = self.imageseries.data[frame_number].T
if self.figure is None:
self.figure = go.FigureWidget(data=dict(type="image", z=data))
else:
self._add_fig_trace(data, frame_number)

def get_frame(self, idx):
if self.imageseries.external_file is not None:
return imread(self.imageseries.external_file, key=idx)
def _set_figure_external(self, time, ext_file_path, starting_time):
frame_number = self.time_to_index(time, starting_time)
data = get_frame(ext_file_path, frame_number)
if self.figure is None:
self.figure = go.FigureWidget(data=dict(type="image", z=data))
else:
return self.image_series.data[idx].T
self._add_fig_trace(data, frame_number)

def set_out_fig(self):
def _add_fig_trace(self, img_data: np.ndarray, index):
self.figure.data[0]["z"] = img_data
self.figure.layout.title = f"Frame no: {index}"

self.out_fig = go.FigureWidget(
data=go.Heatmap(
z=self.get_frame(0),
colorscale="gray",
showscale=False,
)
)
self.out_fig.update_layout(
xaxis=go.layout.XAxis(showticklabels=False, ticks=""),
yaxis=go.layout.YAxis(
showticklabels=False, ticks="", scaleanchor="x", scaleratio=1
),
def time_to_index(self, time, starting_time=None):
starting_time = (
starting_time
if starting_time is not None
else self.imageseries.starting_time
)
if self.imageseries.external_file and self.imageseries.rate:
return int((time - starting_time) * self.imageseries.rate)
else:
return timeseries_time_to_ind(self.imageseries, time)

def on_change(change):
# Read frame
frame_number = self.time_to_index(change["new"][0])
image = self.get_frame(frame_number)
self.out_fig.data[0].z = image
def get_children(self, *widgets):
set_widgets = [wid for wid in widgets if wid is not None]
return [self.figure, self.time_slider, *set_widgets]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a foreign time slider should not be included in the children

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case there is a foreign_time_slider, then what is the best way to add another callback (update figure) ? The usual way of using foreign_time_slider.observe(set_figure) would overwrite the old callbacks.

One possible solution would be to link the values of foreign_time_slider to the new time_slider: widgets.jslink((foreign_time_slider,"value"),(time_slider,"value")) In effect, the time_slider would now be controlled by the foreign slider while keeping the functionality of being changed independently.

Check ae8e9cc


self.controls["time_window"].observe(on_change)
def get_frame(self, idx):
if self.imageseries.external_file is not None:
return get_frame(self.imageseries.external_file[0])
else:
return self.imageseries.data[idx].T


def show_image_series(image_series: ImageSeries, neurodata_vis_spec: dict):
Expand Down
94 changes: 13 additions & 81 deletions nwbwidgets/ophys.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import ipywidgets as widgets
import numpy as np
import plotly.graph_objects as go
import plotly.express as px
from ndx_grayscalevolume import GrayscaleVolume
from pynwb.base import NWBDataInterface
from pynwb.ophys import (
Expand All @@ -14,89 +13,22 @@
ImageSegmentation,
)
from skimage import measure
from tifffile import imread, TiffFile

from .base import df_to_hover_text
from .controllers import ProgressBar
from .image import ImageSeriesWidget
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"]


class TwoPhotonSeriesWidget(widgets.VBox):
class TwoPhotonSeriesWidget(ImageSeriesWidget):
"""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]
n_y, n_x = page.shape

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]
def __init__(self, indexed_timeseries: TwoPhotonSeries, neurodata_vis_spec: dict = None):
super().__init__(indexed_timeseries,neurodata_vis_spec)


def show_df_over_f(df_over_f: DfOverF, neurodata_vis_spec: dict):
Expand Down Expand Up @@ -223,13 +155,13 @@ def update_fig(self, color_by):
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,
self,
color_wheel: list = color_wheel,
color_by: str = None,
threshold: float = 0.01,
fig: go.Figure = None,
width: int = 600,
ref_image=None,
):
"""

Expand Down Expand Up @@ -360,6 +292,6 @@ 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
self, roi_response_series: RoiResponseSeries, neurodata_vis_spec=None, **kwargs
):
super().__init__(roi_response_series, "rois", **kwargs)
Loading