[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[pygame] How pygame's custom 32->32 alpha bit does compositing poorly



I think it's great that pygame has it's own special blitter for 32->32
with alpha that does something closer to compositing than what SDL can
do, cause I think compositing is exactly what everybody expects to
happen in that case (namely that blitting b over a dest, then blititng
a over it afterwards is equivalent to blitting a over b in a temp, and
then blitting the temp to a dest).

I ragged on the implementation of that feature in an earlier email,
and I now think that was rude of me, sorry - the right thing would
have been to demonstrate how I think it could be improved. So attached
is a script that does a few things:
1. it has pygame source for math that does compositing well
2. it prints out statistics on how the included compositing math
compares to pygame's blit with respect to the case I mentioned above
(sum of square error and also the max amount the colors are off)
3. it blits to a window the 3 cases (blitting the 2 surfaces seperate,
blitting a composite surface built with the included math, and
blitting a composite surface built with the pygame blit) so people can
see how the error manifests itself (notice the color hue and
saturation being off in the vertical middle of the gradients in the
bottom set, where pygame did the compositing)

I'm also attaching a unit test for pygame's 32->32 alpha blit, with
respect to compositing.

My goal is to give enough for info for somebody to help them be able
to improve the blend in alphablit.c and confirm it works, for the next
release (I haven't taken the time to get set up to build from source
on any of my machines yet, so I couldn't test a patch)
import pygame
import math

def slowButAccurateCompositeAlphaBlit(src, dest):
    for x in xrange(src.get_width()):
        for y in xrange(src.get_height()):
            sR, sG, sB, sA = src.get_at((x,y))
            if sA > 0:
                dR, dG, dB, dA = dest.get_at((x,y))
                finalA = sA + dA - (sA*dA)/255
                destContrib = dA*(255 - sA)/255
                finalR = (sR*sA + dR*destContrib)/finalA
                finalG = (sG*sA + dG*destContrib)/finalA
                finalB = (sB*sA + dB*destContrib)/finalA
                dest.set_at((x,y), (finalR, finalG, finalB, finalA))

def GetSurfaceColorDiffMaxAndSumSquaredError(surf_a, surf_b):
    sum_squared = 0
    color_diff_max = [0,0,0,0]
    for y in xrange(surf_a.get_height()):
        for x in xrange(surf_a.get_width()):
            color_a = surf_a.get_at((x,y))
            color_b = surf_b.get_at((x,y))
            for i in xrange(4):
                diff = abs(color_a[i] - color_b[i])
                sum_squared += diff*diff
                color_diff_max[i] = max(color_diff_max[i], diff)
    return color_diff_max, sum_squared
    
size = (289,52)

src_a = pygame.surface.Surface(size, pygame.SRCALPHA, 32)
src_b = pygame.surface.Surface(size, pygame.SRCALPHA, 32)
for color_index in xrange(289):
    for alpha_index in xrange(52):
        color_1_value = min(255, (color_index / 17)*16)
        color_2_value = min(255, (color_index % 17)*16)
        alpha_value = alpha_index*5
        pos = (color_index, alpha_index)
        color_a = (color_1_value, color_2_value, 0, alpha_value)
        src_a.set_at(pos, color_a)
        color_b = (0, color_1_value, color_2_value, alpha_value)
        src_b.set_at(pos, color_b)

# see what you get when blitting b, then blitting a
blit_in_turn_dest = pygame.surface.Surface(size, 0, 24)
blit_in_turn_dest.blit(src_b, (0,0))
blit_in_turn_dest.blit(src_a, (0,0))

# see what you get when making a composite of a over b with pygame
composite_dest = pygame.surface.Surface(size, 0, 24)
composite_surface = pygame.surface.Surface(size, pygame.SRCALPHA, 32)
composite_surface.blit(src_b, (0,0))
slowButAccurateCompositeAlphaBlit(src_a, composite_surface)
composite_dest.blit(composite_surface, (0,0))

# see what you get when making a composite of a over b with pygame
pygame_composite_dest = pygame.surface.Surface(size, 0, 24)
pygame_composite_surface = pygame.surface.Surface(size, pygame.SRCALPHA, 32)
pygame_composite_surface.blit(src_b, (0,0))
pygame_composite_surface.blit(src_a, (0,0))
pygame_composite_dest.blit(pygame_composite_surface, (0,0))

# print the results of comparing the surfaces against the reference
max_diff, err_sum = GetSurfaceColorDiffMaxAndSumSquaredError(blit_in_turn_dest, composite_dest)
pygame_max_diff, pygame_err_sum = GetSurfaceColorDiffMaxAndSumSquaredError(blit_in_turn_dest, pygame_composite_dest)

print "sample composite error: ", err_sum
print "pygame composite error: ", pygame_err_sum
print "sample max error: ", max_diff
print "pygame max error: ", pygame_max_diff

####################################################################
pygame.display.init()
screen = pygame.display.set_mode((size[0],size[1]*3))

screen.blit(blit_in_turn_dest,(0,0))
screen.blit(composite_dest,(0,size[1]))
screen.blit(pygame_composite_dest,(0,size[1]*2))
pygame.display.update()

while 1:
    for event in pygame.event.get():
            if event.type == pygame.KEYDOWN:
                if event.key == 27:
                    raise SystemExit
    pygame.display.update()

import pygame
import unittest

class UnitTestAlphaBlitCompositing(unittest.TestCase):
    def assertMaxSurfaceDifference(self, surf_a, surf_b, max_diff):
        self.assertEqual(surf_a.get_width(), surf_b.get_width())
        self.assertEqual(surf_a.get_height(), surf_b.get_height())
        for y in xrange(surf_a.get_height()):
            for x in xrange(surf_a.get_width()):
                color_a = surf_a.get_at((x,y))
                color_b = surf_b.get_at((x,y))
                for i in xrange(4):
                    diff = abs(color_a[i] - color_b[i])
                    if (diff > max_diff):
                        print "max diff exceeded at:", x, y, "with", color_a, "vs.", color_b
                    self.assert_(diff <= max_diff)
    
    def testCompositing(self):
        size = (81,18)
        # first we get 2 source images, that have a variety of alpha values
        # over a range of color values that have different sets
        # intersecting color values and unique color values,
        # making sure to also hit the extremes (0 & 255)
        src_a = pygame.surface.Surface(size, pygame.SRCALPHA, 32)
        src_b = pygame.surface.Surface(size, pygame.SRCALPHA, 32)
        for color_index in xrange(81):
            for alpha_index in xrange(18):
                color_1_value = min(255, (color_index / 9)*32)
                color_2_value = min(255, (color_index % 9)*32)
                alpha_value = alpha_index*15
                pos = (color_index, alpha_index)
                color_a = (color_1_value, color_2_value, 0, alpha_value)
                src_a.set_at(pos, color_a)
                color_b = (0, color_1_value, color_2_value, alpha_value)
                src_b.set_at(pos, color_b)

        # see what you get when blitting b, then blitting a
        blit_in_turn_dest = pygame.surface.Surface(size, 0, 24)
        blit_in_turn_dest.blit(src_b, (0,0))
        blit_in_turn_dest.blit(src_a, (0,0))
        
        # see what you get when making a composite of b over a
        # and then blit that
        composite_blit_dest = pygame.surface.Surface(size, 0, 24)
        composite_surface = pygame.surface.Surface(size, pygame.SRCALPHA, 32)
        composite_surface.blit(src_b, (0,0))
        self.assertMaxSurfaceDifference(composite_surface, src_b, 0)
        composite_surface.blit(src_a, (0,0))
        #self.slowButAccurateCompositeAlphaBlit(src_a, composite_surface)
        composite_blit_dest.blit(composite_surface, (0,0))

        # finally we compare the surfaces
        # the composite cannot be perfect due to rounding, so we test within
        # some limit human eyes probably can't see much
        self.assertMaxSurfaceDifference(composite_blit_dest, blit_in_turn_dest, 4)

unittest.main()