在Java中高效地对图像进行颜色循环

13

我正在编写一个曼德勃罗特分形查看器,并希望以聪明的方式实现颜色循环。给定一张图像,我想修改它的IndexColorModel。

据我所知,没有办法修改IndexColorModel,也没有办法为图像提供新的IndexColorModel。事实上,我认为没有办法提取它的颜色模型或图像数据。

似乎唯一的解决办法是保留用于创建图像的原始图像数据和调色板,手动创建一个具有旋转颜色的新调色板,创建一个新的IndexColorModel,然后从数据和新调色板创建全新的图像。

这一切看起来太麻烦了。有更简单、更快速的方法吗?

这是我能想到的最好的解决方案。这段代码创建了一个1000x1000像素的图像,并显示了每秒约30帧的颜色循环动画。

(old)

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;

public class ColorCycler {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI() {
        JFrame jFrame = new JFrame("Color Cycler");
        jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        jFrame.add(new MyPanel());
        jFrame.pack();
        jFrame.setVisible(true);
    }

}

class MyPanel extends JPanel implements ActionListener {

    private byte[] reds = new byte[216];
    private byte[] greens = new byte[216];
    private byte[] blues = new byte[216];
    private final byte[] imageData = new byte[1000 * 1000];
    private Image image;

    public MyPanel() {
        generateColors();
        generateImageData();
        (new Timer(35, this)).start();
    }

    // The window size is 1000x1000 pixels.
    public Dimension getPreferredSize() {
        return new Dimension(1000, 1000);
    }

    // Generate 216 unique colors for the color model.
    private void generateColors() {
        int index = 0;
        for (int i = 0; i < 6; i++) {
            for (int j = 0; j < 6; j++) {
                for (int k = 0; k < 6; k++, index++) {
                    reds[index] = (byte) (i * 51);
                    greens[index] = (byte) (j * 51);
                    blues[index] = (byte) (k * 51);
                }
            }
        }
    }

    // Create the image data for the MemoryImageSource.
    // This data is created once and never changed.
    private void generateImageData() {
        for (int i = 0; i < 1000 * 1000; i++) {
            imageData[i] = (byte) (i % 216);
        }
    }

    // Draw the image.
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawImage(image, 0, 0, 1000, 1000, null);
    }

    // This method is called by the timer every 35 ms.
    // It creates the modified image to be drawn.
    @Override
    public void actionPerformed(ActionEvent e) { // Called by Timer.
        reds = cycleColors(reds);
        greens = cycleColors(greens);
        blues = cycleColors(blues);
        IndexColorModel colorModel = new IndexColorModel(8, 216, reds, greens, blues);
        image = createImage(new MemoryImageSource(1000, 1000, colorModel, imageData, 0, 1000));
        repaint();
    }

    // Cycle the colors to the right by 1.
    private byte[] cycleColors(byte[] colors) {
        byte[] newColors = new byte[216];
        newColors[0] = colors[215];
        System.arraycopy(colors, 0, newColors, 1, 215);
        return newColors;
    }
}

编辑2:

现在我预计算IndexColorModels。这意味着在每个帧上,我只需要使用新的IndexColorModel更新MemoryImageSource。这似乎是最好的解决方案。

(我还刚注意到,在我的分形浏览器中,我可以重复使用单个预计算的IndexColorModels来生成每个图像。这意味着140K的一次性成本让我能够实时地循环颜色。这很棒。)

这是代码:

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;

public class ColorCycler {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI() {
        JFrame jFrame = new JFrame("Color Cycler");
        jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        jFrame.add(new MyPanel());
        jFrame.pack();
        jFrame.setVisible(true);
    }

}

class MyPanel extends JPanel implements ActionListener {

    private final IndexColorModel[] colorModels = new IndexColorModel[216];
    private final byte[] imageData = new byte[1000 * 1000];
    private final MemoryImageSource imageSource;
    private final Image image;
    private int currentFrame = 0;

    public MyPanel() {
        generateColorModels();
        generateImageData();
        imageSource = new MemoryImageSource(1000, 1000, colorModels[0], imageData, 0, 1000);
        imageSource.setAnimated(true);
        image = createImage(imageSource);
        (new Timer(35, this)).start();
    }

    // The window size is 1000x1000 pixels.
    public Dimension getPreferredSize() {
        return new Dimension(1000, 1000);
    }

    // Generate 216 unique colors models, one for each frame.
    private void generateColorModels() {
        byte[] reds = new byte[216];
        byte[] greens = new byte[216];
        byte[] blues = new byte[216];
        int index = 0;
        for (int i = 0; i < 6; i++) {
            for (int j = 0; j < 6; j++) {
                for (int k = 0; k < 6; k++, index++) {
                    reds[index] = (byte) (i * 51);
                    greens[index] = (byte) (j * 51);
                    blues[index] = (byte) (k * 51);
                }
            }
        }
        for (int i = 0; i < 216; i++) {
            colorModels[i] = new IndexColorModel(8, 216, reds, greens, blues);
            reds = cycleColors(reds);
            greens = cycleColors(greens);
            blues = cycleColors(blues);
        }
    }

    // Create the image data for the MemoryImageSource.
    // This data is created once and never changed.
    private void generateImageData() {
        for (int i = 0; i < 1000 * 1000; i++) {
            imageData[i] = (byte) (i % 216);
        }
    }

    // Draw the image.
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawImage(image, 0, 0, 1000, 1000, null);
    }

    // This method is called by the timer every 35 ms.
    // It updates the ImageSource of the image to be drawn.
    @Override
    public void actionPerformed(ActionEvent e) { // Called by Timer.
        currentFrame++;
        if (currentFrame == 216) {
            currentFrame = 0;
        }
        imageSource.newPixels(imageData, colorModels[currentFrame], 0, 1000);
        repaint();
    }

    // Cycle the colors to the right by 1.
    private byte[] cycleColors(byte[] colors) {
        byte[] newColors = new byte[216];
        newColors[0] = colors[215];
        System.arraycopy(colors, 0, newColors, 1, 215);
        return newColors;
    }
}

编辑:(旧版)

Heisenbug建议我使用MemoryImageSource的newPixels()方法。答案已被删除,但这是个好主意。现在我只创建一个MemoryImageSource和一个Image。每帧我都会创建一个新的IndexColorModel并更新MemoryImageSource。

以下是更新后的代码:(旧版)

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;

public class ColorCycler {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI() {
        JFrame jFrame = new JFrame("Color Cycler");
        jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        jFrame.add(new MyPanel());
        jFrame.pack();
        jFrame.setVisible(true);
    }

}

class MyPanel extends JPanel implements ActionListener {

    private byte[] reds = new byte[216];
    private byte[] greens = new byte[216];
    private byte[] blues = new byte[216];
    private final byte[] imageData = new byte[1000 * 1000];
    private final MemoryImageSource imageSource;
    private final Image image;

    public MyPanel() {
        generateColors();
        generateImageData();
        IndexColorModel colorModel = new IndexColorModel(8, 216, reds, greens, blues);
        imageSource = new MemoryImageSource(1000, 1000, colorModel, imageData, 0, 1000);
        imageSource.setAnimated(true);
        image = createImage(imageSource);
        (new Timer(35, this)).start();
    }

    // The window size is 1000x1000 pixels.
    public Dimension getPreferredSize() {
        return new Dimension(1000, 1000);
    }

    // Generate 216 unique colors for the color model.
    private void generateColors() {
        int index = 0;
        for (int i = 0; i < 6; i++) {
            for (int j = 0; j < 6; j++) {
                for (int k = 0; k < 6; k++, index++) {
                    reds[index] = (byte) (i * 51);
                    greens[index] = (byte) (j * 51);
                    blues[index] = (byte) (k * 51);
                }
            }
        }
    }

    // Create the image data for the MemoryImageSource.
    // This data is created once and never changed.
    private void generateImageData() {
        for (int i = 0; i < 1000 * 1000; i++) {
            imageData[i] = (byte) (i % 216);
        }
    }

    // Draw the image.
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawImage(image, 0, 0, 1000, 1000, null);
    }

    // This method is called by the timer every 35 ms.
    // It updates the ImageSource of the image to be drawn.
    @Override
    public void actionPerformed(ActionEvent e) { // Called by Timer.
        reds = cycleColors(reds);
        greens = cycleColors(greens);
        blues = cycleColors(blues);
        IndexColorModel colorModel = new IndexColorModel(8, 216, reds, greens, blues);
        imageSource.newPixels(imageData, colorModel, 0, 1000);
        repaint();
    }

    // Cycle the colors to the right by 1.
    private byte[] cycleColors(byte[] colors) {
        byte[] newColors = new byte[216];
        newColors[0] = colors[215];
        System.arraycopy(colors, 0, newColors, 1, 215);
        return newColors;
    }
}

3
预先计算一个循环,然后再通过动画进行图像展示怎么样? - Thomas Jungblut
@thomas 上面的代码示例显示了216帧,分辨率为1000x1000像素。计算一个帧需要每个像素4字节。总共需要864 MB。我尝试过这个,但现在明确地避免使用它。 - dln385
2
不要预先计算所有帧,只需完成三个Clut:3 * 216 * 216 = 约140K。 - trashgod
1
+1 for sscce - trashgod
@Heisenbug:如果您恢复了证明有用的答案,请通知我。 - trashgod
显示剩余2条评论
2个回答

8

除了像@ Thomas评论中那样预计算周期,还要将神奇数字1000因素化。这里是一个相关的例子更改BufferedImage的ColorModel以及一个项目,您可能会喜欢。

附加说明:将神奇数字因式分解将使您能够可靠地更改它们,同时进行分析,这是必要的,以查看您是否正在取得进展。

添加说明:虽然我建议每个帧使用三个颜色查找表,但您的想法是预先计算IndexColorModel实例更好。作为数组的替代选择,请考虑Queue<IndexColorModel>,其中LinkedList<IndexColorModel>是一种具体实现。如下所示,这简化了您的模型旋转。

@Override
public void actionPerformed(ActionEvent e) { // Called by Timer.
    imageSource.newPixels(imageData, models.peek(), 0, N);
    models.add(models.remove());
    repaint();
}

附加说明:一种用于动态更改颜色模型和显示时间的变化。 enter image description here
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.IndexColorModel;
import java.awt.image.MemoryImageSource;
import java.util.LinkedList;
import java.util.Queue;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

/** @see http://stackoverflow.com/questions/7546025 */
public class ColorCycler {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                new ColorCycler().create();
            }
        });
    }

    private void create() {
        JFrame jFrame = new JFrame("Color Cycler");
        jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        final ColorPanel cp = new ColorPanel();
        JPanel control = new JPanel();
        final JSpinner s = new JSpinner(
            new SpinnerNumberModel(cp.colorCount, 2, 256, 1));
        s.addChangeListener(new ChangeListener() {

            @Override
            public void stateChanged(ChangeEvent e) {
                cp.setColorCount(((Integer) s.getValue()).intValue());
            }
        });
        control.add(new JLabel("Shades:"));
        control.add(s);
        jFrame.add(cp, BorderLayout.CENTER);
        jFrame.add(control, BorderLayout.SOUTH);
        jFrame.pack();
        jFrame.setLocationRelativeTo(null);
        jFrame.setVisible(true);
    }

    private static class ColorPanel extends JPanel implements ActionListener {

        private static final int WIDE = 256;
        private static final int PERIOD = 40; // ~25 Hz
        private final Queue<IndexColorModel> models =
            new LinkedList<IndexColorModel>();
        private final MemoryImageSource imageSource;
        private final byte[] imageData = new byte[WIDE * WIDE];
        private final Image image;
        private int colorCount = 128;

        public ColorPanel() {
            generateColorModels();
            generateImageData();
            imageSource = new MemoryImageSource(
                WIDE, WIDE, models.peek(), imageData, 0, WIDE);
            imageSource.setAnimated(true);
            image = createImage(imageSource);
            (new Timer(PERIOD, this)).start();
        }

        // The preferred size is NxN pixels.
        @Override
        public Dimension getPreferredSize() {
            return new Dimension(WIDE, WIDE);
        }

        public void setColorCount(int colorCount) {
            this.colorCount = colorCount;
            generateColorModels();
            generateImageData();
            repaint();
        }

        // Generate MODEL_SIZE unique color models.
        private void generateColorModels() {
            byte[] reds = new byte[colorCount];
            byte[] greens = new byte[colorCount];
            byte[] blues = new byte[colorCount];
            for (int i = 0; i < colorCount; i++) {
                reds[i] = (byte) (i * 256 / colorCount);
                greens[i] = (byte) (i * 256 / colorCount);
                blues[i] = (byte) (i * 256 / colorCount);
            }
            models.clear();
            for (int i = 0; i < colorCount; i++) {
                reds = rotateColors(reds);
                greens = rotateColors(greens);
                blues = rotateColors(blues);
                models.add(new IndexColorModel(
                    8, colorCount, reds, greens, blues));
            }
        }

        // Rotate colors to the right by one.
        private byte[] rotateColors(byte[] colors) {
            byte[] newColors = new byte[colors.length];
            newColors[0] = colors[colors.length - 1];
            System.arraycopy(colors, 0, newColors, 1, colors.length - 1);
            return newColors;
        }

        // Create some data for the MemoryImageSource.
        private void generateImageData() {
            for (int i = 0; i < imageData.length; i++) {
                imageData[i] = (byte) (i % colorCount);
            }
        }

        // Draw the image.
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            long start = System.nanoTime();
            imageSource.newPixels(imageData, models.peek(), 0, WIDE);
            models.add(models.remove());
            double delta = (System.nanoTime() - start) / 1000000d;
            g.drawImage(image, 0, 0, getWidth(), getHeight(), null);
            g.drawString(String.format("%1$5.3f", delta), 5, 15);
        }

        // Called by the Timer every PERIOD ms.
        @Override
        public void actionPerformed(ActionEvent e) { // Called by Timer.
            repaint();
        }
    }
}

你说的“将魔术数字1000提取出来”是什么意思? - dln385
我已经详细说明了; 216 是另一个候选项。 - trashgod
1
对于那些不可读的数字,很抱歉。我曾以为使用变量会使代码复杂,但现在看来它们是必要的。 - dln385
感谢您修复魔数。在您的代码变体接近结尾处,newColors [0] = colors [215]; 应更改为 newColors [0] = colors [MODEL_SIZE-1]; - dln385
哇 :) 这看起来很棒,非常像那个地铁示例,不过观看起来更有趣。太棒了 :-) - nIcE cOw
1
@nIcEcOw: 遗憾的是,我更新了代码,但没更新注释! :-) - trashgod

2

非常相关且是个好主意,但我写这个程序是为了在Swing和图像处理方面积累经验。不过还是谢谢你的建议。 - dln385

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