nwb-video-widgets 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ """NWB video player widget with configurable grid layout."""
2
+
3
+ import pathlib
4
+ from typing import Optional
5
+
6
+ import anywidget
7
+ import traitlets
8
+
9
+ # Default grid layout: single row with Left, Body, Right cameras
10
+ DEFAULT_GRID_LAYOUT = [["VideoLeftCamera", "VideoBodyCamera", "VideoRightCamera"]]
11
+
12
+
13
+ class NWBFileVideoPlayer(anywidget.AnyWidget):
14
+ """Display videos in a configurable grid layout with synchronized playback.
15
+
16
+ Parameters
17
+ ----------
18
+ nwbfile_raw : pynwb.NWBFile
19
+ Raw NWB file containing video ImageSeries in acquisition
20
+ dandi_asset : dandi.dandiapi.RemoteBlobAsset
21
+ Asset object for the raw NWB file. The dandiset is derived from
22
+ the asset's client and dandiset_id attributes.
23
+ grid_layout : list of list of str, optional
24
+ Grid layout specifying which videos to display and how to arrange them.
25
+ Each inner list represents a row, and each string is a video series name.
26
+ Videos not found in the NWB file are silently skipped.
27
+ Default: [["VideoLeftCamera", "VideoBodyCamera", "VideoRightCamera"]]
28
+
29
+ Example
30
+ -------
31
+ Default layout (single row):
32
+
33
+ >>> widget = NWBFileVideoPlayer(nwbfile_raw, dandi_asset)
34
+
35
+ Custom 2x2 grid:
36
+
37
+ >>> widget = NWBFileVideoPlayer(
38
+ ... nwbfile_raw, dandi_asset,
39
+ ... grid_layout=[
40
+ ... ["VideoLeftCamera", "VideoRightCamera"],
41
+ ... ["VideoBodyCamera"],
42
+ ... ]
43
+ ... )
44
+
45
+ Single video:
46
+
47
+ >>> widget = NWBFileVideoPlayer(
48
+ ... nwbfile_raw, dandi_asset,
49
+ ... grid_layout=[["VideoLeftCamera"]]
50
+ ... )
51
+ """
52
+
53
+ video_urls = traitlets.Dict({}).tag(sync=True)
54
+ grid_layout = traitlets.List([]).tag(sync=True)
55
+ # Timestamps for each video: {video_name: [t0, t1, ...]}
56
+ # Used to display NWB session time instead of video-relative time
57
+ video_timestamps = traitlets.Dict({}).tag(sync=True)
58
+
59
+ _esm = pathlib.Path(__file__).parent / "video_widget.js"
60
+ _css = pathlib.Path(__file__).parent / "video_widget.css"
61
+
62
+ def __init__(
63
+ self,
64
+ nwbfile_raw,
65
+ dandi_asset,
66
+ grid_layout: Optional[list[list[str]]] = None,
67
+ **kwargs,
68
+ ):
69
+ video_urls = self.get_video_urls_from_dandi(nwbfile_raw, dandi_asset)
70
+ video_timestamps = self.get_video_timestamps(nwbfile_raw)
71
+ layout = grid_layout if grid_layout is not None else DEFAULT_GRID_LAYOUT
72
+ super().__init__(
73
+ video_urls=video_urls,
74
+ grid_layout=layout,
75
+ video_timestamps=video_timestamps,
76
+ **kwargs,
77
+ )
78
+
79
+ @staticmethod
80
+ def get_video_urls_from_dandi(nwbfile_raw, dandi_asset) -> dict[str, str]:
81
+ """Extract video S3 URLs from raw NWB file using DANDI API.
82
+
83
+ Videos in NWB files are stored as ImageSeries with external_file paths.
84
+ This function finds all ImageSeries with external files and resolves
85
+ their relative paths to full S3 URLs using the DANDI API.
86
+
87
+ Parameters
88
+ ----------
89
+ nwbfile_raw : pynwb.NWBFile
90
+ Raw NWB file containing video ImageSeries in acquisition
91
+ dandi_asset : dandi.dandiapi.RemoteBlobAsset
92
+ Asset object for the raw NWB file. The dandiset is derived from
93
+ the asset's client and dandiset_id attributes.
94
+
95
+ Returns
96
+ -------
97
+ dict
98
+ Mapping of video names to S3 URLs.
99
+ Keys: 'VideoLeftCamera', 'VideoBodyCamera', 'VideoRightCamera'
100
+
101
+ Example
102
+ -------
103
+ >>> from dandi.dandiapi import DandiAPIClient
104
+ >>> client = DandiAPIClient()
105
+ >>> dandiset = client.get_dandiset("000409")
106
+ >>> dandi_asset = dandiset.get_asset_by_path("sub-.../sub-..._raw.nwb")
107
+ >>> video_urls = NWBFileVideoPlayer.get_video_urls_from_dandi(
108
+ ... nwbfile_raw, dandi_asset
109
+ ... )
110
+ """
111
+ from pathlib import Path
112
+
113
+ from pynwb.image import ImageSeries
114
+
115
+ # Derive dandiset from dandi_asset
116
+ dandiset = dandi_asset.client.get_dandiset(dandi_asset.dandiset_id)
117
+
118
+ nwb_parent = Path(dandi_asset.path).parent
119
+ video_urls = {}
120
+
121
+ for name, obj in nwbfile_raw.acquisition.items():
122
+ # Videos are stored as ImageSeries with external_file attribute
123
+ if isinstance(obj, ImageSeries) and obj.external_file is not None:
124
+ relative_path = obj.external_file[0].lstrip("./")
125
+ full_path = str(nwb_parent / relative_path)
126
+
127
+ video_asset = dandiset.get_asset_by_path(full_path)
128
+ if video_asset is not None:
129
+ video_urls[name] = video_asset.get_content_url(
130
+ follow_redirects=1, strip_query=True
131
+ )
132
+
133
+ return video_urls
134
+
135
+ @staticmethod
136
+ def get_video_timestamps(nwbfile_raw) -> dict[str, list[float]]:
137
+ """Extract video timestamps from NWB file ImageSeries.
138
+
139
+ Parameters
140
+ ----------
141
+ nwbfile_raw : pynwb.NWBFile
142
+ Raw NWB file containing video ImageSeries in acquisition
143
+
144
+ Returns
145
+ -------
146
+ dict
147
+ Mapping of video names to timestamp arrays.
148
+ Each array contains the NWB session timestamps for each frame.
149
+ """
150
+ from pynwb.image import ImageSeries
151
+
152
+ video_timestamps = {}
153
+
154
+ for name, obj in nwbfile_raw.acquisition.items():
155
+ if isinstance(obj, ImageSeries) and obj.external_file is not None:
156
+ # Get timestamps - could be explicit or computed from starting_time + rate
157
+ if obj.timestamps is not None:
158
+ timestamps = obj.timestamps[:]
159
+ elif obj.starting_time is not None and obj.rate is not None:
160
+ # Compute timestamps from starting_time and rate
161
+ n_frames = len(obj.external_file) if hasattr(obj, 'dimension') else 1
162
+ # For external files, we may not know frame count upfront
163
+ # Use a reasonable estimate or just store start/rate info
164
+ timestamps = [obj.starting_time]
165
+ else:
166
+ timestamps = [0.0]
167
+
168
+ video_timestamps[name] = [float(t) for t in timestamps]
169
+
170
+ return video_timestamps
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: nwb-video-widgets
3
+ Version: 0.1.0
4
+ Summary: Interactive Jupyter widgets for NWB video and pose visualization
5
+ Project-URL: Homepage, https://github.com/catalystneuro/nwb-video-widgets
6
+ Project-URL: Repository, https://github.com/catalystneuro/nwb-video-widgets
7
+ Author: Heberto Mayorquin
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: anywidget,jupyter,neuroscience,nwb,pose,video,widget
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: Jupyter
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: anywidget>=0.9.0
21
+ Requires-Dist: matplotlib
22
+ Requires-Dist: numpy
23
+ Requires-Dist: pynwb
24
+ Provides-Extra: dandi
25
+ Requires-Dist: dandi>=0.60.0; extra == 'dandi'
26
+ Requires-Dist: h5py; extra == 'dandi'
27
+ Requires-Dist: remfile>=0.1.13; extra == 'dandi'
28
+ Provides-Extra: test
29
+ Requires-Dist: opencv-python-headless; extra == 'test'
30
+ Requires-Dist: pytest-cov; extra == 'test'
31
+ Requires-Dist: pytest>=7.0; extra == 'test'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # nwb-video-widgets
35
+
36
+ [![PyPI version](https://badge.fury.io/py/nwb-video-widgets.svg)](https://badge.fury.io/py/nwb-video-widgets)
37
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
+
40
+ Interactive Jupyter widgets for NWB video and pose estimation visualization. Built with [anywidget](https://anywidget.dev/) for compatibility across JupyterLab, Jupyter Notebook, VS Code, and Google Colab.
41
+
42
+ ## Table of Contents
43
+
44
+ - [Installation](#installation)
45
+ - [Video Player Widgets](#video-player-widgets)
46
+ - [Pose Estimation Widgets](#pose-estimation-widgets)
47
+ ## Installation
48
+
49
+ For local only NWB file usage:
50
+
51
+ ```bash
52
+ pip install nwb-video-widgets
53
+ ```
54
+
55
+ For DANDI integration and streaming support:
56
+
57
+ ```bash
58
+ pip install nwb-video-widgets[dandi]
59
+ ```
60
+
61
+ ## Video Player Widgets
62
+
63
+ Multi-camera synchronized video player with configurable layout (Row, Column, or Grid).
64
+
65
+ ![Video Widget Demo](assets/video_widget_preprocessed.gif)
66
+
67
+ **Features:**
68
+
69
+ - Interactive settings panel for video selection
70
+ - Multiple layout modes (Row, Column, Grid)
71
+ - Synchronized playback across all videos
72
+ - Session time display with NWB timestamps
73
+
74
+ ### DANDI Streaming
75
+
76
+ Use `NWBDANDIVideoPlayer` for videos hosted on DANDI:
77
+
78
+ ```python
79
+ from dandi.dandiapi import DandiAPIClient
80
+ from nwb_video_widgets import NWBDANDIVideoPlayer
81
+
82
+ client = DandiAPIClient()
83
+ dandiset = client.get_dandiset("000409", "draft")
84
+ asset = dandiset.get_asset_by_path("sub-NYU-39/sub-NYU-39_ses-..._behavior.nwb")
85
+
86
+ widget = NWBDANDIVideoPlayer(asset=asset)
87
+ widget
88
+ ```
89
+
90
+ ### Local Files
91
+
92
+ Use `NWBLocalVideoPlayer` for local NWB files:
93
+
94
+ ```python
95
+ from pynwb import read_nwb
96
+ from nwb_video_widgets import NWBLocalVideoPlayer
97
+
98
+ nwbfile = read_nwb("experiment.nwb")
99
+ widget = NWBLocalVideoPlayer(nwbfile)
100
+ widget
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Pose Estimation Widgets
106
+
107
+ Overlays DeepLabCut keypoints on streaming video with support for camera selection.
108
+
109
+ ![Pose Estimation Widget Demo](assets/pose_estimation_preprocessed.gif)
110
+
111
+ **Features:**
112
+
113
+ - Camera selection via settings panel
114
+ - Keypoint visibility toggles (All/None/individual)
115
+ - Label display toggle
116
+ - Session time display (NWB timestamps)
117
+ - Custom keypoint colors via colormap or explicit hex values
118
+ - Supports split files (videos in raw file, pose in processed file)
119
+
120
+ ### DANDI Streaming
121
+
122
+ Use `NWBDANDIPoseEstimationWidget` for DANDI-hosted files:
123
+
124
+ ```python
125
+ from dandi.dandiapi import DandiAPIClient
126
+ from nwb_video_widgets import NWBDANDIPoseEstimationWidget
127
+
128
+ client = DandiAPIClient()
129
+ dandiset = client.get_dandiset("000409", "draft")
130
+
131
+ # Single file (videos + pose in same file)
132
+ asset = dandiset.get_asset_by_path("sub-.../sub-..._combined.nwb")
133
+ widget = NWBDANDIPoseEstimationWidget(asset=asset)
134
+
135
+ # Or split files (videos in raw, pose in processed)
136
+ raw_asset = dandiset.get_asset_by_path("sub-.../sub-..._desc-raw.nwb")
137
+ processed_asset = dandiset.get_asset_by_path("sub-.../sub-..._desc-processed.nwb")
138
+ widget = NWBDANDIPoseEstimationWidget(
139
+ asset=processed_asset,
140
+ video_asset=raw_asset,
141
+ )
142
+ widget
143
+ ```
144
+
145
+ ### Local Files
146
+
147
+ Use `NWBLocalPoseEstimationWidget` for local NWB files:
148
+
149
+ ```python
150
+ from pynwb import read_nwb
151
+ from nwb_video_widgets import NWBLocalPoseEstimationWidget
152
+
153
+ # Single file
154
+ nwbfile = read_nwb("experiment.nwb")
155
+ widget = NWBLocalPoseEstimationWidget(nwbfile)
156
+ widget
157
+
158
+ # Or split files
159
+ nwbfile_raw = read_nwb("raw.nwb")
160
+ nwbfile_processed = read_nwb("processed.nwb")
161
+ widget = NWBLocalPoseEstimationWidget(
162
+ nwbfile=nwbfile_processed,
163
+ video_nwbfile=nwbfile_raw,
164
+ )
165
+ widget
166
+ ```
167
+
168
+ **Parameters:**
169
+
170
+ | Parameter | Type | Description |
171
+ |-----------|------|-------------|
172
+ | `keypoint_colors` | `str` or `dict` | Matplotlib colormap name (e.g., `'tab10'`) or dict mapping keypoint names to hex colors |
173
+ | `default_camera` | `str` | Camera to display initially |
174
+
@@ -0,0 +1,15 @@
1
+ nwb_video_widgets/__init__.py,sha256=4m-AlratOMO31jFrnWQSRqU2duXHMs7ZI3Iogs-hbAs,616
2
+ nwb_video_widgets/_utils.py,sha256=kXcgjbq8mdgR0P4drPRy_Wrag3F5Od_7Ysikrr_VQMk,10566
3
+ nwb_video_widgets/dandi_pose_widget.py,sha256=Tk_ZnqZIRIJzIAghuc1OEh0tmP9tMDP8UVDzZ4CoySg,13511
4
+ nwb_video_widgets/dandi_video_widget.py,sha256=OaaJrNe0IoQ8X-fkDddIa6GfpCjsYZoso3-OAJEruTA,5020
5
+ nwb_video_widgets/local_pose_widget.py,sha256=6yrm3qoRf0C1s2CFQXTv9xCUG9NrDm4_mEv2Z8_QYww,12774
6
+ nwb_video_widgets/local_video_widget.py,sha256=c1RRTGkOdbtEBFHUw8FzHbpezWCdRPGauhXW72sTozM,4325
7
+ nwb_video_widgets/pose_widget.css,sha256=nTaPfxr-6TC2IlcjNf0jVHDEqG3dx0i5jF1mtf1tzPo,15172
8
+ nwb_video_widgets/pose_widget.js,sha256=zoXoLI1j63BOvngQ-Riufy7ae297VUidXW1KA7Z2TjI,26888
9
+ nwb_video_widgets/video_widget.css,sha256=h6G67yoRNaVR9obRZWJ8xgo6KBKbzuRfUgDPKCYlBFo,11893
10
+ nwb_video_widgets/video_widget.js,sha256=chqvXBD4p_9GEOvakVZHQqEz7WZwlzfNo0-toI_ix2Y,20235
11
+ nwb_video_widgets/video_widget.py,sha256=glRQOBquOHDFeKC_aW_fMvJGqqh90AsMRWzMGSokonA,6252
12
+ nwb_video_widgets-0.1.0.dist-info/METADATA,sha256=wh2Hie2ItnAEnHc31M0Uf8KRL3g7KsjMGUGecd9Xll4,5177
13
+ nwb_video_widgets-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ nwb_video_widgets-0.1.0.dist-info/licenses/LICENSE,sha256=9Xuwwu2OWDAyD-b0_T2W-E13CysuHPPHYu7aNSOtIMI,1070
15
+ nwb_video_widgets-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CatalystNeuro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.