The Simple Case
If both images are opaque, then this exercise is easy:
g.drawImage(img1, 0, 0, null);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,t));
g.drawImage(img2, 0, 0, null);
This is probably how most slideshow transitions work. It's elegant and simple, but it fails for translucency:
Suppose img2 has large transparent areas where img1 is opaque: in this case we need to fade out img1, in addition to fading in img2:
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,1-t));
g.drawImage(img1, 0, 0, null);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,t));
g.drawImage(img2, 0, 0, null);
This may not give the desired visual effect, though. In areas where both img1 and img2 are opaque, at t = .5 the destination will have an opacity of only 75%.
Background
Why 75%? The short answer is: AlphaComposites are not designed to crossfade. (In fact, they were originally designed for Star Trek special effects, but that's another story.) The javadocs explain the formula for SRC_OVER composites is:
Ar = As + Ad*(1-As)
So in our case:
As = t
Ad = 1-t
Ar = t + (1-t)*(1-t)
Ar = t^2-2t+t+1
Ar = t^2-t+1
This is a parabola where Ar is 1 at t = 0 and t = 1, but at its minimum Ar is only .75. If you study the equations and how the composites work: it appears impossible to use AlphaComposites to crossfade two images so these requirements are met:
- The initial image is completely invisible at t = 1, and the final image is completely invisible at t = 0.
- If two pixels at the same (x,y) location have an alpha of k, then at all times during the crossfade the destination pixel should have an alpha of k.
Alternatives
So what is possible, then? I put together a test program that compares different approaches to this problem:
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,1-t*t));
g.drawImage(img1, 0, 0, null);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,1-(1-t)*(1-t)));
g.drawImage(img2, 0, 0, null);
.93 opacity isn't bad. However that assumes we're working with opaque pixels. If we work with pixels whose alpha values are .5, then we're layering two reasonably-opaque pixels on top of each other, and the resulting pixel is visibly more opaque than it should be (at As = Ad = .5, for example, they would combine to about Ar = .6.)
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,1-t));
g.drawImage(img1, 0, 0, null);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,t));
g.drawImage(img2, 0, 0, null);
float total = 1-t+t*t;
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_IN,
(float)(Math.pow(total,.2))));
g.drawImage(img1, 0, 0, null);
g.drawImage(img2, 0, 0, null);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,1-t));
g.drawImage(img1, 0, 0, null);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,t));
g.drawImage(img2, 0, 0, null);
This was found mainly through trial-and-error, but the basic concept is: at the end we apply the "simple composite" approach, but before that we prep the graphics destination with a ghosted copy of both images. This helps soften (or compensate for) the shortcomings of the simple approach.
As discussed below: this approach works with reasonable accuracy, but it's expensive to calculate.
Cr = Cd*(1-t)+Cs*tThis has a 0% error rate: because it's exactly what we're looking for. But it's also by my tests the most expensive approach.
Results
To compare each of these approaches, I measured the time they take to complete, the memory allocation, and the accuracy. Now "accuracy" can be measured several ways: I chose to measure how much opacity changed for specific pixels that should have a constant opacity. (If you're facing this problem you may need to rewrite the tests with a different definition of accuracy/error.)
Here is a graph showing the error of each approach:
The last two raster-based approaches have zero error, so they don't make an impression on the chart. At its worst the complicated composite approach has an error of about .045. In my experience this is hardly noticeable, especially compared with the .13 error the simplest approach shows.
But the performance of the most accurate approaches make them less desirable:
Each figure here represents the time it takes to make 100 calls to each method, dealing with images that are 200x200 pixels in size.
And the memory performance also favors the simplest methods:
The difference in the two raster-based approaches is: one crossfades 1 row of pixels at a time, and other approach crossfades all pixels in the entire image in one pass. As you can see, dealing with all the pixels at once saves a little time, but is much worse for memory.
Also: a word about memory. Usually when we optimize "performance" we worry only about speed, but memory allocation can be equally important. If developers are sloppy with memory, it has a direct impact on performance:
- Excessive allocations (and not properly letting go of objects) can result in paging memory to run your application. This can range from a "bad" user experience to "catastrophic", depending on what your program does.
- Also sloppy memory allocation means the garbage collector has to run more frequently. On slower computers this can result in visible "burps" while the program is unusable. True: you can configure the garbage collection to run at certain intervals or when the heap reaches certain levels to minimize this problem: but dealing with memory allocation responsibly is still the best practice.
Conclusions
Ideally it'd be great to see a
CrossfadeComposite class. This is effectively what the raster-based solution was, but my solution narrowly focused on specific types of images and ColorModels, and it would take some time/energy to extend it to a generic Composite class.But even then: performance probably isn't going to be what we'd like. (A custom composite in Java won't perform on par with AlphaComposites anyway: AlphaComposites are probably optimized for the platform's graphics pipelines.)
The best thing to do in the mean time is to avoid translucent images! Opaque images are so much simpler. In cases where translucency is unavoidable though: this page outlines a few different options. You can pick which one is "less evil".

(My apologies. My formatting didn't show up right in my last post, making the tables hard to read. Hopefully this one is better...)
ReplyDeleteAnother interesting one! I'd not done the math before to see that the alpha would be 0.75 at the midpoint, although I've got code that does that kind of crossfade animation, so I can see it now that I'm watching for it.
I sat down with Excel and looked for some alternatives. First, here is the alphas for your 1-t*t and 1-(1-t)*(1-t)
As Ad As + Ad*(1-As)
1.000 0.000 1.000
0.998 0.098 0.998
0.990 0.190 0.992
0.978 0.278 0.984
0.960 0.360 0.974
0.938 0.438 0.965
0.910 0.510 0.956
0.878 0.578 0.948
0.840 0.640 0.942
0.798 0.698 0.939
0.750 0.750 0.938
0.698 0.798 0.939
0.640 0.840 0.942
0.578 0.878 0.948
0.510 0.910 0.956
0.438 0.938 0.965
0.360 0.960 0.974
0.278 0.978 0.984
0.190 0.990 0.992
0.097 0.998 0.998
0.000 1.000 1.000
I computed that for t going from 0 to 1 by 0.05. Notice how the alpha starts off slowly falling, and as you pointed out we have the max error of about 6% at the half way point.
Next I tried 1-t*t*t and 1-(1-t)*(1-t)*(1-t)
As Ad As + Ad*(1-As)
1.000 0.000 1.000
1.000 0.143 1.000
0.999 0.271 0.999
0.997 0.386 0.998
0.992 0.488 0.996
0.984 0.578 0.993
0.973 0.657 0.991
0.957 0.725 0.988
0.936 0.784 0.986
0.909 0.834 0.985
0.875 0.875 0.984
0.834 0.909 0.985
0.784 0.936 0.986
0.725 0.957 0.988
0.657 0.973 0.991
0.578 0.984 0.993
0.488 0.992 0.996
0.386 0.997 0.998
0.271 0.999 0.999
0.143 1.000 1.000
0.000 1.000 1.000
Here we have even less error and you can see the slow start is even more pronounced, which probably has a very nice acceleration/deceleration look to it, even though I'm just walking values for t from 0 to 1 linearly in 0.05 increments.
I then thought, why not solve: As + Ad * (1-As) = 1 for Ad, giving the very revealing Ad = 1. The upshot is that we should be able to get good linear crossfades using the standard AlphaComposite compositor by changing the order of compositing to something like:
if (t <= 0.5) {
g.drawImage(img1, 0, 0, null);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,t));
g.drawImage(img2, 0, 0, null);
} else {
g.drawImage(img2, 0, 0, null);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,1-t));
g.drawImage(img1, 0, 0, null);
}
BTW: thanks for bringing up this topic! I'd not done the math before, and I can see how this will help the look of my animations.
I re-read your post and found the telling line of:
ReplyDelete"Suppose img2 has large transparent areas where img1 is opaque: in this case we need to fade out img1, in addition to fading in img2:"
Hmmm, my solution doesn't work well for that, now does it. Interesting problem.
Sorry for the delay in replying. :) I'm glad to see someone else interested in the subject.
ReplyDeleteIt's possible, as you suggested, to continually tweak graphs until the mathematical function is close to a solid line. However this isn't necessarily solving the problem: it's just clever math. (Don't get me wrong: I love clever math.)
But the closer we get a line, the more we're probably cheating. Maybe one image will fade in at a really accelerated pace, or -- as you pointed out -- you'll have something that makes algebraic sense but doesn't actually crossfade.
I'm pretty convinced this is impossible with AlphaComposites, but I'd be happy to have someone prove me wrong. Also I'd be happy to have someone help me write my own custom Composite classes and see how they perform, but I'm a little out of my league in that area.
i done a lot of work on alpha compositing for a project a few months back.
ReplyDeleteheck , i spent weeks optimising it !
i done all that u had done,including using floating point math,fixed point math etc, as well as using the standard alpha composites but i had many problems - speed and memory.
in the end i wrote a small win32 assembler program that would do the blend in a buffer (about 20x faster) using MMX.
its not the perfect platform independant solution but it worked fine for me!
I haven't tried this, but it just occurred to me as the first thing I'd check out to gain some speed compared to raw pixling: What about using two intermediate opaque pictures, both initialized to the background [image], onto where you draw each one of the translucent images. Then you can use the simple opaque approach when crossfading between these two now opaque images?
ReplyDeleteEndre,
ReplyDeleteThat sounds like it should work to me. Except in cases where you don't have access to your destination. (That is: suppose you're drawing abstractly to Graphics2D object and don't have access to the image/buffer you're painting to.)
And... sigh. It's a lot of image transfers back and forth for such a simple effect. But if that's what it takes to get the effect right: you may be on to something. :)
import java.awt.Color;
ReplyDelete//import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
//import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
class watermark2
{
public static void main(String args[])
{
BufferedImage image1= null;
BufferedImage image2= null;
File file = new File("./" + "captcha" + ".jpeg");
BufferedImage captcha= null;
image1 = ImageIO.read(new File("bug2.gif"));
captcha = ImageIO.read(new
File("pass.jpeg"));
//BufferedImage image = ImageIO.read(inputFile);
Graphics2D g = image1.createGraphics();
try {
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_
OVER, 0.5f));// 50% transp <<--- error here
g.drawImage(captcha, image1.getWidth() - captcha.getWidth(),
image1.getHeight() - captcha.getHeight(), null);// draw in
lower right corner
}
finally {
g.dispose();
}
ImageIO.write(image1,"jpeg",file);
}
}
can you help me to find the error in this code now..