mathman opened this issue on Mar 09, 2008 · 38 posts
bagginsbill posted Mon, 17 March 2008 at 10:17 AM
OK I've totally got this figured out. First, you have to read this from the specification of the PNG file format:
PNG uses "unassociated" or "non-premultiplied" alpha so that images with separate transparency masks can be stored losslessly. Another common technique, "premultiplied alpha", stores pixel values premultiplied by the alpha fraction; in effect, the image is already composited against a black background. Any image data hidden by the transparency mask is irretrievably lost by that method, since multiplying by a zero alpha value always produces zero.
Some image rendering techniques generate images with premultiplied alpha (the alpha value actually represents how much of the pixel is covered by the image). This representation can be converted to PNG by dividing the sample values by alpha, except where alpha is zero. The result will look good if displayed by a viewer that handles alpha properly, but will not look very good if the viewer ignores the alpha channel.
**Although each form of alpha storage has its advantages, we did not want to require all PNG viewers to handle both forms. We standardized on non-premultiplied alpha as being the lossless and more general case.
**Now, guess what Poser writes into our PNG files? In direct violation of the specification, it writes PREMULTIPLIED colors. As a result, nothing you do with the image is going to composite correctly. Instead of writing the true color of fringe pixels (pixels partially filled by 3D objects) with the color of the object, it multiplies the color with the alpha value.
A little background:
The blending formula for compositing images with alpha blending is simple. Given F is our foreground color (of the object being rendered), and B is the background color (that we're rendering against), and A is the alpha value (corresponding to the fraction of the pixel filled by the rendered 3D object) the formula for the final pixel is:
(1 - A) * B + A * F
Consider a pixel that is three-quarter filled (A=.75) by an orange object (F=RGB(255,128,64)) on a WHITE background (B = RGB(255,255,255)). The final color will be:
0.25 * RGB(255, 255, 255) + 0.75 * RGB(255, 128, 64) = RGB(255, 160, 112)
So if we render with white, what gets written into this fringe pixel for color is RGB(255, 160, 112). If we then composite this in post, the value of F from our rendered file is wrong. This fringe pixel will have a gray ghost effect to it. We don't want fringe pixels to have any information about the background at the time of rendering.
To combat this, we should (as I've said before) render over black. The formula then is:
0.25 * RGB(0, 0, 0) + 0.75 * RGB(255, 128, 64) = RGB(191, 96, 48)
So that is what Poser puts in the file - RGB(191, 96, 48) with an Alpha =.75. This is called a pre-multiplied value. Some image formats support this. But according to the spec, PNG does NOT allow this. And yet, that is what Poser is doing.
Poser should not be writing the orange color factored with the alpha. It should just write the orange color directly as RGB(255, 128, 64).
However, all is not lost. With a little work, the original true 3D color can be recovered, but only if you render over black.
When blending with black, the first term in the blending function is 0, so all that is being written into the file is:
A * F
If we take that and divide by A:
A * F / A
We're left with:
F
Which is what we really want.
So, using the Python Imaging Library, I wrote a little script to test this.
The script is:
import Image<br></br>
def fixpng(infile, outfile):<br></br>
def fixpixel(p):<br></br>
r,g,b,a = p<br></br>
if a:<br></br>
return (r * 255 / a, g * 255 / a, b * 255 / a,
a)<br></br>
return p<br></br>
im = Image.open(infile)<br></br>
im.putdata([fixpixel(x) for x in im.getdata()])<br></br>
im.save(outfile)<br></br><br></br>
This script successfully recovers the original true foreground
colors. The modified image can then be used just fine. I'll show
you what happened in another post.
Renderosity forum reply notifications are wonky. If I read a follow-up in a thread, but I don't myself reply, then notifications no longer happen AT ALL on that thread. So if I seem to be ignoring a question, that's why. (Updated September 23, 2019)