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

5
使用Graphics2d,我试图在背景图像上绘制一个BufferedImage。在这个图像的任意点上,我想要“切割一个圆形洞”,以便让背景显示出来。
我希望这个洞不是一个实心的形状,而是一个渐变。换句话说,BufferedImage中的每个像素都应该有一个与其距离洞中心成比例的alpha/不透明度。
我对Graphics2d渐变和AlphaComposite有一定的了解,但是否有一种方法将它们结合起来?
有没有一种(不是非常昂贵的)方法来实现这个效果?
2个回答

8
这可以通过使用RadialGradientPaint和适当的AlphaComposite来解决。
以下是一个MCVE,展示了如何实现。它使用与user1803551在他的答案中使用的相同图像,因此截图看起来(几乎)相同。但这个例子添加了一个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 (!). 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"

结果是在所需位置具有径向渐变透明度的图像,中心完全透明,边界完全不透明。这种PaintComposite的组合然后用于填充大小和孔的坐标的椭圆形。 (也可以进行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);
    }
}

您可以调整 RadialGradientPaintfractionscolors 来实现不同的效果。例如,这些值...

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) 
};

会造成一个小的、透明的孔,周围有一个大的、柔和的“日冕”:

TransparentGradientInImage02.png

而这些数值则是

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) 
};

导致一个大的、明显透明的中心,带有一个小的“日冕”:

TransparentGradientInImage01.png

RadialGradientPaint JavaDocs 有一些示例可能有助于找到所需的值。


我在以下相关问题上发布了(类似的)答案:


编辑:对于评论中提出的关于性能的问题的回应

确实很有趣,如何比较 Paint/Composite 方法的性能与 getRGB/setRGB 方法的性能。根据我的以往经验,我的直觉是第一种方法比第二种方法快得多,因为通常情况下,getRGB/setRGB 倾向于缓慢,而内置机制高度优化(在某些情况下甚至可能是硬件加速)。

实际上,Paint/Composite 方法比 getRGB/setRGB 方法更快,但并不如我预期的那么快。以下当然不是真正深入的“基准测试”(我没有使用 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();
    }
}

我的电脑时间大致为

...
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

显示出Paint/Composite方法大约是getRGB/setRGB方法的两倍快。除了性能外,Paint/Composite还有一些其他优点,主要是上面描述的可能的RadialGradientPaint参数化,这些是我更喜欢这个解决方案的原因。

非常好,这个解决方案比我的解决方案性能更好吗? - user1803551
1
@user1803551,它似乎确实提供了更好的性能(为此添加了一个编辑),但是我承认并没有像我预期的那样多。(这可能是由于RadialGradientPaint的复杂性/灵活性 - 您可以使用它实现一些漂亮的效果,而仅仅“切一个洞”只是这个类最简单的情况之一) - Marco13

2
我不知道您是否打算动态创建这个透明的“洞”,或者它只是一个一次性的事情。我确信有几种方法可以实现您想要的效果,我正在展示其中一种方法,直接更改像素,这可能在性能方面不是最佳的(我不确定如何与其他方式相比较,而且这将取决于您确切地做什么)。
在这里,我描绘了澳大利亚上方的臭氧层洞。

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
    }
}

这个想法是使用getRGB获取像素的ARGB值,改变alpha(或其他任何东西),并使用setRGB设置它。我创建了一个方法,根据中心和半径生成径向渐变。它肯定可以改进,这就留给你了(提示:centerX - r可能越界;距离> r的像素可以完全从迭代中删除)。

注:

  • 我先绘制了背景图像,然后在其上方绘制了较小的顶部图像,以清晰地显示背景的外观。
  • 有很多方法来读取和更改int的alpha值,请搜索这个网站,你会找到至少2-3种更多的方法。
  • 将其添加到您喜爱的顶级容器中,并运行。

来源:


谢谢,如果我找不到其他解决方案,这基本上就是我计划要做的。我只是想知道是否有一些内置的、优化的函数可以完成它。 - QuinnFreedman
1
有些边界检查可能也是必要的,例如对于在大小为(100,100)的图像上以参数(99,99,10)调用方法的情况。 - Marco13
@Marco13,我在谈论这个方法时提到了边界检查(“它肯定可以改进,我会把这留给你(提示:centerX - r可能越界)”)。 - user1803551

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接