def allocentric_to_egocentric(quat, proj_ctr, inv_intrinsics):
    """
    Parameters
    ----------
    quat: Tensor
        (N, 4). Batch of (allocentric) quaternions.

    proj_ctr: Tensor
        (N, 2). Projected centers. xy coordninates.

    inv_intrinsics: [type]
        (N, 3, 3). Inverted intrinsics.
    """
    R_obj_to_local = quaternion_to_matrix(quat)

    # ray == z-axis in local orientaion
    ray = unproject_points2d(proj_ctr, inv_intrinsics)
    z = ray / ray.norm(dim=1, keepdim=True)

    # gram-schmit process: local_y = global_y - global_y \dot local_z
    y = z.new_tensor([[0., 1., 0.]]) - z[:, 1:2] * z
    y = y / y.norm(dim=1, keepdim=True)
    x = torch.cross(y, z, dim=1)

    # local -> global
    R_local_to_global = torch.stack([x, y, z], dim=-1)

    # obj -> global
    R_obj_to_global = torch.bmm(R_local_to_global, R_obj_to_local)

    egocentric_quat = matrix_to_quaternion(R_obj_to_global)

    # Make sure it's unit norm.
    quat_norm = egocentric_quat.norm(dim=1, keepdim=True)
    if not torch.allclose(quat_norm, torch.as_tensor(1.), atol=1e-3):
        LOG.warning(
            f"Some of the input quaternions are not unit norm: min={quat_norm.min()}, max={quat_norm.max()}; therefore normalizing."
        )
        egocentric_quat = egocentric_quat / quat_norm.clamp(min=EPS)

    return egocentric_quat

    Parameters
    ----------
    quat: Tensor
        (N, 4). Batch of (allocentric) quaternions.

    proj_ctr: Tensor
        (N, 2). Projected centers. xy coordninates.

    inv_intrinsics: [type]
        (N, 3, 3). Inverted intrinsics.
    
tridet/utils/geometry.py