Skip to content

Commit 5207ed4

Browse files
authored
Merge branch 'master' into drop-old-numpy
2 parents 42d30fa + 1c58cb3 commit 5207ed4

File tree

12 files changed

+604
-99
lines changed

12 files changed

+604
-99
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/baserawio.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,184 @@ def __repr__(self):
239239

240240
return txt
241241

242+
def _repr_html_(self):
243+
"""
244+
HTML representation for the raw recording base.
245+
246+
Returns
247+
-------
248+
html : str
249+
The HTML representation as a string.
250+
"""
251+
html = []
252+
html.append('<div style="font-family: Arial, sans-serif; max-width: 1000px; margin: 0 auto;">')
253+
254+
# Header
255+
html.append(f'<h3 style="color: #2c3e50;">{self.__class__.__name__}: {self.source_name()}</h3>')
256+
257+
if self.is_header_parsed:
258+
# Basic info
259+
nb_block = self.block_count()
260+
html.append(f"<p><strong>nb_block:</strong> {nb_block}</p>")
261+
nb_seg = [self.segment_count(i) for i in range(nb_block)]
262+
html.append(f"<p><strong>nb_segment:</strong> {nb_seg}</p>")
263+
264+
# CSS for tables - using only black, white, and gray colors
265+
html.append(
266+
"""
267+
<style>
268+
#{unique_id} table.neo-table {{
269+
border-collapse: collapse;
270+
width: 100%;
271+
margin-bottom: 20px;
272+
font-size: 14px;
273+
color: inherit;
274+
background-color: transparent;
275+
}}
276+
#{unique_id} table.neo-table th,
277+
#{unique_id} table.neo-table td {{
278+
border: 1px solid #888;
279+
padding: 8px;
280+
text-align: left;
281+
}}
282+
#{unique_id} table.neo-table th {{
283+
background-color: rgba(128,128,128,0.2);
284+
}}
285+
#{unique_id} table.neo-table tr:nth-child(even) {{
286+
background-color: rgba(128,128,128,0.1);
287+
}}
288+
#{unique_id} details {{
289+
margin-bottom: 15px;
290+
border: 1px solid rgba(128,128,128,0.3);
291+
border-radius: 4px;
292+
overflow: hidden;
293+
background-color: transparent;
294+
}}
295+
#{unique_id} summary {{
296+
padding: 10px;
297+
background-color: rgba(128,128,128,0.2);
298+
cursor: pointer;
299+
font-weight: bold;
300+
color: inherit;
301+
}}
302+
#{unique_id} details[open] summary {{
303+
border-bottom: 1px solid rgba(128,128,128,0.3);
304+
}}
305+
#{unique_id} .table-container {{
306+
padding: 10px;
307+
overflow-x: auto;
308+
background-color: transparent;
309+
}}
310+
</style>
311+
"""
312+
)
313+
314+
# Signal Streams
315+
signal_streams = self.header["signal_streams"]
316+
if signal_streams.size > 0:
317+
html.append("<details>")
318+
html.append("<summary>Signal Streams</summary>")
319+
html.append('<div class="table-container">')
320+
html.append('<table class="neo-table">')
321+
html.append("<thead><tr><th>Name</th><th>ID</th><th>Buffer ID</th><th>Channel Count</th></tr></thead>")
322+
html.append("<tbody>")
323+
324+
for i, stream in enumerate(signal_streams):
325+
html.append("<tr>")
326+
html.append(f'<td>{stream["name"]}</td>')
327+
html.append(f'<td>{stream["id"]}</td>')
328+
html.append(f'<td>{stream["buffer_id"]}</td>')
329+
html.append(f"<td>{self.signal_channels_count(i)}</td>")
330+
html.append("</tr>")
331+
332+
html.append("</tbody></table>")
333+
html.append("</div>")
334+
html.append("</details>")
335+
336+
# Signal Channels
337+
signal_channels = self.header["signal_channels"]
338+
if signal_channels.size > 0:
339+
html.append("<details>")
340+
html.append("<summary>Signal Channels</summary>")
341+
html.append('<div class="table-container">')
342+
html.append('<table class="neo-table">')
343+
html.append(
344+
"<thead><tr><th>Name</th><th>ID</th><th>Sampling Rate</th><th>Data Type</th><th>Units</th><th>Gain</th><th>Offset</th><th>Stream ID</th><th>Buffer ID</th></tr></thead>"
345+
)
346+
html.append("<tbody>")
347+
348+
for channel in signal_channels:
349+
html.append("<tr>")
350+
html.append(f'<td>{channel["name"]}</td>')
351+
html.append(f'<td>{channel["id"]}</td>')
352+
html.append(f'<td>{channel["sampling_rate"]}</td>')
353+
html.append(f'<td>{channel["dtype"]}</td>')
354+
html.append(f'<td>{channel["units"]}</td>')
355+
html.append(f'<td>{channel["gain"]}</td>')
356+
html.append(f'<td>{channel["offset"]}</td>')
357+
html.append(f'<td>{channel["stream_id"]}</td>')
358+
html.append(f'<td>{channel["buffer_id"]}</td>')
359+
html.append("</tr>")
360+
361+
html.append("</tbody></table>")
362+
html.append("</div>")
363+
html.append("</details>")
364+
365+
# Spike Channels
366+
spike_channels = self.header["spike_channels"]
367+
if spike_channels.size > 0:
368+
html.append("<details>")
369+
html.append("<summary>Spike Channels</summary>")
370+
html.append('<div class="table-container">')
371+
html.append('<table class="neo-table">')
372+
html.append(
373+
"<thead><tr><th>Name</th><th>ID</th><th>WF Units</th><th>WF Gain</th><th>WF Offset</th><th>WF Left Sweep</th><th>WF Sampling Rate</th></tr></thead>"
374+
)
375+
html.append("<tbody>")
376+
377+
for channel in spike_channels:
378+
html.append("<tr>")
379+
html.append(f'<td>{channel["name"]}</td>')
380+
html.append(f'<td>{channel["id"]}</td>')
381+
html.append(f'<td>{channel["wf_units"]}</td>')
382+
html.append(f'<td>{channel["wf_gain"]}</td>')
383+
html.append(f'<td>{channel["wf_offset"]}</td>')
384+
html.append(f'<td>{channel["wf_left_sweep"]}</td>')
385+
html.append(f'<td>{channel["wf_sampling_rate"]}</td>')
386+
html.append("</tr>")
387+
388+
html.append("</tbody></table>")
389+
html.append("</div>")
390+
html.append("</details>")
391+
392+
# Event Channels
393+
event_channels = self.header["event_channels"]
394+
if event_channels.size > 0:
395+
html.append("<details>")
396+
html.append("<summary>Event Channels</summary>")
397+
html.append('<div class="table-container">')
398+
html.append('<table class="neo-table">')
399+
html.append("<thead><tr><th>Name</th><th>ID</th><th>Type</th></tr></thead>")
400+
html.append("<tbody>")
401+
402+
for channel in event_channels:
403+
html.append("<tr>")
404+
html.append(f'<td>{channel["name"]}</td>')
405+
html.append(f'<td>{channel["id"]}</td>')
406+
html.append(
407+
f'<td>{channel["type"].decode("utf-8") if isinstance(channel["type"], bytes) else channel["type"]}</td>'
408+
)
409+
html.append("</tr>")
410+
411+
html.append("</tbody></table>")
412+
html.append("</div>")
413+
html.append("</details>")
414+
else:
415+
html.append("<p><em>Call <code>parse_header()</code> to load the reader data.</p>")
416+
417+
html.append("</div>")
418+
return "\n".join(html)
419+
242420
def _generate_minimal_annotations(self):
243421
"""
244422
Helper function that generates a nested dict for annotations.

neo/rawio/blackrockrawio.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,12 +276,19 @@ def _parse_header(self):
276276
self.internal_unit_ids = [] # pair of chan['packet_id'], spikes['unit_class_nb']
277277
for i in range(len(self.__nev_ext_header[b"NEUEVWAV"])):
278278

279-
channel_id = self.__nev_ext_header[b"NEUEVWAV"]["electrode_id"][i]
279+
# electrode_id values are stored at uint16 which can overflow when
280+
# multiplying by 1000 below. We convert to a regular python int which
281+
# won't overflow
282+
channel_id = int(self.__nev_ext_header[b"NEUEVWAV"]["electrode_id"][i])
280283

281284
chan_mask = spikes["packet_id"] == channel_id
282285
chan_spikes = spikes[chan_mask]
286+
287+
# all `unit_class_nb` is uint8. Also will have issues with overflow
288+
# cast this to python int
283289
all_unit_id = np.unique(chan_spikes["unit_class_nb"])
284290
for u, unit_id in enumerate(all_unit_id):
291+
unit_id = int(unit_id)
285292
self.internal_unit_ids.append((channel_id, unit_id))
286293
name = f"ch{channel_id}#{unit_id}"
287294
_id = f"Unit {1000 * channel_id + unit_id}"

0 commit comments

Comments
 (0)