def between_two_vectors_3d(vector1, vector2, name=None): """Computes quaternion over the shortest arc between two vectors. Result quaternion describes shortest geodesic rotation from vector1 to vector2. Note: In the following, A1 to An are optional batch dimensions. Args: vector1: A tensor of shape `[A1, ..., An, 3]`, where the last dimension represents the first vector. vector2: A tensor of shape `[A1, ..., An, 3]`, where the last dimension represents the second vector. name: A name for this op that defaults to "quaternion_between_two_vectors_3d". Returns: A tensor of shape `[A1, ..., An, 4]`, where the last dimension represents a normalized quaternion. Raises: ValueError: If the shape of `vector1` or `vector2` is not supported. """ with tf.compat.v1.name_scope(name, "quaternion_between_two_vectors_3d", [vector1, vector2]): vector1 = tf.convert_to_tensor(value=vector1) vector2 = tf.convert_to_tensor(value=vector2) shape.check_static( tensor=vector1, tensor_name="vector1", has_dim_equals=(-1, 3)) shape.check_static( tensor=vector2, tensor_name="vector2", has_dim_equals=(-1, 3)) shape.compare_batch_dimensions( tensors=(vector1, vector2), last_axes=-2, broadcast_compatible=True) # Make sure that we are dealing with unit vectors. vector1 = tf.nn.l2_normalize(vector1, axis=-1) vector2 = tf.nn.l2_normalize(vector2, axis=-1) cos_theta = vector.dot(vector1, vector2) real_part = 1.0 + cos_theta axis = vector.cross(vector1, vector2) # Compute arbitrary antiparallel axes to rotate around in case of opposite # vectors. x, y, z = tf.split(vector1, (1, 1, 1), axis=-1) x_bigger_z = tf.abs(x) > tf.abs(z) x_bigger_z = tf.concat([x_bigger_z] * 3, axis=-1) antiparallel_axis = tf.compat.v1.where( x_bigger_z, tf.concat((-y, x, tf.zeros_like(z)), axis=-1), tf.concat((tf.zeros_like(x), -z, y), axis=-1)) # Compute rotation between two vectors. is_antiparallel = real_part < 1e-6 is_antiparallel = tf.concat([is_antiparallel] * 4, axis=-1) rot = tf.compat.v1.where( is_antiparallel, tf.concat((antiparallel_axis, tf.zeros_like(real_part)), axis=-1), tf.concat((axis, real_part), axis=-1)) return tf.nn.l2_normalize(rot, axis=-1)
def test_cross_jacobian_preset(self, u_init, v_init): """Tests the Jacobian of the dot product.""" u_tensor = tf.convert_to_tensor(value=u_init) v_tensor = tf.convert_to_tensor(value=v_init) y = vector.cross(u_tensor, v_tensor) self.assert_jacobian_is_correct(u_tensor, u_init, y) self.assert_jacobian_is_correct(v_tensor, v_init, y)
def test_cross_random(self): """Tests the cross product function.""" tensor_size = np.random.randint(1, 4) tensor_shape = np.random.randint(1, 10, size=tensor_size).tolist() axis = np.random.randint(tensor_size) tensor_shape[axis] = 3 # pylint: disable=invalid-sequence-index u = np.random.random(size=tensor_shape) v = np.random.random(size=tensor_shape) self.assertAllClose(vector.cross(u, v, axis=axis), np.cross(u, v, axis=axis))
def rotate(point, axis, angle, name=None): r"""Rotates a 3d point using an axis-angle by applying the Rodrigues' formula. Rotates a vector $$\mathbf{v} \in {\mathbb{R}^3}$$ into a vector $$\mathbf{v}' \in {\mathbb{R}^3}$$ using the Rodrigues' rotation formula: $$\mathbf{v}'=\mathbf{v}\cos(\theta)+(\mathbf{a}\times\mathbf{v})\sin(\theta) +\mathbf{a}(\mathbf{a}\cdot\mathbf{v})(1-\cos(\theta)).$$ Note: In the following, A1 to An are optional batch dimensions. Args: point: A tensor of shape `[A1, ..., An, 3]`, where the last dimension represents a 3d point to rotate. axis: A tensor of shape `[A1, ..., An, 3]`, where the last dimension represents a normalized axis. angle: A tensor of shape `[A1, ..., An, 1]`, where the last dimension represents an angle. name: A name for this op that defaults to "axis_angle_rotate". Returns: A tensor of shape `[A1, ..., An, 3]`, where the last dimension represents a 3d point. Raises: ValueError: If `point`, `axis`, or `angle` are of different shape or if their respective shape is not supported. """ with tf.compat.v1.name_scope(name, "axis_angle_rotate", [point, axis, angle]): point = tf.convert_to_tensor(value=point) axis = tf.convert_to_tensor(value=axis) angle = tf.convert_to_tensor(value=angle) shape.check_static(tensor=point, tensor_name="point", has_dim_equals=(-1, 3)) shape.check_static(tensor=axis, tensor_name="axis", has_dim_equals=(-1, 3)) shape.check_static(tensor=angle, tensor_name="angle", has_dim_equals=(-1, 1)) shape.compare_batch_dimensions(tensors=(point, axis, angle), tensor_names=("point", "axis", "angle"), last_axes=-2, broadcast_compatible=True) axis = asserts.assert_normalized(axis) cos_angle = tf.cos(angle) axis_dot_point = vector.dot(axis, point) return point * cos_angle + vector.cross(axis, point) * tf.sin( angle) + axis * axis_dot_point * (1.0 - cos_angle)
def test_cross_jacobian_random(self): """Test the Jacobian of the dot product.""" tensor_size = np.random.randint(3) tensor_shape = np.random.randint(1, 10, size=(tensor_size)).tolist() u_init = np.random.random(size=tensor_shape + [3]) v_init = np.random.random(size=tensor_shape + [3]) u_tensor = tf.convert_to_tensor(value=u_init) v_tensor = tf.convert_to_tensor(value=v_init) y = vector.cross(u_tensor, v_tensor) self.assert_jacobian_is_correct(u_tensor, u_init, y) self.assert_jacobian_is_correct(v_tensor, v_init, y)
def normal(v0: type_alias.TensorLike, v1: type_alias.TensorLike, v2: type_alias.TensorLike, clockwise: bool = False, normalize: bool = True, name: str = "triangle_normal") -> tf.Tensor: """Computes face normals (triangles). Note: In the following, A1 to An are optional batch dimensions, which must be broadcast compatible. Args: v0: A tensor of shape `[A1, ..., An, 3]`, where the last dimension represents the first vertex of a triangle. v1: A tensor of shape `[A1, ..., An, 3]`, where the last dimension represents the second vertex of a triangle. v2: A tensor of shape `[A1, ..., An, 3]`, where the last dimension represents the third vertex of a triangle. clockwise: Winding order to determine front-facing triangles. normalize: A `bool` indicating whether output normals should be normalized by the function. name: A name for this op. Defaults to "triangle_normal". Returns: A tensor of shape `[A1, ..., An, 3]`, where the last dimension represents a normalized vector. Raises: ValueError: If the shape of `v0`, `v1`, or `v2` is not supported. """ with tf.name_scope(name): v0 = tf.convert_to_tensor(value=v0) v1 = tf.convert_to_tensor(value=v1) v2 = tf.convert_to_tensor(value=v2) shape.check_static(tensor=v0, tensor_name="v0", has_dim_equals=(-1, 3)) shape.check_static(tensor=v1, tensor_name="v1", has_dim_equals=(-1, 3)) shape.check_static(tensor=v2, tensor_name="v2", has_dim_equals=(-1, 3)) shape.compare_batch_dimensions(tensors=(v0, v1, v2), last_axes=-2, broadcast_compatible=True) normal_vector = vector.cross(v1 - v0, v2 - v0, axis=-1) normal_vector = asserts.assert_nonzero_norm(normal_vector) if not clockwise: normal_vector *= -1.0 if normalize: return tf.nn.l2_normalize(normal_vector, axis=-1) return normal_vector
def between_two_vectors_3d(vector1, vector2, name=None): """Computes quaternion over the shortest arc between two vectors. Result quaternion describes shortest geodesic rotation from vector1 to vector2. Note: In the following, A1 to An are optional batch dimensions. Args: vector1: A tensor of shape `[A1, ..., An, 3]`, where the last dimension represents the first vector. vector2: A tensor of shape `[A1, ..., An, 3]`, where the last dimension represents the second vector. name: A name for this op that defaults to "quaternion_between_two_vectors_3d". Returns: A tensor of shape `[A1, ..., An, 4]`, where the last dimension represents a normalized quaternion. Raises: ValueError: If the shape of `vector1` or `vector2` is not supported. """ with tf.compat.v1.name_scope(name, "quaternion_between_two_vectors_3d", [vector1, vector2]): vector1 = tf.convert_to_tensor(value=vector1) vector2 = tf.convert_to_tensor(value=vector2) shape.check_static(tensor=vector1, tensor_name="vector1", has_dim_equals=(-1, 3)) shape.check_static(tensor=vector2, tensor_name="vector2", has_dim_equals=(-1, 3)) shape.compare_batch_dimensions(tensors=(vector1, vector2), last_axes=-2, broadcast_compatible=True) # Make sure we deal with unit vectors. vector1 = tf.nn.l2_normalize(vector1, axis=-1) vector2 = tf.nn.l2_normalize(vector2, axis=-1) axis = vector.cross(vector1, vector2) cos_theta = vector.dot(vector1, vector2) rot = tf.concat((axis, 1. + cos_theta), axis=-1) return tf.nn.l2_normalize(rot, axis=-1)
def area(v0: type_alias.TensorLike, v1: type_alias.TensorLike, v2: type_alias.TensorLike, name: str = "triangle_area") -> tf.Tensor: """Computes triangle areas. Note: Computed triangle area = 0.5 * | e1 x e2 | where e1 and e2 are edges of triangle. A degenerate triangle will return 0 area, whereas the normal for a degenerate triangle is not defined. In the following, A1 to An are optional batch dimensions, which must be broadcast compatible. Args: v0: A tensor of shape `[A1, ..., An, 3]`, where the last dimension represents the first vertex of a triangle. v1: A tensor of shape `[A1, ..., An, 3]`, where the last dimension represents the second vertex of a triangle. v2: A tensor of shape `[A1, ..., An, 3]`, where the last dimension represents the third vertex of a triangle. name: A name for this op. Defaults to "triangle_area". Returns: A tensor of shape `[A1, ..., An, 1]`, where the last dimension represents a normalized vector. """ with tf.name_scope(name): v0 = tf.convert_to_tensor(value=v0) v1 = tf.convert_to_tensor(value=v1) v2 = tf.convert_to_tensor(value=v2) shape.check_static(tensor=v0, tensor_name="v0", has_dim_equals=(-1, 3)) shape.check_static(tensor=v1, tensor_name="v1", has_dim_equals=(-1, 3)) shape.check_static(tensor=v2, tensor_name="v2", has_dim_equals=(-1, 3)) shape.compare_batch_dimensions(tensors=(v0, v1, v2), last_axes=-2, broadcast_compatible=True) normals = vector.cross(v1 - v0, v2 - v0, axis=-1) return 0.5 * tf.linalg.norm(tensor=normals, axis=-1, keepdims=True)
def right_handed(camera_position, look_at, up_vector, name=None): """Builds a right handed look at view matrix. Note: In the following, A1 to An are optional batch dimensions. Args: camera_position: A tensor of shape `[A1, ..., An, 3]`, where the last dimension represents the 3D position of the camera. look_at: A tensor of shape `[A1, ..., An, 3]`, with the last dimension storing the position where the camera is looking at. up_vector: A tensor of shape `[A1, ..., An, 3]`, where the last dimension defines the up vector of the camera. name: A name for this op. Defaults to 'right_handed'. Raises: ValueError: if the all the inputs are not of the same shape, or if any input of of an unsupported shape. Returns: A tensor of shape `[A1, ..., An, 4, 4]`, containing right handed look at matrices. """ with tf.compat.v1.name_scope(name, "right_handed", [camera_position, look_at, up_vector]): camera_position = tf.convert_to_tensor(value=camera_position) look_at = tf.convert_to_tensor(value=look_at) up_vector = tf.convert_to_tensor(value=up_vector) shape.check_static(tensor=camera_position, tensor_name="camera_position", has_dim_equals=(-1, 3)) shape.check_static(tensor=look_at, tensor_name="look_at", has_dim_equals=(-1, 3)) shape.check_static(tensor=up_vector, tensor_name="up_vector", has_dim_equals=(-1, 3)) shape.compare_batch_dimensions(tensors=(camera_position, look_at, up_vector), last_axes=-2, tensor_names=("camera_position", "look_at", "up_vector"), broadcast_compatible=False) z_axis = tf.linalg.l2_normalize(look_at - camera_position, axis=-1) horizontal_axis = tf.linalg.l2_normalize(vector.cross( z_axis, up_vector), axis=-1) vertical_axis = vector.cross(horizontal_axis, z_axis) batch_shape = tf.shape(input=horizontal_axis)[:-1] zeros = tf.zeros(shape=tf.concat((batch_shape, (3, )), axis=-1), dtype=horizontal_axis.dtype) one = tf.ones(shape=tf.concat((batch_shape, (1, )), axis=-1), dtype=horizontal_axis.dtype) matrix = tf.concat( (horizontal_axis, -vector.dot(horizontal_axis, camera_position), vertical_axis, -vector.dot(vertical_axis, camera_position), -z_axis, vector.dot(z_axis, camera_position), zeros, one), axis=-1) matrix_shape = tf.shape(input=matrix) output_shape = tf.concat((matrix_shape[:-1], (4, 4)), axis=-1) return tf.reshape(matrix, shape=output_shape)