Source code for aopy.postproc.bmi3d

# bmi3d.py 
# postprocessing specific to BMI3D data

import numpy as np
from scipy.spatial.transform import Rotation as R

[docs]def convert_raw_to_world_coords(manual_input, rotation, offset, scale=1): ''' Transforms 3D manual input to 3D centered world coordinates for BMI3D tasks. For example, for optitrack input, raw coordinates are in the form (x: forward/backward, y: up/down, z: right/left). This function applies the BMI3D offset and scale to the coordinates, then transforms the coordinates to world coordinates (x: right/left, y: forward/backward, z: up/down) and reorders them to be more intuitive (x: right/left, y: up/down, z: forward/backward). For joystick input, the coordinates are in the form (x: left/right, y: backward/forward, z: nothing) and the scale is -1. Thus the output is always in the form (x: right/left, y: up/down, z: forward/backward) if the inputs are copied directly from exp_metadata. Args: manual_input (nt, 3): manual input coordinates from bmi3d, e.g. exp_data['task']['manual_input'] rotation (str): rotation metadata from exp_metadata['rotation'] offset (3-tuple): x,y,z offset for the manual input from exp_metadata['offset'] scale (float, optional): scaling factor for cursor movement from exp_metadata['scale']. Default 1. Returns: (nt, 3): manual input in world coordinates (x: right/left, y: up/down, z: forward/backward) Examples: Load manual input data from an experiment and transform it to world coordinates using the metadata from the experiment. Then segment the data into trials and plot a comparison of the raw input and transformed input. .. code-block:: python subject = 'beignet' id = 5974 date = datetime.date(2022, 7, 1) exp_data, exp_metadata = aopy.data.load_preproc_exp_data(data_dir, subject, id, date) original = exp_data['task']['manual_input'] input = aopy.postproc.bmi3d.convert_raw_to_world_coords(original, exp_metadata['rotation'], exp_metadata['offset']) go_cue = 32 trial_end = 239 print(exp_data['bmi3d_events']['code'], exp_data['bmi3d_events']['time']) segments, times = aopy.preproc.get_trial_segments(exp_data['bmi3d_events']['code'], exp_data['bmi3d_events']['time'], [go_cue], [trial_end]) segments_original = aopy.preproc.get_data_segments(original, times, 1) segments_input = aopy.preproc.get_data_segments(input, times, 1) plt.figure() plt.subplot(2,2,1) aopy.visualization.plot_trajectories(segments_original) plt.title('Raw input') plt.subplot(2,2,2) aopy.visualization.plot_trajectories(segments_input, bounds=[-10,10,-10,10]) plt.title('Transformed') plt.subplot(2,2,3, projection='3d') aopy.visualization.plot_trajectories(segments_original) plt.subplot(2,2,4, projection='3d') aopy.visualization.plot_trajectories(segments_input, bounds=[-10,10,-10,10,-10,10]) .. image:: _images/test_get_bmi3d_mc_input.png ''' from built_in_tasks.rotation_matrices import rotations bmi3d_space_input = np.dot((manual_input + offset), scale * rotations[rotation][:3,:3]) return bmi3d_space_input[:,[0,2,1]] # return (right-handed) world coordinates
[docs]def get_world_to_screen_mapping(exp_rotation='none', x_rot=0, y_rot=0, z_rot=0, exp_gain=1, baseline_rotation='none'): ''' Returns the mapping :math:`M` that transforms 3D centered user input from world to screen coordinates. World coordinates (x: right/left, y: up/down, z: forward/backward) and screen coordinates (x: right/left, y: up/down, z: into/out of the screen) differ only in that the screen may be placed arbitrarily in the world. However the mapping :math:`M` can arbitrarily rotate and scale the user input before projecting it to the screen. The mapping :math:`M` is related to the `exp_rotation` mapping :math:`M_{q}` used by bmi3d, but with axes swapped through multiplication with :math:`T_{q\\rightarrow w} = T_{w\\rightarrow q}`, the transformation that converts bmi3d coordinates to world coordinates (it happens to be its own inverse). The full mapping :math:`M` returned by this function is: :math:`M = T_{w\\rightarrow q} M_q T_{q\\rightarrow w}` Args: exp_rotation (str, optional): desired experimental rotation from exp_metadata['rotation']. Default 'none'. x_rot (float, optional): rotation about x-axis in degrees from exp_metadata['x_perturbation_rotation']. Default 0. y_rot (float, optional): rotation about y-axis in degrees from exp_metadata['pertubation_rotation']. Default 0. z_rot (float, optional): rotation about z-axis in degrees from exp_metadata['z_perturbation_rotation']. Default 0. exp_gain (float, optional): gain scaling factor of the mapping from exp_metadata['exp_gain']. Default 1. Returns: (3, 3): mapping from centered world coordinates to screen coordinates Examples: To reproduce how the mapping was used online, multiply position data in world coordinates (:math:`r_w`) by the mapping :math:`M` to get coordinates in screen space (:math:`r_s`; x is up on the screen, y is right on the screen, z is into the screen). .. math:: r_{s} = r_{w} M Alternatively, to transform screen space coordinates to world coordinates, multiply by the inverse mapping :math:`M^{-1}`: .. math:: r_{w} = r_{s} M^{-1} This can be useful for example to find the plane on which the user data should have been moving if the mapping was correctly learned. ''' from built_in_tasks.rotation_matrices import baseline_rotations, exp_rotations perturbation_rotation = R.from_euler('xyz', [x_rot, y_rot, z_rot], degrees=True).as_matrix() bmi3d_mapping = exp_gain * np.linalg.multi_dot((baseline_rotations[baseline_rotation][:3,:3], exp_rotations[exp_rotation][:3,:3], perturbation_rotation)) return bmi3d_mapping[[0, 2, 1], :][:, [0, 2, 1]] # return mapping in right-handed coordinates
[docs]def get_incremental_world_to_screen_mappings(start, stop, step, bmi3d_axis='y', exp_rotation='none', exp_gain=1, baseline_rotation='none'): ''' Returns the mappings :math:`M` that transform 3D centered user input from world to screen coordinates for an incremental rotation experiment. World coordinates (x: right/left, y: up/down, z: forward/backward) and screen coordinates (x: right/left, y: up/down, z: into/out of the screen) differ only in that the screen may be placed arbitrarily in the world. However the mapping :math:`M` can arbitrarily rotate and scale the user input before projecting it to the screen. Args: start (float): starting angle in degrees stop (float): ending angle in degrees step (float): step size in degrees bmi3d_axis (str, optional): axis about which to rotate the hand. Default 'y'. exp_rotation (str, optional): desired experimental rotation from exp_metadata['rotation']. Default 'none'. exp_gain (float, optional): gain scaling factor of the mapping from exp_metadata['exp_gain']. Default 1. Returns: list: list of mappings from centered world coordinates to screen coordinates ''' from built_in_tasks.rotation_matrices import baseline_rotations, exp_rotations mappings = [] for perturbation in np.arange(start, stop+step, step): perturbation_rotation = R.from_euler(bmi3d_axis, perturbation, degrees=True).as_matrix() mapping = exp_gain * np.linalg.multi_dot((baseline_rotations[baseline_rotation][:3,:3], exp_rotations[exp_rotation][:3,:3], perturbation_rotation)) mappings.append(mapping[[0, 2, 1], :][:, [0, 2, 1]]) # return mappings in right-handed coordinates return mappings