import datetime
import json
from pathlib import Path
from typing import Optional
from dateutil.parser import parse as dateparse
from pydantic import DirectoryPath, FilePath, validate_call
from ..baseimagingextractorinterface import BaseImagingExtractorInterface
[docs]class ScanImageImagingInterface(BaseImagingExtractorInterface):
"""
Interface for reading TIFF files produced via ScanImage.
It extracts metadata from the provided TIFF file and determines the ScanImage version.
For the legacy version 3.8, it creates an instance of ScanImageLegacyImagingInterface.
For newer versions, it parses the metadata and determines the number of planes.
If there is more than one plane and no specific plane is provided, it creates an instance of ScanImageMultiPlaneImagingInterface.
If there is only one plane or a specific plane is provided, it creates an instance of ScanImageSinglePlaneImagingInterface.
"""
display_name = "ScanImage Imaging"
associated_suffixes = (".tif",)
info = "Interface for ScanImage TIFF files."
ExtractorName = "ScanImageTiffImagingExtractor"
[docs] @classmethod
def get_source_schema(cls) -> dict:
"""
Get the source schema for the ScanImage imaging interface.
Returns
-------
dict
The schema dictionary containing input parameters and descriptions
for initializing the ScanImage interface.
"""
source_schema = super().get_source_schema()
source_schema["properties"]["file_path"]["description"] = "Path to Tiff file."
return source_schema
@validate_call
def __new__(
cls,
file_path: FilePath,
channel_name: Optional[str] = None,
plane_name: Optional[str] = None,
fallback_sampling_frequency: Optional[float] = None,
verbose: bool = False,
):
from roiextractors.extractors.tiffimagingextractors.scanimagetiff_utils import (
extract_extra_metadata,
parse_metadata,
)
image_metadata = extract_extra_metadata(file_path=file_path)
version = get_scanimage_major_version(scanimage_metadata=image_metadata)
if version == "3.8":
return ScanImageLegacyImagingInterface(
file_path=file_path,
fallback_sampling_frequency=fallback_sampling_frequency,
verbose=verbose,
)
parsed_metadata = parse_metadata(metadata=image_metadata)
available_planes = [f"{i}" for i in range(parsed_metadata["num_planes"])]
if len(available_planes) > 1 and plane_name is None:
return ScanImageMultiPlaneImagingInterface(
file_path=file_path,
channel_name=channel_name,
image_metadata=image_metadata,
parsed_metadata=parsed_metadata,
verbose=verbose,
)
return ScanImageSinglePlaneImagingInterface(
file_path=file_path,
channel_name=channel_name,
plane_name=plane_name,
image_metadata=image_metadata,
parsed_metadata=parsed_metadata,
verbose=verbose,
)
[docs]class ScanImageLegacyImagingInterface(BaseImagingExtractorInterface):
"""Interface for reading TIFF files produced via ScanImage v3.8."""
display_name = "ScanImage Imaging"
associated_suffixes = (".tif",)
info = "Interface for ScanImage v3.8 TIFF files."
ExtractorName = "ScanImageTiffImagingExtractor"
[docs] @classmethod
def get_source_schema(cls) -> dict:
source_schema = super().get_source_schema()
source_schema["properties"]["file_path"]["description"] = "Path to Tiff file."
return source_schema
def _source_data_to_extractor_kwargs(self, source_data: dict) -> dict:
extractor_kwargs = source_data.copy()
extractor_kwargs.pop("fallback_sampling_frequency", None)
extractor_kwargs["sampling_frequency"] = self.sampling_frequency
return extractor_kwargs
@validate_call
def __init__(
self,
file_path: FilePath,
fallback_sampling_frequency: Optional[float] = None,
verbose: bool = False,
):
"""
DataInterface for reading Tiff files that are generated by ScanImage v3.8. This interface extracts the metadata
from the exif of the tiff file.
Parameters
----------
file_path: FilePath
Path to tiff file.
fallback_sampling_frequency: float, optional
The sampling frequency can usually be extracted from the scanimage metadata in
exif:ImageDescription:state.acq.frameRate. If not, use this.
"""
from roiextractors.extractors.tiffimagingextractors.scanimagetiff_utils import (
extract_extra_metadata,
)
self.image_metadata = extract_extra_metadata(file_path=file_path)
if "state.acq.frameRate" in self.image_metadata:
sampling_frequency = float(self.image_metadata["state.acq.frameRate"])
elif "SI.hRoiManager.scanFrameRate" in self.image_metadata:
sampling_frequency = float(self.image_metadata["SI.hRoiManager.scanFrameRate"])
else:
assert_msg = (
"sampling frequency not found in image metadata, "
"input the frequency using the argument `fallback_sampling_frequency`"
)
assert fallback_sampling_frequency is not None, assert_msg
sampling_frequency = fallback_sampling_frequency
self.sampling_frequency = sampling_frequency
super().__init__(file_path=file_path, fallback_sampling_frequency=fallback_sampling_frequency, verbose=verbose)
[docs]class ScanImageMultiFileImagingInterface(BaseImagingExtractorInterface):
"""
Interface for reading multi-file (buffered) TIFF files produced via ScanImage.
It extracts metadata from the first TIFF file in the provided folder and determines the number of available planes.
If there is more than one plane and no specific plane is provided, it creates an instance of ScanImageMultiPlaneMultiFileImagingInterface.
If there is only one plane or a specific plane is provided, it creates an instance of ScanImageSinglePlaneMultiFileImagingInterface.
"""
display_name = "ScanImage Multi-File Imaging"
associated_suffixes = (".tif",)
info = "Interface for ScanImage multi-file (buffered) TIFF files."
ExtractorName = "ScanImageTiffSinglePlaneMultiFileImagingExtractor"
[docs] @classmethod
def get_source_schema(cls) -> dict:
"""
Get the source schema for the ScanImage multi-file imaging interface.
Returns
-------
dict
The schema dictionary containing input parameters and descriptions
for initializing the ScanImage multi-file interface.
"""
source_schema = super().get_source_schema()
source_schema["properties"]["folder_path"]["description"] = "Path to the folder containing the TIFF files."
return source_schema
@validate_call
def __new__(
cls,
folder_path: DirectoryPath,
file_pattern: str,
channel_name: Optional[str] = None,
plane_name: Optional[str] = None,
extract_all_metadata: bool = False,
verbose: bool = False,
):
from natsort import natsorted
from roiextractors.extractors.tiffimagingextractors.scanimagetiff_utils import (
extract_extra_metadata,
parse_metadata,
)
file_paths = natsorted(Path(folder_path).glob(file_pattern))
first_file_path = file_paths[0]
image_metadata = extract_extra_metadata(file_path=first_file_path)
version = get_scanimage_major_version(scanimage_metadata=image_metadata)
if version == "3.8":
raise ValueError("ScanImage version 3.8 is not supported.")
parsed_metadata = parse_metadata(metadata=image_metadata)
available_planes = [f"{i}" for i in range(parsed_metadata["num_planes"])]
if len(available_planes) > 1 and plane_name is None:
return ScanImageMultiPlaneMultiFileImagingInterface(
folder_path=folder_path,
file_pattern=file_pattern,
channel_name=channel_name,
image_metadata=image_metadata,
parsed_metadata=parsed_metadata,
extract_all_metadata=extract_all_metadata,
verbose=verbose,
)
return ScanImageSinglePlaneMultiFileImagingInterface(
folder_path=folder_path,
file_pattern=file_pattern,
channel_name=channel_name,
plane_name=plane_name,
image_metadata=image_metadata,
parsed_metadata=parsed_metadata,
extract_all_metadata=extract_all_metadata,
verbose=verbose,
)
[docs]class ScanImageMultiPlaneImagingInterface(BaseImagingExtractorInterface):
"""Interface for reading multi plane (volumetric) TIFF files produced via ScanImage."""
display_name = "ScanImage Volumetric Imaging"
associated_suffixes = (".tif",)
info = "Interface for ScanImage multi plane (volumetric) TIFF files."
ExtractorName = "ScanImageTiffMultiPlaneImagingExtractor"
def _source_data_to_extractor_kwargs(self, source_data: dict) -> dict:
extractor_kwargs = source_data.copy()
extractor_kwargs.pop("image_metadata")
extractor_kwargs["metadata"] = self.image_metadata
return extractor_kwargs
@validate_call
def __init__(
self,
file_path: FilePath,
channel_name: Optional[str] = None,
image_metadata: Optional[dict] = None,
parsed_metadata: Optional[dict] = None,
verbose: bool = False,
):
"""
DataInterface for reading multi-file (buffered) TIFF files that are generated by ScanImage.
Parameters
----------
file_path : FilePath
Path to the TIFF file.
channel_name : str
Name of the channel for this extractor.
"""
from roiextractors.extractors.tiffimagingextractors.scanimagetiff_utils import (
extract_extra_metadata,
parse_metadata,
)
image_metadata = image_metadata or extract_extra_metadata(file_path=file_path)
self.image_metadata = image_metadata
parsed_metadata = parsed_metadata or parse_metadata(metadata=self.image_metadata)
if parsed_metadata["num_planes"] == 1:
raise ValueError(
"Only one plane detected. For single plane imaging data use ScanImageSinglePlaneImagingInterface instead."
)
available_channels = parsed_metadata["channel_names"]
if channel_name is None:
if len(available_channels) > 1:
raise ValueError(
"More than one channel is detected! \n "
"Please specify which channel you wish to load with the `channel_name` argument \n "
f"Available channels are: {available_channels}"
)
channel_name = available_channels[0]
assert (
channel_name in available_channels
), f"Channel '{channel_name}' not found! \n Available channels are: {available_channels}"
two_photon_series_name_suffix = None
if len(available_channels) > 1:
two_photon_series_name_suffix = f"{channel_name.replace(' ', '')}"
self.two_photon_series_name_suffix = two_photon_series_name_suffix
self.metadata = image_metadata
self.parsed_metadata = parsed_metadata
super().__init__(
file_path=file_path,
channel_name=channel_name,
image_metadata=image_metadata,
parsed_metadata=parsed_metadata,
verbose=verbose,
)
[docs]class ScanImageMultiPlaneMultiFileImagingInterface(BaseImagingExtractorInterface):
"""Interface for reading volumetric multi-file (buffered) TIFF files produced via ScanImage."""
display_name = "ScanImage Volumetric Multi-File Imaging"
associated_suffixes = (".tif",)
info = "Interface for ScanImage multi-file (buffered) volumetric TIFF files."
ExtractorName = "ScanImageTiffMultiPlaneMultiFileImagingExtractor"
@validate_call
def __init__(
self,
folder_path: DirectoryPath,
file_pattern: str,
channel_name: Optional[str] = None,
extract_all_metadata: bool = False,
image_metadata: Optional[dict] = None,
parsed_metadata: Optional[dict] = None,
verbose: bool = False,
):
"""
DataInterface for reading multi-file (buffered) TIFF files that are generated by ScanImage.
Parameters
----------
folder_path : DirectoryPath
Path to the folder containing the TIFF files.
file_pattern : str
Pattern for the TIFF files to read -- see pathlib.Path.glob for details.
channel_name : str
The name of the channel to load, to determine what channels are available use ScanImageTiffSinglePlaneImagingExtractor.get_available_channels(file_path=...).
extract_all_metadata : bool
If True, extract metadata from every file in the folder. If False, only extract metadata from the first
file in the folder. The default is False.
"""
from natsort import natsorted
from roiextractors.extractors.tiffimagingextractors.scanimagetiff_utils import (
extract_extra_metadata,
parse_metadata,
)
file_paths = natsorted(Path(folder_path).glob(file_pattern))
first_file_path = file_paths[0]
image_metadata = image_metadata or extract_extra_metadata(file_path=first_file_path)
self.image_metadata = image_metadata
version = get_scanimage_major_version(scanimage_metadata=image_metadata)
if version == "3.8":
raise ValueError(
"ScanImage version 3.8 is not supported. \n " "Please use ScanImageImagingInterface instead."
)
parsed_metadata = parsed_metadata or parse_metadata(metadata=image_metadata)
if parsed_metadata["num_planes"] == 1:
raise ValueError(
"Only one plane detected. For single plane imaging data use ScanImageSinglePlaneMultiFileImagingInterface instead."
)
available_channels = parsed_metadata["channel_names"]
if channel_name is None:
if len(available_channels) > 1:
raise ValueError(
"More than one channel is detected! \n "
"Please specify which channel you wish to load with the `channel_name` argument \n "
f"Available channels are: {available_channels}"
)
channel_name = available_channels[0]
assert (
channel_name in available_channels
), f"Channel '{channel_name}' not found! \n Available channels are: {available_channels}"
two_photon_series_name_suffix = None
if len(available_channels) > 1:
two_photon_series_name_suffix = f"{channel_name.replace(' ', '')}"
self.two_photon_series_name_suffix = two_photon_series_name_suffix
super().__init__(
folder_path=folder_path,
file_pattern=file_pattern,
channel_name=channel_name,
extract_all_metadata=extract_all_metadata,
verbose=verbose,
)
[docs]class ScanImageSinglePlaneImagingInterface(BaseImagingExtractorInterface):
"""Interface for reading TIFF files produced via ScanImage."""
display_name = "ScanImage Single Plane Imaging"
associated_suffixes = (".tif",)
info = "Interface for ScanImage TIFF files."
ExtractorName = "ScanImageTiffSinglePlaneImagingExtractor"
def _source_data_to_extractor_kwargs(self, source_data: dict) -> dict:
extractor_kwargs = source_data.copy()
extractor_kwargs.pop("image_metadata")
extractor_kwargs["metadata"] = self.image_metadata
return extractor_kwargs
@validate_call
def __init__(
self,
file_path: FilePath,
channel_name: Optional[str] = None,
plane_name: Optional[str] = None,
image_metadata: Optional[dict] = None,
parsed_metadata: Optional[dict] = None,
verbose: bool = False,
):
"""
DataInterface for reading multi-file (buffered) TIFF files that are generated by ScanImage.
Parameters
----------
file_path : FilePath
Path to the TIFF file.
channel_name : str
The name of the channel to load, to determine what channels are available use ScanImageTiffSinglePlaneImagingExtractor.get_available_channels(file_path=...).
plane_name : str
The name of the plane to load, to determine what planes are available use ScanImageTiffSinglePlaneImagingExtractor.get_available_planes(file_path=...).
"""
from roiextractors.extractors.tiffimagingextractors.scanimagetiff_utils import (
extract_extra_metadata,
parse_metadata,
)
image_metadata = image_metadata or extract_extra_metadata(file_path=file_path)
self.image_metadata = image_metadata
version = get_scanimage_major_version(scanimage_metadata=image_metadata)
if version == "3.8":
raise ValueError(
"ScanImage version 3.8 is not supported. \n " "Please use ScanImageImagingInterface instead."
)
parsed_metadata = parsed_metadata or parse_metadata(metadata=image_metadata)
available_channels = parsed_metadata["channel_names"]
if channel_name is None:
if len(available_channels) > 1:
raise ValueError(
"More than one channel is detected! \n "
"Please specify which channel you wish to load with the `channel_name` argument \n "
f"Available channels are: {available_channels}"
)
channel_name = available_channels[0]
assert (
channel_name in available_channels
), f"Channel '{channel_name}' not found! \n Available channels are: {available_channels}"
available_planes = [f"{i}" for i in range(parsed_metadata["num_planes"])]
if plane_name is None:
if len(available_planes) > 1:
raise ValueError(
"More than one plane is detected! \n "
"Please specify which plane you wish to load with the `plane_name` argument \n "
f"Available planes are: {available_planes}"
)
plane_name = available_planes[0]
assert (
plane_name in available_planes
), f"Plane '{plane_name}' not found! \n Available planes are: {available_planes}"
two_photon_series_name_suffix = None
if len(available_channels) > 1:
two_photon_series_name_suffix = f"{channel_name.replace(' ', '')}"
if len(available_planes) > 1:
two_photon_series_name_suffix = f"{two_photon_series_name_suffix}Plane{plane_name}"
self.two_photon_series_name_suffix = two_photon_series_name_suffix
self.metadata = image_metadata
self.parsed_metadata = parsed_metadata
super().__init__(
file_path=file_path,
channel_name=channel_name,
plane_name=plane_name,
image_metadata=image_metadata,
parsed_metadata=parsed_metadata,
verbose=verbose,
)
[docs]class ScanImageSinglePlaneMultiFileImagingInterface(BaseImagingExtractorInterface):
"""Interface for reading multi-file (buffered) TIFF files produced via ScanImage."""
display_name = "ScanImage Single Plane Multi-File Imaging"
associated_suffixes = (".tif",)
info = "Interface for ScanImage multi-file (buffered) TIFF files."
ExtractorName = "ScanImageTiffSinglePlaneMultiFileImagingExtractor"
@validate_call
def __init__(
self,
folder_path: DirectoryPath,
file_pattern: str,
channel_name: Optional[str] = None,
plane_name: Optional[str] = None,
image_metadata: Optional[dict] = None,
parsed_metadata: Optional[dict] = None,
extract_all_metadata: bool = False,
verbose: bool = False,
):
"""
DataInterface for reading multi-file (buffered) TIFF files that are generated by ScanImage.
Parameters
----------
folder_path : DirectoryPath
Path to the folder containing the TIFF files.
file_pattern : str
Pattern for the TIFF files to read -- see pathlib.Path.glob for details.
channel_name : str
The name of the channel to load, to determine what channels are available use ScanImageTiffSinglePlaneImagingExtractor.get_available_channels(file_path=...).
plane_name : str
The name of the plane to load, to determine what planes are available use ScanImageTiffSinglePlaneImagingExtractor.get_available_planes(file_path=...).
extract_all_metadata : bool
If True, extract metadata from every file in the folder. If False, only extract metadata from the first
file in the folder. The default is False.
"""
from natsort import natsorted
from roiextractors.extractors.tiffimagingextractors.scanimagetiff_utils import (
extract_extra_metadata,
parse_metadata,
)
file_paths = natsorted(Path(folder_path).glob(file_pattern))
first_file_path = file_paths[0]
image_metadata = image_metadata or extract_extra_metadata(file_path=first_file_path)
self.image_metadata = image_metadata
version = get_scanimage_major_version(scanimage_metadata=image_metadata)
if version == "3.8":
raise ValueError(
"ScanImage version 3.8 is not supported. \n " "Please use ScanImageImagingInterface instead."
)
parsed_metadata = parsed_metadata or parse_metadata(metadata=image_metadata)
available_channels = parsed_metadata["channel_names"]
if channel_name is None:
if len(available_channels) > 1:
raise ValueError(
"More than one channel is detected! \n "
"Please specify which channel you wish to load with the `channel_name` argument \n "
f"Available channels are: {available_channels}"
)
channel_name = available_channels[0]
assert (
channel_name in available_channels
), f"Channel '{channel_name}' not found! \n Available channels are: {available_channels}"
available_planes = [f"{i}" for i in range(parsed_metadata["num_planes"])]
if plane_name is None:
if len(available_planes) > 1:
raise ValueError(
"More than one plane is detected! \n "
"Please specify which plane you wish to load with the `plane_name` argument \n "
f"Available planes are: {available_planes}"
)
plane_name = available_planes[0]
assert (
plane_name in available_planes
), f"Plane '{plane_name}' not found! \n Available planes are: {available_planes}"
two_photon_series_name_suffix = None
if len(available_channels) > 1:
two_photon_series_name_suffix = f"{channel_name.replace(' ', '')}"
if len(available_planes) > 1:
two_photon_series_name_suffix = f"{two_photon_series_name_suffix}Plane{plane_name}"
self.two_photon_series_name_suffix = two_photon_series_name_suffix
super().__init__(
folder_path=folder_path,
file_pattern=file_pattern,
channel_name=channel_name,
plane_name=plane_name,
extract_all_metadata=extract_all_metadata,
verbose=verbose,
)
[docs]def get_scanimage_major_version(scanimage_metadata: dict) -> str:
"""
Determine the version of ScanImage that produced the TIFF file.
Parameters
----------
scanimage_metadata : dict
Dictionary of metadata extracted from a TIFF file produced via ScanImage.
Returns
-------
version: str
The version of ScanImage that produced the TIFF file.
Raises
------
ValueError
If the ScanImage version could not be determined from metadata.
"""
if "SI.VERSION_MAJOR" in scanimage_metadata:
return scanimage_metadata["SI.VERSION_MAJOR"]
elif "state.software.version" in scanimage_metadata:
return scanimage_metadata["state.software.version"]
raise ValueError("ScanImage version could not be determined from metadata.")