Skip to content

Commit 5155bd0

Browse files
authored
Merge branch 'master' into neuralynx
2 parents 41c2b19 + be8e663 commit 5155bd0

File tree

8 files changed

+151
-87
lines changed

8 files changed

+151
-87
lines changed

neo/io/tiffio.py

Lines changed: 70 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,53 @@ class TiffIO(BaseIO):
1515
"""
1616
Neo IO module for optical imaging data stored as a folder of TIFF images.
1717
18-
*Usage*:
19-
>>> from neo import io
20-
>>> import quantities as pq
21-
>>> r = io.TiffIO("dir_tiff",spatial_scale=1.0*pq.mm, units='V',
22-
... sampling_rate=1.0*pq.Hz)
23-
>>> block = r.read_block()
24-
read block
25-
creating segment
26-
returning block
27-
>>> block
28-
Block with 1 segments
29-
file_origin: 'test'
30-
# segments (N=1)
31-
0: Segment with 1 imagesequences
32-
annotations: {'tiff_file_names': ['file_tif_1_.tiff',
33-
'file_tif_2.tiff',
34-
'file_tif_3.tiff',
35-
'file_tif_4.tiff',
36-
'file_tif_5.tiff',
37-
'file_tif_6.tiff',
38-
'file_tif_7.tiff',
39-
'file_tif_8.tiff',
40-
'file_tif_9.tiff',
41-
'file_tif_10.tiff',
42-
'file_tif_11.tiff',
43-
'file_tif_12.tiff',
44-
'file_tif_13.tiff',
45-
'file_tif_14.tiff']}
46-
# analogsignals (N=0)
18+
Parameters
19+
----------
20+
directory_path: Path | str | None, default: None
21+
The path to the folder containing tiff images
22+
units: Quantity units | None, default: None
23+
the units for creating the ImageSequence
24+
sampling_rate: Quantity Units | None, default: None
25+
The sampling rate
26+
spatial_scale: Quantity unit | None, default: None
27+
The scale of the images
28+
origin: Literal['top-left'| 'bottom-left'], default: 'top-left'
29+
Whether to use the python default origin for images which is upper left corner ('top-left')
30+
as orgin or to use a bottom left corner as orgin ('bottom-left')
31+
Note that plotting functions like matplotlib.pyplot.imshow expect upper left corner.
32+
**kwargs: dict
33+
The standard neo annotation kwargs
34+
35+
Examples
36+
--------
37+
>>> from neo import io
38+
>>> import quantities as pq
39+
>>> r = io.TiffIO("dir_tiff",spatial_scale=1.0*pq.mm, units='V',
40+
... sampling_rate=1.0*pq.Hz)
41+
>>> block = r.read_block()
42+
read block
43+
creating segment
44+
returning block
45+
>>> block
46+
Block with 1 segments
47+
file_origin: 'test'
48+
# segments (N=1)
49+
0: Segment with 1 imagesequences
50+
annotations: {'tiff_file_names': ['file_tif_1_.tiff',
51+
'file_tif_2.tiff',
52+
'file_tif_3.tiff',
53+
'file_tif_4.tiff',
54+
'file_tif_5.tiff',
55+
'file_tif_6.tiff',
56+
'file_tif_7.tiff',
57+
'file_tif_8.tiff',
58+
'file_tif_9.tiff',
59+
'file_tif_10.tiff',
60+
'file_tif_11.tiff',
61+
'file_tif_12.tiff',
62+
'file_tif_13.tiff',
63+
'file_tif_14.tiff']}
64+
# analogsignals (N=0)
4765
"""
4866

4967
name = "TIFF IO"
@@ -66,13 +84,30 @@ class TiffIO(BaseIO):
6684

6785
mode = "dir"
6886

69-
def __init__(self, directory_path=None, units=None, sampling_rate=None, spatial_scale=None, **kwargs):
70-
import PIL
87+
def __init__(
88+
self,
89+
directory_path=None,
90+
units=None,
91+
sampling_rate=None,
92+
spatial_scale=None,
93+
origin="top-left",
94+
**kwargs,
95+
):
96+
# this block is because people might be confused about the PIL -> pillow change
97+
# between python2 -> python3 (both with namespace PIL)
98+
try:
99+
import PIL
100+
except ImportError:
101+
raise ImportError("To use TiffIO you must first `pip install pillow`")
102+
103+
if origin != "top-left" and origin != "bottom-left":
104+
raise ValueError("`origin` must be either `top-left` or `bottom-left`")
71105

72106
BaseIO.__init__(self, directory_path, **kwargs)
73107
self.units = units
74108
self.sampling_rate = sampling_rate
75109
self.spatial_scale = spatial_scale
110+
self.origin = origin
76111

77112
def read_block(self, lazy=False, **kwargs):
78113
import PIL
@@ -98,13 +133,17 @@ def natural_sort(l):
98133
list_data_image = []
99134
for file_name in file_name_list:
100135
data = np.array(PIL.Image.open(self.filename + "/" + file_name)).astype(np.float32)
136+
if self.origin == "bottom-left":
137+
data = np.flip(data, axis=-2)
101138
list_data_image.append(data)
102139
list_data_image = np.array(list_data_image)
103140
if len(list_data_image.shape) == 4:
104141
list_data_image = []
105142
for file_name in file_name_list:
106143
image = PIL.Image.open(self.filename + "/" + file_name).convert("L")
107144
data = np.array(image).astype(np.float32)
145+
if self.origin == "bottom-left":
146+
data = np.flip(data, axis=-2)
108147
list_data_image.append(data)
109148

110149
print("read block")

neo/rawio/intanrawio.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ def _demultiplex_digital_data(self, raw_digital_data, channel_ids, i_start, i_st
566566
set high and the rest low, the 16-bit word would be 2^0 + 2^4 + 2^5 = 1 + 16 + 32 = 49.
567567
568568
The native_order property for each channel corresponds to its bit position in the packed word.
569-
569+
570570
"""
571571
dtype = np.uint16 # We fix this to match the memmap dtype
572572
output = np.zeros((i_stop - i_start, len(channel_ids)), dtype=dtype)
@@ -902,7 +902,7 @@ def read_rhs(filename, file_format: str):
902902

903903
# Each DC amplifier channel has a corresponding RHS2000 amplifier channel
904904
channel_number_dict["DC Amplifier channel"] = channel_number_dict["RHS2000 amplifier channel"]
905-
905+
906906
if file_format == "one-file-per-channel":
907907
# There is a way to shut off saving amplifier data and only keeping the DC amplifier or shutting off all amplifier file saving,
908908
# so we need to count the number of files we find instead of relying on the header.
@@ -913,7 +913,6 @@ def read_rhs(filename, file_format: str):
913913
else:
914914
channel_number_dict["Stim channel"] = channel_number_dict["RHS2000 amplifier channel"]
915915

916-
917916
header_size = f.tell()
918917

919918
sr = global_info["sampling_rate"]

neo/test/iotest/test_intanio.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ class TestIntanIO(
2323
"intan/intan_fpc_rhs_test_240329_091637/info.rhs", # Format one-file-per-channel
2424
"intan/intan_fps_rhs_test_240329_091536/info.rhs", # Format one-file-per-signal
2525
"intan/rhd_fpc_multistim_240514_082044/info.rhd", # Multiple digital channels one-file-per-channel rhd
26-
"intan/rhs_stim_data_single_file_format/intanTestFile.rhs", # header-attached rhs data with stimulus current
27-
"intan/test_fcs_dc_250327_154333/info.rhs", # this is an example of only having dc amp rather than amp files
28-
#"intan/test_fpc_stim_250327_151617/info.rhs", # wrong files Heberto will fix
26+
"intan/rhs_stim_data_single_file_format/intanTestFile.rhs", # header-attached rhs data with stimulus current
27+
"intan/test_fcs_dc_250327_154333/info.rhs", # this is an example of only having dc amp rather than amp files
28+
# "intan/test_fpc_stim_250327_151617/info.rhs", # wrong files Heberto will fix
2929
]
3030

3131

neo/test/iotest/test_tiffio.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ def test_read_group_of_tiff_grayscale(self):
1717
img = []
1818
for picture in range(10):
1919
img.append([])
20-
for y in range(50):
20+
for x in range(50):
2121
img[picture].append([])
22-
for x in range(50):
23-
img[picture][y].append(x)
22+
for y in range(50):
23+
img[picture][x].append(y)
2424
img = np.array(img, dtype=float)
2525
for image in range(10):
26-
Image.fromarray(img[image]).save(directory + "/tiff_exemple" + str(image) + ".tif")
26+
# rotate image by 90 deg so that shifting the origin is meaningful in later test
27+
Image.fromarray(np.rot90(img[image])).save(directory + "/tiff_exemple" + str(image) + ".tif")
2728

2829
ioclass = TiffIO(
2930
directory_path=directory, units="V", sampling_rate=1.0 * pq.Hz, spatial_scale=1.0 * pq.micrometer
@@ -35,6 +36,21 @@ def test_read_group_of_tiff_grayscale(self):
3536
self.assertEqual(blck.segments[0].imagesequences[0].sampling_rate, 1.0 * pq.Hz)
3637
self.assertEqual(blck.segments[0].imagesequences[0].spatial_scale, 1.0 * pq.micrometer)
3738

39+
ioclass_bl_origin = TiffIO(
40+
directory_path=directory,
41+
units="V",
42+
sampling_rate=1.0 * pq.Hz,
43+
spatial_scale=1.0 * pq.micrometer,
44+
origin="bottom-left",
45+
)
46+
blck_bl_origin = ioclass_bl_origin.read_block()
47+
48+
self.assertAlmostEqual(
49+
blck.segments[0].imagesequences[0][0][0, 0].magnitude,
50+
blck_bl_origin.segments[0].imagesequences[0][0][49, 0].magnitude, # since flipped over y, [0,0] == [49,0]
51+
places=3,
52+
)
53+
3854
# end of directory
3955
shutil.rmtree(directory)
4056

neo/test/rawiotest/common_rawio_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@
3434
HAVE_DATALAD = True
3535
else:
3636
HAVE_DATALAD = False
37-
# pytest skip doesn't explain why we are skipping.
37+
# pytest skip doesn't explain why we are skipping.
3838
# raise error if in CI to prevent tests from spuriously skipping and appearing
3939
# as passing.
40-
if os.environ.get("GITHUB_ACTIONS") == 'true':
40+
if os.environ.get("GITHUB_ACTIONS") == "true":
4141
raise RuntimeError("Datalad is required for running the CI.")
42-
42+
4343

4444
# url_for_tests = "https://portal.g-node.org/neo/" #This is the old place
4545
repo_for_test = default_testing_repo

neo/test/rawiotest/test_intanrawio.py

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@ class TestIntanRawIO(
2121
"intan/intan_fpc_rhs_test_240329_091637/info.rhs", # Format one-file-per-channel
2222
"intan/intan_fps_rhs_test_240329_091536/info.rhs", # Format one-file-per-signal
2323
"intan/rhd_fpc_multistim_240514_082044/info.rhd", # Multiple digital channels one-file-per-channel rhd
24-
"intan/rhs_stim_data_single_file_format/intanTestFile.rhs", # header-attached rhs data with stimulus current
25-
"intan/test_fcs_dc_250327_154333/info.rhs", # this is an example of only having dc amp rather than amp files
26-
"intan/test_fpc_stim_250327_151617/info.rhs", # wrong files names Heberto will fix naimgin in the future
27-
24+
"intan/rhs_stim_data_single_file_format/intanTestFile.rhs", # header-attached rhs data with stimulus current
25+
"intan/test_fcs_dc_250327_154333/info.rhs", # this is an example of only having dc amp rather than amp files
26+
"intan/test_fpc_stim_250327_151617/info.rhs", # wrong files names Heberto will fix naimgin in the future
2827
]
2928

3029
def test_annotations(self):
@@ -88,7 +87,9 @@ def test_correct_reading_one_file_per_channel_amplifiers(self):
8887
amplifier_stream_index = 0
8988
for channel_name, amplifier_file_path in zip(channel_names, amplifier_file_paths):
9089
data_raw = np.fromfile(amplifier_file_path, dtype=np.int16).squeeze()
91-
data_from_neo = intan_reader.get_analogsignal_chunk(channel_ids=[channel_name], stream_index=amplifier_stream_index).squeeze()
90+
data_from_neo = intan_reader.get_analogsignal_chunk(
91+
channel_ids=[channel_name], stream_index=amplifier_stream_index
92+
).squeeze()
9293
np.testing.assert_allclose(data_raw, data_from_neo)
9394

9495
def test_correct_reading_one_file_per_channel_rhs_stim(self):
@@ -101,42 +102,45 @@ def test_correct_reading_one_file_per_channel_rhs_stim(self):
101102
# This should be the folder where the files of all the channels are stored
102103
folder_path = file_path.parent
103104

104-
# The paths for the stim channels are stim-A-000.dat, stim-A-001.dat, stim-A-002.dat,
105+
# The paths for the stim channels are stim-A-000.dat, stim-A-001.dat, stim-A-002.dat,
105106
# Whereas the ids are A-001_STIM, A-002_STIM, A-003_STIM, etc
106107
stim_file_paths = [path for path in folder_path.iterdir() if "stim" in path.name]
107108
channel_ids = [f"{p.stem[5:]}_STIM" for p in stim_file_paths]
108-
109-
stim_stream_index = 2
109+
110+
stim_stream_index = 2
110111
for channel_id, amplifier_file_path in zip(channel_ids, stim_file_paths):
111112
data_raw = np.fromfile(amplifier_file_path, dtype=np.uint16)
112113
decoded_data = intan_reader._decode_current_from_stim_data(data_raw, 0, data_raw.shape[0])
113-
data_from_neo = intan_reader.get_analogsignal_chunk(channel_ids=[channel_id], stream_index=stim_stream_index).squeeze()
114+
data_from_neo = intan_reader.get_analogsignal_chunk(
115+
channel_ids=[channel_id], stream_index=stim_stream_index
116+
).squeeze()
114117
np.testing.assert_allclose(decoded_data, data_from_neo)
115118

116-
117119
def test_correct_decoding_of_stimulus_current(self):
118120
# See https://github.com/NeuralEnsemble/python-neo/pull/1660 for discussion
119-
# See https://gin.g-node.org/NeuralEnsemble/ephy_testing_data/src/master/intan/README.md#rhs_stim_data_single_file_format
120-
# For a description of the data
121-
121+
# See https://gin.g-node.org/NeuralEnsemble/ephy_testing_data/src/master/intan/README.md#rhs_stim_data_single_file_format
122+
# For a description of the data
123+
122124
file_path = Path(self.get_local_path("intan/rhs_stim_data_single_file_format/intanTestFile.rhs"))
123125
intan_reader = IntanRawIO(filename=file_path)
124126
intan_reader.parse_header()
125-
126-
signal_streams = intan_reader.header['signal_streams']
127-
stream_ids = signal_streams['id'].tolist()
128-
stream_index = stream_ids.index('11')
127+
128+
signal_streams = intan_reader.header["signal_streams"]
129+
stream_ids = signal_streams["id"].tolist()
130+
stream_index = stream_ids.index("11")
129131
sampling_rate = intan_reader.get_signal_sampling_rate(stream_index=stream_index)
130132
sig_chunk = intan_reader.get_analogsignal_chunk(stream_index=stream_index, channel_ids=["D-016_STIM"])
131-
final_stim = intan_reader.rescale_signal_raw_to_float(sig_chunk, stream_index=stream_index, channel_ids=["D-016_STIM"])
133+
final_stim = intan_reader.rescale_signal_raw_to_float(
134+
sig_chunk, stream_index=stream_index, channel_ids=["D-016_STIM"]
135+
)
132136

133137
# This contains only the first pulse and I got this by visual inspection
134138
data_to_test = final_stim[200:250]
135-
139+
136140
positive_pulse_size = np.max(data_to_test).item()
137141
negative_pulse_size = np.min(data_to_test).item()
138142

139-
expected_value = 60 * 1e-6# 60 microamperes
143+
expected_value = 60 * 1e-6 # 60 microamperes
140144

141145
# Assert is close float
142146
assert np.isclose(positive_pulse_size, expected_value)
@@ -146,15 +150,15 @@ def test_correct_decoding_of_stimulus_current(self):
146150
argmin = np.argmin(data_to_test)
147151
argmax = np.argmax(data_to_test)
148152
assert argmin < argmax
149-
153+
150154
# Check that the negative pulse is 200 us long
151155
negative_pulse_frames = np.where(data_to_test > 0)[0]
152156
number_of_negative_frames = negative_pulse_frames.size
153157
duration_of_negative_pulse = number_of_negative_frames / sampling_rate
154158

155-
expected_duration = 200 * 1e-6 # 400 microseconds / 2
159+
expected_duration = 200 * 1e-6 # 400 microseconds / 2
156160
assert np.isclose(duration_of_negative_pulse, expected_duration)
157-
161+
158162
# Check that the positive pulse is 200 us long
159163
positive_pulse_frames = np.where(data_to_test > 0)[0]
160164
number_of_positive_frames = positive_pulse_frames.size
@@ -163,30 +167,31 @@ def test_correct_decoding_of_stimulus_current(self):
163167

164168
assert np.isclose(duration_of_positive_pulse, expected_duration)
165169

166-
167170
def test_correct_decoding_of_stimulus_current(self):
168171
# See https://github.com/NeuralEnsemble/python-neo/pull/1660 for discussion
169-
# See https://gin.g-node.org/NeuralEnsemble/ephy_testing_data/src/master/intan/README.md#rhs_stim_data_single_file_format
170-
# For a description of the data
171-
172+
# See https://gin.g-node.org/NeuralEnsemble/ephy_testing_data/src/master/intan/README.md#rhs_stim_data_single_file_format
173+
# For a description of the data
174+
172175
file_path = Path(self.get_local_path("intan/rhs_stim_data_single_file_format/intanTestFile.rhs"))
173176
intan_reader = IntanRawIO(filename=file_path)
174177
intan_reader.parse_header()
175-
176-
signal_streams = intan_reader.header['signal_streams']
177-
stream_ids = signal_streams['id'].tolist()
178-
stream_index = stream_ids.index('11')
178+
179+
signal_streams = intan_reader.header["signal_streams"]
180+
stream_ids = signal_streams["id"].tolist()
181+
stream_index = stream_ids.index("11")
179182
sampling_rate = intan_reader.get_signal_sampling_rate(stream_index=stream_index)
180183
sig_chunk = intan_reader.get_analogsignal_chunk(stream_index=stream_index, channel_ids=["D-016_STIM"])
181-
final_stim = intan_reader.rescale_signal_raw_to_float(sig_chunk, stream_index=stream_index, channel_ids=["D-016_STIM"])
184+
final_stim = intan_reader.rescale_signal_raw_to_float(
185+
sig_chunk, stream_index=stream_index, channel_ids=["D-016_STIM"]
186+
)
182187

183188
# This contains only the first pulse and I got this by visual inspection
184189
data_to_test = final_stim[200:250]
185-
190+
186191
positive_pulse_size = np.max(data_to_test).item()
187192
negative_pulse_size = np.min(data_to_test).item()
188193

189-
expected_value = 60 * 1e-6# 60 microamperes
194+
expected_value = 60 * 1e-6 # 60 microamperes
190195

191196
# Assert is close float
192197
assert np.isclose(positive_pulse_size, expected_value)
@@ -196,15 +201,15 @@ def test_correct_decoding_of_stimulus_current(self):
196201
argmin = np.argmin(data_to_test)
197202
argmax = np.argmax(data_to_test)
198203
assert argmin < argmax
199-
204+
200205
# Check that the negative pulse is 200 us long
201206
negative_pulse_frames = np.where(data_to_test > 0)[0]
202207
number_of_negative_frames = negative_pulse_frames.size
203208
duration_of_negative_pulse = number_of_negative_frames / sampling_rate
204209

205-
expected_duration = 200 * 1e-6 # 400 microseconds / 2
210+
expected_duration = 200 * 1e-6 # 400 microseconds / 2
206211
assert np.isclose(duration_of_negative_pulse, expected_duration)
207-
212+
208213
# Check that the positive pulse is 200 us long
209214
positive_pulse_frames = np.where(data_to_test > 0)[0]
210215
number_of_positive_frames = positive_pulse_frames.size

0 commit comments

Comments
 (0)