【问题标题】:Java bitmap font: blitting 1-bit image with different colorsJava 位图字体:用不同颜色对 1 位图像进行 blitting
【发布时间】:2011-11-10 23:53:55
【问题描述】:

我想在基于 Java AWT 的应用程序中实现一个简单的位图字体绘制。应用程序使用Graphics 对象,我想在其中实现一个简单的算法:

1) 加载一个文件(可能使用ImageIO.read(new File(fileName))),它是 1 位 PNG,看起来像这样:

即它是 8*8 字符的 16*16(或 16*many,如果我想支持 Unicode)矩阵。黑色对应背景色,白色对应前景。

2) 逐个字符地绘制字符串,将该位图的相关部分传送到目标Graphics。到目前为止,我只成功了这样的事情:

    int posX = ch % 16;
    int posY = ch / 16;

    int fontX = posX * CHAR_WIDTH;
    int fontY = posY * CHAR_HEIGHT;

    g.drawImage(
            font,
            dx, dy, dx + CHAR_WIDTH, dy + CHAR_HEIGHT,
            fontX, fontY, fontX + CHAR_WIDTH, fontY + CHAR_HEIGHT,
            null
    );

它可以工作,但是,可惜的是,它会按原样显示文本,即我不能用所需的前景色和背景色替换黑色和白色,我什至不能使背景透明。

所以,问题是:在 Java 中是否有一种简单(且快速!)的方式来将一个 1 位位图的一部分传输到另一个位图,并在传输过程中对其进行着色(即用一种给定的颜色替换所有 0 像素并且全部 1 个像素与另一个像素)?

我研究了几个解决方案,但在我看来,它们都不是最理想的:

  • 使用自定义着色 BufferedImageOp,如 this solution 中所述 - 它应该可以工作,但在每次 blit 操作之前重新着色位图似乎效率很低。
  • 使用多个 32 位 RGBA PNG,黑色像素的 Alpha 通道设置为 0,前景设置为最大值。每个所需的前景色都应该有自己的预渲染位图。通过这种方式,我可以使背景透明并在 blitting 之前将其单独绘制为一个矩形,然后使用我的字体选择一个位图,用所需的颜色预着色并在该矩形上绘制它的一部分。对我来说似乎是一个巨大的矫枉过正 - 是什么让这个选项更糟 - 它将前景色的数量限制在相对较小的数量(即我可以实际加载并保存数百或数千个位图,而不是数百万)
  • 捆绑和加载自定义字体,如this solution 中所述,可以工作,但据我在Font#createFont 文档中看到的,AWT 的Font 似乎只适用于基于矢量的字体,而不适用于基于位图的字体.

可能已经有任何库实现了此类功能?或者是时候让我切换到某种更高级的图形库,比如lwjgl

基准测试结果

我在一个简单的测试中测试了几个算法:我有 2 个字符串,每个字符串 71 个字符,并且一个接一个地连续绘制它们,就在同一个地方:

    for (int i = 0; i < N; i++) {
        cv.putString(5, 5, STR, Color.RED, Color.BLUE);
        cv.putString(5, 5, STR2, Color.RED, Color.BLUE);
    }

然后我测量所用时间并计算速度:每秒字符串和每秒字符数。到目前为止,我测试的各种实现产生了以下结果:

  • 位图字体,16*16字符位图:10991个字符串/秒,780391个字符/秒
  • 位图字体,预分割图像:11048 个字符串/秒,784443 个字符/秒
  • g.drawString():8952 个字符串/秒,635631 个字符/秒
  • 彩色位图字体,使用 LookupOp 和 ByteLookupTable 着色:404 个字符串/秒,28741 个字符/秒

【问题讨论】:

  • “它应该可以工作,但它似乎效率很低”我相信分析而不是意见。
  • +1 表示基准测试结果。所以 64 美元的问题是:任何渲染 28K+ 字符/秒的方法是否达到最低规格。对于这个用例?
  • 我当前的目标是 80*50 字符控制台的 25..30 FPS。理想情况下是 80*50*30 ~ 120K 字符/秒。 28K 会在全控制台动画重绘时提供大约 7 FPS 的效果。我接受了peek at what Notch does at his LD16 game,似乎有一些原始位数组处理魔法。我会尝试更多地挖掘这种可能性。

标签: java fonts bitmap awt lwjgl


【解决方案1】:

您可以将每个位图转换为Shape(或其中许多)并绘制Shape。获取Shape的流程见Smoothing a jagged path

例如

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import java.util.Random;

/* Gain the outline of an image for further processing. */
class ImageShape {

    private BufferedImage image;

    private BufferedImage ImageShape;
    private Area areaOutline = null;
    private JLabel labelOutline;

    private JLabel output;
    private BufferedImage anim;
    private Random random = new Random();
    private int count = 0;
    private long time = System.currentTimeMillis();
    private String rate = "";

    public ImageShape(BufferedImage image) {
        this.image = image;
    }

    public void drawOutline() {
        if (areaOutline!=null) {
            Graphics2D g = ImageShape.createGraphics();
            g.setColor(Color.WHITE);
            g.fillRect(0,0,ImageShape.getWidth(),ImageShape.getHeight());

            g.setColor(Color.RED);
            g.setClip(areaOutline);
            g.fillRect(0,0,ImageShape.getWidth(),ImageShape.getHeight());
            g.setColor(Color.BLACK);
            g.setClip(null);
            g.draw(areaOutline);

            g.dispose();
        }
    }

    public Area getOutline(Color target, BufferedImage bi) {
        // construct the GeneralPath
        GeneralPath gp = new GeneralPath();

        boolean cont = false;
        int targetRGB = target.getRGB();
        for (int xx=0; xx<bi.getWidth(); xx++) {
            for (int yy=0; yy<bi.getHeight(); yy++) {
                if (bi.getRGB(xx,yy)==targetRGB) {
                    if (cont) {
                        gp.lineTo(xx,yy);
                        gp.lineTo(xx,yy+1);
                        gp.lineTo(xx+1,yy+1);
                        gp.lineTo(xx+1,yy);
                        gp.lineTo(xx,yy);
                    } else {
                        gp.moveTo(xx,yy);
                    }
                    cont = true;
                } else {
                    cont = false;
                }
            }
            cont = false;
        }
        gp.closePath();

        // construct the Area from the GP & return it
        return new Area(gp);
    }

    public JPanel getGui() {
        JPanel images = new JPanel(new GridLayout(1,2,2,2));
        JPanel  gui = new JPanel(new BorderLayout(3,3));

        JPanel originalImage =  new JPanel(new BorderLayout(2,2));
        final JLabel originalLabel = new JLabel(new ImageIcon(image));

        originalImage.add(originalLabel);


        images.add(originalImage);

        ImageShape = new BufferedImage(
            image.getWidth(),
            image.getHeight(),
            BufferedImage.TYPE_INT_RGB
            );

        labelOutline = new JLabel(new ImageIcon(ImageShape));
        images.add(labelOutline);

        anim = new BufferedImage(
            image.getWidth()*2,
            image.getHeight()*2,
            BufferedImage.TYPE_INT_RGB);
        output = new JLabel(new ImageIcon(anim));
        gui.add(output, BorderLayout.CENTER);

        updateImages();

        gui.add(images, BorderLayout.NORTH);

        animate();

        ActionListener al = new ActionListener() {
            public void actionPerformed(ActionEvent ae) {
                animate();
            }
        };
        Timer timer = new Timer(1,al);
        timer.start();

        return gui;
    }

    private void updateImages() {
        areaOutline = getOutline(Color.BLACK, image);

        drawOutline();
    }

    private void animate() {
        Graphics2D gr = anim.createGraphics();
        gr.setColor(Color.BLUE);
        gr.fillRect(0,0,anim.getWidth(),anim.getHeight());

        count++;
        if (count%100==0) {
            long now = System.currentTimeMillis();
            long duration = now-time;
            double fraction = (double)duration/1000;
            rate = "" + (double)100/fraction;
            time  = now;
        }
        gr.setColor(Color.WHITE);
        gr.translate(0,0);
        gr.drawString(rate, 20, 20);

        int x = random.nextInt(image.getWidth());
        int y = random.nextInt(image.getHeight());
        gr.translate(x,y);

        int r = 128+random.nextInt(127);
        int g = 128+random.nextInt(127);
        int b = 128+random.nextInt(127);
        gr.setColor(new Color(r,g,b));

        gr.draw(areaOutline);

        gr.dispose();
        output.repaint();
    }

    public static void main(String[] args) throws Exception {
        int size = 150;
        final BufferedImage outline = javax.imageio.ImageIO.read(new java.io.File("img.gif"));

        ImageShape io = new ImageShape(outline);

        JFrame f = new JFrame("Image Outline");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(io.getGui());
        f.pack();
        f.setResizable(false);
        f.setLocationByPlatform(true);
        f.setVisible(true);
    }
}

我不得不计算蓝色图像左上角的 FPS 计数有十倍的误差。 50 FPS 我可以相信,但 500 FPS 似乎..是错误的。

【讨论】:

  • 这是相当尴尬的解决方案 - 如果我采用矢量方式,我宁愿将位图字体转换为矢量字体并使用它。我想更直接的方法会更快更容易实现。
  • “我猜..” (血腥的)猜测已经够了!不要猜测,测试
  • 感谢您的努力,这是非常值得的赏金。你是对的 - 我应该对提到的方法进行一些基准测试。我会尝试发布更新,因为我会进行一些测量。
  • 我添加了几个基准测试结果 - 查看更新后的帖子。
  • 如果您仍然感兴趣,请查看我的原始像素操作的新解决方案 :) 看起来效果很好。
【解决方案2】:

好的,看来我找到了最佳解决方案。成功的关键是访问底层 AWT 结构中的原始像素数组。初始化是这样的:

public class ConsoleCanvas extends Canvas {
    protected BufferedImage buffer;
    protected int w;
    protected int h;
    protected int[] data;

    public ConsoleCanvas(int w, int h) {
        super();
        this.w = w;
        this.h = h;
    }

    public void initialize() {
        data = new int[h * w];

        // Fill data array with pure solid black
        Arrays.fill(data, 0xff000000);

        // Java's endless black magic to get it working
        DataBufferInt db = new DataBufferInt(data, h * w);
        ColorModel cm = ColorModel.getRGBdefault();
        SampleModel sm = cm.createCompatibleSampleModel(w, h);
        WritableRaster wr = Raster.createWritableRaster(sm, db, null);
        buffer = new BufferedImage(cm, wr, false, null);
    }

    @Override
    public void paint(Graphics g) {
        update(g);
    }

    @Override
    public void update(Graphics g) {
        g.drawImage(buffer, 0, 0, null);
    }
}

在此之后,您将获得一个 buffer,您可以在画布更新和底层 ARGB 4 字节整数数组 - data 上进行 blit。

单个字符可以这样画:

private void putChar(int dx, int dy, char ch, int fore, int back) {
    int charIdx = 0;
    int canvasIdx = dy * canvas.w + dx;
    for (int i = 0; i < CHAR_HEIGHT; i++) {
        for (int j = 0; j < CHAR_WIDTH; j++) {
            canvas.data[canvasIdx] = font[ch][charIdx] ? fore : back;
            charIdx++;
            canvasIdx++;
        }
        canvasIdx += canvas.w - CHAR_WIDTH;
    }
}

这个使用一个简单的boolean[][] 数组,其中第一个索引选择字符,第二个索引遍历原始 1 位字符像素数据(true => 前景,false => 背景)。

我将尽快发布一个完整的解决方案,作为我的 Java 终端仿真类集的一部分。

此解决方案以令人印象深刻的 26007 个字符串/秒或 1846553 个字符/秒为基准 - 这比以前最好的非彩色 drawImage() 快 2.3 倍。

【讨论】:

  • "..1846553 chars / sec - 快 2.3 倍.." 有红色条纹吗? ;) 祝贺你成功。当您可以组织起来时,希望看到完整的解决方案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-04-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-11
  • 2012-12-11
相关资源
最近更新 更多