Pygame and blitting: white on white = gray?
Asked Answered
R

2

5

I'm using pygame (1.9.0rc3, though this also happens in 1.8.1) to create a heatmap. To build the heatmap, I use a small, 24-bit 11x11px dot PNG image with a white background and a very low-opacity grey dot that stops exactly at the edges:

Dot image http://img442.imageshack.us/img442/465/dot.png

The area around the dot is perfect white, #ffffff, as it should be. However, when I use pygame to blit the image multiple times to a new surface using BLEND_MULT, a grey square appears, as though the dot background wasn't perfect white, which doesn't make sense.

The following code, plus included images, can reproduce this:

import os
import numpy
import pygame

os.environ['SDL_VIDEODRIVER'] = 'dummy'
pygame.display.init()
pygame.display.set_mode((1,1), 0, 32)

dot_image = pygame.image.load('dot.png').convert_alpha()

surf = pygame.Surface((100, 100), 0, 32)
surf.fill((255, 255, 255))
surf = surf.convert_alpha()

for i in range(50):
    surf.blit(dot_image, (20, 40), None, pygame.BLEND_MULT)    

for i in range(100):
    surf.blit(dot_image, (60, 40), None, pygame.BLEND_MULT)      

pygame.image.save(surf, 'result.png')

When you run the code, you will get the following image:

Resulting image after blending http://img263.imageshack.us/img263/4568/result.png

Is there a reason this happens? How can I work around it?

Rosenberger answered 21/7, 2009 at 5:2 Comment(0)
D
6

After trying around, the only thing I could see was that you're 100% right. Multiplication by 255 results in a subtraction of 1 -- every time. In the end, I downloaded the pygame source code, and the answer is right there, in surface.h:

#define BLEND_MULT(sR, sG, sB, sA, dR, dG, dB, dA) \
    dR = (dR && sR) ? (dR * sR) >> 8 : 0;          \
    dG = (dG && sG) ? (dG * sG) >> 8 : 0;          \
    dB = (dB && sB) ? (dB * sB) >> 8 : 0;

Pygame implements multiply blending as

new_val = old_dest * old_source / 256

and not, which would be the correct way, as

new_val = old_dest * old_source / 255

This is probably done for optimization purposes -- a bit shift is a lot faster than a division. As the ratio 255 / 256 is very close to one, the only difference this makes is an "off by one": The value you get is the expected value minus one -- except if you expected zero, in which case the result is correct.

So, you have these possibilities:

  1. Ignore it, because the off-by-one doesn't matter for most purposes.
  2. Add 1 to all result values. Closest to the expected result, except you lose the zero.
  3. If overall correctness is not important, but you need 255 * 255 == 255 (you know what I mean), ORing 1 instead of adding suffices, and is faster.

Note that if you don't choose answer 1, for performance reasons you'll probably have to write a C extension instead of using Python directly.

Disputable answered 21/7, 2009 at 7:37 Comment(4)
Not just two minutes ago did I realize the same thing (that it's "off by one"), but I couldn't find where in the pygame source the calculation was located. Since this error makes a huge difference when multiplying 255 * 255 (I do need it for the heatmaps), how do I go about implementing option #3? I'm not at all familiar with C, so if you could point me in the right direction with implementation, you'd probably save my sanity (or what's left, at this point).Rosenberger
The best compromise between performance and simplicity would probably be implementing #2 by ADD-bliting an all-(1,1,1) surface onto the result. However, if you want to dig into C extensions, you might start by looking at cython, which is a C-Python-Hybrid: cython.org . Note that I myself don't have much experience in that area.Disputable
Also, if this is feasable in your case, you might just change and recompile pygame itself. However, that would make distributing your program harder, as you would end up with a non-standard version of pygame.Disputable
I just added 1 to the results in surface.h and recompiled myself -- works great! Since I won't be distributing the application, it shouldn't be a problem. Thanks againRosenberger
P
1

Also encountered this problem doing heatmaps and after reading balpha's answer, chose to fix it the "right" (if slower) way. Change the various

(s * d) >> 8

to

(s * d) / 255 

This required patching multiply functions in alphablit.c (though I patched surface.h as well). Not sure how much this impacts performance, but for the specific (heatmap) application, it produces much prettier images.

Palestra answered 10/11, 2009 at 19:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.