Page 1 of 1

Anti-aliasing

#1 DK3250  Icon User is offline

  • Pythonian
  • member icon

Reputation: 275
  • View blog
  • Posts: 892
  • Joined: 27-December 13

Posted 12 February 2016 - 08:37 AM

Your computer display is made of pixels. Whenever you draw a shape on the display, you (or the software) need to decide which pixels to color and which to leave uncolored/background.
As most shapes has curved outlines or lines not perpendicular to the screen, it becomes impossible to make a perfect match between shape and pixels.
The figure found in the link below shows an enlarged part of a circle. The circle gets a jagged perimeter. To compensate for this we use anti-aliasing.

Link: http://www.mediafire...7h02/Code_1.png

The word aliasing (and anti-aliasing) originates from signal processing. In graphic anti-aliasing is the process that smoothens the rough edges. In the circle shown in next link, this is accomplished by coloring the pixels just outside the figure in a soft color tone between figure color and background color.

Link: http://www.mediafire...nuzr/Code_2.png

Many APIs have modules for anti-aliasing. Pygame have the (experimental) gfx module, but even such modules has limitations; the gfx module only work for circles that are monochrome, i.e. same color all over.
To my knowledge, if your circle is multicolored, you need to do the anti-aliasing yourself.

For a start, lets look on a multicolored circle (ball) on a monochrome background and without movement of the figure.

A nice ball-like figure (without anti-aliasing) is obtained by this code:

import pygame, sys
pygame.init()

X = 600  # screen width
Y = 600  # screen heigth

WHITE = (255, 255, 255)

screen = pygame.display.set_mode((X, Y))
pygame.display.set_caption("Demo of anti-aliasing, by DK3250")
clock = pygame.time.Clock()


def red1(d2):  # gradient function
    return [max(255-int(d2*0.5), 0), max(200-int(d2*2), 0), max(200-int(d2*2), 0)]


class Ball():
    """
    The Ball object has three parameters:
     - 'rad' is the radius in pixels
     - 'light_pos' is a (x, y) tuple, the two numbers each in the interval [-1; 1], the value
       indicates light position in radius units relative to center
     - 'color_func' is the gradient function used for colorization of the Ball object
"""
    def __init__(self, rad, light_pos, color_func):
        x0 = rad + 1
        y0 = rad + 1
        x1 = x0 + int(light_pos[0] * rad)
        y1 = y0 + int(light_pos[1] * rad)

        transparent = WHITE
        self.surf = pygame.surface.Surface((x0*2, y0*2))
        self.surf.fill(transparent)
        self.surf.set_colorkey(transparent)

        for i in range(x0*2):
            for j in range(y0*2):
                d = ((i-x0)**2 + (j-y0)**2)**0.5
                if d > rad:
                    continue
                else:
                    # rate of color intensity change
                    d2 = ((i-x1)**2 + (j-y1)**2)**0.5 * 255 / rad

                    # apply color
                    color = color_func(d2)
                    self.surf.set_at((i, j), color)

    def draw(self):
        screen.blit(self.surf, (40, 40))

    def move(self):
        pass

ball = Ball(255, (0.4, -0.4), red1)

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    clock.tick(60)
    screen.fill(WHITE)

    ball.draw()

    pygame.display.flip()


Comments to the code:
By placing the figure on a small surface, it is prepared for later movements.
The gradient function (red1) is responsible for the impression of light from top-right side. Feel free to experiment with it.
The d2-value used with the gradient function includes a compensation for radius change, larger radius need slower change rate (color change per pixel) small radius requires a quick change.
Note the un-even surface of the output this is the primary issue in this post.

Finally, Anti-aliasing....

If you look at the code, you will see the Pythagorean theorem used to determine if a point is inside or outside the circle. For each pixel a distance from center is calculated by
d = ((i-x0)**2 + (j-y0)**2)**0.5
The value 'd' is the pixel-in-question distance from center and this distance is compared to the radius.
If d < radius, the point is inside the circle.
Clearly, if d > radius +1 the point is outside the circle.
But, if radius > d > radius +1, the pixel is on the border part of the pixel is inside the circle and part of it is outside.
If we subtract radius and substitute alfa = d radius, we get
0 > alfa > 1
alfa will actually be the (decimal-)fraction of the pixel belonging to the background. Small alfa means 'd' (pixel distance) is close to (true) radius, i.e. the pixel should mostly belong to the circle; and vice versa.
Knowing the alfa-value, it is quite easy to calculate the color of the pixel in question, using weighted average: For each color-component, red, green, blue, you do
color_component = color_circle * (1 alfa) + color_background * alfa

The code becomes:

import pygame, random, sys
pygame.init()

X = 600  # screen width
Y = 600  # screen heigth

WHITE = (255, 255, 255)

screen = pygame.display.set_mode((X, Y))
pygame.display.set_caption("Demo of anti-aliasing, by DK3250")
clock = pygame.time.Clock()


def red1(d2):
    return [max(255-int(d2*0.5), 0), max(200-int(d2*2), 0), max(200-int(d2*2), 0)]


def red2(d2):
    return [max(255-int(d2*0.5), 0), max(200-int(d2*2), 0), 0]


def red3(d2):
    return [max(255-int(d2*0.5), 0), 0, max(200-int(d2*2), 0)]


color_functions = [red1, red2, red3]


class Ball():
    """
    The Ball object has three parameters:
     - 'rad' is the radius in pixels
     - 'light_pos' is a (x, y) tuple, the two numbers each in the interval [-1; 1], the value
       indicates light position in radius units relative to center
     - 'color_func' is the gradient function used for colorization of the Ball object
"""
    def __init__(self, rad, light_pos, color_func):
        self.rad = rad

        x0 = rad + 1
        y0 = rad + 1
        x1 = x0 + int(light_pos[0] * rad)
        y1 = y0 + int(light_pos[1] * rad)

        transparent = WHITE
        self.surf = pygame.surface.Surface((x0*2, y0*2))
        self.surf.fill(transparent)
        self.surf.set_colorkey(transparent)

        for i in range(x0*2):
            for j in range(y0*2):
                d = ((i-x0)**2 + (j-y0)**2)**0.5
                if d > rad+1:
                    continue
                else:
                    # rate of color intensity change
                    d2 = ((i-x1)**2 + (j-y1)**2)**0.5 * 255 / rad

                    # apply color
                    color = color_func(d2)

                    if d > rad:  # anti-alising
                        alfa = d-rad
                        bg = WHITE
                        color = [c * (1-alfa) + b * alfa for c, b in zip(color, bg)]

                    self.surf.set_at((i, j), color)

    def draw(self):
        screen.blit(self.surf, (40, 40))

    def move(self):
        pass

ball_color = random.choice(color_functions)
ball = Ball(100, (0.4, -0.4), ball_color)

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
    clock.tick(60)
    screen.fill(WHITE)

    ball.draw()

    pygame.display.flip()



The output may look like this

Link: http://www.mediafire...Code_2_full.png

First of all: Please enjoy the perimeter! Anti-aliasing in function.
Notice how simple the anti-aliasing code is.
I have included a few more gradient functions (red2 and red3), primarily for inspiration purposes


Now, what if the background is not monochrome? In Pygame, we can get the background color for each pixel by the display.get_at() function. In the next example I simply create a gradient background and place the ball.

import pygame, random, sys
pygame.init()

X = 600  # screen width
Y = 600  # screen heigth

WHITE = (255, 255, 255)

screen = pygame.display.set_mode((X, Y))
pygame.display.set_caption("Demo of anti-aliasing, by DK3250")
clock = pygame.time.Clock()

background = pygame.surface.Surface((X, Y))
for i in range(100):
    pygame.draw.rect(background, (0, int(i*2.5), 255-int(i*2.5)), (i*6, 0, 6, Y), 0)


def red1(d2):
    return [max(255-int(d2*0.5), 0), max(200-int(d2*2), 0), max(200-int(d2*2), 0)]


def red2(d2):
    return [max(255-int(d2*0.5), 0), max(200-int(d2*2), 0), 0]


def red3(d2):
    return [max(255-int(d2*0.5), 0), 0, max(200-int(d2*2), 0)]


def green1(d2):
    return [max(200-int(d2*2), 0), max(255-int(d2*0.5), 0), max(200-int(d2*2), 0)]


def green2(d2):
    return [0, max(255-int(d2*0.5), 0), max(200-int(d2*2), 0)]


def green3(d2):
    return [max(200-int(d2*2), 0), max(255-int(d2*0.5), 0), 0]


def blue1(d2):
    return [max(200-int(d2*2), 0), max(200-int(d2*2), 0), max(255-int(d2*0.3), 0)]


def blue2(d2):
    return [max(200-int(d2*2), 0), 0, max(255-int(d2*0.3), 0)]


def blue3(d2):
    return [0, max(200-int(d2*2), 0), max(255-int(d2*0.3), 0)]


def cyan1(d2):
    return [max(200-int(d2*2), 0), max(255-int(d2*0.3), 0), max(255-int(d2*0.3), 0)]


def magenta1(d2):
    return [max(255-int(d2*0.3), 0), max(100-int(d2*1.5), 0), max(255-int(d2*0.3), 0)]


def yellow1(d2):
    return [max(255-int(d2*0.6), 0), max(255-int(d2*0.6), 0), 0]


color_functions = [red1, red2, red3, green1, green2, green3, blue1, blue2, blue3, cyan1, magenta1, yellow1]


class Ball():
    """
    The Ball object has three parameters:
     - 'rad' is the radius in pixels
     - 'light_pos' is a (x, y) tuple, the two numbers each in the interval [-1; 1], the value
       indicates light position in radius units relative to center
     - 'color_func' is the gradient function used for colorization of the Ball object
"""
    def __init__(self, rad, light_pos, color_func):
        self.rad = rad

        x0 = rad + 1
        y0 = rad + 1
        x1 = x0 + int(light_pos[0] * rad)
        y1 = y0 + int(light_pos[1] * rad)

        transparent = WHITE
        self.surf = pygame.surface.Surface((x0*2, y0*2))
        self.surf.fill(transparent)
        self.surf.set_colorkey(transparent)

        self.perimeter = []  # list of pixels that needs anti-aliasing

        for i in range(x0*2):
            for j in range(y0*2):
                d = ((i-x0)**2 + (j-y0)**2)**0.5
                if d > rad+1:
                    continue
                else:
                    # rate of color intensity change
                    d2 = ((i-x1)**2 + (j-y1)**2)**0.5 * 255 / rad

                    # apply color
                    color = color_func(d2)

                    if d > rad:  # prepare for anti-aliasing, get the perimeter pixels
                        alfa = d-rad
                        color_rim = [c * (1-alfa) for c in color]
                        self.perimeter.append(((i, j), color_rim, alfa))
                        color = transparent  # colorkey at rim

                    self.surf.set_at((i, j), color)

    def draw(self):
        """
    first: the self.surf (picture without perimeter) is blitted to screen
    second: the perimeter is updated, anti-aliasing to the background
    """
        screen.blit(background, (0, 0))
        screen.blit(self.surf, (40, 40))  # hard coded position

        # anti-aliasing
        for p in self.perimeter:
            x, y = p[0]
            x += 40  # hard coded position
            y += 40
            color = p[1]
            alfa = p[2]
            bg = screen.get_at((x, y))
            color_aa = [rim + back * alfa for rim, back in zip(color, bg)]
            screen.set_at((x, y), color_aa)

    def move(self):
        pass


ball_color = random.choice(color_functions)
ball = Ball(100, (0.4, -0.4), ball_color)

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
    clock.tick(60)

    ball.draw()

    pygame.display.flip()



Now that we cannot know the background color during __init__(), we simply set the perimeter to transparent, and store relevant data in a perimeter list.
Later, in the draw() method, the background color is read, and the anti-aliasing calculation is finalized.
The hard-coded position is only for demonstration, but will change now...

What about movements?
That is easy, now that all preparations are in place. Here is a simple bouncing ball example:
import pygame, random, sys
pygame.init()

X = 600  # screen width
Y = 600  # screen heigth

WHITE = (255, 255, 255)

screen = pygame.display.set_mode((X, Y))
pygame.display.set_caption("Demo of anti-aliasing, by DK3250")
clock = pygame.time.Clock()

background = pygame.surface.Surface((X, Y))
for i in range(100):
    pygame.draw.rect(background, (0, int(i*2.5), 255-int(i*2.5)), (i*6, 0, 6, Y), 0)


def red1(d2):
    return [max(255-int(d2*0.5), 0), max(200-int(d2*2), 0), max(200-int(d2*2), 0)]


def red2(d2):
    return [max(255-int(d2*0.5), 0), max(200-int(d2*2), 0), 0]


def red3(d2):
    return [max(255-int(d2*0.5), 0), 0, max(200-int(d2*2), 0)]


def green1(d2):
    return [max(200-int(d2*2), 0), max(255-int(d2*0.5), 0), max(200-int(d2*2), 0)]


def green2(d2):
    return [0, max(255-int(d2*0.5), 0), max(200-int(d2*2), 0)]


def green3(d2):
    return [max(200-int(d2*2), 0), max(255-int(d2*0.5), 0), 0]


def blue1(d2):
    return [max(200-int(d2*2), 0), max(200-int(d2*2), 0), max(255-int(d2*0.3), 0)]


def blue2(d2):
    return [max(200-int(d2*2), 0), 0, max(255-int(d2*0.3), 0)]


def blue3(d2):
    return [0, max(200-int(d2*2), 0), max(255-int(d2*0.3), 0)]


def cyan1(d2):
    return [max(200-int(d2*2), 0), max(255-int(d2*0.3), 0), max(255-int(d2*0.3), 0)]


def magenta1(d2):
    return [max(255-int(d2*0.3), 0), max(100-int(d2*1.5), 0), max(255-int(d2*0.3), 0)]


def yellow1(d2):
    return [max(255-int(d2*0.6), 0), max(255-int(d2*0.6), 0), 0]


color_functions = [red1, red2, red3, green1, green2, green3, blue1, blue2, blue3, cyan1, magenta1, yellow1]


class Ball():
    """
    The Ball object has three parameters:
     - 'rad' is the radius in pixels
     - 'light_pos' is a (x, y) tuple, the two numbers each in the interval [-1; 1], the value
       indicates light position in radius units relative to center
     - 'color_func' is the gradient function used for colorization of the Ball object
"""
    def __init__(self, rad, light_pos, color_func):
        self.rad = rad

        x0 = rad + 1
        y0 = rad + 1
        x1 = x0 + int(light_pos[0] * rad)
        y1 = y0 + int(light_pos[1] * rad)
        transparent = WHITE

        self.x = random.randint(0, X - 2 * x0)  # position
        self.y = random.randint(0, Y - 2 * y0)
        self.dx = random.randint(3, 6)  # speed
        self.dy = random.randint(3, 6)

        self.surf = pygame.surface.Surface((x0*2, y0*2))
        self.surf.fill(transparent)
        self.surf.set_colorkey(transparent)

        self.perimeter = []  # list of pixels that needs anti-aliasing

        for i in range(x0*2):
            for j in range(y0*2):
                d = ((i-x0)**2 + (j-y0)**2)**0.5
                if d > rad+1:
                    continue
                else:
                    # rate of color intensity change
                    d2 = ((i-x1)**2 + (j-y1)**2)**0.5 * 255 / rad

                    # apply color
                    color = color_func(d2)

                    if d > rad:  # prepare for anti-aliasing, get the perimeter pixels
                        alfa = d-rad
                        color_rim = [c * (1-alfa) for c in color]
                        self.perimeter.append(((i, j), color_rim, alfa))
                        color = transparent  # colorkey at rim

                    self.surf.set_at((i, j), color)

    def draw(self):
        """
    first: the self.surf (picture without perimeter) is blitted to screen
    second: the perimeter is updated, anti-aliasing to the background
    """
        screen.blit(self.surf, (self.x, self.y))

        # anti-aliasing
        for p in self.perimeter:
            x, y = p[0]
            x += self.x
            y += self.y
            color = p[1]
            alfa = p[2]
            bg = screen.get_at((x, y))  # this will work with any background, not only monochrome one
            color_aa = [rim + back * alfa for rim, back in zip(color, bg)]
            screen.set_at((x, y), color_aa)

    def move(self):  # a simple move() function - only for demonstration
        self.x += self.dx
        self.y += self.dy

        if self.x < 0 or self.x > X-2*self.rad-2:
            self.dx *= -1
            self.x += self.dx

        if self.y < 0 or self.y > Y-2*self.rad-2:
            self.dy *= -1
            self.y += self.dy

ball_color = random.choice(color_functions)
ball = Ball(100, (0.4, -0.4), ball_color)

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
    clock.tick(60)
    screen.blit(background, (0, 0))

    ball.move()
    ball.draw()

    pygame.display.flip()



See how we only need to re-calculate the perimeter! Everything else stay the same!

Finally, I include an example where the background is changing and the ball moving. The background color is determined by a generator function that may be somewhat subtle. It is, however, outside the subject for this tutorial. Just enjoy the anti-aliasing!
import pygame, random, sys
pygame.init()

X = 1020  # screen width
Y = 600  # screen heigth

WHITE = (255, 255, 255)

screen = pygame.display.set_mode((X, Y))
pygame.display.set_caption("Demo of anti-aliasing, by DK3250")
clock = pygame.time.Clock()


def red1(d2):
    return [max(255-int(d2*0.5), 0), max(200-int(d2*2), 0), max(200-int(d2*2), 0)]


def red2(d2):
    return [max(255-int(d2*0.5), 0), max(200-int(d2*2), 0), 0]


def red3(d2):
    return [max(255-int(d2*0.5), 0), 0, max(200-int(d2*2), 0)]


def green1(d2):
    return [max(200-int(d2*2), 0), max(255-int(d2*0.5), 0), max(200-int(d2*2), 0)]


def green2(d2):
    return [0, max(255-int(d2*0.5), 0), max(200-int(d2*2), 0)]


def green3(d2):
    return [max(200-int(d2*2), 0), max(255-int(d2*0.5), 0), 0]


def blue1(d2):
    return [max(200-int(d2*2), 0), max(200-int(d2*2), 0), max(255-int(d2*0.3), 0)]


def blue2(d2):
    return [max(200-int(d2*2), 0), 0, max(255-int(d2*0.3), 0)]


def blue3(d2):
    return [0, max(200-int(d2*2), 0), max(255-int(d2*0.3), 0)]


def cyan1(d2):
    return [max(200-int(d2*2), 0), max(255-int(d2*0.3), 0), max(255-int(d2*0.3), 0)]


def magenta1(d2):
    return [max(255-int(d2*0.3), 0), max(100-int(d2*1.5), 0), max(255-int(d2*0.3), 0)]


def yellow1(d2):
    return [max(255-int(d2*0.6), 0), max(255-int(d2*0.6), 0), 0]


color_functions = [red1, red2, red3, green1, green2, green3, blue1, blue2, blue3, cyan1, magenta1, yellow1]


def getDisplayColor():
    """
    Generator function used to avoid repetition of the initialization.
    The generator cycles through the the three colors (r, g, B)/>/>/> and move one color value
    at a time to a random end point in steps of +1 or -1
"""
    color = [200] * 3                # start color
    rgb_index = 1                    # indicates the r, g, b color to change, r=0, g=1, b=2
    colEnd = random.randint(0, 255)  # the color change end point

    while True:
        dif = colEnd - color[rgb_index]
        if dif != 0:
            color[rgb_index] += dif/abs(dif)
            yield color

        else:
            colEnd = random.randint(0, 255)
            rgb_index = (rgb_index+1) % 3


class Ball():
    """
    The Ball object has three parameters:
     - 'rad' is the radius in pixels
     - 'light_pos' is a (x, y) tuple, the two numbers each in the interval [-1; 1], the value
       indicates light position in radius units relative to center
     - 'color_func' is the gradient function used for colorization of the Ball object
"""
    def __init__(self, rad, light_pos, color_func):
        self.rad = rad

        x0 = rad + 1
        y0 = rad + 1
        x1 = x0 + int(light_pos[0] * rad)
        y1 = y0 + int(light_pos[1] * rad)
        transparent = WHITE

        self.x = random.randint(0, X - 2 * x0)  # position
        self.y = random.randint(0, Y - 2 * y0)
        self.dx = random.randint(3, 6)  # speed
        self.dy = random.randint(3, 6)

        self.surf = pygame.surface.Surface((x0*2, y0*2))
        self.surf.fill(transparent)
        self.surf.set_colorkey(transparent)

        self.perimeter = []  # list of pixels that needs anti-aliasing

        for i in range(x0*2):
            for j in range(y0*2):
                d = ((i-x0)**2 + (j-y0)**2)**0.5
                if d > rad+1:
                    continue
                else:
                    # rate of color intensity change
                    d2 = ((i-x1)**2 + (j-y1)**2)**0.5 * 255 / rad

                    # apply color
                    color = color_func(d2)

                    if d > rad:  # prepare for anti-aliasing, get the perimeter pixels
                        alfa = d-rad
                        color_rim = [c * (1-alfa) for c in color]
                        self.perimeter.append(((i, j), color_rim, alfa))
                        color = transparent  # colorkey at rim

                    self.surf.set_at((i, j), color)

    def draw(self):
        """
    first: the self.surf (picture without perimeter) is blitted to screen
    second: the perimeter is updated, anti-aliasing to the background
    """
        screen.blit(self.surf, (self.x, self.y))

        # anti-aliasing
        for p in self.perimeter:
            x, y = p[0]
            x += self.x
            y += self.y
            color = p[1]
            alfa = p[2]
            bg = screen.get_at((x, y))  # this will work with any background, not only monochrome one
            color_aa = [rim + back * alfa for rim, back in zip(color, bg)]
            screen.set_at((x, y), color_aa)

    def move(self):  # a simple move() function - only for demonstration
        self.x += self.dx
        self.y += self.dy

        if self.x < 0 or self.x > X-2*self.rad-2:
            self.dx *= -1
            self.x += self.dx

        if self.y < 0 or self.y > Y-2*self.rad-2:
            self.dy *= -1
            self.y += self.dy

ball_color = random.choice(color_functions)
ball = Ball(100, (0.4, -0.4), ball_color)

bg_color = getDisplayColor()  # initialiazation of the generator
while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
    clock.tick(60)
    screen.fill(next(bg_color))  # call of generator

    ball.move()
    ball.draw()

    pygame.display.flip()



Is This A Good Question/Topic? 0
  • +

Page 1 of 1