Skip to content

Commit d2e4286

Browse files
authored
Merge pull request #46 from CBroz1/dev
Add NWB export function
2 parents 2143d20 + b9580de commit d2e4286

4 files changed

Lines changed: 145 additions & 1 deletion

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Exporting data to NWB
2+
3+
## Description
4+
5+
The `export/nwb.py` module calls [DLC2NWB](https://github.com/DeepLabCut/DLC2NWB/) to
6+
save output generated by Element DeepLabCut as NWB files.
7+
The main function, `dlc_session_to_nwb`, contains a flag to control calling a parallel
8+
function in [Element Session](https://github.com/datajoint/element-session/blob/main/element_session/export/nwb.py).
9+
10+
As DLC2NWB does not currently offer a separate function for generating `PoseEstimation`
11+
objects (see [ndx-pose](https://github.com/rly/ndx-pose)), the current solution is to
12+
allow DLC2NWB to write to disk, and optionally rewrite this file using metadata provided
13+
by the export function in Element Session.
14+
15+
## Usage
16+
17+
Before using, please install [DLC2NWB](https://github.com/DeepLabCut/DLC2NWB/)
18+
19+
```bash
20+
pip install dlc2nwb
21+
```
22+
23+
Then, call the export function using keys from the `PoseEstimation` table.
24+
25+
```python
26+
from element_deeplabcut import model
27+
from element_session import session
28+
from element_deeplabcut.export import dlc_session_to_nwb
29+
30+
session_key = (session.Session & CONDITION)
31+
pose_key = (model.PoseEstimation & session_key).fetch1('KEY')
32+
dlc_session_to_nwb(pose_key, use_element_session=True, session_kwargs=SESSION_KWARGS)
33+
```
34+
35+
36+
Here, `CONDITION` should uniquely identify a session and `SESSION_KWARGS` can be any of
37+
the items described in the docstring of `element_session.export.nwb.session_to_nwb`
38+
as a dictionary.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .nwb import dlc_session_to_nwb

element_deeplabcut/export/nwb.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""
2+
Portions of code adapted from DeepLabCut/DLC2NWB
3+
MIT License Copyright (c) 2022 Alexander Mathis
4+
DataJoint export methods for DeepLabCut 2.x
5+
"""
6+
import logging
7+
import warnings
8+
from pathlib import Path
9+
from collections import abc
10+
from pynwb import NWBHDF5IO
11+
from hdmf.build.warnings import DtypeConversionWarning
12+
from .. import model
13+
14+
try: # Not all users will want NWB export, so dependency not in requirements.
15+
from dlc2nwb.utils import convert_h5_to_nwb, write_subject_to_nwb
16+
except ImportError:
17+
raise ImportError(
18+
"The package `dlc2nwb` is missing. Please run `pip install dlc2nwb`."
19+
)
20+
21+
logger = logging.getLogger("datajoint")
22+
23+
24+
def dlc_session_to_nwb(keys, use_element_session=True, session_kwargs=None):
25+
"""Using keys from PoseEstimation table, save DLC's h5 output to NWB.
26+
27+
Calls DLC2NWB to export NWB file using current h5 on disk. If use_element_session,
28+
calls NWB export function from Elements for lab, animal and session, passing
29+
session_kwargs. Saves output based on naming convention in DLC2NWB. If output path
30+
already exists, returns output path without making changes to the file.
31+
NOTE: does not support multianimal exports
32+
33+
Parameters
34+
----------
35+
keys: One or more keys from model.PoseEstimation
36+
use_element_session: Optional. If True, call NWB export from Element Session
37+
session_kwargs: Optional. Additional keyword arguments for Element Session export
38+
39+
Returns output path of saved file
40+
"""
41+
if not isinstance(keys, abc.Sequence): # Ensure list for following loop
42+
keys = [keys]
43+
44+
for key in keys:
45+
write_file = True
46+
subject_id = key["subject"]
47+
output_dir = model.PoseEstimationTask.infer_output_dir(key)
48+
config_file = str(output_dir / "dj_dlc_config.yaml")
49+
video_name = Path((model.VideoRecording.File & key).fetch1("file_path")).stem
50+
h5file = next(output_dir.glob(f"{video_name}*h5"))
51+
output_path = h5file.replace(".h5", f"_{subject_id}.nwb") # DLC2NWB convention
52+
53+
if Path(output_path).exists():
54+
logger.warning(f"Skipping {subject_id}. NWB already exists.")
55+
write_file = False
56+
57+
# Use standard DLC2NWB export
58+
if write_file and not use_element_session:
59+
output_path = convert_h5_to_nwb(config_file, h5file, subject_id)
60+
61+
# Pass Element Session export items in export
62+
if write_file and use_element_session:
63+
from element_session.export.nwb import session_to_nwb
64+
65+
session_nwb = session_to_nwb(key, **session_kwargs) # call session export
66+
dlc_nwb = write_subject_to_nwb(session_nwb, h5file, subject_id, config_file)
67+
# warnings filter from DLC2NWB
68+
with warnings.catch_warnings(), NWBHDF5IO(output_path, mode="w") as io:
69+
warnings.filterwarnings("ignore", category=DtypeConversionWarning)
70+
io.write(dlc_nwb)
71+
72+
return output_path

element_deeplabcut/readers/dlc_reader.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import re
2+
import logging
23
import numpy as np
34
import pandas as pd
45
from pathlib import Path
56
import pickle
67
import ruamel.yaml as yaml
8+
from element_interface.utils import find_root_directory
9+
from .. import model
10+
from ..model import get_dlc_root_data_dir
11+
from datajoint.errors import DataJointError
12+
13+
logger = logging.getLogger("datajoint")
714

815

916
class PoseEstimation:
@@ -215,7 +222,11 @@ def do_pose_estimation(
215222
use_shelve=False,
216223
modelprefix="", # need from paramset
217224
):
218-
"""Launch DLC's analyze_videos within element-deeplabcut
225+
"""Launch DLC's analyze_videos within element-deeplabcut.
226+
227+
Also saves a copy of the current config in the output dir, with ensuring analyzed
228+
videos in the video_set. NOTE: Config-specificed cropping not supported when adding
229+
to config in this manner.
219230
220231
Parameters
221232
----------
@@ -232,6 +243,28 @@ def do_pose_estimation(
232243
dlc_project_path = Path(project_path)
233244
dlc_config["project_path"] = dlc_project_path.as_posix()
234245

246+
# ---- Add current video to config ---
247+
for video_filepath in video_filepaths:
248+
if video_filepath not in dlc_config["video_sets"]:
249+
root_dir = find_root_directory(get_dlc_root_data_dir(), video_filepath)
250+
relative_path = Path(video_filepath).relative_to(root_dir)
251+
recording_id = (
252+
model.VideoRecording.File & f'file_path="{relative_path}"'
253+
).fetch1("recording_id")
254+
try:
255+
px_width, px_height = (
256+
model.RecordingInfo & f'recording_id="{recording_id}"'
257+
).fetch1("px_width", "px_height")
258+
except DataJointError:
259+
logger.warn(
260+
f"Could not find RecordingInfo for {video_filepath.stem}"
261+
+ "\n\tUsing zeros for crop value in config."
262+
)
263+
px_height, px_width = 0, 0
264+
dlc_config["video_sets"].update(
265+
{str(video_filepath): {"crop": f"0, {px_width}, 0, {px_height}"}}
266+
)
267+
235268
# ---- Write config files ----
236269
# To output dir: Important for loading/parsing output in datajoint
237270
_ = save_yaml(output_dir, dlc_config)

0 commit comments

Comments
 (0)