简体   繁体   English

Java Graphics2D - 使用渐变不透明度绘制图像

[英]Java Graphics2D - draw an image with gradient opacity

Using Graphics2d , I am trying to draw a BufferedImage on top of a background image. 使用Graphics2d ,我试图在背景图像上绘制一个BufferedImage 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. 换句话说, BufferedImage每个像素都应具有与其距孔中心的距离成比例的alpha /不透明度。

I am somewhat familiar with Graphics2d gradients and with AlphaComposite , but is there a way to combine these? 我对Graphics2d渐变和AlphaComposite有点熟悉,但有没有办法将它们结合起来?

Is there a (not insanely expensive) way to achieve this effect? 是否有(不是非常昂贵)的方式来实现这种效果?

This can be solved with a RadialGradientPaint and the appropriate AlphaComposite . 这可以通过RadialGradientPaint和适当的AlphaComposite来解决。

The following is a MCVE that shows how this can be done. 以下是MCVE ,展示了如何做到这一点。 It uses the same images as user1803551 used in his answer , so a screenshot would look (nearly) the same. 它使用与他的答案中使用的user1803551相同的图像,因此屏幕截图看起来(几乎)相同。 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: 但是这个添加了一个MouseMotionListener ,它允许您通过将当前鼠标位置传递给updateGradientAt方法来移动孔,在该方法中实际创建所需图像:

  • 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 (!). 然后它创建一个RadialGradientPaint ,它在中心有一个完全不透明的颜色,在边框有一个完全透明的颜色(!)。 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 . AlphaComposite.DstOut分配给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" 其中r代表“结果”, s代表“源”, d代表“目的地”

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. 然后使用PaintComposite这种组合来填充具有孔的尺寸和坐标的椭圆。 (One could also do a fillRect call, filling the whole image - it would not change the outcome). (也可以进行fillRect调用,填充整个图像 - 它不会改变结果)。

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. 您可以使用RadialGradientPaintfractionscolors来实现不同的效果。 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. RadialGradientPaint JavaDocs有一些示例可能有助于找到所需的值。


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. 关于Paint / Composite方法的性能与getRGB / setRGB方法相比的问题确实很有趣。 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). 根据我之前的经验,我的直觉是第一个比第二个快得多,因为一般来说, getRGB / setRGB往往很慢,并且内置机制经过高度优化(在某些情况下) ,甚至可能是硬件加速)。

In fact, the Paint / Composite approach is faster than the getRGB / setRGB approach, but not as much as I expected. 事实上, Paint / Composite方法getRGB / setRGB方法,但并不如我预期的多。 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: 以下当然不是一个非常深刻的“基准”(我没有使用Caliper或JMH),但应该对实际性能给出一个很好的估计:

// 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. 显示Paint / Composite方法的速度大约是getRGB / setRGB方法的两倍。 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. 除了性能之外, Paint / Composite还有一些其他优点,主要是上面描述的RadialGradientPaint的可能参数化,这也是我更喜欢这个解决方案的原因。

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: 在这里,我描绘了澳大利亚臭氧层中的洞:

在此输入图像描述

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 . 我们的想法是使用getRGB获取像素的ARGB值,更改alpha(或其他任何内容),并使用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). 它肯定可以改进,我会留给你(提示: centerX - r可以超出范围; distance > r像素可以完全从迭代中移除)。

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. 有很多方法可以读取和更改int的alpha值,搜索此站点,您将找到至少2-3种方法。
  • Add to your favorite top level container and run. 添加到您最喜欢的顶级容器并运行。

Sources: 资料来源:

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM