¿Cómo puedo dibujar líneas en matrices numpy?

Me gustaría poder dibujar líneas en matrices numpy para obtener características fuera de línea para el reconocimiento de escritura a mano en línea. Esto significa que no necesito la imagen en absoluto, pero necesito algunas posiciones en una matriz numpy a la que se vería una imagen de un tamaño determinado.

Me gustaría poder especificar un tamaño de imagen y luego dibujar trazos como este:

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) 

¿Es algo tan simple como eso posible (preferiblemente directamente con adormecimiento / símil)?

(Tenga en cuenta que quiero interpolación en escala de grises. Por lo tanto, las features deben ser una matriz de valores en [0, 255].)

Gracias a Joe Kington por la respuesta! Estaba buscando 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) 

Me encontré con esta pregunta mientras buscaba una solución, y la respuesta proporcionada la resuelve bastante bien. Sin embargo, realmente no se adaptaba a mis propósitos, por lo que necesitaba una solución “tensorizable” (es decir, implementada en un número sin ciclos explícitos), y posiblemente con una opción de ancho de línea. Terminé implementando mi propia versión, y como al final también es bastante más rápido que line_aa, pensé que podía compartirla.

Se presenta en dos sabores, con y sin ancho de línea. En realidad, la primera no es una generalización de la última, y ​​ninguna de las dos está perfectamente de acuerdo con line_aa, pero para mis propósitos están bien y en las plots se ven bien.

 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))) 

Llamé a esto "ingenuo" porque es bastante similar a la implementación ingenua en Wikipedia , pero con algo de suavizado, aunque ciertamente no es perfecto (por ejemplo, hace diagonales muy finas).

La versión ponderada proporciona una línea mucho más gruesa más pronunciada 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]) 

El ajuste de peso es bastante arbitrario, por lo que cualquiera puede ajustar eso a sus gustos. Ahora se necesitan rmin y rmax para evitar píxeles fuera de la imagen. Una comparación:

Una comparación está aquí

Como puede ver, incluso con w = 1, weighted_line es un poco más grueso, pero de una forma homogénea; De manera similar, naive_line es homogéneamente ligeramente más delgada.

Nota final sobre la evaluación comparativa: en mi máquina, ejecutar %timeit f(1,1,100,240) para las diversas funciones (w = 1 para weighted_line) resultó en un tiempo de 90 µs para line_aa, 84 µs para weighted_line (aunque el tiempo por supuesto aumenta) con el peso) y 18 µs para naive_line. Nuevamente para comparación, la reimplementación de line_aa en Python puro (en lugar de Cython como en el paquete) tomó 350 µs.

He encontrado que el enfoque val * 255 en la respuesta es subóptimo, porque parece funcionar correctamente solo sobre fondo negro. Si el fondo contiene regiones más oscuras y shinys, esto no parece del todo correcto:

introduzca la descripción de la imagen aquí

Para que funcione correctamente en todos los fondos, uno tiene que tomar en cuenta los colores de los píxeles que están cubiertos por la línea de suavizado.

Aquí hay una pequeña demostración que se basa en la respuesta original:

 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) 

La parte interesante es

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

donde el nuevo color se calcula a partir del color original de la imagen y el color de la línea, mediante interpolación lineal utilizando los valores de los weights alias. Aquí hay un resultado, una línea naranja que corre sobre dos tipos de fondo:

introduzca la descripción de la imagen aquí

Ahora los píxeles que rodean la línea en la mitad superior se vuelven más oscuros , mientras que los píxeles en la mitad inferior se vuelven más shinys .