def corner_shape_parameters(corners, frame_shape, cb_shape):
    """
    Return a tuple of shape parameters for a given set of corners. This is
    based on the parameters from ROS's perception library[1]. The parameters
    returned are mean x- and y- co-ordinate normalised onto the interval
    [0,1], the relative size of the set of corners within the frame on the
    interval [0,1] and a 'skewness' metric on the inteval [0,1].

    *corners* is a Nx2 numpy array of detected corner locations.

    *frame_shape* is a pair giving the width and height of the frame.

    *cb_shape* is a pair giving the number of horizontal and vertical corners

    [1] https://github.com/ros-perception/image_pipeline/

    """
    corners = corners.reshape(-1, 2)
    assert corners.shape[0] == cb_shape[0] * cb_shape[1]

    h, w = frame_shape

    # Extract the "outside" corners which define the shape of the board in
    # a clockwise order. I.e. [A,B,C,D] where:
    #
    # A----B
    # |    |
    # D----C
    A, B, C, D = tuple(x.reshape(-1) for x in corners[[0, cb_shape[0]-1, -1, -cb_shape[0]], :])

    ba, bc = A - B, C - B   # Edges
    bd, ac = D - B, C - A   # Diagonals

    # Compute the angle between AB and BC
    angle = np.arccos(np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc)))

    # Compute skew metric
    skew = min(1, 2 * np.abs(0.5*np.pi - angle))

    # Compute area (assuming quadrilateral) and hence size metric
    area = 0.5 * np.abs(bd[0]*ac[1] - bd[1]*ac[0])
    size = np.sqrt(area / (h*w))

    # For X and Y, we "shrink" the image all around by approx. half the board
    # size. Otherwise large boards are penalized because you can't get much
    # X/Y variation.
    border = np.sqrt(area)
    X = np.clip((np.mean(corners[:,0]) - 0.5*border) / (w - border), 0, 1)
    Y = np.clip((np.mean(corners[:,1]) - 0.5*border) / (h - border), 0, 1)

    return (X, Y, size, skew)