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()
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