Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for Thorlabs RAW files (io.raw) #1069

Merged
merged 5 commits into from
Dec 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/inputs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,10 @@ you're using this and having trouble because it's not straightforward.
Thorlabs raw files
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Christoph Schmidt-Hieber (@neurodroid) has written `haussmeister`_ which
can load and convert ThorLabs \*.raw files to suite2p binary files!
suite2p will automatically use this if you have pip installed it
(``pip install haussmeister``).
Suite2p has been upgraded with internal support for Thorlabs raw files (Yael Prilutski).
Specify "raw" for "input_format".
Designed to work with one or several planes and/or channels.


.. _hdf5-files-and-sbx:

Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"nd2",
"sbxreader",
"h5py",
"opencv-python-headless"
"opencv-python-headless",
"xmltodict"
]

nwb_deps = [
Expand Down
2 changes: 1 addition & 1 deletion suite2p/gui/rungui.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ def create_buttons(self):
self.inputformat = QComboBox()
[
self.inputformat.addItem(f)
for f in ["tif", "binary", "bruker", "sbx", "h5", "movie", "nd2", "mesoscan", "haus"]
for f in ["tif", "binary", "bruker", "sbx", "h5", "movie", "nd2", "mesoscan", "raw"]
]
self.inputformat.currentTextChanged.connect(self.parse_inputformat)
self.layout.addWidget(self.inputformat, 2, 0, 1, 1)
Expand Down
1 change: 1 addition & 0 deletions suite2p/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Copyright © 2023 Howard Hughes Medical Institute, Authored by Carsen Stringer and Marius Pachitariu.
"""
from .h5 import h5py_to_binary
from .raw import raw_to_binary
from .nwb import save_nwb, read_nwb, nwb_to_binary
from .save import combined, compute_dydx, save_mat
from .sbx import sbx_to_binary
Expand Down
308 changes: 308 additions & 0 deletions suite2p/io/raw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
"""
Copyright © 2023 Yoav Livneh Lab, Authored by Yael Prilutski.
"""

import numpy as np

from os import makedirs, listdir
from os.path import isdir, isfile, getsize, join

try:
from xmltodict import parse
HAS_XML = True
except (ModuleNotFoundError, ImportError):
HAS_XML = False

Check warning on line 14 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L13-L14

Added lines #L13 - L14 were not covered by tests

EXTENSION = 'raw'


def raw_to_binary(ops, use_recorded_defaults=True):

""" Finds RAW files and writes them to binaries

Parameters
----------
ops : dictionary
"data_path"

use_recorded_defaults : bool
Recorded session parameters are used when 'True',
otherwise |ops| is expected to contain the following (additional) keys:
"nplanes",
"nchannels",
"fs"

Returns
-------
ops : dictionary of first plane

"""

if not HAS_XML:
raise ImportError("xmltodict is required for RAW file support (pip install xmltodict)")

Check warning on line 42 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L41-L42

Added lines #L41 - L42 were not covered by tests

# Load raw file configurations
raw_file_configurations = [_RawFile(path) for path in ops['data_path']]

Check warning on line 45 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L45

Added line #L45 was not covered by tests

# Split ops by captured planes
ops_paths = _initialize_destination_files(ops, raw_file_configurations, use_recorded_defaults=use_recorded_defaults)

Check warning on line 48 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L48

Added line #L48 was not covered by tests

# Convert all runs in order
for path in ops['data_path']:
print(f'Converting raw to binary: `{path}`')
ops_loaded = [np.load(i, allow_pickle=True)[()] for i in ops_paths]
_raw2bin(ops_loaded, _RawFile(path))

Check warning on line 54 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L51-L54

Added lines #L51 - L54 were not covered by tests

# Reload edited ops
ops_loaded = [np.load(i, allow_pickle=True)[()] for i in ops_paths]

Check warning on line 57 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L57

Added line #L57 was not covered by tests

# Create a mean image with the final number of frames
_update_mean(ops_loaded)

Check warning on line 60 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L60

Added line #L60 was not covered by tests

# Load & return all ops
return ops_loaded[0]

Check warning on line 63 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L63

Added line #L63 was not covered by tests


def _initialize_destination_files(ops, raw_file_configurations, use_recorded_defaults=True):

""" Prepares raw2bin conversion environment (files & folders) """

configurations = [

Check warning on line 70 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L70

Added line #L70 was not covered by tests
[cfg.channel, cfg.zplanes, cfg.xpx, cfg.ypx, cfg.frame_rate, cfg.xsize, cfg.ysize]
for cfg in raw_file_configurations
]

# Make sure all ops match each other
assert all(conf == configurations[0] for conf in configurations), \

Check warning on line 76 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L76

Added line #L76 was not covered by tests
f'Data attributes do not match. Can not concatenate shapes: {[conf for conf in configurations]}'

# Load configuration from first file in paths
cfg = raw_file_configurations[0]

Check warning on line 80 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L80

Added line #L80 was not covered by tests

# Expand configuration from defaults when necessary
if use_recorded_defaults:
ops['nplanes'] = cfg.zplanes
if cfg.channel > 1:
ops['nchannels'] = 2
ops['fs'] = cfg.frame_rate

Check warning on line 87 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L83-L87

Added lines #L83 - L87 were not covered by tests

# Prepare conversion environment for all files
ops_paths = []
nplanes = ops['nplanes']
nchannels = ops['nchannels']
second_plane = False
for i in range(0, nplanes):
ops['save_path'] = join(ops['save_path0'], 'suite2p', f'plane{i}')

Check warning on line 95 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L90-L95

Added lines #L90 - L95 were not covered by tests

if ('fast_disk' not in ops) or len(ops['fast_disk']) == 0 or second_plane:
ops['fast_disk'] = ops['save_path']
second_plane = True

Check warning on line 99 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L97-L99

Added lines #L97 - L99 were not covered by tests
else:
ops['fast_disk'] = join(ops['fast_disk'], 'suite2p', f'plane{i}')

Check warning on line 101 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L101

Added line #L101 was not covered by tests

ops['ops_path'] = join(ops['save_path'], 'ops.npy')
ops['reg_file'] = join(ops['fast_disk'], 'data.bin')
isdir(ops['fast_disk']) or makedirs(ops['fast_disk'])
isdir(ops['save_path']) or makedirs(ops['save_path'])
open(ops['reg_file'], 'wb').close()
if nchannels > 1:
ops['reg_file_chan2'] = join(ops['fast_disk'], 'data_chan2.bin')
open(ops['reg_file_chan2'], 'wb').close()

Check warning on line 110 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L103-L110

Added lines #L103 - L110 were not covered by tests

ops['meanImg'] = np.zeros((cfg.xpx, cfg.ypx), np.float32)
ops['nframes'] = 0
ops['frames_per_run'] = []
if nchannels > 1:
ops['meanImg_chan2'] = np.zeros((cfg.xpx, cfg.ypx), np.float32)

Check warning on line 116 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L112-L116

Added lines #L112 - L116 were not covered by tests

# write ops files
do_registration = ops['do_registration']
ops['Ly'] = cfg.xpx
ops['Lx'] = cfg.ypx
if not do_registration:
ops['yrange'] = np.array([0, ops['Ly']])
ops['xrange'] = np.array([0, ops['Lx']])

Check warning on line 124 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L119-L124

Added lines #L119 - L124 were not covered by tests

ops_paths.append(ops['ops_path'])
np.save(ops['ops_path'], ops)

Check warning on line 127 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L126-L127

Added lines #L126 - L127 were not covered by tests

# Environment ready;
return ops_paths

Check warning on line 130 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L130

Added line #L130 was not covered by tests


def _raw2bin(all_ops, cfg):

""" Converts a single RAW file to BIN format """

frames_in_chunk = int(all_ops[0]['batch_size'])

Check warning on line 137 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L137

Added line #L137 was not covered by tests

with open(cfg.path, 'rb') as raw_file:
chunk = frames_in_chunk * cfg.xpx * cfg.ypx * cfg.channel * cfg.recorded_planes * 2
raw_data_chunk = raw_file.read(chunk)
while raw_data_chunk:
data = np.frombuffer(raw_data_chunk, dtype=np.int16)
current_frames = int(len(data) / cfg.xpx / cfg.ypx / cfg.recorded_planes)

Check warning on line 144 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L139-L144

Added lines #L139 - L144 were not covered by tests

if cfg.channel > 1:
channel_a, channel_b = _split_into_2_channels(data.reshape(

Check warning on line 147 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L146-L147

Added lines #L146 - L147 were not covered by tests
current_frames * cfg.recorded_planes, cfg.xpx, cfg.ypx))
reshaped_data = []
for i in range(cfg.recorded_planes):
channel_a_plane = channel_a[i::cfg.recorded_planes]
channel_b_plane = channel_b[i::cfg.recorded_planes]
reshaped_data.append([channel_a_plane, channel_b_plane])

Check warning on line 153 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L149-L153

Added lines #L149 - L153 were not covered by tests

else:
reshaped_data = data.reshape(cfg.recorded_planes, current_frames, cfg.xpx, cfg.ypx)

Check warning on line 156 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L156

Added line #L156 was not covered by tests

for plane in range(0, cfg.zplanes):
ops = all_ops[plane]
plane_data = reshaped_data[plane]

Check warning on line 160 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L158-L160

Added lines #L158 - L160 were not covered by tests

if cfg.channel > 1:
with open(ops['reg_file'], 'ab') as bin_file:
bin_file.write(bytearray(plane_data[0].astype(np.int16)))
with open(ops['reg_file_chan2'], 'ab') as bin_file2:
bin_file2.write(bytearray(plane_data[1].astype(np.int16)))
ops['meanImg'] += plane_data[0].astype(np.float32).sum(axis=0)
ops['meanImg_chan2'] = ops['meanImg_chan2'] + plane_data[1].astype(np.float32).sum(axis=0)

Check warning on line 168 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L162-L168

Added lines #L162 - L168 were not covered by tests

else:
with open(ops['reg_file'], 'ab') as bin_file:
bin_file.write(bytearray(plane_data.astype(np.int16)))
ops['meanImg'] = ops['meanImg'] + plane_data.astype(np.float32).sum(axis=0)

Check warning on line 173 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L171-L173

Added lines #L171 - L173 were not covered by tests

raw_data_chunk = raw_file.read(chunk)

Check warning on line 175 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L175

Added line #L175 was not covered by tests

for ops in all_ops:
total_frames = int(cfg.size / cfg.xpx / cfg.ypx / cfg.recorded_planes / cfg.channel / 2)
ops['frames_per_run'].append(total_frames)
ops['nframes'] += total_frames
np.save(ops['ops_path'], ops)

Check warning on line 181 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L177-L181

Added lines #L177 - L181 were not covered by tests


def _split_into_2_channels(data):

""" Utility function, used during conversion - splits given raw data into 2 separate channels """

frames = data.shape[0]
channel_a_index = list(filter(lambda x: x % 2 == 0, range(frames)))
channel_b_index = list(filter(lambda x: x % 2 != 0, range(frames)))
return data[channel_a_index], data[channel_b_index]

Check warning on line 191 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L188-L191

Added lines #L188 - L191 were not covered by tests


def _update_mean(ops_loaded):

""" Adjusts all "meanImg" values at the end of raw-to-binary conversion. """

for ops in ops_loaded:
ops['meanImg'] /= ops['nframes']
np.save(ops['ops_path'], ops)

Check warning on line 200 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L198-L200

Added lines #L198 - L200 were not covered by tests


class _RawConfig:

""" Handles XML configuration parsing and exposes video shape & parameters for Thorlabs RAW files """

def __init__(self, raw_file_size, xml_path):

assert isfile(xml_path)

Check warning on line 209 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L209

Added line #L209 was not covered by tests

self._xml_path = xml_path

Check warning on line 211 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L211

Added line #L211 was not covered by tests

self.zplanes = 1
self.recorded_planes = 1

Check warning on line 214 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L213-L214

Added lines #L213 - L214 were not covered by tests

self.xpx = None
self.ypx = None
self.channel = None
self.frame_rate = None
self.xsize = None
self.ysize = None
self.nframes = None

Check warning on line 222 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L216-L222

Added lines #L216 - L222 were not covered by tests

# Load configuration defaults
with open(self._xml_path, 'r', encoding='utf-8') as file:
self._load_xml_config(raw_file_size, parse(file.read()))

Check warning on line 226 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L225-L226

Added lines #L225 - L226 were not covered by tests

# Make sure all fields have been filled
assert None not in (self.xpx, self.ypx, self.channel, self.frame_rate, self.xsize, self.ysize, self.nframes)

Check warning on line 229 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L229

Added line #L229 was not covered by tests

# Extract data shape
self._shape = self._find_shape()

Check warning on line 232 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L232

Added line #L232 was not covered by tests

@property
def shape(self): return self._shape

def _find_shape(self):

""" Discovers data dimensions """

shape = [self.nframes, self.xpx, self.ypx]
if self.recorded_planes > 1:
shape.insert(0, self.recorded_planes)
if self.channel > 1:
shape[0] = self.nframes * 2
return shape

Check warning on line 246 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L241-L246

Added lines #L241 - L246 were not covered by tests

def _load_xml_config(self, raw_file_size, xml):

""" Loads recording parameters from attached XML;

:param raw_file_size: Size (in bytes) of main RAW file
:param xml: Original XML contents as created during data acquisition (pre-parsed to a python dictionary) """

xml_data = xml['ThorImageExperiment']

Check warning on line 255 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L255

Added line #L255 was not covered by tests

self.xpx = int(xml_data['LSM']['@pixelX'])
self.ypx = int(xml_data['LSM']['@pixelY'])
self.channel = int(xml_data['LSM']['@channel'])
self.frame_rate = float(xml_data['LSM']['@frameRate'])
self.xsize = float(xml_data['LSM']['@widthUM'])
self.ysize = float(xml_data['LSM']['@heightUM'])
self.nframes = int(xml_data['Streaming']['@frames'])

Check warning on line 263 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L257-L263

Added lines #L257 - L263 were not covered by tests

flyback = int(xml_data['Streaming']['@flybackFrames'])
zenable = int(xml_data['Streaming']['@zFastEnable'])
planes = int(xml_data['ZStage']['@steps'])

Check warning on line 267 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L265-L267

Added lines #L265 - L267 were not covered by tests

if self.channel > 1:
self.channel = 2

Check warning on line 270 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L269-L270

Added lines #L269 - L270 were not covered by tests

if zenable > 0:
self.zplanes = planes
self.recorded_planes = flyback + self.zplanes
self.nframes = int(self.nframes / self.recorded_planes)

Check warning on line 275 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L272-L275

Added lines #L272 - L275 were not covered by tests

if xml_data['ExperimentStatus']['@value'] == 'Stopped':

Check warning on line 277 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L277

Added line #L277 was not covered by tests
# Recording stopped in the middle, the written frame number isn't correct
all_frames = int(raw_file_size / self.xpx / self.ypx / self.recorded_planes / self.channel / 2)
self.nframes = int(all_frames / self.recorded_planes)

Check warning on line 280 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L279-L280

Added lines #L279 - L280 were not covered by tests


class _RawFile(_RawConfig):

""" These objects represents all recording parameters per single Thorlabs RAW file """

_MAIN_FILE_SUFFIX = f'001.{EXTENSION}'

def __init__(self, dir_name):
self._dirname = dir_name
filenames = listdir(dir_name)

Check warning on line 291 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L290-L291

Added lines #L290 - L291 were not covered by tests

# Find main raw file
main_files = [fn for fn in filenames if fn.lower().endswith(self._MAIN_FILE_SUFFIX)]
assert 1 == len(main_files), f'Corrupted directory structure: "{dir_name}"'
self._raw_file_path = join(dir_name, main_files[0])
self._raw_file_size = getsize(self._raw_file_path)

Check warning on line 297 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L294-L297

Added lines #L294 - L297 were not covered by tests

# Load XML config
xml_files = [fn for fn in filenames if fn.lower().endswith('.xml')]
assert 1 == len(xml_files), f'Missing required XML configuration file from dir="{dir_name}"'
_RawConfig.__init__(self, self._raw_file_size, join(dir_name, xml_files[0]))

Check warning on line 302 in suite2p/io/raw.py

View check run for this annotation

Codecov / codecov/patch

suite2p/io/raw.py#L300-L302

Added lines #L300 - L302 were not covered by tests

@property
def path(self): return self._raw_file_path

@property
def size(self): return self._raw_file_size
13 changes: 2 additions & 11 deletions suite2p/run_s2p.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@

from . import extraction, io, registration, detection, classification, default_ops

try:
from haussmeister import haussio
HAS_HAUS = True
except ImportError:
HAS_HAUS = False

try:
import pynwb
HAS_NWB = True
Expand Down Expand Up @@ -466,8 +460,6 @@ def run_s2p(ops={}, db={}, server={}):
ops["input_format"] = "nd2"
if not HAS_ND2:
raise ImportError("nd2 not found; pip install nd2")
elif HAS_HAUS:
ops["input_format"] = "haus"
elif not "input_format" in ops:
ops["input_format"] = "tif"
elif ops["input_format"] == "movie":
Expand All @@ -486,9 +478,8 @@ def run_s2p(ops={}, db={}, server={}):
io.nd2_to_binary,
"mesoscan":
io.mesoscan_to_binary,
"haus":
lambda ops: haussio.load_haussio(ops["data_path"][0]).tosuite2p(ops.
copy()),
"raw":
io.raw_to_binary,
"bruker":
io.ome_to_binary,
"movie":
Expand Down
Loading