Skip to content

Commit 0863475

Browse files
author
Thinh Nguyen
authored
Merge pull request #156 from kushalbakshi/main
Export from DataJoint Element to NWB
2 parents e2dd742 + cbf572c commit 0863475

File tree

10 files changed

+360
-5
lines changed

10 files changed

+360
-5
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
Observes [Semantic Versioning](https://semver.org/spec/v2.0.0.html) standard and
44
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/) convention.
55

6+
## [0.9.0] - 2023-10-13
7+
8+
+ Add - Export to NWB and upload to DANDI
9+
610
## [0.8.1] - 2023-08-31
711

812
+ Fix - Rename `get_image_files` to `get_calcium_imaging_files` where missed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Contribution Guidelines
22

33
This project follows the
4-
[DataJoint Contribution Guidelines](https://datajoint.com/docs/community/contribute/).
4+
[DataJoint Contribution Guidelines](https://datajoint.com/docs/about/contribute/).
55
Please reference the link for more full details.

docs/src/concepts.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,18 @@ segmented.
6060
- Scanbox
6161
- Nikon NIS-Elements
6262
- Bruker Prairie View
63+
64+
## Data Export and Publishing
65+
66+
Element Calcium Imaging supports exporting of all data into standard Neurodata
67+
Without Borders (NWB) files. This makes it easy to share files with collaborators and
68+
publish results on [DANDI Archive](https://dandiarchive.org/).
69+
[NWB](https://www.nwb.org/), as an organization, is dedicated to standardizing data
70+
formats and maximizing interoperability across tools for neurophysiology.
71+
72+
To use the export functionality with additional related dependencies, install the
73+
Element with the `nwb` option as follows:
74+
75+
```console
76+
pip install element-calcium-imaging[nwb]
77+
```

docs/src/roadmap.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ the common motifs to create Element Calcium Imaging. Major features include:
1616
- [x] Quality metrics
1717
- [ ] Data compression
1818
- [ ] Deepinterpolation
19-
- [ ] Data export to NWB
20-
- [ ] Data publishing to DANDI
19+
- [x] Data export to NWB
20+
- [x] Data publishing to DANDI
2121

2222
Further development of this Element is community driven. Upon user requests and based on
2323
guidance from the Scientific Steering Group we will continue adding features to this

element_calcium_imaging/export/__init__.py

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .nwb import imaging_session_to_nwb, write_nwb
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
import numpy as np
2+
import datajoint as dj
3+
from datajoint import DataJointError
4+
from pynwb import NWBHDF5IO, NWBFile
5+
from pynwb.ophys import (
6+
Fluorescence,
7+
ImageSegmentation,
8+
OpticalChannel,
9+
RoiResponseSeries,
10+
TwoPhotonSeries,
11+
)
12+
13+
from ... import scan, imaging_no_curation
14+
from ...scan import get_calcium_imaging_files, get_imaging_root_data_dir
15+
16+
17+
logger = dj.logger
18+
19+
if imaging_no_curation.schema.is_activated():
20+
imaging = imaging_no_curation
21+
else:
22+
raise DataJointError(
23+
"This export function is designed for the `imaging_no_curation` module."
24+
)
25+
26+
27+
def imaging_session_to_nwb(
28+
session_key,
29+
include_raw_data=False,
30+
lab_key=None,
31+
project_key=None,
32+
protocol_key=None,
33+
nwbfile_kwargs=None,
34+
):
35+
"""Main function for converting calcium imaging data to NWB.
36+
37+
Args:
38+
session_key (dict): key from Session table.
39+
include_raw_data (bool): Optional. Default False. Include the raw data from
40+
source. `ScanImage`, `Scanbox`, and `PrairieView` are supported.
41+
lab_key (dict): Optional key to add metadata from Element Lab.
42+
project_key (dict): Optional key to add metadata from Element Lab.
43+
protocol_key (dict): Optional key to add metadata from Element Lab.
44+
nwbfile_kwargs (dict): Optional. If Element Session is not used, this argument
45+
is required and must be a dictionary containing 'session_description' (str),
46+
'identifier' (str), and 'session_start_time' (datetime), the required
47+
minimal data for instantiating an NWBFile object. If element-session is
48+
being used, this argument can optionally be used to overwrite NWBFile
49+
fields.
50+
Returns:
51+
nwbfile (NWBFile): nwb file
52+
"""
53+
54+
session_to_nwb = getattr(imaging._linking_module, "session_to_nwb", False)
55+
56+
if session_to_nwb:
57+
nwb_file = session_to_nwb(
58+
session_key,
59+
lab_key=lab_key,
60+
project_key=project_key,
61+
protocol_key=protocol_key,
62+
additional_nwbfile_kwargs=nwbfile_kwargs,
63+
)
64+
else:
65+
nwb_file = NWBFile(**nwbfile_kwargs)
66+
67+
if include_raw_data:
68+
_create_raw_data_nwbfile(session_key, linked_nwb_file=nwb_file)
69+
if not nwb_file.imaging_planes:
70+
_add_scan_to_nwb(session_key, nwbfile=nwb_file)
71+
72+
else:
73+
_add_scan_to_nwb(session_key, nwbfile=nwb_file)
74+
_add_image_series_to_nwb(
75+
session_key, imaging_plane=nwb_file.imaging_planes["ImagingPlane"]
76+
)
77+
_add_segmentation_data_to_nwb(
78+
session_key,
79+
nwbfile=nwb_file,
80+
imaging_plane=nwb_file.imaging_planes["ImagingPlane"],
81+
)
82+
83+
return nwb_file
84+
85+
86+
def _create_raw_data_nwbfile(session_key, linked_nwb_file):
87+
"""Adds raw data to NWB file.
88+
89+
Args:
90+
session_key (dict): key from Session table
91+
linked_nwb_file (NWBFile): nwb file
92+
"""
93+
94+
acquisition_software = (scan.Scan & session_key).fetch1("acq_software")
95+
frame_rate = (scan.ScanInfo & session_key).fetch1("fps")
96+
97+
if acquisition_software == "NIS":
98+
raise NotImplementedError(
99+
"Packaging raw data acquired from `Nikon NIS Elements` software is not supported at this time."
100+
)
101+
102+
elif acquisition_software == "PrairieView":
103+
n_planes = (scan.ScanInfo & session_key).fetch1("ndepths")
104+
raw_data_files_location = get_calcium_imaging_files(
105+
session_key, acquisition_software
106+
)
107+
108+
if n_planes > 1:
109+
from neuroconv.converters import (
110+
BrukerTiffMultiPlaneConverter as BrukerTiffConverter,
111+
)
112+
113+
imaging_interface = BrukerTiffConverter(
114+
file_path=raw_data_files_location[0],
115+
fallback_sampling_frequency=frame_rate,
116+
plane_separation_type="disjoint",
117+
)
118+
else:
119+
from neuroconv.converters import (
120+
BrukerTiffSinglePlaneConverter as BrukerTiffConverter,
121+
)
122+
123+
imaging_interface = BrukerTiffConverter(
124+
file_path=raw_data_files_location[0],
125+
fallback_sampling_frequency=frame_rate,
126+
)
127+
metadata = imaging_interface.get_metadata()
128+
imaging_interface.add_to_nwbfile(
129+
nwbfile=linked_nwb_file,
130+
metadata=metadata,
131+
)
132+
else:
133+
if acquisition_software == "ScanImage":
134+
from neuroconv.datainterfaces import (
135+
ScanImageImagingInterface as ImagingInterface,
136+
)
137+
elif acquisition_software == "Scanbox":
138+
from neuroconv.datainterfaces import SbxImagingInterface as ImagingInterface
139+
140+
raw_data_files_location = get_calcium_imaging_files(
141+
session_key, acquisition_software
142+
)
143+
144+
imaging_interface = ImagingInterface(
145+
file_path=raw_data_files_location[0], fallback_sampling_frequency=frame_rate
146+
)
147+
metadata = imaging_interface.get_metadata()
148+
imaging_interface.add_to_nwbfile(
149+
nwbfile=linked_nwb_file,
150+
metadata=metadata,
151+
)
152+
153+
154+
def _add_scan_to_nwb(session_key, nwbfile):
155+
"""Adds metadata for a scan from database.
156+
157+
Args:
158+
session_key (dict): key from Session table
159+
nwbfile (NWBFile): nwb file
160+
"""
161+
162+
from math import nan
163+
164+
try:
165+
scan_key = (scan.Scan & session_key).fetch1("KEY")
166+
except DataJointError:
167+
raise NotImplementedError(
168+
"Exporting more than one scan per session to NWB is not supported yet."
169+
)
170+
171+
scanner_name, scan_notes = (scan.Scan & scan_key).fetch1("scanner", "scan_notes")
172+
device = nwbfile.create_device(
173+
name=scanner_name if scanner_name is not None else "TwoPhotonMicroscope",
174+
description="Two photon microscope",
175+
manufacturer="Microscope manufacturer",
176+
)
177+
178+
no_channels, frame_rate = (scan.ScanInfo & scan_key).fetch1("nchannels", "fps")
179+
180+
field_keys = (scan.ScanInfo.Field & scan_key).fetch("KEY")
181+
182+
for channel in range(no_channels):
183+
optical_channel = OpticalChannel(
184+
name=f"OpticalChannel{channel+1}",
185+
description=f"Optical channel number {channel+1}",
186+
emission_lambda=nan,
187+
)
188+
189+
for field_key in field_keys:
190+
field_no = (scan.ScanInfo.Field & field_key).fetch1("field_idx")
191+
imaging_plane = nwbfile.create_imaging_plane(
192+
name="ImagingPlane",
193+
optical_channel=optical_channel,
194+
imaging_rate=frame_rate,
195+
description=scan_notes
196+
if scan_notes != ""
197+
else f"Imaging plane for field {field_no+1}, channel {channel+1}",
198+
device=device,
199+
excitation_lambda=nan,
200+
indicator="unknown",
201+
location="unknown",
202+
grid_spacing=(0.01, 0.01),
203+
grid_spacing_unit="meters",
204+
origin_coords=[1.0, 2.0, 3.0],
205+
origin_coords_unit="meters",
206+
)
207+
return imaging_plane
208+
209+
210+
def _add_image_series_to_nwb(session_key, imaging_plane):
211+
"""Adds TwoPhotonSeries to NWB file.
212+
213+
Args:
214+
session_key (dict): key from Session table
215+
imaging_plane (NWBFile Imaging Plane): nwb file imaging plane object
216+
"""
217+
218+
imaging_files = (scan.ScanInfo.ScanFile & session_key).fetch("file_path")
219+
two_p_series = TwoPhotonSeries(
220+
name="TwoPhotonSeries",
221+
dimension=(scan.ScanInfo.Field & session_key).fetch1("px_height", "px_width"),
222+
external_file=imaging_files,
223+
imaging_plane=imaging_plane,
224+
starting_frame=[0],
225+
format="external",
226+
starting_time=0.0,
227+
rate=(scan.ScanInfo & session_key).fetch1("fps"),
228+
)
229+
return two_p_series
230+
231+
232+
def _add_motion_correction_to_nwb(session_key, nwbfile):
233+
raise NotImplementedError(
234+
"Motion Correction data cannot be packaged into NWB at this time."
235+
)
236+
237+
238+
def _add_segmentation_data_to_nwb(session_key, nwbfile, imaging_plane):
239+
"""Adds segmentation data from database.
240+
241+
Args:
242+
session_key (dict): key from Session table
243+
nwbfile (NWBFile): nwb file
244+
imaging_plane (NWBFile Imaging Plane): nwb file imaging plane object
245+
"""
246+
247+
ophys_module = nwbfile.create_processing_module(
248+
name="ophys", description="optical physiology processed data"
249+
)
250+
img_seg = ImageSegmentation()
251+
ps = img_seg.create_plane_segmentation(
252+
name="PlaneSegmentation",
253+
description="output from segmenting",
254+
imaging_plane=imaging_plane,
255+
)
256+
ophys_module.add(img_seg)
257+
258+
mask_keys = (imaging.Segmentation.Mask & session_key).fetch("KEY")
259+
for mask_key in mask_keys:
260+
ps.add_roi(
261+
pixel_mask=np.asarray(
262+
(imaging.Segmentation.Mask() & mask_key).fetch1(
263+
"mask_xpix", "mask_ypix", "mask_weights"
264+
)
265+
).T
266+
)
267+
268+
rt_region = ps.create_roi_table_region(
269+
region=((imaging.Segmentation.Mask & session_key).fetch("mask")).tolist(),
270+
description="All ROIs from database.",
271+
)
272+
273+
channels = (scan.ScanInfo & session_key).fetch1("nchannels")
274+
for channel in range(channels):
275+
roi_resp_series = RoiResponseSeries(
276+
name=f"Fluorescence_{channel}",
277+
data=np.stack(
278+
(
279+
imaging.Fluorescence.Trace
280+
& session_key
281+
& f"fluo_channel='{channel}'"
282+
).fetch("fluorescence")
283+
).T,
284+
rois=rt_region,
285+
unit="a.u.",
286+
rate=(scan.ScanInfo & session_key).fetch1("fps"),
287+
)
288+
neuropil_series = RoiResponseSeries(
289+
name=f"Neuropil_{channel}",
290+
data=np.stack(
291+
(
292+
imaging.Fluorescence.Trace
293+
& session_key
294+
& f"fluo_channel='{channel}'"
295+
).fetch("neuropil_fluorescence")
296+
).T,
297+
rois=rt_region,
298+
unit="a.u.",
299+
rate=(scan.ScanInfo & session_key).fetch1("fps"),
300+
)
301+
deconvolved_series = RoiResponseSeries(
302+
name=f"Deconvolved_{channel}",
303+
data=np.stack(
304+
(
305+
imaging.Activity.Trace & session_key & f"fluo_channel='{channel}'"
306+
).fetch("activity_trace")
307+
).T,
308+
rois=rt_region,
309+
unit="a.u.",
310+
rate=(scan.ScanInfo & session_key).fetch1("fps"),
311+
)
312+
fl = Fluorescence(
313+
roi_response_series=[roi_resp_series, neuropil_series, deconvolved_series]
314+
)
315+
ophys_module.add(fl)
316+
317+
318+
def write_nwb(nwbfile, fname, check_read=True):
319+
"""Export NWBFile
320+
321+
Args:
322+
nwbfile (NWBFile): nwb file
323+
fname (str): Absolute path including `*.nwb` extension.
324+
check_read (bool): If True, PyNWB will try to read the produced NWB file and
325+
ensure that it can be read.
326+
"""
327+
with NWBHDF5IO(fname, "w") as io:
328+
io.write(nwbfile)
329+
330+
if check_read:
331+
with NWBHDF5IO(fname, "r") as io:
332+
io.read()
333+
logger.info("File saved successfully")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
dandi
2+
pynwb
3+
neuroconv[scanimage, brukertiff, scanbox, caiman, suite2p, extract]

element_calcium_imaging/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""Package metadata."""
2-
__version__ = "0.8.1"
2+
__version__ = "0.9.0"

tests/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,3 @@ def test_data(pipeline):
7676
_ = [find_full_path(root_dirs(), p) for p in sessions_dirs]
7777
except FileNotFoundError as e:
7878
print(e)
79-

0 commit comments

Comments
 (0)