Java Graphics2D - draw an image with gradient opacity
Asked Answered
S

2

5

Using Graphics2d, I am trying to draw a BufferedImage on top of a background image. At an arbitrary point in this image, I would like to "cut a circular hole" in the drawn image to let the background show through.

I would like the hole not be a solid shape, but rather a gradient. In other words, every pixel in the BufferedImage should have an alpha/opacity proportional to its distance from the center of the hole.

I am somewhat familiar with Graphics2d gradients and with AlphaComposite, but is there a way to combine these?

Is there a (not insanely expensive) way to achieve this effect?

Sectarianize answered 9/1, 2016 at 0:11 Comment(0)
B
8

This can be solved with a RadialGradientPaint and the appropriate AlphaComposite.

The following is a MCVE that shows how this can be done. It uses the same images as user1803551 used in his answer, so a screenshot would look (nearly) the same. But this one adds a MouseMotionListener that allows you to move the hole around, by passing the current mouse position to the updateGradientAt method, where the actual creation of the desired image takes place:

  • It first fills the image with the original image
  • Then it creates a RadialGradientPaint, which has a fully opaque color in the center, and a completely transparent color at the border (!). This may seen counterintuitive, but the intention is to "cut out" the hole out of an existing image, which is done with the next step:
  • An AlphaComposite.DstOut is assigned to the Graphics2D. This one causes an "inversion" of the alpha values, as in the formula

    Ar = Ad*(1-As)
    Cr = Cd*(1-As)
    

    where r stands for "result", s stands for "source", and d stands for "destination"

The result is an image that has the radial gradient transparency at the desired location, being fully transparent at the center and fully opaque at the border (!). This combination of Paint and Composite is then used for filling an oval with the size and coordinates of the hole. (One could also do a fillRect call, filling the whole image - it would not change the outcome).

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RadialGradientPaint;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class TransparentGradientInImage
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        TransparentGradientInImagePanel p =
            new TransparentGradientInImagePanel();
        f.getContentPane().add(p);
        f.setSize(800, 600);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

}

class TransparentGradientInImagePanel extends JPanel
{
    private BufferedImage background;
    private BufferedImage originalImage;
    private BufferedImage imageWithGradient;

    TransparentGradientInImagePanel()
    {
        try
        {
            background = ImageIO.read(
                new File("night-sky-astrophotography-1.jpg"));
            originalImage = convertToARGB(ImageIO.read(new File("7bI1Y.jpg")));
            imageWithGradient = convertToARGB(originalImage);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }

        addMouseMotionListener(new MouseAdapter()
        {
            @Override
            public void mouseMoved(MouseEvent e)
            {
                updateGradientAt(e.getPoint());
            }
        });
    }


    private void updateGradientAt(Point point)
    {
        Graphics2D g = imageWithGradient.createGraphics();
        g.drawImage(originalImage, 0, 0, null);

        int radius = 100;
        float fractions[] = { 0.0f, 1.0f };
        Color colors[] = { new Color(0,0,0,255), new Color(0,0,0,0) };
        RadialGradientPaint paint = 
            new RadialGradientPaint(point, radius, fractions, colors);
        g.setPaint(paint);

        g.setComposite(AlphaComposite.DstOut);
        g.fillOval(point.x - radius, point.y - radius, radius * 2, radius * 2);
        g.dispose();
        repaint();
    }

    private static BufferedImage convertToARGB(BufferedImage image)
    {
        BufferedImage newImage =
            new BufferedImage(image.getWidth(), image.getHeight(),
                BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = newImage.createGraphics();
        g.drawImage(image, 0, 0, null);
        g.dispose();
        return newImage;
    }

    @Override
    protected void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        g.drawImage(background, 0, 0, null);
        g.drawImage(imageWithGradient, 0, 0, null);
    }
}

You may play with the fractions and colors of the RadialGradientPaint to achieve different effects. For example, these values...

float fractions[] = { 0.0f, 0.1f, 1.0f };
Color colors[] = { 
    new Color(0,0,0,255), 
    new Color(0,0,0,255), 
    new Color(0,0,0,0) 
};

cause a small, transparent hole, with a large, soft "corona":

TransparentGradientInImage02.png

whereas these values

float fractions[] = { 0.0f, 0.9f, 1.0f };
Color colors[] = { 
    new Color(0,0,0,255), 
    new Color(0,0,0,255), 
    new Color(0,0,0,0) 
};

cause a large, sharply transparent center, with a small "corona":

TransparentGradientInImage01.png

The RadialGradientPaint JavaDocs have some examples that may help to find the desired values.


Some related questions where I posted (similar) answers:


EDIT In response to the question about the performance that was asked in the comments

The question of how the performance of the Paint/Composite approach compares to the getRGB/setRGB approach is indeed interesting. From my previous experience, my gut feeling would have been that the first one is much faster than the second, because, in general, getRGB/setRGB tends to be slow, and the built-in mechanisms are highly optimized (and, in some cases, may even be hardware accelerated).

In fact, the Paint/Composite approach is faster than the getRGB/setRGB approach, but not as much as I expected. The following is of course not a really profound "benchmark" (I didn't employ Caliper or JMH for this), but should give a good estimation about the actual performance:

// NOTE: This is not really a sophisticated "Benchmark", 
// but gives a rough estimate about the performance

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RadialGradientPaint;
import java.awt.image.BufferedImage;

public class TransparentGradientInImagePerformance
{
    public static void main(String[] args)
    {
        int w = 1000;
        int h = 1000;
        BufferedImage image0 = new BufferedImage(w, h,
            BufferedImage.TYPE_INT_ARGB);
        BufferedImage image1 = new BufferedImage(w, h,
            BufferedImage.TYPE_INT_ARGB);

        long before = 0;
        long after = 0;
        int runs = 100;
        for (int radius = 100; radius <=400; radius += 10)
        {
            before = System.nanoTime();
            for (int i=0; i<runs; i++)
            {
                transparitize(image0, w/2, h/2, radius);
            }
            after = System.nanoTime();
            System.out.println(
                "Radius "+radius+" with getRGB/setRGB: "+(after-before)/1e6);

            before = System.nanoTime();
            for (int i=0; i<runs; i++)
            {
                updateGradientAt(image0, image1, new Point(w/2, h/2), radius);
            }
            after = System.nanoTime();
            System.out.println(
                "Radius "+radius+" with paint          "+(after-before)/1e6);
        }
    }

    private static void transparitize(
        BufferedImage imgA, int centerX, int centerY, int r)
    {

        for (int x = centerX - r; x < centerX + r; x++)
        {
            for (int y = centerY - r; y < centerY + r; y++)
            {
                double distance = Math.sqrt(
                    Math.pow(Math.abs(centerX - x), 2) +
                    Math.pow(Math.abs(centerY - y), 2));
                if (distance > r)
                    continue;
                int argb = imgA.getRGB(x, y);
                int a = (argb >> 24) & 255;
                double factor = distance / r;
                argb = (argb - (a << 24) + ((int) (a * factor) << 24));
                imgA.setRGB(x, y, argb);
            }
        }
    }

    private static void updateGradientAt(BufferedImage originalImage,
        BufferedImage imageWithGradient, Point point, int radius)
    {
        Graphics2D g = imageWithGradient.createGraphics();
        g.drawImage(originalImage, 0, 0, null);

        float fractions[] = { 0.0f, 1.0f };
        Color colors[] = { new Color(0, 0, 0, 255), new Color(0, 0, 0, 0) };
        RadialGradientPaint paint = new RadialGradientPaint(point, radius,
            fractions, colors);
        g.setPaint(paint);

        g.setComposite(AlphaComposite.DstOut);
        g.fillOval(point.x - radius, point.y - radius, radius * 2, radius * 2);
        g.dispose();
    }
}

The timings on my PC are along the lines of

...
Radius 390 with getRGB/setRGB: 1518.224404
Radius 390 with paint          764.11017
Radius 400 with getRGB/setRGB: 1612.854049
Radius 400 with paint          794.695199

showing that the Paint/Composite method is roughly twice as fast as the getRGB/setRGB method. Apart from the performance, the Paint/Composite has some other advantages, mainly the possible parametrizations of the RadialGradientPaint that are described above, which are reasons why I would prefer this solution.

Bedlamite answered 11/1, 2016 at 0:34 Comment(2)
Very nice, does this give better performance than my solution?Seibel
@Seibel It does seem to give a better performance (added an EDIT for this), but not as much as I expected, admittedly. (This might be due to the complexity/flexibility of RadialGradientPaint - you could achieve some nifty effects with that, and simply "cutting a hole" is one of the simplest possible cases for this class)Bedlamite
S
2

I don't know if you intend to create this transparent "hole" dynamically or if it's a one-time thing. I'm sure there are several methods to accomplish what you want and I'm showing one of them with directly changing the pixels, which might not be the best performance-wise (I just don't how it compares to other ways and I think it will depends on what you do exactly).

Here I depict the hole in the ozone layer over Australia:

enter image description here

public class Paint extends JPanel {

    BufferedImage imgA;
    BufferedImage bck;

    Paint() {

        BufferedImage img = null;
        try {
            img = ImageIO.read(getClass().getResource("img.jpg")); // images linked below
            bck = ImageIO.read(getClass().getResource("bck.jpg"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        imgA = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = imgA.createGraphics();
        g2d.drawImage(img, 0, 0, null);
        g2d.dispose();

        transparitize(200, 100, 80);
    }

    private void transparitize(int centerX, int centerY, int r) {

        for (int x = centerX - r; x < centerX + r; x++) {
            for (int y = centerY - r; y < centerY + r; y++) {
                double distance = Math.sqrt(Math.pow(Math.abs(centerX - x), 2)
                                            + Math.pow(Math.abs(centerY - y), 2));
                if (distance > r)
                    continue;
                int argb = imgA.getRGB(x, y);
                int a = (argb >> 24) & 255;
                double factor = distance / r;
                argb = (argb - (a << 24) + ((int) (a * factor) << 24));
                imgA.setRGB(x, y, argb);
            }
        }
    }

    @Override
    protected void paintComponent(Graphics g) {

        super.paintComponent(g);
        g.drawImage(bck, 0, 0, null);
        g.drawImage(imgA, 0, 0, null);
    }

    @Override
    public Dimension getPreferredSize() {

        return new Dimension(bck.getWidth(), bck.getHeight()); // because bck is larger than imgA, otherwise use Math.max
    }
}

The idea is to get the pixel's ARGB value with getRGB, change the alpha (or anything else), and set it with setRGB. I created a method that makes a radial gradient given a center and a radius. It can certainly be improved, I'll leave that to you (hints: centerX - r can be out of bounds; pixels with distance > r can be removed from the iteration altogether).

Notes:

  • I painted the background image and on top of it the smaller over-image just to show clearly what the background looks like.
  • There are quite a few ways to read and change the alpha value of the int, search this site and you'll find at least 2-3 more ways.
  • Add to your favorite top level container and run.

Sources:

Seibel answered 9/1, 2016 at 3:21 Comment(3)
Thanks, this is more or less what I was planning to do if I couldn't find another solution. I was just wondering if there was some built-in, optimized function to do it.Sectarianize
Some bounds checks might also be necessary, e.g. for the case that the method is called with the parameters (99,99,10) on an image with size (100,100).Bedlamite
@Bedlamite I mentioned the bounds check when talking about the method ("It can certainly be improved, I'll leave that to you (hints: centerX - r can be out of bounds").Seibel

© 2022 - 2024 — McMap. All rights reserved.