/* * polycasso - Cubism Artwork generator * Copyright 2009-2017 MeBigFatGuy.com * Copyright 2009-2017 Dave Brosius * Inspired by work by Roger Alsing * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and limitations * under the License. */ package com.mebigfatguy.polycasso; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Composite; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * class that generates test images iteratively looking for the best image that matches a target. * The images are generated from semi-transparent polygons that are improved upon over time. * This class generates multiple images in parallel to keep multicore processors busy. */ public class DefaultImageGenerator implements ImageGenerator, Runnable { private final Set<ImageGeneratedListener> listeners = new HashSet<ImageGeneratedListener>(); private final Settings settings; private final BufferedImage targetImage; private GenerationHandler generationHandler; private final Dimension imageSize; private Feedback feedback; private Thread[] t = null; private final Object startStopLock = new Object(); /** * creates an ImageGenerator for the given target image, and size * @param confSettings the configuration settings * @param image the target image * @param size the dimension of the image */ public DefaultImageGenerator(Settings confSettings, Image image, Dimension size) { settings = confSettings; imageSize = trimSize(size, settings.getMaxImageSize()); targetImage = new BufferedImage(imageSize.width, imageSize.height, BufferedImage.TYPE_4BYTE_ABGR); Graphics g = targetImage.getGraphics(); try { g.drawImage(image, 0, 0, imageSize.width, imageSize.height, Color.WHITE, null); generationHandler = new GenerationHandler(settings, imageSize); feedback = new DefaultFeedback(); feedback.setTargetImage(targetImage); } finally { g.dispose(); } } /** * retrieves the scaled target iamge * * @return the target image */ @Override public BufferedImage getTargetImage() { return targetImage; } /** * returns the image size that is being generated. This size might be different the original image * if the size is bigger then the max setting. * * @return the image size */ @Override public Dimension getImageSize() { return imageSize; } /** * allows interested parties to register to receive events when a new best image has been * found. * * @param listener the listener that is interested in events */ @Override public void addImageGeneratedListener(ImageGeneratedListener listener) { listeners.add(listener); } /** * allows uninterested parties to unregister to receive events when a new best image is * found * * @param listener the listener that is no longer needed */ @Override public void removeImageGeneratedListener(ImageGeneratedListener listener) { listeners.remove(listener); } /** * informs all listeners that a new best image has been found * * @param image the new best image */ @Override public void fireImageGenerated(Image image) { ImageGeneratedEvent event = new ImageGeneratedEvent(this, image); for (ImageGeneratedListener listener : listeners) { listener.imageGenerated(event); } } /** * starts up threads to start looking for images that are closest to the target */ @Override public void startGenerating() { synchronized(startStopLock) { if (t == null) { populateGenerationZeroElite(); t = new Thread[Runtime.getRuntime().availableProcessors() + 1]; for (int i = 0; i < t.length; i++) { t[i] = new Thread(this); t[i].setName("Improver : " + i); t[i].start(); } } } } /** * shuts down threads that were looking for images */ @Override public void stopGenerating() { synchronized(startStopLock) { if (t != null) { try { for (Thread element : t) { element.interrupt(); } for (Thread element : t) { element.join(); } } catch (InterruptedException ie) { } finally { t = null; } } } } /** * completes the image by transforming the polygon image to the real image */ @Override public void complete() { synchronized(startStopLock) { if (t != null) { stopGenerating(); t = new Thread[1]; t[0] = new Thread(new ImageCompleter(this, targetImage, generationHandler.getBestMember().getData(), imageSize)); t[0].start(); } } } /** * retrieves the best set of polygons for drawing the image so far * * @return the best set of polygons */ @Override public PolygonData[] getBestData() { return generationHandler.getBestMember().getData(); } /** * the runnable interface implementation to repeatedly improve upon the image and check to * see if it is closer to the target image. Images are created in batches of settings.numCompetingImages * and the best one (if better than the parent) is selected as the new best. */ @Override public void run() { try { BufferedImage image = new BufferedImage(imageSize.width, imageSize.height, BufferedImage.TYPE_4BYTE_ABGR); Graphics2D g2d = (Graphics2D)image.getGraphics(); try { Composite srcOpaque = AlphaComposite.getInstance(AlphaComposite.SRC, 1.0f); Improver improver = new Improver(settings, generationHandler, imageSize); while (!Thread.interrupted()) { ImprovementType type = improver.improveRandomly(); List<PolygonData> data = improver.getData(); imagePolygonData(g2d, data, srcOpaque); GenerationMember parentMember = improver.getParentGenerationMember(); Score delta = feedback.calculateScore(image, (parentMember != null) ? parentMember.getScore() : null, improver.getChangedArea()); boolean wasSuccessful; ImprovementResult result = generationHandler.addPolygonData(delta, data.toArray(new PolygonData[data.size()])); switch (result) { case BEST: fireImageGenerated(image); wasSuccessful = true; image = new BufferedImage(imageSize.width, imageSize.height, BufferedImage.TYPE_4BYTE_ABGR); g2d.dispose(); g2d = (Graphics2D)image.getGraphics(); break; case ELITE: wasSuccessful = true; break; default: wasSuccessful = false; } improver.typeWasSuccessful(type, wasSuccessful); } } finally { g2d.dispose(); } } catch (Exception e) { e.printStackTrace(); } } private void imagePolygonData(Graphics2D g2d, List<PolygonData> polygonData, Composite srcOpaque) { g2d.setColor(Color.BLACK); g2d.setComposite(srcOpaque); g2d.fillRect(0, 0, imageSize.width, imageSize.height); for (PolygonData pd : polygonData) { pd.draw(g2d); } } private void populateGenerationZeroElite() { Composite srcOpaque = AlphaComposite.getInstance(AlphaComposite.SRC, 1.0f); for (int i = 0; i < settings.getEliteSize(); i++) { BufferedImage image = new BufferedImage(imageSize.width, imageSize.height, BufferedImage.TYPE_4BYTE_ABGR); List<PolygonData> polygons = new ArrayList<PolygonData>(); PolygonData pd = PolygonData.randomPoly(imageSize, settings.getMaxPoints()); polygons.add(pd); Graphics2D g2d = (Graphics2D)image.getGraphics(); try { imagePolygonData(g2d, polygons, srcOpaque); Score delta = feedback.calculateScore(image, null, null); generationHandler.addPolygonData(delta, polygons.toArray(new PolygonData[polygons.size()])); } finally { g2d.dispose(); } } } private Dimension trimSize(Dimension origSize, Dimension maxSize) { if ((origSize.width < maxSize.width) && (origSize.height < maxSize.height)) { return origSize; } double hFrac = (double)maxSize.width / (double)origSize.width; double vFrac = (double)maxSize.height/ (double)origSize.height; double frac = (hFrac < vFrac) ? hFrac : vFrac; return new Dimension((int)(frac * origSize.width), (int)(frac * origSize.height)); } }