Search code examples
pythonimageoptimizationpygamepygame-surface

How can i optimize the code of inversion mask in Pygame


And so, I wrote a test function to invert an image in a specific area and shape with the ability to change the location and size. The only problem is that the larger the size of the mask, the slower the mask moves or enlarges. Are there any solutions to speed up the code?

import pygame

fps = pygame.time.Clock()
pygame.init()
pygame.event.set_allowed([pygame.QUIT])

# declare all necessary vars
image = pygame.image.load('...\\image.png')
org_mask_inversion = pygame.image.load('...\\mask_inversion.png')
ix, iy = 0, 0
size_x, size_y = 100, 100
current_ix, current_iy = ix, iy
current_size_x, current_size_y = size_x, size_y
screen = pygame.display.set_mode([960, 720])
mask_inversion = pygame.transform.scale(org_mask_inversion, (size_x, size_y))
image2 = image


# function that inverts color values
def inversion_of_color(r, g, b):
    return [255 - r, 255 - g, 255 - b]


# image inversion function
def inversion_of_surface(surface, sx, sy):
    # it creates a new surface of the same size as the one specified in the function
    inverted_surface = pygame.surface.Surface(mask_inversion.get_size(), pygame.SRCALPHA)

    # passes over the entire size of the mask
    for x in range(mask_inversion.get_width()):
        for y in range(mask_inversion.get_height()):
            # takes the color of the mask pixel
            m_color = mask_inversion.get_at((x, y))
            # checks if the pixel is green color
            if m_color.g == 255:
                # checks if the mask extends beyond the screen area
                if 0 <= sx + x <= screen.get_width() - 1 and 0 <= sy + y <= screen.get_height() - 1:
                    # takes the color of a surface pixel
                    color = surface.get_at((x + sx, y + sy))
                    # inverts the pixel color and inserts the inverted pixel into the created surface
                    inverted_color = inversion_of_color(color.r, color.g, color.b)
                    inverted_surface.set_at((x, y), inverted_color)

    return inverted_surface


running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    fps.tick(60)
    keys = pygame.key.get_pressed()
    pygame.display.set_caption(f'Inverted Image {round(fps.get_fps())} x:{ix} y:{iy}')

    if keys[pygame.K_LEFT]:
        ix -= 6
    elif keys[pygame.K_RIGHT]:
        ix += 6
    if keys[pygame.K_UP]:
        iy -= 6
    elif keys[pygame.K_DOWN]:
        iy += 6

    if keys[pygame.K_a]:
        size_x -= 6
        size_y -= 6
        mask_inversion = pygame.transform.scale(org_mask_inversion, (size_x, size_y))
    elif keys[pygame.K_d]:
        size_x += 6
        size_y += 6
        mask_inversion = pygame.transform.scale(org_mask_inversion, (size_x, size_y))

    # checks if the size or location of the mask has changed
    if current_ix != ix or current_iy != iy or current_size_x != size_y or current_size_x != size_x:
        image2 = inversion_of_surface(image, ix, iy)
        current_ix, current_iy = ix, iy
        current_size_x, current_size_y = size_x, size_y

    screen.blit(image, (0, 0))
    screen.blit(image2, (ix, iy))
    pygame.display.update()

pygame.quit()

Here's the image of the mask itself enter image description here

I tried different ways to cache the result, I tried to use numpy arrays, and tried to check the already changed pixels, but all to no avail.


Solution

  • You can achieve what you want with Pygame functions alone and without for loops:

    mask = pygame.mask.from_surface(inversionMaskImage)
    inversionMask = mask.to_surface(setcolor=(255, 255, 255, 255), unsetcolor=(0, 0, 0, 0))
    
    subSurface = surface.subsurface(pygame.Rect((sx, sy), mask.get_size()))
    
    • Create a copy of the mask and blend the inverted surface with the mask using the BLEND_SUB mode (see pygame.Surface.blit)
    finalImage = mask.copy()
    finalImage.blit(invertedArea, (0, 0), special_flags = pygame.BLEND_MULT)
    

    See also Blending and transparency and Clipping


    Minimal example

    import pygame
    
    pygame.init()
    screen = pygame.display.set_mode((1024, 683))
    clock = pygame.time.Clock()
    
    image = pygame.image.load('image/parrot1.png').convert_alpha()
    
    inversionMaskImage = pygame.Surface((200, 200), pygame.SRCALPHA)
    pygame.draw.circle(inversionMaskImage, (255, 255, 255), inversionMaskImage.get_rect().center, inversionMaskImage.get_width()//2)
    mask = pygame.mask.from_surface(inversionMaskImage)
    inversionMask = mask.to_surface(setcolor=(255, 255, 255, 255), unsetcolor=(0, 0, 0, 0))
    
    def invert_surface(surface, mask, sx, sy):
        areaRect = pygame.Rect((sx, sy), mask.get_size())
        clipRect = areaRect.clip(surface.get_rect())
        subSurface = surface.subsurface(clipRect)
        finalImage = mask.copy()
        finalImage.blit(subSurface, (clipRect.x - areaRect.x, clipRect.y - areaRect.y), special_flags = pygame.BLEND_SUB)
        return finalImage
    
    run = True
    while run:
        clock.tick(100)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False
    
        areaRect = pygame.Rect(pygame.mouse.get_pos(), (0, 0)).inflate(inversionMask.get_size())
        invertedArea = invert_surface(image, inversionMask, areaRect.x, areaRect.y)
    
        screen.fill('black')
        screen.blit(image, (0, 0))
        screen.blit(invertedArea, areaRect)
        pygame.display.flip()
    
    pygame.quit()
    exit()