White spots when drawing mutliple lines close to e

2019-07-10 05:13发布

问题:

How can I get rid of white spots when drawing multiple circles close to each other in pygame?

Here is my code:

import pygame
from pygame import gfxdraw
from math import pow, atan2

def getColor(r, col1, col2, fun="lin"):
    if fun =="pol2":
        r = pow(r,2)

    col1_r = tuple([r*x for x in col1])
    col2_r = tuple([(1-r)*x for x in col2])
    final_col = tuple(sum(i) for i in zip(col1_r, col2_r))
    return final_col

def draw(sizeX, sizeY):
    # Initialize the game engine
    pygame.init()

    screen = pygame.display.set_mode([sizeX, sizeY])

    #Loop until the user clicks the close button.
    done = False
    clock = pygame.time.Clock()

    while not done:

        # This limits the while loop to a max of 10 times per second.
        # Leave this out and we will use all CPU we can.
        clock.tick(10)

        for event in pygame.event.get(): # User did something
            if event.type == pygame.QUIT: # If user clicked close
                done=True # Flag that we are done so we exit this loop

        screen.fill(WHITE)

        for y in range(200,500):
            for x in range(0,10):
                gfxdraw.arc(screen, 400, 400, y, x*15, (x+1)*15, getColor(x/10,(0,0,(y-200)/2),(255,255,(y-200)/2), fun="lin"))

        pygame.display.flip()

    pygame.quit()

回答1:

This phenomenon is called aliasing and happens when you take a continuous signal and samples it. In your case, gfx.draw() uses continuous functions (the trigonometric functions) to calculate which pixel to draw the color onto. Since theses calculations are in floats and have to be rounded to integers, it may happen that some pixels are missed.

To fix this you need an anti-aliasing filter. There are many different types such as low pass (blurring), oversampling etc.


Since these holes almost always are one pixel I'd create a function that identifies these holes and fills them with the average of it's neighbours colors. The problem is that Pygame is not very good at manually manipulating pixels, so it can be slow depending on the size of the image. Although, Pygame has a module called surfarray that's built on numpy which allows you to access pixels easier and faster, so that will speed it up some. Of course, it'll require you to install numpy.

I couldn't get your program to work, so next time make sure you really have a Minimal, Complete, and Verifiable example. The following code is just based on the image you provided.

import numpy as np
import pygame
pygame.init()

RADIUS = 1080 // 2
FPS = 30
screen = pygame.display.set_mode((RADIUS * 2, RADIUS * 2))
clock = pygame.time.Clock()

circle_size = (RADIUS * 2, RADIUS * 2)
circle = pygame.Surface(circle_size)

background_color = (255, 255, 255)
circle_color = (255, 0, 0)

pygame.draw.circle(circle, circle_color, (RADIUS, RADIUS), RADIUS, RADIUS // 2)


def remove_holes(surface, background=(0, 0, 0)):
    """
    Removes holes caused by aliasing.

    The function locates pixels of color 'background' that are surrounded by pixels of different colors and set them to
    the average of their neighbours. Won't fix pixels with 2 or less adjacent pixels.

    Args:
        surface (pygame.Surface): the pygame.Surface to anti-aliasing.
        background (3 element list or tuple): the color of the holes.

    Returns:
        anti-aliased pygame.Surface.
    """
    width, height = surface.get_size()
    array = pygame.surfarray.array3d(surface)
    contains_background = (array == background).all(axis=2)

    neighbours = (0, 1), (0, -1), (1, 0), (-1, 0)

    for row in range(1, height-1):
        for col in range(1, width-1):
            if contains_background[row, col]:
                average = np.zeros(shape=(1, 3), dtype=np.uint16)
                elements = 0
                for y, x in neighbours:
                    if not contains_background[row+y, col+x]:
                        elements += 1
                        average += array[row+y, col+x]
                if elements > 2:  # Only apply average if more than 2 neighbours is not of background color.
                    array[row, col] = average // elements

    return pygame.surfarray.make_surface(array)


def main():
    running = True
    image = pygame.image.load('test.png').convert()
    # image = circle
    pos = image.get_rect(center=(RADIUS, RADIUS))
    while running:

        clock.tick(FPS)

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    running = False
                elif event.key == pygame.K_1:
                    print('Reset circle.')
                    image = circle
                elif event.key == pygame.K_2:
                    print('Starting removing holes.')
                    time = pygame.time.get_ticks()
                    image = remove_holes(image, background=(255, 255, 255))
                    time = pygame.time.get_ticks() - time
                    print('Finished removing holes in {:.4E} s.'.format(time / 1000))

        screen.fill(background_color)
        screen.blit(image, pos)
        pygame.display.update()


if __name__ == '__main__':
    main()

Result

Before

After

Time

As I said before, it's not a very fast operation. Here are some benchmarks based on the circle in the example:

Surface size: (100, 100) | Time: 1.1521E-02 s
Surface size: (200, 200) | Time: 4.3365E-02 s
Surface size: (300, 300) | Time: 9.7489E-02 s
Surface size: (400, 400) | Time: 1.7257E-01 s
Surface size: (500, 500) | Time: 2.6911E-01 s
Surface size: (600, 600) | Time: 3.8759E-01 s
Surface size: (700, 700) | Time: 5.2999E-01 s
Surface size: (800, 800) | Time: 6.9134E-01 s
Surface size: (900, 900) | Time: 9.1454E-01 s

And with your image:

Time: 1.6557E-01 s