//  [python]
def reprojection_error(tgt_points, src_points, depth, pose, intrinsics):
    """Compute re-projection error given match points
    
    Arguments:
        tgt_points {[type]} -- homogeneous coordinates [batch_size, match_num, 3]
        src_points {[type]} -- homogeneous coordinates [batch_size, match_num, 3]
        depth {depth mat} -- [batch_size, height, width]
        pose {pose vector} -- [batch_size, 6]
        intrinsics {[type]} -- [batch_size, 3, 3]
    
    Returns:
        reprojection error given sparse matches
    """
    batch = tf.shape(tgt_points)[0]
    match_num = tf.shape(tgt_points)[1]
    _, height, width, ch = depth.get_shape().as_list()
    pose = pose_vec2mat(pose)
    tgt_coords = tgt_points[:,:,:2]
    src_coords = src_points[:,:,:2]
    coords_x, coords_y = tf.split(tgt_coords, [1, 1], axis=-1)
    coords_x = tf.cast(coords_x, 'float32')
    coords_y = tf.cast(coords_y, 'float32')
    x0 = tf.floor(coords_x)
    x1 = x0 + 1
    y0 = tf.floor(coords_y)
    y1 = y0 + 1

    y_max = tf.cast(tf.shape(depth)[1] - 1, 'float32')
    x_max = tf.cast(tf.shape(depth)[2] - 1, 'float32')
    zero = tf.zeros([1], dtype='float32')

    x0_safe = tf.clip_by_value(x0, zero, x_max)
    y0_safe = tf.clip_by_value(y0, zero, y_max)
    x1_safe = tf.clip_by_value(x1, zero, x_max)
    y1_safe = tf.clip_by_value(y1, zero, y_max)

    wt_x0 = x1_safe - coords_x
    wt_x1 = coords_x - x0_safe
    wt_y0 = y1_safe - coords_y
    wt_y1 = coords_y - y0_safe

    one_img_size = height * width
    depths_flat = tf.reshape(depth, tf.stack([-1, 1]))
    for i in range(tgt_points.shape[0]):
        if i == 0:
            base = tf.zeros(shape=[1, match_num, 1])
        else:
            start = i * one_img_size * tf.ones(shape=[1, match_num, 1])
            base = tf.concat([base, start], axis=0)
    
    base_y0 = base + y0_safe * width 
    base_y1 = base + y1_safe * width 
    idx00 = x0_safe + base_y0
    idx01 = x0_safe + base_y1
    idx10 = x1_safe + base_y0
    idx11 = x1_safe + base_y1

    ## sample from depth
    im00 = tf.squeeze(tf.gather(depths_flat, tf.cast(idx00, 'int32')), axis=-1)
    im01 = tf.squeeze(tf.gather(depths_flat, tf.cast(idx01, 'int32')), axis=-1)
    im10 = tf.squeeze(tf.gather(depths_flat, tf.cast(idx10, 'int32')), axis=-1)
    im11 = tf.squeeze(tf.gather(depths_flat, tf.cast(idx11, 'int32')), axis=-1)

    w00 = wt_x0 * wt_y0
    w01 = wt_x0 * wt_y1
    w10 = wt_x1 * wt_y0
    w11 = wt_x1 * wt_y1
    sparse_depth = tf.add_n([w00 * im00, w01 * im01, w10 * im10, w11 * im11])

    #tgt_points = tf_render.reshape(tgt_points, shape=[batch, 3, -1])
    inverse_k = tf.expand_dims(tf.matrix_inverse(intrinsics), axis=1)
    inverse_k = tf.tile(inverse_k, [1, match_num, 1, 1])
    sparse_depth = tf.tile(tf.reshape(sparse_depth, shape=[batch, match_num, 1, 1]), [1,1,3,1])
    cam_coords = tf.matmul(inverse_k, tf.expand_dims(tgt_points, axis=-1)) * sparse_depth
    ones = tf.ones([batch, match_num, 1, 1])
    cam_coords = tf.concat([cam_coords, ones], axis=2)
    # Construct a 4x4 intrinsic matrix
    filler = tf.constant([0.0, 0.0, 0.0, 1.0], shape=[1, 1, 4])
    filler = tf.tile(filler, [batch, 1, 1])
    intrinsics = tf.concat([intrinsics, tf.zeros([batch, 3, 1])], axis=2)
    intrinsics = tf.concat([intrinsics, filler], axis=1)
    proj = tf.matmul(intrinsics, pose)
    proj = tf.tile(tf.expand_dims(proj, axis=1), [1, match_num, 1, 1])

    #cam_coords = tf_render.reshape(cam_coords, [batch, 4, -1])
    unnormalized_pixel_coords = tf.matmul(proj, cam_coords)
    x_u = tf.slice(unnormalized_pixel_coords, [0, 0, 0, 0], [-1, -1, 1, -1])
    y_u = tf.slice(unnormalized_pixel_coords, [0, 0, 1, 0], [-1, -1, 1, -1])
    z_u = tf.slice(unnormalized_pixel_coords, [0, 0, 2, 0], [-1, -1, 1, -1])
    x_n = x_u / (z_u + 1e-10)
    y_n = y_u / (z_u + 1e-10)
    src_pixel_coords = tf.squeeze(tf.concat([x_n, y_n], axis=2), axis=-1)

    reproj_error = tf.norm(src_pixel_coords - src_coords + 1e-6, axis=2)
    reproj_error = tf.reduce_mean(reproj_error, axis=1)
    return reproj_error