10

I'm drawing a bunch of lines with the Python Imaging Library's ImageDraw.line(), but they look horrid since I can't find a way to anti-alias them. How can I anti-alias lines in PIL?

Martin Thoma
  • 124,992
  • 159
  • 614
  • 958
dieki
  • 2,435
  • 5
  • 23
  • 29
  • Possible answer: http://stackoverflow.com/questions/1828345/any-way-to-make-nice-antialiased-round-corners-for-images-in-python – unutbu Jun 25 '10 at 23:25
  • Well, I suppose that is one solution, if a slow one. – dieki Jun 26 '10 at 13:06

2 Answers2

14

This is a really quickly hacked together function to draw an anti-aliased line with PIL that I wrote after googling for the same issue, seeing this post and failing to install aggdraw and being on a tight deadline. It's an implementation of Xiaolin Wu's line algorithm. I hope it helps anyone googling for the same thing!!

:)

"""Library to draw an antialiased line."""
# http://stackoverflow.com/questions/3122049/drawing-an-anti-aliased-line-with-thepython-imaging-library
# https://en.wikipedia.org/wiki/Xiaolin_Wu%27s_line_algorithm
import math


def plot(draw, img, x, y, c, col, steep, dash_interval):
    """Draws an antiliased pixel on a line."""
    if steep:
        x, y = y, x
    if x < img.size[0] and y < img.size[1] and x >= 0 and y >= 0:
        c = c * (float(col[3]) / 255.0)
        p = img.getpixel((x, y))
        x = int(x)
        y = int(y)
        if dash_interval:
            d = dash_interval - 1
            if (x / dash_interval) % d == 0 and (y / dash_interval) % d == 0:
                return
        draw.point((x, y), fill=(
            int((p[0] * (1 - c)) + col[0] * c),
            int((p[1] * (1 - c)) + col[1] * c),
            int((p[2] * (1 - c)) + col[2] * c), 255))


def iround(x):
    """Rounds x to the nearest integer."""
    return ipart(x + 0.5)


def ipart(x):
    """Floors x."""
    return math.floor(x)


def fpart(x):
    """Returns the fractional part of x."""
    return x - math.floor(x)


def rfpart(x):
    """Returns the 1 minus the fractional part of x."""
    return 1 - fpart(x)


def draw_line_antialiased(draw, img, x1, y1, x2, y2, col, dash_interval=None):
    """Draw an antialised line in the PIL ImageDraw.

    Implements the Xialon Wu antialiasing algorithm.

    col - color
    """
    dx = x2 - x1
    if not dx:
        draw.line((x1, y1, x2, y2), fill=col, width=1)
        return

    dy = y2 - y1
    steep = abs(dx) < abs(dy)
    if steep:
        x1, y1 = y1, x1
        x2, y2 = y2, x2
        dx, dy = dy, dx
    if x2 < x1:
        x1, x2 = x2, x1
        y1, y2 = y2, y1
    gradient = float(dy) / float(dx)

    # handle first endpoint
    xend = round(x1)
    yend = y1 + gradient * (xend - x1)
    xgap = rfpart(x1 + 0.5)
    xpxl1 = xend    # this will be used in the main loop
    ypxl1 = ipart(yend)
    plot(draw, img, xpxl1, ypxl1, rfpart(yend) * xgap, col, steep,
         dash_interval)
    plot(draw, img, xpxl1, ypxl1 + 1, fpart(yend) * xgap, col, steep,
         dash_interval)
    intery = yend + gradient  # first y-intersection for the main loop

    # handle second endpoint
    xend = round(x2)
    yend = y2 + gradient * (xend - x2)
    xgap = fpart(x2 + 0.5)
    xpxl2 = xend    # this will be used in the main loop
    ypxl2 = ipart(yend)
    plot(draw, img, xpxl2, ypxl2, rfpart(yend) * xgap, col, steep,
         dash_interval)
    plot(draw, img, xpxl2, ypxl2 + 1, fpart(yend) * xgap, col, steep,
         dash_interval)

    # main loop
    for x in range(int(xpxl1 + 1), int(xpxl2)):
        plot(draw, img, x, ipart(intery), rfpart(intery), col, steep,
             dash_interval)
        plot(draw, img, x, ipart(intery) + 1, fpart(intery), col, steep,
             dash_interval)
        intery = intery + gradient
David Underhill
  • 15,896
  • 7
  • 53
  • 61
toastie
  • 1,934
  • 3
  • 22
  • 35
  • What is col? What is steep? A little explanation would be greatly appreciated. – Matt S Jul 17 '13 at 07:21
  • @RamenRecon, I was curious about col and steep too. Based on my reading of the code, steep is a boolean for whether xdelta is less than ydelta and thus if the line is steep which probably affects the algorithm, but it is only used internally by the drawLine function and its plot helper function so isnt anything the user has to worry about. Col on the other hand has to be given by the user, and appears to be a 4-tuple RGBA color (red,gr,blu,alpha) which is used to decide each points alpha transparancy value (ie the antialiasing effect). Curious to try it and see how fast it is... – Karim Bahgat Mar 17 '14 at 23:57
  • 1
    Btw @toastie yours is the only manual Python implementation for antialiasing ive seen anywhere. Im surprised your answer doesnt have more upvotes, im sure it has helped a bunch of people though. +1 – Karim Bahgat Mar 18 '14 at 00:04
  • Thanks for this answer. @MattN.D.Hat, 'steep' is, as Karim'smenationed, a boolean for whether the line is closer to vertical than horizontal. The Wu line dawing algorithm works by taking the pixels either side of the line and weighting them (essentially giving them an opacity or alpha value) according to how close they lie to the line. If the line is closer to the vertical then these pixels are horizontal, otherwise they are vertical. I hope this is helpful. This code seems to be adapted from the wikipedia article at http://en.wikipedia.org/wiki/Xiaolin_Wu%27s_line_algorithm – Sam Sharp May 05 '15 at 11:57
  • You don't need to know what c or steep is. Just call, draw_line_antialiased(draw, img, x1, y1, x2, y2, col) col is the colour and must include RGB and A – gabbahey Sep 28 '20 at 17:42
2

I had a similar problem, my lines had rough edges where changing directions. I took a clue from how lines are drawn in IOS and came up with this code. It puts rounded line caps on the ends of the lines and really cleans things up. Not exactly anti-aliasing, but am totally new to PIL and had such a hard time finding an answer I figured I would share. Needs some tweaking and there is probably a better way but does what I need :)


    from PIL import Image
    import ImageDraw

    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y

    class DrawLines:
        def draw(self, points, color, imageName):
            img = Image.new("RGBA", [1440,1080], (255,255,255,0))
            draw  =  ImageDraw.Draw(img)
            linePoints = []
            for point in points:
                draw.ellipse((point.x-7, point.y-7, point.x+7, point.y+7), fill=color)
                linePoints.append(point.x)
                linePoints.append(point.y)
            draw.line(linePoints, fill=color, width=14)
            img.save(imageName)

    p1 = Point(100,200)
    p2 = Point(190,250)
    points = [p1,p2]

    red = (255,0,0)
    drawLines = DrawLines()
    drawLines.draw(points, red, "C:\\test.png")
Dave_750
  • 1,225
  • 1
  • 13
  • 28