1

I would like to draw a line plot into an image with OpenCV in Python. The behavior should be similar to the matplotlib library.

Dots/dashes should be evenly distributed across the line string.


Other questions does not really provide me with the desired output. They usually divide each line independently, which causes uneven distribution of dots. There is also a problem with plots that have shorter lines than the specified gap.

Such solutions results in an ugly plot.

opencv rectangle with dotted or dashed lines

Others suggest using the LineIterator which does not exists in the python version of OpenCV

Erik Hulmák
  • 133
  • 1
  • 7

1 Answers1

0

If you want to obtain uniformly distributed dots/dashes with guaranteed gap, you have to place them with regard to the beginning of the line string. First, we sample distances linearly up to the sum of line distances.

All points are then projected on corresponding segments. Finally, we plot lines, dashes, dots, etc...

def get_rotation_matrix(theta):
    return np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]])


def angle(v1, v2):
    """ get angle between two vectors. """
    ang = np.arccos(v1@v2 / (np.linalg.norm(v1) * np.linalg.norm(v2)))
    return -1 * np.sign(np.cross(v1, v2)) * ang


def subdivide_linestring(points: np.ndarray, gap=20):
    cumsum_dist = np.cumsum(np.linalg.norm(points[:-1] - points[1:], axis=1))
    x = np.linspace(0, cumsum_dist[-1], int(cumsum_dist[-1] / gap))
    samples = np.vstack([x, np.zeros_like(x)]).T

    d = [0] + cumsum_dist.tolist()
    xy = []
    for i in range(len(points)-1):
        p0, p1 = points[[i, i+1]]
        # can be done more efficiently
        m = np.bitwise_and(d[i]-np.finfo(float).eps < x, x < d[i+1]) 
        R = get_rotation_matrix(angle(p1-p0, [1, 0]))
        xy.append(p0 + (samples[m] - [d[i], 0]) @ R.T)
    return np.vstack(xy)


def plot(img, points: np.ndarray, color, thickness, linestyle="-", gap=20, alpha=1.):
    im = img.copy()
    if linestyle not in ("-", "-.", "--", ":"):
        raise NotImplementedError(f"Linestyle: {linestyle} not implemented.")
        
    xy = points if linestyle == "-" else subdivide_linestring(points, gap)
    xy = np.rint(xy).astype(int)
    
    if linestyle == "-":
        for p1, p2 in zip(xy[:-1], xy[1:]):
            cv2.line(im, p1, p2, color, thickness)
    elif linestyle == "--":
        for i, (p1, p2) in enumerate(zip(xy[:-1], xy[1:])):
            if (i+1) % 3:
                cv2.line(im, p1, p2, color, thickness)
    elif linestyle == ":":
        for p in xy:
            cv2.circle(im, p, thickness, color, -1)
    elif linestyle == "-.":
        for i in range(len(xy)-1):
            if (i%4) == 0: 
                cv2.line(im, *xy[[i, i+1]], color, thickness)
            if (i%4) == 2:
                p = np.rint(xy[i] + (xy[i+1]-xy[i])/2).astype(int) # interpolation
                cv2.circle(im, p, thickness, color, -1)
    return cv2.addWeighted(im, alpha, img, 1 - alpha, 0, im)
Erik Hulmák
  • 133
  • 1
  • 7