11from 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