Skip to content

Commit

Permalink
Merge pull request #507 from catalystneuro/update_BrukerTiffDataInter…
Browse files Browse the repository at this point in the history
…face

Update `BrukerTiffImagingInterface` for volumetric imaging
  • Loading branch information
CodyCBakerPhD authored Aug 24, 2023
2 parents 96d23da + 628feec commit 7545155
Show file tree
Hide file tree
Showing 13 changed files with 982 additions and 46 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/dev-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,8 @@ jobs:
- name: Install full requirements (-e needed for codecov report)
run: pip install -e .[full,test]

# TODO - remove this temporarily disabling when new Bruker interfaces are through
#- name: Dev gallery - ROIExtractors
# run: pip install git+https://github.com/CatalystNeuro/roiextractors@main
- name: Dev gallery - ROIExtractors
run: pip install git+https://github.com/CatalystNeuro/roiextractors@main
- name: Dev gallery - PyNWB
run: pip install git+https://github.com/NeurodataWithoutBorders/pynwb@dev
- name: Dev gallery - SpikeInterface
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@

* Added deepcopy for metadata in `make_nwbfile_from_metadata`. [PR #545](https://github.com/catalystneuro/neuroconv/pull/545)

### Features

* Added converters for Bruker TIF format to support multiple streams of imaging data.
Added `BrukerTiffSinglePlaneConverter` for single plane imaging data which initializes a `BrukerTiffSinglePlaneImagingInterface` for each data stream.
The available data streams can be checked by `BrukerTiffSinglePlaneImagingInterface.get_streams(folder_path)` method.
Added `BrukerTiffMultiPlaneConverter` for volumetric imaging data with `plane_separation_type` argument that defines
whether to load the imaging planes as a volume (`"contiguous"`) or separately (`"disjoint"`).
The available data streams for the defined `plane_separation_type` can be checked by `BrukerTiffMultiPlaneImagingInterface.get_streams(folder_path, plane_separation_type)` method.



# v0.4.1
Expand Down
2 changes: 1 addition & 1 deletion docs/api/interfaces.ophys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Base Imaging

Bruker Tiff Imaging
-------------------
.. automodule:: neuroconv.datainterfaces.ophys.brukertiff.brukertiffdatainterface
.. automodule:: neuroconv.datainterfaces.ophys.brukertiff.brukertiffconverter

HDF5 Imaging
------------
Expand Down
40 changes: 34 additions & 6 deletions docs/conversion_examples_gallery/imaging/brukertiff.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,53 @@ Install NeuroConv with the additional dependencies necessary for reading Bruker
pip install neuroconv[brukertiff]
**Convert single imaging plane**

Convert Bruker TIFF imaging data to NWB using
:py:class:`~neuroconv.datainterfaces.ophys.brukertiff.brukertiffdatainterface.BrukerTiffImagingInterface`.
:py:class:`~neuroconv.converters.BrukerTiffSinglePlaneConverter`.

.. code-block:: python
>>> from dateutil import tz
>>> from neuroconv.datainterfaces import BrukerTiffImagingInterface
>>> from neuroconv.converters import BrukerTiffSinglePlaneConverter
>>>
>>> # The 'folder_path' is the path to the folder containing the OME-TIF image files and the XML configuration file.
>>> folder_path = OPHYS_DATA_PATH / "imaging_datasets" / "BrukerTif" / "NCCR32_2023_02_20_Into_the_void_t_series_baseline-000"
>>> interface = BrukerTiffImagingInterface(folder_path=folder_path)
>>> converter = BrukerTiffSinglePlaneConverter(folder_path=folder_path)
>>>
>>> metadata = converter.get_metadata()
>>> # For data provenance we can add the time zone information to the conversion if missing
>>> session_start_time = metadata["NWBFile"]["session_start_time"]
>>> tzinfo = tz.gettz("US/Pacific")
>>> metadata["NWBFile"].update(session_start_time=session_start_time.replace(tzinfo=tzinfo))
>>>
>>> # Choose a path for saving the nwb file and run the conversion
>>> nwbfile_path = f"{path_to_save_nwbfile}"
>>> converter.run_conversion(nwbfile_path=nwbfile_path, metadata=metadata)
**Convert multiple imaging planes**

Convert volumetric Bruker TIFF imaging data to NWB using
:py:class:`~neuroconv.converters.BrukerTiffMultiPlaneConverter`.
The `plane_separation_type` parameter defines how to load the imaging planes.
Use "contiguous" to create the volumetric two photon series, and "disjoint" to create separate imaging plane and two photon series for each plane.

.. code-block:: python
>>> from dateutil import tz
>>> from neuroconv.converters import BrukerTiffMultiPlaneConverter
>>>
>>> # The 'folder_path' is the path to the folder containing the OME-TIF image files and the XML configuration file.
>>> folder_path = OPHYS_DATA_PATH / "imaging_datasets" / "BrukerTif" / "NCCR32_2022_11_03_IntoTheVoid_t_series-005"
>>> converter = BrukerTiffMultiPlaneConverter(folder_path=folder_path, plane_separation_type="contiguous")
>>>
>>> metadata = interface.get_metadata()
>>> metadata = converter.get_metadata()
>>> # For data provenance we can add the time zone information to the conversion if missing
>>> session_start_time = metadata["NWBFile"]["session_start_time"]
>>> tzinfo = tz.gettz("US/Pacific")
>>> metadata["NWBFile"].update(session_start_time=session_start_time.replace(tzinfo=tzinfo))
>>>
>>> # Choose a path for saving the nwb file and run the conversion
>>> nwbfile_path = f"{path_to_save_nwbfile}"
>>> interface.run_conversion(nwbfile_path=nwbfile_path, metadata=metadata)
NWB file saved at ...
>>> converter.run_conversion(nwbfile_path=nwbfile_path, metadata=metadata)
4 changes: 4 additions & 0 deletions src/neuroconv/converters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
from ..datainterfaces.ecephys.spikeglx.spikeglxconverter import SpikeGLXConverterPipe
from ..datainterfaces.ophys.brukertiff.brukertiffconverter import (
BrukerTiffMultiPlaneConverter,
BrukerTiffSinglePlaneConverter,
)
from ..datainterfaces.ophys.miniscope.miniscopeconverter import MiniscopeConverter
8 changes: 6 additions & 2 deletions src/neuroconv/datainterfaces/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@
from .icephys.abf.abfdatainterface import AbfInterface

# Ophys
from .ophys.brukertiff.brukertiffdatainterface import BrukerTiffImagingInterface
from .ophys.brukertiff.brukertiffdatainterface import (
BrukerTiffMultiPlaneImagingInterface,
BrukerTiffSinglePlaneImagingInterface,
)
from .ophys.caiman.caimandatainterface import CaimanSegmentationInterface
from .ophys.cnmfe.cnmfedatainterface import CnmfeSegmentationInterface
from .ophys.extract.extractdatainterface import ExtractSegmentationInterface
Expand Down Expand Up @@ -138,7 +141,8 @@
TiffImagingInterface,
Hdf5ImagingInterface,
ScanImageImagingInterface,
BrukerTiffImagingInterface,
BrukerTiffMultiPlaneImagingInterface,
BrukerTiffSinglePlaneImagingInterface,
MicroManagerTiffImagingInterface,
MiniscopeImagingInterface,
# Behavior
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ def add_to_nwbfile(
nwbfile: NWBFile,
metadata: Optional[dict] = None,
photon_series_type: Literal["TwoPhotonSeries", "OnePhotonSeries"] = "TwoPhotonSeries",
photon_series_index: int = 0,
stub_test: bool = False,
stub_frames: int = 100,
):
Expand All @@ -119,4 +120,5 @@ def add_to_nwbfile(
nwbfile=nwbfile,
metadata=metadata,
photon_series_type=photon_series_type,
photon_series_index=photon_series_index,
)
197 changes: 197 additions & 0 deletions src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
from typing import Literal, Optional

from pynwb import NWBFile

from ... import (
BrukerTiffMultiPlaneImagingInterface,
BrukerTiffSinglePlaneImagingInterface,
)
from ....nwbconverter import NWBConverter
from ....tools.nwb_helpers import make_or_load_nwbfile
from ....utils import FolderPathType, get_schema_from_method_signature


class BrukerTiffMultiPlaneConverter(NWBConverter):
@classmethod
def get_source_schema(cls):
return get_schema_from_method_signature(cls)

def get_conversion_options_schema(self):
interface_name = list(self.data_interface_objects.keys())[0]
return self.data_interface_objects[interface_name].get_conversion_options_schema()

def __init__(
self,
folder_path: FolderPathType,
plane_separation_type: Literal["disjoint", "contiguous"] = None,
verbose: bool = False,
):
"""
Initializes the data interfaces for Bruker volumetric imaging data stream.
Parameters
----------
folder_path : PathType
The path to the folder that contains the Bruker TIF image files (.ome.tif) and configuration files (.xml, .env).
plane_separation_type: {'contiguous', 'disjoint'}
Defines how to write volumetric imaging data. Use 'contiguous' to create the volumetric two photon series,
and 'disjoint' to create separate imaging plane and two photon series for each plane.
verbose : bool, default: True
Controls verbosity.
"""
self.verbose = verbose
self.data_interface_objects = dict()

if plane_separation_type is None or plane_separation_type not in ["disjoint", "contiguous"]:
raise ValueError(
"For volumetric imaging data the plane separation method must be one of 'disjoint' or 'contiguous'."
)

streams = BrukerTiffMultiPlaneImagingInterface.get_streams(
folder_path=folder_path,
plane_separation_type=plane_separation_type,
)

channel_streams = streams["channel_streams"]
interface_name = "BrukerImaging"
for channel_stream_name in channel_streams:
plane_streams = streams["plane_streams"][channel_stream_name]
for plane_stream in plane_streams:
if len(plane_streams) > 1:
interface_name += plane_stream.replace("_", "")
if plane_separation_type == "contiguous":
self.data_interface_objects[interface_name] = BrukerTiffMultiPlaneImagingInterface(
folder_path=folder_path,
stream_name=plane_stream,
)
elif plane_separation_type == "disjoint":
self.data_interface_objects[interface_name] = BrukerTiffSinglePlaneImagingInterface(
folder_path=folder_path,
stream_name=plane_stream,
)

def add_to_nwbfile(
self,
nwbfile: NWBFile,
metadata,
stub_test: bool = False,
stub_frames: int = 100,
):
for photon_series_index, (interface_name, data_interface) in enumerate(self.data_interface_objects.items()):
data_interface.add_to_nwbfile(
nwbfile=nwbfile,
metadata=metadata,
photon_series_index=photon_series_index,
stub_test=stub_test,
stub_frames=stub_frames,
)

def run_conversion(
self,
nwbfile_path: Optional[str] = None,
nwbfile: Optional[NWBFile] = None,
metadata: Optional[dict] = None,
overwrite: bool = False,
stub_test: bool = False,
stub_frames: int = 100,
) -> None:
if metadata is None:
metadata = self.get_metadata()

self.validate_metadata(metadata=metadata)

self.temporally_align_data_interfaces()

with make_or_load_nwbfile(
nwbfile_path=nwbfile_path,
nwbfile=nwbfile,
metadata=metadata,
overwrite=overwrite,
verbose=self.verbose,
) as nwbfile_out:
self.add_to_nwbfile(nwbfile=nwbfile_out, metadata=metadata, stub_test=stub_test, stub_frames=stub_frames)


class BrukerTiffSinglePlaneConverter(NWBConverter):
@classmethod
def get_source_schema(cls):
return get_schema_from_method_signature(cls)

def get_conversion_options_schema(self):
interface_name = list(self.data_interface_objects.keys())[0]
return self.data_interface_objects[interface_name].get_conversion_options_schema()

def __init__(
self,
folder_path: FolderPathType,
verbose: bool = False,
):
"""
Initializes the data interfaces for Bruker imaging data stream.
Parameters
----------
folder_path : PathType
The path to the folder that contains the Bruker TIF image files (.ome.tif) and configuration files (.xml, .env).
verbose : bool, default: True
Controls verbosity.
"""
from roiextractors.extractors.tiffimagingextractors.brukertiffimagingextractor import (
_determine_imaging_is_volumetric,
)

if _determine_imaging_is_volumetric(folder_path=folder_path):
raise ValueError("For volumetric imaging data use BrukerTiffMultiPlaneConverter.")

self.verbose = verbose
self.data_interface_objects = dict()

streams = BrukerTiffSinglePlaneImagingInterface.get_streams(folder_path=folder_path)
channel_streams = streams["channel_streams"]
interface_name = "BrukerImaging"
for channel_stream_name in channel_streams:
if len(channel_streams) > 1:
interface_name += channel_stream_name.replace("_", "")
self.data_interface_objects[interface_name] = BrukerTiffSinglePlaneImagingInterface(
folder_path=folder_path,
stream_name=channel_stream_name,
)

def add_to_nwbfile(
self,
nwbfile: NWBFile,
metadata,
stub_test: bool = False,
stub_frames: int = 100,
):
for photon_series_index, (interface_name, data_interface) in enumerate(self.data_interface_objects.items()):
data_interface.add_to_nwbfile(
nwbfile=nwbfile,
metadata=metadata,
photon_series_index=photon_series_index,
stub_test=stub_test,
stub_frames=stub_frames,
)

def run_conversion(
self,
nwbfile_path: Optional[str] = None,
nwbfile: Optional[NWBFile] = None,
metadata: Optional[dict] = None,
overwrite: bool = False,
stub_test: bool = False,
stub_frames: int = 100,
) -> None:
if metadata is None:
metadata = self.get_metadata()

self.validate_metadata(metadata=metadata)

self.temporally_align_data_interfaces()

with make_or_load_nwbfile(
nwbfile_path=nwbfile_path,
nwbfile=nwbfile,
metadata=metadata,
overwrite=overwrite,
verbose=self.verbose,
) as nwbfile_out:
self.add_to_nwbfile(nwbfile=nwbfile_out, metadata=metadata, stub_test=stub_test, stub_frames=stub_frames)
Loading

0 comments on commit 7545155

Please sign in to comment.