From 0828faefab5af643e01367443fbd40150011a9c4 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Wed, 5 Jul 2023 16:00:56 +0200 Subject: [PATCH 01/20] update interface for volumetric imaging --- .../brukertiff/brukertiffdatainterface.py | 129 +++++++++++++++--- tests/test_on_data/test_imaging_interfaces.py | 37 +++++ 2 files changed, 147 insertions(+), 19 deletions(-) diff --git a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py index 7ca318a35..8b2653315 100644 --- a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py +++ b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py @@ -1,6 +1,12 @@ +from copy import deepcopy +from typing import Literal, Optional +from warnings import warn + from dateutil.parser import parse +from pynwb import NWBFile from ..baseimagingextractorinterface import BaseImagingExtractorInterface +from ....tools.roiextractors import add_imaging from ....utils import FolderPathType from ....utils.dict import DeepDict @@ -16,7 +22,12 @@ def get_source_schema(cls) -> dict: ] = "The path that points to the folder containing the Bruker TIF image files and configuration files." return source_schema - def __init__(self, folder_path: FolderPathType, verbose: bool = True): + def __init__( + self, + folder_path: FolderPathType, + plane_separation_type: Optional[Literal["contiguous", "disjoint"]] = None, + verbose: bool = True, + ): """ Initialize reading of TIFF files. @@ -24,9 +35,57 @@ def __init__(self, folder_path: FolderPathType, verbose: bool = True): ---------- folder_path : FolderPathType 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. The default behavior is to assume the planes are contiguous, + and the imaging plane is a volume. Use 'disjoint' for writing them as a separate plane. verbose : bool, default: True """ + self.plane_separation_type = plane_separation_type super().__init__(folder_path=folder_path, verbose=verbose) + self._image_size = self.imaging_extractor.get_image_size() + # we can also check if the difference in the changing z positions are equal to + # the number of microns per pixel (5) then we know its volumetric + # that is probably better once the multicolor example gets in the picture + if len(self._image_size) == 3 and plane_separation_type not in ["disjoint", "contiguous"]: + raise AssertionError( + "For volumetric imaging data the plane separation method must be one of 'disjoint' or 'contiguous'." + ) + if plane_separation_type is not None and len(self._image_size) == 2: + warn("The plane separation method is ignored for non-volumetric data.") + # for disjoint planes the frame rate should be divided by the number of planes + if plane_separation_type is "disjoint" and len(self._image_size) == 3: + self.imaging_extractor._sampling_frequency /= self._image_size[-1] + + def update_metadata_for_disjoint_planes(self, metadata: DeepDict) -> DeepDict: + num_z_planes = self._image_size[-1] + + first_imaging_plane_metadata = metadata["Ophys"]["ImagingPlane"].pop(0) + first_two_photon_series_metadata = metadata["Ophys"]["TwoPhotonSeries"].pop(0) + + positions = self.imaging_extractor.xml_metadata["positionCurrent"][:num_z_planes] + for plane_num in range(num_z_planes): + # TODO: not always the last is the variable z-axis parameter + z_position = positions[plane_num][-1]["ZAxis"] + z_position_value = float(z_position["value"]) / 1e6 + + imaging_plane_metadata = deepcopy(first_imaging_plane_metadata) + imaging_plane_name = f"ImagingPlane{plane_num + 1}" + imaging_plane_metadata.update( + name=imaging_plane_name, + description=f"The plane {plane_num + 1} imaged at {z_position_value} meters by the microscope.", + ) + metadata["Ophys"]["ImagingPlane"].append(imaging_plane_metadata) + + two_photon_series_metadata = deepcopy(first_two_photon_series_metadata) + two_photon_series_metadata.update( + name=f"TwoPhotonSeries{plane_num + 1}", + description=f"The imaging data for plane {plane_num + 1} acquired from the Bruker Two-Photon Microscope.", + imaging_plane=imaging_plane_name, + field_of_view=two_photon_series_metadata["field_of_view"].pop(-1), + ) + metadata["Ophys"]["TwoPhotonSeries"].append(two_photon_series_metadata) + + return metadata def get_metadata(self) -> DeepDict: metadata = super().get_metadata() @@ -56,25 +115,57 @@ def get_metadata(self) -> DeepDict: ) microns_per_pixel = xml_metadata["micronsPerPixel"] - if microns_per_pixel: - image_size_in_pixels = self.imaging_extractor.get_image_size() - x_position_in_meters = float(microns_per_pixel[0]["XAxis"]) / 1e6 - y_position_in_meters = float(microns_per_pixel[1]["YAxis"]) / 1e6 - z_plane_position_in_meters = float(microns_per_pixel[2]["ZAxis"]) / 1e6 - grid_spacing = [ - y_position_in_meters, - x_position_in_meters, - ] + x_position_in_meters = float(microns_per_pixel[0]["XAxis"]) / 1e6 + y_position_in_meters = float(microns_per_pixel[1]["YAxis"]) / 1e6 + z_plane_position_in_meters = float(microns_per_pixel[2]["ZAxis"]) / 1e6 + grid_spacing = [y_position_in_meters, x_position_in_meters] + if len(self._image_size) == 3: + grid_spacing.append(z_plane_position_in_meters) - imaging_plane_metadata.update( - grid_spacing=grid_spacing, description=f"The plane imaged at {z_plane_position_in_meters} meters depth." - ) + imaging_plane_metadata.update( + grid_spacing=grid_spacing, description=f"The plane imaged at {z_plane_position_in_meters} meters depth." + ) - field_of_view = [ - y_position_in_meters * image_size_in_pixels[1], - x_position_in_meters * image_size_in_pixels[0], - z_plane_position_in_meters, - ] - two_photon_series_metadata.update(field_of_view=field_of_view) + field_of_view = [ + y_position_in_meters * self._image_size[1], + x_position_in_meters * self._image_size[0], + z_plane_position_in_meters, # for disjoint planes this should be 2D + ] + two_photon_series_metadata.update(field_of_view=field_of_view) + + if self.plane_separation_type == "disjoint" and len(self._image_size) == 3: + return self.update_metadata_for_disjoint_planes(metadata=metadata) + if self.plane_separation_type == "contiguous" and len(self._image_size) == 3: + two_photon_series_metadata.update( + description="The volumetric imaging data acquired from the Bruker Two-Photon Microscope." + ) return metadata + + def add_to_nwbfile( + self, + nwbfile: NWBFile, + metadata: Optional[dict] = None, + photon_series_type: Literal["TwoPhotonSeries", "OnePhotonSeries"] = "TwoPhotonSeries", + stub_test: bool = False, + stub_frames: int = 100, + ): + extractor_image_size = self.imaging_extractor.get_image_size() + if (self.plane_separation_type == "contiguous") or len(extractor_image_size) == 2: + return super().add_to_nwbfile( + nwbfile=nwbfile, + metadata=metadata, + photon_series_type=photon_series_type, + stub_test=stub_test, + stub_frames=stub_frames, + ) + + num_z_planes = self.imaging_extractor.get_image_size()[-1] + for plane_num in range(num_z_planes): + imaging_extractor = self.imaging_extractor.depth_slice(start_plane=plane_num, end_plane=plane_num + 1) + add_imaging( + imaging=imaging_extractor, + nwbfile=nwbfile, + metadata=metadata, + photon_series_type=photon_series_type, + ) diff --git a/tests/test_on_data/test_imaging_interfaces.py b/tests/test_on_data/test_imaging_interfaces.py index ba63b6ee6..08fd1db01 100644 --- a/tests/test_on_data/test_imaging_interfaces.py +++ b/tests/test_on_data/test_imaging_interfaces.py @@ -164,6 +164,43 @@ def check_read_nwb(self, nwbfile_path: str): super().check_read_nwb(nwbfile_path=nwbfile_path) +class TestBrukerTiffImagingInterfaceDualPlaneCase(ImagingExtractorInterfaceTestMixin, TestCase): + data_interface_cls = BrukerTiffImagingInterface + interface_kwargs = dict( + folder_path=str( + OPHYS_DATA_PATH / "imaging_datasets" / "BrukerTif" / "NCCR32_2022_11_03_IntoTheVoid_t_series-005" + ), + plane_separation_type="contiguous", + ) + save_directory = OUTPUT_PATH + + @classmethod + def setUpClass(cls) -> None: + cls.photon_series_name = "TwoPhotonSeries" + cls.num_frames = 5 + cls.image_shape = (512, 512, 2) + cls.device_metadata = dict(name="BrukerFluorescenceMicroscope", description="Version 5.6.64.400") + cls.optical_channel_metadata = dict( + name="Ch2", + emission_lambda=np.NAN, + description="An optical channel of the microscope.", + ) + + def check_extracted_metadata(self, metadata: dict): + self.assertEqual(metadata["NWBFile"]["session_start_time"], datetime(2022, 11, 3, 11, 20, 34)) + # self.assertDictEqual(metadata["Ophys"], self.ophys_metadata) + + def check_read_nwb(self, nwbfile_path: str): + # TODO: also add maybe NwbVolumetricImagingExtractor to support checking volumetric TwoPhotonSeries + + with NWBHDF5IO(path=nwbfile_path) as io: + nwbfile = io.read() + photon_series = nwbfile.acquisition[self.photon_series_name] + self.assertEqual(photon_series.data.shape, (self.num_frames, *self.image_shape)) + assert_array_equal(photon_series.dimension[:], self.image_shape) + self.assertEqual(photon_series.rate, 20.629515014336377) + + class TestMicroManagerTiffImagingInterface(ImagingExtractorInterfaceTestMixin, TestCase): data_interface_cls = MicroManagerTiffImagingInterface interface_kwargs = dict( From 0cfee7a6b14d2a41f29f620c36395b0483e174ee Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 6 Jul 2023 15:21:03 +0200 Subject: [PATCH 02/20] add stream_name to interface --- .../ophys/brukertiff/brukertiffdatainterface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py index 8b2653315..9fb376c1d 100644 --- a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py +++ b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py @@ -25,6 +25,7 @@ def get_source_schema(cls) -> dict: def __init__( self, folder_path: FolderPathType, + stream_name: Optional[str] = None, plane_separation_type: Optional[Literal["contiguous", "disjoint"]] = None, verbose: bool = True, ): @@ -35,13 +36,15 @@ def __init__( ---------- folder_path : FolderPathType The path to the folder that contains the Bruker TIF image files (.ome.tif) and configuration files (.xml, .env). + stream_name : str, optional + The name of the recording stream (e.g. 'Ch2'). plane_separation_type: {'contiguous', 'disjoint'} Defines how to write volumetric imaging data. The default behavior is to assume the planes are contiguous, and the imaging plane is a volume. Use 'disjoint' for writing them as a separate plane. verbose : bool, default: True """ self.plane_separation_type = plane_separation_type - super().__init__(folder_path=folder_path, verbose=verbose) + super().__init__(folder_path=folder_path, stream_name=stream_name, verbose=verbose) self._image_size = self.imaging_extractor.get_image_size() # we can also check if the difference in the changing z positions are equal to # the number of microns per pixel (5) then we know its volumetric From 4da012639031b826414f21d1477172b8c0f3bd6c Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Sun, 23 Jul 2023 20:07:33 +0200 Subject: [PATCH 03/20] small update --- .../brukertiff/brukertiffdatainterface.py | 31 ++++++++----------- tests/test_on_data/test_imaging_interfaces.py | 2 +- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py index 9fb376c1d..84feee7ae 100644 --- a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py +++ b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py @@ -44,20 +44,13 @@ def __init__( verbose : bool, default: True """ self.plane_separation_type = plane_separation_type - super().__init__(folder_path=folder_path, stream_name=stream_name, verbose=verbose) + super().__init__( + folder_path=folder_path, + stream_name=stream_name, + verbose=verbose, + plane_separation_type=plane_separation_type, + ) self._image_size = self.imaging_extractor.get_image_size() - # we can also check if the difference in the changing z positions are equal to - # the number of microns per pixel (5) then we know its volumetric - # that is probably better once the multicolor example gets in the picture - if len(self._image_size) == 3 and plane_separation_type not in ["disjoint", "contiguous"]: - raise AssertionError( - "For volumetric imaging data the plane separation method must be one of 'disjoint' or 'contiguous'." - ) - if plane_separation_type is not None and len(self._image_size) == 2: - warn("The plane separation method is ignored for non-volumetric data.") - # for disjoint planes the frame rate should be divided by the number of planes - if plane_separation_type is "disjoint" and len(self._image_size) == 3: - self.imaging_extractor._sampling_frequency /= self._image_size[-1] def update_metadata_for_disjoint_planes(self, metadata: DeepDict) -> DeepDict: num_z_planes = self._image_size[-1] @@ -110,8 +103,14 @@ def get_metadata(self) -> DeepDict: imaging_rate=self.imaging_extractor.get_sampling_frequency(), ) two_photon_series_metadata = metadata["Ophys"]["TwoPhotonSeries"][0] + two_photon_series_description = "Imaging data acquired from the Bruker Two-Photon Microscope." + if len(self._image_size) == 3: + two_photon_series_description = ( + "The volumetric imaging data acquired from the Bruker Two-Photon Microscope." + ) + two_photon_series_metadata.update( - description="Imaging data acquired from the Bruker Two-Photon Microscope.", + description=two_photon_series_description, unit="px", format="tiff", scan_line_rate=1 / float(xml_metadata["scanLinePeriod"]), @@ -138,10 +137,6 @@ def get_metadata(self) -> DeepDict: if self.plane_separation_type == "disjoint" and len(self._image_size) == 3: return self.update_metadata_for_disjoint_planes(metadata=metadata) - if self.plane_separation_type == "contiguous" and len(self._image_size) == 3: - two_photon_series_metadata.update( - description="The volumetric imaging data acquired from the Bruker Two-Photon Microscope." - ) return metadata diff --git a/tests/test_on_data/test_imaging_interfaces.py b/tests/test_on_data/test_imaging_interfaces.py index 08fd1db01..da3af8829 100644 --- a/tests/test_on_data/test_imaging_interfaces.py +++ b/tests/test_on_data/test_imaging_interfaces.py @@ -110,7 +110,7 @@ def setUpClass(cls) -> None: location="unknown", device=cls.device_metadata["name"], optical_channel=[cls.optical_channel_metadata], - imaging_rate=30.345939461428763, + imaging_rate=29.873732099062256, grid_spacing=[1.1078125e-06, 1.1078125e-06], ) From f117c1186d7d53b8af2f70c079d2cdfc17a1040f Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Mon, 7 Aug 2023 16:46:45 +0200 Subject: [PATCH 04/20] separate interfaces for multi and single planes --- src/neuroconv/datainterfaces/__init__.py | 8 +- .../brukertiff/brukertiffdatainterface.py | 199 +++++++++++------- tests/test_on_data/test_imaging_interfaces.py | 136 +++++++++++- 3 files changed, 262 insertions(+), 81 deletions(-) diff --git a/src/neuroconv/datainterfaces/__init__.py b/src/neuroconv/datainterfaces/__init__.py index 4ca5ff1f9..ce369997f 100644 --- a/src/neuroconv/datainterfaces/__init__.py +++ b/src/neuroconv/datainterfaces/__init__.py @@ -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 @@ -138,7 +141,8 @@ TiffImagingInterface, Hdf5ImagingInterface, ScanImageImagingInterface, - BrukerTiffImagingInterface, + BrukerTiffMultiPlaneImagingInterface, + BrukerTiffSinglePlaneImagingInterface, MicroManagerTiffImagingInterface, MiniscopeImagingInterface, # Behavior diff --git a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py index 84feee7ae..8c850ad2f 100644 --- a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py +++ b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py @@ -6,27 +6,40 @@ from pynwb import NWBFile from ..baseimagingextractorinterface import BaseImagingExtractorInterface -from ....tools.roiextractors import add_imaging from ....utils import FolderPathType from ....utils.dict import DeepDict -class BrukerTiffImagingInterface(BaseImagingExtractorInterface): - """Data Interface for BrukerTiffImagingExtractor.""" +class BrukerTiffMultiPlaneImagingInterface(BaseImagingExtractorInterface): + """Data Interface for BrukerTiffMultiPlaneImagingExtractor.""" @classmethod def get_source_schema(cls) -> dict: source_schema = super().get_source_schema() source_schema["properties"]["folder_path"][ "description" - ] = "The path that points to the folder containing the Bruker TIF image files and configuration files." + ] = "The path that points to the folder containing the Bruker volumetric TIF image files and configuration files." return source_schema + @classmethod + def get_streams( + cls, + folder_path: FolderPathType, + plane_separation_type: Literal["contiguous", "disjoint"] = None, + ) -> dict: + from roiextractors import BrukerTiffMultiPlaneImagingExtractor + + streams = BrukerTiffMultiPlaneImagingExtractor.get_streams(folder_path=folder_path) + channel_stream_name = streams["channel_streams"][0] + if plane_separation_type == "contiguous": + streams["plane_streams"].update({channel_stream_name: [streams["plane_streams"][channel_stream_name][0]]}) + return streams + def __init__( self, folder_path: FolderPathType, stream_name: Optional[str] = None, - plane_separation_type: Optional[Literal["contiguous", "disjoint"]] = None, + plane_separation_type: Literal["contiguous", "disjoint"] = None, verbose: bool = True, ): """ @@ -43,46 +56,114 @@ def __init__( and the imaging plane is a volume. Use 'disjoint' for writing them as a separate plane. verbose : bool, default: True """ - self.plane_separation_type = plane_separation_type + self.streams = self.get_streams(folder_path=folder_path, plane_separation_type=plane_separation_type) super().__init__( folder_path=folder_path, stream_name=stream_name, verbose=verbose, - plane_separation_type=plane_separation_type, ) + self._stream_name = self.imaging_extractor.stream_name.replace("_", "") self._image_size = self.imaging_extractor.get_image_size() - def update_metadata_for_disjoint_planes(self, metadata: DeepDict) -> DeepDict: - num_z_planes = self._image_size[-1] + def get_metadata(self) -> DeepDict: + metadata = super().get_metadata() - first_imaging_plane_metadata = metadata["Ophys"]["ImagingPlane"].pop(0) - first_two_photon_series_metadata = metadata["Ophys"]["TwoPhotonSeries"].pop(0) + xml_metadata = self.imaging_extractor.xml_metadata + session_start_time = parse(xml_metadata["date"]) + metadata["NWBFile"].update(session_start_time=session_start_time) - positions = self.imaging_extractor.xml_metadata["positionCurrent"][:num_z_planes] - for plane_num in range(num_z_planes): - # TODO: not always the last is the variable z-axis parameter - z_position = positions[plane_num][-1]["ZAxis"] - z_position_value = float(z_position["value"]) / 1e6 + description = f"Version {xml_metadata['version']}" + device_name = "BrukerFluorescenceMicroscope" + metadata["Ophys"]["Device"][0].update( + name=device_name, + description=description, + ) - imaging_plane_metadata = deepcopy(first_imaging_plane_metadata) - imaging_plane_name = f"ImagingPlane{plane_num + 1}" - imaging_plane_metadata.update( - name=imaging_plane_name, - description=f"The plane {plane_num + 1} imaged at {z_position_value} meters by the microscope.", - ) - metadata["Ophys"]["ImagingPlane"].append(imaging_plane_metadata) + imaging_plane_metadata = metadata["Ophys"]["ImagingPlane"][0] + imaging_plane_metadata.update( + device=device_name, + imaging_rate=self.imaging_extractor.get_sampling_frequency(), + ) + two_photon_series_metadata = metadata["Ophys"]["TwoPhotonSeries"][0] + two_photon_series_metadata.update( + description="The volumetric imaging data acquired from the Bruker Two-Photon Microscope.", + unit="px", + format="tiff", + scan_line_rate=1 / float(xml_metadata["scanLinePeriod"]), + ) - two_photon_series_metadata = deepcopy(first_two_photon_series_metadata) + if len(self.streams["channel_streams"]) > 1: + imaging_plane_name = f"ImagingPlane{self._stream_name}" + imaging_plane_metadata.update(name=imaging_plane_name) two_photon_series_metadata.update( - name=f"TwoPhotonSeries{plane_num + 1}", - description=f"The imaging data for plane {plane_num + 1} acquired from the Bruker Two-Photon Microscope.", + name=f"TwoPhotonSeries{self._stream_name}", imaging_plane=imaging_plane_name, - field_of_view=two_photon_series_metadata["field_of_view"].pop(-1), ) - metadata["Ophys"]["TwoPhotonSeries"].append(two_photon_series_metadata) + + microns_per_pixel = xml_metadata["micronsPerPixel"] + x_position_in_meters = float(microns_per_pixel[0]["XAxis"]) / 1e6 + y_position_in_meters = float(microns_per_pixel[1]["YAxis"]) / 1e6 + z_plane_position_in_meters = float(microns_per_pixel[2]["ZAxis"]) / 1e6 + grid_spacing = [y_position_in_meters, x_position_in_meters, z_plane_position_in_meters] + field_of_view = [ + y_position_in_meters * self._image_size[1], + x_position_in_meters * self._image_size[0], + z_plane_position_in_meters, + ] + + imaging_plane_metadata.update( + grid_spacing=grid_spacing, description=f"The plane imaged at {z_plane_position_in_meters} meters depth." + ) + + two_photon_series_metadata.update(field_of_view=field_of_view) return metadata + +class BrukerTiffSinglePlaneImagingInterface(BaseImagingExtractorInterface): + """Data Interface for BrukerTiffSinglePlaneImagingExtractor.""" + + @classmethod + def get_source_schema(cls) -> dict: + source_schema = super().get_source_schema() + source_schema["properties"]["folder_path"][ + "description" + ] = "The path that points to the folder containing the Bruker TIF image files and configuration files." + return source_schema + + @classmethod + def get_streams(cls, folder_path: FolderPathType) -> dict: + from roiextractors import BrukerTiffMultiPlaneImagingExtractor + + streams = BrukerTiffMultiPlaneImagingExtractor.get_streams(folder_path=folder_path) + return streams + + def __init__( + self, + folder_path: FolderPathType, + stream_name: Optional[str] = None, + verbose: bool = True, + ): + """ + Initialize reading of TIFF files. + + Parameters + ---------- + folder_path : FolderPathType + The path to the folder that contains the Bruker TIF image files (.ome.tif) and configuration files (.xml, .env). + stream_name : str, optional + The name of the recording stream (e.g. 'Ch2'). + verbose : bool, default: True + """ + super().__init__( + folder_path=folder_path, + stream_name=stream_name, + verbose=verbose, + ) + self.folder_path = folder_path + self._stream_name = self.imaging_extractor.stream_name.replace("_", "") + self._image_size = self.imaging_extractor.get_image_size() + def get_metadata(self) -> DeepDict: metadata = super().get_metadata() @@ -103,67 +184,39 @@ def get_metadata(self) -> DeepDict: imaging_rate=self.imaging_extractor.get_sampling_frequency(), ) two_photon_series_metadata = metadata["Ophys"]["TwoPhotonSeries"][0] - two_photon_series_description = "Imaging data acquired from the Bruker Two-Photon Microscope." - if len(self._image_size) == 3: - two_photon_series_description = ( - "The volumetric imaging data acquired from the Bruker Two-Photon Microscope." - ) two_photon_series_metadata.update( - description=two_photon_series_description, + description="Imaging data acquired from the Bruker Two-Photon Microscope.", unit="px", format="tiff", scan_line_rate=1 / float(xml_metadata["scanLinePeriod"]), ) + streams = self.get_streams(folder_path=self.folder_path) + if len(streams["channel_streams"]) > 1 or len(streams["plane_streams"]): + imaging_plane_name = f"ImagingPlane{self._stream_name}" + imaging_plane_metadata.update(name=imaging_plane_name) + two_photon_series_metadata.update( + name=f"TwoPhotonSeries{self._stream_name}", + imaging_plane=imaging_plane_name, + ) + microns_per_pixel = xml_metadata["micronsPerPixel"] x_position_in_meters = float(microns_per_pixel[0]["XAxis"]) / 1e6 y_position_in_meters = float(microns_per_pixel[1]["YAxis"]) / 1e6 z_plane_position_in_meters = float(microns_per_pixel[2]["ZAxis"]) / 1e6 grid_spacing = [y_position_in_meters, x_position_in_meters] - if len(self._image_size) == 3: - grid_spacing.append(z_plane_position_in_meters) - - imaging_plane_metadata.update( - grid_spacing=grid_spacing, description=f"The plane imaged at {z_plane_position_in_meters} meters depth." - ) - field_of_view = [ y_position_in_meters * self._image_size[1], x_position_in_meters * self._image_size[0], - z_plane_position_in_meters, # for disjoint planes this should be 2D ] - two_photon_series_metadata.update(field_of_view=field_of_view) - if self.plane_separation_type == "disjoint" and len(self._image_size) == 3: - return self.update_metadata_for_disjoint_planes(metadata=metadata) + if len(streams["plane_streams"]): + num_planes_per_channel_stream = len(list(streams["plane_streams"].values())[0]) + z_plane_position_in_meters /= num_planes_per_channel_stream + imaging_plane_metadata.update( + grid_spacing=grid_spacing, description=f"The plane imaged at {z_plane_position_in_meters} meters depth." + ) + two_photon_series_metadata.update(field_of_view=field_of_view) return metadata - - def add_to_nwbfile( - self, - nwbfile: NWBFile, - metadata: Optional[dict] = None, - photon_series_type: Literal["TwoPhotonSeries", "OnePhotonSeries"] = "TwoPhotonSeries", - stub_test: bool = False, - stub_frames: int = 100, - ): - extractor_image_size = self.imaging_extractor.get_image_size() - if (self.plane_separation_type == "contiguous") or len(extractor_image_size) == 2: - return super().add_to_nwbfile( - nwbfile=nwbfile, - metadata=metadata, - photon_series_type=photon_series_type, - stub_test=stub_test, - stub_frames=stub_frames, - ) - - num_z_planes = self.imaging_extractor.get_image_size()[-1] - for plane_num in range(num_z_planes): - imaging_extractor = self.imaging_extractor.depth_slice(start_plane=plane_num, end_plane=plane_num + 1) - add_imaging( - imaging=imaging_extractor, - nwbfile=nwbfile, - metadata=metadata, - photon_series_type=photon_series_type, - ) diff --git a/tests/test_on_data/test_imaging_interfaces.py b/tests/test_on_data/test_imaging_interfaces.py index da3af8829..abafdc429 100644 --- a/tests/test_on_data/test_imaging_interfaces.py +++ b/tests/test_on_data/test_imaging_interfaces.py @@ -10,7 +10,8 @@ from pynwb import NWBHDF5IO from neuroconv.datainterfaces import ( - BrukerTiffImagingInterface, + BrukerTiffMultiPlaneImagingInterface, + BrukerTiffSinglePlaneImagingInterface, Hdf5ImagingInterface, MicroManagerTiffImagingInterface, MiniscopeImagingInterface, @@ -86,7 +87,7 @@ class TestSbxImagingInterface(ImagingExtractorInterfaceTestMixin, TestCase): class TestBrukerTiffImagingInterface(ImagingExtractorInterfaceTestMixin, TestCase): - data_interface_cls = BrukerTiffImagingInterface + data_interface_cls = BrukerTiffSinglePlaneImagingInterface interface_kwargs = dict( folder_path=str( OPHYS_DATA_PATH / "imaging_datasets" / "BrukerTif" / "NCCR32_2023_02_20_Into_the_void_t_series_baseline-000" @@ -165,12 +166,11 @@ def check_read_nwb(self, nwbfile_path: str): class TestBrukerTiffImagingInterfaceDualPlaneCase(ImagingExtractorInterfaceTestMixin, TestCase): - data_interface_cls = BrukerTiffImagingInterface + data_interface_cls = BrukerTiffMultiPlaneImagingInterface interface_kwargs = dict( folder_path=str( OPHYS_DATA_PATH / "imaging_datasets" / "BrukerTif" / "NCCR32_2022_11_03_IntoTheVoid_t_series-005" ), - plane_separation_type="contiguous", ) save_directory = OUTPUT_PATH @@ -180,19 +180,25 @@ def setUpClass(cls) -> None: cls.num_frames = 5 cls.image_shape = (512, 512, 2) cls.device_metadata = dict(name="BrukerFluorescenceMicroscope", description="Version 5.6.64.400") + cls.available_streams = dict(channel_streams=["Ch2"], plane_streams=dict(Ch2=["Ch2_000001"])) cls.optical_channel_metadata = dict( name="Ch2", emission_lambda=np.NAN, description="An optical channel of the microscope.", ) + def run_custom_checks(self): + # check stream names + streams = self.data_interface_cls.get_streams( + folder_path=self.interface_kwargs["folder_path"], plane_separation_type="contiguous" + ) + self.assertEqual(streams, self.available_streams) + def check_extracted_metadata(self, metadata: dict): self.assertEqual(metadata["NWBFile"]["session_start_time"], datetime(2022, 11, 3, 11, 20, 34)) # self.assertDictEqual(metadata["Ophys"], self.ophys_metadata) def check_read_nwb(self, nwbfile_path: str): - # TODO: also add maybe NwbVolumetricImagingExtractor to support checking volumetric TwoPhotonSeries - with NWBHDF5IO(path=nwbfile_path) as io: nwbfile = io.read() photon_series = nwbfile.acquisition[self.photon_series_name] @@ -201,6 +207,124 @@ def check_read_nwb(self, nwbfile_path: str): self.assertEqual(photon_series.rate, 20.629515014336377) +class TestBrukerTiffImagingInterfaceDualPlaneDisjointCase(ImagingExtractorInterfaceTestMixin, TestCase): + data_interface_cls = BrukerTiffSinglePlaneImagingInterface + interface_kwargs = dict( + folder_path=str( + OPHYS_DATA_PATH / "imaging_datasets" / "BrukerTif" / "NCCR32_2022_11_03_IntoTheVoid_t_series-005" + ), + stream_name="Ch2_000002", + ) + save_directory = OUTPUT_PATH + + @classmethod + def setUpClass(cls) -> None: + cls.photon_series_name = "TwoPhotonSeriesCh2000002" + cls.num_frames = 5 + cls.image_shape = (512, 512) + cls.device_metadata = dict(name="BrukerFluorescenceMicroscope", description="Version 5.6.64.400") + cls.available_streams = dict(channel_streams=["Ch2"], plane_streams=dict(Ch2=["Ch2_000001", "Ch2_000002"])) + cls.optical_channel_metadata = dict( + name="Ch2", + emission_lambda=np.NAN, + description="An optical channel of the microscope.", + ) + + def run_custom_checks(self): + # check stream names + streams = self.data_interface_cls.get_streams(folder_path=self.interface_kwargs["folder_path"]) + self.assertEqual(streams, self.available_streams) + + def check_extracted_metadata(self, metadata: dict): + self.assertEqual(metadata["NWBFile"]["session_start_time"], datetime(2022, 11, 3, 11, 20, 34)) + # self.assertDictEqual(metadata["Ophys"], self.ophys_metadata) + + def check_nwbfile_temporal_alignment(self): + nwbfile_path = str( + self.save_directory / f"{self.data_interface_cls.__name__}_{self.case}_test_starting_time_alignment.nwb" + ) + + interface = self.data_interface_cls(**self.test_kwargs) + + aligned_starting_time = 1.23 + interface.set_aligned_starting_time(aligned_starting_time=aligned_starting_time) + + metadata = interface.get_metadata() + interface.run_conversion(nwbfile_path=nwbfile_path, overwrite=True, metadata=metadata) + + with NWBHDF5IO(path=nwbfile_path) as io: + nwbfile = io.read() + + assert nwbfile.acquisition[self.photon_series_name].starting_time == aligned_starting_time + + def check_read_nwb(self, nwbfile_path: str): + with NWBHDF5IO(path=nwbfile_path) as io: + nwbfile = io.read() + photon_series = nwbfile.acquisition[self.photon_series_name] + self.assertEqual(photon_series.data.shape, (self.num_frames, *self.image_shape)) + assert_array_equal(photon_series.dimension[:], self.image_shape) + self.assertEqual(photon_series.rate, 10.314757507168189) + + +class TestBrukerTiffImagingInterfaceDualColorCase(ImagingExtractorInterfaceTestMixin, TestCase): + data_interface_cls = BrukerTiffSinglePlaneImagingInterface + interface_kwargs = dict( + folder_path=str( + OPHYS_DATA_PATH / "imaging_datasets" / "BrukerTif" / "NCCR62_2023_07_06_IntoTheVoid_t_series_Dual_color-000" + ), + stream_name="Ch2", + ) + save_directory = OUTPUT_PATH + + @classmethod + def setUpClass(cls) -> None: + cls.photon_series_name = "TwoPhotonSeriesCh2" + cls.num_frames = 10 + cls.image_shape = (512, 512) + cls.device_metadata = dict(name="BrukerFluorescenceMicroscope", description="Version 5.6.64.400") + cls.available_streams = dict(channel_streams=["Ch1", "Ch2"], plane_streams=dict()) + cls.optical_channel_metadata = dict( + name="Ch2", + emission_lambda=np.NAN, + description="An optical channel of the microscope.", + ) + + def run_custom_checks(self): + # check stream names + streams = self.data_interface_cls.get_streams(folder_path=self.interface_kwargs["folder_path"]) + self.assertEqual(streams, self.available_streams) + + def check_extracted_metadata(self, metadata: dict): + self.assertEqual(metadata["NWBFile"]["session_start_time"], datetime(2023, 7, 6, 15, 13, 58)) + # self.assertDictEqual(metadata["Ophys"], self.ophys_metadata) + + def check_read_nwb(self, nwbfile_path: str): + with NWBHDF5IO(path=nwbfile_path) as io: + nwbfile = io.read() + photon_series = nwbfile.acquisition[self.photon_series_name] + self.assertEqual(photon_series.data.shape, (self.num_frames, *self.image_shape)) + assert_array_equal(photon_series.dimension[:], self.image_shape) + self.assertEqual(photon_series.rate, 29.873615189896864) + + def check_nwbfile_temporal_alignment(self): + nwbfile_path = str( + self.save_directory / f"{self.data_interface_cls.__name__}_{self.case}_test_starting_time_alignment.nwb" + ) + + interface = self.data_interface_cls(**self.test_kwargs) + + aligned_starting_time = 1.23 + interface.set_aligned_starting_time(aligned_starting_time=aligned_starting_time) + + metadata = interface.get_metadata() + interface.run_conversion(nwbfile_path=nwbfile_path, overwrite=True, metadata=metadata) + + with NWBHDF5IO(path=nwbfile_path) as io: + nwbfile = io.read() + + assert nwbfile.acquisition[self.photon_series_name].starting_time == aligned_starting_time + + class TestMicroManagerTiffImagingInterface(ImagingExtractorInterfaceTestMixin, TestCase): data_interface_cls = MicroManagerTiffImagingInterface interface_kwargs = dict( From a545ae13c1de73b693ecdfaf67c251404b074e2c Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Mon, 7 Aug 2023 16:47:54 +0200 Subject: [PATCH 05/20] add converter --- .../ophys/brukertiff/brukertiffconverter.py | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py diff --git a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py new file mode 100644 index 000000000..483ab45e1 --- /dev/null +++ b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py @@ -0,0 +1,190 @@ +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 load volumetric imaging data. The default behavior is to assume the planes are contiguous, + and the imaging plane is a volume. Use 'disjoint' to load one 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. + """ + 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) From 0abd097a1e7fb4b39c78f70ad2ca66014fc537ba Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Mon, 7 Aug 2023 16:49:18 +0200 Subject: [PATCH 06/20] add test for multi plane converter --- src/neuroconv/converters/__init__.py | 4 ++ .../test_brukertiff_converter.py | 68 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 tests/test_on_data/test_format_converters/test_brukertiff_converter.py diff --git a/src/neuroconv/converters/__init__.py b/src/neuroconv/converters/__init__.py index c01576187..6232ac470 100644 --- a/src/neuroconv/converters/__init__.py +++ b/src/neuroconv/converters/__init__.py @@ -1 +1,5 @@ from ..datainterfaces.ecephys.spikeglx.spikeglxconverter import SpikeGLXConverterPipe +from ..datainterfaces.ophys.brukertiff.brukertiffconverter import ( + BrukerTiffMultiPlaneConverter, + BrukerTiffSinglePlaneConverter, +) diff --git a/tests/test_on_data/test_format_converters/test_brukertiff_converter.py b/tests/test_on_data/test_format_converters/test_brukertiff_converter.py new file mode 100644 index 000000000..18eb5dec4 --- /dev/null +++ b/tests/test_on_data/test_format_converters/test_brukertiff_converter.py @@ -0,0 +1,68 @@ +import shutil +import tempfile +from pathlib import Path +from warnings import warn + +from hdmf.testing import TestCase +from pynwb import NWBHDF5IO + +from neuroconv import NWBConverter +from neuroconv.converters import BrukerTiffMultiPlaneConverter +from tests.test_on_data.setup_paths import OPHYS_DATA_PATH + + +class TestBrukerTiffMultiPlaneConverterDisjointPlaneCase(TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.folder_path = str( + OPHYS_DATA_PATH / "imaging_datasets" / "BrukerTif" / "NCCR32_2022_11_03_IntoTheVoid_t_series-005" + ) + cls.converter = BrukerTiffMultiPlaneConverter(folder_path=cls.folder_path, plane_separation_type="disjoint") + cls.test_dir = Path(tempfile.mkdtemp()) + + cls.photon_series_names = ["TwoPhotonSeriesCh2000001", "TwoPhotonSeriesCh2000002"] + cls.imaging_plane_names = ["ImagingPlaneCh2000001", "ImagingPlaneCh2000002"] + cls.stub_frames = 2 + cls.conversion_options = dict(stub_test=True, stub_frames=cls.stub_frames) + + @classmethod + def tearDownClass(cls) -> None: + try: + shutil.rmtree(cls.test_dir) + except PermissionError: + warn(f"Unable to cleanup testing data at {cls.test_dir}! Please remove it manually.") + + def test_run_conversion_add_conversion_options(self): + nwbfile_path = str(self.test_dir / "test_miniscope_converter_conversion_options.nwb") + self.converter.run_conversion( + nwbfile_path=nwbfile_path, + **self.conversion_options, + ) + + with NWBHDF5IO(path=nwbfile_path) as io: + nwbfile = io.read() + + self.assertEqual(len(nwbfile.acquisition), len(self.photon_series_names)) + self.assertEqual(len(nwbfile.imaging_planes), len(self.imaging_plane_names)) + + num_frames = nwbfile.acquisition[self.photon_series_names[0]].data.shape[0] + self.assertEqual(num_frames, self.stub_frames) + + def test_converter_conversion_options(self): + class TestConverter(NWBConverter): + data_interface_classes = dict(TestBrukerTiffConverter=BrukerTiffMultiPlaneConverter) + + nwbfile_path = str(self.test_dir / "test_miniscope_converter_in_nwbconverter_conversion_options.nwb") + converter = TestConverter( + source_data=dict( + TestBrukerTiffConverter=dict(folder_path=self.folder_path, plane_separation_type="disjoint"), + ) + ) + conversion_options = dict(TestBrukerTiffConverter=self.conversion_options) + converter.run_conversion(nwbfile_path=nwbfile_path, conversion_options=conversion_options) + + with NWBHDF5IO(path=nwbfile_path) as io: + nwbfile = io.read() + + num_frames = nwbfile.acquisition[self.photon_series_names[0]].data.shape[0] + self.assertEqual(num_frames, self.stub_frames) From 0ad25181704ac310ac2b41b1f841ecfcbbb8174c Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Mon, 7 Aug 2023 17:44:35 +0200 Subject: [PATCH 07/20] make necessary changes for multi streams --- .../datainterfaces/ophys/baseimagingextractorinterface.py | 2 ++ src/neuroconv/tools/roiextractors/roiextractors.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/neuroconv/datainterfaces/ophys/baseimagingextractorinterface.py b/src/neuroconv/datainterfaces/ophys/baseimagingextractorinterface.py index 22d8d8b8d..64a3fbfaa 100644 --- a/src/neuroconv/datainterfaces/ophys/baseimagingextractorinterface.py +++ b/src/neuroconv/datainterfaces/ophys/baseimagingextractorinterface.py @@ -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, ): @@ -119,4 +120,5 @@ def add_to_nwbfile( nwbfile=nwbfile, metadata=metadata, photon_series_type=photon_series_type, + photon_series_index=photon_series_index, ) diff --git a/src/neuroconv/tools/roiextractors/roiextractors.py b/src/neuroconv/tools/roiextractors/roiextractors.py index f8139d50a..9c0df4798 100644 --- a/src/neuroconv/tools/roiextractors/roiextractors.py +++ b/src/neuroconv/tools/roiextractors/roiextractors.py @@ -360,7 +360,8 @@ def add_photon_series( return nwbfile # Add the image plane to nwb - nwbfile = add_imaging_plane(nwbfile=nwbfile, metadata=metadata_copy) + # TODO: change imaging_plane_index to photon_series_key + nwbfile = add_imaging_plane(nwbfile=nwbfile, metadata=metadata_copy, imaging_plane_index=photon_series_index) imaging_plane_name = photon_series_kwargs["imaging_plane"] imaging_plane = nwbfile.get_imaging_plane(name=imaging_plane_name) photon_series_kwargs.update(imaging_plane=imaging_plane) @@ -483,6 +484,7 @@ def add_imaging( nwbfile: NWBFile, metadata: Optional[dict] = None, photon_series_type: Literal["TwoPhotonSeries", "OnePhotonSeries"] = "TwoPhotonSeries", + photon_series_index: int = 0, iterator_type: Optional[str] = "v2", iterator_options: Optional[dict] = None, ): @@ -492,6 +494,7 @@ def add_imaging( nwbfile=nwbfile, metadata=metadata, photon_series_type=photon_series_type, + photon_series_index=photon_series_index, iterator_type=iterator_type, iterator_options=iterator_options, ) From e0627a92626653ed6cd6cfe26c09d783c98b36bd Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Tue, 8 Aug 2023 14:17:48 +0200 Subject: [PATCH 08/20] fix field_of_view change --- tests/test_on_data/test_imaging_interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_on_data/test_imaging_interfaces.py b/tests/test_on_data/test_imaging_interfaces.py index abafdc429..8d45ce0a2 100644 --- a/tests/test_on_data/test_imaging_interfaces.py +++ b/tests/test_on_data/test_imaging_interfaces.py @@ -123,7 +123,7 @@ def setUpClass(cls) -> None: imaging_plane=cls.imaging_plane_metadata["name"], format="tiff", scan_line_rate=15840.580398865815, - field_of_view=[0.0005672, 0.0005672, 5e-06], + field_of_view=[0.0005672, 0.0005672], ) cls.ophys_metadata = dict( From be2cd7c9814f25f8f81ee59397bc1c16e27fe9fb Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Tue, 8 Aug 2023 16:27:27 +0200 Subject: [PATCH 09/20] modify doc --- .../imaging/brukertiff.rst | 40 ++++++++++++++++--- .../ophys/brukertiff/brukertiffconverter.py | 4 +- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/docs/conversion_examples_gallery/imaging/brukertiff.rst b/docs/conversion_examples_gallery/imaging/brukertiff.rst index 266704374..7e64ad842 100644 --- a/docs/conversion_examples_gallery/imaging/brukertiff.rst +++ b/docs/conversion_examples_gallery/imaging/brukertiff.rst @@ -7,19 +7,49 @@ Install NeuroConv with the additional dependencies necessary for reading Bruker pip install neuroconv[brukertiff] +**Convert Bruker 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) + NWB file saved at ... + + +**Convert Bruker multiple imaging planes** + +Convert volumetric Bruker TIFF imaging data to NWB using +:py:class:`~neuroconv.converters.BrukerTiffMultiPlaneConverter`. +The `plane_separation_type` defined how to handle 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") @@ -27,5 +57,5 @@ Convert Bruker TIFF imaging data to NWB using >>> >>> # 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) + >>> converter.run_conversion(nwbfile_path=nwbfile_path, metadata=metadata) NWB file saved at ... diff --git a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py index 483ab45e1..0e6d22003 100644 --- a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py +++ b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py @@ -33,8 +33,8 @@ def __init__( 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 load volumetric imaging data. The default behavior is to assume the planes are contiguous, - and the imaging plane is a volume. Use 'disjoint' to load one plane. + 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. """ From 445cf64bcfdd6e17b96cc9fd7a4f58b315b0e611 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Tue, 8 Aug 2023 16:27:27 +0200 Subject: [PATCH 10/20] modify doc --- docs/api/interfaces.ophys.rst | 2 +- .../imaging/brukertiff.rst | 40 ++++++++++++++++--- .../ophys/brukertiff/brukertiffconverter.py | 4 +- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/docs/api/interfaces.ophys.rst b/docs/api/interfaces.ophys.rst index d13c9404b..4664a82cc 100644 --- a/docs/api/interfaces.ophys.rst +++ b/docs/api/interfaces.ophys.rst @@ -7,7 +7,7 @@ Base Imaging Bruker Tiff Imaging ------------------- -.. automodule:: neuroconv.datainterfaces.ophys.brukertiff.brukertiffdatainterface +.. automodule:: neuroconv.datainterfaces.ophys.brukertiff.brukertiffconverter HDF5 Imaging ------------ diff --git a/docs/conversion_examples_gallery/imaging/brukertiff.rst b/docs/conversion_examples_gallery/imaging/brukertiff.rst index 266704374..7e64ad842 100644 --- a/docs/conversion_examples_gallery/imaging/brukertiff.rst +++ b/docs/conversion_examples_gallery/imaging/brukertiff.rst @@ -7,19 +7,49 @@ Install NeuroConv with the additional dependencies necessary for reading Bruker pip install neuroconv[brukertiff] +**Convert Bruker 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) + NWB file saved at ... + + +**Convert Bruker multiple imaging planes** + +Convert volumetric Bruker TIFF imaging data to NWB using +:py:class:`~neuroconv.converters.BrukerTiffMultiPlaneConverter`. +The `plane_separation_type` defined how to handle 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") @@ -27,5 +57,5 @@ Convert Bruker TIFF imaging data to NWB using >>> >>> # 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) + >>> converter.run_conversion(nwbfile_path=nwbfile_path, metadata=metadata) NWB file saved at ... diff --git a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py index 483ab45e1..0e6d22003 100644 --- a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py +++ b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py @@ -33,8 +33,8 @@ def __init__( 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 load volumetric imaging data. The default behavior is to assume the planes are contiguous, - and the imaging plane is a volume. Use 'disjoint' to load one plane. + 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. """ From 231ecb6ce42251cb58dac6a1c63bb7e4ea2853a9 Mon Sep 17 00:00:00 2001 From: Szonja Weigl Date: Wed, 16 Aug 2023 11:45:46 +0200 Subject: [PATCH 11/20] Apply suggestions from code review Co-authored-by: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> --- .../datainterfaces/ophys/brukertiff/brukertiffdatainterface.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py index 8c850ad2f..155b55eb6 100644 --- a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py +++ b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py @@ -87,8 +87,6 @@ def get_metadata(self) -> DeepDict: two_photon_series_metadata = metadata["Ophys"]["TwoPhotonSeries"][0] two_photon_series_metadata.update( description="The volumetric imaging data acquired from the Bruker Two-Photon Microscope.", - unit="px", - format="tiff", scan_line_rate=1 / float(xml_metadata["scanLinePeriod"]), ) From ba0a6dd9d005416280df8d62387fd1bb41fb08d9 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Wed, 16 Aug 2023 11:50:13 +0200 Subject: [PATCH 12/20] fix tests --- .../ophys/brukertiff/brukertiffdatainterface.py | 5 ----- tests/test_on_data/test_imaging_interfaces.py | 4 +--- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py index 155b55eb6..a77ab9839 100644 --- a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py +++ b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py @@ -1,9 +1,6 @@ -from copy import deepcopy from typing import Literal, Optional -from warnings import warn from dateutil.parser import parse -from pynwb import NWBFile from ..baseimagingextractorinterface import BaseImagingExtractorInterface from ....utils import FolderPathType @@ -185,8 +182,6 @@ def get_metadata(self) -> DeepDict: two_photon_series_metadata.update( description="Imaging data acquired from the Bruker Two-Photon Microscope.", - unit="px", - format="tiff", scan_line_rate=1 / float(xml_metadata["scanLinePeriod"]), ) diff --git a/tests/test_on_data/test_imaging_interfaces.py b/tests/test_on_data/test_imaging_interfaces.py index 8d45ce0a2..e4fc2efc7 100644 --- a/tests/test_on_data/test_imaging_interfaces.py +++ b/tests/test_on_data/test_imaging_interfaces.py @@ -118,10 +118,9 @@ def setUpClass(cls) -> None: cls.two_photon_series_metadata = dict( name="TwoPhotonSeries", description="Imaging data acquired from the Bruker Two-Photon Microscope.", - unit="px", + unit="n.a.", dimension=[512, 512], imaging_plane=cls.imaging_plane_metadata["name"], - format="tiff", scan_line_rate=15840.580398865815, field_of_view=[0.0005672, 0.0005672], ) @@ -158,7 +157,6 @@ def check_read_nwb(self, nwbfile_path: str): two_photon_series = nwbfile.acquisition[self.two_photon_series_metadata["name"]] self.assertEqual(two_photon_series.description, self.two_photon_series_metadata["description"]) self.assertEqual(two_photon_series.unit, self.two_photon_series_metadata["unit"]) - self.assertEqual(two_photon_series.format, self.two_photon_series_metadata["format"]) self.assertEqual(two_photon_series.scan_line_rate, self.two_photon_series_metadata["scan_line_rate"]) assert_array_equal(two_photon_series.field_of_view[:], self.two_photon_series_metadata["field_of_view"]) From 205fb77e2e309a8da1dc33fe1acba55ca5ed69a4 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Wed, 16 Aug 2023 14:18:55 +0200 Subject: [PATCH 13/20] remove roiextractors pin --- .github/workflows/dev-testing.yml | 5 ++--- src/neuroconv/datainterfaces/ophys/requirements.txt | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dev-testing.yml b/.github/workflows/dev-testing.yml index fa8efc0e2..4f90f64d6 100644 --- a/.github/workflows/dev-testing.yml +++ b/.github/workflows/dev-testing.yml @@ -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 diff --git a/src/neuroconv/datainterfaces/ophys/requirements.txt b/src/neuroconv/datainterfaces/ophys/requirements.txt index e6dcb188d..287addd51 100644 --- a/src/neuroconv/datainterfaces/ophys/requirements.txt +++ b/src/neuroconv/datainterfaces/ophys/requirements.txt @@ -1 +1 @@ -roiextractors==0.5.3 +roiextractors>=0.5.3 From 26e0f43d247a5fb5739a245f325a12a212257497 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Wed, 16 Aug 2023 14:51:49 +0200 Subject: [PATCH 14/20] fix doc --- docs/conversion_examples_gallery/imaging/brukertiff.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/conversion_examples_gallery/imaging/brukertiff.rst b/docs/conversion_examples_gallery/imaging/brukertiff.rst index 7e64ad842..7739aa6ef 100644 --- a/docs/conversion_examples_gallery/imaging/brukertiff.rst +++ b/docs/conversion_examples_gallery/imaging/brukertiff.rst @@ -30,7 +30,6 @@ Convert Bruker TIFF imaging data to NWB using >>> # 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) - NWB file saved at ... **Convert Bruker multiple imaging planes** @@ -58,4 +57,3 @@ Use "contiguous" to create the volumetric two photon series, and "disjoint" to c >>> # 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) - NWB file saved at ... From 30fab1c7e470e6c225fc57f04422d26759ae620d Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Wed, 16 Aug 2023 15:19:19 +0200 Subject: [PATCH 15/20] extend metadata check for dual color --- tests/test_on_data/test_imaging_interfaces.py | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/tests/test_on_data/test_imaging_interfaces.py b/tests/test_on_data/test_imaging_interfaces.py index e4fc2efc7..31048c852 100644 --- a/tests/test_on_data/test_imaging_interfaces.py +++ b/tests/test_on_data/test_imaging_interfaces.py @@ -279,13 +279,40 @@ def setUpClass(cls) -> None: cls.photon_series_name = "TwoPhotonSeriesCh2" cls.num_frames = 10 cls.image_shape = (512, 512) - cls.device_metadata = dict(name="BrukerFluorescenceMicroscope", description="Version 5.6.64.400") + cls.device_metadata = dict(name="BrukerFluorescenceMicroscope", description="Version 5.8.64.200") cls.available_streams = dict(channel_streams=["Ch1", "Ch2"], plane_streams=dict()) cls.optical_channel_metadata = dict( name="Ch2", emission_lambda=np.NAN, description="An optical channel of the microscope.", ) + cls.imaging_plane_metadata = dict( + name="ImagingPlaneCh2", + description="The plane imaged at 5e-06 meters depth.", + excitation_lambda=np.NAN, + indicator="unknown", + location="unknown", + device=cls.device_metadata["name"], + optical_channel=[cls.optical_channel_metadata], + imaging_rate=29.873615189896864, + grid_spacing=[1.1078125e-06, 1.1078125e-06], + ) + + cls.two_photon_series_metadata = dict( + name=cls.photon_series_name, + description="Imaging data acquired from the Bruker Two-Photon Microscope.", + unit="n.a.", + dimension=[512, 512], + imaging_plane=cls.imaging_plane_metadata["name"], + scan_line_rate=15835.56350852745, + field_of_view=[0.0005672, 0.0005672], + ) + + cls.ophys_metadata = dict( + Device=[cls.device_metadata], + ImagingPlane=[cls.imaging_plane_metadata], + TwoPhotonSeries=[cls.two_photon_series_metadata], + ) def run_custom_checks(self): # check stream names @@ -294,7 +321,7 @@ def run_custom_checks(self): def check_extracted_metadata(self, metadata: dict): self.assertEqual(metadata["NWBFile"]["session_start_time"], datetime(2023, 7, 6, 15, 13, 58)) - # self.assertDictEqual(metadata["Ophys"], self.ophys_metadata) + self.assertDictEqual(metadata["Ophys"], self.ophys_metadata) def check_read_nwb(self, nwbfile_path: str): with NWBHDF5IO(path=nwbfile_path) as io: From 51203671d9e031c4035817ff2429ad02d2def10e Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 17 Aug 2023 14:44:24 +0200 Subject: [PATCH 16/20] add volumetric check for single plane converter --- .../datainterfaces/ophys/brukertiff/brukertiffconverter.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py index 0e6d22003..cbb819d72 100644 --- a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py +++ b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffconverter.py @@ -134,6 +134,13 @@ def __init__( 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() From c78d774e100bd0be69547fc62d1d4c62e5bbf107 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 17 Aug 2023 14:52:26 +0200 Subject: [PATCH 17/20] update position current values --- .../brukertiff/brukertiffdatainterface.py | 138 +++++++++++++++--- 1 file changed, 119 insertions(+), 19 deletions(-) diff --git a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py index a77ab9839..46f713001 100644 --- a/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py +++ b/src/neuroconv/datainterfaces/ophys/brukertiff/brukertiffdatainterface.py @@ -1,4 +1,4 @@ -from typing import Literal, Optional +from typing import List, Literal, Optional from dateutil.parser import parse @@ -27,16 +27,17 @@ def get_streams( from roiextractors import BrukerTiffMultiPlaneImagingExtractor streams = BrukerTiffMultiPlaneImagingExtractor.get_streams(folder_path=folder_path) - channel_stream_name = streams["channel_streams"][0] - if plane_separation_type == "contiguous": - streams["plane_streams"].update({channel_stream_name: [streams["plane_streams"][channel_stream_name][0]]}) + for channel_stream_name in streams["channel_streams"]: + if plane_separation_type == "contiguous": + streams["plane_streams"].update( + {channel_stream_name: [streams["plane_streams"][channel_stream_name][0]]} + ) return streams def __init__( self, folder_path: FolderPathType, stream_name: Optional[str] = None, - plane_separation_type: Literal["contiguous", "disjoint"] = None, verbose: bool = True, ): """ @@ -48,12 +49,9 @@ def __init__( The path to the folder that contains the Bruker TIF image files (.ome.tif) and configuration files (.xml, .env). stream_name : str, optional The name of the recording stream (e.g. 'Ch2'). - plane_separation_type: {'contiguous', 'disjoint'} - Defines how to write volumetric imaging data. The default behavior is to assume the planes are contiguous, - and the imaging plane is a volume. Use 'disjoint' for writing them as a separate plane. verbose : bool, default: True """ - self.streams = self.get_streams(folder_path=folder_path, plane_separation_type=plane_separation_type) + self.folder_path = folder_path super().__init__( folder_path=folder_path, stream_name=stream_name, @@ -62,6 +60,58 @@ def __init__( self._stream_name = self.imaging_extractor.stream_name.replace("_", "") self._image_size = self.imaging_extractor.get_image_size() + def _determine_position_current(self) -> List[float]: + """ + Returns y, x, and z position values. The unit of values is in the microscope reference frame. + """ + from roiextractors.extractors.tiffimagingextractors.brukertiffimagingextractor import ( + _parse_xml, + ) + + streams = self.get_streams(folder_path=self.folder_path) + channel_stream_name = streams["channel_streams"][0] + plane_streams_per_channel = streams["plane_streams"][channel_stream_name] + + # general positionCurrent + position_values = [] + xml_root_element = _parse_xml(folder_path=self.folder_path) + default_position_element = xml_root_element.find(".//PVStateValue[@key='positionCurrent']") + + for index_value in ["YAxis", "XAxis"]: + position_sub_indexed_values = default_position_element.find(f"./SubindexedValues[@index='{index_value}']") + for position_sub_indexed_value in position_sub_indexed_values: + position_values.append(float(position_sub_indexed_value.attrib["value"])) + + z_plane_values = [] + for plane_stream in plane_streams_per_channel: + frames_per_stream = [ + frame + for frame in xml_root_element.findall(".//Frame") + for file in frame.findall("File") + if plane_stream in file.attrib["filename"] + ] + + # The frames for each plane will have the same positionCurrent values + position_element = frames_per_stream[0].find(".//PVStateValue[@key='positionCurrent']") + default_z_position_values = default_position_element.find(f"./SubindexedValues[@index='ZAxis']") + z_positions = [] + for z_sub_indexed_value in default_z_position_values: + z_value = float(z_sub_indexed_value.attrib["value"]) + z_positions.append(z_value) + + z_position_values = position_element.find("./SubindexedValues[@index='ZAxis']") + for z_device_ind, z_position_value in enumerate(z_position_values): + z_value = float(z_position_value.attrib["value"]) + # find the changing z position value + if z_positions[z_device_ind] != z_value: + z_plane_values.append(z_value) + + # difference between start position and end position of the z scan + if len(z_plane_values) > 1: + z_value = abs(z_plane_values[0] - z_plane_values[-1]) + position_values.append(z_value) + return position_values + def get_metadata(self) -> DeepDict: metadata = super().get_metadata() @@ -87,7 +137,8 @@ def get_metadata(self) -> DeepDict: scan_line_rate=1 / float(xml_metadata["scanLinePeriod"]), ) - if len(self.streams["channel_streams"]) > 1: + streams = self.get_streams(folder_path=self.folder_path, plane_separation_type="contiguous") + if len(streams["channel_streams"]) > 1: imaging_plane_name = f"ImagingPlane{self._stream_name}" imaging_plane_metadata.update(name=imaging_plane_name) two_photon_series_metadata.update( @@ -98,16 +149,20 @@ def get_metadata(self) -> DeepDict: microns_per_pixel = xml_metadata["micronsPerPixel"] x_position_in_meters = float(microns_per_pixel[0]["XAxis"]) / 1e6 y_position_in_meters = float(microns_per_pixel[1]["YAxis"]) / 1e6 - z_plane_position_in_meters = float(microns_per_pixel[2]["ZAxis"]) / 1e6 - grid_spacing = [y_position_in_meters, x_position_in_meters, z_plane_position_in_meters] + + origin_coords = self._determine_position_current() + z_plane_current_position_in_meters = abs(origin_coords[-1]) / 1e6 + grid_spacing = [y_position_in_meters, x_position_in_meters, z_plane_current_position_in_meters] field_of_view = [ y_position_in_meters * self._image_size[1], x_position_in_meters * self._image_size[0], - z_plane_position_in_meters, + z_plane_current_position_in_meters, ] imaging_plane_metadata.update( - grid_spacing=grid_spacing, description=f"The plane imaged at {z_plane_position_in_meters} meters depth." + grid_spacing=grid_spacing, + origin_coords=origin_coords, + description=f"The imaging plane origin_coords units are in the microscope reference frame.", ) two_photon_series_metadata.update(field_of_view=field_of_view) @@ -155,10 +210,51 @@ def __init__( stream_name=stream_name, verbose=verbose, ) + self._determine_position_current() self.folder_path = folder_path self._stream_name = self.imaging_extractor.stream_name.replace("_", "") self._image_size = self.imaging_extractor.get_image_size() + def _determine_position_current(self) -> List[float]: + """ + Returns y, x, and z position values. The unit of values is in the microscope reference frame. + """ + stream_name = self.imaging_extractor.stream_name + frames_per_stream = [ + frame + for frame in self.imaging_extractor._xml_root.findall(".//Frame") + for file in frame.findall("File") + if stream_name in file.attrib["filename"] + ] + + # general positionCurrent + position_values = [] + default_position_element = self.imaging_extractor._xml_root.find(".//PVStateValue[@key='positionCurrent']") + + for index_value in ["YAxis", "XAxis"]: + position_sub_indexed_values = default_position_element.find(f"./SubindexedValues[@index='{index_value}']") + for position_sub_indexed_value in position_sub_indexed_values: + position_values.append(float(position_sub_indexed_value.attrib["value"])) + + # The frames for each plane will have the same positionCurrent values + position_element = frames_per_stream[0].find(".//PVStateValue[@key='positionCurrent']") + if not position_element: + return position_values + + default_z_position_values = default_position_element.find(f"./SubindexedValues[@index='ZAxis']") + z_positions = [] + for z_sub_indexed_value in default_z_position_values: + z_positions.append(float(z_sub_indexed_value.attrib["value"])) + + z_position_values = position_element.find("./SubindexedValues[@index='ZAxis']") + for z_device_ind, z_position_value in enumerate(z_position_values): + z_value = float(z_position_value.attrib["value"]) + # find the changing z position value + if z_positions[z_device_ind] != z_value: + position_values.append(z_value) + + return position_values + def get_metadata(self) -> DeepDict: metadata = super().get_metadata() @@ -197,18 +293,22 @@ def get_metadata(self) -> DeepDict: microns_per_pixel = xml_metadata["micronsPerPixel"] x_position_in_meters = float(microns_per_pixel[0]["XAxis"]) / 1e6 y_position_in_meters = float(microns_per_pixel[1]["YAxis"]) / 1e6 - z_plane_position_in_meters = float(microns_per_pixel[2]["ZAxis"]) / 1e6 grid_spacing = [y_position_in_meters, x_position_in_meters] + origin_coords = self._determine_position_current() field_of_view = [ y_position_in_meters * self._image_size[1], x_position_in_meters * self._image_size[0], ] - if len(streams["plane_streams"]): - num_planes_per_channel_stream = len(list(streams["plane_streams"].values())[0]) - z_plane_position_in_meters /= num_planes_per_channel_stream + if len(streams["plane_streams"]) and len(origin_coords) == 3: + z_plane_current_position_in_meters = abs(origin_coords[-1]) / 1e6 + grid_spacing.append(z_plane_current_position_in_meters) + field_of_view.append(z_plane_current_position_in_meters) + imaging_plane_metadata.update( - grid_spacing=grid_spacing, description=f"The plane imaged at {z_plane_position_in_meters} meters depth." + grid_spacing=grid_spacing, + origin_coords=origin_coords, + description="The imaging plane origin_coords units are in the microscope reference frame.", ) two_photon_series_metadata.update(field_of_view=field_of_view) From 26915eb34eff68e2e900c3c3b1036cd5e50aa1db Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 17 Aug 2023 14:53:02 +0200 Subject: [PATCH 18/20] update metadata tests --- tests/test_on_data/test_imaging_interfaces.py | 66 +++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/tests/test_on_data/test_imaging_interfaces.py b/tests/test_on_data/test_imaging_interfaces.py index 31048c852..833b4ec1c 100644 --- a/tests/test_on_data/test_imaging_interfaces.py +++ b/tests/test_on_data/test_imaging_interfaces.py @@ -105,7 +105,7 @@ def setUpClass(cls) -> None: ) cls.imaging_plane_metadata = dict( name="ImagingPlane", - description="The plane imaged at 5e-06 meters depth.", + description="The imaging plane origin_coords units are in the microscope reference frame.", excitation_lambda=np.NAN, indicator="unknown", location="unknown", @@ -113,6 +113,7 @@ def setUpClass(cls) -> None: optical_channel=[cls.optical_channel_metadata], imaging_rate=29.873732099062256, grid_spacing=[1.1078125e-06, 1.1078125e-06], + origin_coords=[0.0, 0.0], ) cls.two_photon_series_metadata = dict( @@ -184,6 +185,34 @@ def setUpClass(cls) -> None: emission_lambda=np.NAN, description="An optical channel of the microscope.", ) + cls.imaging_plane_metadata = dict( + name="ImagingPlane", + description="The imaging plane origin_coords units are in the microscope reference frame.", + excitation_lambda=np.NAN, + indicator="unknown", + location="unknown", + device=cls.device_metadata["name"], + optical_channel=[cls.optical_channel_metadata], + imaging_rate=20.629515014336377, + grid_spacing=[1.1078125e-06, 1.1078125e-06, 0.00026], + origin_coords=[56.215, 14.927, 260.0], + ) + + cls.two_photon_series_metadata = dict( + name="TwoPhotonSeries", + description="The volumetric imaging data acquired from the Bruker Two-Photon Microscope.", + unit="n.a.", + dimension=[512, 512, 2], + imaging_plane=cls.imaging_plane_metadata["name"], + scan_line_rate=15842.086085895791, + field_of_view=[0.0005672, 0.0005672, 0.00026], + ) + + cls.ophys_metadata = dict( + Device=[cls.device_metadata], + ImagingPlane=[cls.imaging_plane_metadata], + TwoPhotonSeries=[cls.two_photon_series_metadata], + ) def run_custom_checks(self): # check stream names @@ -194,7 +223,7 @@ def run_custom_checks(self): def check_extracted_metadata(self, metadata: dict): self.assertEqual(metadata["NWBFile"]["session_start_time"], datetime(2022, 11, 3, 11, 20, 34)) - # self.assertDictEqual(metadata["Ophys"], self.ophys_metadata) + self.assertDictEqual(metadata["Ophys"], self.ophys_metadata) def check_read_nwb(self, nwbfile_path: str): with NWBHDF5IO(path=nwbfile_path) as io: @@ -227,6 +256,34 @@ def setUpClass(cls) -> None: emission_lambda=np.NAN, description="An optical channel of the microscope.", ) + cls.imaging_plane_metadata = dict( + name="ImagingPlaneCh2000002", + description="The imaging plane origin_coords units are in the microscope reference frame.", + excitation_lambda=np.NAN, + indicator="unknown", + location="unknown", + device=cls.device_metadata["name"], + optical_channel=[cls.optical_channel_metadata], + imaging_rate=10.314757507168189, + grid_spacing=[1.1078125e-06, 1.1078125e-06, 0.00013], + origin_coords=[56.215, 14.927, 130.0], + ) + + cls.two_photon_series_metadata = dict( + name=cls.photon_series_name, + description="Imaging data acquired from the Bruker Two-Photon Microscope.", + unit="n.a.", + dimension=[512, 512], + imaging_plane=cls.imaging_plane_metadata["name"], + scan_line_rate=15842.086085895791, + field_of_view=[0.0005672, 0.0005672, 0.00013], + ) + + cls.ophys_metadata = dict( + Device=[cls.device_metadata], + ImagingPlane=[cls.imaging_plane_metadata], + TwoPhotonSeries=[cls.two_photon_series_metadata], + ) def run_custom_checks(self): # check stream names @@ -235,7 +292,7 @@ def run_custom_checks(self): def check_extracted_metadata(self, metadata: dict): self.assertEqual(metadata["NWBFile"]["session_start_time"], datetime(2022, 11, 3, 11, 20, 34)) - # self.assertDictEqual(metadata["Ophys"], self.ophys_metadata) + self.assertDictEqual(metadata["Ophys"], self.ophys_metadata) def check_nwbfile_temporal_alignment(self): nwbfile_path = str( @@ -288,7 +345,7 @@ def setUpClass(cls) -> None: ) cls.imaging_plane_metadata = dict( name="ImagingPlaneCh2", - description="The plane imaged at 5e-06 meters depth.", + description="The imaging plane origin_coords units are in the microscope reference frame.", excitation_lambda=np.NAN, indicator="unknown", location="unknown", @@ -296,6 +353,7 @@ def setUpClass(cls) -> None: optical_channel=[cls.optical_channel_metadata], imaging_rate=29.873615189896864, grid_spacing=[1.1078125e-06, 1.1078125e-06], + origin_coords=[0.0, 0.0], ) cls.two_photon_series_metadata = dict( From 599d13d582499dd71d2fad6676afcf80d441fe60 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 17 Aug 2023 14:53:14 +0200 Subject: [PATCH 19/20] add more converter test --- .../test_brukertiff_converter.py | 151 +++++++++++++++++- 1 file changed, 147 insertions(+), 4 deletions(-) diff --git a/tests/test_on_data/test_format_converters/test_brukertiff_converter.py b/tests/test_on_data/test_format_converters/test_brukertiff_converter.py index 18eb5dec4..c2f1f8ae2 100644 --- a/tests/test_on_data/test_format_converters/test_brukertiff_converter.py +++ b/tests/test_on_data/test_format_converters/test_brukertiff_converter.py @@ -4,10 +4,14 @@ from warnings import warn from hdmf.testing import TestCase +from numpy.testing import assert_array_equal from pynwb import NWBHDF5IO from neuroconv import NWBConverter from neuroconv.converters import BrukerTiffMultiPlaneConverter +from neuroconv.datainterfaces.ophys.brukertiff.brukertiffconverter import ( + BrukerTiffSinglePlaneConverter, +) from tests.test_on_data.setup_paths import OPHYS_DATA_PATH @@ -17,7 +21,8 @@ def setUpClass(cls) -> None: cls.folder_path = str( OPHYS_DATA_PATH / "imaging_datasets" / "BrukerTif" / "NCCR32_2022_11_03_IntoTheVoid_t_series-005" ) - cls.converter = BrukerTiffMultiPlaneConverter(folder_path=cls.folder_path, plane_separation_type="disjoint") + cls.converter_kwargs = dict(folder_path=cls.folder_path, plane_separation_type="disjoint") + cls.converter = BrukerTiffMultiPlaneConverter(**cls.converter_kwargs) cls.test_dir = Path(tempfile.mkdtemp()) cls.photon_series_names = ["TwoPhotonSeriesCh2000001", "TwoPhotonSeriesCh2000002"] @@ -32,8 +37,18 @@ def tearDownClass(cls) -> None: except PermissionError: warn(f"Unable to cleanup testing data at {cls.test_dir}! Please remove it manually.") + def test_volumetric_imaging_raises_with_single_plane_converter(self): + exc_msg = "For volumetric imaging data use BrukerTiffMultiPlaneConverter." + with self.assertRaisesWith(ValueError, exc_msg=exc_msg): + BrukerTiffSinglePlaneConverter(folder_path=self.folder_path) + + def test_incorrect_plane_separation_type_raises(self): + exc_msg = "For volumetric imaging data the plane separation method must be one of 'disjoint' or 'contiguous'." + with self.assertRaisesWith(ValueError, exc_msg=exc_msg): + BrukerTiffMultiPlaneConverter(folder_path=self.folder_path, plane_separation_type="test") + def test_run_conversion_add_conversion_options(self): - nwbfile_path = str(self.test_dir / "test_miniscope_converter_conversion_options.nwb") + nwbfile_path = str(self.test_dir / "test_brukertiff_converter_conversion_options.nwb") self.converter.run_conversion( nwbfile_path=nwbfile_path, **self.conversion_options, @@ -41,8 +56,16 @@ def test_run_conversion_add_conversion_options(self): with NWBHDF5IO(path=nwbfile_path) as io: nwbfile = io.read() + first_imaging_plane = nwbfile.imaging_planes[self.imaging_plane_names[0]] + first_origin_coords = first_imaging_plane.origin_coords[:] + + second_imaging_plane = nwbfile.imaging_planes[self.imaging_plane_names[1]] + second_origin_coords = second_imaging_plane.origin_coords[:] self.assertEqual(len(nwbfile.acquisition), len(self.photon_series_names)) + assert_array_equal(first_origin_coords, [56.215, 14.927, -130.0]) + assert_array_equal(second_origin_coords, [56.215, 14.927, 130.0]) + self.assertEqual(len(nwbfile.imaging_planes), len(self.imaging_plane_names)) num_frames = nwbfile.acquisition[self.photon_series_names[0]].data.shape[0] @@ -52,10 +75,130 @@ def test_converter_conversion_options(self): class TestConverter(NWBConverter): data_interface_classes = dict(TestBrukerTiffConverter=BrukerTiffMultiPlaneConverter) - nwbfile_path = str(self.test_dir / "test_miniscope_converter_in_nwbconverter_conversion_options.nwb") + nwbfile_path = str(self.test_dir / "test_brukertiff_converter_in_nwbconverter_conversion_options.nwb") + converter = TestConverter( + source_data=dict( + TestBrukerTiffConverter=self.converter_kwargs, + ) + ) + conversion_options = dict(TestBrukerTiffConverter=self.conversion_options) + converter.run_conversion(nwbfile_path=nwbfile_path, conversion_options=conversion_options) + + with NWBHDF5IO(path=nwbfile_path) as io: + nwbfile = io.read() + + num_frames = nwbfile.acquisition[self.photon_series_names[0]].data.shape[0] + self.assertEqual(num_frames, self.stub_frames) + + +class TestBrukerTiffMultiPlaneConverterContiguousPlaneCase(TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.folder_path = str( + OPHYS_DATA_PATH / "imaging_datasets" / "BrukerTif" / "NCCR32_2022_11_03_IntoTheVoid_t_series-005" + ) + cls.converter_kwargs = dict(folder_path=cls.folder_path, plane_separation_type="contiguous") + cls.converter = BrukerTiffMultiPlaneConverter(**cls.converter_kwargs) + cls.test_dir = Path(tempfile.mkdtemp()) + + cls.photon_series_name = "TwoPhotonSeries" + cls.imaging_plane_name = "ImagingPlane" + cls.stub_frames = 2 + cls.conversion_options = dict(stub_test=True, stub_frames=cls.stub_frames) + + @classmethod + def tearDownClass(cls) -> None: + try: + shutil.rmtree(cls.test_dir) + except PermissionError: + warn(f"Unable to cleanup testing data at {cls.test_dir}! Please remove it manually.") + + def test_run_conversion_add_conversion_options(self): + nwbfile_path = str(self.test_dir / "test_brukertiff_volumetric_converter_conversion_options.nwb") + self.converter.run_conversion( + nwbfile_path=nwbfile_path, + **self.conversion_options, + ) + + with NWBHDF5IO(path=nwbfile_path) as io: + nwbfile = io.read() + + self.assertEqual(len(nwbfile.acquisition), 1) + self.assertIn(self.photon_series_name, nwbfile.acquisition) + self.assertEqual(len(nwbfile.imaging_planes), 1) + self.assertIn(self.imaging_plane_name, nwbfile.imaging_planes) + + num_frames = nwbfile.acquisition[self.photon_series_name].data.shape[0] + self.assertEqual(num_frames, self.stub_frames) + + def test_converter_conversion_options(self): + class TestConverter(NWBConverter): + data_interface_classes = dict(TestBrukerTiffConverter=BrukerTiffMultiPlaneConverter) + + nwbfile_path = str( + self.test_dir / "test_brukertiff_volumetric_converter_in_nwbconverter_conversion_options.nwb" + ) + converter = TestConverter( + source_data=dict( + TestBrukerTiffConverter=self.converter_kwargs, + ) + ) + conversion_options = dict(TestBrukerTiffConverter=self.conversion_options) + converter.run_conversion(nwbfile_path=nwbfile_path, conversion_options=conversion_options) + + with NWBHDF5IO(path=nwbfile_path) as io: + nwbfile = io.read() + + num_frames = nwbfile.acquisition[self.photon_series_name].data.shape[0] + self.assertEqual(num_frames, self.stub_frames) + + +class TestBrukerTiffSinglePlaneConverterCase(TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.folder_path = str( + OPHYS_DATA_PATH / "imaging_datasets" / "BrukerTif" / "NCCR62_2023_07_06_IntoTheVoid_t_series_Dual_color-000" + ) + cls.converter = BrukerTiffSinglePlaneConverter(folder_path=cls.folder_path) + cls.test_dir = Path(tempfile.mkdtemp()) + + cls.photon_series_names = ["TwoPhotonSeriesCh1", "TwoPhotonSeriesCh2"] + cls.imaging_plane_names = ["ImagingPlaneCh1", "ImagingPlaneCh2"] + cls.stub_frames = 2 + cls.conversion_options = dict(stub_test=True, stub_frames=cls.stub_frames) + + @classmethod + def tearDownClass(cls) -> None: + try: + shutil.rmtree(cls.test_dir) + except PermissionError: + warn(f"Unable to cleanup testing data at {cls.test_dir}! Please remove it manually.") + + def test_run_conversion_add_conversion_options(self): + nwbfile_path = str(self.test_dir / "test_brukertiff_dualcolor_converter_conversion_options.nwb") + self.converter.run_conversion( + nwbfile_path=nwbfile_path, + **self.conversion_options, + ) + + with NWBHDF5IO(path=nwbfile_path) as io: + nwbfile = io.read() + + self.assertEqual(len(nwbfile.acquisition), 2) + self.assertEqual(len(nwbfile.imaging_planes), 2) + self.assertEqual(len(nwbfile.devices), 1) + + num_frames = nwbfile.acquisition[self.photon_series_names[0]].data.shape[0] + self.assertEqual(num_frames, self.stub_frames) + + def test_converter_conversion_options(self): + class TestConverter(NWBConverter): + data_interface_classes = dict(TestBrukerTiffConverter=BrukerTiffSinglePlaneConverter) + + nwbfile_path = str(self.test_dir / "test_brukertiff_dualcolor_converter_in_nwbconverter_conversion_options.nwb") converter = TestConverter( source_data=dict( - TestBrukerTiffConverter=dict(folder_path=self.folder_path, plane_separation_type="disjoint"), + TestBrukerTiffConverter=dict(folder_path=self.folder_path), ) ) conversion_options = dict(TestBrukerTiffConverter=self.conversion_options) From 05f00abe4e0ffccd892bb1f40e5bcd175ef3f82f Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Thu, 24 Aug 2023 19:38:00 +0200 Subject: [PATCH 20/20] update CHANGELOG.md --- CHANGELOG.md | 9 +++++++++ docs/conversion_examples_gallery/imaging/brukertiff.rst | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bb819667..e7557f52c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/conversion_examples_gallery/imaging/brukertiff.rst b/docs/conversion_examples_gallery/imaging/brukertiff.rst index 7739aa6ef..91b763f50 100644 --- a/docs/conversion_examples_gallery/imaging/brukertiff.rst +++ b/docs/conversion_examples_gallery/imaging/brukertiff.rst @@ -7,7 +7,7 @@ Install NeuroConv with the additional dependencies necessary for reading Bruker pip install neuroconv[brukertiff] -**Convert Bruker single imaging plane** +**Convert single imaging plane** Convert Bruker TIFF imaging data to NWB using :py:class:`~neuroconv.converters.BrukerTiffSinglePlaneConverter`. @@ -32,11 +32,11 @@ Convert Bruker TIFF imaging data to NWB using >>> converter.run_conversion(nwbfile_path=nwbfile_path, metadata=metadata) -**Convert Bruker multiple imaging planes** +**Convert multiple imaging planes** Convert volumetric Bruker TIFF imaging data to NWB using :py:class:`~neuroconv.converters.BrukerTiffMultiPlaneConverter`. -The `plane_separation_type` defined how to handle the imaging planes. +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