在Java BufferedImage中绘制完全透明的“白色”

3
这可能听起来有点奇怪,但请耐心看完,这里有原因:
我正在尝试在灰色背景下生成文本周围的白色发光效果。
为了产生发光效果,我创建了一个比文本更大的新缓冲图像,然后将文本绘制成白色并通过ConvolveOp运行高斯模糊处理,希望得到以下结果:
 enter image description here 一开始我有点惊讶,因为发光效果比文本的灰色背景还要暗:
 enter image description here 但经过一番思考,我明白了问题所在:
卷积会对每个颜色通道(R、G、B 和 A)进行单独操作以计算模糊图像。图片的透明背景具有颜色值 0x00000000,即完全透明的黑色!因此,当卷积滤镜在图像上运行时,它不仅混合了 alpha 值,而且还将黑色混入白色像素的 RGB 值中。这就是为什么发光看起来很暗的原因。
要解决这个问题,我需要将图像初始化为0x00FFFFFF,即完全透明的白色,但如果我只设置该颜色并用其填充矩形,则不会发生任何事情,因为 Java 会说:“你正在绘制一个完全透明的矩形!它不会改变图像... 让我为您进行优化... 完成... 不客气。”。
相反,如果我将颜色设置为0x01FFFFFF,即几乎完全透明的白色,它会绘制矩形和美丽的发光效果,但最终结果是在周围出现了一个非常微弱的白色框...
有没有办法可以使整个图像都初始化为0x00FFFFFF?
更新:
我找到了一种方法,但可能是最低效的方法:
我将一个不透明的白色矩形绘制到图像上,然后运行RescaleOp,将所有 alpha 值设为0。虽然这样可行,但从性能的角度来看,这可能是一种可怕的方式。
我是否能以某种方式获得更好的效果?
PS:我也乐于听取创建此发光效果的完全不同建议。

在这种情况下,提供一个最小可重现示例(MCVE)总是很有帮助的。否则,人们将不得不编写大量样板代码并弄清楚ConvolveOp等内容,这会妨碍快速和专注的答案... - Marco13
2个回答

3
你的初始方法导致发光看起来更暗的主要原因很可能是你没有使用带有预乘alpha分量的图像。ConvolveOp的JavaDoc包含一些关于卷积期间如何处理alpha分量的信息。
你可以通过使用“几乎完全透明的白色”来解决这个问题。但是,作为替代方案,你可以简单地使用具有预乘alpha的图像,即类型为TYPE_INT_ARGB_PRE的图像。
这里有一个MCVE,它绘制了一个带有一些文本和文本周围脉动发光的面板(移除计时器并设置一个固定的半径以去除脉冲 - 我在这里无法抗拒玩耍...)。

Glow

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class TextGlowTest
{
    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);
        f.getContentPane().add(new TextGlowPanel());
        f.setSize(300,200);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }
}

class TextGlowPanel extends JPanel
{
    private BufferedImage image;
    private int radius = 1;

    TextGlowPanel()
    {
        Timer t = new Timer(50, new ActionListener()
        {
            long startMillis = -1;

            @Override
            public void actionPerformed(ActionEvent e)
            {
                if (startMillis == -1)
                {
                    startMillis = System.currentTimeMillis();
                }
                long d = System.currentTimeMillis() - startMillis;
                double s = d / 1000.0;
                radius = (int)(1 + 15 * (Math.sin(s * 3) * 0.5 + 0.5));
                repaint();
            }
        });
        t.start();
    }

    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        gr.setColor(Color.GRAY);

        int w = getWidth();
        int h = getHeight();
        gr.fillRect(0, 0, w, h);

        if (image == null || image.getWidth() != w || image.getHeight() != h)
        {
            // Must be prmultiplied!
            image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE);
        }

        Graphics2D g = image.createGraphics();
        Font font = g.getFont().deriveFont(70.0f).deriveFont(Font.BOLD);
        g.setFont(font);

        g.setComposite(AlphaComposite.Src);
        g.setColor(new Color(255,255,255,0));
        g.fillRect(0,0,w,h);

        g.setComposite(AlphaComposite.SrcOver);
        g.setColor(new Color(255,255,255,0));
        g.fillRect(0,0,w,h);

        g.setColor(Color.WHITE);
        g.drawString("Glow!", 50, 100);

        image = getGaussianBlurFilter(radius, true).filter(image, null);
        image = getGaussianBlurFilter(radius, false).filter(image, null);

        g.dispose();

        g = image.createGraphics();
        g.setFont(font);
        g.setColor(Color.BLUE);
        g.drawString("Glow!", 50, 100);
        g.dispose();

        gr.drawImage(image, 0, 0, null);
    }


    // From
    // http://www.java2s.com/Code/Java/Advanced-Graphics/GaussianBlurDemo.htm
    public static ConvolveOp getGaussianBlurFilter(
        int radius, boolean horizontal)
    {
        if (radius < 1)
        {
            throw new IllegalArgumentException("Radius must be >= 1");
        }

        int size = radius * 2 + 1;
        float[] data = new float[size];

        float sigma = radius / 3.0f;
        float twoSigmaSquare = 2.0f * sigma * sigma;
        float sigmaRoot = (float) Math.sqrt(twoSigmaSquare * Math.PI);
        float total = 0.0f;

        for (int i = -radius; i <= radius; i++)
        {
            float distance = i * i;
            int index = i + radius;
            data[index] =
                (float) Math.exp(-distance / twoSigmaSquare) / sigmaRoot;
            total += data[index];
        }

        for (int i = 0; i < data.length; i++)
        {
            data[i] /= total;
        }

        Kernel kernel = null;
        if (horizontal)
        {
            kernel = new Kernel(size, 1, data);
        }
        else
        {
            kernel = new Kernel(1, size, data);
        }
        return new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
    }
}

我一直想知道TYPE_INT_ARGB_PRE的意义是什么...现在我知道了!这是解决这个问题的一个非常好的方法!谢谢!+1你知道使用这种图像类型是否会减慢任何图形操作吗? - Markus A.
我不确定可能存在的性能差异,但是可以运行一些测试来验证。 - Marco13
我真的很喜欢你的解决方案,因为它提出了一个新的概念,我需要理解,并且非常快速的测试也没有显示使用TYPE_INT_ARGB_PRE会有任何显著的性能影响。谢谢!但是我认为我会接受@Radiodef的答案,因为它的影响更加有限(如果这样说得通的话)。我完全理解它是如何以及为什么工作的,我确信它不会做任何可能在后面出现的其他事情(请参见这里:http://stackoverflow.com/questions/26575187/impact-of-type-int-argb-pre)。从这个角度来看,它是一个“更简单”的解决方案(在智力上),尽管你的只需要4个字符。 :) - Markus A.

2

我发现clearRect应该使用透明颜色进行绘制。

g.setBackground(new Color(0x00FFFFFF, true));
g.clearRect(0, 0, img.getWidth(), img.getHeight());

您还可以通过直接设置像素数据,强制BufferedImage填充透明颜色。
public static void forceFill(BufferedImage img, int rgb) {
    for(int x = 0; x < img.getWidth(); x++) {
        for(int y = 0; y < img.getHeight(); y++) {
            img.setRGB(x, y, rgb);
        }
    }
}

虽然没有明确的文档说明,但我已经测试过了,setRGB 方法似乎接受 ARGB 值。


不需要使用 setBackground+clearRect,只需使用简单的 setColor+fillRect 即可实现(如果这样可以达到一般期望的效果-我不确定...) - Marco13
正如我在问题中写的那样,如果您将颜色设置为完全透明,则 setColor+fillRect 不起作用(至少在我的情况下),因此这不是一个选项,但是 setBackground+clearRect 有效!太好了!+1 - Markus A.
@MarkusA。正如你在我的例子中看到的那样:当你想用透明像素“覆盖”现有像素时,你必须首先设置g.setComposite(AlphaComposite.Src) - 否则,你是正确的:它不会绘制任何东西。 - Marco13
@Marco13 我明白了。我漏掉了那部分。不过我期望 clearRect 要快得多,因为它可能只是直接设置图像中的像素值,而无需担心绘制模式之类的问题。 - Markus A.

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