views:

1524

answers:

3

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.

+5  A: 

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: P0 = A * t + (1 - t) * B and 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 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 yiels 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.

Jasper Bekkers
thanks for your answer, i may end up doing this in the end. that's what i meant when i said "I guess I could calculate pixel by pixel"... that i could just do the math but was wondering if something built in could be used.
carrier
+3  A: 

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

EDIT:

I made an example only to discover there is a bug in the Path class regarding 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...

Toni Ruža
+1  A: 
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:

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