From f32a684839dc4b58ee2521b4bc08b39e4f98a433 Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:07:34 -0400 Subject: [PATCH 01/22] port over dlc utils --- .../behavior/deeplabcut/_dlc_utils.py | 314 ++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py new file mode 100644 index 000000000..01a10de62 --- /dev/null +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py @@ -0,0 +1,314 @@ +import datetime +import os +import pickle +import warnings +from pathlib import Path +from platform import python_version +import importlib + +import cv2 +import yaml +import numpy as np +import pandas as pd +from hdmf.build.warnings import DtypeConversionWarning +from packaging.version import Version # Installed with setuptools +from pynwb import NWBFile, NWBHDF5IO +from ndx_pose import PoseEstimationSeries, PoseEstimation +from ruamel.yaml import YAML + + +def _read_config(configname): + """ + Reads structured config file defining a project. + """ + ruamelFile = YAML() + path = Path(configname) + if os.path.exists(path): + try: + with open(path, "r") as f: + cfg = ruamelFile.load(f) + curr_dir = os.path.dirname(configname) + if cfg["project_path"] != curr_dir: + cfg["project_path"] = curr_dir + except Exception as err: + if len(err.args) > 2: + if ( + err.args[2] + == "could not determine a constructor for the tag '!!python/tuple'" + ): + with open(path, "r") as ymlfile: + cfg = yaml.load(ymlfile, Loader=yaml.SafeLoader) + else: + raise + + else: + raise FileNotFoundError( + "Config file is not found. Please make sure that the file exists and/or that you passed the path of the config file correctly!" + ) + return cfg + + + +def _get_movie_timestamps(movie_file, VARIABILITYBOUND=1000, infer_timestamps=True): + """ + Return numpy array of the timestamps for a video. + + Parameters + ---------- + movie_file : str + Path to movie_file + """ + # TODO: consider moving this to DLC, and actually extract alongside video analysis! + + reader = cv2.VideoCapture(movie_file) + timestamps = [] + for _ in range(len(reader)): + _ = reader.read() + timestamps.append(reader.get(cv2.CAP_PROP_POS_MSEC)) + + timestamps = np.array(timestamps) / 1000 # Convert to seconds + + if np.nanvar(np.diff(timestamps)) < 1.0 / reader.fps * 1.0 / VARIABILITYBOUND: + warnings.warn( + "Variability of timestamps suspiciously small. See: https://github.com/DeepLabCut/DLC2NWB/issues/1" + ) + + if any(timestamps[1:] == 0): + # Infers times when OpenCV provides 0s + warning_msg = "Removing" + timestamp_zero_count = np.count_nonzero(timestamps == 0) + timestamps[1:][timestamps[1:] == 0] = np.nan # replace 0s with nan + + if infer_timestamps: + warning_msg = "Replacing" + timestamps = _infer_nan_timestamps(timestamps) + + warnings.warn( # warns user of percent of 0 frames + "%s cv2 timestamps returned as 0: %f%%" + % (warning_msg, ( timestamp_zero_count / len(timestamps) * 100)) + ) + + return timestamps + + +def _infer_nan_timestamps(timestamps): + """Given np.array, interpolate nan values using index * sampling rate""" + bad_timestamps_mask = np.isnan(timestamps) + # Runs of good timestamps + good_run_indices = np.where( + np.diff(np.hstack(([False], bad_timestamps_mask == False, [False]))) + )[0].reshape(-1, 2) + + # For each good run, get the diff and append to cumulative array + sampling_diffs = np.array([]) + for idx in good_run_indices: + sampling_diffs = np.append(sampling_diffs, np.diff(timestamps[idx[0]:idx[1]])) + estimated_sampling_rate = np.mean(sampling_diffs) # Average over diffs + + # Infer timestamps with avg sampling rate + bad_timestamps_indexes = np.argwhere(bad_timestamps_mask)[:, 0] + inferred_timestamps = bad_timestamps_indexes * estimated_sampling_rate + timestamps[bad_timestamps_mask] = inferred_timestamps + + return timestamps + + +def _ensure_individuals_in_header(df, dummy_name): + if "individuals" not in df.columns.names: + # Single animal project -> add individual row to + # the header of single animal projects. + temp = pd.concat({dummy_name: df}, names=["individuals"], axis=1) + df = temp.reorder_levels( + ["scorer", "individuals", "bodyparts", "coords"], axis=1 + ) + return df + + +def _get_pes_args(config_file, h5file, individual_name, infer_timestamps=True): + if "DLC" not in h5file or not h5file.endswith(".h5"): + raise IOError("The file passed in is not a DeepLabCut h5 data file.") + + cfg = _read_config(config_file) + + vidname, scorer = os.path.split(h5file)[-1].split("DLC") + scorer = "DLC" + os.path.splitext(scorer)[0] + video = None + + df = _ensure_individuals_in_header(pd.read_hdf(h5file), individual_name) + + # Fetch the corresponding metadata pickle file + paf_graph = [] + filename, _ = os.path.splitext(h5file) + for i, c in enumerate(filename[::-1]): + if c.isnumeric(): + break + if i > 0: + filename = filename[:-i] + metadata_file = filename + "_meta.pickle" + if os.path.isfile(metadata_file): + with open(metadata_file, "rb") as file: + metadata = pickle.load(file) + test_cfg = metadata["data"]["DLC-model-config file"] + paf_graph = test_cfg.get("partaffinityfield_graph", []) + if paf_graph: + paf_inds = test_cfg.get("paf_best") + if paf_inds is not None: + paf_graph = [paf_graph[i] for i in paf_inds] + else: + warnings.warn("Metadata not found...") + + for video_path, params in cfg["video_sets"].items(): + if vidname in video_path: + video = video_path, params["crop"] + break + + if video is None: + warnings.warn(f"The video file corresponding to {h5file} could not be found...") + video = "fake_path", "0, 0, 0, 0" + + timestamps = ( + df.index.tolist() + ) # setting timestamps to dummy TODO: extract timestamps in DLC? + else: + timestamps = _get_movie_timestamps(video[0], infer_timestamps=infer_timestamps) + return scorer, df, video, paf_graph, timestamps, cfg + + +def _write_pes_to_nwbfile( + nwbfile, + animal, + df_animal, + scorer, + video, # Expects this to be a tuple; first index is string path, second is the image shape as "0, width, 0, height" + paf_graph, + timestamps, + exclude_nans, +): + pose_estimation_series = [] + for kpt, xyp in df_animal.groupby(level="bodyparts", axis=1, sort=False): + data = xyp.to_numpy() + + if exclude_nans: + # exclude_nans is inverse infer_timestamps. if not infer, there may be nans + data = data[~np.isnan(timestamps)] + timestamps_cleaned = timestamps[~np.isnan(timestamps)] + else: + timestamps_cleaned = timestamps + + pes = PoseEstimationSeries( + name=f"{animal}_{kpt}", + description=f"Keypoint {kpt} from individual {animal}.", + data=data[:, :2], + unit="pixels", + reference_frame="(0,0) corresponds to the bottom left corner of the video.", + timestamps=timestamps_cleaned, + confidence=data[:, 2], + confidence_definition="Softmax output of the deep neural network.", + ) + pose_estimation_series.append(pes) + + deeplabcut_version = None + is_deeplabcut_installed = importlib.util.find_spec(name="deeplabcut") is not None + if is_deeplabcut_installed: + deeplabcut_version = importlib.metadata.version(distribution_name="deeplabcut") + pe = PoseEstimation( + pose_estimation_series=pose_estimation_series, + description="2D keypoint coordinates estimated using DeepLabCut.", + original_videos=[video[0]], + # TODO check if this is a mandatory arg in ndx-pose (can skip if video is not found_ + dimensions=[list(map(int, video[1].split(",")))[1::2]], + scorer=scorer, + source_software="DeepLabCut", + source_software_version=deeplabcut_version, + nodes=[pes.name for pes in pose_estimation_series], + edges=paf_graph if paf_graph else None, + ) + if 'behavior' in nwbfile.processing: + behavior_pm = nwbfile.processing["behavior"] + else: + behavior_pm = nwbfile.create_processing_module( + name="behavior", description="processed behavioral data" + ) + behavior_pm.add(pe) + return nwbfile + + +def write_subject_to_nwb(nwbfile, h5file, individual_name, config_file): + """ + Given, subject name, write h5file to an existing nwbfile. + + Parameters + ---------- + nwbfile: pynwb.NWBFile + nwbfile to write the subject specific pose estimation series. + h5file : str + Path to a h5 data file + individual_name : str + Name of the subject (whose pose is predicted) for single-animal DLC project. + For multi-animal projects, the names from the DLC project will be used directly. + config_file : str + Path to a project config.yaml file + config_dict : dict + dict containing configuration options. Provide this as alternative to config.yml file. + + Returns + ------- + nwbfile: pynwb.NWBFile + nwbfile with pes written in the behavior module + """ + scorer, df, video, paf_graph, timestamps, _ = _get_pes_args(config_file, h5file, individual_name) + df_animal = df.groupby(level="individuals", axis=1).get_group(individual_name) + return _write_pes_to_nwbfile(nwbfile, individual_name, df_animal, scorer, video, paf_graph, timestamps) + + +def convert_h5_to_nwb(config, h5file, individual_name="ind1", infer_timestamps=True): + """ + Convert a DeepLabCut (DLC) video prediction, h5 data file to Neurodata Without Borders (NWB). Also + takes project config, to store relevant metadata. + + Parameters + ---------- + config : str + Path to a project config.yaml file + + h5file : str + Path to a h5 data file + + individual_name : str + Name of the subject (whose pose is predicted) for single-animal DLC project. + For multi-animal projects, the names from the DLC project will be used directly. + + infer_timestamps : bool + Default True. Uses framerate to infer the timestamps returned as 0 from OpenCV. + If False, exclude these frames from resulting NWB file. + + TODO: allow one to overwrite those names, with a mapping? + + Returns + ------- + list of str + List of paths to the newly created NWB data files. + By default NWB files are stored in the same folder as the h5file. + + """ + scorer, df, video, paf_graph, timestamps, cfg = _get_pes_args(config, h5file, individual_name, + infer_timestamps=infer_timestamps) + output_paths = [] + for animal, df_ in df.groupby(level="individuals", axis=1): + nwbfile = NWBFile( + session_description=cfg["Task"], + experimenter=cfg["scorer"], + identifier=scorer, + session_start_time=datetime.datetime.now(datetime.timezone.utc), + ) + + # TODO Store the test_pose_config as well? + nwbfile = _write_pes_to_nwbfile(nwbfile, animal, df_, scorer, video, paf_graph, timestamps, + exclude_nans=(not infer_timestamps)) + output_path = h5file.replace(".h5", f"_{animal}.nwb") + with warnings.catch_warnings(), NWBHDF5IO(output_path, mode="w") as io: + warnings.filterwarnings("ignore", category=DtypeConversionWarning) + io.write(nwbfile) + output_paths.append(output_path) + + return output_paths From 76726b5270b4541561ad7fa139618109009917b6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:11:49 +0000 Subject: [PATCH 02/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../behavior/deeplabcut/_dlc_utils.py | 65 ++++++++----------- 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py index 01a10de62..f1d8d7b64 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py @@ -1,19 +1,19 @@ import datetime +import importlib import os import pickle import warnings from pathlib import Path from platform import python_version -import importlib import cv2 -import yaml import numpy as np import pandas as pd +import yaml from hdmf.build.warnings import DtypeConversionWarning +from ndx_pose import PoseEstimation, PoseEstimationSeries from packaging.version import Version # Installed with setuptools -from pynwb import NWBFile, NWBHDF5IO -from ndx_pose import PoseEstimationSeries, PoseEstimation +from pynwb import NWBHDF5IO, NWBFile from ruamel.yaml import YAML @@ -32,10 +32,7 @@ def _read_config(configname): cfg["project_path"] = curr_dir except Exception as err: if len(err.args) > 2: - if ( - err.args[2] - == "could not determine a constructor for the tag '!!python/tuple'" - ): + if err.args[2] == "could not determine a constructor for the tag '!!python/tuple'": with open(path, "r") as ymlfile: cfg = yaml.load(ymlfile, Loader=yaml.SafeLoader) else: @@ -48,7 +45,6 @@ def _read_config(configname): return cfg - def _get_movie_timestamps(movie_file, VARIABILITYBOUND=1000, infer_timestamps=True): """ Return numpy array of the timestamps for a video. @@ -77,15 +73,14 @@ def _get_movie_timestamps(movie_file, VARIABILITYBOUND=1000, infer_timestamps=Tr # Infers times when OpenCV provides 0s warning_msg = "Removing" timestamp_zero_count = np.count_nonzero(timestamps == 0) - timestamps[1:][timestamps[1:] == 0] = np.nan # replace 0s with nan + timestamps[1:][timestamps[1:] == 0] = np.nan # replace 0s with nan if infer_timestamps: warning_msg = "Replacing" timestamps = _infer_nan_timestamps(timestamps) - warnings.warn( # warns user of percent of 0 frames - "%s cv2 timestamps returned as 0: %f%%" - % (warning_msg, ( timestamp_zero_count / len(timestamps) * 100)) + warnings.warn( # warns user of percent of 0 frames + "%s cv2 timestamps returned as 0: %f%%" % (warning_msg, (timestamp_zero_count / len(timestamps) * 100)) ) return timestamps @@ -95,16 +90,14 @@ def _infer_nan_timestamps(timestamps): """Given np.array, interpolate nan values using index * sampling rate""" bad_timestamps_mask = np.isnan(timestamps) # Runs of good timestamps - good_run_indices = np.where( - np.diff(np.hstack(([False], bad_timestamps_mask == False, [False]))) - )[0].reshape(-1, 2) - + good_run_indices = np.where(np.diff(np.hstack(([False], bad_timestamps_mask == False, [False]))))[0].reshape(-1, 2) + # For each good run, get the diff and append to cumulative array sampling_diffs = np.array([]) - for idx in good_run_indices: - sampling_diffs = np.append(sampling_diffs, np.diff(timestamps[idx[0]:idx[1]])) - estimated_sampling_rate = np.mean(sampling_diffs) # Average over diffs - + for idx in good_run_indices: + sampling_diffs = np.append(sampling_diffs, np.diff(timestamps[idx[0] : idx[1]])) + estimated_sampling_rate = np.mean(sampling_diffs) # Average over diffs + # Infer timestamps with avg sampling rate bad_timestamps_indexes = np.argwhere(bad_timestamps_mask)[:, 0] inferred_timestamps = bad_timestamps_indexes * estimated_sampling_rate @@ -118,9 +111,7 @@ def _ensure_individuals_in_header(df, dummy_name): # Single animal project -> add individual row to # the header of single animal projects. temp = pd.concat({dummy_name: df}, names=["individuals"], axis=1) - df = temp.reorder_levels( - ["scorer", "individuals", "bodyparts", "coords"], axis=1 - ) + df = temp.reorder_levels(["scorer", "individuals", "bodyparts", "coords"], axis=1) return df @@ -166,9 +157,7 @@ def _get_pes_args(config_file, h5file, individual_name, infer_timestamps=True): warnings.warn(f"The video file corresponding to {h5file} could not be found...") video = "fake_path", "0, 0, 0, 0" - timestamps = ( - df.index.tolist() - ) # setting timestamps to dummy TODO: extract timestamps in DLC? + timestamps = df.index.tolist() # setting timestamps to dummy TODO: extract timestamps in DLC? else: timestamps = _get_movie_timestamps(video[0], infer_timestamps=infer_timestamps) return scorer, df, video, paf_graph, timestamps, cfg @@ -183,15 +172,15 @@ def _write_pes_to_nwbfile( paf_graph, timestamps, exclude_nans, -): +): pose_estimation_series = [] for kpt, xyp in df_animal.groupby(level="bodyparts", axis=1, sort=False): data = xyp.to_numpy() - if exclude_nans: + if exclude_nans: # exclude_nans is inverse infer_timestamps. if not infer, there may be nans data = data[~np.isnan(timestamps)] - timestamps_cleaned = timestamps[~np.isnan(timestamps)] + timestamps_cleaned = timestamps[~np.isnan(timestamps)] else: timestamps_cleaned = timestamps @@ -223,12 +212,10 @@ def _write_pes_to_nwbfile( nodes=[pes.name for pes in pose_estimation_series], edges=paf_graph if paf_graph else None, ) - if 'behavior' in nwbfile.processing: + if "behavior" in nwbfile.processing: behavior_pm = nwbfile.processing["behavior"] else: - behavior_pm = nwbfile.create_processing_module( - name="behavior", description="processed behavioral data" - ) + behavior_pm = nwbfile.create_processing_module(name="behavior", description="processed behavioral data") behavior_pm.add(pe) return nwbfile @@ -291,8 +278,9 @@ def convert_h5_to_nwb(config, h5file, individual_name="ind1", infer_timestamps=T By default NWB files are stored in the same folder as the h5file. """ - scorer, df, video, paf_graph, timestamps, cfg = _get_pes_args(config, h5file, individual_name, - infer_timestamps=infer_timestamps) + scorer, df, video, paf_graph, timestamps, cfg = _get_pes_args( + config, h5file, individual_name, infer_timestamps=infer_timestamps + ) output_paths = [] for animal, df_ in df.groupby(level="individuals", axis=1): nwbfile = NWBFile( @@ -303,8 +291,9 @@ def convert_h5_to_nwb(config, h5file, individual_name="ind1", infer_timestamps=T ) # TODO Store the test_pose_config as well? - nwbfile = _write_pes_to_nwbfile(nwbfile, animal, df_, scorer, video, paf_graph, timestamps, - exclude_nans=(not infer_timestamps)) + nwbfile = _write_pes_to_nwbfile( + nwbfile, animal, df_, scorer, video, paf_graph, timestamps, exclude_nans=(not infer_timestamps) + ) output_path = h5file.replace(".h5", f"_{animal}.nwb") with warnings.catch_warnings(), NWBHDF5IO(output_path, mode="w") as io: warnings.filterwarnings("ignore", category=DtypeConversionWarning) From 0ab7fe77870e733dc76412a063138846917d8d3c Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:12:55 -0400 Subject: [PATCH 03/22] remove from requirements --- .../datainterfaces/behavior/deeplabcut/requirements.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/requirements.txt b/src/neuroconv/datainterfaces/behavior/deeplabcut/requirements.txt index 17d8300fb..29659c517 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/requirements.txt +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/requirements.txt @@ -1,4 +1,2 @@ -dlc2nwb>=0.3 -tables<3.9.0;python_version<'3.9' # imported by package but not included in pip setup (is included in setup.cfg) tables<3.9.2;sys_platform=="darwin" tables;sys_platform=="linux" or sys_platform=="win32" From 3882cdb9a0615acbd6ef3647c1a244751fb3216b Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:14:34 -0400 Subject: [PATCH 04/22] adjust lazy import --- .../behavior/deeplabcut/deeplabcutdatainterface.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py index 8fa1d3cf1..f946e00fc 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py @@ -37,14 +37,14 @@ def write_subject_to_nwb( nwbfile : pynwb.NWBFile nwbfile with pes written in the behavior module """ - dlc2nwb = get_package(package_name="dlc2nwb") + from ._dlc_utils import _get_pes_args, _write_pes_to_nwbfile - scorer, df, video, paf_graph, dlc_timestamps, _ = dlc2nwb.utils._get_pes_args(config_file, h5file, individual_name) + scorer, df, video, paf_graph, dlc_timestamps, _ = _get_pes_args(config_file, h5file, individual_name) if timestamps is None: timestamps = dlc_timestamps df_animal = df.groupby(level="individuals", axis=1).get_group(individual_name) - return dlc2nwb.utils._write_pes_to_nwbfile( + return _write_pes_to_nwbfile( nwbfile, individual_name, df_animal, scorer, video, paf_graph, timestamps, exclude_nans=False ) @@ -126,7 +126,6 @@ def set_aligned_timestamps(self, aligned_timestamps: Union[List, np.ndarray]): aligned_timestamps : list, np.ndarray alternative timestamps vector. """ - self._timestamps = np.array(aligned_timestamps) def add_to_nwbfile( @@ -144,7 +143,6 @@ def add_to_nwbfile( metadata: dict metadata info for constructing the nwb file (optional). """ - write_subject_to_nwb( nwbfile=nwbfile, h5file=str(self.source_data["file_path"]), From 82555a53fff0815260377c48e9fac0b5ad504a85 Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:14:54 -0400 Subject: [PATCH 05/22] remove 3.8 classifier --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 1af10b6c3..e06c90077 100644 --- a/setup.py +++ b/setup.py @@ -88,7 +88,6 @@ license="BSD-3-Clause", classifiers=[ "Intended Audience :: Science/Research", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From 2696417ae4f6b7c83cc172d097e2b0106c168617 Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:18:54 -0400 Subject: [PATCH 06/22] Update CHANGELOG.md --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04e10f6a7..14ec72857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,7 @@ * Make annotations from the raw format available on `IntanRecordingInterface`. [PR #934](https://github.com/catalystneuro/neuroconv/pull/943) * Add an option to suppress display the progress bar (tqdm) in `VideoContext` [PR #937](https://github.com/catalystneuro/neuroconv/pull/937) * Automatic compression of data in the `LightnignPoseDataInterface` has been disabled - users should refer to the new `configure_backend` method for a general approach for setting compression. [PR #942](https://github.com/catalystneuro/neuroconv/pull/942) - - +* Port over `dlc2nwb` utility functions for ease of maintenance. [PR #946](https://github.com/catalystneuro/neuroconv/pull/946) From 5d42589571141142390b3b35b89f69dabb03cb0e Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:27:23 -0400 Subject: [PATCH 07/22] remove unused functions, move over essential piece --- .../behavior/deeplabcut/_dlc_utils.py | 104 +++++++----------- 1 file changed, 37 insertions(+), 67 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py index f1d8d7b64..b546922ad 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py @@ -5,6 +5,7 @@ import warnings from pathlib import Path from platform import python_version +from typing import Optional, Union, List import cv2 import numpy as np @@ -16,6 +17,7 @@ from pynwb import NWBHDF5IO, NWBFile from ruamel.yaml import YAML +from ....utils import FilePathType def _read_config(configname): """ @@ -58,13 +60,20 @@ def _get_movie_timestamps(movie_file, VARIABILITYBOUND=1000, infer_timestamps=Tr reader = cv2.VideoCapture(movie_file) timestamps = [] + n_frames = int(reader.get(cv2.CAP_PROP_FRAME_COUNT)) + fps = reader.get(cv2.CAP_PROP_FPS) + + for _ in range(n_frames): + _ = reader.read() + timestamps.append(reader.get(cv2.CAP_PROP_POS_MSEC)) + for _ in range(len(reader)): _ = reader.read() timestamps.append(reader.get(cv2.CAP_PROP_POS_MSEC)) timestamps = np.array(timestamps) / 1000 # Convert to seconds - if np.nanvar(np.diff(timestamps)) < 1.0 / reader.fps * 1.0 / VARIABILITYBOUND: + if np.nanvar(np.diff(timestamps)) < 1.0 / fps * 1.0 / VARIABILITYBOUND: warnings.warn( "Variability of timestamps suspiciously small. See: https://github.com/DeepLabCut/DLC2NWB/issues/1" ) @@ -172,7 +181,10 @@ def _write_pes_to_nwbfile( paf_graph, timestamps, exclude_nans, + pose_estimation_container_kwargs: Optional[dict] = None, ): + pose_estimation_container_kwargs = pose_estimation_container_kwargs or dict() + pose_estimation_series = [] for kpt, xyp in df_animal.groupby(level="bodyparts", axis=1, sort=False): data = xyp.to_numpy() @@ -211,6 +223,7 @@ def _write_pes_to_nwbfile( source_software_version=deeplabcut_version, nodes=[pes.name for pes in pose_estimation_series], edges=paf_graph if paf_graph else None, + **pose_estimation_container_kwargs, ) if "behavior" in nwbfile.processing: behavior_pm = nwbfile.processing["behavior"] @@ -220,84 +233,41 @@ def _write_pes_to_nwbfile( return nwbfile -def write_subject_to_nwb(nwbfile, h5file, individual_name, config_file): +def add_subject_to_nwbfile( + nwbfile: NWBFile, + h5file: FilePathType, + individual_name: str, + config_file: FilePathType, + timestamps: Optional[Union[List, np.ndarray]] = None, +) -> NWBFile: """ - Given, subject name, write h5file to an existing nwbfile. + Given the subject name, add the DLC .h5 file to an in-memory NWBFile object. Parameters ---------- - nwbfile: pynwb.NWBFile - nwbfile to write the subject specific pose estimation series. - h5file : str - Path to a h5 data file + nwbfile : pynwb.NWBFile + The in-memory nwbfile object to which the subject specific pose estimation series will be added. + h5file : str or path + Path to the DeepLabCut .h5 output file. individual_name : str Name of the subject (whose pose is predicted) for single-animal DLC project. For multi-animal projects, the names from the DLC project will be used directly. - config_file : str + config_file : str or path Path to a project config.yaml file - config_dict : dict - dict containing configuration options. Provide this as alternative to config.yml file. + timestamps : list, np.ndarray or None, default: None + Alternative timestamps vector. If None, then use the inferred timestamps from DLC2NWB Returns ------- - nwbfile: pynwb.NWBFile + nwbfile : pynwb.NWBFile nwbfile with pes written in the behavior module """ - scorer, df, video, paf_graph, timestamps, _ = _get_pes_args(config_file, h5file, individual_name) - df_animal = df.groupby(level="individuals", axis=1).get_group(individual_name) - return _write_pes_to_nwbfile(nwbfile, individual_name, df_animal, scorer, video, paf_graph, timestamps) - - -def convert_h5_to_nwb(config, h5file, individual_name="ind1", infer_timestamps=True): - """ - Convert a DeepLabCut (DLC) video prediction, h5 data file to Neurodata Without Borders (NWB). Also - takes project config, to store relevant metadata. - - Parameters - ---------- - config : str - Path to a project config.yaml file - - h5file : str - Path to a h5 data file - - individual_name : str - Name of the subject (whose pose is predicted) for single-animal DLC project. - For multi-animal projects, the names from the DLC project will be used directly. - - infer_timestamps : bool - Default True. Uses framerate to infer the timestamps returned as 0 from OpenCV. - If False, exclude these frames from resulting NWB file. - - TODO: allow one to overwrite those names, with a mapping? - - Returns - ------- - list of str - List of paths to the newly created NWB data files. - By default NWB files are stored in the same folder as the h5file. + scorer, df, video, paf_graph, dlc_timestamps, _ = _get_pes_args(config_file, h5file, individual_name) + if timestamps is None: + timestamps = dlc_timestamps - """ - scorer, df, video, paf_graph, timestamps, cfg = _get_pes_args( - config, h5file, individual_name, infer_timestamps=infer_timestamps + df_animal = df.groupby(level="individuals", axis=1).get_group(individual_name) + + return _write_pes_to_nwbfile( + nwbfile, individual_name, df_animal, scorer, video, paf_graph, timestamps, exclude_nans=False ) - output_paths = [] - for animal, df_ in df.groupby(level="individuals", axis=1): - nwbfile = NWBFile( - session_description=cfg["Task"], - experimenter=cfg["scorer"], - identifier=scorer, - session_start_time=datetime.datetime.now(datetime.timezone.utc), - ) - - # TODO Store the test_pose_config as well? - nwbfile = _write_pes_to_nwbfile( - nwbfile, animal, df_, scorer, video, paf_graph, timestamps, exclude_nans=(not infer_timestamps) - ) - output_path = h5file.replace(".h5", f"_{animal}.nwb") - with warnings.catch_warnings(), NWBHDF5IO(output_path, mode="w") as io: - warnings.filterwarnings("ignore", category=DtypeConversionWarning) - io.write(nwbfile) - output_paths.append(output_path) - - return output_paths From 4c504f1a29dc6ba118ab2ff3fd3bfe4665861461 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:27:34 +0000 Subject: [PATCH 08/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../datainterfaces/behavior/deeplabcut/_dlc_utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py index b546922ad..fe487f6c5 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py @@ -5,7 +5,7 @@ import warnings from pathlib import Path from platform import python_version -from typing import Optional, Union, List +from typing import List, Optional, Union import cv2 import numpy as np @@ -19,6 +19,7 @@ from ....utils import FilePathType + def _read_config(configname): """ Reads structured config file defining a project. @@ -66,7 +67,7 @@ def _get_movie_timestamps(movie_file, VARIABILITYBOUND=1000, infer_timestamps=Tr for _ in range(n_frames): _ = reader.read() timestamps.append(reader.get(cv2.CAP_PROP_POS_MSEC)) - + for _ in range(len(reader)): _ = reader.read() timestamps.append(reader.get(cv2.CAP_PROP_POS_MSEC)) @@ -267,7 +268,7 @@ def add_subject_to_nwbfile( timestamps = dlc_timestamps df_animal = df.groupby(level="individuals", axis=1).get_group(individual_name) - + return _write_pes_to_nwbfile( nwbfile, individual_name, df_animal, scorer, video, paf_graph, timestamps, exclude_nans=False ) From e09a4fa466dcf1fab6000a37c8c2b882afb25c55 Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:28:01 -0400 Subject: [PATCH 09/22] simplify interface --- .../deeplabcut/deeplabcutdatainterface.py | 44 ++----------------- 1 file changed, 3 insertions(+), 41 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py index f946e00fc..4d70f32db 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py @@ -9,46 +9,6 @@ from ....utils import FilePathType -def write_subject_to_nwb( - nwbfile: NWBFile, - h5file: FilePathType, - individual_name: str, - config_file: FilePathType, - timestamps: Optional[Union[List, np.ndarray]] = None, -): - """ - Given, subject name, write h5file to an existing nwbfile. - - Parameters - ---------- - nwbfile : pynwb.NWBFile - The in-memory nwbfile object to which the subject specific pose estimation series will be added. - h5file : str or path - Path to the DeepLabCut .h5 output file. - individual_name : str - Name of the subject (whose pose is predicted) for single-animal DLC project. - For multi-animal projects, the names from the DLC project will be used directly. - config_file : str or path - Path to a project config.yaml file - timestamps : list, np.ndarray or None, default: None - Alternative timestamps vector. If None, then use the inferred timestamps from DLC2NWB - Returns - ------- - nwbfile : pynwb.NWBFile - nwbfile with pes written in the behavior module - """ - from ._dlc_utils import _get_pes_args, _write_pes_to_nwbfile - - scorer, df, video, paf_graph, dlc_timestamps, _ = _get_pes_args(config_file, h5file, individual_name) - if timestamps is None: - timestamps = dlc_timestamps - - df_animal = df.groupby(level="individuals", axis=1).get_group(individual_name) - return _write_pes_to_nwbfile( - nwbfile, individual_name, df_animal, scorer, video, paf_graph, timestamps, exclude_nans=False - ) - - class DeepLabCutInterface(BaseTemporalAlignmentInterface): """Data interface for DeepLabCut datasets.""" @@ -143,7 +103,9 @@ def add_to_nwbfile( metadata: dict metadata info for constructing the nwb file (optional). """ - write_subject_to_nwb( + from ._dlc_utils import add_subject_to_nwbfile + + add_subject_to_nwbfile( nwbfile=nwbfile, h5file=str(self.source_data["file_path"]), individual_name=self.subject_name, From 4d3f422dbaa4d3aa1b654400d898094b15116f48 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:28:14 +0000 Subject: [PATCH 10/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../behavior/deeplabcut/deeplabcutdatainterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py index 4d70f32db..5af93c07c 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py @@ -104,7 +104,7 @@ def add_to_nwbfile( metadata info for constructing the nwb file (optional). """ from ._dlc_utils import add_subject_to_nwbfile - + add_subject_to_nwbfile( nwbfile=nwbfile, h5file=str(self.source_data["file_path"]), From 58f57afa636ebb1b39789b6ea402e814bad64ce1 Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:34:42 -0400 Subject: [PATCH 11/22] expose to new public helper --- .../datainterfaces/behavior/deeplabcut/_dlc_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py index fe487f6c5..39d9c696a 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py @@ -240,6 +240,7 @@ def add_subject_to_nwbfile( individual_name: str, config_file: FilePathType, timestamps: Optional[Union[List, np.ndarray]] = None, + pose_estimation_container_kwargs: Optional[dict] = None, ) -> NWBFile: """ Given the subject name, add the DLC .h5 file to an in-memory NWBFile object. @@ -257,6 +258,8 @@ def add_subject_to_nwbfile( Path to a project config.yaml file timestamps : list, np.ndarray or None, default: None Alternative timestamps vector. If None, then use the inferred timestamps from DLC2NWB + pose_estimation_container_kwargs : dict, optional + Dictionary of keyword argument pairs to pass to the PoseEstimation container. Returns ------- @@ -270,5 +273,5 @@ def add_subject_to_nwbfile( df_animal = df.groupby(level="individuals", axis=1).get_group(individual_name) return _write_pes_to_nwbfile( - nwbfile, individual_name, df_animal, scorer, video, paf_graph, timestamps, exclude_nans=False + nwbfile, individual_name, df_animal, scorer, video, paf_graph, timestamps, exclude_nans=False, pose_estimation_container_kwargs=pose_estimation_container_kwargs, ) From 21812fb88a5a7bd4a3779cc71cd936eb0cbf02b6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:34:54 +0000 Subject: [PATCH 12/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../datainterfaces/behavior/deeplabcut/_dlc_utils.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py index 39d9c696a..f80d1855a 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py @@ -273,5 +273,13 @@ def add_subject_to_nwbfile( df_animal = df.groupby(level="individuals", axis=1).get_group(individual_name) return _write_pes_to_nwbfile( - nwbfile, individual_name, df_animal, scorer, video, paf_graph, timestamps, exclude_nans=False, pose_estimation_container_kwargs=pose_estimation_container_kwargs, + nwbfile, + individual_name, + df_animal, + scorer, + video, + paf_graph, + timestamps, + exclude_nans=False, + pose_estimation_container_kwargs=pose_estimation_container_kwargs, ) From 8f4b18476b9357aa46622111aa69131ebed71539 Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Wed, 10 Jul 2024 16:43:32 -0400 Subject: [PATCH 13/22] fix init --- .../behavior/deeplabcut/deeplabcutdatainterface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py index 5af93c07c..95a55161c 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py @@ -47,13 +47,13 @@ def __init__( verbose: bool, default: True controls verbosity. """ - dlc2nwb = get_package(package_name="dlc2nwb") - + from ._dlc_utils import _read_config + file_path = Path(file_path) if "DLC" not in file_path.stem or ".h5" not in file_path.suffixes: raise IOError("The file passed in is not a DeepLabCut h5 data file.") - self._config_file = dlc2nwb.utils.read_config(config_file_path) + self._config_file = _read_config(config_file_path=config_file_path) self.subject_name = subject_name self.verbose = verbose super().__init__(file_path=file_path, config_file_path=config_file_path) From 8b9a50dd0a8caf5ec7661dea66d5b799316e62d3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 20:43:44 +0000 Subject: [PATCH 14/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../behavior/deeplabcut/deeplabcutdatainterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py index 95a55161c..8484f66f6 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py @@ -48,7 +48,7 @@ def __init__( controls verbosity. """ from ._dlc_utils import _read_config - + file_path = Path(file_path) if "DLC" not in file_path.stem or ".h5" not in file_path.suffixes: raise IOError("The file passed in is not a DeepLabCut h5 data file.") From b4f07c37b1a485733ac74a56d56344e6b8b0634a Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Wed, 10 Jul 2024 16:45:29 -0400 Subject: [PATCH 15/22] update requirements --- .../datainterfaces/behavior/deeplabcut/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/requirements.txt b/src/neuroconv/datainterfaces/behavior/deeplabcut/requirements.txt index 29659c517..03e0ee0b0 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/requirements.txt +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/requirements.txt @@ -1,2 +1,4 @@ tables<3.9.2;sys_platform=="darwin" tables;sys_platform=="linux" or sys_platform=="win32" +ndx-pose==0.1.1 +neuroconv[video] From 94584257a45b5b4c28faf352f6799a117185193b Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Wed, 10 Jul 2024 20:42:03 -0400 Subject: [PATCH 16/22] synch kwarg --- .../datainterfaces/behavior/deeplabcut/_dlc_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py index f80d1855a..751e17c53 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py @@ -20,17 +20,17 @@ from ....utils import FilePathType -def _read_config(configname): +def _read_config(config_file_path): """ Reads structured config file defining a project. """ ruamelFile = YAML() - path = Path(configname) + path = Path(config_file_path) if os.path.exists(path): try: with open(path, "r") as f: cfg = ruamelFile.load(f) - curr_dir = os.path.dirname(configname) + curr_dir = os.path.dirname(config_file_path) if cfg["project_path"] != curr_dir: cfg["project_path"] = curr_dir except Exception as err: From 165588e310f68267801c11a95badfea14708dae0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 11 Jul 2024 02:12:31 +0000 Subject: [PATCH 17/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../datainterfaces/behavior/deeplabcut/_dlc_utils.py | 6 +----- .../behavior/deeplabcut/deeplabcutdatainterface.py | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py index 751e17c53..675741a2a 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py @@ -1,20 +1,16 @@ -import datetime import importlib import os import pickle import warnings from pathlib import Path -from platform import python_version from typing import List, Optional, Union import cv2 import numpy as np import pandas as pd import yaml -from hdmf.build.warnings import DtypeConversionWarning from ndx_pose import PoseEstimation, PoseEstimationSeries -from packaging.version import Version # Installed with setuptools -from pynwb import NWBHDF5IO, NWBFile +from pynwb import NWBFile from ruamel.yaml import YAML from ....utils import FilePathType diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py index 8484f66f6..f5aea74aa 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py @@ -5,7 +5,6 @@ from pynwb.file import NWBFile from ....basetemporalalignmentinterface import BaseTemporalAlignmentInterface -from ....tools import get_package from ....utils import FilePathType From 92abc4bde930477af53dd7f307e55c1290627549 Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Wed, 10 Jul 2024 22:16:15 -0400 Subject: [PATCH 18/22] lazy imports in utils --- .../datainterfaces/behavior/deeplabcut/_dlc_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py index 675741a2a..67339ab1f 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py @@ -5,11 +5,9 @@ from pathlib import Path from typing import List, Optional, Union -import cv2 import numpy as np import pandas as pd import yaml -from ndx_pose import PoseEstimation, PoseEstimationSeries from pynwb import NWBFile from ruamel.yaml import YAML @@ -53,7 +51,7 @@ def _get_movie_timestamps(movie_file, VARIABILITYBOUND=1000, infer_timestamps=Tr movie_file : str Path to movie_file """ - # TODO: consider moving this to DLC, and actually extract alongside video analysis! + import cv2 reader = cv2.VideoCapture(movie_file) timestamps = [] @@ -180,6 +178,8 @@ def _write_pes_to_nwbfile( exclude_nans, pose_estimation_container_kwargs: Optional[dict] = None, ): + from ndx_pose import PoseEstimation, PoseEstimationSeries + pose_estimation_container_kwargs = pose_estimation_container_kwargs or dict() pose_estimation_series = [] From b76425215eeaf16b5535121bc9e267cafbd3d1d8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 11 Jul 2024 02:16:50 +0000 Subject: [PATCH 19/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py index 67339ab1f..99a29fc39 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py @@ -179,7 +179,7 @@ def _write_pes_to_nwbfile( pose_estimation_container_kwargs: Optional[dict] = None, ): from ndx_pose import PoseEstimation, PoseEstimationSeries - + pose_estimation_container_kwargs = pose_estimation_container_kwargs or dict() pose_estimation_series = [] From c18819eba27dacdbf9c0b3d76876ec1c06923b35 Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Wed, 10 Jul 2024 22:19:57 -0400 Subject: [PATCH 20/22] generalize container kwarg passing --- .../behavior/deeplabcut/_dlc_utils.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py index 99a29fc39..669a745e2 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py @@ -209,7 +209,8 @@ def _write_pes_to_nwbfile( is_deeplabcut_installed = importlib.util.find_spec(name="deeplabcut") is not None if is_deeplabcut_installed: deeplabcut_version = importlib.metadata.version(distribution_name="deeplabcut") - pe = PoseEstimation( + + pose_estimation_default_kwargs = dict( pose_estimation_series=pose_estimation_series, description="2D keypoint coordinates estimated using DeepLabCut.", original_videos=[video[0]], @@ -222,11 +223,15 @@ def _write_pes_to_nwbfile( edges=paf_graph if paf_graph else None, **pose_estimation_container_kwargs, ) - if "behavior" in nwbfile.processing: - behavior_pm = nwbfile.processing["behavior"] + pose_estimation_default_kwargs.update(pose_estimation_container_kwargs) + pose_estimation_container = PoseEstimation(**pose_estimation_default_kwargs) + + if "behavior" in nwbfile.processing: # TODO: replace with get_module + behavior_processing_module = nwbfile.processing["behavior"] else: - behavior_pm = nwbfile.create_processing_module(name="behavior", description="processed behavioral data") - behavior_pm.add(pe) + behavior_processing_module = nwbfile.create_processing_module(name="behavior", description="processed behavioral data") + behavior_processing_module.add(pose_estimation_container) + return nwbfile From 61a56bd8880f3aba88a386aaa484cd69598f4090 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 11 Jul 2024 02:20:47 +0000 Subject: [PATCH 21/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../datainterfaces/behavior/deeplabcut/_dlc_utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py index 669a745e2..72ddafe28 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/_dlc_utils.py @@ -225,13 +225,15 @@ def _write_pes_to_nwbfile( ) pose_estimation_default_kwargs.update(pose_estimation_container_kwargs) pose_estimation_container = PoseEstimation(**pose_estimation_default_kwargs) - + if "behavior" in nwbfile.processing: # TODO: replace with get_module behavior_processing_module = nwbfile.processing["behavior"] else: - behavior_processing_module = nwbfile.create_processing_module(name="behavior", description="processed behavioral data") + behavior_processing_module = nwbfile.create_processing_module( + name="behavior", description="processed behavioral data" + ) behavior_processing_module.add(pose_estimation_container) - + return nwbfile From d3ea2a1fcff90500943538a02851f8146be36e14 Mon Sep 17 00:00:00 2001 From: Luigi Petrucco Date: Thu, 11 Jul 2024 17:34:03 +0200 Subject: [PATCH 22/22] add name parameter to the `DeepLabCutInterface` (#917) Co-authored-by: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> --- .../deeplabcut/deeplabcutdatainterface.py | 2 ++ tests/test_on_data/test_behavior_interfaces.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py b/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py index f5aea74aa..e053e88fa 100644 --- a/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py +++ b/src/neuroconv/datainterfaces/behavior/deeplabcut/deeplabcutdatainterface.py @@ -91,6 +91,7 @@ def add_to_nwbfile( self, nwbfile: NWBFile, metadata: Optional[dict] = None, + container_name: str = "PoseEstimation", ): """ Conversion from DLC output files to nwb. Derived from dlc2nwb library. @@ -110,4 +111,5 @@ def add_to_nwbfile( individual_name=self.subject_name, config_file=str(self.source_data["config_file_path"]), timestamps=self._timestamps, + pose_estimation_container_kwargs=dict(name=container_name), ) diff --git a/tests/test_on_data/test_behavior_interfaces.py b/tests/test_on_data/test_behavior_interfaces.py index 155b32d72..36011da92 100644 --- a/tests/test_on_data/test_behavior_interfaces.py +++ b/tests/test_on_data/test_behavior_interfaces.py @@ -335,6 +335,7 @@ class TestDeepLabCutInterface(DeepLabCutInterfaceMixin, unittest.TestCase): def run_custom_checks(self): self.check_custom_timestamps(nwbfile_path=self.nwbfile_path) + self.check_renaming_instance(nwbfile_path=self.nwbfile_path) def check_custom_timestamps(self, nwbfile_path: str): # TODO: Peel out into separate test class and replace this part with check_read_nwb @@ -361,6 +362,22 @@ def check_custom_timestamps(self, nwbfile_path: str): pose_timestamps = pose_estimation.timestamps np.testing.assert_array_equal(pose_timestamps, self._custom_timestamps_case_1) + def check_renaming_instance(self, nwbfile_path: str): + custom_container_name = "TestPoseEstimation" + + metadata = self.interface.get_metadata() + metadata["NWBFile"].update(session_start_time=datetime.now().astimezone()) + + self.interface.run_conversion( + nwbfile_path=nwbfile_path, overwrite=True, metadata=metadata, container_name=custom_container_name + ) + + with NWBHDF5IO(path=nwbfile_path, mode="r", load_namespaces=True) as io: + nwbfile = io.read() + assert "behavior" in nwbfile.processing + assert "PoseEstimation" not in nwbfile.processing["behavior"].data_interfaces + assert custom_container_name in nwbfile.processing["behavior"].data_interfaces + def check_read_nwb(self, nwbfile_path: str): # TODO: move this to the upstream mixin with NWBHDF5IO(path=nwbfile_path, mode="r", load_namespaces=True) as io: