Как рисовать линии в массивах numpy?
Я хотел бы иметь возможность рисовать линии в массивах numpy, чтобы получить автономные функции для распознавания рукописного ввода в режиме онлайн. Это означает, что мне вообще не нужен образ, но мне нужно, чтобы некоторые позиции были в массиве numpy, у которого будет изображение с заданным размером.
Я хотел бы указать размер изображения, а затем нарисовать штрихи следующим образом:
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)
Возможно ли что-то простое (возможно, непосредственно с numpy/scipy)?
(Обратите внимание, что мне нужна интерполяция по шкале серого. Поэтому features
должна быть матрицей значений в [0, 255].)
Ответы
Ответ 1
Спасибо Джо Кингтону за ответ! Я искал 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)
Ответ 2
Я наткнулся на этот вопрос, ища решение, и предоставленный ответ решает его достаточно хорошо. Однако это не совсем соответствовало моим целям, для чего мне понадобилось "тензорное" решение (т.е. Реализовано в numpy без явных циклов) и, возможно, с параметром ширины линии. Я закончил реализацию своей собственной версии, и, поскольку в конце она также довольно быстро, чем line_aa, я думал, что могу поделиться ею.
Он поставляется в двух вариантах, с шириной линии и без нее. На самом деле первое не является обобщением последнего, и ничто не согласуется с линией_аа, но для моих целей они просто прекрасны, и на сюжетах они выглядят хорошо.
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)))
Я назвал это "наивным", потому что он очень похож на наивную реализацию в Wikipedia, но с некоторым сглаживанием, хотя по общему признанию, не идеально (например, делает очень тонкие диагонали).
Взвешенная версия дает намного более толстую линию более выраженного сглаживания.
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])
Регулировка веса, по общему признанию, довольно произвольная, поэтому любой может приспособить ее к своим вкусам. Rmin и rmax теперь необходимы, чтобы избежать пикселей за пределами изображения. Сравнение:
![Сравнение здесь]()
Как вы можете видеть, даже с w = 1, weighted_line немного толще, но в виде однородного способа; аналогично, naive_line однородно немного тоньше.
Заключительная записка о бенчмаркинге: на моей машине запуск %timeit f(1,1,100,240)
для различных функций (w = 1 для weighted_line) привел к времени 90 мкс для line_aa, 84 мкс для weighted_line (хотя время, конечно, увеличивается с вес) и 18 мкс для naive_line. Опять для сравнения, переопределение line_aa в чистом Python (вместо Cython, как в пакете), заняло 350 мкс.
Ответ 3
Я нашел подход val * 255
в ответе неоптимальным, потому что он, кажется, работает правильно только на черном фоне. Если фон содержит более темные и яркие области, это не совсем верно:
![enter image description here]()
Чтобы заставить его работать корректно на всех фонах, нужно учитывать цвета пикселей, которые покрыты сглаженной линией.
Вот небольшая демонстрация, основанная на оригинальном ответе:
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)
Интересная часть
np.multiply((1 - w) * np.ones([1, 3]),img[rows, cols, 0:3]) +
w * np.array([lineColorRgb])
где новый цвет вычисляется из исходного цвета изображения и цвета линии путем линейной интерполяции с использованием значений из weights
. Вот результат, оранжевая линия, проходящая через два вида фона:
![enter image description here]()
Теперь пиксели, которые окружают линию в верхней половине, становятся темнее, тогда как пиксели в нижней половине становятся ярче.