diff --git a/src/userguide/java/org/apache/commons/math3/userguide/genetics/ImageEvolutionExample.java b/src/userguide/java/org/apache/commons/math3/userguide/genetics/ImageEvolutionExample.java index ba7a019df..f89740070 100644 --- a/src/userguide/java/org/apache/commons/math3/userguide/genetics/ImageEvolutionExample.java +++ b/src/userguide/java/org/apache/commons/math3/userguide/genetics/ImageEvolutionExample.java @@ -9,6 +9,7 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; import java.io.File; +import java.io.IOException; import java.util.LinkedList; import java.util.List; @@ -16,8 +17,6 @@ import javax.imageio.ImageIO; import javax.swing.Box; import javax.swing.ImageIcon; import javax.swing.JButton; -import javax.swing.JComponent; -import javax.swing.JFrame; import javax.swing.JLabel; import org.apache.commons.math3.genetics.Chromosome; @@ -26,14 +25,34 @@ import org.apache.commons.math3.genetics.GeneticAlgorithm; import org.apache.commons.math3.genetics.Population; import org.apache.commons.math3.genetics.TournamentSelection; import org.apache.commons.math3.genetics.UniformCrossover; +import org.apache.commons.math3.userguide.ExampleUtils; +import org.apache.commons.math3.userguide.ExampleUtils.ExampleFrame; /** - * Based on http://www.nihilogic.dk/labs/evolving-images/ + * This example shows a more advanced use of a genetic algorithm: approximate a raster image + * with ~100 semi-transparent polygons of length 6. + *

+ * The fitness function is quite simple yet expensive to compute: + * + * - draw the polygons of a chromosome to an image + * - compare each pixel with the corresponding reference image + *

+ * To improve the speed of the calculation, we calculate the fitness not on the original image size, + * but rather on a scaled down version, which is sufficient to demonstrate the power of such a genetic algorithm. + *

+ * TODO: + * - improve user interface + * - make algorithm parameters configurable + * - add a gallery of results after x iterations / minutes (either automatic or based on button click) + * - allow loading / selection of other images + * - add logging in the user interface, e.g. number of generations, time spent, ... + * + * @see Evolving Images with JavaScript and canvas (Nihilogic) */ @SuppressWarnings("serial") -public class ImageEvolutionExample extends JComponent { +public class ImageEvolutionExample { - public static final int POPULATION_SIZE = 40; + public static final int POPULATION_SIZE = 50; public static final int TOURNAMENT_ARITY = 2; public static final float MUTATION_RATE = 0.02f; public static final float MUTATION_CHANGE = 0.1f; @@ -41,65 +60,160 @@ public class ImageEvolutionExample extends JComponent { public static final int POLYGON_LENGTH = 6; public static final int POLYGON_COUNT = 100; - private GeneticAlgorithm ga; - private Population currentPopulation; - private Chromosome bestFit; - - private Thread internalThread; - private volatile boolean noStopRequested; - - private BufferedImage ref; - private BufferedImage buf; - - private ImagePainter painter; - - public ImageEvolutionExample() throws Exception { - super(); - setLayout(new FlowLayout()); - - Box bar = Box.createHorizontalBox(); - - ref = ImageIO.read(new File("resources/canvas_small.png")); - - JLabel picLabel = new JLabel(new ImageIcon(ref)); - bar.add(picLabel); - - painter = new ImagePainter(ref); - bar.add(painter); - - buf = new BufferedImage(ref.getWidth(), ref.getHeight(), BufferedImage.TYPE_INT_ARGB); + public static class Display extends ExampleFrame { - // TODO: improve this - PolygonChromosome.setRefImage(ref); - PolygonChromosome.setTestImage(buf); + private GeneticAlgorithm ga; + private Population currentPopulation; + private Chromosome bestFit; - add(bar); + private Thread internalThread; + private volatile boolean noStopRequested; - JButton startButton = new JButton("Start"); - startButton.setActionCommand("start"); - add(startButton); + private BufferedImage ref; + + private BufferedImage referenceImage; + private BufferedImage testImage; + + private ImagePainter painter; - startButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - if (isAlive()) { - stopRequest(); - } else { - startEvolution(); + public Display() throws Exception { + setTitle("Commons-Math: Image Evolution Example"); + setSize(600, 400); + + setLayout(new FlowLayout()); + + Box bar = Box.createHorizontalBox(); + + ref = ImageIO.read(new File("resources/monalisa.png")); + //ref = ImageIO.read(new File("resources/feather-small.gif")); + + referenceImage = resizeImage(ref, 50, 50, BufferedImage.TYPE_INT_ARGB); + testImage = new BufferedImage(referenceImage.getWidth(), referenceImage.getHeight(), BufferedImage.TYPE_INT_ARGB); + + JLabel picLabel = new JLabel(new ImageIcon(ref)); + bar.add(picLabel); + + painter = new ImagePainter(ref.getWidth(), ref.getHeight()); + bar.add(painter); + + // set the images used for calculating the fitness function: + // refImage - the reference image + // testImage - the test image to draw the current chromosome + PolygonChromosome.setRefImage(referenceImage); + PolygonChromosome.setTestImage(testImage); + + add(bar); + + JButton startButton = new JButton("Start"); + startButton.setActionCommand("start"); + add(startButton); + + startButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + if (isAlive()) { + stopRequest(); + } else { + startEvolution(); + } } + }); + + // initialize a new genetic algorithm + ga = new GeneticAlgorithm(new UniformCrossover(0.5), 1.0, + new RandomPolygonMutation(MUTATION_RATE, MUTATION_CHANGE), 1.0, + new TournamentSelection(TOURNAMENT_ARITY)); + + // initial population + currentPopulation = getInitialPopulation(); + bestFit = currentPopulation.getFittestChromosome(); + } + + public boolean isAlive() { + return internalThread != null && internalThread.isAlive(); + } + + public void stopRequest() { + noStopRequested = false; + internalThread.interrupt(); + internalThread = null; + } + + public void startEvolution() { + noStopRequested = true; + Runnable r = new Runnable() { + public void run() { + try { + double lastBestFit = Double.MIN_VALUE; + int evolution = 0; + while (noStopRequested) { + currentPopulation = ga.nextGeneration(currentPopulation); + + System.out.println("generation: " + evolution++ + ": " + bestFit.toString()); + bestFit = currentPopulation.getFittestChromosome(); + + if (lastBestFit > bestFit.getFitness()) { + System.out.println("gotcha"); + } + lastBestFit = bestFit.getFitness(); + painter.repaint(); + } + } catch (Exception x) { + // in case ANY exception slips through + x.printStackTrace(); + } + } + }; + + internalThread = new Thread(r); + internalThread.start(); + } + + private class ImagePainter extends Component { + + private int width; + private int height; + + public ImagePainter(int width, int height) { + this.width = width; + this.height = height; } - }); - // initialize a new genetic algorithm - ga = new GeneticAlgorithm(new UniformCrossover(0.5), 1.0, - new RandomPolygonMutation(MUTATION_RATE, MUTATION_CHANGE), 1.0, - new TournamentSelection(TOURNAMENT_ARITY)); + public Dimension getPreferredSize() { + return new Dimension(width, height); + } + + @Override + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + @Override + public Dimension getMaximumSize() { + return getPreferredSize(); + } + + public void paint(Graphics g) { + PolygonChromosome chromosome = (PolygonChromosome) bestFit; + chromosome.draw((Graphics2D) g, ref.getWidth(), ref.getHeight()); + } + + } - // initial population - currentPopulation = getInitialPopulation(); - bestFit = currentPopulation.getFittestChromosome(); } - private Population getInitialPopulation() { + public static void main(String[] args) throws Exception { + ExampleUtils.showExampleFrame(new Display()); + } + + private static BufferedImage resizeImage(BufferedImage originalImage, int width, int height, int type) throws IOException { + BufferedImage resizedImage = new BufferedImage(width, height, type); + Graphics2D g = resizedImage.createGraphics(); + g.drawImage(originalImage, 0, 0, width, height, null); + g.dispose(); + return resizedImage; + } + + private static Population getInitialPopulation() { List popList = new LinkedList(); for (int i = 0; i < POPULATION_SIZE; i++) { popList.add(PolygonChromosome.randomChromosome(POLYGON_LENGTH, POLYGON_COUNT)); @@ -107,99 +221,4 @@ public class ImageEvolutionExample extends JComponent { return new ElitisticListPopulation(popList, popList.size(), 0.25); } - public boolean isAlive() { - return internalThread != null && internalThread.isAlive(); - } - - public void stopRequest() { - noStopRequested = false; - internalThread.interrupt(); - internalThread = null; - } - - public void startEvolution() { - noStopRequested = true; - Runnable r = new Runnable() { - public void run() { - try { - double lastBestFit = Double.MIN_VALUE; - int evolution = 0; - while (noStopRequested) { - currentPopulation = ga.nextGeneration(currentPopulation); - - System.out.println("generation: " + evolution++ + ": " + bestFit.toString()); - bestFit = currentPopulation.getFittestChromosome(); - - if (lastBestFit > bestFit.getFitness()) { - System.out.println("gotcha"); - } - lastBestFit = bestFit.getFitness(); - painter.repaint(); - } - } catch (Exception x) { - // in case ANY exception slips through - x.printStackTrace(); - } - } - }; - - internalThread = new Thread(r); - internalThread.start(); - } - - private static void createAndShowGUI() throws Exception { - // Create and set up the window. - JFrame frame = new JFrame("Image Evolution Example"); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - - frame.getContentPane().add(new ImageEvolutionExample()); - - // Display the window. - frame.pack(); - frame.setSize(new Dimension(500, 300)); - frame.setVisible(true); - } - - public static void main(String[] args) { - // Schedule a job for the event-dispatching thread: - // creating and showing this application's GUI. - javax.swing.SwingUtilities.invokeLater(new Runnable() { - public void run() { - try { - createAndShowGUI(); - } catch (Exception e) { - e.printStackTrace(); - } - } - }); - } - - private class ImagePainter extends Component { - BufferedImage ref; - - public ImagePainter(final BufferedImage ref) { - this.ref = ref; - } - - public Dimension getPreferredSize() { - return new Dimension(ref.getWidth(), ref.getHeight()); - } - - @Override - public Dimension getMinimumSize() { - return getPreferredSize(); - } - - @Override - public Dimension getMaximumSize() { - return getPreferredSize(); - } - - public void paint(Graphics g) { - PolygonChromosome chromosome = (PolygonChromosome) bestFit; - chromosome.draw((Graphics2D) g, ref.getWidth(), ref.getHeight()); - } - - } - } diff --git a/src/userguide/java/org/apache/commons/math3/userguide/genetics/Polygon.java b/src/userguide/java/org/apache/commons/math3/userguide/genetics/Polygon.java index c83f525c3..e9d95f04a 100644 --- a/src/userguide/java/org/apache/commons/math3/userguide/genetics/Polygon.java +++ b/src/userguide/java/org/apache/commons/math3/userguide/genetics/Polygon.java @@ -1,5 +1,9 @@ package org.apache.commons.math3.userguide.genetics; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.geom.GeneralPath; + import org.apache.commons.math3.genetics.GeneticAlgorithm; import org.apache.commons.math3.random.RandomGenerator; import org.apache.commons.math3.util.FastMath; @@ -13,7 +17,7 @@ public class Polygon { // 2 | blue component // 3 | alpha channel // - protected float[] data; + private float[] data; public static Polygon randomPolygon(int length) { final int polygonSize = 4 + 2 * length; @@ -58,4 +62,18 @@ public class Polygon { return mutated; } + public void draw(Graphics2D g, int width, int height) { + g.setColor(new Color(data[0], data[1], data[2], data[3])); + + GeneralPath path = new GeneralPath(); + path.moveTo(data[4] * width, data[5] * height); + + int polygonLength = (data.length - 4) / 2; + for (int j = 1; j < polygonLength; j++) { + path.lineTo(data[4 + j * 2] * width, data[5 + j * 2] * height); + } + path.closePath(); + + g.fill(path); + } } diff --git a/src/userguide/java/org/apache/commons/math3/userguide/genetics/PolygonChromosome.java b/src/userguide/java/org/apache/commons/math3/userguide/genetics/PolygonChromosome.java index 38deaff06..e30bb59f2 100644 --- a/src/userguide/java/org/apache/commons/math3/userguide/genetics/PolygonChromosome.java +++ b/src/userguide/java/org/apache/commons/math3/userguide/genetics/PolygonChromosome.java @@ -4,7 +4,6 @@ import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; -import java.awt.geom.GeneralPath; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.List; @@ -55,15 +54,15 @@ public class PolygonChromosome extends AbstractListChromosome { g2.dispose(); int[] refPixels = refImage.getData().getPixels(0, 0, refImage.getWidth(), refImage.getHeight(), (int[]) null); - int[] pixels = testImage.getData().getPixels(0, 0, testImage.getWidth(), testImage.getHeight(), (int[]) null); + int[] testPixels = testImage.getData().getPixels(0, 0, testImage.getWidth(), testImage.getHeight(), (int[]) null); int diff = 0; - int p = width * height * 4 - 1; + int p = width * height * 4 - 1; // 4 channels: rgba int idx = 0; do { if (idx++ % 4 != 0) { - int dp = pixels[p] - refPixels[p]; + int dp = testPixels[p] - refPixels[p]; if (dp < 0) { diff -= dp; } else { @@ -76,27 +75,15 @@ public class PolygonChromosome extends AbstractListChromosome { } public void draw(Graphics2D g, int width, int height) { - g.setBackground(Color.RED); + g.setBackground(Color.WHITE); g.clearRect(0, 0, width, height); - //if (true) return; g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); List polygons = getPolygonRepresentation(); for (Polygon p : polygons) { - g.setColor(new Color(p.data[0], p.data[1], p.data[2], p.data[3])); - - GeneralPath path = new GeneralPath(); - path.moveTo(p.data[4] * width, p.data[5] * height); - - int polygonLength = (p.data.length - 4) / 2; - for (int j = 1; j < polygonLength; j++) { - path.lineTo(p.data[4 + j * 2] * width, p.data[5 + j * 2] * height); - } - path.closePath(); - - g.fill(path); + p.draw(g, width, height); } } diff --git a/src/userguide/resources/feather-small.gif b/src/userguide/resources/feather-small.gif new file mode 100644 index 000000000..9696a0023 Binary files /dev/null and b/src/userguide/resources/feather-small.gif differ diff --git a/src/userguide/resources/monalisa.png b/src/userguide/resources/monalisa.png new file mode 100644 index 000000000..f35a3b265 Binary files /dev/null and b/src/userguide/resources/monalisa.png differ diff --git a/src/userguide/resources/references.txt b/src/userguide/resources/references.txt new file mode 100644 index 000000000..427b41477 --- /dev/null +++ b/src/userguide/resources/references.txt @@ -0,0 +1 @@ +monalisa.png - http://commons.wikimedia.org/wiki/File:Mona_Lisa.jpg