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

Posted

技术标签:

【中文标题】在 Java 中有效地对图像进行颜色循环【英文标题】:Efficiently color cycling an image in Java 【发布时间】:2011-11-24 13:34:19 【问题描述】:

我正在编写一个 Mandelbrot 分形查看器,我想以一种智能的方式实现颜色循环。给定一张图片,我想修改它的 IndexColorModel。

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

似乎唯一的解决方案是保留用于创建图像的原始图像数据和调色板,手动创建一个带有旋转颜色的新调色板,创建一个新的 IndexColorModel,然后创建一个全新的图像来自数据和新的颜色模型。

这一切看起来工作量太大了。有没有更简单快捷的方法?

这是我能想到的最佳解决方案。此代码创建一个 1000x1000 像素的图像,并显示颜色以每秒 30 帧左右的速度循环的动画。

(旧)

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。这似乎是最好的解决方案。

(我还刚刚注意到,在我的分形资源管理器中,我可以在生成的每张图像上重复使用一组预先计算的 IndexColorModel。这意味着 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;
    

【问题讨论】:

如何预先计算一个循环然后为图像设置动画? @thomas 上面的代码示例以 1000x1000 像素显示 216 帧。计算的帧每个像素使用 4 个字节。那是 864 MB。我已经尝试过了,我现在特别避免它。 不要预先计算所有帧,只做三个 cluts:3 * 216 * 216 = ~140K 为sscce+1。 @trashgod 你的意思是我应该预先计算 216 个 IndexColorModels,然后为每一帧选择一个使用? 【参考方案1】:

除了预先计算循环之外,作为@Thomas cmets,还可以分解出幻数 1000。下面是您可能喜欢的 Changing the ColorModel of a BufferedImage 和 project 的相关示例。

附录:分解出 magic numbers 将允许您在分析时可靠地更改它们,这是查看您是否取得进展所必需的。

附录:虽然我建议每帧使用三个颜色查找表,但您预先计算 IndexColorModel 实例的想法更好。作为数组的替代方案,可以考虑使用Queue&lt;IndexColorModel&gt;,将LinkedList&lt;IndexColorModel&gt; 作为具体实现。这简化了您的模型旋转,如下所示。

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

附录:动态更改颜色模型和显示时间的另一种变体。

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://***.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”是什么意思? 我上面已经详细说明了; 216 是另一位候选人。 对不起所有的神奇数字。我认为变量会使代码复杂化,但我想它们是必要的。 感谢您修复神奇数字。在代码变体接近尾声时,newColors[0] = colors[215]; 应该是 newColors[0] = colors[MODEL_SIZE - 1]; @nIcEcOw:遗憾的是,我更新了代码,但没有更新 cmets! :-)【参考方案2】:

我会使用带有 Mandelbrot 像素着色器的 LWJGL(OpenGL 接口到 Java),并在着色器中进行颜色循环。比使用 Java2D 高效得多。

http://nuclear.mutantstargoat.com/articles/sdr_fract/

【讨论】:

非常相关和一个好主意,但我编写这个程序是为了体验 Swing 和图像处理。不过,谢谢。

以上是关于在 Java 中有效地对图像进行颜色循环的主要内容,如果未能解决你的问题,请参考以下文章

如何将固定颜色条添加到 3D 散点动画图并相应地对点进行颜色映射?

获取图像最常见的颜色

在循环中更改 div 元素的背景颜色交叉淡入淡出

绘制大量不同颜色的正方形

使用位深度来确定 BMP 图像中颜色表的大小是不是有效?

如何在处理中加载的 png 图像上创建颜色剪贴蒙版?