Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
399 views
in Technique[技术] by (71.8m points)

performance - Efficiently color cycling an image in Java

I'm writing a Mandelbrot fractal viewer, and I would like to implement color cycling in a smart way. Given an image, I would like to modify its IndexColorModel.

As far as I can tell, there's no way to modify an IndexColorModel, and there's no way to give an image a new IndexColorModel. In fact, I think there's no way to extract its color model or image data.

It seems that the only solution is to hold on to the raw image data and color palette that were used to create the image, manually create a new palette with the rotated colors, create a new IndexColorModel, then create a whole new image from the data and new color model.

This all seems like too much work. Is there an easier and faster way?

Here's the best solution I can come up with. This code creates a 1000x1000 pixel image and shows an animation of the colors cycling at about 30 frames per second.

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

Edit 2:

Now I precompute the IndexColorModels. This means that on each frame I only need to update the MemoryImageSource with a new IndexColorModel. This seems like the best solution.

(I also just noticed that in my fractal explorer, I can reuse the single set of precomputed IndexColorModels on every image I generate. That means the one-time cost of 140K lets me color cycle everything in real-time. This is great.)

Here's the code:

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

Edit: (old)

Heisenbug suggested that I use the newPixels() method of MemoryImageSource. The answer has since been deleted, but it turned out to be a good idea. Now I only create one MemoryImageSource and one Image. On each frame I create a new IndexColorModel and update the MemoryImageSource.

Here's the updated code: (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 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;
    }
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

In addition to pre-computing the cycles, as @Thomas comments, factor out the magic number 1000. Here's a related example of Changing the ColorModel of a BufferedImage and a project you may like.

Addendum: Factoring out magic numbers will allow you to change them reliably while profiling, which is required to see if you're making progress.

Addendum: While I suggested three color lookup tables per frame, your idea to pre-compute IndexColorModel instances is even better. As an alternative to an array, consider a Queue<IndexColorModel>, with LinkedList<IndexColorModel> as a concrete implementation. This simplifies your model rotation as shown below.

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

Addendum: One more variation to dynamically change the color models and display timing.

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...