Skip to content

Commit 399e38e

Browse files
authored
Merge pull request #923 from datajoint/nd2support
nd2 support
2 parents d007f4f + 74b96b8 commit 399e38e

File tree

6 files changed

+167
-1
lines changed

6 files changed

+167
-1
lines changed

docs/inputs.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,14 @@ Scanbox binary files (*.sbx) work out of the box if you set ``ops['input_format'
148148
When recording in bidirectional mode some columns might have every other line saturated; to trim these during loading set ``ops['sbx_ndeadcols']``. Set this option to ``-1`` to let suite2p compute the number of columns automatically, a positive integer to specify the number of columns to trim.
149149
Joao Couto (@jcouto) wrote the binary sbx parser.
150150

151+
152+
Nikon nd2 files
153+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
154+
155+
Suite2p reads nd2 files using the nd2 package and returns a numpy array representing the data with a minimum of two dimensions (Height, Width). The data can also have additional dimensions for Time, Depth, and Channel. If any dimensions are missing, Suite2p adds them in the order of Time, Depth, Channel, Height, and Width, resulting in a 5-dimensional array. To use Suite2p with nd2 files, simply set ``ops['input_format'] = "nd2".``
156+
157+
158+
151159
BinaryRWFile
152160
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
153161

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
'h5py',
1515
'sbxreader',
1616
'scikit-learn',
17-
'cellpose']
17+
'cellpose',
18+
'nd2']
1819

1920
gui_deps = [
2021
"pyqt5",

suite2p/io/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
from .save import combined, compute_dydx, save_mat
44
from .sbx import sbx_to_binary
55
from .tiff import mesoscan_to_binary, ome_to_binary, tiff_to_binary, generate_tiff_filename, save_tiff
6+
from .nd2 import nd2_to_binary
67
from .binary import BinaryFile, BinaryRWFile, BinaryFileCombined
78
from .server import send_jobs

suite2p/io/nd2.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import os
2+
import gc
3+
import math
4+
import time
5+
import numpy as np
6+
from . import utils
7+
import nd2
8+
9+
10+
def nd2_to_binary(ops):
11+
"""finds nd2 files and writes them to binaries
12+
13+
Parameters
14+
----------
15+
ops: dictionary
16+
'nplanes', 'data_path', 'save_path', 'save_folder', 'fast_disk',
17+
'nchannels', 'keep_movie_raw', 'look_one_level_down'
18+
19+
Returns
20+
-------
21+
ops : dictionary of first plane
22+
ops['reg_file'] or ops['raw_file'] is created binary
23+
assigns keys 'Ly', 'Lx', 'tiffreader', 'first_tiffs',
24+
'nframes', 'meanImg', 'meanImg_chan2'
25+
"""
26+
27+
t0 = time.time()
28+
# copy ops to list where each element is ops for each plane
29+
ops1 = utils.init_ops(ops)
30+
31+
# open all binary files for writing
32+
# look for nd2s in all requested folders
33+
ops1, fs, reg_file, reg_file_chan2 = utils.find_files_open_binaries(ops1, False)
34+
ops = ops1[0]
35+
36+
# loop over all nd2 files
37+
iall = 0
38+
ik = 0
39+
for file_name in fs:
40+
# open nd2
41+
nd2_file = nd2.ND2File(file_name)
42+
nd2_dims = {k: i for i, k in enumerate(nd2_file.sizes)}
43+
44+
valid_dimensions = "TZCYX"
45+
assert set(nd2_dims) <= set(
46+
valid_dimensions
47+
), f"Unknown dimensions {set(nd2_dims)-set(valid_dimensions)} in file {file_name}."
48+
49+
# Sort the dimensions in the order of TZCYX, skipping the missing ones.
50+
im = nd2_file.asarray().transpose(
51+
[nd2_dims[x] for x in valid_dimensions if x in nd2_dims]
52+
)
53+
54+
# Expand array to include the missing dimensions.
55+
for i, dim in enumerate("TZC"):
56+
if dim not in nd2_dims:
57+
im = np.expand_dims(im, i)
58+
59+
nplanes = nd2_file.sizes["Z"] if "Z" in nd2_file.sizes else 1
60+
nchannels = nd2_file.sizes["C"] if "C" in nd2_file.sizes else 1
61+
nframes = nd2_file.sizes["T"] if "T" in nd2_file.sizes else 1
62+
63+
iblocks = np.arange(0, nframes, ops1[0]["batch_size"])
64+
if iblocks[-1] < nframes:
65+
iblocks = np.append(iblocks, nframes)
66+
67+
if nchannels > 1:
68+
nfunc = ops1[0]["functional_chan"] - 1
69+
else:
70+
nfunc = 0
71+
72+
assert im.max() < 32768 and im.min() >= -32768, "image data is out of range"
73+
im = im.astype(np.int16)
74+
75+
# loop over all frames
76+
for ichunk, onset in enumerate(iblocks[:-1]):
77+
offset = iblocks[ichunk + 1]
78+
im_p = np.array(im[onset:offset, :, :, :, :])
79+
im2mean = im_p.mean(axis=0).astype(np.float32) / len(iblocks)
80+
for ichan in range(nchannels):
81+
nframes = im_p.shape[0]
82+
im2write = im_p[:, :, ichan, :, :]
83+
for j in range(0, nplanes):
84+
if iall == 0:
85+
ops1[j]["meanImg"] = np.zeros(
86+
(im_p.shape[3], im_p.shape[4]), np.float32
87+
)
88+
if nchannels > 1:
89+
ops1[j]["meanImg_chan2"] = np.zeros(
90+
(im_p.shape[3], im_p.shape[4]), np.float32
91+
)
92+
ops1[j]["nframes"] = 0
93+
if ichan == nfunc:
94+
ops1[j]["meanImg"] += np.squeeze(im2mean[j, ichan, :, :])
95+
reg_file[j].write(
96+
bytearray(im2write[:, j, :, :].astype("int16"))
97+
)
98+
else:
99+
ops1[j]["meanImg_chan2"] += np.squeeze(im2mean[j, ichan, :, :])
100+
reg_file_chan2[j].write(
101+
bytearray(im2write[:, j, :, :].astype("int16"))
102+
)
103+
104+
ops1[j]["nframes"] += im2write.shape[0]
105+
ik += nframes
106+
iall += nframes
107+
108+
nd2_file.close()
109+
110+
# write ops files
111+
do_registration = ops1[0]["do_registration"]
112+
for ops in ops1:
113+
ops["Ly"] = im.shape[3]
114+
ops["Lx"] = im.shape[4]
115+
if not do_registration:
116+
ops["yrange"] = np.array([0, ops["Ly"]])
117+
ops["xrange"] = np.array([0, ops["Lx"]])
118+
np.save(ops["ops_path"], ops)
119+
# close all binary files and write ops files
120+
for j in range(0, nplanes):
121+
reg_file[j].close()
122+
if nchannels > 1:
123+
reg_file_chan2[j].close()
124+
return ops1[0]

suite2p/io/utils.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,30 @@ def get_tif_list(ops):
161161
print('** Found %d tifs - converting to binary **'%(len(fsall)))
162162
return fsall, ops
163163

164+
165+
def get_nd2_list(ops):
166+
""" make list of nd2 files to process
167+
if ops['look_one_level_down'], then all nd2's in all folders + one level down
168+
"""
169+
froot = ops['data_path']
170+
fold_list = ops['data_path']
171+
fsall = []
172+
nfs = 0
173+
first_tiffs = []
174+
for k,fld in enumerate(fold_list):
175+
fs, ftiffs = list_files(fld, ops['look_one_level_down'],
176+
["*.nd2"])
177+
fsall.extend(fs)
178+
first_tiffs.extend(list(ftiffs))
179+
if len(fs)==0:
180+
print('Could not find any nd2 files')
181+
raise Exception('no nd2s')
182+
else:
183+
ops['first_tiffs'] = np.array(first_tiffs).astype('bool')
184+
print('** Found %d nd2 files - converting to binary **'%(len(fsall)))
185+
return fsall, ops
186+
187+
164188
def find_files_open_binaries(ops1, ish5=False):
165189
""" finds tiffs or h5 files and opens binaries for writing
166190
@@ -216,6 +240,11 @@ def find_files_open_binaries(ops1, ish5=False):
216240
fs, ops2 = get_sbx_list(ops1[0])
217241
print('Scanbox files:')
218242
print('\n'.join(fs))
243+
elif input_format == 'nd2':
244+
# find nd2s
245+
fs, ops2 = get_nd2_list(ops1[0])
246+
print('Nikon files:')
247+
print('\n'.join(fs))
219248
else:
220249
# find tiffs
221250
fs, ops2 = get_tif_list(ops1[0])

suite2p/run_s2p.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,8 @@ def run_s2p(ops={}, db={}, server={}):
385385
ops['input_format'] = 'nwb'
386386
elif ops.get('mesoscan'):
387387
ops['input_format'] = 'mesoscan'
388+
elif ops.get('nd2'):
389+
ops['input_format'] = 'nd2'
388390
elif HAS_HAUS:
389391
ops['input_format'] = 'haus'
390392
elif not 'input_format' in ops:
@@ -396,6 +398,7 @@ def run_s2p(ops={}, db={}, server={}):
396398
'h5': io.h5py_to_binary,
397399
'nwb': io.nwb_to_binary,
398400
'sbx': io.sbx_to_binary,
401+
'nd2': io.nd2_to_binary,
399402
'mesoscan': io.mesoscan_to_binary,
400403
'haus': lambda ops: haussio.load_haussio(ops['data_path'][0]).tosuite2p(ops.copy()),
401404
'bruker': io.ome_to_binary,

0 commit comments

Comments
 (0)