How to create BufferedImage for 32 bits per sample, 3 samples image data

2.3k views Asked by At

I am trying to create a BufferedImage from some image data which is a byte array. The image is RGB format with 3 samples per pixel - R, G, and B and 32 bits per sample (for each sample, not all 3 samples).

Now I want to create a BufferedImage from this byte array. This is what I have done:

        ColorModel cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] {32, 32, 32}, false, false, Transparency.OPAQUE, DataBuffer.TYPE_INT);
        Object tempArray = ArrayUtils.toNBits(bitsPerSample, pixels, samplesPerPixel*imageWidth, endian == IOUtils.BIG_ENDIAN);
        WritableRaster raster = cm.createCompatibleWritableRaster(imageWidth, imageHeight);
        raster.setDataElements(0, 0, imageWidth, imageHeight, tempArray); 
        BufferedImage bi = new BufferedImage(cm, raster, false, null);

The above code works with 24 bits per sample RGB image but not 32 bits per sample. The generated image is garbage which is shown on the right of the image. It is supposed to be like the left side of the image.

Note: the only image reader on my machine which can read this image is ImageMagick. All the others show similar results as the garbage one to the right of the following image.

The ArrayUtils.toNBits() just translates the byte array to int array with correct endianess. I'm sure this one is correct as I have cross checked with other methods to generate the same int array.

I guess the problem might arise from the fact I am using all the 32 bits int to represent the color which would contain negative values. Looks like I need long data type, but there is no DataBuffer type for long.

Instances of ComponentColorModel created with transfer types DataBuffer.TYPE_BYTE, DataBuffer.TYPE_USHORT, and DataBuffer.TYPE_INT have pixel sample values which are treated as unsigned integral values.

The above quote is from Java document for ComponentColorModel. This means the 32 bit sample does get treated as unsigned integer value. Then the problem could be somewhere else.

Has any body met similar problem and got a workaround or I may have done some thing wrong here?

Update2: The "real" problem lies in the fact when 32 bit sample is used, the algorithm for the ComponentColorModel will shift 1 to the left 0 times (1<<0) since shift on int is always within 0~31 inclusive. This is not the expected value. To solve this problem (actually shift left 32 times), the only thing needs to be done is change 1 from int to long type as 1L as shown in the fix below.

Update: from the answer by HaraldK and the comments, we have finally agreed that the problem is coming from Java's ComponentColorModel which is not handling 32 bit sample correctly. The proposed fix by HaraldK works for my case too. The following is my version:

import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;

public class Int32ComponentColorModel extends ComponentColorModel {
   //
   public Int32ComponentColorModel(ColorSpace cs, boolean alpha) {
        super(cs, alpha, false, alpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE, DataBuffer.TYPE_INT);
   }

   @Override
   public float[] getNormalizedComponents(Object pixel, float[] normComponents, int normOffset) {
       int numComponents = getNumComponents();

       if (normComponents == null || normComponents.length < numComponents + normOffset) {
           normComponents = new float[numComponents + normOffset];
       }

       switch (transferType) {
           case DataBuffer.TYPE_INT:
               int[] ipixel = (int[]) pixel;
               for (int c = 0, nc = normOffset; c < numComponents; c++, nc++) {
                   normComponents[nc] = ipixel[c] / ((float) ((1L << getComponentSize(c)) - 1));
               }
               break;
           default: // I don't think we can ever come this far. Just in case!!!
               throw new UnsupportedOperationException("This method has not been implemented for transferType " + transferType);
       }

       return normComponents;
   }
}

enter image description here

1

There are 1 answers

2
Harald K On BEST ANSWER

Update:

This seems to be a known bug: ComponentColorModel.getNormalizedComponents() does not handle 32-bit TYPE_INT, reported 10 (TEN!) years ago, against Java 5.

The upside, Java is now partly open-sourced. We can now propose a patch, and with some luck it will be evaluated for Java 9 or so... :-P

The bug proposes the following workaround:

Subclass ComponentColorModel and override getNormalizedComponents() to properly handle 32 bit per sample TYPE_INT data by dividing the incoming pixel value by 'Math.pow(2, 32) - 1' when dealing with this data, rather than using the erroneous bit shift. (Using a floating point value is ok, since getNormalizedComponents() converts everything to floating point anyway).

My fix is a little different, but the basic idea is the same (feel free to optimize as you see fit :-)):

private static class TypeIntComponentColorModel extends ComponentColorModel {
    public TypeIntComponentColorModel(final ColorSpace cs, final boolean alpha) {
        super(cs, alpha, false, alpha ? TRANSLUCENT : OPAQUE, DataBuffer.TYPE_INT);
    }

    @Override
    public float[] getNormalizedComponents(Object pixel, float[] normComponents, int normOffset) {
        int numComponents = getNumComponents();

        if (normComponents == null) {
            normComponents = new float[numComponents + normOffset];
        }

        switch (transferType) {
            case DataBuffer.TYPE_INT:
                int[] ipixel = (int[]) pixel;
                for (int c = 0, nc = normOffset; c < numComponents; c++, nc++) {
                    normComponents[nc] = ((float) (ipixel[c] & 0xffffffffl)) / ((float) ((1l << getComponentSize(c)) - 1));
                }
                break;
            default:
                throw new UnsupportedOperationException("This method has not been implemented for transferType " + transferType);
        }

        return normComponents;
    }
}

Consider the below code. If run as is, for me it displays a mostly black image, with the upper right quarter white overlayed with a black circle. If I change the datatype to TYPE_USHORT (uncomment the transferType line), it displays half/half white and a linear gradient from black to white, with an orange circle in the middle (as it should).

Using ColorConvertOp to convert to a standard type seems to make no difference.

public class Int32Image {
    public static void main(String[] args) {
        // Define dimensions and layout of the image
        int w = 300;
        int h = 200;
        int transferType = DataBuffer.TYPE_INT;
//        int transferType = DataBuffer.TYPE_USHORT;

        ColorModel colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, Transparency.OPAQUE, transferType);
        WritableRaster raster = colorModel.createCompatibleWritableRaster(w, h);
        BufferedImage image = new BufferedImage(colorModel, raster, false, null);

        // Start with linear gradient
        if (raster.getTransferType() == DataBuffer.TYPE_INT) {
            DataBufferInt buffer = (DataBufferInt) raster.getDataBuffer();
            int[] data = buffer.getData();

            for (int y = 0; y < h; y++) {
                int value = (int) (y * 0xffffffffL / h);

                for (int x = 0; x < w; x++) {
                    int offset = y * w * 3 + x * 3;
                    data[offset] = value;
                    data[offset + 1] = value;
                    data[offset + 2] = value;
                }
            }
        }
        else if (raster.getTransferType() == DataBuffer.TYPE_USHORT) {
            DataBufferUShort buffer = (DataBufferUShort) raster.getDataBuffer();
            short[] data = buffer.getData();

            for (int y = 0; y < h; y++) {
                short value = (short) (y * 0xffffL / h);

                for (int x = 0; x < w; x++) {
                    int offset = y * w * 3 + x * 3;
                    data[offset] = value;
                    data[offset + 1] = value;
                    data[offset + 2] = value;
                }
            }
        }

        // Paint something (in  color)
        Graphics2D g = image.createGraphics();
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, w / 2, h);
        g.setColor(Color.ORANGE);
        g.fillOval(100, 50, w - 200, h - 100);
        g.dispose();

        System.out.println("image = " + image);

//        image = new ColorConvertOp(null).filter(image, new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB));

        JFrame frame = new JFrame();
        frame.add(new JLabel(new ImageIcon(image)));
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

To me, this seems to suggest that there's something wrong with the ColorModel using transferType TYPE_INT. But I'd be happy to be wrong. ;-)

Another thing you could try, is to scale the values down to 16 bit, use a TYPE_USHORT raster and color model, and see if that makes a difference. I bet it will, but I'm too lazy to try. ;-)