From c558deed1832616291a037fef3b3871dcf6ccfb0 Mon Sep 17 00:00:00 2001 From: Ben Dichter Date: Sun, 18 Feb 2024 20:25:56 +0000 Subject: [PATCH 01/19] add FacemapInterface, which currently only supports eye tracking. --- src/neuroconv/datainterfaces/__init__.py | 4 + .../behavior/facemap/__init__.py | 0 .../behavior/facemap/facemapdatainterface.py | 116 ++++++++++++++++++ .../behavior/video/video_utils.py | 4 +- .../test_on_data/test_behavior_interfaces.py | 1 + 5 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 src/neuroconv/datainterfaces/behavior/facemap/__init__.py create mode 100644 src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py diff --git a/src/neuroconv/datainterfaces/__init__.py b/src/neuroconv/datainterfaces/__init__.py index c173b2038..8b1bf4f49 100644 --- a/src/neuroconv/datainterfaces/__init__.py +++ b/src/neuroconv/datainterfaces/__init__.py @@ -9,6 +9,7 @@ from .behavior.neuralynx.neuralynx_nvt_interface import NeuralynxNvtInterface from .behavior.sleap.sleapdatainterface import SLEAPInterface from .behavior.video.videodatainterface import VideoInterface +from .behavior.facemap.facemapdatainterface import FacemapInterface # Ecephys from .ecephys.alphaomega.alphaomegadatainterface import AlphaOmegaRecordingInterface @@ -155,6 +156,7 @@ FicTracDataInterface, NeuralynxNvtInterface, LightningPoseDataInterface, + FacemapInterface, # Text CsvTimeIntervalsInterface, ExcelTimeIntervalsInterface, @@ -183,11 +185,13 @@ }, icephys=dict(Abf=AbfInterface), behavior=dict( + AudioInterface=AudioInterface, Video=VideoInterface, DeepLabCut=DeepLabCutInterface, SLEAP=SLEAPInterface, FicTrac=FicTracDataInterface, LightningPose=LightningPoseDataInterface, + FacemapInterface=FacemapInterface, # Text CsvTimeIntervals=CsvTimeIntervalsInterface, ExcelTimeIntervals=ExcelTimeIntervalsInterface, diff --git a/src/neuroconv/datainterfaces/behavior/facemap/__init__.py b/src/neuroconv/datainterfaces/behavior/facemap/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py new file mode 100644 index 000000000..b289507bd --- /dev/null +++ b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py @@ -0,0 +1,116 @@ +from typing import Optional + +import h5py +import numpy as np +from pynwb.base import TimeSeries +from pynwb.behavior import EyeTracking, SpatialSeries, PupilTracking +from pynwb.file import NWBFile + +from ..video.video_utils import get_video_timestamps +from ....basetemporalalignmentinterface import BaseTemporalAlignmentInterface +from ....tools import get_module +from ....utils import FilePathType + + +class FacemapInterface(BaseTemporalAlignmentInterface): + display_name = "Facemap" + help = "Interface for Facemap output." + + keywords = ["eye tracking"] + + def __init__(self, mat_file_path: FilePathType, video_file_path: FilePathType, verbose: bool = True): + """ + Load and prepare data for facemap. + + Parameters + ---------- + mat_file_path : string or Path + Path to the .mat file. + video_file_path : string or Path + Path to the .avi file. + verbose : bool, default: True + Allows verbose. + """ + super().__init__(mat_file_path=mat_file_path, video_file_path=video_file_path, verbose=verbose) + self.original_timestamps = None + self.timestamps = None + + def add_pupil_data(self, nwbfile: NWBFile): + + timestamps = self.get_timestamps() + + with h5py.File(self.source_data["mat_file_path"], "r") as file: + + behavior_module = get_module(nwbfile=nwbfile, name="behavior", description="behavioral data") + + eye_com = SpatialSeries( + name="eye_center_of_mass", + description="The position of the eye measured in degrees.", + data=file["proc"]["pupil"]["com"][:].T, + reference_frame="unknown", + unit="degrees", + timestamps=timestamps, + ) + + eye_tracking = EyeTracking(name="EyeTracking", spatial_series=eye_com) + + behavior_module.add(eye_tracking) + + pupil_area = TimeSeries( + name="pupil_area", + description="Area of pupil", + data=file["proc"]["pupil"]["area"][:].T, + unit="unknown", + timestamps=eye_com, + ) + + pupil_area_raw = TimeSeries( + name="pupil_area_raw", + description="Raw unprocessed area of pupil", + data=file["proc"]["pupil"]["area_raw"][:].T, + unit="unknown", + timestamps=eye_com, + ) + + pupil_tracking = PupilTracking(time_series=[pupil_area, pupil_area_raw], name="PupilTracking") + + behavior_module.add(pupil_tracking) + + def get_original_timestamps(self) -> np.ndarray: + if self.original_timestamps is None: + self.original_timestamps = get_video_timestamps(self.source_data["video_file_path"]) + return self.original_timestamps + + def get_timestamps(self) -> np.ndarray: + if self.timestamps is None: + return self.get_original_timestamps() + else: + return self.timestamps + + def set_aligned_timestamps(self, aligned_timestamps: np.ndarray) -> None: + self.timestamps = aligned_timestamps + + def add_to_nwbfile( + self, + nwbfile: NWBFile, + metadata: Optional[dict] = None, + compression: Optional[str] = "gzip", + compression_opts: Optional[int] = None, + ): + """ + Add facemap data to NWBFile. + + Parameters + ---------- + nwbfile : NWBFile + NWBFile to add facemap data to. + metadata : dict, optional + Metadata to add to the NWBFile. + compression : str, optional + Compression type. + compression_opts : int, optional + Compression options. + """ + + self.add_pupil_data(nwbfile=nwbfile) + diff --git a/src/neuroconv/datainterfaces/behavior/video/video_utils.py b/src/neuroconv/datainterfaces/behavior/video/video_utils.py index df70ee77b..bb17982b3 100644 --- a/src/neuroconv/datainterfaces/behavior/video/video_utils.py +++ b/src/neuroconv/datainterfaces/behavior/video/video_utils.py @@ -9,7 +9,7 @@ from ....utils import FilePathType -def get_video_timestamps(file_path: FilePathType, max_frames: Optional[int] = None) -> list: +def get_video_timestamps(file_path: FilePathType, max_frames: Optional[int] = None) -> np.ndarray: """Extract the timestamps of the video located in file_path Parameters @@ -43,7 +43,7 @@ def __init__(self, file_path: FilePathType): self._frame_count = None self._video_open_msg = "The video file is not open!" - def get_video_timestamps(self, max_frames=None): + def get_video_timestamps(self, max_frames=None) -> np.ndarray: """Return numpy array of the timestamps(s) for a video file.""" cv2 = get_package(package_name="cv2", installation_instructions="pip install opencv-python-headless") diff --git a/tests/test_on_data/test_behavior_interfaces.py b/tests/test_on_data/test_behavior_interfaces.py index 17b956b6c..e5f87e01f 100644 --- a/tests/test_on_data/test_behavior_interfaces.py +++ b/tests/test_on_data/test_behavior_interfaces.py @@ -24,6 +24,7 @@ NeuralynxNvtInterface, SLEAPInterface, VideoInterface, + FacemapInterface, ) from neuroconv.tools.testing.data_interface_mixins import ( DataInterfaceTestMixin, From 275bf42e64e80877b39285f691b6f4529a9d00d2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 18 Feb 2024 20:27:31 +0000 Subject: [PATCH 02/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/neuroconv/datainterfaces/__init__.py | 2 +- .../datainterfaces/behavior/facemap/facemapdatainterface.py | 3 +-- tests/test_on_data/test_behavior_interfaces.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/neuroconv/datainterfaces/__init__.py b/src/neuroconv/datainterfaces/__init__.py index 8b1bf4f49..c11c0c189 100644 --- a/src/neuroconv/datainterfaces/__init__.py +++ b/src/neuroconv/datainterfaces/__init__.py @@ -1,6 +1,7 @@ # Behavior from .behavior.audio.audiointerface import AudioInterface from .behavior.deeplabcut.deeplabcutdatainterface import DeepLabCutInterface +from .behavior.facemap.facemapdatainterface import FacemapInterface from .behavior.fictrac.fictracdatainterface import FicTracDataInterface from .behavior.lightningpose.lightningposedatainterface import ( LightningPoseDataInterface, @@ -9,7 +10,6 @@ from .behavior.neuralynx.neuralynx_nvt_interface import NeuralynxNvtInterface from .behavior.sleap.sleapdatainterface import SLEAPInterface from .behavior.video.videodatainterface import VideoInterface -from .behavior.facemap.facemapdatainterface import FacemapInterface # Ecephys from .ecephys.alphaomega.alphaomegadatainterface import AlphaOmegaRecordingInterface diff --git a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py index b289507bd..93c555852 100644 --- a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py @@ -3,7 +3,7 @@ import h5py import numpy as np from pynwb.base import TimeSeries -from pynwb.behavior import EyeTracking, SpatialSeries, PupilTracking +from pynwb.behavior import EyeTracking, PupilTracking, SpatialSeries from pynwb.file import NWBFile from ..video.video_utils import get_video_timestamps @@ -113,4 +113,3 @@ def add_to_nwbfile( """ self.add_pupil_data(nwbfile=nwbfile) - diff --git a/tests/test_on_data/test_behavior_interfaces.py b/tests/test_on_data/test_behavior_interfaces.py index e5f87e01f..6a25da08b 100644 --- a/tests/test_on_data/test_behavior_interfaces.py +++ b/tests/test_on_data/test_behavior_interfaces.py @@ -18,13 +18,13 @@ from neuroconv import NWBConverter from neuroconv.datainterfaces import ( DeepLabCutInterface, + FacemapInterface, FicTracDataInterface, LightningPoseDataInterface, MiniscopeBehaviorInterface, NeuralynxNvtInterface, SLEAPInterface, VideoInterface, - FacemapInterface, ) from neuroconv.tools.testing.data_interface_mixins import ( DataInterfaceTestMixin, From 16523f9e345d9df3d563505ab7d46aac5716b56d Mon Sep 17 00:00:00 2001 From: Ben Dichter Date: Sun, 18 Feb 2024 15:30:44 -0500 Subject: [PATCH 03/19] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e35121630..457e00b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * Added `LightningPoseConverter` to add pose estimation data and the original and the optional labeled video added as ImageSeries to NWB. [PR #633](https://github.com/catalystneuro/neuroconv/pull/633) * Added gain as a required `__init__` argument for `TdtRecordingInterface`. [PR #704](https://github.com/catalystneuro/neuroconv/pull/704) * Extract session_start_time from Plexon plx recording file. [PR #723](https://github.com/catalystneuro/neuroconv/pull/723) +* Add `FacemapInterface`, which currently only handles eye tracking [PR #752](https://github.com/catalystneuro/neuroconv/pull/752) ### Improvements * `nwbinspector` has been removed as a minimal dependency. It becomes an extra (optional) dependency with `neuroconv[dandi]`. [PR #672](https://github.com/catalystneuro/neuroconv/pull/672) From 78e8794fd67c7c0a826aac96db5b054af84b66ea Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Fri, 5 Apr 2024 18:59:03 +0200 Subject: [PATCH 04/19] add motion svd --- .../behavior/facemap/facemapdatainterface.py | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py index 93c555852..963942078 100644 --- a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py @@ -8,7 +8,7 @@ from ..video.video_utils import get_video_timestamps from ....basetemporalalignmentinterface import BaseTemporalAlignmentInterface -from ....tools import get_module +from ....tools import get_module, get_package from ....utils import FilePathType @@ -76,6 +76,60 @@ def add_pupil_data(self, nwbfile: NWBFile): behavior_module.add(pupil_tracking) + def add_motion_SVD(self, nwbfile: NWBFile): + """ + Add data motion SVD and motion mask for each ROI. + + Parameters + ---------- + nwbfile : NWBFile + NWBFile to add motion SVD components data to. + """ + from ndx_facemap_motionsvd import MotionSVDSeries, MotionSVDMasks + from pynwb.core import DynamicTableRegion + + with h5py.File(self.source_data["mat_file_path"], "r") as file: + + behavior_module = get_module(nwbfile=nwbfile, name="behavior", description="behavioral data") + + timestamps = self.get_timestamps() + n = 0 + for series_ref, mask_ref in zip(file["proc"]["motSVD"][:], file["proc"]["uMotMask"][:]): + mask_ref = mask_ref[0] + series_ref = series_ref[0] + + motion_masks_table = MotionSVDMasks( + name=f"MotionSVDMasksROI{n}", + description=f"motion mask for ROI{n}", + ) + + for component in file[mask_ref]: + motion_masks_table.add_row(image_mask=component) + + motion_masks = DynamicTableRegion( + name="motion_masks", + data=list(range(len(file["proc"]["motSVD"][:]))), + description="all the mask", + table=motion_masks_table, + ) + + data = np.array(file[series_ref]) + + motionsvd_series = MotionSVDSeries( + name=f"MotionSVDSeriesROI{n}", + description=f"SVD components for ROI{n}", + data=data.T, + motion_masks=motion_masks, + unit="unknown", + timestamps=timestamps, + ) + n = +1 + + behavior_module.add(motion_masks_table) + behavior_module.add(motionsvd_series) + + return + def get_original_timestamps(self) -> np.ndarray: if self.original_timestamps is None: self.original_timestamps = get_video_timestamps(self.source_data["video_file_path"]) @@ -113,3 +167,4 @@ def add_to_nwbfile( """ self.add_pupil_data(nwbfile=nwbfile) + self.add_motion_SVD(nwbfile=nwbfile) From 07e21f903e4bbb874bc1bd933d8095a518207d3d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:01:03 +0000 Subject: [PATCH 05/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../datainterfaces/behavior/facemap/facemapdatainterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py index 963942078..0e6a5b94a 100644 --- a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py @@ -85,7 +85,7 @@ def add_motion_SVD(self, nwbfile: NWBFile): nwbfile : NWBFile NWBFile to add motion SVD components data to. """ - from ndx_facemap_motionsvd import MotionSVDSeries, MotionSVDMasks + from ndx_facemap_motionsvd import MotionSVDMasks, MotionSVDSeries from pynwb.core import DynamicTableRegion with h5py.File(self.source_data["mat_file_path"], "r") as file: From 2f256782a6ebdd79bcd806cfea8fe7eaa3028c03 Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Mon, 8 Apr 2024 16:57:22 +0200 Subject: [PATCH 06/19] separate multivideo masks and ROI masks --- .../behavior/facemap/facemapdatainterface.py | 74 +++++++++++++++++-- 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py index 0e6a5b94a..b8a1b7566 100644 --- a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py @@ -18,7 +18,13 @@ class FacemapInterface(BaseTemporalAlignmentInterface): keywords = ["eye tracking"] - def __init__(self, mat_file_path: FilePathType, video_file_path: FilePathType, verbose: bool = True): + def __init__( + self, + mat_file_path: FilePathType, + video_file_path: FilePathType, + include_multivideo_SVD: bool = True, + verbose: bool = True, + ): """ Load and prepare data for facemap. @@ -32,6 +38,7 @@ def __init__(self, mat_file_path: FilePathType, video_file_path: FilePathType, v Allows verbose. """ super().__init__(mat_file_path=mat_file_path, video_file_path=video_file_path, verbose=verbose) + self.include_multivideo_SVD = include_multivideo_SVD self.original_timestamps = None self.timestamps = None @@ -88,28 +95,79 @@ def add_motion_SVD(self, nwbfile: NWBFile): from ndx_facemap_motionsvd import MotionSVDMasks, MotionSVDSeries from pynwb.core import DynamicTableRegion + # From documentation + # motSVD: cell array of motion SVDs [time x components] (in order: multivideo, ROI1, ROI2, ROI3) + # uMotMask: cell array of motion masks [pixels x components] (in order: multivideo, ROI1, ROI2, ROI3) + # motion masks of multivideo are reported as 2D-arrays npixels x components + # while ROIs motion masks are reported as 3D-arrays x_pixels x y_pixels x components + with h5py.File(self.source_data["mat_file_path"], "r") as file: behavior_module = get_module(nwbfile=nwbfile, name="behavior", description="behavioral data") timestamps = self.get_timestamps() - n = 0 - for series_ref, mask_ref in zip(file["proc"]["motSVD"][:], file["proc"]["uMotMask"][:]): - mask_ref = mask_ref[0] - series_ref = series_ref[0] + + # store multivideo motion mask and motion series + if self.include_multivideo_SVD: + # add multivideo mask + mask_ref = file["proc"]["uMotMask"][0][0] + motion_masks_table = MotionSVDMasks( + name=f"MotionSVDMasksMultivideo", + description=f"motion mask for multivideo", + ) + + multivideo_mask_ref = file["proc"]["wpix"][0][0] + multivideo_mask = file[multivideo_mask_ref] + multivideo_mask = multivideo_mask[:] + non_zero_multivideo_mask = np.where(multivideo_mask == 1) + y_indices, x_indices = non_zero_multivideo_mask + top = np.min(y_indices) + left = np.min(x_indices) + bottom = np.max(y_indices) + right = np.max(x_indices) + submask = multivideo_mask[top : bottom + 1, left : right + 1] + componendt_2d_shape = submask.shape + + for component in file[mask_ref]: + componendt_2d = component.reshape(componendt_2d_shape) + motion_masks_table.add_row(image_mask=componendt_2d.T) + + motion_masks = DynamicTableRegion( + name="motion_masks", + data=list(range(len(file["proc"]["motSVD"][:]))), + description="all the multivideo motion mask", + table=motion_masks_table, + ) + + series_ref = file["proc"]["motSVD"][0][0] + data = np.array(file[series_ref]) + + motionsvd_series = MotionSVDSeries( + name=f"MotionSVDSeriesMultivideo", + description=f"SVD components for multivideo", + data=data.T, + motion_masks=motion_masks, + unit="unknown", + timestamps=timestamps, + ) + behavior_module.add(motion_masks_table) + behavior_module.add(motionsvd_series) + + # store ROIs motion mask and motion series + n = 1 + for series_ref, mask_ref in zip(file["proc"]["motSVD"][1:][0], file["proc"]["uMotMask"][1:][0]): motion_masks_table = MotionSVDMasks( name=f"MotionSVDMasksROI{n}", description=f"motion mask for ROI{n}", ) - for component in file[mask_ref]: - motion_masks_table.add_row(image_mask=component) + motion_masks_table.add_row(image_mask=component.T) motion_masks = DynamicTableRegion( name="motion_masks", data=list(range(len(file["proc"]["motSVD"][:]))), - description="all the mask", + description="all the ROIs motion mask", table=motion_masks_table, ) From 056d78723d49272c8835899cae92ecd0e6a686f0 Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Mon, 15 Apr 2024 18:38:42 +0200 Subject: [PATCH 07/19] separate multivideo and rois motion masks --- .../behavior/facemap/facemapdatainterface.py | 162 ++++++++++++------ 1 file changed, 111 insertions(+), 51 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py index b8a1b7566..34a2f0c0a 100644 --- a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py @@ -5,10 +5,11 @@ from pynwb.base import TimeSeries from pynwb.behavior import EyeTracking, PupilTracking, SpatialSeries from pynwb.file import NWBFile - +from ndx_facemap_motionsvd import MotionSVDMasks, MotionSVDSeries +from pynwb.core import DynamicTableRegion from ..video.video_utils import get_video_timestamps from ....basetemporalalignmentinterface import BaseTemporalAlignmentInterface -from ....tools import get_module, get_package +from ....tools import get_module from ....utils import FilePathType @@ -22,6 +23,7 @@ def __init__( self, mat_file_path: FilePathType, video_file_path: FilePathType, + first_n_components: int = 500, include_multivideo_SVD: bool = True, verbose: bool = True, ): @@ -34,10 +36,15 @@ def __init__( Path to the .mat file. video_file_path : string or Path Path to the .avi file. + first_n_components : int, default: 500 + Number of components to store. + include_multivideo_SVD : bool, default: True + Include multivideo motion SVD. verbose : bool, default: True Allows verbose. """ super().__init__(mat_file_path=mat_file_path, video_file_path=video_file_path, verbose=verbose) + self.first_n_components = first_n_components self.include_multivideo_SVD = include_multivideo_SVD self.original_timestamps = None self.timestamps = None @@ -83,23 +90,20 @@ def add_pupil_data(self, nwbfile: NWBFile): behavior_module.add(pupil_tracking) - def add_motion_SVD(self, nwbfile: NWBFile): + def add_multivideo_motion_SVD(self, nwbfile: NWBFile): """ - Add data motion SVD and motion mask for each ROI. + Add data motion SVD and motion mask for the whole video. Parameters ---------- nwbfile : NWBFile NWBFile to add motion SVD components data to. """ - from ndx_facemap_motionsvd import MotionSVDMasks, MotionSVDSeries - from pynwb.core import DynamicTableRegion # From documentation # motSVD: cell array of motion SVDs [time x components] (in order: multivideo, ROI1, ROI2, ROI3) # uMotMask: cell array of motion masks [pixels x components] (in order: multivideo, ROI1, ROI2, ROI3) # motion masks of multivideo are reported as 2D-arrays npixels x components - # while ROIs motion masks are reported as 3D-arrays x_pixels x y_pixels x components with h5py.File(self.source_data["mat_file_path"], "r") as file: @@ -107,71 +111,113 @@ def add_motion_SVD(self, nwbfile: NWBFile): timestamps = self.get_timestamps() + # Extract mask_coordinates + mask_coordinates = file[file[file["proc"]["ROI"][0][0]][0][0]] + y1 = int(np.round(mask_coordinates[0][0])-1) # correct matlab indexing + x1 = int(np.round(mask_coordinates[1][0])-1) # correct matlab indexing + y2 = y1 + int(np.round(mask_coordinates[2][0])) + x2 = x1 + int(np.round(mask_coordinates[3][0])) + mask_coordinates = [x1, y1, x2, y2] + # store multivideo motion mask and motion series - if self.include_multivideo_SVD: - # add multivideo mask - mask_ref = file["proc"]["uMotMask"][0][0] - motion_masks_table = MotionSVDMasks( - name=f"MotionSVDMasksMultivideo", - description=f"motion mask for multivideo", - ) + motion_masks_table = MotionSVDMasks( + name=f"MotionSVDMasksMultivideo", + description=f"motion mask for multivideo", + mask_coordinates=mask_coordinates, + downsampling_factor=self._get_downsamplig_factor(), + processed_frame_dimension=self._get_processed_frame_dimension(), + ) - multivideo_mask_ref = file["proc"]["wpix"][0][0] - multivideo_mask = file[multivideo_mask_ref] - multivideo_mask = multivideo_mask[:] - non_zero_multivideo_mask = np.where(multivideo_mask == 1) - y_indices, x_indices = non_zero_multivideo_mask - top = np.min(y_indices) - left = np.min(x_indices) - bottom = np.max(y_indices) - right = np.max(x_indices) - submask = multivideo_mask[top : bottom + 1, left : right + 1] - componendt_2d_shape = submask.shape - - for component in file[mask_ref]: - componendt_2d = component.reshape(componendt_2d_shape) - motion_masks_table.add_row(image_mask=componendt_2d.T) + # add multivideo mask + mask_ref = file["proc"]["uMotMask"][0][0] + for c, component in enumerate(file[mask_ref]): + if c == self.first_n_components: + break + componendt_2d = component.reshape((y2-y1, x2-x1)) + motion_masks_table.add_row(image_mask=componendt_2d.T) + + motion_masks = DynamicTableRegion( + name="motion_masks", + data=list(range(len(file["proc"]["motSVD"][:]))), + description="all the multivideo motion mask", + table=motion_masks_table, + ) - motion_masks = DynamicTableRegion( - name="motion_masks", - data=list(range(len(file["proc"]["motSVD"][:]))), - description="all the multivideo motion mask", - table=motion_masks_table, - ) + series_ref = file["proc"]["motSVD"][0][0] + data = np.array(file[series_ref]) + data = data[: self.first_n_components, :] - series_ref = file["proc"]["motSVD"][0][0] - data = np.array(file[series_ref]) + motionsvd_series = MotionSVDSeries( + name=f"MotionSVDSeriesMultivideo", + description=f"SVD components for multivideo", + data=data.T, + motion_masks=motion_masks, + unit="unknown", + timestamps=timestamps, + ) + behavior_module.add(motion_masks_table) + behavior_module.add(motionsvd_series) - motionsvd_series = MotionSVDSeries( - name=f"MotionSVDSeriesMultivideo", - description=f"SVD components for multivideo", - data=data.T, - motion_masks=motion_masks, - unit="unknown", - timestamps=timestamps, - ) - behavior_module.add(motion_masks_table) - behavior_module.add(motionsvd_series) + return + + def add_motion_SVD(self, nwbfile: NWBFile): + """ + Add data motion SVD and motion mask for each ROI. + + Parameters + ---------- + nwbfile : NWBFile + NWBFile to add motion SVD components data to. + """ + + # From documentation + # motSVD: cell array of motion SVDs [time x components] (in order: multivideo, ROI1, ROI2, ROI3) + # uMotMask: cell array of motion masks [pixels x components] (in order: multivideo, ROI1, ROI2, ROI3) + # ROIs motion masks are reported as 3D-arrays x_pixels x y_pixels x components + + with h5py.File(self.source_data["mat_file_path"], "r") as file: + + behavior_module = get_module(nwbfile=nwbfile, name="behavior", description="behavioral data") + timestamps = self.get_timestamps() + downsampling_factor=self._get_downsamplig_factor() + processed_frame_dimension=self._get_processed_frame_dimension() # store ROIs motion mask and motion series n = 1 - for series_ref, mask_ref in zip(file["proc"]["motSVD"][1:][0], file["proc"]["uMotMask"][1:][0]): + for series_ref, mask_ref in zip(file["proc"]["motSVD"][1:], file["proc"]["uMotMask"][1:]): + series_ref = series_ref[0] + mask_ref = mask_ref[0] + + # skipping the first ROI because it referes to "running" mask, from Facemap doc + mask_coordinates = file[file["proc"]["locROI"][n][0]] + y1 = int(np.round(mask_coordinates[0][0])-1) # correct matlab indexing + x1 = int(np.round(mask_coordinates[1][0])-1) # correct matlab indexing + y2 = y1 + int(np.round(mask_coordinates[2][0])) + x2 = x1 + int(np.round(mask_coordinates[3][0])) + mask_coordinates = [x1, y1, x2, y2] motion_masks_table = MotionSVDMasks( name=f"MotionSVDMasksROI{n}", description=f"motion mask for ROI{n}", + mask_coordinates=mask_coordinates, + downsampling_factor=downsampling_factor, + processed_frame_dimension=processed_frame_dimension, ) - for component in file[mask_ref]: + + for c, component in enumerate(file[mask_ref]): + if c == self.first_n_components: + break motion_masks_table.add_row(image_mask=component.T) motion_masks = DynamicTableRegion( name="motion_masks", - data=list(range(len(file["proc"]["motSVD"][:]))), + data=list(range(self.first_n_components)), description="all the ROIs motion mask", table=motion_masks_table, ) data = np.array(file[series_ref]) + data = data[: self.first_n_components, :] motionsvd_series = MotionSVDSeries( name=f"MotionSVDSeriesROI{n}", @@ -201,7 +247,19 @@ def get_timestamps(self) -> np.ndarray: def set_aligned_timestamps(self, aligned_timestamps: np.ndarray) -> None: self.timestamps = aligned_timestamps - + + def _get_downsamplig_factor(self) -> float: + with h5py.File(self.source_data["mat_file_path"], "r") as file: + downsamplig_factor = file["proc"]["sc"][0][0] + return downsamplig_factor + + def _get_processed_frame_dimension(self) -> np.ndarray: + with h5py.File(self.source_data["mat_file_path"], "r") as file: + processed_frame_ref = file["proc"]["wpix"][0][0] + frame = file[processed_frame_ref] + return [frame.shape[1],frame.shape[0]] + + def add_to_nwbfile( self, nwbfile: NWBFile, @@ -226,3 +284,5 @@ def add_to_nwbfile( self.add_pupil_data(nwbfile=nwbfile) self.add_motion_SVD(nwbfile=nwbfile) + if self.add_multivideo_motion_SVD: + self.add_multivideo_motion_SVD(nwbfile=nwbfile) From 8ac9c471a2ee8b05267fa326c49f751015fee9b3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 16:41:40 +0000 Subject: [PATCH 08/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../behavior/facemap/facemapdatainterface.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py index 34a2f0c0a..21ef04b17 100644 --- a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py @@ -2,11 +2,12 @@ import h5py import numpy as np +from ndx_facemap_motionsvd import MotionSVDMasks, MotionSVDSeries from pynwb.base import TimeSeries from pynwb.behavior import EyeTracking, PupilTracking, SpatialSeries -from pynwb.file import NWBFile -from ndx_facemap_motionsvd import MotionSVDMasks, MotionSVDSeries from pynwb.core import DynamicTableRegion +from pynwb.file import NWBFile + from ..video.video_utils import get_video_timestamps from ....basetemporalalignmentinterface import BaseTemporalAlignmentInterface from ....tools import get_module @@ -113,8 +114,8 @@ def add_multivideo_motion_SVD(self, nwbfile: NWBFile): # Extract mask_coordinates mask_coordinates = file[file[file["proc"]["ROI"][0][0]][0][0]] - y1 = int(np.round(mask_coordinates[0][0])-1) # correct matlab indexing - x1 = int(np.round(mask_coordinates[1][0])-1) # correct matlab indexing + y1 = int(np.round(mask_coordinates[0][0]) - 1) # correct matlab indexing + x1 = int(np.round(mask_coordinates[1][0]) - 1) # correct matlab indexing y2 = y1 + int(np.round(mask_coordinates[2][0])) x2 = x1 + int(np.round(mask_coordinates[3][0])) mask_coordinates = [x1, y1, x2, y2] @@ -133,7 +134,7 @@ def add_multivideo_motion_SVD(self, nwbfile: NWBFile): for c, component in enumerate(file[mask_ref]): if c == self.first_n_components: break - componendt_2d = component.reshape((y2-y1, x2-x1)) + componendt_2d = component.reshape((y2 - y1, x2 - x1)) motion_masks_table.add_row(image_mask=componendt_2d.T) motion_masks = DynamicTableRegion( @@ -180,8 +181,8 @@ def add_motion_SVD(self, nwbfile: NWBFile): behavior_module = get_module(nwbfile=nwbfile, name="behavior", description="behavioral data") timestamps = self.get_timestamps() - downsampling_factor=self._get_downsamplig_factor() - processed_frame_dimension=self._get_processed_frame_dimension() + downsampling_factor = self._get_downsamplig_factor() + processed_frame_dimension = self._get_processed_frame_dimension() # store ROIs motion mask and motion series n = 1 for series_ref, mask_ref in zip(file["proc"]["motSVD"][1:], file["proc"]["uMotMask"][1:]): @@ -190,8 +191,8 @@ def add_motion_SVD(self, nwbfile: NWBFile): # skipping the first ROI because it referes to "running" mask, from Facemap doc mask_coordinates = file[file["proc"]["locROI"][n][0]] - y1 = int(np.round(mask_coordinates[0][0])-1) # correct matlab indexing - x1 = int(np.round(mask_coordinates[1][0])-1) # correct matlab indexing + y1 = int(np.round(mask_coordinates[0][0]) - 1) # correct matlab indexing + x1 = int(np.round(mask_coordinates[1][0]) - 1) # correct matlab indexing y2 = y1 + int(np.round(mask_coordinates[2][0])) x2 = x1 + int(np.round(mask_coordinates[3][0])) mask_coordinates = [x1, y1, x2, y2] @@ -247,19 +248,18 @@ def get_timestamps(self) -> np.ndarray: def set_aligned_timestamps(self, aligned_timestamps: np.ndarray) -> None: self.timestamps = aligned_timestamps - + def _get_downsamplig_factor(self) -> float: with h5py.File(self.source_data["mat_file_path"], "r") as file: downsamplig_factor = file["proc"]["sc"][0][0] return downsamplig_factor - + def _get_processed_frame_dimension(self) -> np.ndarray: with h5py.File(self.source_data["mat_file_path"], "r") as file: processed_frame_ref = file["proc"]["wpix"][0][0] frame = file[processed_frame_ref] - return [frame.shape[1],frame.shape[0]] - - + return [frame.shape[1], frame.shape[0]] + def add_to_nwbfile( self, nwbfile: NWBFile, From e413efa1a2eba1d9f4c2ca4116cad54fbd4aa631 Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Fri, 17 May 2024 14:54:10 +0200 Subject: [PATCH 09/19] set facempa test (not working) --- .../behavior/facemap/facemapdatainterface.py | 14 +++++- .../test_on_data/test_behavior_interfaces.py | 43 ++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py index 21ef04b17..5630ca93a 100644 --- a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py @@ -2,7 +2,6 @@ import h5py import numpy as np -from ndx_facemap_motionsvd import MotionSVDMasks, MotionSVDSeries from pynwb.base import TimeSeries from pynwb.behavior import EyeTracking, PupilTracking, SpatialSeries from pynwb.core import DynamicTableRegion @@ -13,6 +12,19 @@ from ....tools import get_module from ....utils import FilePathType +import subprocess +import sys + + +def install_package(package): + subprocess.check_call([sys.executable, "-m", "pip", "install", package]) + +try: + from ndx_facemap_motionsvd import MotionSVDMasks, MotionSVDSeries +except ImportError: + # TODO: to be change when ndx-facemap-motionsvd version on pip + install_package('git+https://github.com/catalystneuro/ndx-facemap-motionsvd.git@main') + from ndx_facemap_motionsvd import MotionSVDMasks, MotionSVDSeries class FacemapInterface(BaseTemporalAlignmentInterface): display_name = "Facemap" diff --git a/tests/test_on_data/test_behavior_interfaces.py b/tests/test_on_data/test_behavior_interfaces.py index e96fc6ab0..d53d372b1 100644 --- a/tests/test_on_data/test_behavior_interfaces.py +++ b/tests/test_on_data/test_behavior_interfaces.py @@ -2,6 +2,7 @@ from datetime import datetime, timezone from pathlib import Path +import h5py import numpy as np import pandas as pd import sleap_io @@ -13,7 +14,7 @@ from numpy.testing import assert_array_equal from parameterized import param, parameterized from pynwb import NWBHDF5IO -from pynwb.behavior import Position, SpatialSeries +from pynwb.behavior import Position, SpatialSeries, EyeTracking from neuroconv import NWBConverter from neuroconv.datainterfaces import ( @@ -738,5 +739,45 @@ def check_video_stub(self): assert nwbfile.acquisition[self.image_series_name].data.shape[0] == 10 +class TestFacemapInterface(DataInterfaceTestMixin, TemporalAlignmentMixin, unittest.TestCase): + + data_interface_cls = FacemapInterface + interface_kwargs = dict( + mat_file_path=str(BEHAVIOR_DATA_PATH / "Facemap" / "facemap_output_test.mat"), + video_file_path=str(BEHAVIOR_DATA_PATH / "Facemap" / "raw_behavioral_video.avi"), + first_n_components=3, + ) + conversion_options = dict() + save_directory = OUTPUT_PATH + + @classmethod + def setUpClass(cls): + cls.eye_com_name = "eye_center_of_mass" + cls.eye_com_expected_metadata = DeepDict( + SpatialSeries=dict( + name="eye_com_name", + description="The position of the eye measured in degrees.", + reference_frame="unknown", + unit="degrees", + ) + ) + with h5py.File(cls.interface_kwargs["mat_file_path"], "r") as file: + cls.eye_com_test_data = file["proc"]["pupil"]["com"][:].T + + cls.eye_tracking_name = "EyeTracking" + + def check_extracted_metadata(self, metadata: dict): + + self.assertIn(self.eye_tracking_name, metadata["Behavior"]) + + def check_read_nwb(self, nwbfile_path: str): + with NWBHDF5IO(path=nwbfile_path, mode="r", load_namespaces=True) as io: + nwbfile = io.read() + self.assertIn("behavior", nwbfile.processing) + self.assertIn(self.eye_tracking_name, nwbfile.processing["behavior"].data_interfaces) + eye_tracking_container = nwbfile.processing["behavior"].data_interfaces[self.eye_tracking_name] + self.assertIsInstance(eye_tracking_container, EyeTracking) + + if __name__ == "__main__": unittest.main() From 578eeb9411fe52a8495971928f230336cc0a4056 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 12:54:23 +0000 Subject: [PATCH 10/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../behavior/facemap/facemapdatainterface.py | 9 +++++---- tests/test_on_data/test_behavior_interfaces.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py index 5630ca93a..fb19cc626 100644 --- a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py @@ -1,3 +1,5 @@ +import subprocess +import sys from typing import Optional import h5py @@ -12,20 +14,19 @@ from ....tools import get_module from ....utils import FilePathType -import subprocess -import sys - def install_package(package): subprocess.check_call([sys.executable, "-m", "pip", "install", package]) + try: from ndx_facemap_motionsvd import MotionSVDMasks, MotionSVDSeries except ImportError: # TODO: to be change when ndx-facemap-motionsvd version on pip - install_package('git+https://github.com/catalystneuro/ndx-facemap-motionsvd.git@main') + install_package("git+https://github.com/catalystneuro/ndx-facemap-motionsvd.git@main") from ndx_facemap_motionsvd import MotionSVDMasks, MotionSVDSeries + class FacemapInterface(BaseTemporalAlignmentInterface): display_name = "Facemap" help = "Interface for Facemap output." diff --git a/tests/test_on_data/test_behavior_interfaces.py b/tests/test_on_data/test_behavior_interfaces.py index d53d372b1..defe3f90c 100644 --- a/tests/test_on_data/test_behavior_interfaces.py +++ b/tests/test_on_data/test_behavior_interfaces.py @@ -14,7 +14,7 @@ from numpy.testing import assert_array_equal from parameterized import param, parameterized from pynwb import NWBHDF5IO -from pynwb.behavior import Position, SpatialSeries, EyeTracking +from pynwb.behavior import EyeTracking, Position, SpatialSeries from neuroconv import NWBConverter from neuroconv.datainterfaces import ( From 168279326dad81e7afe7db64b2292884a73e3bda Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Tue, 21 May 2024 13:29:32 +0200 Subject: [PATCH 11/19] add get_metadata() function --- .../behavior/facemap/facemapdatainterface.py | 79 +++++++++++++------ .../test_on_data/test_behavior_interfaces.py | 16 ++-- 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py index fb19cc626..e7bcacfa6 100644 --- a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py @@ -1,6 +1,6 @@ import subprocess import sys -from typing import Optional +from typing import Optional, Literal import h5py import numpy as np @@ -9,6 +9,8 @@ from pynwb.core import DynamicTableRegion from pynwb.file import NWBFile +from neuroconv.utils.dict import DeepDict + from ..video.video_utils import get_video_timestamps from ....basetemporalalignmentinterface import BaseTemporalAlignmentInterface from ....tools import get_module @@ -63,20 +65,41 @@ def __init__( self.original_timestamps = None self.timestamps = None - def add_pupil_data(self, nwbfile: NWBFile): + def get_metadata(self) -> DeepDict: + metadata = super().get_metadata() + metadata["Behavior"]["EyeTracking"] = { + "name": "eye_center_of_mass", + "description": "The position of the eye measured in degrees.", + "reference_frame": "unknown", + "unit": "degrees", + } + metadata["Behavior"]["PupilTracking"]["area"] = { + "name": "pupil_area", + "description": "Area of pupil.", + "unit": "unknown", + } + metadata["Behavior"]["PupilTracking"]["area_raw"] = { + "name": "pupil_area_raw", + "description": "Raw unprocessed area of pupil.", + "unit": "unknown", + } + return metadata + + def add_eye_tracking(self, nwbfile: NWBFile, metadata: DeepDict): timestamps = self.get_timestamps() with h5py.File(self.source_data["mat_file_path"], "r") as file: behavior_module = get_module(nwbfile=nwbfile, name="behavior", description="behavioral data") + eye_tracking_metadata = metadata["Behavior"]["EyeTracking"] eye_com = SpatialSeries( - name="eye_center_of_mass", - description="The position of the eye measured in degrees.", + name=eye_tracking_metadata["name"], + description=eye_tracking_metadata["description"], data=file["proc"]["pupil"]["com"][:].T, - reference_frame="unknown", - unit="degrees", + reference_frame=eye_tracking_metadata["reference_frame"], + unit=eye_tracking_metadata["unit"], timestamps=timestamps, ) @@ -84,25 +107,36 @@ def add_pupil_data(self, nwbfile: NWBFile): behavior_module.add(eye_tracking) - pupil_area = TimeSeries( - name="pupil_area", - description="Area of pupil", - data=file["proc"]["pupil"]["area"][:].T, - unit="unknown", - timestamps=eye_com, - ) + def add_pupil_data(self, nwbfile: NWBFile, metadata: DeepDict, pupil_trace_type: Literal["area_raw", "area"] = "area"): - pupil_area_raw = TimeSeries( - name="pupil_area_raw", - description="Raw unprocessed area of pupil", - data=file["proc"]["pupil"]["area_raw"][:].T, - unit="unknown", + with h5py.File(self.source_data["mat_file_path"], "r") as file: + + behavior_module = get_module(nwbfile=nwbfile, name="behavior", description="behavioral data") + + pupil_area_metadata = metadata["Behavior"]["PupilTracking"][pupil_trace_type] + + if "EyeTracking" not in behavior_module.data_interfaces: + self.add_eye_tracking(nwbfile=nwbfile, metadata=metadata) + + eye_tracking_name = metadata["Behavior"]["EyeTracking"]["name"] + eye_com = behavior_module.data_interfaces["EyeTracking"].spatial_series[eye_tracking_name] + + pupil_trace = TimeSeries( + name=pupil_area_metadata["name"], + description=pupil_area_metadata["description"], + data=file["proc"]["pupil"][pupil_trace_type][:].T, + unit=pupil_area_metadata["unit"], timestamps=eye_com, ) - pupil_tracking = PupilTracking(time_series=[pupil_area, pupil_area_raw], name="PupilTracking") + if "PupilTracking" not in behavior_module.data_interfaces: + pupil_tracking = PupilTracking(name="PupilTracking") + behavior_module.add(pupil_tracking) + else: + pupil_tracking = behavior_module.data_interfaces["PupilTracking"] + + pupil_tracking.add_timeseries(pupil_trace) - behavior_module.add(pupil_tracking) def add_multivideo_motion_SVD(self, nwbfile: NWBFile): """ @@ -294,8 +328,9 @@ def add_to_nwbfile( compression_opts : int, optional Compression options. """ - - self.add_pupil_data(nwbfile=nwbfile) + # self.add_eye_tracking(nwbfile=nwbfile, metadata=metadata) + self.add_pupil_data(nwbfile=nwbfile, metadata=metadata, pupil_trace_type="area_raw") + self.add_pupil_data(nwbfile=nwbfile, metadata=metadata, pupil_trace_type="area") self.add_motion_SVD(nwbfile=nwbfile) if self.add_multivideo_motion_SVD: self.add_multivideo_motion_SVD(nwbfile=nwbfile) diff --git a/tests/test_on_data/test_behavior_interfaces.py b/tests/test_on_data/test_behavior_interfaces.py index defe3f90c..97956749f 100644 --- a/tests/test_on_data/test_behavior_interfaces.py +++ b/tests/test_on_data/test_behavior_interfaces.py @@ -753,29 +753,29 @@ class TestFacemapInterface(DataInterfaceTestMixin, TemporalAlignmentMixin, unitt @classmethod def setUpClass(cls): cls.eye_com_name = "eye_center_of_mass" - cls.eye_com_expected_metadata = DeepDict( - SpatialSeries=dict( - name="eye_com_name", + cls.eye_com_expected_metadata = dict( + name=cls.eye_com_name, description="The position of the eye measured in degrees.", reference_frame="unknown", unit="degrees", ) - ) + with h5py.File(cls.interface_kwargs["mat_file_path"], "r") as file: cls.eye_com_test_data = file["proc"]["pupil"]["com"][:].T - cls.eye_tracking_name = "EyeTracking" + cls.eye_tracking_module = "EyeTracking" def check_extracted_metadata(self, metadata: dict): - self.assertIn(self.eye_tracking_name, metadata["Behavior"]) + self.assertIn(self.eye_tracking_module, metadata["Behavior"]) + self.assertEqual(self.eye_com_expected_metadata, metadata["Behavior"]["EyeTracking"]) def check_read_nwb(self, nwbfile_path: str): with NWBHDF5IO(path=nwbfile_path, mode="r", load_namespaces=True) as io: nwbfile = io.read() self.assertIn("behavior", nwbfile.processing) - self.assertIn(self.eye_tracking_name, nwbfile.processing["behavior"].data_interfaces) - eye_tracking_container = nwbfile.processing["behavior"].data_interfaces[self.eye_tracking_name] + self.assertIn(self.eye_tracking_module, nwbfile.processing["behavior"].data_interfaces) + eye_tracking_container = nwbfile.processing["behavior"].data_interfaces[self.eye_tracking_module] self.assertIsInstance(eye_tracking_container, EyeTracking) From 6f80805aa79102d3e5763c86752a46ea4685d63a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 11:29:46 +0000 Subject: [PATCH 12/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../behavior/facemap/facemapdatainterface.py | 17 +++++++++-------- tests/test_on_data/test_behavior_interfaces.py | 12 ++++++------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py index e7bcacfa6..220fa6b2e 100644 --- a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py @@ -1,6 +1,6 @@ import subprocess import sys -from typing import Optional, Literal +from typing import Literal, Optional import h5py import numpy as np @@ -68,7 +68,7 @@ def __init__( def get_metadata(self) -> DeepDict: metadata = super().get_metadata() metadata["Behavior"]["EyeTracking"] = { - "name": "eye_center_of_mass", + "name": "eye_center_of_mass", "description": "The position of the eye measured in degrees.", "reference_frame": "unknown", "unit": "degrees", @@ -78,7 +78,7 @@ def get_metadata(self) -> DeepDict: "description": "Area of pupil.", "unit": "unknown", } - metadata["Behavior"]["PupilTracking"]["area_raw"] = { + metadata["Behavior"]["PupilTracking"]["area_raw"] = { "name": "pupil_area_raw", "description": "Raw unprocessed area of pupil.", "unit": "unknown", @@ -107,7 +107,9 @@ def add_eye_tracking(self, nwbfile: NWBFile, metadata: DeepDict): behavior_module.add(eye_tracking) - def add_pupil_data(self, nwbfile: NWBFile, metadata: DeepDict, pupil_trace_type: Literal["area_raw", "area"] = "area"): + def add_pupil_data( + self, nwbfile: NWBFile, metadata: DeepDict, pupil_trace_type: Literal["area_raw", "area"] = "area" + ): with h5py.File(self.source_data["mat_file_path"], "r") as file: @@ -117,10 +119,10 @@ def add_pupil_data(self, nwbfile: NWBFile, metadata: DeepDict, pupil_trace_type: if "EyeTracking" not in behavior_module.data_interfaces: self.add_eye_tracking(nwbfile=nwbfile, metadata=metadata) - + eye_tracking_name = metadata["Behavior"]["EyeTracking"]["name"] eye_com = behavior_module.data_interfaces["EyeTracking"].spatial_series[eye_tracking_name] - + pupil_trace = TimeSeries( name=pupil_area_metadata["name"], description=pupil_area_metadata["description"], @@ -134,9 +136,8 @@ def add_pupil_data(self, nwbfile: NWBFile, metadata: DeepDict, pupil_trace_type: behavior_module.add(pupil_tracking) else: pupil_tracking = behavior_module.data_interfaces["PupilTracking"] - - pupil_tracking.add_timeseries(pupil_trace) + pupil_tracking.add_timeseries(pupil_trace) def add_multivideo_motion_SVD(self, nwbfile: NWBFile): """ diff --git a/tests/test_on_data/test_behavior_interfaces.py b/tests/test_on_data/test_behavior_interfaces.py index 97956749f..ece73b6fb 100644 --- a/tests/test_on_data/test_behavior_interfaces.py +++ b/tests/test_on_data/test_behavior_interfaces.py @@ -754,12 +754,12 @@ class TestFacemapInterface(DataInterfaceTestMixin, TemporalAlignmentMixin, unitt def setUpClass(cls): cls.eye_com_name = "eye_center_of_mass" cls.eye_com_expected_metadata = dict( - name=cls.eye_com_name, - description="The position of the eye measured in degrees.", - reference_frame="unknown", - unit="degrees", - ) - + name=cls.eye_com_name, + description="The position of the eye measured in degrees.", + reference_frame="unknown", + unit="degrees", + ) + with h5py.File(cls.interface_kwargs["mat_file_path"], "r") as file: cls.eye_com_test_data = file["proc"]["pupil"]["com"][:].T From 52d53834fc006b1448d03ae215c541011a3a06f6 Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Tue, 21 May 2024 15:03:30 +0200 Subject: [PATCH 13/19] add tests for PupilTracking --- .../behavior/facemap/facemapdatainterface.py | 5 ++-- .../test_on_data/test_behavior_interfaces.py | 28 +++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py index 220fa6b2e..f51a17758 100644 --- a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py @@ -87,7 +87,8 @@ def get_metadata(self) -> DeepDict: def add_eye_tracking(self, nwbfile: NWBFile, metadata: DeepDict): - timestamps = self.get_timestamps() + if self.timestamps is None: + self.timestamps = self.get_timestamps() with h5py.File(self.source_data["mat_file_path"], "r") as file: @@ -100,7 +101,7 @@ def add_eye_tracking(self, nwbfile: NWBFile, metadata: DeepDict): data=file["proc"]["pupil"]["com"][:].T, reference_frame=eye_tracking_metadata["reference_frame"], unit=eye_tracking_metadata["unit"], - timestamps=timestamps, + timestamps=self.timestamps, ) eye_tracking = EyeTracking(name="EyeTracking", spatial_series=eye_com) diff --git a/tests/test_on_data/test_behavior_interfaces.py b/tests/test_on_data/test_behavior_interfaces.py index ece73b6fb..acecb0a7f 100644 --- a/tests/test_on_data/test_behavior_interfaces.py +++ b/tests/test_on_data/test_behavior_interfaces.py @@ -14,7 +14,7 @@ from numpy.testing import assert_array_equal from parameterized import param, parameterized from pynwb import NWBHDF5IO -from pynwb.behavior import EyeTracking, Position, SpatialSeries +from pynwb.behavior import EyeTracking, Position, SpatialSeries, PupilTracking from neuroconv import NWBConverter from neuroconv.datainterfaces import ( @@ -752,24 +752,39 @@ class TestFacemapInterface(DataInterfaceTestMixin, TemporalAlignmentMixin, unitt @classmethod def setUpClass(cls): - cls.eye_com_name = "eye_center_of_mass" + + cls.eye_tracking_module = "EyeTracking" cls.eye_com_expected_metadata = dict( - name=cls.eye_com_name, + name="eye_center_of_mass", description="The position of the eye measured in degrees.", reference_frame="unknown", unit="degrees", ) + cls.pupil_tracking_module = "PupilTracking" + cls.pupil_area_expected_metadata = dict( + name="pupil_area", + description="Area of pupil.", + unit="unknown", + ) + cls.pupil_area_raw__expected_metadata = dict( + name="pupil_area_raw", + description="Raw unprocessed area of pupil.", + unit="unknown", + ) + with h5py.File(cls.interface_kwargs["mat_file_path"], "r") as file: cls.eye_com_test_data = file["proc"]["pupil"]["com"][:].T - cls.eye_tracking_module = "EyeTracking" - def check_extracted_metadata(self, metadata: dict): self.assertIn(self.eye_tracking_module, metadata["Behavior"]) self.assertEqual(self.eye_com_expected_metadata, metadata["Behavior"]["EyeTracking"]) + self.assertIn(self.pupil_tracking_module, metadata["Behavior"]) + self.assertEqual(self.pupil_area_expected_metadata, metadata["Behavior"]["PupilTracking"]["area"]) + self.assertEqual(self.pupil_area_raw__expected_metadata, metadata["Behavior"]["PupilTracking"]["area_raw"]) + def check_read_nwb(self, nwbfile_path: str): with NWBHDF5IO(path=nwbfile_path, mode="r", load_namespaces=True) as io: nwbfile = io.read() @@ -777,6 +792,9 @@ def check_read_nwb(self, nwbfile_path: str): self.assertIn(self.eye_tracking_module, nwbfile.processing["behavior"].data_interfaces) eye_tracking_container = nwbfile.processing["behavior"].data_interfaces[self.eye_tracking_module] self.assertIsInstance(eye_tracking_container, EyeTracking) + self.assertIn(self.pupil_tracking_module, nwbfile.processing["behavior"].data_interfaces) + pupil_tracking_container = nwbfile.processing["behavior"].data_interfaces[self.pupil_tracking_module] + self.assertIsInstance(pupil_tracking_container, PupilTracking) if __name__ == "__main__": From 9c0aec3f78303b8240bb2cd2cd7b03f7f13262e6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 13:03:47 +0000 Subject: [PATCH 14/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../datainterfaces/behavior/facemap/facemapdatainterface.py | 2 +- tests/test_on_data/test_behavior_interfaces.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py index f51a17758..2d5fd979e 100644 --- a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py @@ -88,7 +88,7 @@ def get_metadata(self) -> DeepDict: def add_eye_tracking(self, nwbfile: NWBFile, metadata: DeepDict): if self.timestamps is None: - self.timestamps = self.get_timestamps() + self.timestamps = self.get_timestamps() with h5py.File(self.source_data["mat_file_path"], "r") as file: diff --git a/tests/test_on_data/test_behavior_interfaces.py b/tests/test_on_data/test_behavior_interfaces.py index acecb0a7f..3b65a5239 100644 --- a/tests/test_on_data/test_behavior_interfaces.py +++ b/tests/test_on_data/test_behavior_interfaces.py @@ -14,7 +14,7 @@ from numpy.testing import assert_array_equal from parameterized import param, parameterized from pynwb import NWBHDF5IO -from pynwb.behavior import EyeTracking, Position, SpatialSeries, PupilTracking +from pynwb.behavior import EyeTracking, Position, PupilTracking, SpatialSeries from neuroconv import NWBConverter from neuroconv.datainterfaces import ( @@ -771,7 +771,7 @@ def setUpClass(cls): name="pupil_area_raw", description="Raw unprocessed area of pupil.", unit="unknown", - ) + ) with h5py.File(cls.interface_kwargs["mat_file_path"], "r") as file: cls.eye_com_test_data = file["proc"]["pupil"]["com"][:].T From d92ec25a10a2f0541a38b1986cf791f0a6ad0ca9 Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Tue, 21 May 2024 15:48:10 +0200 Subject: [PATCH 15/19] motion svd metadata --- .../behavior/facemap/facemapdatainterface.py | 68 ++++++++++++------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py index f51a17758..e1f00f631 100644 --- a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py @@ -83,12 +83,20 @@ def get_metadata(self) -> DeepDict: "description": "Raw unprocessed area of pupil.", "unit": "unknown", } + metadata["Behavior"]["MotionSVDMasks"] = { + "name": "MotionSVDMasks", + "description": "Motion masks", + } + metadata["Behavior"]["MotionSVDSeries"] = { + "name": "MotionSVDSeries", + "description": "Motion SVD components", + } return metadata def add_eye_tracking(self, nwbfile: NWBFile, metadata: DeepDict): if self.timestamps is None: - self.timestamps = self.get_timestamps() + self.timestamps = self.get_timestamps() with h5py.File(self.source_data["mat_file_path"], "r") as file: @@ -140,7 +148,7 @@ def add_pupil_data( pupil_tracking.add_timeseries(pupil_trace) - def add_multivideo_motion_SVD(self, nwbfile: NWBFile): + def add_multivideo_motion_SVD(self, nwbfile: NWBFile, metadata: DeepDict): """ Add data motion SVD and motion mask for the whole video. @@ -153,14 +161,19 @@ def add_multivideo_motion_SVD(self, nwbfile: NWBFile): # From documentation # motSVD: cell array of motion SVDs [time x components] (in order: multivideo, ROI1, ROI2, ROI3) # uMotMask: cell array of motion masks [pixels x components] (in order: multivideo, ROI1, ROI2, ROI3) - # motion masks of multivideo are reported as 2D-arrays npixels x components + # motion masks of multivideo are reported as 2D-arrays npixels x + if self.timestamps is None: + self.timestamps = self.get_timestamps() + + motion_mask_name = metadata["Behavior"]["MotionSVDMasks"]["name"] + motion_mask_description = metadata["Behavior"]["MotionSVDMasks"]["description"] + motion_series_name = metadata["Behavior"]["MotionSVDSeries"]["name"] + motion_series_description = metadata["Behavior"]["MotionSVDSeries"]["description"] with h5py.File(self.source_data["mat_file_path"], "r") as file: behavior_module = get_module(nwbfile=nwbfile, name="behavior", description="behavioral data") - timestamps = self.get_timestamps() - # Extract mask_coordinates mask_coordinates = file[file[file["proc"]["ROI"][0][0]][0][0]] y1 = int(np.round(mask_coordinates[0][0]) - 1) # correct matlab indexing @@ -171,8 +184,8 @@ def add_multivideo_motion_SVD(self, nwbfile: NWBFile): # store multivideo motion mask and motion series motion_masks_table = MotionSVDMasks( - name=f"MotionSVDMasksMultivideo", - description=f"motion mask for multivideo", + name=f"{motion_mask_name}Multivideo", + description=f"{motion_mask_description} for multivideo.", mask_coordinates=mask_coordinates, downsampling_factor=self._get_downsamplig_factor(), processed_frame_dimension=self._get_processed_frame_dimension(), @@ -197,20 +210,20 @@ def add_multivideo_motion_SVD(self, nwbfile: NWBFile): data = np.array(file[series_ref]) data = data[: self.first_n_components, :] - motionsvd_series = MotionSVDSeries( - name=f"MotionSVDSeriesMultivideo", - description=f"SVD components for multivideo", + motion_series = MotionSVDSeries( + name=f"{motion_series_name}Multivideo", + description=f"{motion_series_description} for multivideo.", data=data.T, motion_masks=motion_masks, unit="unknown", - timestamps=timestamps, + timestamps=self.timestamps, ) behavior_module.add(motion_masks_table) - behavior_module.add(motionsvd_series) + behavior_module.add(motion_series) return - def add_motion_SVD(self, nwbfile: NWBFile): + def add_motion_SVD(self, nwbfile: NWBFile, metadata: DeepDict): """ Add data motion SVD and motion mask for each ROI. @@ -225,11 +238,18 @@ def add_motion_SVD(self, nwbfile: NWBFile): # uMotMask: cell array of motion masks [pixels x components] (in order: multivideo, ROI1, ROI2, ROI3) # ROIs motion masks are reported as 3D-arrays x_pixels x y_pixels x components + if self.timestamps is None: + self.timestamps = self.get_timestamps() + + motion_mask_name = metadata["Behavior"]["MotionSVDMasks"]["name"] + motion_mask_description = metadata["Behavior"]["MotionSVDMasks"]["description"] + motion_series_name = metadata["Behavior"]["MotionSVDSeries"]["name"] + motion_series_description = metadata["Behavior"]["MotionSVDSeries"]["description"] + with h5py.File(self.source_data["mat_file_path"], "r") as file: behavior_module = get_module(nwbfile=nwbfile, name="behavior", description="behavioral data") - timestamps = self.get_timestamps() downsampling_factor = self._get_downsamplig_factor() processed_frame_dimension = self._get_processed_frame_dimension() # store ROIs motion mask and motion series @@ -247,8 +267,8 @@ def add_motion_SVD(self, nwbfile: NWBFile): mask_coordinates = [x1, y1, x2, y2] motion_masks_table = MotionSVDMasks( - name=f"MotionSVDMasksROI{n}", - description=f"motion mask for ROI{n}", + name=f"{motion_mask_name}ROI{n}", + description=f"{motion_mask_description} for ROI{n}", mask_coordinates=mask_coordinates, downsampling_factor=downsampling_factor, processed_frame_dimension=processed_frame_dimension, @@ -269,18 +289,18 @@ def add_motion_SVD(self, nwbfile: NWBFile): data = np.array(file[series_ref]) data = data[: self.first_n_components, :] - motionsvd_series = MotionSVDSeries( - name=f"MotionSVDSeriesROI{n}", - description=f"SVD components for ROI{n}", + motion_series = MotionSVDSeries( + name=f"{motion_series_name}ROI{n}", + description=f"{motion_series_description} for ROI{n}", data=data.T, motion_masks=motion_masks, unit="unknown", - timestamps=timestamps, + timestamps=self.timestamps, ) n = +1 behavior_module.add(motion_masks_table) - behavior_module.add(motionsvd_series) + behavior_module.add(motion_series) return @@ -333,6 +353,6 @@ def add_to_nwbfile( # self.add_eye_tracking(nwbfile=nwbfile, metadata=metadata) self.add_pupil_data(nwbfile=nwbfile, metadata=metadata, pupil_trace_type="area_raw") self.add_pupil_data(nwbfile=nwbfile, metadata=metadata, pupil_trace_type="area") - self.add_motion_SVD(nwbfile=nwbfile) - if self.add_multivideo_motion_SVD: - self.add_multivideo_motion_SVD(nwbfile=nwbfile) + self.add_motion_SVD(nwbfile=nwbfile, metadata=metadata) + if self.include_multivideo_SVD: + self.add_multivideo_motion_SVD(nwbfile=nwbfile, metadata=metadata) From ecb5dde56f9ab7160f9fa6749c9d9b54ce09bf3d Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Tue, 21 May 2024 15:55:22 +0200 Subject: [PATCH 16/19] test yey_tracking spatial series --- tests/test_on_data/test_behavior_interfaces.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_on_data/test_behavior_interfaces.py b/tests/test_on_data/test_behavior_interfaces.py index acecb0a7f..8d4da0dc6 100644 --- a/tests/test_on_data/test_behavior_interfaces.py +++ b/tests/test_on_data/test_behavior_interfaces.py @@ -14,7 +14,7 @@ from numpy.testing import assert_array_equal from parameterized import param, parameterized from pynwb import NWBHDF5IO -from pynwb.behavior import EyeTracking, Position, SpatialSeries, PupilTracking +from pynwb.behavior import EyeTracking, Position, PupilTracking, SpatialSeries from neuroconv import NWBConverter from neuroconv.datainterfaces import ( @@ -771,10 +771,12 @@ def setUpClass(cls): name="pupil_area_raw", description="Raw unprocessed area of pupil.", unit="unknown", - ) + ) with h5py.File(cls.interface_kwargs["mat_file_path"], "r") as file: - cls.eye_com_test_data = file["proc"]["pupil"]["com"][:].T + cls.eye_tracking_test_data = file["proc"]["pupil"]["com"][:].T + cls.pupil_area_test_data = file["proc"]["pupil"]["area"][:].T + cls.pupil_area_raw_test_data = file["proc"]["pupil"]["area_raw"][:].T def check_extracted_metadata(self, metadata: dict): @@ -789,9 +791,13 @@ def check_read_nwb(self, nwbfile_path: str): with NWBHDF5IO(path=nwbfile_path, mode="r", load_namespaces=True) as io: nwbfile = io.read() self.assertIn("behavior", nwbfile.processing) + self.assertIn(self.eye_tracking_module, nwbfile.processing["behavior"].data_interfaces) eye_tracking_container = nwbfile.processing["behavior"].data_interfaces[self.eye_tracking_module] self.assertIsInstance(eye_tracking_container, EyeTracking) + eye_tracking_spatial_series = eye_tracking_container.spatial_series.values() + self.assertEqual(eye_tracking_spatial_series.data.shape, self.eye_tracking_test_data.shape) + self.assertIn(self.pupil_tracking_module, nwbfile.processing["behavior"].data_interfaces) pupil_tracking_container = nwbfile.processing["behavior"].data_interfaces[self.pupil_tracking_module] self.assertIsInstance(pupil_tracking_container, PupilTracking) From 651eeb8cbc66e8eb5b14f1bc653c8f0a95a478ba Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Tue, 21 May 2024 16:14:14 +0200 Subject: [PATCH 17/19] test pupil tracking time series --- tests/test_on_data/test_behavior_interfaces.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_on_data/test_behavior_interfaces.py b/tests/test_on_data/test_behavior_interfaces.py index 8d4da0dc6..e390ed07d 100644 --- a/tests/test_on_data/test_behavior_interfaces.py +++ b/tests/test_on_data/test_behavior_interfaces.py @@ -795,12 +795,19 @@ def check_read_nwb(self, nwbfile_path: str): self.assertIn(self.eye_tracking_module, nwbfile.processing["behavior"].data_interfaces) eye_tracking_container = nwbfile.processing["behavior"].data_interfaces[self.eye_tracking_module] self.assertIsInstance(eye_tracking_container, EyeTracking) - eye_tracking_spatial_series = eye_tracking_container.spatial_series.values() + eye_tracking_spatial_series = eye_tracking_container.spatial_series["eye_center_of_mass"] self.assertEqual(eye_tracking_spatial_series.data.shape, self.eye_tracking_test_data.shape) + assert_array_equal(eye_tracking_spatial_series.data[:], self.eye_tracking_test_data) self.assertIn(self.pupil_tracking_module, nwbfile.processing["behavior"].data_interfaces) pupil_tracking_container = nwbfile.processing["behavior"].data_interfaces[self.pupil_tracking_module] self.assertIsInstance(pupil_tracking_container, PupilTracking) + pupil_area_time_series = pupil_tracking_container.time_series["pupil_area"] + self.assertEqual(pupil_area_time_series.data.shape, self.pupil_area_test_data.shape) + assert_array_equal(pupil_area_time_series.data[:], self.pupil_area_test_data) + pupil_area_raw_time_series = pupil_tracking_container.time_series["pupil_area_raw"] + self.assertEqual(pupil_area_raw_time_series.data.shape, self.pupil_area_raw_test_data.shape) + assert_array_equal(pupil_area_raw_time_series.data[:], self.pupil_area_raw_test_data) if __name__ == "__main__": From 06302d395d3fb5964d3c9a953cd7a8c8ed33644a Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Tue, 21 May 2024 17:47:03 +0200 Subject: [PATCH 18/19] add testing for MotionSVDMasks and MotionSVDSeries --- .../test_on_data/test_behavior_interfaces.py | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/tests/test_on_data/test_behavior_interfaces.py b/tests/test_on_data/test_behavior_interfaces.py index e390ed07d..3af9909d3 100644 --- a/tests/test_on_data/test_behavior_interfaces.py +++ b/tests/test_on_data/test_behavior_interfaces.py @@ -8,6 +8,7 @@ import sleap_io from hdmf.testing import TestCase from natsort import natsorted +from ndx_facemap_motionsvd import MotionSVDMasks, MotionSVDSeries from ndx_miniscope import Miniscope from ndx_miniscope.utils import get_timestamps from ndx_pose import PoseEstimation, PoseEstimationSeries @@ -767,12 +768,22 @@ def setUpClass(cls): description="Area of pupil.", unit="unknown", ) - cls.pupil_area_raw__expected_metadata = dict( + cls.pupil_area_raw_expected_metadata = dict( name="pupil_area_raw", description="Raw unprocessed area of pupil.", unit="unknown", ) + cls.motion_masks_module = "MotionSVDMasks" + cls.motion_masks_expected_metadata = dict( + name="MotionSVDMasks", + description="Motion masks", + ) + cls.motion_series_module = "MotionSVDSeries" + cls.motion_series_expected_metadata = dict( + name="MotionSVDSeries", + description="Motion SVD components", + ) with h5py.File(cls.interface_kwargs["mat_file_path"], "r") as file: cls.eye_tracking_test_data = file["proc"]["pupil"]["com"][:].T cls.pupil_area_test_data = file["proc"]["pupil"]["area"][:].T @@ -785,7 +796,13 @@ def check_extracted_metadata(self, metadata: dict): self.assertIn(self.pupil_tracking_module, metadata["Behavior"]) self.assertEqual(self.pupil_area_expected_metadata, metadata["Behavior"]["PupilTracking"]["area"]) - self.assertEqual(self.pupil_area_raw__expected_metadata, metadata["Behavior"]["PupilTracking"]["area_raw"]) + self.assertEqual(self.pupil_area_raw_expected_metadata, metadata["Behavior"]["PupilTracking"]["area_raw"]) + + self.assertIn(self.motion_masks_module, metadata["Behavior"]) + self.assertEqual(self.motion_masks_expected_metadata, metadata["Behavior"]["MotionSVDMasks"]) + + self.assertIn(self.motion_series_module, metadata["Behavior"]) + self.assertEqual(self.motion_series_expected_metadata, metadata["Behavior"]["MotionSVDSeries"]) def check_read_nwb(self, nwbfile_path: str): with NWBHDF5IO(path=nwbfile_path, mode="r", load_namespaces=True) as io: @@ -809,6 +826,29 @@ def check_read_nwb(self, nwbfile_path: str): self.assertEqual(pupil_area_raw_time_series.data.shape, self.pupil_area_raw_test_data.shape) assert_array_equal(pupil_area_raw_time_series.data[:], self.pupil_area_raw_test_data) + self.assertIn("MotionSVDMasksMultivideo", nwbfile.processing["behavior"].data_interfaces) + motion_masks_container = nwbfile.processing["behavior"].data_interfaces["MotionSVDMasksMultivideo"] + self.assertIsInstance(motion_masks_container, MotionSVDMasks) + assert_array_equal(motion_masks_container.processed_frame_dimension[:], [295, 288]) + assert_array_equal(motion_masks_container.mask_coordinates[:], [49, 0, 294, 287]) + self.assertEqual(motion_masks_container.downsampling_factor, 4.0) + self.assertEqual(motion_masks_container["image_mask"].shape[0], 3) + self.assertIn("MotionSVDSeriesMultivideo", nwbfile.processing["behavior"].data_interfaces) + motion_seires_container = nwbfile.processing["behavior"].data_interfaces["MotionSVDSeriesMultivideo"] + self.assertIsInstance(motion_seires_container, MotionSVDSeries) + self.assertEqual(motion_seires_container.data.shape[0], 18078) + self.assertIn("MotionSVDMasksROI1", nwbfile.processing["behavior"].data_interfaces) + motion_masks_container = nwbfile.processing["behavior"].data_interfaces["MotionSVDMasksROI1"] + self.assertIsInstance(motion_masks_container, MotionSVDMasks) + assert_array_equal(motion_masks_container.processed_frame_dimension[:], [295, 288]) + assert_array_equal(motion_masks_container.mask_coordinates[:], [147, 112, 279, 240]) + self.assertEqual(motion_masks_container.downsampling_factor, 4.0) + self.assertEqual(motion_masks_container["image_mask"].shape[0], 3) + self.assertIn("MotionSVDSeriesROI1", nwbfile.processing["behavior"].data_interfaces) + motion_seires_container = nwbfile.processing["behavior"].data_interfaces["MotionSVDSeriesROI1"] + self.assertIsInstance(motion_seires_container, MotionSVDSeries) + self.assertEqual(motion_seires_container.data.shape[0], 18078) + if __name__ == "__main__": unittest.main() From f067458ae43d0d6b6bf966878d8e754f6276a118 Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Thu, 13 Jun 2024 13:48:28 +0200 Subject: [PATCH 19/19] set check_ragged to False to speed up add_row --- .../behavior/facemap/facemapdatainterface.py | 95 +++++++++++++------ 1 file changed, 64 insertions(+), 31 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py index e1f00f631..96d572a94 100644 --- a/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/facemap/facemapdatainterface.py @@ -14,7 +14,7 @@ from ..video.video_utils import get_video_timestamps from ....basetemporalalignmentinterface import BaseTemporalAlignmentInterface from ....tools import get_module -from ....utils import FilePathType +from ....utils import FilePathType, get_base_schema, get_schema_from_hdmf_class def install_package(package): @@ -65,32 +65,64 @@ def __init__( self.original_timestamps = None self.timestamps = None + def get_metadata_schema(self) -> dict: + metadata_schema = super().get_metadata_schema() + metadata_schema["properties"]["Behavior"] = get_base_schema(tag="Behavior") + spatial_series_metadata_schema = get_schema_from_hdmf_class(SpatialSeries) + time_series_metadata_schema = get_schema_from_hdmf_class(TimeSeries) + metadata_schema["properties"]["Behavior"].update( + required=["EyeTracking", "PupilTracking", "MotionSVDMasks", "MotionSVDSeries"], + properties=dict( + EyeTracking=dict( + type="array", + minItems=1, + items=spatial_series_metadata_schema, + ), + PupilTracking=dict( + type="array", + minItems=1, + items=time_series_metadata_schema, + ), + MotionSVDMasks=dict( + type="object", + properties=dict( + name=dict(type="string"), + description=dict(type="string"), + ), + required=["name", "description"], + ), + MotionSVDSeries=dict( + type="object", + properties=dict( + name=dict(type="string"), + description=dict(type="string"), + ), + required=["name", "description"], + ), + ), + ) + + return metadata_schema + def get_metadata(self) -> DeepDict: metadata = super().get_metadata() - metadata["Behavior"]["EyeTracking"] = { - "name": "eye_center_of_mass", - "description": "The position of the eye measured in degrees.", - "reference_frame": "unknown", - "unit": "degrees", - } - metadata["Behavior"]["PupilTracking"]["area"] = { - "name": "pupil_area", - "description": "Area of pupil.", - "unit": "unknown", - } - metadata["Behavior"]["PupilTracking"]["area_raw"] = { - "name": "pupil_area_raw", - "description": "Raw unprocessed area of pupil.", - "unit": "unknown", - } - metadata["Behavior"]["MotionSVDMasks"] = { - "name": "MotionSVDMasks", - "description": "Motion masks", - } - metadata["Behavior"]["MotionSVDSeries"] = { - "name": "MotionSVDSeries", - "description": "Motion SVD components", - } + behavior_metadata = dict( + EyeTracking=[ + dict( + name="eye_center_of_mass", + description="The position of the eye measured in degrees.", + reference_frame="unknown", + unit="degrees", + ) + ], + PupilTracking=[ + dict(name="pupil_area", description="Area of pupil.", unit="unknown"), + dict(name="pupil_area_raw", description="Raw unprocessed area of pupil.", unit="unknown"), + ], + MotionSVDMasks=dict(name="MotionSVDMasks", description="Motion masks"), + MotionSVDSeries=dict(name="MotionSVDSeries", description="Motion SVD components"), + ) + metadata["Behavior"] = behavior_metadata return metadata def add_eye_tracking(self, nwbfile: NWBFile, metadata: DeepDict): @@ -101,7 +133,7 @@ def add_eye_tracking(self, nwbfile: NWBFile, metadata: DeepDict): with h5py.File(self.source_data["mat_file_path"], "r") as file: behavior_module = get_module(nwbfile=nwbfile, name="behavior", description="behavioral data") - eye_tracking_metadata = metadata["Behavior"]["EyeTracking"] + eye_tracking_metadata = metadata["Behavior"]["EyeTracking"][0] eye_com = SpatialSeries( name=eye_tracking_metadata["name"], @@ -124,12 +156,13 @@ def add_pupil_data( behavior_module = get_module(nwbfile=nwbfile, name="behavior", description="behavioral data") - pupil_area_metadata = metadata["Behavior"]["PupilTracking"][pupil_trace_type] + pupil_area_metadata_ind = 0 if pupil_trace_type == "area" else 1 + pupil_area_metadata = metadata["Behavior"]["PupilTracking"][pupil_area_metadata_ind] if "EyeTracking" not in behavior_module.data_interfaces: self.add_eye_tracking(nwbfile=nwbfile, metadata=metadata) - eye_tracking_name = metadata["Behavior"]["EyeTracking"]["name"] + eye_tracking_name = metadata["Behavior"]["EyeTracking"][0]["name"] eye_com = behavior_module.data_interfaces["EyeTracking"].spatial_series[eye_tracking_name] pupil_trace = TimeSeries( @@ -197,7 +230,7 @@ def add_multivideo_motion_SVD(self, nwbfile: NWBFile, metadata: DeepDict): if c == self.first_n_components: break componendt_2d = component.reshape((y2 - y1, x2 - x1)) - motion_masks_table.add_row(image_mask=componendt_2d.T) + motion_masks_table.add_row(image_mask=componendt_2d.T, check_ragged=False) motion_masks = DynamicTableRegion( name="motion_masks", @@ -258,7 +291,7 @@ def add_motion_SVD(self, nwbfile: NWBFile, metadata: DeepDict): series_ref = series_ref[0] mask_ref = mask_ref[0] - # skipping the first ROI because it referes to "running" mask, from Facemap doc + # skipping the first ROI because it refers to "running" mask, from Facemap doc mask_coordinates = file[file["proc"]["locROI"][n][0]] y1 = int(np.round(mask_coordinates[0][0]) - 1) # correct matlab indexing x1 = int(np.round(mask_coordinates[1][0]) - 1) # correct matlab indexing @@ -277,7 +310,7 @@ def add_motion_SVD(self, nwbfile: NWBFile, metadata: DeepDict): for c, component in enumerate(file[mask_ref]): if c == self.first_n_components: break - motion_masks_table.add_row(image_mask=component.T) + motion_masks_table.add_row(image_mask=component.T, check_ragged=False) motion_masks = DynamicTableRegion( name="motion_masks",