Search code examples
javacolorspixelbufferedimage

Multiply greyscale BufferedImage by custom Color in Java


Been searching all over the place, found a number of approaches. However on a per-pixel approach (pull byte data from Raster, bit shift / multiply values for ARGB result) I get a bit stuck figuring out how to apply my Color to that pixel.

Here's the image:

ARGB bitmap

This is my current approach and code but it's foiled by a bug that will be fixed in a subsequent JRE / JDK release but isn't as of yet (8u66):

public BufferedImage applyShader(BufferedImage input) {
    BufferedImage output = new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_INT_ARGB);
    float red = new Float(Integer.toString(shader.getRed()));
    float blue = new Float(Integer.toString(shader.getBlue()));
    float green = new Float(Integer.toString(shader.getGreen()));
    if(red > 0.0f) red = 255.0f / red * 100f;
    if(blue > 0.0f) blue = 255.0f / blue * 100f;
    if(green > 0.0f) green = 255.0f / green * 100f;     
    System.out.println(red + ", " + blue + ", " + green);       

    float[] factors = new float[] {
        1.0f, 1.0f, 1.0f, 1.0f
    };

    float[] offsets = new float[] {
        red, blue, green
    };

    RescaleOp op = new RescaleOp(factors, offsets, null);
    output = op.filter(input, null);
    return output;
}

This is my previous approach with the Color values hopefully thrown in correctly (Stack Overflow source), but it hangs indefinitely (well, for several minutes before I killed it). Probably is my code, but performance could also be something to consider:

public BufferedImage applyShader(BufferedImage input) {
    BufferedImage output = new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_INT_ARGB);
    float red = new Float(Integer.toString(shader.getRed()));
    float blue = new Float(Integer.toString(shader.getBlue()));
    float green = new Float(Integer.toString(shader.getGreen()));
    int red1 = shader.getRed();
    int blue1 = shader.getBlue();
    int green1 = shader.getGreen();

    final byte[] pixels = ((DataBufferByte) input.getRaster().getDataBuffer()).getData();
    final int width = input.getWidth();
    final int height = input.getHeight();
    final boolean hasAlphaChannel = input.getAlphaRaster() != null;

    int[][] result = new int[height][width];
    if(hasAlphaChannel) {
        final int pixelLength = 4;
        for(int pixel = 0, row = 0, col = 0; pixel < pixels.length; pixel += pixelLength) {
            int argb = 0;
            argb += (((int) pixels[pixel] & 0xff) << 24); // alpha
            argb += ((int) (pixels[pixel + 1] + blue1) & 0xff); // blue
            argb += (((int) (pixels[pixel + 2] + green1) & 0xff) << 8); // green
            argb += (((int) (pixels[pixel + 3] + red1) & 0xff) << 16); // red
            result[row][col] = argb;
            col++;
            if (col == width) {
                col = 0;
                row++;
            }
        }
    } else {
        final int pixelLength = 3;
        for(int pixel = 0, row = 0, col = 0; pixel < pixels.length; pixel += pixelLength) {
            int argb = 0;
            argb += -16777216; // 255 alpha
            argb += ((int) (pixels[pixel] + blue1) & 0xff); // blue
            argb += (((int) (pixels[pixel + 1] + green1) & 0xff) << 8); // green
            argb += (((int) (pixels[pixel + 2] + red1) & 0xff) << 16); // red
            result[row][col] = argb;
            col++;
            if (col == width) {
                col = 0;
                row++;
            }
        }
    }

    System.out.println(input.getRaster().getWidth() + " / " + input.getRaster().getHeight());
    WritableRaster raster = Raster.createWritableRaster(input.getSampleModel(), new Point(0, 0));
    for(int i = 0; i < input.getRaster().getWidth(); i++) {
        for(int j = 0; j < input.getRaster().getHeight(); j++) {
            int k = result[i][j];
            raster.setSample(i, j, 0, k);
        }
    }

    output.setData(raster);
    return output;
}

Any advice or links? Am I missing something simple in the alternate byte to ARGB method?

This is the closest question I've found, but surprisingly doesn't even have a single vote, nevermind answer.


Solution

  • From my comments above, using 4 factors and 4 offsets, using the first created output and re-arranging the RBG order to RGB, I have the following code and it's working fine:

    public class ShaderTest {
        private final Color shader;
    
        public ShaderTest(final Color shader) {
            this.shader = shader;
        }
    
        public  BufferedImage applyShader(BufferedImage input) {
            BufferedImage output = new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_INT_ARGB);
            float red = shader.getRed();
            float green = shader.getGreen();
            float blue = shader.getBlue();
            if(red > 0.0f) red = 255.0f / red * 100f;
            if(green > 0.0f) green = 255.0f / green * 100f;
            if(blue > 0.0f) blue = 255.0f / blue * 100f;
            System.out.println(red + ", " + green + ", " + blue);
    
            float[] factors = new float[] {
                    1.0f, 1.0f, 1.0f, 1.0f
            };
    
            float[] offsets = new float[] {
                    red, green, blue, 0
            };
    
            RescaleOp op = new RescaleOp(factors, offsets, null);
            return op.filter(input, output);
        }
    
        // Test code
        public static void main(String[] args) throws IOException {
            BufferedImage image = ImageIO.read(new File(args[0]));
            final BufferedImage shaded = new ShaderTest(Color.ORANGE).applyShader(image);
    
            // Show shaded image
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    JFrame frame = new JFrame("ShaderTest");
                    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
                    frame.add(new JLabel(new ImageIcon(shaded)));
    
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    }