【问题标题】:Java Graphics2D - draw an image with gradient opacityJava Graphics2D - 绘制具有渐变不透明度的图像
【发布时间】:2016-04-13 19:02:16
【问题描述】:

使用Graphics2d,我试图在背景图像上绘制BufferedImage。在此图像中的任意点,我想在绘制的图像中“切一个圆形孔”以让背景显示出来。

我希望孔不是实心形状,而是渐变。换句话说,BufferedImage 中的每个像素都应具有与其到孔中心的距离成正比的 alpha/不透明度。

我对@9​​87654324@ 渐变和AlphaComposite 有点熟悉,但有没有办法将它们结合起来?

有没有(不是非常昂贵的)方法来实现这种效果?

【问题讨论】:

    标签: java image gradient alpha graphics2d


    【解决方案1】:

    这可以通过RadialGradientPaint 和适当的AlphaComposite 来解决。

    以下是MCVE,它显示了如何做到这一点。它使用与user1803551 used in his answer 相同的图像,因此屏幕截图看起来(几乎)相同。但是这个添加了一个MouseMotionListener,它允许您通过将当前鼠标位置传递给updateGradientAt方法来移动孔,在该方法中实际创建所需的图像:

    • 它首先用原始图像填充图像
    • 然后它创建一个RadialGradientPaint,它的中心颜色完全不透明,边框颜色完全透明(!)。这可能看起来违反直觉,但其目的是从现有图像中“切出”洞,这是通过下一步完成的:
    • AlphaComposite.DstOut 分配给Graphics2D。这会导致 alpha 值的“反转”,如公式中所示

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

      其中r 代表“结果”,s 代表“来源”,d 代表“目标”

    结果是图像在所需位置具有径向渐变透明度,在中心完全透明,在边界完全不透明 (!)。然后使用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) 
    };
    

    造成一个小的透明孔,带有一个大而柔软的“电晕”:

    而这些值

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

    产生一个大而清晰的中心,带有一个小的“电晕”:

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


    我发布(类似)答案的一些相关问题:


    编辑针对在 cmets 中提出的有关性能的问题

    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 它似乎确实提供了更好的性能(为此添加了一个编辑),但诚然,没有我预期的那么多。 (这可能是由于RadialGradientPaint 的复杂性/灵活性——你可以用它来实现一些漂亮的效果,简单地“挖一个洞”是这个类最简单的情况之一)
    【解决方案2】:

    我不知道你是打算动态创建这个透明的“洞”还是一次性的。我确信有几种方法可以实现您想要的,我正在展示其中一种直接更改像素的方法,这可能不是最好的性能(我只是不知道如何它与其他方式相比,我认为这将取决于你具体做什么)。

    我在这里描绘了澳大利亚上空臭氧层的空洞:

    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 可能超出范围;distance &gt; r 的像素可以从迭代中完全删除)。

    注意事项:

    • 我绘制了背景图像,并在其顶部绘制了较小的覆盖图像,只是为了清楚地显示背景的样子。
    • 有很多方法可以读取和更改 int 的 alpha 值,搜索此站点,您会发现至少还有 2-3 种方法。
    • 添加到您最喜欢的顶级容器并运行。

    来源:

    【讨论】:

    • 谢谢,如果我找不到其他解决方案,这或多或少是我打算做的。我只是想知道是否有一些内置的优化功能可以做到这一点。
    • 可能还需要一些边界检查,例如对于在大小为 (100,100) 的图像上使用参数 (99,99,10) 调用该方法的情况。
    • @Marco13 我在谈论方法时提到了边界检查(“它当然可以改进,我将把它留给你(提示:centerX - r 可能超出范围").
    猜你喜欢
    • 1970-01-01
    • 2014-05-08
    • 1970-01-01
    • 2013-05-13
    • 2016-10-28
    • 2011-02-07
    • 2014-07-21
    • 1970-01-01
    • 2016-11-25
    相关资源
    最近更新 更多