def inverse(quaternion, name=None): """Computes the inverse of a quaternion. Note: In the following, A1 to An are optional batch dimensions. Args: quaternion: A tensor of shape `[A1, ..., An, 4]`, where the last dimension represents a normalized quaternion. name: A name for this op that defaults to "quaternion_inverse". Returns: A tensor of shape `[A1, ..., An, 4]`, where the last dimension represents a normalized quaternion. Raises: ValueError: If the shape of `quaternion` is not supported. """ with tf.compat.v1.name_scope(name, "quaternion_inverse", [quaternion]): quaternion = tf.convert_to_tensor(value=quaternion) shape.check_static( tensor=quaternion, tensor_name="quaternion", has_dim_equals=(-1, 4)) quaternion = asserts.assert_normalized(quaternion) squared_norm = tf.reduce_sum( input_tensor=tf.square(quaternion), axis=-1, keepdims=True) return safe_ops.safe_unsigned_div(conjugate(quaternion), squared_norm)
def cartesian_to_spherical_coordinates(point_cartesian, name=None): """Function to transform Cartesian coordinates to spherical coordinates. This function assumes a right handed coordinate system with `z` pointing up. When `x` and `y` are both `0`, the function outputs `0` for `phi`. Note that the function is not smooth when `x = y = 0`. Note: In the following, A1 to An are optional batch dimensions. Args: point_cartesian: A tensor of shape `[A1, ..., An, 3]`. In the last dimension, the data follows the `x`, `y`, `z` order. name: A name for this op. Defaults to `cartesian_to_spherical_coordinates`. Returns: A tensor of shape `[A1, ..., An, 3]`. The last dimensions contains (`r`,`theta`,`phi`), where `r` is the sphere radius, `theta` is the polar angle and `phi` is the azimuthal angle. """ with tf.compat.v1.name_scope(name, "cartesian_to_spherical_coordinates", [point_cartesian]): point_cartesian = tf.convert_to_tensor(value=point_cartesian) shape.check_static( tensor=point_cartesian, tensor_name="point_cartesian", has_dim_equals=(-1, 3)) x, y, z = tf.unstack(point_cartesian, axis=-1) radius = tf.norm(tensor=point_cartesian, axis=-1) theta = tf.acos(safe_ops.safe_unsigned_div(z, radius)) phi = tf.atan2(y, x) return tf.stack((radius, theta, phi), axis=-1)
def from_quaternion(quaternion, name=None): """Converts a quaternion to an axis-angle representation. Note: In the following, A1 to An are optional batch dimensions. Args: quaternion: A tensor of shape `[A1, ..., An, 4]`, where the last dimension represents a normalized quaternion. name: A name for this op that defaults to "axis_angle_from_quaternion". Returns: Tuple of two tensors of shape `[A1, ..., An, 3]` and `[A1, ..., An, 1]`, where the first tensor represents the axis, and the second represents the angle. The resulting axis is a normalized vector. Raises: ValueError: If the shape of `quaternion` is not supported. """ with tf.compat.v1.name_scope(name, "axis_angle_from_quaternion", [quaternion]): quaternion = tf.convert_to_tensor(value=quaternion) shape.check_static( tensor=quaternion, tensor_name="quaternion", has_dim_equals=(-1, 4)) quaternion = asserts.assert_normalized(quaternion) # This prevents zero norm xyz and zero w, and is differentiable. quaternion += asserts.select_eps_for_addition(quaternion.dtype) xyz, w = tf.split(quaternion, (3, 1), axis=-1) norm = tf.norm(tensor=xyz, axis=-1, keepdims=True) angle = 2.0 * tf.atan2(norm, tf.abs(w)) axis = safe_ops.safe_unsigned_div(safe_ops.nonzero_sign(w) * xyz, norm) return axis, angle
def test_safe_signed_div_exception_raised(self, dtype): """Checks that signed division causes Inf values for zero eps.""" vector = tf.convert_to_tensor(value=_pick_random_vector(), dtype=dtype) zero_vector = tf.zeros_like(vector) with self.assertRaises(tf.errors.InvalidArgumentError): self.evaluate( safe_ops.safe_unsigned_div( tf.norm(tensor=vector), tf.sin(zero_vector), eps=0.0))
def cartesian_to_spherical_coordinates( point_cartesian: TensorLike, eps: Float = None, name: str = "cartesian_to_spherical_coordinates") -> tf.Tensor: """Function to transform Cartesian coordinates to spherical coordinates. This function assumes a right handed coordinate system with `z` pointing up. When `x` and `y` are both `0`, the function outputs `0` for `phi`. Note that the function is not smooth when `x = y = 0`. Note: In the following, A1 to An are optional batch dimensions. Args: point_cartesian: A tensor of shape `[A1, ..., An, 3]`. In the last dimension, the data follows the `x`, `y`, `z` order. eps: A small `float`, to be added to the denominator. If left as `None`, its value is automatically selected using `point_cartesian.dtype`. name: A name for this op. Defaults to "cartesian_to_spherical_coordinates". Returns: A tensor of shape `[A1, ..., An, 3]`. The last dimensions contains (`r`,`theta`,`phi`), where `r` is the sphere radius, `theta` is the polar angle and `phi` is the azimuthal angle. Returns `NaN` gradient if x = y = 0. """ with tf.name_scope(name): point_cartesian = tf.convert_to_tensor(value=point_cartesian) shape.check_static(tensor=point_cartesian, tensor_name="point_cartesian", has_dim_equals=(-1, 3)) x, y, z = tf.unstack(point_cartesian, axis=-1) radius = tf.norm(tensor=point_cartesian, axis=-1) theta = tf.acos( tf.clip_by_value(safe_ops.safe_unsigned_div(z, radius, eps), -1., 1.)) phi = tf.atan2(y, x) return tf.stack((radius, theta, phi), axis=-1)
def vertex_normals(vertices, indices, clockwise=True, name=None): """Computes vertex normals from a mesh. This function computes vertex normals as the weighted sum of the adjacent face normals, where the weights correspond to the area of each face. This function supports planar convex polygon faces. For non-triangular meshes, this function converts them into triangular meshes to calculate vertex normals. Note: In the following, A1 to An are optional batch dimensions. Args: vertices: A tensor of shape `[A1, ..., An, V, 3]`, where V is the number of vertices. indices: A tensor of shape `[A1, ..., An, F, M]`, where F is the number of faces and M is the number of vertices per face. clockwise: Winding order to determine front-facing faces. The order of vertices should be either clockwise or counterclockwise. name: A name for this op. Defaults to "normals_vertex_normals". Returns: A tensor of shape `[A1, ..., An, V, 3]` containing vertex normals. If vertices and indices have different batch dimensions, this function broadcasts them into the same batch dimensions and the output batch dimensions are the broadcasted. Raises: ValueError: If the shape of `vertices`, `indices` is not supported. """ with tf.compat.v1.name_scope(name, "normals_vertex_normals", [vertices, indices]): vertices = tf.convert_to_tensor(value=vertices) indices = tf.convert_to_tensor(value=indices) shape.check_static(tensor=vertices, tensor_name="vertices", has_rank_greater_than=1, has_dim_equals=(-1, 3)) shape.check_static(tensor=indices, tensor_name="indices", has_rank_greater_than=1, has_dim_greater_than=(-1, 2)) shape.compare_batch_dimensions(tensors=(vertices, indices), last_axes=(-3, -3), broadcast_compatible=True) shape_indices = indices.shape.as_list() if None in shape_indices[:-2]: raise ValueError("'indices' must have specified batch dimensions.") common_batch_dims = shape.get_broadcasted_shape( vertices.shape[:-2], indices.shape[:-2]) vertices_repeat = [ common_batch_dims[x] // vertices.shape.as_list()[x] for x in range(len(common_batch_dims)) ] indices_repeat = [ common_batch_dims[x] // shape_indices[x] for x in range(len(common_batch_dims)) ] vertices = tf.tile(vertices, vertices_repeat + [1, 1], name="vertices_broadcast") indices = tf.tile(indices, indices_repeat + [1, 1], name="indices_broadcast") # Triangulate non-triangular faces. if shape_indices[-1] > 3: triangle_indices = [] for i in range(1, shape_indices[-1] - 1): triangle_indices.append( tf.concat((indices[..., 0:1], indices[..., i:i + 2]), axis=-1)) indices = tf.concat(triangle_indices, axis=-2) shape_indices = indices.shape.as_list() face_vertices = gather_faces(vertices, indices) # Use unnormalized face normals to scale normals by area. mesh_face_normals = face_normals(face_vertices, clockwise=clockwise, normalize=False) if vertices.shape.ndims > 2: outer_indices = np.meshgrid( *[np.arange(i) for i in shape_indices[:-2]], sparse=False, indexing="ij") outer_indices = [np.expand_dims(i, axis=-1) for i in outer_indices] outer_indices = np.concatenate(outer_indices, axis=-1) outer_indices = np.expand_dims(outer_indices, axis=-2) outer_indices = tf.constant(outer_indices, dtype=tf.int32) outer_indices = tf.tile(outer_indices, [1] * len(shape_indices[:-2]) + [tf.shape(input=indices)[-2]] + [1]) unnormalized_vertex_normals = tf.zeros_like(vertices) for i in range(shape_indices[-1]): scatter_indices = tf.concat( [outer_indices, indices[..., i:i + 1]], axis=-1) unnormalized_vertex_normals = tf.compat.v1.tensor_scatter_add( unnormalized_vertex_normals, scatter_indices, mesh_face_normals) else: unnormalized_vertex_normals = tf.zeros_like(vertices) for i in range(shape_indices[-1]): unnormalized_vertex_normals = tf.compat.v1.tensor_scatter_add( unnormalized_vertex_normals, indices[..., i:i + 1], mesh_face_normals) vector_norms = tf.sqrt( tf.reduce_sum(input_tensor=unnormalized_vertex_normals**2, axis=-1, keepdims=True)) return safe_ops.safe_unsigned_div(unnormalized_vertex_normals, vector_norms)