def eval_accuracy(X, Y, Y_hat, X_train, Y_train, X_test, Y_test, t_treshold, r_threshold): # Have to swap axes due to how tensorflow quaternions work Yq = tf.transpose(Y[:4], [1, 0]) Yq_hat = tf.transpose(Y_hat[:4], [1, 0]) # Compute difference quaternion then swap axes back diff = tfq.multiply(tfq.normalize(Yq), tfq.inverse(tfq.normalize(Yq_hat))) diff = tf.transpose(diff, [1, 0]) # Compute angle difference in degrees diffW = tf.clip_by_value(diff[3], clip_value_min=-1.0, clip_value_max=1.0) angle_diff = tf_rad2deg(math.pi - tf.math.abs(2 * tf.math.acos(diffW) - math.pi)) correct_rot = tf.cast(tf.less(angle_diff, [r_threshold]), "float") correct_trans = tf.cast(tf.less(tf.norm(Y[4:] - Y_hat[4:], axis = 0), [t_treshold]), "float") t_accuracy = tf.reduce_mean(tf.cast(correct_trans, "float")) r_accuracy = tf.reduce_mean(tf.cast(correct_rot, "float")) accuracy = tf.reduce_mean(tf.cast(correct_trans * correct_rot, "float")) print('\n--------------') print("Evaluating accuracy with rotation threshold of " + str(r_threshold) + " and translation treshold of " + str(t_treshold)) print('') print ("Train Accuracy:", accuracy.eval({X: X_train, Y: Y_train})) print ("Test Accuracy:", accuracy.eval({X: X_test, Y: Y_test})) print('') print ("Train Accuracy on just translation:", t_accuracy.eval({X: X_train, Y: Y_train})) print ("Test Accuracy on just translation:", t_accuracy.eval({X: X_test, Y: Y_test})) print('') print ("Train Accuracy on just rotation:", r_accuracy.eval({X: X_train, Y: Y_train})) print ("Test Accuracy on just rotation:", r_accuracy.eval({X: X_test, Y: Y_test})) print('\n--------------')
def test_normalize_jacobian_preset(self): """Test the Jacobian of the normalize function.""" x_init = test_helpers.generate_preset_test_quaternions() x = tf.convert_to_tensor(value=x_init) y = quaternion.normalize(x) self.assert_jacobian_is_correct(x, x_init, y)
def test_normalize_jacobian_random(self): """Test the Jacobian of the normalize function.""" tensor_dimensions = np.random.randint(low=1, high=3) tensor_shape = np.random.randint(1, 10, size=(tensor_dimensions)).tolist() x_init = test_helpers.generate_random_test_quaternions(tensor_shape) x = tf.convert_to_tensor(value=x_init) y = quaternion.normalize(x) self.assert_jacobian_is_correct(x, x_init, y)
def d_q(q1, q2): """Distance between 2 quaternions The quaternion distance takes values between [0, pi] Parameters ---------- q1: tf.tensor/np.ndarray 1st quaternion q2: tf.tensor/np.ndarray 2nd quaternion Returns ------- : distnace between these 2 quaternions """ q1 = tf.cast(tf.convert_to_tensor(value=q1), dtype=tf.float64) q2 = tf.cast(tf.convert_to_tensor(value=q2), dtype=tf.float64) shape.check_static(tensor=q1, tensor_name="quaternion1", has_dim_equals=(-1, 4)) shape.check_static(tensor=q2, tensor_name="quaternion2", has_dim_equals=(-1, 4)) q1 = quaternion.normalize(q1) q2 = quaternion.normalize(q2) dot_product = vector.dot(q1, q2, keepdims=False) # Ensure dot product is in range [-1. 1]. eps_dot_prod = 1.8 * asserts.select_eps_for_addition(dot_product.dtype) dot_product = safe_ops.safe_shrink(dot_product, -1, 1, open_bounds=False, eps=eps_dot_prod) return 2.0 * tf.acos(tf.abs(dot_product))
def test_normalize_random(self): """Tests that normalize works as intended.""" random_quaternion = test_helpers.generate_random_test_quaternions() tensor_shape = random_quaternion.shape[:-1] unnormalized_random_quaternion = random_quaternion * 1.01 quat = np.concatenate((random_quaternion, unnormalized_random_quaternion), axis=0) mask = np.concatenate( (np.ones(shape=tensor_shape + (1,), dtype=bool), np.zeros(shape=tensor_shape + (1,), dtype=bool)), axis=0) is_normalized_before = quaternion.is_normalized(quat) normalized = quaternion.normalize(quat) is_normalized_after = quaternion.is_normalized(normalized) self.assertAllEqual(mask, is_normalized_before) self.assertAllEqual(is_normalized_after, np.ones(shape=is_normalized_after.shape, dtype=bool))
def transform_from_quat_and_trans(quaternion, trans_vector): """ Method that creates an augmented transform which includes the batch size in the output shape :param quaternion: (batch_size, 4) quaternion vectors :param trans_vector: (batch_size, 3, 1) translation vectors :return: transform_augm (batch_size, 4, 4) augmented transform matrix """ #We use [w, x, y, z] quaternion notation but TF Geometry lib expects [x, y, z, w] #quaternion = tf.concat([quaternion2[:, 1:], tf.expand_dims(quaternion2[:, 0], axis=1)], axis=-1) quaternion = tfg_quaternion.normalize(quaternion) #predicted_rot_mat = tfg_rot_mat.from_quaternion(quat_normalized) predicted_rot_mat = rot_matrix_from_quat_wxyz(quaternion) paddings = tf.constant([[0, 0], [0, 1], [0, 0]]) predicted_rot_mat_augm = tf.pad(predicted_rot_mat, paddings, constant_values=0) decalib_qt_trans_augm = tf.pad(trans_vector, paddings, constant_values=1) transform_augm = tf.concat([predicted_rot_mat_augm, decalib_qt_trans_augm], axis=-1) return transform_augm
def energy(vertices_rest_pose, vertices_deformed_pose, quaternions, edges, vertex_weight=None, edge_weight=None, conformal_energy=True, aggregate_loss=True, name=None): """Estimates an As Conformal As Possible (ACAP) fitting energy. For a given mesh in rest pose, this function evaluates a variant of the ACAP [1] fitting energy for a batch of deformed meshes. The vertex weights and edge weights are defined on the rest pose. The method implemented here is similar to [2], but with an added free variable capturing a scale factor per vertex. [1]: Yusuke Yoshiyasu, Wan-Chun Ma, Eiichi Yoshida, and Fumio Kanehiro. "As-Conformal-As-Possible Surface Registration." Computer Graphics Forum. Vol. 33. No. 5. 2014.</br> [2]: Olga Sorkine, and Marc Alexa. "As-rigid-as-possible surface modeling". Symposium on Geometry Processing. Vol. 4. 2007. Note: In the description of the arguments, V corresponds to the number of vertices in the mesh, and E to the number of edges in this mesh. Note: In the following, A1 to An are optional batch dimensions. Args: vertices_rest_pose: A tensor of shape `[V, 3]` containing the position of all the vertices of the mesh in rest pose. vertices_deformed_pose: A tensor of shape `[A1, ..., An, V, 3]` containing the position of all the vertices of the mesh in deformed pose. quaternions: A tensor of shape `[A1, ..., An, V, 4]` defining a rigid transformation to apply to each vertex of the rest pose. See Section 2 from [1] for further details. edges: A tensor of shape `[E, 2]` defining indices of vertices that are connected by an edge. vertex_weight: An optional tensor of shape `[V]` defining the weight associated with each vertex. Defaults to a tensor of ones. edge_weight: A tensor of shape `[E]` defining the weight of edges. Common choices for these weights include uniform weighting, and cotangent weights. Defaults to a tensor of ones. conformal_energy: A `bool` indicating whether each vertex is associated with a scale factor or not. If this parameter is True, scaling information must be encoded in the norm of `quaternions`. If this parameter is False, this function implements the energy described in [2]. aggregate_loss: A `bool` defining whether the returned loss should be an aggregate measure. When True, the mean squared error is returned. When False, returns two losses for every edge of the mesh. name: A name for this op. Defaults to "as_conformal_as_possible_energy". Returns: When aggregate_loss is `True`, returns a tensor of shape `[A1, ..., An]` containing the ACAP energies. When aggregate_loss is `False`, returns a tensor of shape `[A1, ..., An, 2*E]` containing each term of the summation described in the equation 7 of [2]. Raises: ValueError: if the shape of `vertices_rest_pose`, `vertices_deformed_pose`, `quaternions`, `edges`, `vertex_weight`, or `edge_weight` is not supported. """ with tf.compat.v1.name_scope(name, "as_conformal_as_possible_energy", [ vertices_rest_pose, vertices_deformed_pose, quaternions, edges, conformal_energy, vertex_weight, edge_weight ]): vertices_rest_pose = tf.convert_to_tensor(value=vertices_rest_pose) vertices_deformed_pose = tf.convert_to_tensor( value=vertices_deformed_pose) quaternions = tf.convert_to_tensor(value=quaternions) edges = tf.convert_to_tensor(value=edges) if vertex_weight is not None: vertex_weight = tf.convert_to_tensor(value=vertex_weight) if edge_weight is not None: edge_weight = tf.convert_to_tensor(value=edge_weight) shape.check_static(tensor=vertices_rest_pose, tensor_name="vertices_rest_pose", has_rank=2, has_dim_equals=(-1, 3)) shape.check_static(tensor=vertices_deformed_pose, tensor_name="vertices_deformed_pose", has_rank_greater_than=1, has_dim_equals=(-1, 3)) shape.check_static(tensor=quaternions, tensor_name="quaternions", has_rank_greater_than=1, has_dim_equals=(-1, 4)) shape.compare_batch_dimensions(tensors=(vertices_deformed_pose, quaternions), last_axes=(-3, -3), broadcast_compatible=False) shape.check_static(tensor=edges, tensor_name="edges", has_rank=2, has_dim_equals=(-1, 2)) tensors_with_vertices = [ vertices_rest_pose, vertices_deformed_pose, quaternions ] names_with_vertices = [ "vertices_rest_pose", "vertices_deformed_pose", "quaternions" ] axes_with_vertices = [-2, -2, -2] if vertex_weight is not None: shape.check_static(tensor=vertex_weight, tensor_name="vertex_weight", has_rank=1) tensors_with_vertices.append(vertex_weight) names_with_vertices.append("vertex_weight") axes_with_vertices.append(0) shape.compare_dimensions(tensors=tensors_with_vertices, axes=axes_with_vertices, tensor_names=names_with_vertices) if edge_weight is not None: shape.check_static(tensor=edge_weight, tensor_name="edge_weight", has_rank=1) shape.compare_dimensions(tensors=(edges, edge_weight), axes=(0, 0), tensor_names=("edges", "edge_weight")) if not conformal_energy: quaternions = quaternion.normalize(quaternions) # Extracts the indices of vertices. indices_i, indices_j = tf.unstack(edges, axis=-1) # Extracts the vertices we need per term. vertices_i_rest = tf.gather(vertices_rest_pose, indices_i, axis=-2) vertices_j_rest = tf.gather(vertices_rest_pose, indices_j, axis=-2) vertices_i_deformed = tf.gather(vertices_deformed_pose, indices_i, axis=-2) vertices_j_deformed = tf.gather(vertices_deformed_pose, indices_j, axis=-2) # Extracts the weights we need per term. weights_shape = vertices_i_rest.shape.as_list()[-2] if vertex_weight is not None: weight_i = tf.gather(vertex_weight, indices_i) weight_j = tf.gather(vertex_weight, indices_j) else: weight_i = weight_j = tf.ones(weights_shape, dtype=vertices_rest_pose.dtype) weight_i = tf.expand_dims(weight_i, axis=-1) weight_j = tf.expand_dims(weight_j, axis=-1) if edge_weight is not None: weight_ij = edge_weight else: weight_ij = tf.ones(weights_shape, dtype=vertices_rest_pose.dtype) weight_ij = tf.expand_dims(weight_ij, axis=-1) # Extracts the rotation we need per term. quaternion_i = tf.gather(quaternions, indices_i, axis=-2) quaternion_j = tf.gather(quaternions, indices_j, axis=-2) # Computes the energy. deformed_ij = vertices_i_deformed - vertices_j_deformed rotated_rest_ij = quaternion.rotate( (vertices_i_rest - vertices_j_rest), quaternion_i) energy_ij = weight_i * weight_ij * (deformed_ij - rotated_rest_ij) deformed_ji = vertices_j_deformed - vertices_i_deformed rotated_rest_ji = quaternion.rotate( (vertices_j_rest - vertices_i_rest), quaternion_j) energy_ji = weight_j * weight_ij * (deformed_ji - rotated_rest_ji) energy_ij_squared = vector.dot(energy_ij, energy_ij, keepdims=False) energy_ji_squared = vector.dot(energy_ji, energy_ji, keepdims=False) if aggregate_loss: average_energy_ij = tf.reduce_mean(input_tensor=energy_ij_squared, axis=-1) average_energy_ji = tf.reduce_mean(input_tensor=energy_ji_squared, axis=-1) return (average_energy_ij + average_energy_ji) / 2.0 return tf.concat((energy_ij_squared, energy_ji_squared), axis=-1)