我正在使用Python的成像库,我想绘制一些贝塞尔曲线.我想我可以逐像素计算,但我希望有更简单的东西.
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')
Bezier曲线并不难以吸引自己.考虑到三点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
(delta t),我们需要意识到这一点0 <= t <= 1
.正如您可能想象的那样,这不会给我们一条平滑的曲线,而是只产生一组离散的位置.解决此问题的最简单方法是在当前点和前一点之间绘制一条线.
您可以在PIL顶部使用aggdraw,支持贝塞尔曲线.
编辑:
我做了一个例子,发现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()
如果您要重新编译模块,这应该可以修复bug ...
尽管贝塞尔曲线到路径不适用于Aggdraw,如@ToniRuža所述,但在Aggdraw中还有另一种方法。使用Aggdraw代替PIL或您自己的bezier函数的好处是Aggdraw将对图像进行抗锯齿处理,使其看起来更平滑(请参见底部的图片)。
Aggdraw符号
除了可以使用aggdraw.Path()类进行绘制外,还可以使用aggdraw.Symbol(pathstring)
基本相同的类,只是将路径写为字符串。根据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!"
输出是一个看起来平滑的弯曲贝塞尔曲线图: