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 all 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
214 changes: 155 additions & 59 deletions nwbwidgets/image.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
from pathlib import Path, PureWindowsPath

from pathlib import Path
from typing import Union
from tqdm import tqdm
import matplotlib.pyplot as plt
import numpy as np
import plotly.graph_objects as go
import plotly.express as px
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, get_fps
from .utils.timeseries import (
get_timeseries_maxt,
get_timeseries_mint,
timeseries_time_to_ind,
)
from .controllers.time_window_controllers import StartAndDurationController
PathType = Union[str, Path]


class ImageSeriesWidget(widgets.VBox):
Expand All @@ -22,73 +27,164 @@ class ImageSeriesWidget(widgets.VBox):
def __init__(
self,
imageseries: ImageSeries,
foreign_time_window_controller: StartAndDurationController = None,
**kwargs
foreign_start_duration_controller: StartAndDurationController = None,
neurodata_vis_spec: dict = None,
):
super().__init__()
self.imageseries = imageseries
self.controls = {}
self.out_fig = None

# 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)
self.figure = None
self.time_slider = None
self.external_file = imageseries.external_file
self.file_selector = None
self.video_start_times = []
self.fps = self.get_fps()
self.external_files = []

if imageseries.external_file is not None:
self.external_files = [i for i in self.imageseries.external_file]
self.video_start_times = self._get_video_start_times()
self.time_slider = widgets.FloatSlider(
min=self.video_start_times[0],
max=self.video_start_times[1]-1/self.fps,
orientation="horizontal",
description="time(s)",
continuous_update=False,
)
self.external_file = imageseries.external_file[0]
# set file selector:
if len(imageseries.external_file) > 1:
self.file_selector = widgets.Dropdown(options=imageseries.external_file)
self.external_file = self.file_selector.value
self.file_selector.observe(self._update_time_slider, names="value")

self.time_slider.observe(self._time_slider_callback_external, names="value")
self._set_figure_from_time(
imageseries.starting_time, imageseries.starting_time, self.external_file,
)
else:
self.time_window_controller = foreign_time_window_controller
self.set_controls(**kwargs)

# Make widget figure
self.set_out_fig()
tmin = get_timeseries_mint(imageseries)
tmax = get_timeseries_maxt(imageseries)
self.time_slider = widgets.FloatSlider(
value=tmin,
min=tmin,
max=tmax,
orientation="horizontal",
description="time(s)",
continuous_update=False,
)
if len(imageseries.data.shape) == 3:
self._set_figure_from_frame(0)
self.time_slider.observe(self._time_slider_callback_2d, names="value")

self.children = [self.out_fig, self.time_window_controller]
elif len(imageseries.data.shape) == 4:
self._set_figure_3d(0)
self.time_slider.observe(self._time_slider_callback_3d, names="value")
else:
raise NotImplementedError

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)
# set visible time slider:
if foreign_start_duration_controller is None:
self.visible_time_slider = self.time_slider
else:
return timeseries_time_to_ind(self.imageseries, time)

def set_controls(self, **kwargs):
self.controls.update(
timeseries=fixed(self.imageseries), time_window=self.time_window_controller
self.visible_time_slider = foreign_start_duration_controller
# link the value[0] to time_slider value
def _link_time_slider(change):
self.time_slider.value = change["new"][0]
self.visible_time_slider.observe(_link_time_slider, names="value")
self.children = self.get_children(self.file_selector)

def _get_video_start_times(self):
if self.external_file is not None:
start_times=[self.imageseries.starting_time]
for file in tqdm(self.imageseries.external_file,
desc="retrieving video start times"):
file_time_duration = get_frame_count(file) / self.fps
start_times.append(file_time_duration)
return np.cumsum(start_times)

def _time_slider_callback_2d(self, change):
self._set_figure_from_time(change["new"][0])

def _time_slider_callback_3d(self, change):
frame_number = self.time_to_index(change["new"][0])
self._set_figure_3d(frame_number)

def _time_slider_callback_external(self, change):
time = change["new"]
starting_time = change["owner"].min
self._set_figure_from_time(time, starting_time, self.external_file)

def _update_time_slider(self, value):
path_ext_file = value["new"]
self.external_file = path_ext_file
idx = self.external_files.index(self.external_file)
tmin = self.video_start_times[idx]
tmax = self.video_start_times[idx+1]
tmax_ = tmax - 1/self.fps
tmax = tmax_ if tmax_>tmin else tmax
if tmax < self.time_slider.min: # order of setting min/max depends
self.time_slider.min = tmin
self.time_slider.max = tmax
else:
self.time_slider.max = tmax
self.time_slider.min = tmin
self._set_figure_from_frame(0, self.external_file)

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()})

def get_frame(self, idx):
if self.imageseries.external_file is not None:
return imread(self.imageseries.external_file, key=idx)
output.clear_output(wait=True)
self.figure = output
with output:
p3.show()

def _set_figure_from_time(self, time, starting_time, ext_file_path=None):
frame_number = self.time_to_index(time, starting_time)
self._set_figure_from_frame(frame_number, ext_file_path)

def _set_figure_from_frame(self, frame_number, ext_file_path=None):
data = self.get_frame(frame_number, ext_file_path)
if self.figure is None:
img = px.imshow(data, binary_string=True)
self.figure = go.FigureWidget(img)
else:
return self.image_series.data[idx].T
img = px.imshow(data, binary_string=True)
self.figure.for_each_trace(lambda trace: trace.update(img.data[0]))
self.figure.layout.title = f"Frame no: {frame_number}"

def set_out_fig(self):

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 get_fps(self):
if self.imageseries.rate is None:
fps = self.imageseries.timestamps[1]-self.imageseries.timestamps[0]
else:
fps = self.imageseries.rate
return fps

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:
return int((time - starting_time) * self.fps)
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.visible_time_slider, *set_widgets]

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


def show_image_series(image_series: ImageSeries, neurodata_vis_spec: dict):
Expand Down
80 changes: 7 additions & 73 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,24 @@
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
Loading