Remove border from opencv generated ellipse in pygame

Question:

I’ve set up this window where I blit an ellipse to the screen. I would like to have the ellipse fully white with a smooth border. It looks acceptable, but when I add the ellipse to a white background, the border of the ellipse shows up.

import pygame
import cv2
import numpy as np

pygame.init()

# Set up the Pygame window
screen_width = 640
screen_height = 480
screen = pygame.display.set_mode((screen_width, screen_height))

def drawAACircle(surf, color, center, radius, width, angle):
    circle_image = np.zeros((radius*2, radius*2, 4), dtype = np.uint8)
    circle_image = cv2.ellipse(circle_image, (radius, radius), (radius-width, radius-width), (angle*-.5)-90 , 0, angle, (*color, 255), width, lineType=cv2.LINE_AA)  
    #draw it on the surface
    surf.blit(pygame.image.frombuffer(circle_image.tobytes(), circle_image.shape[1::-1], "RGBA").convert_alpha(), (center[0]-radius, center[1]-radius))

# Wait for the user to close the window
while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            quit()
    
    screen.fill((255,255,255))

    drawAACircle(screen, (255,255,255), (screen_width/2,screen_height/2), 200, 20, 360)

    # Update the display
    pygame.display.flip()

I noticed the border when I changed the background to fully white:

black and white ellipse

I’m currently developing a decal tool where I can stack multiple ellipses on top of each other. I refresh the background screen with a copy() like this:

def takeSnapshot():
    global snapshot
    snapshot = screen.copy()

def clearSnapshot():
    global snapshot
    snapshot = None

----
# Run the main loop
running = True
while running:
    events = pygame.event.get()
    for event in events:
        if event.type == pygame.QUIT:
            running = False      

    
    # Clear the screen
    if snapshot is None:
        screen.fill((0,0,0))
         
    else:
        screen.blit(snapshot, (0,0))

I’ve tried gfx.aapolygon icm with a filled polygon. But I just can’t get it as crisp and clean as the opencv ellipse.

If someone knows an alternative I’ll be happy to hear, or I might be overlooking something where I could just get the opencv ellipse fully white.

EDIT: just to make it clear, I’ve chosen the opencv ellipse for the thickness option, be able to make an arc shape, and the smooth looks.

Asked By: Stechouse

||

Answers:

You do have an alpha channel… However, OpenCV is dumb, doesn’t know what "alpha" is. It simply blends the commanded values of each channel with what’s there already (black!), and what you’re getting is effectively premultiplied alpha.

You need to tell pygame about this, and hope that it knows how to deal with that.

If that’s not possible, you’ll have to un-premultiply, i.e. divide, your RGB channels by the A channel. Then those border pixels are actually white, instead of already being blended with black, and the blending will work properly, assuming you can blend with "straight alpha" semantics.

Answered By: Christoph Rackwitz

The problem is that antialiasing is applied not only to the alpha channel, but also to the color channels (RGB channels). Basically, this means that the color channels are already multiplied by the alpha channel. You have to use the mode "BLEND_PREMULTIPLIED" to blend this texture correctly (see blit):

def drawAACircle(surf, color, center, radius, width, angle):
    circle_image = np.zeros((radius*2, radius*2, 4), dtype = np.uint8)
    circle_image = cv2.ellipse(circle_image, (radius, radius), (radius-width, radius-width), (angle*-.5)-90 , 0, angle, (*color, 255), width, lineType=cv2.LINE_AA)  
    circle_surf = pygame.image.frombuffer(circle_image.tobytes(), circle_image.shape[1::-1], "RGBA")
    pos = (center[0]-radius, center[1]-radius)
    
    surf.blit(circle_surf, pos, special_flags=pygame.BLEND_PREMULTIPLIED)

(I’m not showing the result here because it’s just all white.)

Answered By: Rabbid76