Computer art - parte 2
Autor: fernando tenorio | Publicado: 2012-04-09T14:56:00-07:00


No post anterior implementamos o framework básico que nos permite
aplicar o algoritmo a qualquer problema. O desafio agora é encontrar uma maneira
de 'traduzir' um vetor de polígonos (nossa aproximação da imagem) em um cromossomo, e implementar um
método getFitness que nos diga a qualidade de nossa aproximação. Antes disso vamos definir 
algumas classes utilitárias para nos auxiliar com as imagens e com a configuração de parâmetros.




Classe ImageUtils

Essa classe nos ajuda a converter objetos Image em BufferedImage, assim como salvar essas
imagens em formato .jpg. Segue a classe ImageUtils:



import javax.swing.*;
import java.io.*;
import java.util.*;
import java.awt.*;
import java.awt.image.*;
import com.sun.image.codec.jpeg.*;

public class ImageUtils
{
 public static BufferedImage imageToBufferedImage(Image img) 
 {
  BufferedImage bi = new BufferedImage(img.getWidth(null), img.getHeight(null),  
  BufferedImage.TYPE_INT_RGB);
  Graphics2D g2 = bi.createGraphics();
  g2.drawImage(img, null, null);
   
  return bi;
 }
 
 public static void saveJPG(Image img, String filename) 
 {  
  BufferedImage bi = imageToBufferedImage(img);
  FileOutputStream out = null;
  try 
  { 
   out = new FileOutputStream(filename);
  } 
  catch (java.io.FileNotFoundException io) 
  { 
   System.out.println("File Not Found"); 
  }
  
  JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
  JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(bi);
  param.setQuality(1.0f, false); //ou 0.8f 
  encoder.setJPEGEncodeParam(param);
  try 
  { 
   encoder.encode(bi); 
   out.close(); 
  } 
  catch (java.io.IOException io) 
  {
   System.out.println("IOException"); 
  }
 }
 
 public static Image loadJPG(String filename) 
 {
  FileInputStream in = null;
  try 
  { 
   in = new FileInputStream(filename);
  }
  catch (java.io.FileNotFoundException io) 
  { 
   System.out.println("File Not Found"); 
  }
  JPEGImageDecoder decoder = JPEGCodec.createJPEGDecoder(in);
  BufferedImage bi = null;
  try 
  { 
   bi = decoder.decodeAsBufferedImage(); 
   in.close(); 
  } 
  catch (java.io.IOException io) 
  {
   System.out.println("IOException");
  }
  return bi;
 }
 
}


Classe Params

 Essa classe nos permite usar um arquivo externo para configurar alguns parâmetros do programa, como
 o número de polígonos, o nome da imagem, etc. A estrutura do arquivo Params.ini é mostrada
 após a classe:



import java.io.*;
import java.util.HashMap;
import java.util.Scanner;

public class Params 
{
 static int VERTICES = 0;
 static int N_POLY = 0;
 static float MUTATION_RATE = 0.0f;
 static int PHOTO_INTERVAL_SEC = 0;
 static String TARGET_FILE;
 static boolean RANDOMIZE_START = false;
 static int MIN_ALPHA = 0;
 static int MAX_ALPHA = 0;
 
 public Params() 
 { 
  Scanner scanner = new Scanner(this.getClass().getResourceAsStream("params.ini")); 
  HashMap&lt;String, String&gt; map = new HashMap&lt;String, String&gt;();
  
  while (scanner.hasNext()) 
  {
   map.put(scanner.next(), scanner.next());
  }
  
  TARGET_FILE = map.get("target_file");
  VERTICES = Integer.parseInt(map.get("vertices"));
  N_POLY = Integer.parseInt(map.get("n_poly"));    
  MUTATION_RATE = Float.parseFloat(map.get("mutation_rate"));
  PHOTO_INTERVAL_SEC = Integer.parseInt(map.get("photo_interval_sec"));
  RANDOMIZE_START = Boolean.parseBoolean(map.get("randomize_start"));
  MIN_ALPHA = Integer.parseInt(map.get("min_alpha"));
  MAX_ALPHA = Integer.parseInt(map.get("max_alpha"));
 }
 
} //fim da classe


Arquivo params.ini
Salve esse arquivo como 'params.ini', pois a classe Params irá procurar por esse nome
ao tentar carregá-lo:



vertices 3
n_poly 80
mutation_rate 0.001f
photo_interval_sec 30
target_file apple.jpg
randomize_start true
min_alpha 10
max_alpha 120


Classe PolygonColor

 O objetivo agora é transformar um vetor de polígonos em um BinaryIntChromosome e encontrar
 uma maneira de comparar nossa aproximação com a imagem alvo. Para começar, vamos definir a classe
 PolygonColor, que representa um Polígono e sua cor RGBA. A classe é bem simples:



import java.awt.*;

public class PolygonColor
{
 protected Color color;
 protected Polygon polygon;
 
 public PolygonColor(){}
 
 public PolygonColor(Color color, Polygon polygon)
 {
  this.color = color;
  this.polygon = polygon;
 }
}


Classe PolygonDecoder

Essa classe nos ajuda a decodificar um cromossomo em um vetor de polígonos, além de
definir os valores mínimos e máximos que cada gene pode assumir. Esses cálculos dependem
basicamente do número de polígonos utilizados e do número de vértices de cada polígono. 


 O método chromosomeToPolygons recebe o fenótipo de um cromossomo e a configuração dos 
 polígonos, retornando um vetor de objetos PolygonColor. O método getChromosomeBounds
 recebe as dimensões do 'Canvas' e a configuração dos polígonos, nos retornando dois vetores. Esses vetores representam as
 variáveis de instância minVals e maxVals de um cromossomo, sendo utilizados uma única vez ao se instanciar 
 a classe HillClimb:



import java.awt.Color;
import java.awt.Polygon;

public class PolygonDecoder
{
 protected static PolygonColor[] chromosomeToPolygons(int[] data, int verts, int n, int colors)
 {  
  int index = 0;
  int genes = 2 *  verts + colors;
  int dim = genes * n;
  PolygonColor[] polygons = new PolygonColor[n];
  
  int[] x = new int[verts];
  int[] y = new int[verts];
  int[] col = new int[colors];
  
  for (int i = 0; i < dim; i += genes )
  {  
   //vertices
   int start = i;
   int end = start + verts;
   
   //x vertices
   int k = 0;
   for (int m = start; m < end; m++)
    x[k++] = data[m];
   
   start = end;
   end = start + verts;
   
   //y vertices
   k = 0;
   for (int m = start; m < end; m++)
    y[k++] = data[m];
    
   k = 0;
   //color
   for (int c = i + 2 * verts; c < i + 2 * verts + colors; c++)
   {
    col[k++] = data[c]; 
   }
   
   PolygonColor pc = new PolygonColor();
   pc.polygon = new Polygon(x, y, verts);
   pc.color = new Color(col[0], col[1], col[2], col[3]);
            
   polygons[index++] = pc;
  }
  
  return polygons;
 }
 
protected static int[][] getChromosomeBounds(int w, int h, int verts,
int n, int colors,int minAlpha, int maxAlpha)
 {    
  int genes = 2 *  verts + colors;
  int dim = genes * n;
  int[] minV = new int[dim];
  int[] maxV = new int[dim];
  
  for (int i = 0; i < dim; i += genes )
  {   
   int start = i;
   int end = start + verts;
   
   //x vertices
   for (int m = start; m < end; m++)
    maxV[m] = w;
   
   start = end;
   end = start + verts;
   
   //y vertices
   for (int m = start; m < end; m++)
    maxV[m] = h;
          
   //color
   for (int c = i + 2 * verts; c < i + 2 * verts + colors; c++)
   {    
    //alpha component
    if (c == i + 2 * verts + colors - 1)
    {
     minV[c] = minAlpha;
     maxV[c] = maxAlpha;         
    }
    //rgb component 
    else  
     maxV[c] = 255;   
   }
  }
  
  return new int[][]{minV, maxV};
 }
   
} //fim da classe


Classe EvolveDraw

 Essa é a classe que executa o nosso programa, tentando aproximar a imagem especificada em 
 dentro do arquivo params.ini. Para obter bons resultados é fundamental usar parâmetros sensíveis, que funcionem
 bem em conjunto:
 
  vertices - Valores entre 3 e 6 parecem funcionar bem.
  
  n_poly - Dependendo da complexidade da imagem, valores entre 50 e 300 funcionam bem, porém o consumo de cpu aumenta bastante ao se utilizar um número grande de polígonos.
  
  mutation_rate - Esse é talvez o parâmetro mais sensível. Valores muito baixos ou muito altos não funcionam bem, uma valor razoável é 1.0/((vertices + 4) * n_poly).
  
  photo_interval_sec - O intervalo em segundos em que uma nova imagem da aproximação é salva. Caso não queira salvar as imagens, especifique um valor alto, ou comente a chamada ao método checkSnap e recompile.

  target_file - A imagem alvo, em formato .jpg.
  
  randomize_start - Especifica se o cromossomo inicial é randomizado ao se iniciar o algoritmo. Ao passar false, teremos um 'Canvas' inicialmente vazio. Passar true parece ajudar na velocidade de convergência, mas não estou certo disso. 
  
  minAlpha, maxAlpha - O valor mínimo e máximo do componente alpha da cor, respectivamente. O normal aqui seria especificar 0 e 255, mas podemos cortar um pouco o espaço de busca ao diminuir esse range. Valores entre 0 e 10 para minAlpha e entre 100 e 150 para maxAlpha parecem funcionar bem.
  



 A classe ficou um pouco grande, mas a idéia é bem simples: invocamos o método evolve em um laço infinito
  e decodificamos o cromossomo atual, que em seguida é desenhado na tela.
 Repare que implementamos a interface FitnessDelegate, onde novamente decodificamos o cromossomo em um vetor de
 polígonos, desenhamos esses polígonos em um buffer e fazemos uma comparação pixel-a-pixel com a imagem alvo. 



 Repare também que a imagem alvo é escaneada uma única vez no método loadTarget, e seus componentes RGBA são salvos 
 em arrays separadas. Por fim, o método checkSnap cria uma imagem da tela e a salva no diretório atual, caso o intervalo
 especificado em photo_interval_sec tenha passado. Segue a classe EvolveDraw:



import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import com.sun.image.codec.jpeg.*;
import javax.swing.*;
import java.util.concurrent.*;
import java.util.*;
import java.io.*;

public class EvolveDraw extends JPanel implements Runnable, FitnessDelegate
{
 //polygons
 protected final int VERTICES;
 protected final int N_POLY;
 
 //nao mude COLOR_COMP!
 protected final int COLOR_COMP = 4;
 protected final int MIN_ALPHA;
 protected final int MAX_ALPHA;
 protected PolygonColor[] polygons;
 protected int gen = 0;
 protected Color bgColor = Color.white;
 
 //algorithm params
 protected float MUTATION_RATE;
 protected BinaryIntChromosome best;
 protected boolean RANDOMIZE_START;
 
 //images
 protected final String TARGET_FILE;
 protected int[] offImagePixels; 
 protected BufferedImage targetImage;
 protected BufferedImage offImage; 
 protected int[] targetPixels;
 protected int[] redTarget;
 protected int[] greenTarget;
 protected int[] blueTarget;
 protected int[] alphaTarget;
 protected int w;
 protected int h;
 
 //save drawings
 protected final int PHOTO_INTERVAL_SEC;
    protected long lastPhotoTime = 0L;
 
 public EvolveDraw()
 {
  new Params();
  TARGET_FILE = Params.TARGET_FILE;
  VERTICES = Params.VERTICES;
  N_POLY = Params.N_POLY;    
  MUTATION_RATE = Params.MUTATION_RATE;
  PHOTO_INTERVAL_SEC = Params.PHOTO_INTERVAL_SEC;
  RANDOMIZE_START = Params.RANDOMIZE_START;
  MIN_ALPHA = Params.MIN_ALPHA;
  MAX_ALPHA = Params.MAX_ALPHA;
  
  this.setDoubleBuffered(true);
  this.setBackground(bgColor);
  this.loadTarget();
 }
 
 private void loadTarget()
 {
  this.targetImage = ImageUtils.imageToBufferedImage(ImageUtils.loadJPG(TARGET_FILE));
  this.w = targetImage.getWidth();
  this.h = targetImage.getHeight();
  this.setPreferredSize(new Dimension(w, h));
  
  this.offImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);  
  this.targetPixels = new int[w * h];
  this.offImagePixels = new int[w * h];
  this.targetImage.getRGB(0, 0, w, h, targetPixels, 0, w);
  
  this.redTarget = new int[w * h];
  this.greenTarget = new int[w * h];
  this.blueTarget = new int[w * h];
  this.alphaTarget = new int[w * h];
    
  for (int i = 0; i < this.targetPixels.length; i++)
  {
   int pixel = targetPixels[i];
   int alpha = (pixel >> 24) & 0xff;
   int red = (pixel >> 16) & 0xff;
   int green = (pixel >> 8) & 0xff;
   int blue = (pixel) & 0xff;

   this.redTarget[i] = red;
   this.greenTarget[i] = green;
   this.blueTarget[i] = blue;
   this.alphaTarget[i] = alpha;
  } 
 }
 
 public void run()
 { 
 
  int[][] bounds = PolygonDecoder.getChromosomeBounds(w, h,
  VERTICES, N_POLY, COLOR_COMP, MIN_ALPHA, MAX_ALPHA);
  HillClimb evolution = new HillClimb(bounds[0], bounds[1], this,  MUTATION_RATE, null, RANDOMIZE_START);
   
  do
  {
   CountDownLatch signal = new CountDownLatch(1);
   evolution.signal = signal;
   
   new Thread(evolution).start();
   try 
   {
    signal.await();
   }
   catch(InterruptedException ex)
   {
    ex.printStackTrace();
   }   
    
   best = evolution.getBest();     
   polygons = PolygonDecoder.chromosomeToPolygons(best.fenotype, VERTICES, N_POLY, COLOR_COMP);
   repaint();
        
   gen++;
   checkSnap();
      
  } while (true);
   
 }
   
 public void paintComponent(Graphics g)
 {
  super.paintComponent(g);
  if (best == null)
   return;     
   
   ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
   RenderingHints.VALUE_ANTIALIAS_ON);
   
   for (PolygonColor pc : polygons)
   {
    g.setColor(pc.color);
    g.fillPolygon(pc.polygon);
   }      
 }
 
 public BufferedImage createImage(JPanel panel) 
 {
  int wid = panel.getWidth();
  int heig = panel.getHeight();
  BufferedImage bi = new BufferedImage(wid, heig, BufferedImage.TYPE_INT_ARGB);
  Graphics2D g = bi.createGraphics();
  g.setBackground(bgColor);
  g.clearRect(0, 0, wid, heig);
  panel.paint(g);
  g.dispose();
    
  return bi;
 }
 
 protected void checkSnap()
 {
  long time = System.currentTimeMillis();
  long interval  = time - lastPhotoTime;
  
  if (interval >= PHOTO_INTERVAL_SEC * 1000L)
  {
   this.lastPhotoTime = time;
   ImageUtils.saveJPG(createImage(this), TARGET_FILE + gen + ".jpg");
  }
 }
 
 public float getFitness(int[] fenotype)
 {
  Graphics2D g = this.offImage.createGraphics();
  g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
  RenderingHints.VALUE_ANTIALIAS_ON);
  g.setBackground(bgColor);
  g.clearRect(0, 0, w, h);       
          
  PolygonColor[] polygons = PolygonDecoder.chromosomeToPolygons(fenotype, VERTICES, N_POLY,  COLOR_COMP);
  for (PolygonColor pc : polygons)
  {
   g.setColor(pc.color);
   g.fillPolygon(pc.polygon);
  }
  
  g.dispose();
  this.offImage.getRGB(0, 0, w, h, offImagePixels, 0, w);
        
  float diff = 0.0f;
  for (int i = 0; i < offImagePixels.length; i++)
  {
   int pixelOff = offImagePixels[i];
   int alphaOff = (pixelOff >> 24) & 0xff;
   int redOff = (pixelOff >> 16) & 0xff;
   int greenOff = (pixelOff >> 8) & 0xff;
   int blueOff = (pixelOff) & 0xff;
   
   int alp = alphaTarget[i] - alphaOff;
   int red = redTarget[i] - redOff;
   int blue = blueTarget[i] - blueOff;
   int green = greenTarget[i] - greenOff;
   
   diff += alp * alp + red * red + blue * blue + green * green;                       
  }
        
   return 1.0f/(diff + 1.0f);
 }
  
 //executa
 public static void main (String[] args)
 {
  JFrame frame = new JFrame("Evolve Draw");
  final EvolveDraw panel = new EvolveDraw();         
  frame.getContentPane().add(panel);
  frame.pack();
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  frame.setVisible(true);  
  frame.setResizable(false);
    
  Thread runnable = new Thread(panel);  
  runnable.start();  
 }
 
} //fim da classe



Usamos uma abordagem bem simples, mas que ilustra bem o poder de um algoritmo evolucionário.
Uma idéia para tentar melhorar o programa seria usar um vetor de polígonos onde o número de vértices não é mantido fixo, isso provavelmente iria acelerar a convergência. Outra idéia consiste em 'injetar' os polígonos pouco a pouco, de acordo com alguma regra que leve em conta a taxa de aproximação. 



Era isso, até o próximo post!