Как я могу нарисовать кривую Безье, используя PIL Python?
-
05-07-2019 - |
Вопрос
Я использую библиотеку изображений Python и хотел бы нарисовать несколько кривых Безье.Думаю, я мог бы посчитать попиксельно, но надеюсь, что есть что-то попроще.
Решение
Кривая Безье не так сложно нарисовать. Для трех точек A
, B
, C
вам потребуется три линейные интерполяции, чтобы нарисовать кривую. Мы используем скаляр t
в качестве параметра для линейной интерполяции:
P0 = A * t + (1 - t) * B
P1 = B * t + (1 - t) * C
Это интерполирует между двумя ребрами, которые мы создали, ребром AB и ребром BC. Единственное, что нам теперь нужно сделать, чтобы вычислить точку, которую мы должны нарисовать, это интерполировать между P0 и P1, используя один и тот же t, например:
Pfinal = P0 * t + (1 - t) * P1
Есть несколько вещей, которые необходимо сделать, прежде чем мы начнем рисовать кривую. Прежде всего, мы пройдемся немного dt
(дельта t), и мы должны знать, что 0 <= t <= 1
. Как вы можете себе представить, это не даст нам плавную кривую, а даст только дискретный набор позиций для построения графика. Самый простой способ решить эту проблему - просто нарисовать линию между текущей точкой и предыдущей точкой.
Другие советы
def make_bezier(xys):
# xys should be a sequence of 2-tuples (Bezier control points)
n = len(xys)
combinations = pascal_row(n-1)
def bezier(ts):
# This uses the generalized formula for bezier curves
# http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Generalization
result = []
for t in ts:
tpowers = (t**i for i in range(n))
upowers = reversed([(1-t)**i for i in range(n)])
coefs = [c*a*b for c, a, b in zip(combinations, tpowers, upowers)]
result.append(
tuple(sum([coef*p for coef, p in zip(coefs, ps)]) for ps in zip(*xys)))
return result
return bezier
def pascal_row(n, memo={}):
# This returns the nth row of Pascal's Triangle
if n in memo:
return memo[n]
result = [1]
x, numerator = 1, n
for denominator in range(1, n//2+1):
# print(numerator,denominator,x)
x *= numerator
x /= denominator
result.append(x)
numerator -= 1
if n&1 == 0:
# n is even
result.extend(reversed(result[:-1]))
else:
result.extend(reversed(result))
memo[n] = result
return result
Это, например, рисует сердце:
from PIL import Image
from PIL import ImageDraw
if __name__ == '__main__':
im = Image.new('RGBA', (100, 100), (0, 0, 0, 0))
draw = ImageDraw.Draw(im)
ts = [t/100.0 for t in range(101)]
xys = [(50, 100), (80, 80), (100, 50)]
bezier = make_bezier(xys)
points = bezier(ts)
xys = [(100, 50), (100, 0), (50, 0), (50, 35)]
bezier = make_bezier(xys)
points.extend(bezier(ts))
xys = [(50, 35), (50, 0), (0, 0), (0, 50)]
bezier = make_bezier(xys)
points.extend(bezier(ts))
xys = [(0, 50), (20, 80), (50, 100)]
bezier = make_bezier(xys)
points.extend(bezier(ts))
draw.polygon(points, fill = 'red')
im.save('out.png')
Вы можете использовать раздражать поверх PIL кривые Безье поддерживается.
РЕДАКТИРОВАТЬ:
Я привел пример только для того, чтобы обнаружить ошибку в Path
класс относительно curveto
:(
Во всяком случае, вот пример:
from PIL import Image
import aggdraw
img = Image.new("RGB", (200, 200), "white")
canvas = aggdraw.Draw(img)
pen = aggdraw.Pen("black")
path = aggdraw.Path()
path.moveto(0, 0)
path.curveto(0, 60, 40, 100, 100, 100)
canvas.path(path.coords(), path, pen)
canvas.flush()
img.save("curve.png", "PNG")
img.show()
Этот должен исправить ошибку, если вы готовы перекомпилировать модуль...
Хотя пути кривых Безье не работают с Aggdraw, как упоминал @ToniRuža, в Aggdraw есть другой способ сделать это.Преимущество использования Aggdraw вместо PIL или ваших собственных функций Безье заключается в том, что Aggdraw будет сглаживать изображение, делая его более гладким (см. рисунок внизу).
Символы Agdraw
Вместо использования класса aggdraw.Path() для рисования вы можете использовать aggdraw.Symbol(pathstring)
class, который по сути тот же, за исключением того, что вы пишете путь в виде строки.Согласно документации Aggdraw, способ записи пути в виде строки заключается в использовании синтаксиса пути SVG (см.: http://www.w3.org/TR/SVG/paths.html).По сути, каждое дополнение (узел) к пути обычно начинается с
- буква, обозначающая действие рисования (прописная для абсолютного пути, строчная для относительного пути), за которой следует (без пробелов между ними)
- координата x (перед ней ставится знак минус, если это отрицательное число или направление)
- запятая
- координата y (перед ней ставится знак минус, если это отрицательное число или направление)
В строке пути просто разделите несколько узлов пробелом.После того, как вы создали свой символ, не забудьте нарисовать его, передав его в качестве одного из аргументов функции. draw.symbol(args)
.
Кривые Безье в символах Aggdraw
В частности, для кубических кривых Безье вы пишете букву «C» или «c», за которой следуют 6 цифр (3 набора координат xy x1, y1, x2, y2, x3, y3 с запятыми между числами, но не между первым числом и письмо).Согласно документации, существуют и другие версии Безье с использованием буквы «S (гладкая кубическая Безье), Q (квадратичная Безье), T (гладкая квадратичная Безье)».Вот полный пример кода (требуется PIL и aggdraw):
print "initializing script"
# imports
from PIL import Image
import aggdraw
# setup
img = Image.new("RGBA", (1000,1000)) # last part is image dimensions
draw = aggdraw.Draw(img)
outline = aggdraw.Pen("black", 5) # 5 is the outlinewidth in pixels
fill = aggdraw.Brush("yellow")
# the pathstring:
#m for starting point
#c for bezier curves
#z for closing up the path, optional
#(all lowercase letters for relative path)
pathstring = " m0,0 c300,300,700,600,300,900 z"
# create symbol
symbol = aggdraw.Symbol(pathstring)
# draw and save it
xy = (20,20) # xy position to place symbol
draw.symbol(xy, symbol, outline, fill)
draw.flush()
img.save("testbeziercurves.png") # this image gets saved to same folder as the script
print "finished drawing and saved!"
В результате получается гладкая изогнутая фигура Безье:
Я нашел более простой способ создания кривой Безье (без сложных и сложных функций). Р>
import math
from PIL import Image
from PIL import ImageDraw
image = Image.new('RGB',(1190,841),'white')
draw = ImageDraw.Draw(image)
curve_smoothness = 100
#First, select start and end of curve (pixels)
curve_start = [(167,688)]
curve_end = [(678,128)]
#Second, split the path into segments
curve = []
for i in range(1,curve_smoothness,1):
split = (curve_end[0][0] - curve_start[0][0])/curve_smoothness
x = curve_start[0][0] + split * i
curve.append((x, -7 * math.pow(10,-7) * math.pow(x,3) - 0.0011 * math.pow(x,2) + 0.235 * x + 682.68))
#Third, edit any other corners of polygon
other =[(1026,721), (167,688)]
#Finally, combine all parts of polygon into one list
xys = curve_start + curve + curve_end + other #putting all parts of the polygon together
draw.polygon(xys, fill = None, outline = 256)
image.show()