Optimizing Bitmap Display: setRGB vs MemoryImageSource
![]()
A couple of Data Flow Diagrams quickly showed me that the extreme pull method for pixel subscriber results in way too much coupling and that the extreme push model for pixel data using an intelligent data wrapper in the form of PixelSpace remains the best approach.
But, when large pixel arrays are displayed at the single pixel zoom level the whole display process slows way down with frame rate skipping at every frame!
Coming from the Java old school my first instinct, as shown in version 1.0 of PixelSpace, was to use a MemoryImageSource but this does not cooperate with BufferedImage where the ‘new’ (well at least since Java 1.4 so not that new) is to use:
BufferedImage.setRGB(int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize)
This sets an array of integer pixels in the default RGB color model (TYPE_INT_ARGB) and default sRGB color space, into a portion of the image data.
And it does so very quickly, fantastic!
Here is the improved PixelGrid and modernised PixelSpace:
PixelGrid
/*
* PixelGrid.java
* BlogPixel
*
* Created by headwedge on 08/07/2007.
* Copyright 2007 www.headwedge.com
* as in: head wedged up own arse
* All rights reserved.
*
*/
import headwedge.game.*;
import headwedge.pixel.*;
import java.awt.*;
import java.util.*;
/**
* @author <img style="width: 16px; height: 16px; float: left;"alt="favicon" src="doc-files/favicon.png"><a href="http://www.headwedge.com">www.headwedge.com</a>
* @version 2.1 12/10/07
* paint optimized to fast draw bitmap if no zoom
* @version 2.0 27/09/07
* paint optimized to draw only visible flyweights.
* @version 1.0
* A game component that displays a scalable grid of glyphs to represent domain
* data, usually pixel or cell information.
* As a pixel subsciber responds to pushed notification of a new PixelSpace
* containing domain data.
*/
public class PixelGrid extends Widget implements PixelSubscriber {
public PixelGrid(GameComposite parent, PixelFlyweight flyweight, int x, int y, int width, int height, int zoom) {
super(parent,0,0,width,height);
setFlyweight(flyweight);
setZoom(zoom);
}
protected void setFlyweight(PixelFlyweight flyweight) {
this.flyweight = flyweight;
}
public void setZoom(int zoom) {
this.zoom = zoom;
resize();
}
public void notify(PixelSpace pixels) {
data = pixels;
beDirty();
}
public void update(Graphics g) {
if (data == null) {
printCentre(g,ERROR_MESSAGE,Color.red);
}
}
public void paint(Graphics g) {
update(g);
if (!isGlued()) {
//paint background
Rectangle dirtyRectangle = g.getClipBounds();
g.setColor(Color.white);
g.fillRect(dirtyRectangle.x,dirtyRectangle.y,dirtyRectangle.width - 1,dirtyRectangle.height - 1);
//paint pixels
Rectangle bounds = getBoundingBox();
//select only visible data for display
int columnStart = (bounds.width < dirtyRectangle.width) ?0 :dirtyRectangle.x / zoom;
int columnEnd = (bounds.width < dirtyRectangle.width) ?data.width :((dirtyRectangle.x + dirtyRectangle.width) / zoom);
int rowStart = (bounds.height < dirtyRectangle.height) ?0 :dirtyRectangle.y / zoom;
int rowEnd = (bounds.height < dirtyRectangle.height) ?data.height :((dirtyRectangle.y + dirtyRectangle.height) / zoom);
//calculate screen coordinates so left and top edges overlap border
int x = (columnStart * zoom);
int y = (rowStart * zoom);
//centre if within bounds of display
x += (bounds.width < dirtyRectangle.width) ?(dirtyRectangle.width - bounds.width) / 2 :0;
y += (bounds.height < dirtyRectangle.height) ?(dirtyRectangle.height - bounds.height) / 2 :0;
//overlap right & bottom edges with border where appropriate
columnEnd += (columnEnd < data.width) ?1 :0;
rowEnd += (rowEnd <data.height) ?1 :0;
//paint the visible flyweights
if (zoom > 1) {
int j = y;
for (int row = rowStart; row < rowEnd; ++row) {
int i = x;
for (int column = columnStart; column < columnEnd; ++column) {
flyweight.paint(g,i,j,new Color(data.getPixel(column,row)),zoom,0);
i += zoom;
}
j += zoom;
}
}
//optimize as bitmap if not zoomed
else {
g.drawImage(data.getBufferedImage(),0,0,null);
}
glue();
}
}
public void resize() {
if (data != null) {
setSize(data.width * zoom, data.height * zoom);
}
getStage().setSize(this.getSize());
}
public void beDirty() {
unglue();
getStage().beDirty();
}
protected int zoom;
private PixelFlyweight flyweight;
private headwedge.game.Label error;
private PixelSpace data;
private static final String ERROR_MESSAGE = "No Pixel Data To Display!";
}
PixelSpace
/*
* PixelSpace.java
* BlogPixel
*
* Created by headwedge on 28/07/2007.
* Copyright 2007 www.headwedge.com
* as in: head wedged up own arse
* All rights reserved.
*
*/
import java.awt.image.*;
import headwedge.game.*;
/**
* @author <img style="width: 16px; height: 16px; float: left;"alt="favicon" src="doc-files/favicon.png"><a href="http://www.headwedge.com">www.headwedge.com</a>
* @version 1.2 on 15/10/07 - add pixel counting methods
* @version 1.1 on 12/10/07 - add buffered image
* @version 1.0
* A data class to encapsulate an array of int pixels argb info and the scansize,
* with methods for addressing pixels & creating a memory image source.
*/
public class PixelSpace {
public PixelSpace(int width, int height) {
pixels = new int[width * height];
this.width = width;
this.height = height;
bufferedImage = ImageFactory.createBufferedImage(width,height);
}
public MemoryImageSource getMemoryImageSource() {
return new MemoryImageSource(width, height, pixels, 0, width);
}
public BufferedImage getBufferedImage() {
bufferedImage.setRGB(0,0,width,height,pixels,0,width);
return bufferedImage;
}
public int[] toArray() {
return pixels;
}
public int getTotalPixelCount() {
return pixels.length;
}
public int getColourPixelCount(int argb) {
int count = 0;
for (int i = 0; i < pixels.length; ++i) {
if (argb == pixels[i]) {
++count;
}
}
return count;
}
public int getFuzzyColourPixelCount(int argb, double tolerance) {
int alpha = argb & 0xFF000000;
int red = (argb & 0×00FF0000) >> 16;
int green = (argb & 0×0000FF00) >> 8;
int blue = argb & 0×000000FF;
int min_red = red - (int)(red * tolerance);
int min_green = green - (int)(green * tolerance);
int min_blue = blue - (int)(blue * tolerance);
int max_red = red + (int)(red * tolerance);
int max_green = green + (int)(green * tolerance);
int max_blue = blue + (int)(blue * tolerance);
int count = 0;
for (int i = 0; i < pixels.length; ++i) {
red = (pixels[i] & 0×00FF0000) >> 16;
green = (pixels[i] & 0×0000FF00) >> 8;
blue = pixels[i] & 0×000000FF;
if ((red <= max_red) && (red >= min_red) && (green <= max_green) && (green >= min_green) && (blue <= max_blue) && (blue >= min_blue)) {
++count;
}
}
return count;
}
synchronized public int getPixel(int x, int y) {
return pixels[x + (y * width)];
}
synchronized public void setPixel(int x, int y, int argb) {
pixels[x + (y * width)] = argb;
}
public int width, height;
protected int[] pixels;
private BufferedImage bufferedImage;
}