diff --git a/src/schneider_lab_to_nwb/schneider_2024/__init__.py b/src/schneider_lab_to_nwb/schneider_2024/__init__.py index eafd574..8d17765 100644 --- a/src/schneider_lab_to_nwb/schneider_2024/__init__.py +++ b/src/schneider_lab_to_nwb/schneider_2024/__init__.py @@ -1,2 +1,3 @@ from .schneider_2024_behaviorinterface import Schneider2024BehaviorInterface +from .schneider_2024_intrinsic_signal_imaging_interface import Schneider2024IntrinsicSignalOpticalImagingInterface from .schneider_2024_nwbconverter import Schneider2024NWBConverter diff --git a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_behaviorinterface.py b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_behaviorinterface.py index 6d2b818..63b7b97 100644 --- a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_behaviorinterface.py +++ b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_behaviorinterface.py @@ -16,7 +16,7 @@ class Schneider2024BehaviorInterface(BaseDataInterface): """Behavior interface for schneider_2024 conversion""" - keywords = ["behavior"] + keywords = ("behavior",) def __init__(self, file_path: FilePath): super().__init__(file_path=file_path) diff --git a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_convert_all_sessions.py b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_convert_all_sessions.py index 0d59044..8cb5833 100644 --- a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_convert_all_sessions.py +++ b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_convert_all_sessions.py @@ -1,6 +1,5 @@ """Primary script to run to convert all sessions in a dataset using session_to_nwb.""" from pathlib import Path -from typing import Union from concurrent.futures import ProcessPoolExecutor, as_completed from pprint import pformat import traceback @@ -11,8 +10,8 @@ def dataset_to_nwb( *, - data_dir_path: Union[str, Path], - output_dir_path: Union[str, Path], + data_dir_path: str | Path, + output_dir_path: str | Path, max_workers: int = 1, verbose: bool = True, ): @@ -20,9 +19,9 @@ def dataset_to_nwb( Parameters ---------- - data_dir_path : Union[str, Path] + data_dir_path : str | Path The path to the directory containing the raw data. - output_dir_path : Union[str, Path] + output_dir_path : str | Path The path to the directory where the NWB files will be saved. max_workers : int, optional The number of workers to use for parallel processing, by default 1 @@ -39,7 +38,7 @@ def dataset_to_nwb( for session_to_nwb_kwargs in session_to_nwb_kwargs_per_session: session_to_nwb_kwargs["output_dir_path"] = output_dir_path session_to_nwb_kwargs["verbose"] = verbose - exception_file_path = data_dir_path / f"ERROR_.txt" # Add error file path here + exception_file_path = data_dir_path / f"ERROR_.txt" # Add error file path here futures.append( executor.submit( safe_session_to_nwb, @@ -51,7 +50,7 @@ def dataset_to_nwb( pass -def safe_session_to_nwb(*, session_to_nwb_kwargs: dict, exception_file_path: Union[Path, str]): +def safe_session_to_nwb(*, session_to_nwb_kwargs: dict, exception_file_path: str | Path): """Convert a session to NWB while handling any errors by recording error messages to the exception_file_path. Parameters @@ -72,13 +71,13 @@ def safe_session_to_nwb(*, session_to_nwb_kwargs: dict, exception_file_path: Uni def get_session_to_nwb_kwargs_per_session( *, - data_dir_path: Union[str, Path], + data_dir_path: str | Path, ): """Get the kwargs for session_to_nwb for each session in the dataset. Parameters ---------- - data_dir_path : Union[str, Path] + data_dir_path : str | Path The path to the directory containing the raw data. Returns @@ -86,11 +85,11 @@ def get_session_to_nwb_kwargs_per_session( list[dict[str, Any]] A list of dictionaries containing the kwargs for session_to_nwb for each session. """ - ##### - # # Implement this function to return the kwargs for session_to_nwb for each session - # This can be a specific list with hard-coded sessions, a path expansion or any conversion specific logic that you might need - ##### - raise NotImplementedError + ##### + # # Implement this function to return the kwargs for session_to_nwb for each session + # This can be a specific list with hard-coded sessions, a path expansion or any conversion specific logic that you might need + ##### + raise NotImplementedError if __name__ == "__main__": diff --git a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_convert_session.py b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_convert_session.py index a9aa167..8c1792a 100644 --- a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_convert_session.py +++ b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_convert_session.py @@ -1,6 +1,5 @@ """Primary script to run to convert an entire session for of data using the NWBConverter.""" from pathlib import Path -from typing import Union import datetime import pytz from zoneinfo import ZoneInfo @@ -13,17 +12,19 @@ def session_to_nwb( - recording_folder_path: Union[str, Path], - sorting_folder_path: Union[str, Path], - behavior_file_path: Union[str, Path], - video_folder_path: Union[str, Path], - output_dir_path: Union[str, Path], + recording_folder_path: str | Path, + sorting_folder_path: str | Path, + behavior_file_path: str | Path, + video_folder_path: str | Path, + intrinsic_signal_optical_imaging_folder_path: str | Path, + output_dir_path: str | Path, stub_test: bool = False, ): recording_folder_path = Path(recording_folder_path) sorting_folder_path = Path(sorting_folder_path) behavior_file_path = Path(behavior_file_path) video_folder_path = Path(video_folder_path) + intrinsic_signal_optical_imaging_folder_path = Path(intrinsic_signal_optical_imaging_folder_path) output_dir_path = Path(output_dir_path) video_file_paths = [ file_path for file_path in video_folder_path.glob("*.mp4") if not file_path.name.startswith("._") @@ -59,6 +60,10 @@ def session_to_nwb( source_data.update({metadata_key_name: dict(file_paths=[video_file_path], metadata_key_name=metadata_key_name)}) conversion_options.update({metadata_key_name: dict()}) + # Add Intrinsic Signal Optical Imaging + source_data.update(dict(ISOI=dict(folder_path=intrinsic_signal_optical_imaging_folder_path))) + conversion_options.update(dict(ISOI=dict())) + converter = Schneider2024NWBConverter(source_data=source_data) # Add datetime to conversion @@ -115,11 +120,13 @@ def main(): ) behavior_file_path = data_dir_path / "NWB_Share" / "Sample behavior data" / "m74_ephysSample.mat" video_folder_path = data_dir_path / "Schneider sample Data" / "Video" / "m69_231031" + intrinsic_signal_optical_imaging_folder_path = data_dir_path / "NWB_Share" / "Sample Intrinsic imaging data" session_to_nwb( recording_folder_path=recording_folder_path, sorting_folder_path=sorting_folder_path, behavior_file_path=behavior_file_path, video_folder_path=video_folder_path, + intrinsic_signal_optical_imaging_folder_path=intrinsic_signal_optical_imaging_folder_path, output_dir_path=output_dir_path, stub_test=stub_test, ) diff --git a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_intrinsic_signal_imaging_interface.py b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_intrinsic_signal_imaging_interface.py new file mode 100644 index 0000000..ee6341f --- /dev/null +++ b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_intrinsic_signal_imaging_interface.py @@ -0,0 +1,97 @@ +"""Primary class for converting intrinsic signal optical imaging.""" +from pynwb.file import NWBFile +from pynwb.base import Images +from pynwb.image import GrayscaleImage, RGBImage +from pynwb.device import Device +from pydantic import DirectoryPath +import numpy as np +from PIL import Image + +from neuroconv.basedatainterface import BaseDataInterface +from neuroconv.utils import DeepDict +from neuroconv.tools import nwb_helpers + + +class Schneider2024IntrinsicSignalOpticalImagingInterface(BaseDataInterface): + """Intrinsic signal optical imaging interface for schneider_2024 conversion""" + + keywords = ("intrinsic signal optical imaging",) + + def __init__(self, folder_path: DirectoryPath): + super().__init__(folder_path=folder_path) + + def get_metadata(self) -> DeepDict: + # Automatically retrieve as much metadata as possible from the source files available + metadata = super().get_metadata() + + return metadata + + def get_metadata_schema(self) -> dict: + metadata_schema = super().get_metadata_schema() + metadata_schema["properties"]["IntrinsicSignalOpticalImaging"] = dict() + metadata_schema["properties"]["IntrinsicSignalOpticalImaging"]["properties"] = { + "Module": { + "properties": { + "name": {"type": "string"}, + "description": {"type": "string"}, + }, + }, + "Images": { + "properties": { + "name": {"type": "string"}, + "description": {"type": "string"}, + }, + }, + "RawImage": { + "properties": { + "name": {"type": "string"}, + "description": {"type": "string"}, + }, + }, + "ProcessedImage": { + "properties": { + "name": {"type": "string"}, + "description": {"type": "string"}, + }, + }, + } + return metadata_schema + + def add_to_nwbfile(self, nwbfile: NWBFile, metadata: dict): + # Read Data + folder_path = self.source_data["folder_path"] + raw_image_path = folder_path / "BloodvesselPattern.tiff" + processed_image_path = folder_path / "IOS_imageOverlaidFinal.jpg" + with Image.open(raw_image_path) as image: + raw_image_array = np.array(image) + with Image.open(processed_image_path) as image: + processed_image_array = np.array(image) + + # Add Data to NWBFile + isoi_metadata = metadata["IntrinsicSignalOpticalImaging"] + isoi_module = nwb_helpers.get_module( + nwbfile=nwbfile, + name=isoi_metadata["Module"]["name"], + description=isoi_metadata["Module"]["description"], + ) + raw_image = GrayscaleImage( + name=isoi_metadata["RawImage"]["name"], + data=raw_image_array, + description=isoi_metadata["RawImage"]["description"], + ) + processed_image = RGBImage( + name=isoi_metadata["ProcessedImage"]["name"], + data=processed_image_array, + description=isoi_metadata["ProcessedImage"]["description"], + ) + images = Images( + name=isoi_metadata["Images"]["name"], + description=isoi_metadata["Images"]["description"], + images=[raw_image, processed_image], + ) + isoi_module.add(images) + + # Add Devices + for device_kwargs in isoi_metadata["Devices"]: + device = Device(**device_kwargs) + nwbfile.add_device(device) diff --git a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_metadata.yaml b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_metadata.yaml index 24d1541..0f19a86 100644 --- a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_metadata.yaml +++ b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_metadata.yaml @@ -87,3 +87,24 @@ Behavior: Sorting: units_description: Neural spikes will be sorted offline using Kilosort 2.5 and Phy2 software and manually curated to ensure precise spike time acquisition. + +IntrinsicSignalOpticalImaging: + Module: + name: intrinsic_signal_optical_imaging + description: For precise targeting of auditory cortex, intrinsic optical imaging (IOS) will be performed using a 2-photon microscope (Neurolabware). The skull is first bilaterally thinned over a region of interest (ROI) and made translucent. On experiment day, 680nm red light (ThorLabs) is used to image the ROI. Data is collected via MATLAB running custom suites for online and offline analyses. + Images: + name: images + description: Intrinsic signal optical images. + RawImage: + name: raw_image + description: Original image capture of ROI (blood vessel pattern) for intrinsic imaging. + ProcessedImage: + name: processed_image + description: The primary auditory cortex was identified through tonotopic mapping using a temporally periodic acoustic stimulation consisting of sequences of single pure tones of ascending or descending frequencies (20 tones per sequence, 2 to 40kHz exponentially separated, 75dB SPL, 50ms tone duration, 500ms onset-to-onset interval). Tone sequences were delivered from a free-field speaker 10cm above the mouse and repeated 40-60 times (0.1Hz). To compute tonotopic maps, the time course of each pixel was first highpass filtered using a moving average (10s window). Next, a Fourier transform was computed to extract the phase and the power of the frequency component at the frequency of acoustic stimulation (0.1 Hz). The phase indicates the sound frequency driving the response of a pixel, and the power indicates the strength of its response. To account for the hemodynamic delay and compute maps of absolute tonotopy, the response time to ascending sequence of tones was subtracted from the response time to the descending sequence. From these maps of absolute tonotopy, equally spaced iso-frequency contour lines were extracted, color-coded for sound frequency, and overlaid on top of the image of the blood vessel pattern. + Devices: + - name: two_photon_microscope + description: (Neurolabware) + manufacturer: Neurolabware + - name: intrinsic_signal_optical_imaging_laser + description: (ThorLabs) + manufacturer: ThorLabs diff --git a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_notes.md b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_notes.md index 54ac346..3190d87 100644 --- a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_notes.md +++ b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_notes.md @@ -4,6 +4,13 @@ ## Video +## Intrinsic Signal Optical Imaging +- Just including raw blood vessel image and processed overlay + pixel locations bc including the isoi roi response series would really require an extension for context, but seems like it has limited reuse potential. +- Used the Audette paper for description of overlay image. +- Need pixel locs for ephys +- Need device info for 2p microscope and red light laser +- Why is the overlaid image flipped left/right compared to the original? + ## Data Requests - Mice sexes - Remaining data for Grant's project diff --git a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_nwbconverter.py b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_nwbconverter.py index 5b1d40e..0f97bf6 100644 --- a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_nwbconverter.py +++ b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_nwbconverter.py @@ -7,7 +7,10 @@ ) from neuroconv.basedatainterface import BaseDataInterface -from schneider_lab_to_nwb.schneider_2024 import Schneider2024BehaviorInterface +from schneider_lab_to_nwb.schneider_2024 import ( + Schneider2024BehaviorInterface, + Schneider2024IntrinsicSignalOpticalImagingInterface, +) class Schneider2024NWBConverter(NWBConverter): @@ -19,4 +22,5 @@ class Schneider2024NWBConverter(NWBConverter): Behavior=Schneider2024BehaviorInterface, VideoCamera1=VideoInterface, VideoCamera2=VideoInterface, + ISOI=Schneider2024IntrinsicSignalOpticalImagingInterface, )