6

I am writing a Buddhabrot fractal generator using aparapi. I got the OpenCL part of it to work, resulting in a single-dimension array that represents each pixel. I have the dimensions of the final image as final ints, and have written code to get the index of arbitrary points in that array. I want to save this as an image and I'm trying to use BufferedImage with TYPE_USHORT_GRAY. Here's what I have so far:

    BufferedImage image=new BufferedImage(VERTICAL_PIXELS, HORIZONTAL_PIXELS, BufferedImage.TYPE_USHORT_GRAY);
    for(int i=0; i<VERTICAL_PIXELS; i++)
        for(int k=0; k<HORIZONTAL_PIXELS; k++)
            image.setRGB(k, i, normalized[getArrayIndex(k,i,HORIZONTAL_PIXELS)]);

The problem is, I don't know what to set the RGB as. What do I need to do?

Shawn Walton
  • 1,714
  • 2
  • 14
  • 23

2 Answers2

7

The problem here is that setRGB() wants an 0xRRGGBB color value. BufferedImage likes to pretend that the image is RGB, no matter what the data is stored as. You can actually get at the internal DataBufferShort (with getTile(0, 0).getDataBuffer()), but it can be tricky to figure out how it is laid out.

If you already have your pixels in a short[], a simpler solution might be to copy them into an int[] instead an jam it into a MemoryImageSource:

int[] buffer = /* pixels */;

ColorModel model = new ComponentColorModel(
   ColorSpace.getInstance(ColorSpace.CS_GRAY), new int[] { 16 }, 
   false, true, Transparency.OPAQUE, DataBuffer.TYPE_USHORT);

Image image = Toolkit.getDefaultToolkit().createImage(
   new MemoryImageSource(VERTICAL_PIXELS, HORIZONTAL_PIXELS, 
                         model, buffer, 0, VERTICAL_PIXELS));

The advantage of this approach is that you control the underlying pixel array. You could make changes to that array and call newPixels() on your MemoryImageSource, and it would update live. It also gives you complete power to define your own palette other than grayscale:

int[] cmap = new int[65536];
for(int i = 0; i < 65536; ++i) {

    cmap[i] = (((i % 10000) * 256 / 10000) << 16) 
            | (((i % 20000) * 256 / 20000) << 8)
            | (((i % 40000) * 256 / 40000) << 0);
}
ColorModel model = new IndexColorModel(16, 65536, cmap, 0, false, -1, DataBuffer.TYPE_USHORT);

This approach works fine if you just want to display the image on the screen:

JFrame frame = new JFrame();
frame.getContentPane().add(new JLabel(new ImageIcon(image)));
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);

However, if you wanted to write it out to a file and preserve the one-short-per-pixel format (say, to load into Matlab) then you're out of luck. The best you can do is to paint it into a BufferedImage and save that with ImageIO, which will save as RGB.

If you definitely need a BufferedImage at the end, another approach is to apply the color palette yourself, calculate the RGB values, and then copy them into the image:

short[] data = /* your data */;
int[] cmap = /* as above */;
int[] rgb = new int[data.length];

for(int i = i; i < rgb.length; ++i) {
   rgb[i] = cmap[data[i]];
}

BufferedImage image = new BufferedImage(
   VERTICAL_PIXELS, HORIZONTAL_PIXELS, 
   BufferedImage.TYPE_INT_RGB);

image.setRGB(0, 0, VERTICAL_PIXELS, HORIZONTAL_PIXELS,
   pixels, 0, VERTICAL_PIXELS);
Russell Zahniser
  • 16,188
  • 39
  • 30
  • I did that, then realized my drawing code is FUBAR with ArrayOutOfBounds exceptions everywhere. Looking at it, though, it seems like it would work fine. All I need to do is output it as a .png, which I have done (hopefully) successfully. I'll mark this as correct because it seems like it would work fine. – Shawn Walton Jan 07 '12 at 00:17
  • When this puts the pixels in the image, does it draw them in vertical lines that start in the left and go right or horizontal that start at the top and go down? Because my partially-working code has the image rotated 90 degrees clockwise and I don't think it's the drawing's fault. – Shawn Walton Jan 07 '12 at 02:54
  • Nevermind! I had to change the 200 in the MemoryImageSource part to HORIZONTAL_PIXELS. It works great, thanks! – Shawn Walton Jan 07 '12 at 03:00
  • Sorry about that - I was working with a fixed-size 200x200 image and then I tried to switch in your constants, but I missed one. – Russell Zahniser Jan 07 '12 at 15:17
  • I did this and it works great. Problem is, it takes pretty much forever to draw large (2000x2000) images using img.getGraphics().drawImage(image, 0, 0, null); so that they can be saved with ImageIO. Is there some faster way to do it? – Shawn Walton Jan 07 '12 at 20:21
  • That code is where I draw into a BufferedImage so I can save it, sorry. – Shawn Walton Jan 07 '12 at 20:30
  • I would have expected the saving to take a lot longer than copying the image in memory. There is a potentially faster way that avoids the copy - I'll add it above. – Russell Zahniser Jan 07 '12 at 20:53
  • I think VERTICAL_PIXELS and HORIZONTAL_PIXELS are inverted. Its width first, then Height. – dgimenes Feb 06 '14 at 04:41
3

For reference, this example shows how two different BufferedImage types interpret the same 16 bit data. You can mouse over the images to see the pixel values.

Addendum: To elaborate on the word interpret, note that setRGB() tries to find the closest match to the specified value in the given ColorModel.

BufferedImageTest

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;

/** @see http://stackoverflow.com/questions/8765004 */
public class BufferedImageTest extends JPanel {

    private static final int SIZE = 256;
    private static final Random r = new Random();
    private final BufferedImage image;

    public BufferedImageTest(int type) {
        image = new BufferedImage(SIZE, SIZE, type);
        this.setPreferredSize(new Dimension(SIZE, SIZE));
        for (int row = 0; row < SIZE; row++) {
            for (int col = 0; col < SIZE; col++) {
                image.setRGB(col, row, 0xff00 << 16 | row << 8 | col);
            }
        }
        this.addMouseMotionListener(new MouseAdapter() {

            @Override
            public void mouseMoved(MouseEvent e) {
                Point p = e.getPoint();
                int x = p.x * SIZE / getWidth();
                int y = p.y * SIZE / getHeight();
                int c = image.getRGB(x, y);
                setToolTipText(x + "," + y + ": "
                    + String.format("%08X", c));
            }
        });
    }

    @Override
    protected void paintComponent(Graphics g) {
        g.drawImage(image, 0, 0, getWidth(), getHeight(), null);
    }

    static private void display() {
        JFrame f = new JFrame("BufferedImageTest");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setLayout(new GridLayout(1, 0));
        f.add(new BufferedImageTest(BufferedImage.TYPE_INT_ARGB));
        f.add(new BufferedImageTest(BufferedImage.TYPE_USHORT_GRAY));
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                display();
            }
        });
    }
}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • 1
    Technically speaking, these aren't showing the same 16 bit data - they are showing the same RGB values when coerced into either USHORT grayscale or RGB. This is because setRGB() does a color conversion into the palette of the image - it doesn't just write a pixel value. If it wrote the pixel value, the grayscale one would be white at the bottom. – Russell Zahniser Jan 07 '12 at 15:19