Skip to content

Commit 2f23cb8

Browse files
authored
Merge pull request calderast#15 from calderast/yangsun
Yang-Sun with adding video and dlc!
2 parents a94ddb3 + abf13b5 commit 2f23cb8

File tree

2 files changed

+165
-2
lines changed

2 files changed

+165
-2
lines changed

src/jdb_to_nwb/convert_dlc.py

Whitespace-only changes.

src/jdb_to_nwb/convert_video.py

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,174 @@
11
from pynwb import NWBFile
2+
from scipy.ndimage import gaussian_filter
3+
import csv
4+
import numpy as np
25

6+
def add_video(nwbfile: NWBFile, metadata: dict):
7+
print("Adding video..")
38

9+
# Get file paths for video from metadata file
10+
video_file_path = metadata["video"]["arduino_video_file_path"]
11+
video_timestamps_file_path = metadata["video"]["arduino_video_timestamps_file_path"]
412

13+
# metadata should include the full name of dlc algorithm
14+
phot_dlc = metadata["video"]["dlc_algorithm"] # Behav_Vid0DLC_resnet50_Triangle_Maze_EphysDec7shuffle1_800000.h5
515

6-
def convert_video(nwbfile: NWBFile, metadata: dict):
16+
# If pixels_per_cm exists in metadata, use that value
17+
if "pixels_per_cm" in metadata["video"]:
18+
PIXELS_PER_CM = metadata["video"]["pixels_per_cm"]
19+
# Otherwise, assign it based on the date of the experiment
20+
else:
21+
PIXELS_PER_CM = assign_pixels_per_cm(metadata["date"])
722

23+
# Read times of each position point, equal to the time of each camera frame recording
24+
with open(video_timestamps_file_path, "r") as video_timestamps_file:
25+
video_timestamps = np.array(list(csv.reader(open(video_timestamps_file, 'r'))), dtype=float).ravel()
26+
27+
28+
# Read x and y position data and calculate velocity and acceleration
29+
x, y, velocity, acceleration = read_dlc(deeplabcut_file_path, phot_dlc = phot_dlc, cutoff = 0.9, cam_fps = 15, pixels_per_cm = PIXELS_PER_CM)
30+
31+
return video_timestamps, x, y, velocity, acceleration
32+
33+
def assign_pixels_per_cm(date_str):
34+
"""
35+
Assigns constant PIXELS_PER_CM based on the provided date string in MMDDYYYY format.
36+
PIXELS_PER_CM is 3.14 if video is before IM-1594 (before 01012023), 2.3 before (01112024), or 2.688 after (old maze)
837
38+
Args:
39+
- date_str (str): Date string in MMDDYYYY format, e.g., '11122022'.
40+
41+
Returns:
42+
- float: The corresponding PIXELS_PER_CM value.
43+
"""
44+
# Define the date format
45+
date_format = "%m%d%Y"
46+
47+
try:
48+
# Parse the input date string into a datetime object
49+
date = datetime.strptime(date_str, date_format)
50+
except ValueError:
51+
raise ValueError(f"Date string '{date_str}' is not in the expected format 'mmddyyyy'.")
52+
53+
# Define cutoff dates
54+
cutoff1 = datetime.strptime("12312022", date_format) # December 31, 2022
55+
cutoff2 = datetime.strptime("01112024", date_format) # January 11, 2024
56+
57+
# Assign pixelsPerCm based on the date
58+
if date <= cutoff1:
59+
pixels_per_cm = 3.14
60+
elif cutoff1 < date <= cutoff2:
61+
pixels_per_cm = 2.3
62+
else:
63+
pixels_per_cm = 2.688 # After January 11, 2024
64+
65+
return pixels_per_cm
66+
67+
def read_dlc(deeplabcut_file_path, phot_dlc = phot_dlc, cutoff = 0.9, cam_fps = 15, pixels_per_cm)
68+
"""
69+
Read dlc position data from the deeplabcut file, that contains algorithm name and position data
70+
71+
Position data is under the column names: cap_back and cap_front.
72+
Cap_bak is the back of the rat implant (red), and cap_front is the front of the rat implant (green)
73+
74+
After reading the position data, the position data is used to calculate velocity and acceleration based on the camera fps and pixels_per_cm
75+
76+
Returns:
77+
- x: x coordinates of the rat's body parts
78+
- y: y coordinates of the rat's body parts
79+
- velocity: velocity of the rat's body parts
80+
- acceleration: acceleration of the rat's body parts
81+
"""
82+
# Build file path dynamically and load position data
83+
position_col = 'cap_back'
84+
dlc_position_file = f'Behav_Vid0{phot_dlc}.h5'
85+
dlc_position = pd.read_hdf(deeplabcut_file_path + dlc_position_file)[phot_dlc]
86+
87+
# Remove position with uncertainty
88+
position = dlc_position[position_col][['x', 'y']].copy()
89+
position.loc[dlc_position[position_col].likelihood < cutoff, ['x', 'y']] = np.nan
90+
91+
# Remove the abrupt jump of position bigger than a body of rat (30cm)
92+
pixel_jump_cutoff = 30 * pixels_per_cm
93+
position.loc[position.x.notnull(),['x','y']] = detect_and_replace_jumps(
94+
position.loc[position.x.notnull(),['x','y']].values,pixel_jump_cutoff)
95+
96+
# Fill the missing gaps
97+
position.loc[:,['x','y']] = fill_missing_gaps(position.loc[:,['x','y']].values)
98+
99+
# Calculate velocity and acceleration
100+
velocity, acceleration = calculate_velocity_acceleration(position['x'].values, position['y'].values,fps = cam_fps, pixels_per_cm = pixels_per_cm)
101+
102+
return position['x'].values, position['y'].values, velocity, acceleration
103+
104+
105+
def detect_and_replace_jumps(coordinates, pixel_jump_cutoff):
106+
"""
107+
Detect and replace jumps in the position data that are bigger than pixel_jump_cutoff (default 30 cm)
108+
Jumps are replaced with NaN
109+
"""
110+
n = len(coordinates)
111+
jumps = []
112+
113+
# Calculate Euclidean distances between consecutive points
114+
distances = np.linalg.norm(coordinates[1:] - coordinates[:-1], axis=1)
115+
116+
# Find positions where the distance exceeds the threshold pixel_jump_cutoff
117+
jump_indices = np.where(distances > pixel_jump_cutoff)[0] + 1
118+
119+
# Mark all points within the jump range
120+
for idx in jump_indices:
121+
start = max(0, idx - 1)
122+
end = min(n, idx + 2)
123+
jumps.extend(range(start, end))
124+
125+
# Replace points belonging to jumps with NaN
126+
coordinates[jumps] = np.nan
127+
128+
return coordinates
129+
130+
def fill_missing_gaps(position_data):
131+
"""
132+
Fill missing values in the position data
133+
It identifies gaps in the position data and fills them with linear interpolation
134+
"""
135+
# Identify missing values as NaNs
136+
missing_values = np.isnan(position_data[:, 0]) | np.isnan(position_data[:, 1])
137+
138+
# Compute the cumulative sum of missing values to identify contiguous gaps
139+
cumulative_sum = np.cumsum(missing_values)
140+
gap_starts = np.where(np.diff(cumulative_sum) == 1)[0] + 1
141+
gap_ends = np.where(np.diff(cumulative_sum) == -1)[0]
142+
143+
# Interpolate the missing values in each gap using linear interpolation
144+
for gap_start, gap_end in zip(gap_starts, gap_ends):
145+
if gap_start == 0 or gap_end == len(position_data) - 1:
146+
continue # ignore gaps at the beginning or end of the data
147+
else:
148+
x = position_data[gap_start - 1:gap_end + 1, 0]
149+
y = position_data[gap_start - 1:gap_end + 1, 1]
150+
interp_func = interp1d(x, y, kind='linear')
151+
position_data[gap_start:gap_end, 0] = np.linspace(x[0], x[-1], gap_end - gap_start + 1)
152+
position_data[gap_start:gap_end, 1] = interp_func(position_data[gap_start:gap_end, 0])
153+
154+
return position_data
155+
156+
def calculate_velocity_acceleration(x, y, fps, pixels_per_cm):
157+
"""
158+
Calculate velocity and acceleration based on the camera fps and pixels_per_cm
159+
"""
160+
# convert pixel to cm
161+
x_cm = x * pixels_per_cm
162+
y_cm = y * pixels_per_cm
163+
164+
# calculate velocity
165+
velocity_x = np.gradient(x_cm) * fps
166+
velocity_y = np.gradient(y_cm) * fps
167+
velocity = np.sqrt(velocity_x ** 2 + velocity_y ** 2)
9168

169+
# calculate acceleration
170+
acceleration_x = np.gradient(velocity_x) * fps
171+
acceleration_y = np.gradient(velocity_y) * fps
172+
acceleration = np.sqrt(acceleration_x ** 2 + acceleration_y ** 2)
10173

11-
pass
174+
return velocity, acceleration

0 commit comments

Comments
 (0)