Skip to content

Commit 87acf4d

Browse files
niksirbiedenopre-commit-ci[bot]lochhh
authored
Add I/O support for the ndx-pose NWB extension: take 2 (#360)
* Create nwb_export.py * NWB requires one file per individual * Add script * Remove import error handling * Add nwb optional dependencies * Fix linting based on pre-commit hooks * Add example docstring * Rename to fit module naming pattern * Add import from nwb * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Apply suggestions from code review Co-authored-by: Niko Sirmpilatze <niko.sirbiladze@gmail.com> * Update make pynwb and ndx-pose core dependencies * Cleanup of docstrings and variable names from code review * Rename function for clarity * Update with example converting back to movement * Add file validation and handling for single path * Add preliminary tests * Convert to numpy array * Handle lack of confidence * Display xarray * Refactor tests * Create nwb_export.py * NWB requires one file per individual * Remove import error handling * Add nwb optional dependencies * Fix linting based on pre-commit hooks * Rename to fit module naming pattern * Add import from nwb * Update make pynwb and ndx-pose core dependencies * Add file validation and handling for single path * Convert to numpy array * fix logging module import * constrained pynwb>=0.2.1 * fixed existing unit tests * add key_name argument to convert_nwb_to_movement * tests should only create temp file * use Generator instead of legacy np.random.random * reorder dims and use from_numpy for creating movement ds * define default nwb kwargs as constants * renamed and reformatted `add_movement_dataset_to_nwb` to `ds_to_nwb` * Expanded module-level docstring * use individual instead of subject * refactored functions for loading ds from nwb * make mypy happy with numpy typing * rename nwb example * renamed private func for creating pose estimation and skeletons objects * incorporate NWB loading into load_poses module * incorporate NWB saving function into save_poses module * simplified private nwb functions * provide examples in docstrings instead of sphinx gallery example * fix docstring syntax error * use pose estimation series rate if possible * use dot notation to access pose estimation attributes * remove underscore from _nwb.py file name * move imports at the top of docstring example * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix deprecated logger calls * Fix line too long * Add load_nwb test case * Move file fixtures to tests/fixtures/files.py * Add NWB file validator * Extend from_nwb_file tests * Add to_nwb_file tests * Rename NWBFile fixture * Remove repeated default nwb kwargs * Simplify `_ds_to_pose_and_skeleton_objects` test * Add pynwb to intersphinx mapping * Draft to_nwb_file refactor * Allow single key dict for single-ind datasets * Create Subject in NWBFile * Refactor resolve_kwargs * Reduce resolve_kwargs complexity * Include nwb module in API docs * Resolve pose_estimation_series_kwargs for single keypoint * Refactor _ds_to_pose_and_skeleton_objects pt1 * Allow deprioritising input ids * Add test for _ds_to_pose_and_skeletons * Update docstrings * Refactor NWBFileSaveConfig tests * Allow 0-d individuals * Link Subject in Skeleton * Update docstrings + rename resolve methods * Resolve pose_estimation_kwargs * Resolve skeleton_kwargs * Remove unused functions * Refactor _write_behavior_processing_module * Convert frames to seconds when saving * Use numpy.random.Generator * Refactor to_nwb_file tests * Remove unused constant * Add to_nwb_file example in docstrings * Add load NWB in IO guide * Add save NWB in IO guide * Use rng fixture in NWBFile fixtures * Apply docstring suggestions from code review Co-authored-by: Niko Sirmpilatze <niko.sirbiladze@gmail.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix reference to NWBFileSaveConfig in docstrings * Mention NWBFile config option in I/O guide * Clarify from_file fps arg * Replace `identifier` with `experimenter` in to_nwb_file config example * Only set `source_file` attribute when loading from NWB file path * Log warning for missing session_start_time in NWBFileSaveConfig * Rename `is_multi_individual` to `from_multi_individual` * Remove "open" when describing NWBFile objects * Use full NWBFile module refs in docstrings * Return a single NWBFile when saving single-individual datasets * Remove `identifier` from nwbfile_kwargs defaults * Gather NWB fixtures * Allow specifying key for processing module * Allow customising processing module via kwargs * Update nwb test config and comments for clarity --------- Co-authored-by: Eric Denovellis <edeno@bu.edu> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Denovellis <edeno@users.noreply.github.com> Co-authored-by: lochhh <changhuan.lo@ucl.ac.uk>
1 parent 30a7c95 commit 87acf4d

File tree

14 files changed

+2683
-499
lines changed

14 files changed

+2683
-499
lines changed

docs/source/conf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@
175175
("css/custom.css", {"priority": 100}),
176176
]
177177
html_js_files = [
178-
"js/contributors.js", # javascript for contributors table
178+
"js/contributors.js", # javascript for contributors table
179179
]
180180
html_favicon = "_static/light-logo-niu.png"
181181

@@ -225,6 +225,7 @@
225225
"pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None),
226226
"python": ("https://docs.python.org/3", None),
227227
"loguru": ("https://loguru.readthedocs.io/en/stable/", None),
228+
"pynwb": ("https://pynwb.readthedocs.io/en/stable/", None),
228229
}
229230

230231

docs/source/user_guide/input_output.md

Lines changed: 85 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ To analyse pose tracks, `movement` supports loading data from various frameworks
1212
- [LightingPose](lp:) (LP)
1313
- [Anipose](anipose:) (Anipose)
1414

15+
Additionally, `movement` supports loading data stored in [Neurodata Without Borders (NWB)](https://nwb-overview.readthedocs.io/en/latest/) format (using the [``ndx-pose``](https://github.com/rly/ndx-pose) extension).
16+
1517
To analyse bounding box tracks, `movement` currently supports the [VGG Image Annotator](via:) (VIA) format for [tracks annotation](via:docs/face_track_annotation.html).
1618

1719
:::{note}
@@ -36,44 +38,39 @@ To read a pose tracks file into a [movement poses dataset](target-poses-and-bbox
3638

3739
::::{tab-set}
3840

39-
:::{tab-item} SLEAP
40-
41-
To load [SLEAP analysis files](sleap:tutorials/analysis) in .h5 format (recommended):
41+
:::{tab-item} DeepLabCut
42+
To load DeepLabCut files in .h5 format:
4243
```python
43-
ds = load_poses.from_sleap_file("/path/to/file.analysis.h5", fps=30)
44+
ds = load_poses.from_dlc_file("/path/to/file.h5", fps=30)
4445

4546
# or equivalently
4647
ds = load_poses.from_file(
47-
"/path/to/file.analysis.h5", source_software="SLEAP", fps=30
48+
"/path/to/file.h5", source_software="DeepLabCut", fps=30
4849
)
4950
```
50-
To load [SLEAP analysis files](sleap:tutorials/analysis) in .slp format (experimental, see notes in {func}`movement.io.load_poses.from_sleap_file`):
51-
51+
To load DeepLabCut files in .csv format:
5252
```python
53-
ds = load_poses.from_sleap_file("/path/to/file.predictions.slp", fps=30)
53+
ds = load_poses.from_dlc_file("/path/to/file.csv", fps=30)
5454
```
5555
:::
5656

57-
:::{tab-item} DeepLabCut
58-
59-
To load DeepLabCut files in .h5 format:
57+
:::{tab-item} SLEAP
58+
To load [SLEAP analysis files](sleap:tutorials/analysis) in .h5 format (recommended):
6059
```python
61-
ds = load_poses.from_dlc_file("/path/to/file.h5", fps=30)
60+
ds = load_poses.from_sleap_file("/path/to/file.analysis.h5", fps=30)
6261

6362
# or equivalently
6463
ds = load_poses.from_file(
65-
"/path/to/file.h5", source_software="DeepLabCut", fps=30
64+
"/path/to/file.analysis.h5", source_software="SLEAP", fps=30
6665
)
6766
```
68-
69-
To load DeepLabCut files in .csv format:
67+
To load [SLEAP analysis files](sleap:tutorials/analysis) in .slp format (experimental, see notes in {func}`movement.io.load_poses.from_sleap_file`):
7068
```python
71-
ds = load_poses.from_dlc_file("/path/to/file.csv", fps=30)
69+
ds = load_poses.from_sleap_file("/path/to/file.predictions.slp", fps=30)
7270
```
7371
:::
7472

7573
:::{tab-item} LightningPose
76-
7774
To load LightningPose files in .csv format:
7875
```python
7976
ds = load_poses.from_lp_file("/path/to/file.analysis.csv", fps=30)
@@ -86,26 +83,53 @@ ds = load_poses.from_file(
8683
:::
8784

8885
:::{tab-item} Anipose
89-
9086
To load Anipose files in .csv format:
9187
```python
9288
ds = load_poses.from_anipose_file(
9389
"/path/to/file.analysis.csv", fps=30, individual_name="individual_0"
94-
) # We can optionally specify the individual name, by default it is "individual_0"
90+
) # Optionally specify the individual name; defaults to "individual_0"
9591

9692
# or equivalently
9793
ds = load_poses.from_file(
98-
"/path/to/file.analysis.csv", source_software="Anipose", fps=30, individual_name="individual_0"
94+
"/path/to/file.analysis.csv",
95+
source_software="Anipose",
96+
fps=30,
97+
individual_name="individual_0",
9998
)
99+
```
100+
:::
100101

102+
:::{tab-item} NWB
103+
To load NWB files in .nwb format:
104+
```python
105+
ds = load_poses.from_nwb_file(
106+
"path/to/file.nwb",
107+
processing_module_key="behavior",
108+
pose_estimation_key="PoseEstimation",
109+
) # Optionally specify the name of the ProcessingModule and PoseEstimation objects.
110+
# Defaults are "behavior" and "PoseEstimation", respectively.
111+
112+
# or equivalently
113+
ds = load_poses.from_file(
114+
"path/to/file.nwb",
115+
source_software="NWB",
116+
processing_module_key="behavior",
117+
pose_estimation_key="PoseEstimation",
118+
)
119+
```
120+
The above functions also accept an {class}`NWBFile<pynwb.file.NWBFile>` object as input:
121+
```python
122+
with pynwb.NWBHDF5IO("path/to/file.nwb", mode="r") as io:
123+
nwb_file = io.read()
124+
ds = load_poses.from_nwb_file(
125+
nwb_file, pose_estimation_key="PoseEstimation"
126+
)
101127
```
102128
:::
103129

104130
:::{tab-item} From NumPy
105-
106131
In the example below, we create random position data for two individuals, ``Alice`` and ``Bob``,
107132
with three keypoints each: ``snout``, ``centre``, and ``tail_base``. These keypoints are tracked in 2D space for 100 frames, at 30 fps. The confidence scores are set to 1 for all points.
108-
109133
```python
110134
import numpy as np
111135

@@ -142,7 +166,6 @@ We currently support loading bounding box tracks in the VGG Image Annotator (VIA
142166

143167
::::{tab-set}
144168
:::{tab-item} VGG Image Annotator
145-
146169
To load a VIA tracks .csv file:
147170
```python
148171
ds = load_bboxes.from_via_tracks_file("path/to/file.csv", fps=30)
@@ -154,17 +177,12 @@ ds = load_bboxes.from_file(
154177
fps=30,
155178
)
156179
```
157-
158180
Note that the x,y coordinates in the input VIA tracks .csv file represent the the top-left corner of each bounding box. Instead the corresponding ``movement`` dataset `ds` will hold in its `position` array the centroid of each bounding box.
159-
160-
161181
:::
162182

163183
:::{tab-item} From NumPy
164-
165184
In the example below, we create random position data for two bounding boxes, ``id_0`` and ``id_1``,
166185
both with the same width (40 pixels) and height (30 pixels). These are tracked in 2D space for 100 frames, which will be numbered in the resulting dataset from 0 to 99. The confidence score for all bounding boxes is set to 0.5.
167-
168186
```python
169187
import numpy as np
170188

@@ -182,16 +200,16 @@ ds = load_bboxes.from_numpy(
182200

183201
The resulting data structure `ds` will include the centroid trajectories for each tracked bounding box, the boxes' widths and heights, and their associated confidence values if provided.
184202

185-
186-
187203
For more information on the bounding boxes data structure, see the [movement dataset](target-poses-and-bboxes-dataset) page.
188204

189205

190206
(target-saving-pose-tracks)=
191207
## Saving pose tracks
192208
[movement poses datasets](target-poses-and-bboxes-dataset) can be saved in a variety of
193-
formats, including DeepLabCut-style files (.h5 or .csv) and
194-
[SLEAP-style analysis files](sleap:tutorials/analysis) (.h5).
209+
formats:
210+
- DeepLabCut-style files (.h5 or .csv)
211+
- [SLEAP-style analysis files](sleap:tutorials/analysis) (.h5)
212+
- NWB files (.nwb)
195213

196214
To export pose tracks from `movement`, first import the {mod}`movement.io.save_poses` module:
197215

@@ -203,13 +221,22 @@ Then, depending on the desired format, use one of the following functions:
203221

204222
:::::{tab-set}
205223

206-
::::{tab-item} SLEAP
224+
::::{tab-item} DeepLabCut
225+
To save as a DeepLabCut file, in .h5 or .csv format:
226+
```python
227+
save_poses.to_dlc_file(ds, "/path/to/file.h5") # preferred format
228+
save_poses.to_dlc_file(ds, "/path/to/file.csv")
229+
```
230+
The {func}`movement.io.save_poses.to_dlc_file` function also accepts
231+
a `split_individuals` boolean argument. If set to `True`, the function will
232+
save the data as separate single-animal DeepLabCut-style files.
233+
::::
207234

235+
::::{tab-item} SLEAP
208236
To save as a SLEAP analysis file in .h5 format:
209237
```python
210238
save_poses.to_sleap_analysis_file(ds, "/path/to/file.h5")
211239
```
212-
213240
:::{note}
214241
When saving to SLEAP-style files, only `track_names`, `node_names`, `tracks`, `track_occupancy`,
215242
and `point_scores` are saved. `labels_path` will only be saved if the source
@@ -222,22 +249,7 @@ each attribute and data variable represents, see the
222249
:::
223250
::::
224251

225-
::::{tab-item} DeepLabCut
226-
227-
To save as a DeepLabCut file, in .h5 or .csv format:
228-
```python
229-
save_poses.to_dlc_file(ds, "/path/to/file.h5") # preferred format
230-
save_poses.to_dlc_file(ds, "/path/to/file.csv")
231-
```
232-
233-
The {func}`movement.io.save_poses.to_dlc_file` function also accepts
234-
a `split_individuals` boolean argument. If set to `True`, the function will
235-
save the data as separate single-animal DeepLabCut-style files.
236-
237-
::::
238-
239252
::::{tab-item} LightningPose
240-
241253
To save as a LightningPose file in .csv format:
242254
```python
243255
save_poses.to_lp_file(ds, "/path/to/file.csv")
@@ -249,8 +261,32 @@ DeepLabCut .csv format, the above command is equivalent to:
249261
save_poses.to_dlc_file(ds, "/path/to/file.csv", split_individuals=True)
250262
```
251263
:::
264+
::::
265+
266+
::::{tab-item} NWB
267+
To convert a `movement` poses dataset to {class}`NWBFile<pynwb.file.NWBFile>` objects:
268+
```python
269+
nwb_files = save_poses.to_nwb_file(ds)
270+
```
271+
The {func}`movement.io.save_poses.to_nwb_file` function also accepts
272+
a {class}`movement.io.nwb.NWBFileSaveConfig` object as its ``config`` argument
273+
for customising metadata such as session or subject information in the resulting `NWBFile`s
274+
(see {func}`the API reference<movement.io.save_poses.to_nwb_file>` for examples).
252275

276+
These `NWBFile`s can then be saved to disk as .nwb files using {class}`pynwb.NWBHDF5IO`:
277+
```python
278+
from pynwb import NWBHDF5IO
279+
280+
for file in nwb_files:
281+
with NWBHDF5IO(f"{file.identifier}.nwb", "w") as io:
282+
io.write(file)
283+
```
284+
:::{note}
285+
To allow adding additional data to NWB files before saving, {func}`to_nwb_file<movement.io.save_poses.to_nwb_file>` does not write to disk directly.
286+
Instead, it returns a list of {class}`NWBFile<pynwb.file.NWBFile>` objects---one per individual in the dataset---since NWB files are designed to represent data from a single individual.
287+
:::
253288
::::
289+
254290
:::::
255291

256292

0 commit comments

Comments
 (0)