如何使用Python的PIL来绘制bezier曲线?

时间:2021-06-18 00:24:26

I'm using Python's Imaging Library and I would like to draw some bezier curves. I guess I could calculate pixel by pixel but I'm hoping there is something simpler.

我正在使用Python的图像库,我想画一些贝塞尔曲线。我想我可以逐像素计算,但我希望有更简单的东西。

5 个解决方案

#1


11  

A bezier curve isn't that hard to draw yourself. Given three points A, B, C you require three linear interpolations in order to draw the curve. We use the scalar t as the parameter for the linear interpolation:

要画出比齐尔曲线并不难。给定三个点A, B, C你需要三个线性插值才能画出曲线。我们用标量t作为线性插值的参数:

P0 = A * t + (1 - t) * B
P1 = B * t + (1 - t) * C

This interpolates between two edges we've created, edge AB and edge BC. The only thing we now have to do to calculate the point we have to draw is interpolate between P0 and P1 using the same t like so:

这个插值在我们创建的两条边之间,边AB和边BC。我们现在要做的唯一一件事就是计算我们要画的点就是用同样的t在P0和P1之间进行插值

Pfinal = P0 * t + (1 - t) * P1

There are a couple of things that need to be done before we actually draw the curve. First off we have will walk some dt (delta t) and we need to be aware that 0 <= t <= 1. As you might be able to imagine, this will not give us a smooth curve, instead it yields only a discrete set of positions at which to plot. The easiest way to solve this is to simply draw a line between the current point and the previous point.

在绘制曲线之前,我们需要做一些事情。首先,我们要走一些dt (t)我们需要知道0 <= t <= 1。正如你可以想象的,这不会给我们一个平滑的曲线,相反,它只会产生一组离散的位置来绘制。解决这个问题最简单的方法就是在当前点和前一点之间画一条线。

#2


17  

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):
    # This returns the nth row of Pascal's Triangle
    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)) 
    return result

This, for example, draws a heart:

举个例子来说,这引起了人们的关注:

from PILL 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')

#3


7  

You can use the aggdraw on top of PIL, bezier curves are supported.

您可以在PIL上使用aggdraw,支持bezier曲线。

EDIT:

编辑:

I made an example only to discover there is a bug in the Path class regarding curveto :(

我举了一个例子,结果发现Path类中有一个关于curveto的错误。

Here is the example anyway:

举个例子:

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

This should fix the bug if you're up for recompiling the module...

如果要重新编译模块,这应该可以修复错误……

#4


4  

Although bezier curveto paths don't work with Aggdraw, as mentioned by @ToniRuža, there is another way to do this in Aggdraw. The benefit of using Aggdraw instead of PIL or your own bezier functions is that Aggdraw will antialias the image making it look smoother (see pic at bottom).

虽然贝塞尔曲线curveto路径与Aggdraw不工作,由@ToniRuža如前所述,在Aggdraw还有另一种方法。使用Aggdraw而不是PIL或您自己的bezier函数的好处是,Aggdraw可以消除图像的别名,使其看起来更平滑(见下图)。

Aggdraw Symbols

Aggdraw符号

Instead of using the aggdraw.Path() class to draw, you can use the aggdraw.Symbol(pathstring) class which is basically the same except you write the path as a string. According to the Aggdraw docs the way to write your path as a string is to use SVG path syntax (see: http://www.w3.org/TR/SVG/paths.html). Basically, each addition (node) to the path normally starts with

与使用aggdraw.Path()类来绘制不同,您可以使用aggdraw.Symbol(pathstring)类,它基本上是相同的,除非您将路径作为字符串写入。根据Aggdraw文档,将路径写成字符串的方法是使用SVG路径语法(参见:http://www.w3.org/TR/SVG/paths.html)。基本上,对路径的每个添加(节点)通常都是从开始的

  • a letter representing the drawing action (uppercase for absolute path, lowercase for relative path), followed by (no spaces in between)
  • 表示绘图动作的字母(绝对路径用大写,相对路径用小写),后跟(中间没有空格)
  • the x coordinate (precede by a minus sign if it is a negative number or direction)
  • x坐标(如果是负数或方向,则在前面加上负号)
  • a comma
  • 一个逗号
  • the y coordinate (precede by a minus sign if it is a negative number or direction)
  • y坐标(如果是负数或方向,则在负号之前)

In your pathstring just separate your multiple nodes with a space. Once you have created your symbol, just remember to draw it by passing it as one of the arguments to draw.symbol(args).

在路径字符串中,只需用空格分隔多个节点。创建了符号后,只需记住将其作为绘制.symbol(args)的参数之一传递给它就可以了。

Bezier Curves in Aggdraw Symbols

在Aggdraw符号中的Bezier曲线

Specifically for cubic bezier curves you write the letter "C" or "c" followed by 6 numbers (3 sets of xy coordinates x1,y1,x2,y2,x3,y3 with commas in between the numbers but not between the first number and the letter). According the docs there are also other bezier versions by using the letter "S (smooth cubic bezier), Q (quadratic bezier), T (smooth quadratic bezier)". Here is a complete example code (requires PIL and 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!"

And the output is a smooth-looking curved bezier figure: 如何使用Python的PIL来绘制bezier曲线?

输出是一个光滑的曲面bezier图形:

#5


0  

I found a simpler way creating a bezier curve (without aggraw and without complex functions).

我发现了一种创建bezier曲线的更简单的方法(没有aggraw,也没有复杂的函数)。

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

#1


11  

A bezier curve isn't that hard to draw yourself. Given three points A, B, C you require three linear interpolations in order to draw the curve. We use the scalar t as the parameter for the linear interpolation:

要画出比齐尔曲线并不难。给定三个点A, B, C你需要三个线性插值才能画出曲线。我们用标量t作为线性插值的参数:

P0 = A * t + (1 - t) * B
P1 = B * t + (1 - t) * C

This interpolates between two edges we've created, edge AB and edge BC. The only thing we now have to do to calculate the point we have to draw is interpolate between P0 and P1 using the same t like so:

这个插值在我们创建的两条边之间,边AB和边BC。我们现在要做的唯一一件事就是计算我们要画的点就是用同样的t在P0和P1之间进行插值

Pfinal = P0 * t + (1 - t) * P1

There are a couple of things that need to be done before we actually draw the curve. First off we have will walk some dt (delta t) and we need to be aware that 0 <= t <= 1. As you might be able to imagine, this will not give us a smooth curve, instead it yields only a discrete set of positions at which to plot. The easiest way to solve this is to simply draw a line between the current point and the previous point.

在绘制曲线之前,我们需要做一些事情。首先,我们要走一些dt (t)我们需要知道0 <= t <= 1。正如你可以想象的,这不会给我们一个平滑的曲线,相反,它只会产生一组离散的位置来绘制。解决这个问题最简单的方法就是在当前点和前一点之间画一条线。

#2


17  

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):
    # This returns the nth row of Pascal's Triangle
    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)) 
    return result

This, for example, draws a heart:

举个例子来说,这引起了人们的关注:

from PILL 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')

#3


7  

You can use the aggdraw on top of PIL, bezier curves are supported.

您可以在PIL上使用aggdraw,支持bezier曲线。

EDIT:

编辑:

I made an example only to discover there is a bug in the Path class regarding curveto :(

我举了一个例子,结果发现Path类中有一个关于curveto的错误。

Here is the example anyway:

举个例子:

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

This should fix the bug if you're up for recompiling the module...

如果要重新编译模块,这应该可以修复错误……

#4


4  

Although bezier curveto paths don't work with Aggdraw, as mentioned by @ToniRuža, there is another way to do this in Aggdraw. The benefit of using Aggdraw instead of PIL or your own bezier functions is that Aggdraw will antialias the image making it look smoother (see pic at bottom).

虽然贝塞尔曲线curveto路径与Aggdraw不工作,由@ToniRuža如前所述,在Aggdraw还有另一种方法。使用Aggdraw而不是PIL或您自己的bezier函数的好处是,Aggdraw可以消除图像的别名,使其看起来更平滑(见下图)。

Aggdraw Symbols

Aggdraw符号

Instead of using the aggdraw.Path() class to draw, you can use the aggdraw.Symbol(pathstring) class which is basically the same except you write the path as a string. According to the Aggdraw docs the way to write your path as a string is to use SVG path syntax (see: http://www.w3.org/TR/SVG/paths.html). Basically, each addition (node) to the path normally starts with

与使用aggdraw.Path()类来绘制不同,您可以使用aggdraw.Symbol(pathstring)类,它基本上是相同的,除非您将路径作为字符串写入。根据Aggdraw文档,将路径写成字符串的方法是使用SVG路径语法(参见:http://www.w3.org/TR/SVG/paths.html)。基本上,对路径的每个添加(节点)通常都是从开始的

  • a letter representing the drawing action (uppercase for absolute path, lowercase for relative path), followed by (no spaces in between)
  • 表示绘图动作的字母(绝对路径用大写,相对路径用小写),后跟(中间没有空格)
  • the x coordinate (precede by a minus sign if it is a negative number or direction)
  • x坐标(如果是负数或方向,则在前面加上负号)
  • a comma
  • 一个逗号
  • the y coordinate (precede by a minus sign if it is a negative number or direction)
  • y坐标(如果是负数或方向,则在负号之前)

In your pathstring just separate your multiple nodes with a space. Once you have created your symbol, just remember to draw it by passing it as one of the arguments to draw.symbol(args).

在路径字符串中,只需用空格分隔多个节点。创建了符号后,只需记住将其作为绘制.symbol(args)的参数之一传递给它就可以了。

Bezier Curves in Aggdraw Symbols

在Aggdraw符号中的Bezier曲线

Specifically for cubic bezier curves you write the letter "C" or "c" followed by 6 numbers (3 sets of xy coordinates x1,y1,x2,y2,x3,y3 with commas in between the numbers but not between the first number and the letter). According the docs there are also other bezier versions by using the letter "S (smooth cubic bezier), Q (quadratic bezier), T (smooth quadratic bezier)". Here is a complete example code (requires PIL and 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!"

And the output is a smooth-looking curved bezier figure: 如何使用Python的PIL来绘制bezier曲线?

输出是一个光滑的曲面bezier图形:

#5


0  

I found a simpler way creating a bezier curve (without aggraw and without complex functions).

我发现了一种创建bezier曲线的更简单的方法(没有aggraw,也没有复杂的函数)。

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