dodo.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. import os
  2. import shutil
  3. from os import path, symlink
  4. from glob import glob
  5. import json
  6. import numpy as np
  7. import pandas as pd
  8. import h5py
  9. import nested_h5py
  10. import ratcave as rc
  11. def read_motive_csv(fname):
  12. df = pd.read_csv(fname, skiprows=1, header=[0, 1, 3, 4],
  13. index_col=[0, 1], tupleize_cols=True)
  14. df.index.names = ['Frame', 'Time']
  15. df.columns = [tuple(cols) if 'Unnamed' not in cols[3] else tuple([*cols[:-1] + (cols[-2],)]) for cols in df.columns]
  16. df.columns = pd.MultiIndex.from_tuples(df.columns, names=['DataSource', 'ObjectName', 'CoordinateType', 'Axis'])
  17. return df
  18. def extract_motive_metadata(motive_csv_fname):
  19. with open(motive_csv_fname) as f:
  20. line = f.readline()
  21. cols = line.strip().split(',')
  22. session_metadata = {x: y for x, y in zip(cols[::2], cols[1::2])}
  23. # Attempt to coerce values to numeric data types, if possible
  24. for key, value in session_metadata.items():
  25. try:
  26. session_metadata[key] = float(value) if '.' in value else int(value)
  27. except ValueError:
  28. pass
  29. return session_metadata
  30. def convert_motive_csv_to_hdf5(csv_fname, h5_fname):
  31. if not path.exists(path.split(path.abspath(h5_fname))[0]):
  32. os.makedirs(path.split(path.abspath(h5_fname))[0])
  33. df = read_motive_csv(csv_fname)
  34. #df = df.reset_index('Time')
  35. session_metadata = extract_motive_metadata(csv_fname)
  36. if session_metadata['Total Exported Frames'] != len(df):
  37. with open('log_csv_to_hdf5.txt', 'a') as f:
  38. f.write('Incomplete: {}, (csv: {} Frames of {} Motive Recorded Frames\r\n'.format(path.basename(csv_fname),
  39. len(df), session_metadata['Total Exported Frames']))
  40. nested_h5py.write_to_hdf5_group(h5_fname, df, '/raw/',
  41. compression='gzip', compression_opts=7)
  42. with h5py.File(h5_fname, 'r+') as f:
  43. f.attrs.update(session_metadata)
  44. return None
  45. def add_orientation_dataset(h5_fname, **kwargs):
  46. with h5py.File(h5_fname, 'r+') as f:
  47. f.copy('/raw', '/preprocessed')
  48. for name, obj in nested_h5py.walk_h5py_path(f['/preprocessed/']):
  49. if not isinstance(obj, h5py.Dataset) or not 'Rotation' in name:
  50. continue
  51. rot_df = pd.DataFrame.from_records(obj.value).set_index(['Frame', 'Time'])
  52. rot_df.columns = rot_df.columns.str.lower()
  53. oris, ori0 = [], rc.Camera().orientation0
  54. for _, row in rot_df.iterrows():
  55. oris.append(rc.RotationQuaternion(**row).rotate(ori0))
  56. odf = pd.DataFrame(oris, columns=['X', 'Y', 'Z'], index=rot_df.index)
  57. f.create_dataset(name=obj.parent.name + '/Orientation',
  58. data=odf.to_records(), compression='gzip', compression_opts=7)
  59. return None
  60. def rotate_and_offset(array, rotation_matrices, mean_pos, pos_offset):
  61. dat = array.copy()
  62. try:
  63. dat -= mean_pos
  64. except:
  65. import ipdb
  66. ipdb.set_trace()
  67. for matrix in rotation_matrices:
  68. dat = dat @ matrix
  69. dat[:, 1] += mean_pos[1] + pos_offset[1]
  70. return dat
  71. def unrotate_objects(h5_fname, group='/preprocessed/Rigid Body', source_object_name='Arena',
  72. add_rotation=9., index_cols=2, y_offset=-0.66, **kwargs):
  73. """
  74. Un-rotate the objects in an hdf5 group by either a set rotationa mount, another object's rotation, or both.
  75. Arguments:
  76. -h5_fname (str): filename of hdf5 file to read in.
  77. -group (str): hdf5 group directory where all objects can be found
  78. -source_object_name (str): object name to use as un-rotation parent.
  79. -add_rotation (float): additional amount (degrees) to rotate by.
  80. -mean_center (bool): if the position should also be moved by the source_object's position.
  81. """
  82. source_obj = nested_h5py.read_from_h5_group(h5_fname, path.join(group, source_object_name), index_cols=index_cols)
  83. mean_pos = source_obj.Position.mean()
  84. mean_rot = source_obj.Rotation.mean()
  85. mean_rot.index = mean_rot.index.str.lower()
  86. rot_mat = rc.RotationQuaternion(**mean_rot).to_matrix()
  87. manrot = rc.RotationEulerDegrees(x=0, y=add_rotation, z=0).to_matrix()
  88. rot_matrices = [rot_mat[:-1, :-1], manrot[:-1, :-1]]
  89. with h5py.File(h5_fname, 'r+') as f:
  90. for name in filter(lambda s: 'position' in s.lower(), f.attrs):
  91. if isinstance(f.attrs[name], np.int64):
  92. continue
  93. positions = np.matrix(f.attrs[name])
  94. positions = positions @ manrot[:3, :3] + np.array([0., .2, 0])
  95. # positions = [rotate_and_offset(np.matrix(pos), rot_matrices[1:2], np.zeros(3), (0., 0., 0.)).squeeze() for pos in positions]
  96. f.attrs[name + '_corr'] = np.array(positions).squeeze()
  97. bodies = f[group]
  98. body_paths = [bodies[body].name for body in bodies]
  99. for body in body_paths:
  100. obj = nested_h5py.read_from_h5_group(h5_fname, path.join(group, body), index_cols=index_cols)
  101. obj['Position'] = rotate_and_offset(obj.Position.values, rot_matrices, mean_pos.values, (0., y_offset, 0.))
  102. obj['Orientation'] = rotate_and_offset(obj.Orientation.values, rot_matrices, (0., 0., 0.), (0., 0., 0.))
  103. nested_h5py.write_to_hdf5_group(h5_fname, obj, body + '/', mode='r+', overwrite=True)
  104. with h5py.File(h5_fname, 'r+') as f:
  105. for marker_name, dset in f['preprocessed/Rigid Body Marker/'].items():
  106. obj = pd.DataFrame.from_records(dset['Position'][:]).set_index(['Frame', 'Time'])
  107. obj[:] = rotate_and_offset(obj[:], rot_matrices, mean_pos.values, (0., y_offset, 0.))
  108. dset['Position'][:] = obj.to_records()
  109. return None
  110. event_log_dir = '/home/nickdg/theta_storage/data/VR_Experiments_Round_2/logs/event_logs/'
  111. settings_log_dir = '/home/nickdg/theta_storage/data/VR_Experiments_Round_2/logs/settings_logs/'
  112. def add_event_log(csv_fname, h5_fname, **kwargs):
  113. log_fname = path.join(event_log_dir, path.basename(csv_fname))
  114. if not path.exists(log_fname):
  115. # Attempt to match name
  116. for backidx in range(1, 15):
  117. fname_part = log_fname[:-backidx]
  118. matches = glob(fname_part + '*')
  119. if len(matches) == 1:
  120. log_fname = matches[0]
  121. break
  122. if len(matches) > 1:
  123. print('No matching log found for {}'.format(log_fname))
  124. return
  125. else:
  126. print('No matching log found for {}'.format(log_fname))
  127. return
  128. events = pd.read_csv(log_fname, sep=';',)# parse_dates=[0], infer_datetime_format=True)
  129. events.columns = events.columns.str.lstrip()
  130. events['Event'] = events.Event.str.lstrip()
  131. events['EventArguments'] = events.EventArguments.str.lstrip()
  132. times = pd.read_hdf(h5_fname, '/raw/Rigid Body/Rat/Position').set_index('Frame')['Time']
  133. event_frames = np.searchsorted(times.values.flatten(), events['MotiveExpTimeSecs'])
  134. events['Frame'] = event_frames
  135. events['Time'] = times.loc[event_frames].values
  136. phase_frames = events[events.Event.str.match('set_')].reset_index().Frame.values
  137. events.set_index(['Frame', 'Time'], inplace=True)
  138. event_names = events.Event.values.astype('S')
  139. del events['Event']
  140. event_arguments = events.EventArguments.values.astype('S')
  141. del events['EventArguments']
  142. del events['DateTime']
  143. with h5py.File(h5_fname, 'r+') as f:
  144. f.create_dataset('/events/eventlog', data=events.to_records(),
  145. compression='gzip', compression_opts=7)
  146. f.create_dataset('/events/eventNames', data=event_names)
  147. f.create_dataset('/events/eventArguments', data=event_arguments)
  148. if len(phase_frames) > 0:
  149. f.create_dataset('/events/phaseStartFrameNum', data=phase_frames)
  150. return None
  151. def add_settings_log(json_fname, h5_fname, **kwargs):
  152. """
  153. Writes a settings log to the hdf5 file as root user attributes, using csv data.
  154. Arguments:
  155. -json_fname (str): json filename to read for settings info
  156. -h5_fname (str): hdf5 filename to write to.
  157. """
  158. log_fname = path.join(settings_log_dir, path.basename(json_fname))
  159. if not path.exists(log_fname):
  160. # Attempt to match name
  161. for backidx in range(1, 15):
  162. fname_part = log_fname[:-backidx]
  163. matches = glob(fname_part + '*')
  164. if len(matches) == 1:
  165. log_fname = matches[0]
  166. break
  167. if len(matches) > 1:
  168. print('No matching log found for {}'.format(log_fname))
  169. return
  170. else:
  171. print('No matching log found for {}'.format(log_fname))
  172. return
  173. with open(log_fname) as f:
  174. sess_data = json.load(f)
  175. for key, value in sess_data.items():
  176. if type(value) == bool:
  177. sess_data[key] = int(value)
  178. if type(value) == type(None):
  179. sess_data[key] = 0
  180. with h5py.File(h5_fname, 'r+') as f:
  181. f.attrs.update(sess_data)
  182. return None
  183. def add_softlink_to_markers(h5_fname, **kwargs):
  184. with h5py.File(h5_fname, 'r+') as f:
  185. for body_name, body_group in f['preprocessed/Rigid Body'].items():
  186. body_group.create_group("Markers")
  187. marker_names = [name for name in f['/preprocessed/Rigid Body Marker'] if body_name in name]
  188. for marker_name in marker_names:
  189. body_group['Markers'][marker_name] = h5py.SoftLink('/preprocessed/Rigid Body Marker/'+marker_name)
  190. return None
  191. def symlink_to_experiment_directory(dirname):
  192. expdir = '/home/nickdg/theta_storage/data/VR_Experiments_Round_2/processed_data_by_experiment'
  193. expname = path.split(dirname)[-1].split('_')[0]
  194. if expname == 'VR':
  195. expname = '_'.join(path.split(dirname)[-1].split('_')[0:2])
  196. if 'H' in expname[:6]:
  197. expname = 'VRHabit'
  198. elif not 'VR' in expname[:3]:
  199. expname = 'Unlabeled'
  200. if not path.exists(path.join(expdir, expname)):
  201. os.makedirs(path.join(expdir, expname))
  202. symlink(dirname, path.join(expdir, expname, path.split(dirname)[1]), target_is_directory=True)
  203. return None
  204. def skip_if_conf_file_exists(conf_fname):
  205. """
  206. Skips running function if a given file exists. If not, runs functions and creates the file.
  207. Arguments:
  208. -conf_fname (str): filename to check for. (Note: Must be full path)
  209. """
  210. def decorator(fun):
  211. def wrapper(*args, **kwargs):
  212. if path.exists(conf_fname):
  213. print(fun.__name__ + ' already made for this file. Skipping step...')
  214. return
  215. else:
  216. output = fun(*args, **kwargs)
  217. with open(conf_fname, 'w'):
  218. pass
  219. return output
  220. return wrapper
  221. return decorator
  222. basedir = '/home/nickdg/theta_storage/data/VR_Experiments_Round_2/Converted Motive Files'
  223. csv_fnames = glob(basedir + '/**/*.csv', recursive=True)
  224. new_basedir = path.join(path.commonpath(csv_fnames), '..', 'processed_data')
  225. h5_fnames = [path.join(new_basedir, path.basename(path.splitext(name)[0]), path.basename(path.splitext(name)[0] + '.h5')) for name in csv_fnames]
  226. def task_preprocess_all_data():
  227. for csv_fname, h5_fname in zip(csv_fnames[:], h5_fnames):
  228. # if not 'Spat' in h5_fname:
  229. # continue
  230. dirname = path.dirname(h5_fname)
  231. if 'test' in csv_fname.lower():
  232. continue
  233. if 'habit' in csv_fname.lower():
  234. continue
  235. if not 'spat' in csv_fname.lower():
  236. continue
  237. convert_task = {
  238. 'actions': [(convert_motive_csv_to_hdf5, (csv_fname, h5_fname))],
  239. 'targets': [h5_fname],
  240. 'file_dep': [csv_fname],
  241. 'name': 'convert_csv_to_h5: {}'.format(path.basename(h5_fname)),
  242. }
  243. yield convert_task
  244. symlink_task = {
  245. 'actions': [(symlink_to_experiment_directory, (dirname,))],
  246. # 'targets': [path.join(dirname, path.basename(h5_fname))],
  247. 'file_dep': [h5_fname],
  248. 'name': 'symlink_exp: {}'.format(path.basename(h5_fname)),
  249. }
  250. yield symlink_task
  251. for avi_fname in glob(path.join(path.dirname(csv_fname), '*.avi')):
  252. vid_copy_task = {
  253. 'actions': [(shutil.copy, (avi_fname, dirname))],
  254. 'targets': [path.join(dirname, path.basename(avi_fname))],
  255. 'file_dep': [avi_fname],
  256. 'name': 'copy_video: {}'.format(path.basename(avi_fname)),
  257. }
  258. yield vid_copy_task
  259. # mp4_fname = path.splitext(path.basename(avi_fname))[0] + '.mp4'
  260. # # 'actions': ['ffmpeg -i "%(dependencies)s" -c:v libx264 -crf 19 -preset slow -c:a libfdk_aac -b:a 192k -ac 2 "%(targets)s"',],
  261. # vid_convert_task = {
  262. # 'actions': ['ffmpeg -i "%(dependencies)s" -c:v libx264 -crf 19 -preset slow -c:a libfdk_aac -ac 2 "%(targets)s"',],
  263. # 'targets': [path.join(dirname, mp4_fname)],
  264. # 'file_dep': [path.join(dirname, path.basename(avi_fname))],
  265. # 'name': 'convert_video: {}'.format(path.basename(avi_fname)),
  266. # 'verbosity': 2,
  267. # }
  268. # yield vid_convert_task
  269. conf_fname = dirname + '/event_log_added.txt'
  270. add_event_log_if_no_conf_file = skip_if_conf_file_exists(conf_fname)(add_event_log)
  271. event_task = {
  272. 'actions': [(add_event_log_if_no_conf_file, (csv_fname, h5_fname,))],
  273. 'targets': [conf_fname],
  274. 'task_dep': [convert_task['name']],
  275. 'file_dep': [h5_fname],
  276. 'name': 'add_event_log: {}'.format(path.basename(h5_fname)),
  277. 'verbosity': 2,
  278. }
  279. yield event_task
  280. conf_fname = dirname + '/settings_log_added.txt'
  281. add_settings_log_if_no_conf_file = skip_if_conf_file_exists(conf_fname)(add_settings_log)
  282. settings_task = {
  283. 'actions': [(add_settings_log_if_no_conf_file, (csv_fname, h5_fname,))],
  284. 'targets': [conf_fname],
  285. 'task_dep': [event_task['name']],
  286. 'file_dep': [h5_fname],
  287. 'name': 'add_settings_log: {}'.format(path.basename(h5_fname)),
  288. 'verbosity': 2,
  289. }
  290. yield settings_task
  291. conf_fname = dirname + '/ori_added.txt'
  292. add_orientation_dataset_if_no_conf_file = skip_if_conf_file_exists(conf_fname)(add_orientation_dataset)
  293. ori_task = {
  294. 'actions': [(add_orientation_dataset_if_no_conf_file, (h5_fname,))],
  295. 'targets': [conf_fname],
  296. # 'file_dep': [h5_fname],
  297. 'task_dep': [settings_task['name']],
  298. 'name': 'add_orientation: {}'.format(path.basename(h5_fname)),
  299. 'verbosity': 2,
  300. }
  301. yield ori_task
  302. conf_fname = dirname + '/unrotated.txt'
  303. unrotate_objects_if_no_conf_file = skip_if_conf_file_exists(conf_fname)(unrotate_objects)
  304. rotate_task = {
  305. 'actions': [(unrotate_objects_if_no_conf_file, (h5_fname,))],
  306. 'targets': [conf_fname],
  307. # 'file_dep': [h5_fname],
  308. 'task_dep': [ori_task['name']],
  309. 'name': 'unrotate: {}'.format(path.basename(h5_fname)),
  310. 'verbosity': 2,
  311. }
  312. yield rotate_task
  313. conf_fname = dirname + '/marker_softlink_added.txt'
  314. add_softlinks_if_no_conf_file = skip_if_conf_file_exists(conf_fname)(add_softlink_to_markers)
  315. marker_softlink_task = {
  316. 'actions': [(add_softlinks_if_no_conf_file, (h5_fname,))],
  317. 'targets': [conf_fname],
  318. # 'file_dep': [h5_fname],
  319. 'task_dep': [rotate_task['name']],
  320. 'name': 'softlink: {}'.format(path.basename(h5_fname)),
  321. 'verbosity': 2,
  322. }
  323. yield marker_softlink_task
  324. if __name__ == '__main__':
  325. import doit
  326. doit.run(globals())