Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Intrinsic signal imaging #10

Merged
merged 11 commits into from
Oct 9, 2024
1 change: 1 addition & 0 deletions src/schneider_lab_to_nwb/schneider_2024/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,18 +10,18 @@

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,
):
"""Convert the entire 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
Expand All @@ -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_<nwbfile_name>.txt" # Add error file path here
exception_file_path = data_dir_path / f"ERROR_<nwbfile_name>.txt" # Add error file path here
futures.append(
executor.submit(
safe_session_to_nwb,
Expand All @@ -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
Expand All @@ -72,25 +71,25 @@ 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
-------
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__":
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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("._")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""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 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"]
pauladkisson marked this conversation as resolved.
Show resolved Hide resolved

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):
pauladkisson marked this conversation as resolved.
Show resolved Hide resolved
# 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(
pauladkisson marked this conversation as resolved.
Show resolved Hide resolved
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)
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,17 @@ 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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not follow the naming convention for the types, right?

https://nwbinspector.readthedocs.io/en/dev/best_practices/general.html#naming-conventions

Is that intentional? I am missing something?

Same for the processed image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is some open discussion on this: rly/ndx-pose#31
And given all the considerations mentioned on that issue, I think snake_case is the more appropriate/intuitive choice for naming, so I'm going to stick with it for this conversion.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a horse in this race but maybe it is important for you to know (or remember?) that when we discussed this in the LBNL - CN meeting Ben was very strong against changing the best practice and we did not come to any conclusion about it but Ryan promised to write some modified best practices. Maybe something to bring up again.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know, thx.

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.
pauladkisson marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

## 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

## Data Requests
- Mice sexes
- Remaining data for Grant's project
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -19,4 +22,5 @@ class Schneider2024NWBConverter(NWBConverter):
Behavior=Schneider2024BehaviorInterface,
VideoCamera1=VideoInterface,
VideoCamera2=VideoInterface,
ISOI=Schneider2024IntrinsicSignalOpticalImagingInterface,
)