Skip to main content

Is there a better way to blend colors when painting with Graphics2D?

2 replies [Last post]
kshetline
Offline
Joined: 2010-12-20
Points: 0

I've recently been using Java to produce some simple anaglyph 3-D images -- "anaglyph" being the type of 3-D where you paint images for the left eye in one color (typically red) and for the right eye in another color (typically cyan) so, if you put on a pair of goofy looking 3-D glasses and look at the combined results, a stereoscopic 3-D image appears.
I currently have a working example at http://www.skyviewcafe.com/skyview.php (in the Orbits tab of the applet), but I'm not satisfied with the rendering speed. In order to blend the left eye and right eye images I ended up painting to two different offscreen BufferedImages, then looping through those images to blend pixels one pixel at a time.

  @Override
  <span class="keyword">protected</span> <span class="keyword">void</span> paintComponent(Graphics2D g, <span class="keyword">int</span> width, <span class="keyword">int</span> height, <span class="keyword">boolean</span> printing)
  {
    <span class="keyword">if</span> (anaglyph3D.isSelected()) {
      <span class="keyword">if</span> (leftImage == <span class="keyword">null</span> || leftImage.getWidth() != width || leftImage.getHeight() != height) {
        leftImage  = <span class="keyword">new</span> BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
        rightImage = <span class="keyword">new</span> BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
      }

      paintComponent(LEFT_EYE,  (Graphics2D)  leftImage.getGraphics(), width, height, printing);
      paintComponent(RIGHT_EYE, (Graphics2D) rightImage.getGraphics(), width, height, printing);

      <span class="keyword">for</span> (<span class="keyword">int</span> y = 0; y &lt; height; ++y) {
        <span class="keyword">for</span> (<span class="keyword">int</span> x = 0; x &lt; width; ++x) {
          <span class="keyword">int</span>   leftPixel = leftImage.getRGB(x, y);
          <span class="keyword">int</span>   rightPixel = rightImage.getRGB(x, y);

          leftImage.setRGB(x, y, leftPixel | rightPixel);
        }
      }

      g.drawImage(leftImage, 0, 0, <span class="keyword">null</span>);
    }
    <span class="keyword">else</span>
      paintComponent(FULL_COLOR, g, width, height, printing);
  }

I'd much, MUCH rather simply paint one color on top of the other to the original Graphics2D target, but I haven't found a way to make the colors blend as they should where the colors overlap.
Painting is being done on a black background. If I paint a red line, then paint a cyan line crossing the red line, the intersection of the two lines should appear white. As I am drawing antialiased lines, pixels where lines overlap need to come out in a variety of mixes of red and cyan as well.
If I weren't trying to antialias, if all I needed as an end result was a mix of pure black, white, red, and cyan pixels, using an AlphaComposite might have done the trick. I thought about painting with semi-transparent colors by specifying alpha components for the colors I'm painting with, but that results in a weighted-average blending of color components -- lines drawn across a black background, for example, would end up darker than they should be.
I tried creating my own java.awt.CompositeContext implementation, but every single drawing operation called the compose() method with larger rasters in need of processing, with no way I could see to easily and quickly deal with the few pixels actually being drawn in need of blending.
What I really need is a painting mode that does color mixing on RGB components individually such that:
DR = max(DR, SR)
DG = max(DG, SG)
DB = max(DB, SB)
...where D is the destination pixel and S is the source pixel being drawn over the existing D pixel.
While not needed for my current project, other useful painting modes would be an additive mode (draw a dark blue pixel on top of another dark blue pixel, and the result is a brighter blue pixel -- with 8-bit components, adding source and destination values, limiting the result to a maximum value of 255), a minimum mode (lowest component value has precedence -- this would be more like painting with pigments), and a subtractive mode (Dx = 255 - min((255 - Dx) + (255 - Sx), 255)) -- even more like pigments, where succesive painting over one spot makes it darker. Averaging components would be a useful mode too (although this is equivalent to painting in the normal paining mode with 50% alpha).
I can think of other useful painting modes (such as HSB-based blending rules), and all the complications alpha channels add to the weighting of component values, but even the above five painting modes would add a great deal of intelligence and flexibility to the kind of image rendering you can do with Graphics2D. If I were proposing an update for the API, it might look something like this:
public void setPaintMode(PaintMode mode)
...where mode was one of the following values: REPLACE_PIXELS (what setPaintMode() already does by default), BLEND_MAXIMUM, BLEND_ADDITIVE, BLEND_MINIMUM, BLEND_SUBTRACTIVE, and BLEND_AVERAGE.

Edit: This is my first time posting here. What happended to all of the beautiful text formatting that appeared nicely in the rich text editor!? My code intendation, my use of subscripts... all gone!

Reply viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
walterln
Offline
Joined: 2007-04-17
Points: 0

Yes, the forum is bad :(. You might get a better response at Java 2D ( http://forums.oracle.com/forums/forum.jspa?forumID=938 ).
I think the SwingX BlendComposite is what you're looking for (but I have no idea if it's faster):
import java.awt.*;

import javax.swing.JFrame;
import javax.swing.JPanel;

import org.jdesktop.swingx.graphics.BlendComposite;

public class TestBlendComposite extends JPanel {
public TestBlendComposite() {
setBackground(Color.BLACK);
setPreferredSize(new Dimension(400, 400));
}

@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);

int part = getWidth() / 4;
Graphics2D gg = (Graphics2D) g.create();
gg.setColor(Color.CYAN);
gg.fillOval(0, 0, part * 3, getHeight());
gg.setColor(Color.RED);
gg.setComposite(BlendComposite.Add);
gg.fillOval(part, 0, part * 3, getHeight());
gg.dispose();
}

public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.getContentPane().add(new TestBlendComposite());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
}

kshetline
Offline
Joined: 2010-12-20
Points: 0

I found one easy way to almost double my frame rate: Using BufferedImage.TYPE_INT_RGB instead of BufferedImage.TYPE_3BYTE_BGR.

I didn't think about how the different kinds of BufferedImages were constructed behind the scenes when I first wrote my code, but if I'd thought about how this loop works:

      for (int y = 0; y &amp;lt; height; ++y) {<br />
        for (int x = 0; x &amp;lt; width; ++x)<br />
          leftImage.setRGB(x, y, leftImage.getRGB(x, y) | rightImage.getRGB(x, y));<br />
      }

...it might have occurred to me that TYPE_3BYTE_BGR means separate byte arrays for each RGB component, and that accessing those components as ints means a lot of byte shifting and shuffling. Not only does my loop to merge the two BufferedImages run faster with TYPE_INT_RGB, the drawing to the BufferedImages before the merge step is faster too. The Graphics2D drawing routines are probably better optimized for the TYPE_INT_RGB data format.

Once I was thinking more about how the BufferedImages are constructed, I tried an experiment with creating a buffered image that would put all of the bytes for red, green, and blue components into separate byte arrays for each color component. If that worked, the thought was I could merge the images using System.arraycopy(...) to quickly copy all of the red bytes from the left eye image into the right eye image, a technique I hoped would merge the two images much faster than nested x/y for loops reading and writing lots of individual pixel values.

  protected static final String RED_BYTES   = &amp;quot;red.bytes&amp;quot;;<br />
  protected static final String GREEN_BYTES = &amp;quot;green.bytes&amp;quot;;<br />
  protected static final String BLUE_BYTES  = &amp;quot;blue.bytes&amp;quot;;<br />
  {<br />
  protected static BufferedImage getAnaglyphBufferedImage(int width, int height)<br />
  {<br />
    ColorModel      cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] {8, 8, 8},<br />
                      false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);<br />
    int             wordWidth = ((width + 3) / 4) * 4;<br />
    int             size = wordWidth * height;<br />
    final byte[][]  data = new byte[3][size];<br />
    DataBuffer      buffer = new DataBufferByte(data, size);<br />
    WritableRaster  raster =  Raster.createBandedRaster(buffer, width, height, wordWidth,<br />
                      new int[] {0, 1, 2}, new int[] {0, 0, 0}, new Point(0, 0));<br />
<br />
    return new BufferedImage(cm, raster, false, null)<br />
      {<br />
        @Override<br />
        public Object getProperty(String name, ImageObserver observer)<br />
        {<br />
          if (RED_BYTES.equals(name))<br />
            return data[0];<br />
          else if (GREEN_BYTES.equals(name))<br />
            return data[1];<br />
          else if (BLUE_BYTES.equals(name))<br />
            return data[2];<br />
          else<br />
            return super.getProperty(name, observer);<br />
        }<br />
      };<br />
  }

There were two problems:

One big problem was that using this kind of BufferedImage turned out to be a great way to hang the JVM. Drawing beyond the bounds of the image seems to be what got the JVM in trouble and locked up my application.

Even without that bugginess, however, drawing to one of these BufferedImages was slooooow. Whatever speed up I might have gotten out of being able to quickly copy of one color component's bytes was lost to the sluggish drawing.

As for the idea of using BlendComposite, here's the code for that:

http://www.java2s.com/Open-Source/Java-Document/Swing-Library/swingx/org...

...which provides a lot of the flexible color blending I'd hope for. Instead of pulling all of this code into my project, however, I wrote a simplified version to do just the part I needed:

  protected static class LightenComposite implements Composite<br />
  {<br />
    static final CompositeContext   lightenContext = new LightenContext();<br />
<br />
    public CompositeContext createContext(ColorModel srcColorModel, ColorModel dstColorModel, RenderingHints hints)<br />
    {<br />
      return lightenContext;<br />
    }<br />
<br />
    protected static class LightenContext implements CompositeContext<br />
    {<br />
      public void compose(Raster src, Raster dstIn, WritableRaster dstOut)<br />
      {<br />
        int     width  = Math.min(src.getWidth(),  dstIn.getWidth());<br />
        int     height = Math.min(src.getHeight(), dstIn.getHeight());<br />
        int[]   srcRow = new int[width];<br />
        int[]   dstRow = new int[width];<br />
<br />
        for (int y = 0; y &amp;lt; height; ++y) {<br />
          src.getDataElements(0, y, width, 1, srcRow);<br />
          dstIn.getDataElements(0, y, width, 1, dstRow);<br />
<br />
          for (int x = 0; x &amp;lt; width; ++x) {<br />
            int   s = srcRow[x];<br />
            int   d = dstRow[x];<br />
            int   a = Math.min(((s &amp;gt;&amp;gt; 24) &amp; 0xFF) + ((d &amp;gt;&amp;gt; 24) &amp; 0xFF), 255) &amp;lt;&amp;lt; 24;<br />
            int   r = Math.max( (s &amp;gt;&amp;gt; 16) &amp; 0xFF, (d &amp;gt;&amp;gt; 16) &amp; 0xFF) &amp;lt;&amp;lt; 16;<br />
            int   g = Math.max( (s &amp;gt;&amp;gt;  8) &amp; 0xFF, (d &amp;gt;&amp;gt;  8) &amp; 0xFF) &amp;lt;&amp;lt;  8;<br />
            int   b = Math.max(  s        &amp; 0xFF,  d        &amp; 0xFF);<br />
<br />
            dstRow[x] = a | r | g | b;<br />
          }<br />
<br />
          dstOut.setDataElements(0, y, width, 1, dstRow);<br />
        }<br />
      }<br />
<br />
      public void dispose()<br />
      {<br />
      }<br />
    }<br />
  }

Two problems yet again... First, although it didn't crash the JVM, the results were buggy. When drawing shapes like ovals and rectangles, the results were correct, just what I'd expect. But for some reason text and line drawing behaved very weirdly, with line drawing causing portions of the image beyond particular lines I was drawing to be modified.

This was also slow. Not so terribly slow as my custom BufferedImage experiment, but not as fast as the gain I'm finding I get simply by switching to standard TYPE_INT_RGB BufferedImages.