A couple months ago I signed up to do a presentation on concurrency for the St. Louis Java User Group, which happened last night. I wanted to explore a bunch of techniques and issues but stick to one domain, so I chose generation of the Mandelbrot set, just for fun.
It takes me way back to my middle school days, when I wrote my first Mandelbrot program in Turbo Pascal on my IBM PC (the original one). At the time, understanding how to program the algorithm was the challenging part – working through the recursive algorithm, splitting the complex and real parts of the calculation, understanding the escape tests, etc. Actually writing the picture to the screen was easy – for each pixel you just did something like draw(x,y,color). The original IBM PC was CGA so “color” meant a massive 2 bits: cyan, magenta, aqua, or black. I’d set up a particular coordinate region, kick it off, and go to bed. If I was lucky, it would be done painting the screen before I left for school the next morning. Then I could PrintScreen it to my dot matrix, stuff it in my TrapperKeeper, and strap on my Zips. Yeah, I’ve always been a nerd.
I roughed out the equivalent in Java the other night and found life has changed a lot. It took me only a few minutes to build up the algorithm and actually start printing out some really crummy Mandelbrots in text with the characters “.oO”. Then (not being a GUI guy) I started looking at how to actually draw some pixels on the screen in Java. Gadzooks! You gotta know about BufferedImages and ColorModels and Rasters and SampleModels. These fancy displays we’ve got have made life pretty complicated. But in the end, the key thing is that you want to paint some pixel on your screen a certain color.
I called this a dummy’s guide because I’m a total novice at this stuff. So, when I say really stupid stuff below, please have a good laugh and then drop me a comment or an email so I can fix it. This article is not exhaustive – if you want that, go buy a book. I’m trying to give you the skeleton as far as I understand it and some key insights and examples.
The Model
Java has had java.awt.Image forever as a way to read in and represent a chunk of image data. But there was no writable or editable equivalent until Java2D was added and we now have BufferedImage.
As always, remember that the goal is to paint pixels. The BufferedImage is comprised of a Raster, a description of per-pixel data values, and a ColorModel that describes how to take each set of pixel values and derive a color for that pixel. Conceptually, for each pixel in the image, the Raster is consulted for a set of values describing a pixel, then the ColorModel is used to determine the color corresponding to that set of values.
At this point in the detail we must acknowledge that there is a high amount of variability in the actual display system in use. For high performance, it is desirable to represent data close to how the display wants to receive it. The data types, the way they are packed, the order they are packed in, and so on all differ across the vast Java universe. Providing both a general programming abstraction and a high performance solution is difficult. I’ve seen this happen in design before and there is no good solution – typically you just pick a middle road and try to balance the forces. I feel that tension in these classes.
Inside the Raster, the pixel data is broken into two parts: the DataBuffer and the SampleModel. The DataBuffer stores the actual pixel data as one or more arrays of values, each called a bank. The values in each bank may be of a variety of types (byte, short, unsigned short, integer, float, double). To understand the data within the DataBuffer, we need the SampleModel, which describes how to extract a particular pixel’s data to pass to the ColorModel. The SampleModel is your map to unlock the mysteries of a particular DataBuffer.
For example, DataBuffers might store red, green, and blue pixels each in a separate bank. Or it might store them interleaved in a single bank. Or something else.
To summarize the general flow of information:
- DataBuffer holds raw data values in some form
- SampleModel understands how to read the DataBuffer and produce one or more values describing a single pixel
- ColorModel understands how to take the data values describing a pixel and produce the appropriate color on the display
Colors
Before talking further about data models, let’s talk about colors first. ColorModels are nothing more than translators (in both directions) between a set of samples (data values) describing a pixel and colors. Of course, there are many ways to do this conversion and many ways to specify the samples.
The most direct way to specify a color model is with ComponentColorModel where each sample value maps directly to the components of a color (RGB for example). The ComponentColorModel expects to receive a set of values (an int[] for example) where each value of the array represents red, green, or blue.
There is also a PackedColorModel that expects to receive all of the samples for a pixel in one value. The PackedColorModel unpacks the single value using a set of specified bit masks to retrieve each channel from the single value. A subclass called DirectColorModel deals specifically with the most common case of RGB packed values.
The IndexColorModel is used for cases where you have a fixed palette of colors and a single value serves as an index into that palette (list) of colors. In this case, the pixel value does not represent components of color but the choice of a particular color by index.
Putting it all together
There are a zillion ways to put all these pieces together and much better references than this page on how to do so. But here I’ll show you an actual example for what I needed to do. I wanted to just calculate an iteration value for each pixel and then use that to index into a simple color palette with 256 colors.
[source:java]
public class PointPanel extends JPanel {
// lots of code to set up the config for generating the image
private ColorModel colorModel = calculateColorModel();
public void paintComponent(Graphics g) {
if(needsRepaint()) {
int width = getWidth(); // Get actual current dimensions of panel
int heigh = getHeight();
byte[] data = generateData(width, height, …);
drawImage(g, data);
}
}
private void drawImage(Graphics g, byte[] data) {
DataBuffer buffer = new DataBufferByte(data, data.length);
SampleModel sm = colorModel.createCompatibleSampleModel(getWidth(), getHeight());
WritableRaster raster = Raster.createWritableRaster(sm, buffer, null);
BufferedImage img = new BufferedImage(colorModel, raster, false, null);
g.drawImage(img, 0, 0, null);
}
private ColorModel createColorModel() {
byte[] reds = new byte[colors];
byte[] greens = new byte[colors];
byte[] blues = new byte[colors];
for(int i=0; i<256; i++) { reds[i] = (byte) i; greens[i] = (byte) i; blues[i] = (byte) i; } return new IndexColorModel(8, 256, reds, greens, blues); } private boolean needsRepaint() { // determine whether I’ve already painted these bounds and image to avoid repaint } private byte[] generateData(int width, int height, …) { // actually calculate the pixel values } [/source] So, you’ll notice here that the data is in a single array and not a 2-dimensional array. This is done to the data buffer and sample model that I’m using that expects all of the data to be spanned into a single long array. The index color model I’m creating above is really dumb. I actually defined a gradient in my model but the code is too long to reproduce here. All the various pieces get stuck together to create a BufferedImage and that is simply draw onto the Graphics object. This is not the most exciting example but hopefully it’s good enough that you know where to look to make modifications. Good luck!