5

Is there a fast way in Java to replace all instances in a bitmap of certain colors with other colors?

The image I am working with is a single very large 5616 x 2160 24bit non-transparent unindexed bitmap, although the pixel values of this bitmap will vary.

This is the code I am using at the moment, but it is much too slow: http://pastebin.com/UjgwgB0V

public class DisplayImage extends JFrame {

public DisplayImage(boolean resize, boolean mapCountries) throws IOException {
super("Province Map");
File mapProvinceFile = new File("map\\provinces.bmp");
BufferedImage mapProvinceImage = ImageIO.read(mapProvinceFile);

byte[] pixels = (byte[])mapProvinceImage.getRaster().getDataElements(0, 0, mapProvinceImage.getWidth(), mapProvinceImage.getHeight(), null);

if (mapCountries) {
    for (int i = 0; i < Victoria2Stats.provinceDefinitionArray.size(); i++) {
        for (int p = 0; p < pixels.length-3; p = p + 3) {
           if ((byte)Victoria2Stats.provinceDefinitionArray.get(i).rgb[0] == pixels[p]) {
               if ((byte)Victoria2Stats.provinceDefinitionArray.get(i).rgb[1] == pixels[p+1]) {
                   if ((byte)Victoria2Stats.provinceDefinitionArray.get(i).rgb[2] == pixels[p+2]) {
                       try {
                           if ((Victoria2Stats.provinceDataTable[i].ownerColor == null) && !(Victoria2Stats.provinceDataTable[i].lifeRating == 0)) {
                                pixels[p] = (byte)255;
                                pixels[p+1] = (byte)255;
                                pixels[p+2] = (byte)255;
                           } else {
                                pixels[p] = (byte)(Victoria2Stats.provinceDataTable[i].ownerColor.getRed());
                                pixels[p+1] = (byte)(Victoria2Stats.provinceDataTable[i].ownerColor.getBlue());
                                pixels[p+2] = (byte)(Victoria2Stats.provinceDataTable[i].ownerColor.getGreen());
                           }
                       } catch (NullPointerException e) {
                       // I realise this is a bad practice, but it is unrelated to the question and will be fixed later
                       }
                   }
               }
           }
      }
  }
}

BufferedImage buffer = new BufferedImage(mapProvinceImage.getWidth(), mapProvinceImage.getHeight(), mapProvinceImage.getType());
DataBuffer dataBuffer = new DataBufferByte(pixels, pixels.length);

SampleModel sampleModel = new ComponentSampleModel(DataBuffer.TYPE_BYTE, mapProvinceImage.getWidth(), mapProvinceImage.getHeight(), 3, mapProvinceImage.getWidth()*3, new int[]{0,1,2});
Raster raster = Raster.createRaster(sampleModel, dataBuffer, null);
buffer.setData(raster);

BufferedImage fixedImage = ImageUtils.verticalflip(buffer);
ImageIcon ii = new ImageIcon(fixedImage);
JScrollPane jsp = new JScrollPane(new JLabel(ii));
getContentPane().add(jsp);
setSize(800, 600);
setVisible(true);
}

}

Here is an example image: http://www.mediafire.com/?rttpk4o33b3oj74

I was thinking of somehow converting it to an indexed bitmap and then swapping the color indexes, but I couldn't figure out any way to successful assign it/recreate it with a color index with Java.

mKorbel
  • 109,525
  • 20
  • 134
  • 319
Kalelovil
  • 51
  • 3
  • Why did you edit out the code that I [edited into the question](http://stackoverflow.com/posts/12984550/revisions)? Many people are prevented from following external links, others simply refuse. Still, it's your question, so I'm not going to enter an editing war with you.. – Andrew Thompson Oct 20 '12 at 01:54
  • Leave the code here, don't link to pastebin. – vz0 Oct 20 '12 at 01:54
  • 3
    Every time I see catch(NullPointerException) I know I can stop reading. – MK. Oct 20 '12 at 01:58
  • Another problem with external links is that they disappear after a few days / weeks / months, making the question next to meaningless. – Stephen C Oct 20 '12 at 02:00
  • 2
    Apart from all the comments so far, the only thing I can think of is to divide the image into more manageable chunks and pass those chunks off to separate threads for processing – MadProgrammer Oct 20 '12 at 02:07
  • 1
    @Andrew Thompson: I didn't realise you had edited that in, I though I must have done so accidentally. Using pastebin is a habbit from the java IRC channels which I will avoid using on Stackoverflow in future. – Kalelovil Oct 20 '12 at 04:13

4 Answers4

2

General comments:

  • Your idea of converting to an indexed bitmap is unlikely to help, because (I think you will find that) conversion cost exceeds the saving.

  • You may do better solving the problem using an external application or a native code library.

I think there is scope for micro-optimization. For example:

public class DisplayImage extends JFrame {

  public DisplayImage(boolean resize, boolean mapCountries) 
      throws IOException {
    super("Province Map");
    File mapProvinceFile = new File("map\\provinces.bmp");
    BufferedImage mapProvinceImage = ImageIO.read(mapProvinceFile);

    byte[] pixels = (byte[])mapProvinceImage.getRaster().getDataElements(
        0, 0, mapProvinceImage.getWidth(), mapProvinceImage.getHeight(), null);

    if (mapCountries) {
      int len = Victoria2Stats.provinceDefinitionArray.size();
      for (int i = 0; i < len; i++) {
        int[] rgb = Victoria2Stats.provinceDefinitionArray.get(i);
        ProvinceDataTable pdt = Victoria2Stats.provinceDataTable[i];
        for (int p = 0; p < pixels.length-3; p = p + 3) {
          if ((byte) rgb[0] == pixels[p] &&
              (byte) rgb[1] == pixels[p+1] &&
              (byte) rgb[2] == pixels[p+2]) {
            if (pdt.ownerColor == null && pdt.lifeRating != 0) {  // HERE
              pixels[p] = (byte)255;
              pixels[p+1] = (byte)255;
              pixels[p+2] = (byte)255;
            } else {
              pixels[p] = (byte)(pdt.ownerColor.getRed());
              pixels[p+1] = (byte)(pdt.ownerColor.getBlue());
              pixels[p+2] = (byte)(pdt.ownerColor.getGreen());
            }
          }
        }
      }
    }

    BufferedImage buffer = new BufferedImage(
       mapProvinceImage.getWidth(), mapProvinceImage.getHeight(),
       mapProvinceImage.getType());
    DataBuffer dataBuffer = new DataBufferByte(pixels, pixels.length);

    SampleModel sampleModel = new ComponentSampleModel(
       DataBuffer.TYPE_BYTE, mapProvinceImage.getWidth(),
       mapProvinceImage.getHeight(), 3, mapProvinceImage.getWidth()*3,
       new int[]{0,1,2});
    Raster raster = Raster.createRaster(sampleModel, dataBuffer, null);
    buffer.setData(raster);

    BufferedImage fixedImage = ImageUtils.verticalflip(buffer);
    ImageIcon ii = new ImageIcon(fixedImage);
    JScrollPane jsp = new JScrollPane(new JLabel(ii));
    getContentPane().add(jsp);
    setSize(800, 600);
    setVisible(true);
  }
}

IMO, the improvement in readability makes this worthwhile even if it doesn't improve performance. Fixing the indentation helps too!

Incidentally, once I removed all of the verbiage, the probable source of your NPE problems became more obvious. Look at the line that I labelled "HERE". Note that the condition is such that the "then" branch is only taken if the ownerColor is null AND the lifeRating is non-zero. If the ownerColor is null AND the lifeRating is zero, you will take the "else" branch and an NPE is inevitable.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
2

An instance of java.awt.image.LookupOp with a suitable LookupTable may be faster. An example is cited here.

ColorConvertOp, illustrated here, would require a suitable ColorSpace.

ImageJ, mentioned here, can be used for batch processing.

Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
2

If you want to do this really fast, the best way is to access the underlying pixel data:

int[] data = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();

Then you should write your algorithm so that it does exactly one pass through this array, testing each int and changing it if necessary.

This approach should be as fast as native code.

mikera
  • 105,238
  • 25
  • 256
  • 415
  • My code is already doing something similar to that. Will 'int[] data = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();' be more efficient than 'byte[] pixels = (byte[])mapProvinceImage.getRaster().getDataElements(0, 0, mapProvinceImage.getWidth(), mapProvinceImage.getHeight(), null);'? – Kalelovil Oct 20 '12 at 05:12
  • Kalelovil: Edit your question to show new code, where it will be easier to read. Also, consider [profiling](http://stackoverflow.com/q/2064427/230513) to measure rorgress. – trashgod Oct 20 '12 at 12:42
  • That is existing code. See line 8 of the code in the question. – Kalelovil Oct 20 '12 at 13:03
2

I actually think you should try using Processing (www.processing.org) . I was able to do a simple color change for your image with very little code:

import processing.opengl.PGraphics3D;
//place provinces.bmp in the PROJECTFOLDER/data  
PImage p = loadImage("provinces.bmp"); 

//This just changes the window size, but the full image will be loaded
size(1000,800,P2D); 
PGraphics big = createGraphics(p.width, p.height, P2D);
big.beginDraw();
big.image(p,0,0);
//BEGIN GRAPHICS MANIPULATION
big.loadPixels();
for (int i = 0; i < big.pixels.length; i++) {
  color c = color(big.pixels[i]);
  float r = red(c);
  float g = green(c);
  float b = blue(c);
  //Let's do a simple color change, keeping r the same, setting green equal to old b, and setting blue to 0
  big.pixels[i] = color(r,b,0);
}
//END GRAPHICS MANIPULATION
big.updatePixels();
big.endDraw();
String path = savePath("big.jpg"); //change to tif or something else for uncompressed
big.save(path);
image(big, 0, 0); 

Leave a comment if you need some advice on how to do different color mappings, but you can change the code around the comment ("Let's do a simple color change")

Now obviously this is probably not the exact color change you want, but here is the result of the above code: http://www.kapparate.com/hw/big.jpg

FYI you can use PImage and PGraphics in a non-processing Java app by include core.jar from processing in your java classpath

Arcymag
  • 1,037
  • 1
  • 8
  • 18
  • Will it be faster than my Java code in the question? I haven't used Processing before, and trying to use your example code resulted in "NullPointerException at processing.core.PApplet.size(PApplet.java:1579)" on line 'size(1000,800,P2D);'. – Kalelovil Oct 20 '12 at 09:23
  • You could be getting a null pointer exception because the you didn't put the image in the "data" folder of your project. How fast is your code? This runs in under a second for me for one image. – Arcymag Oct 20 '12 at 19:26
  • If I place the bitmap in a {project}\data folder then the program can't find it. If I place it in the root directory of the project then it can be loaded and return the image dimensions, but a NullPointerException results whenever P2D is accessed. Perhaps this is related to my running the Processing code inside a PApplet inside a JPanel inside a JFrame. – Kalelovil Oct 21 '12 at 06:07