How to draw lines in numpy arrays?

I would like to be able to draw lines in numpy arrays in order to get offline functions for online handwriting recognition. This means that I don't need an image at all, but I need some positions to be in a numpy array that will have an image with a given size.

I would like to specify the size of the image and then draw the strokes like this:

import module
im = module.new_image(width=800, height=200)
im.add_stroke(from={'x': 123, 'y': 2}, to={'x': 42, 'y': 3})
im.add_stroke(from={'x': 4, 'y': 3}, to={'x': 2, 'y': 1})
features = im.get(x_min=12, x_max=15, y_min=0, y_max=111)

      

Is something simple possible (perhaps directly from numpy / scipy)?

(Note that I need grayscale interpolation. So it features

should be a matrix of values ​​at [0, 255].)

+11


source to share


3 answers


Thanks to Joe Kington for the answer! I've been looking skimage.draw.line_aa

.



import scipy.misc
import numpy as np
from skimage.draw import line_aa
img = np.zeros((10, 10), dtype=np.uint8)
rr, cc, val = line_aa(1, 1, 8, 4)
img[rr, cc] = val * 255
scipy.misc.imsave("out.png", img)

      

+16


source


I stumbled upon this question while looking for a solution and the answer provided solves it quite well. However, this didn't quite suit my purposes, for which I needed a "tensor" solution (ie Implemented in numpy without explicit loops) and possibly with a line width parameter. I ended up implementing my own version, and since it is also quite fast than line_aa in the end, I thought I could share it.

It comes in two versions, with and without line width. In fact, the former is not a generalization of the latter, and nothing agrees with line_aa, but for my purposes they are just fine and they look good on the plots.

def naive_line(r0, c0, r1, c1):
    # The algorithm below works fine if c1 >= c0 and c1-c0 >= abs(r1-r0).
    # If either of these cases are violated, do some switches.
    if abs(c1-c0) < abs(r1-r0):
        # Switch x and y, and switch again when returning.
        xx, yy, val = naive_line(c0, r0, c1, r1)
        return (yy, xx, val)

    # At this point we know that the distance in columns (x) is greater
    # than that in rows (y). Possibly one more switch if c0 > c1.
    if c0 > c1:
        return naive_line(r1, c1, r0, c0)

    # We write y as a function of x, because the slope is always <= 1
    # (in absolute value)
    x = np.arange(c0, c1+1, dtype=float)
    y = x * (r1-r0) / (c1-c0) + (c1*r0-c0*r1) / (c1-c0)

    valbot = np.floor(y)-y+1
    valtop = y-np.floor(y)

    return (np.concatenate((np.floor(y), np.floor(y)+1)).astype(int), np.concatenate((x,x)).astype(int),
            np.concatenate((valbot, valtop)))

      

I called this "naive" because it is very similar to the naive implementation in Wikipedia , but with some anti-aliasing, although admittedly not ideal (makes very thin diagonals for example).

The weighted version gives a much thicker line for more anti-aliasing.



def trapez(y,y0,w):
    return np.clip(np.minimum(y+1+w/2-y0, -y+1+w/2+y0),0,1)

def weighted_line(r0, c0, r1, c1, w, rmin=0, rmax=np.inf):
    # The algorithm below works fine if c1 >= c0 and c1-c0 >= abs(r1-r0).
    # If either of these cases are violated, do some switches.
    if abs(c1-c0) < abs(r1-r0):
        # Switch x and y, and switch again when returning.
        xx, yy, val = weighted_line(c0, r0, c1, r1, w, rmin=rmin, rmax=rmax)
        return (yy, xx, val)

    # At this point we know that the distance in columns (x) is greater
    # than that in rows (y). Possibly one more switch if c0 > c1.
    if c0 > c1:
        return weighted_line(r1, c1, r0, c0, w, rmin=rmin, rmax=rmax)

    # The following is now always < 1 in abs
    slope = (r1-r0) / (c1-c0)

    # Adjust weight by the slope
    w *= np.sqrt(1+np.abs(slope)) / 2

    # We write y as a function of x, because the slope is always <= 1
    # (in absolute value)
    x = np.arange(c0, c1+1, dtype=float)
    y = x * slope + (c1*r0-c0*r1) / (c1-c0)

    # Now instead of 2 values for y, we have 2*np.ceil(w/2).
    # All values are 1 except the upmost and bottommost.
    thickness = np.ceil(w/2)
    yy = (np.floor(y).reshape(-1,1) + np.arange(-thickness-1,thickness+2).reshape(1,-1))
    xx = np.repeat(x, yy.shape[1])
    vals = trapez(yy, y.reshape(-1,1), w).flatten()

    yy = yy.flatten()

    # Exclude useless parts and those outside of the interval
    # to avoid parts outside of the picture
    mask = np.logical_and.reduce((yy >= rmin, yy < rmax, vals > 0))

    return (yy[mask].astype(int), xx[mask].astype(int), vals[mask])

      

Weight adjustment is admittedly quite arbitrary, so anyone can tailor it to their tastes. Rmin and rmax are now needed to avoid pixels outside the image. Comparison:

Compare here

As you can see, even with w = 1, the weighted_line is slightly thicker, but in a uniform fashion; likewise, naive_line is uniformly slightly thinner.

Final note on benchmarking: on my machine, running %timeit f(1,1,100,240)

for various functions (w = 1 for weighted_line) resulted in a time of 90 μs for line_aa, 84 μs for weighted_line (although the time certainly increases with weight) and 18 μs for naive_line. Again for comparison, overriding line_aa in pure Python (instead of Cython as in a package) took 350 μs.

+8


source


I found the approach val * 255

in the answer suboptimal because it only seems to work correctly on a black background. If the background contains darker and brighter areas, this is not entirely true:

enter image description here

To make it work correctly on all backgrounds, you need to consider the colors of the pixels that are covered by the anti-aliased line.

Here's a small demo based on the original answer:

from scipy import ndimage
from scipy import misc
from skimage.draw import line_aa
import numpy as np


img = np.zeros((100, 100, 4), dtype = np.uint8)  # create image
img[:,:,3] = 255                                 # set alpha to full
img[30:70, 40:90, 0:3] = 255                     # paint white rectangle
rows, cols, weights = line_aa(10, 10, 90, 90)    # antialias line

w = weights.reshape([-1, 1])            # reshape anti-alias weights
lineColorRgb = [255, 120, 50]           # color of line, orange here

img[rows, cols, 0:3] = (
  np.multiply((1 - w) * np.ones([1, 3]),img[rows, cols, 0:3]) +
  w * np.array([lineColorRgb])
)
misc.imsave('test.png', img)

      

The interesting part

np.multiply((1 - w) * np.ones([1, 3]),img[rows, cols, 0:3]) +
w * np.array([lineColorRgb])

      

where the new color is calculated from the original image color and line color by linear interpolation using values ​​from weights

. Here's the result, an orange line going through the two backgrounds:

enter image description here

Now the pixels that surround the line in the upper half become darker, while the pixels in the lower half become brighter.

+4


source







All Articles