Images in Java
Sections in this page
Memory requirements
Let's get the size issue out of the way first. We have seen in another section that a typical photographic image these days occupies around 100 megabytes (Mb) or more of memory when it is loaded (uncompressed) for processing. That has an implication for the JVM because by default that will allocate only 64Mb to an application when it starts and increase that in steps of 16Mb up to 128Mb when necessary. If it needs any more it will throw an OutOfMemoryError, which is fatal. To prevent this it is necessary to specify initial and maximum memory sizes in the command line for running the application. Eg,
java -Xms256m -Xmx1024m classname
would start with 256 Mb and be able to expand to 1024 Mb (= 1 Gb).
The class representing 8- or 16-bit images
The class to instantiate for holding an image in memory is java.awt.image.BufferedImage. The Oracle API documentation shows that this can hold various kinds of image with different numbers of channels and 8 or 16 bits per channel. When loading an image from a disc file the type is set for us (see the image reading and writing page). It is important to ensure that when we come to display (a scaled down version of) the BufferedImage type it is compatible with the display hardware. Otherwise it can take a very long time for an image to be displayed or repainted. If we create an empty image it is necessary to use
java.awt.GraphicsEnvironment ge =
java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment ();
java.awt.GraphicsDevice gd = ge.getDefaultScreenDevice ();
java.awt.GraphicsConfiguration gc = gd.getDefaultConfiguration ();
java.awt.image.BufferedImage bim = gc.createCompatibleImage (width, height);
Similarly, when cloning an image or creating a destination image for an operation on an already loaded image:
BufferedImage dstBim = new BufferedImage (srcBim.getColorModel (),
srcBim.getRaster ().createCompatibleWritableRaster (dstWd, dstHt),
srcBim.isAlphaPremultiplied (), null);
In fact all of these details are taken care of in relevant classes in package net.grelf.grip: Im, ImPane, ImProcess.
Rasters
Contained within every BufferedImage object is a java.awt.image.WritableRaster object containing the multi-channel arrays of pixel values. The getWritableRaster () method of BufferedImage gets a reference to the raster. If you only want to read the pixel values and want to avoid inadvertently changing them, use instead the getRaster () method of BufferedImage, which gets a read-only reference to the raster, of the super-type java.awt.image.Raster.
We use a common pattern throughout our software for reading and writing pixel data in rasters. Here is one of the simplest instances of the pattern, from class ImProcess:
/** Invert the contrast in an image. */
public static void invert (java.awt.image.BufferedImage bim)
{
java.awt.image.WritableRaster wr = bim.getRaster ();
int maxLevel = net.grelf.grip.Im.getMaxLevel (bim);
int nBands = net.grelf.grip.Im.getNBands (bim);
int [] px = new int [nBands];
for (int y = 0; y < wr.getHeight (); y++)
{
for (int x = 0; x < wr.getWidth (); x++)
{
wr.getPixel (x, y, px);
for (int b = 0; b < nBands; b++)
{
px [b] = maxLevel - px [b];
}
wr.setPixel (x, y, px);
} }
} // invert
The classes for 32- or 64-bit images
For images deeper than 16 bits per channel, GRIP has its own data structure. The interface net.grelf.grip.Image currently has 2 implementations (in the same package): ImageInt and ImageDouble. ImageInt holds pixels as 32-bit integers per channel. ImageDouble holds them as 64-bit floating point numbers per channel.
All objects in Java have a 12-byte overhead. In a multidimensional array each sub-array is an object. So if you naively create an image as, say, int [x][y][channel] then there is an overhead of 12 bytes per pixel plus 12 bytes per row, so the 3-dimensional array requires several times the amount of memory that it should. GRIP does much better by storing such an image as int [channel][x + width * y].
Such an image is scanned by using code such as this (within the ImageInt class itself):
/** Invert the contrast in this ImageInt. */
public void invert ()
{
int maxLevel = this.extremes.high;
for (int y = 0, i = 0; y < getHeight (); y++)
{
for (int x = 0; x < getWidth (); x++, i++)
{
for (int b = 0; b < getNBands (); b++)
{
data [b][i] = maxLevel - data [b][i];
} } }
int min = maxLevel - this.extremes.high;
int max = maxLevel - this.extremes.low;
this.extremes = new net.grelf.grip.RangeInt (min, max);
} // invert
Notice that our Image classes hold a pair of numbers (a RangeInt or a RangeDouble) which are the minimum and maximum grey level actually occurring, across all channels. They are updated whenever a processing operation is performed, as shown in the example above. Unlike BufferedImage, it is not assumed that the full theoretical range (0..Integer.MAX_VALUE, or Double.MIN_VALUE..Double.MAX_VALUE) is used. This makes it easier and quicker to extract histograms, for example.
For processing from another class (without direct access to the data array) you would write in the following style.
/** Invert the contrast in an ImageInt. */
public void invert (net.grelf.grip.ImageInt image)
{
int maxLevel = image.getRange ().high;
// getRange () is quick: does not scan the image
int nBands = image.getNBands ();
int [] px = new int [nBands];
for (int y = 0; y < image.getHeight (); y++)
{
for (int x = 0; x < image.getWidth (); x++)
{
px = image.getPixel (x, y);
for (int b = 0; b < nBands; b++)
{
px [b] = maxLevel - px [b];
}
image.setPixel (x, y, px);
} }
image.getOverallRange ();
// Slower: rescans the image and sets the extremes.
} // invert
Processing any kind of image
We now show the use of the class net.grelf.grip.Im which can contain either a java.awt.image.BufferedImage or a net.grelf.grip.Image, so the following code will work with all kinds of images: any number of channels, from 8 to 64 bits per channel.
/** Invert the contrast in an image. */
public void invert (net.grelf.grip.Im im)
{
if (im.hasImage ())
{
net.grelf.grip.Image image = im.getImage ();
// Image is an interface. Implementations: ImageInt or ImageDouble
image.invert ();
}
else // The Im must have a BufferedImage:
{
java.awt.BufferedImage bim = im.getBufferedImage ();
net.grelf.grip.Improcess.invert (bim);
}
} // invert
Displaying an image
An instance of the class Im, introduced in the previous section, is wrapped in a net.grelf.grip.ImPane which handles all zooming, panning and scrolling. It in turn is wrapped in a net.grelf.grip.ImFrame which is a javax.swing.JFrame and displays itself as such. More details of the structure of an ImFrame can be seen on the next page.

