def recalculate(self): assert self.recent_input and self.recent_labels width, height = self.g_pool.capture.frame_size prediction = self.g_pool.active_gaze_mapping_plugin.map_batch( self.recent_input) # reuse closest_matches_monocular to correlate one label to each prediction # correlated['ref']: prediction, correlated['pupil']: label location correlated = closest_matches_monocular(prediction, self.recent_labels) # [[pred.x, pred.y, label.x, label.y], ...], shape: n x 4 locations = np.array([(*e['ref']['norm_pos'], *e['pupil']['norm_pos']) for e in correlated]) self.error_lines = locations.copy() # n x 4 locations[:, ::2] *= width locations[:, 1::2] = (1. - locations[:, 1::2]) * height # Accuracy is calculated as the average angular # offset (distance) (in degrees of visual angle) # between fixations locations and the corresponding # locations of the fixation targets. undistorted = self.g_pool.capture.intrinsics.undistortPoints(locations) undistorted.shape = -1, 2 # append column with z=1 # using idea from https://stackoverflow.com/questions/8486294/how-to-add-an-extra-column-to-an-numpy-array undistorted_3d = np.ones((undistorted.shape[0], 3)) # shape: 2n x 3 undistorted_3d[:, :-1] = undistorted # normalize vectors: undistorted_3d /= np.linalg.norm(undistorted_3d, axis=1)[:, np.newaxis] # Cosine distance of A and B: (A @ B) / (||A|| * ||B||) # No need to calculate norms, since A and B are normalized in our case. # np.einsum('ij,ij->i', A, B) equivalent to np.diagonal(A @ B.T) but faster. angular_err = np.einsum('ij,ij->i', undistorted_3d[::2, :], undistorted_3d[1::2, :]) # Good values are close to 1. since cos(0) == 1. # Therefore we look for values greater than cos(outlier_threshold) selected_indices = angular_err > np.cos( np.deg2rad(self.outlier_threshold)) selected_samples = angular_err[selected_indices] num_used, num_total = selected_samples.shape[0], angular_err.shape[0] self.error_lines = self.error_lines[selected_indices].reshape( -1, 2) # shape: num_used x 2 self.accuracy = np.rad2deg(np.arccos(selected_samples.mean())) logger.info('Angular accuracy: {}. Used {} of {} samples.'.format( self.accuracy, num_used, num_total)) # lets calculate precision: (RMS of distance of succesive samples.) # This is a little rough as we do not compensate headmovements in this test. # Precision is calculated as the Root Mean Square (RMS) # of the angular distance (in degrees of visual angle) # between successive samples during a fixation undistorted_3d.shape = -1, 6 # shape: n x 6 succesive_distances_gaze = np.einsum('ij,ij->i', undistorted_3d[:-1, :3], undistorted_3d[1:, :3]) succesive_distances_ref = np.einsum('ij,ij->i', undistorted_3d[:-1, 3:], undistorted_3d[1:, 3:]) # if the ref distance is to big we must have moved to a new fixation or there is headmovement, # if the gaze dis is to big we can assume human error # both times gaze data is not valid for this mesurement selected_indices = np.logical_and( succesive_distances_gaze > self.succession_threshold, succesive_distances_ref > self.succession_threshold) succesive_distances = succesive_distances_gaze[selected_indices] num_used, num_total = succesive_distances.shape[ 0], succesive_distances_gaze.shape[0] self.precision = np.sqrt(np.mean(np.arccos(succesive_distances)**2)) logger.info("Angular precision: {}. Used {} of {} samples.".format( self.precision, num_used, num_total))
def calc_acc_prec_errlines(self, gaze_pos, ref_pos, intrinsics): width, height = intrinsics.resolution # reuse closest_matches_monocular to correlate one label to each prediction # correlated['ref']: prediction, correlated['pupil']: label location correlated = closest_matches_monocular(gaze_pos, ref_pos) # [[pred.x, pred.y, label.x, label.y], ...], shape: n x 4 locations = np.array([(*e['ref']['norm_pos'], *e['pupil']['norm_pos']) for e in correlated]) error_lines = locations.copy() # n x 4 locations[:, ::2] *= width locations[:, 1::2] = (1. - locations[:, 1::2]) * height locations.shape = -1, 2 # Accuracy is calculated as the average angular # offset (distance) (in degrees of visual angle) # between fixations locations and the corresponding # locations of the fixation targets. undistorted_3d = intrinsics.unprojectPoints(locations, normalize=True) # Cosine distance of A and B: (A @ B) / (||A|| * ||B||) # No need to calculate norms, since A and B are normalized in our case. # np.einsum('ij,ij->i', A, B) equivalent to np.diagonal(A @ B.T) but faster. angular_err = np.einsum('ij,ij->i', undistorted_3d[::2, :], undistorted_3d[1::2, :]) # Good values are close to 1. since cos(0) == 1. # Therefore we look for values greater than cos(outlier_threshold) selected_indices = angular_err > np.cos( np.deg2rad(self.outlier_threshold)) selected_samples = angular_err[selected_indices] num_used, num_total = selected_samples.shape[0], angular_err.shape[0] error_lines = error_lines[selected_indices].reshape( -1, 2) # shape: num_used x 2 accuracy = np.rad2deg(np.arccos(selected_samples.mean())) accuracy_result = Calculation_Result(accuracy, num_used, num_total) # lets calculate precision: (RMS of distance of succesive samples.) # This is a little rough as we do not compensate headmovements in this test. # Precision is calculated as the Root Mean Square (RMS) # of the angular distance (in degrees of visual angle) # between successive samples during a fixation undistorted_3d.shape = -1, 6 # shape: n x 6 succesive_distances_gaze = np.einsum('ij,ij->i', undistorted_3d[:-1, :3], undistorted_3d[1:, :3]) succesive_distances_ref = np.einsum('ij,ij->i', undistorted_3d[:-1, 3:], undistorted_3d[1:, 3:]) # if the ref distance is to big we must have moved to a new fixation or there is headmovement, # if the gaze dis is to big we can assume human error # both times gaze data is not valid for this mesurement selected_indices = np.logical_and( succesive_distances_gaze > self.succession_threshold, succesive_distances_ref > self.succession_threshold) succesive_distances = succesive_distances_gaze[selected_indices] num_used, num_total = succesive_distances.shape[ 0], succesive_distances_gaze.shape[0] precision = np.sqrt(np.mean(np.arccos(succesive_distances)**2)) precision_result = Calculation_Result(precision, num_used, num_total) return accuracy_result, precision_result, error_lines
def calc_acc_prec_errlines( gaze_pos, ref_pos, intrinsics, outlier_threshold, succession_threshold=np.cos(np.deg2rad(0.5)), ): width, height = intrinsics.resolution # reuse closest_matches_monocular to correlate one label to each prediction # correlated['ref']: prediction, correlated['pupil']: label location correlated = closest_matches_monocular(gaze_pos, ref_pos) # [[pred.x, pred.y, label.x, label.y], ...], shape: n x 4 locations = np.array( [(*e["ref"]["norm_pos"], *e["pupil"]["norm_pos"]) for e in correlated] ) if locations.size == 0: accuracy_result = Calculation_Result(0.0, 0, 0) precision_result = Calculation_Result(0.0, 0, 0) error_lines = np.array([]) return accuracy_result, precision_result, error_lines error_lines = locations.copy() # n x 4 locations[:, ::2] *= width locations[:, 1::2] = (1.0 - locations[:, 1::2]) * height locations.shape = -1, 2 # Accuracy is calculated as the average angular # offset (distance) (in degrees of visual angle) # between fixations locations and the corresponding # locations of the fixation targets. undistorted_3d = intrinsics.unprojectPoints(locations, normalize=True) # Cosine distance of A and B: (A @ B) / (||A|| * ||B||) # No need to calculate norms, since A and B are normalized in our case. # np.einsum('ij,ij->i', A, B) equivalent to np.diagonal(A @ B.T) but faster. angular_err = np.einsum( "ij,ij->i", undistorted_3d[::2, :], undistorted_3d[1::2, :] ) # Good values are close to 1. since cos(0) == 1. # Therefore we look for values greater than cos(outlier_threshold) selected_indices = angular_err > np.cos(np.deg2rad(outlier_threshold)) selected_samples = angular_err[selected_indices] num_used, num_total = selected_samples.shape[0], angular_err.shape[0] error_lines = error_lines[selected_indices].reshape( -1, 2 ) # shape: num_used x 2 accuracy = np.rad2deg(np.arccos(selected_samples.clip(-1.0, 1.0).mean())) accuracy_result = Calculation_Result(accuracy, num_used, num_total) # lets calculate precision: (RMS of distance of succesive samples.) # This is a little rough as we do not compensate headmovements in this test. # Precision is calculated as the Root Mean Square (RMS) # of the angular distance (in degrees of visual angle) # between successive samples during a fixation undistorted_3d.shape = -1, 6 # shape: n x 6 succesive_distances_gaze = np.einsum( "ij,ij->i", undistorted_3d[:-1, :3], undistorted_3d[1:, :3] ) succesive_distances_ref = np.einsum( "ij,ij->i", undistorted_3d[:-1, 3:], undistorted_3d[1:, 3:] ) # if the ref distance is to big we must have moved to a new fixation or there is headmovement, # if the gaze dis is to big we can assume human error # both times gaze data is not valid for this mesurement selected_indices = np.logical_and( succesive_distances_gaze > succession_threshold, succesive_distances_ref > succession_threshold, ) succesive_distances = succesive_distances_gaze[selected_indices] num_used, num_total = ( succesive_distances.shape[0], succesive_distances_gaze.shape[0], ) precision = np.sqrt( np.mean(np.rad2deg(np.arccos(succesive_distances.clip(-1.0, 1.0))) ** 2) ) precision_result = Calculation_Result(precision, num_used, num_total) return accuracy_result, precision_result, error_lines