Eficazmente el ciclo de color de una imagen en Java

Estoy escribiendo un visor de fractales de Mandelbrot y me gustaría implementar el ciclo de colores de una manera inteligente. Dada una imagen, me gustaría modificar su IndexColorModel.

Por lo que puedo decir, no hay forma de modificar un IndexColorModel, y no hay forma de darle a una imagen un nuevo IndexColorModel. De hecho, creo que no hay forma de extraer su modelo de color o datos de imagen.

Parece que la única solución es aferrarse a los datos de imágenes en bruto y la paleta de colores que se usaron para crear la imagen, crear manualmente una nueva paleta con los colores girados, crear un nuevo IndexColorModel y luego crear una imagen completamente nueva a partir de los datos y nuevo modelo de color.

Todo esto parece demasiado trabajo. ¿Hay una manera más fácil y más rápida?

Esta es la mejor solución que puedo encontrar. Este código crea una imagen de 1000×1000 píxeles y muestra una animación de los colores que recorren alrededor de 30 fotogtwigs por segundo.

(antiguo)

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

Editar 2:

Ahora precomputo los IndexColorModels. Esto significa que en cada marco solo necesito actualizar MemoryImageSource con un nuevo IndexColorModel. Esta parece ser la mejor solución.

(También me di cuenta de que en mi explorador de fractales, puedo reutilizar el único conjunto de IndexColorModels precalculado en cada imagen que genero. Eso significa que el costo único de 140K me permite hacer ciclos de todo en tiempo real. Esto es genial).

Aquí está el código:

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

Editar: (viejo)

Heisenbug sugirió que use el nuevo método Pixels () de MemoryImageSource. La respuesta ha sido eliminada, pero resultó ser una buena idea. Ahora solo creo un MemoryImageSource y una Imagen. En cada marco, creo un nuevo IndexColorModel y actualizo el MemoryImageSource.

Aquí está el código actualizado: (viejo)

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

Además de calcular previamente los ciclos, como comenta @Thomas, tenga en cuenta el número mágico 1000. Aquí hay un ejemplo relacionado de Cambio del modelo de color de una imagen almacenada y un proyecto que le puede gustar.

Adición: factorizar los números mágicos te permitirá cambiarlos de manera confiable mientras se realizan los perfiles, lo cual es necesario para ver si estás progresando.

Adición: Si bien sugerí tres tablas de búsqueda de color por cuadro, su idea de IndexColorModel instancias de IndexColorModel es aún mejor. Como alternativa a una matriz, considere una Queue , con LinkedList como una implementación concreta. Esto simplifica la rotación de su modelo como se muestra a continuación.

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

Adición: Una variación más para cambiar dinámicamente los modelos de color y mostrar el tiempo.

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 models = new LinkedList(); 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(); } } } 

Utilizaría LWJGL (interfaz OpenGL para Java) con un sombreador de píxeles Mandelbrot y realizaría el ciclo de colores en el sombreado. Mucho más eficiente que usar Java2D.

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