/*
* Java Genetic Algorithm Library (@__identifier__@).
* Copyright (c) @__year__@ Franz Wilhelmstötter
*
* 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.
*
* Author:
* Franz Wilhelmstötter (franz.wilhelmstoetter@gmx.at)
*/
package org.jenetics.example.image;
import static java.util.Objects.requireNonNull;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import org.jenetics.Genotype;
import org.jenetics.MeanAlterer;
import org.jenetics.Optimize;
import org.jenetics.TournamentSelector;
import org.jenetics.TruncationSelector;
import org.jenetics.engine.Codec;
import org.jenetics.engine.Engine;
import org.jenetics.engine.EvolutionResult;
import org.jenetics.stat.MinMax;
/**
* Performs the actual evolution.
*/
final class EvolvingImagesWorker {
private final BufferedImage _image;
private final BufferedImage _refImage;
private final int[] _refImagePixels;
private final ThreadLocal<BufferedImage> _workingImage;
private final Engine<PolygonGene, Double> _engine;
private volatile Thread _thread;
private boolean _paused = false;
private final Lock _pauseLock = new ReentrantLock();
private final Condition _pauseCondition = _pauseLock.newCondition();
/**
* Create an new worker instance with the given parameter and for the given
* image.
*
* @param param the GA engine parameter
* @param image the image to polygonize
*/
private EvolvingImagesWorker(
final EngineParam param,
final BufferedImage image
) {
_image = requireNonNull(image);
_refImage = resizeImage(
_image,
param.getReferenceImageSize().width,
param.getReferenceImageSize().height,
BufferedImage.TYPE_INT_ARGB
);
_workingImage = ThreadLocal.withInitial(() -> new BufferedImage(
_refImage.getWidth(),
_refImage.getHeight(),
BufferedImage.TYPE_INT_ARGB
));
_refImagePixels = _refImage.getData().getPixels(
0, 0, _refImage.getWidth(), _refImage.getHeight(), (int[])null
);
final Codec<PolygonChromosome, PolygonGene> codec = Codec.of(
Genotype.of(new PolygonChromosome(
param.getPolygonCount(), param.getPolygonLength()
)),
gt -> (PolygonChromosome) gt.getChromosome()
);
_engine = Engine.builder(this::fitness, codec)
.populationSize(param.getPopulationSize())
.optimize(Optimize.MAXIMUM)
.maximalPhenotypeAge(50)
.survivorsSelector(new TruncationSelector<>())
.offspringSelector(new TournamentSelector<>(param.getTournamentSize()))
.alterers(
new MeanAlterer<>(0.175),
new PolygonMutator<>(param.getMutationRate(), param.getMutationMultitude()),
new UniformCrossover<>(0.5))
.build();
}
private static BufferedImage resizeImage(
final BufferedImage image,
final int width,
final int height,
final int type
) {
final BufferedImage resizedImage = new BufferedImage(width, height, type);
final Graphics2D g = resizedImage.createGraphics();
g.drawImage(image, 0, 0, width, height, null);
g.dispose();
return resizedImage;
}
BufferedImage getImage() {
return _image;
}
/**
* Calculate the fitness function for a Polygon chromosome.
* <p>
* For this purpose, we first draw the polygons on the test buffer, and
* then compare the resulting image pixel by pixel with the reference image.
*/
private double fitness(final PolygonChromosome chromosome) {
final BufferedImage img = _workingImage.get();
final Graphics2D g2 = img.createGraphics();
final int width = img.getWidth();
final int height = img.getHeight();
chromosome.draw(g2, width, height);
g2.dispose();
final int[] refPixels = _refImagePixels;
final int[] testPixels = img.getData()
.getPixels(0, 0, width, height, (int[])null);
int diff = 0;
int p = width*height*4 - 1; // 4 channels: rgba
int idx = 0;
do {
if (idx++%4 != 0) { // ignore the alpha channel for fitness
int dp = testPixels[p] - refPixels[p];
diff += (dp < 0) ? -dp : dp;
}
} while (--p > 0);
return 1.0 - diff/(width*height*3.0*256);
}
/**
* Starts the evolution worker with the given evolution result callback. The
* callback may be null.
*
* @param callback the {@code EvolutionResult} callback. The first parameter
* contains the current result and the second the best.
*/
public void start(
final BiConsumer<
EvolutionResult<PolygonGene, Double>,
EvolutionResult<PolygonGene, Double>> callback
) {
final Thread thread = new Thread(() -> {
final MinMax<EvolutionResult<PolygonGene, Double>> best = MinMax.of();
_engine.stream()
.limit(result -> !Thread.currentThread().isInterrupted())
.peek(best)
.forEach(r -> {
waiting();
if (callback != null) {
callback.accept(r, best.getMax());
}
});
});
thread.start();
_thread = thread;
}
private void waiting() {
_pauseLock.lock();
try {
while (_paused) {
try {
_pauseCondition.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} finally {
_pauseLock.unlock();
}
}
/**
* Stops the current evolution, if running.
*/
public void stop() {
resume();
final Thread thread = _thread;
if (thread != null) {
thread.interrupt();
try {
thread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
_thread = null;
}
}
}
/**
* Waits for the evolution thread.
*
* @throws InterruptedException if the calling thread has been interrupted.
*/
public void join() throws InterruptedException {
final Thread thread = _thread;
if (thread != null) {
thread.join();
}
}
/**
* Pauses the current evolution run.
*/
public void pause() {
_pauseLock.lock();
try {
_paused = true;
} finally {
_pauseLock.unlock();
}
}
/**
* Resumes the current evolution run.
*/
public void resume() {
_pauseLock.lock();
try {
_paused = false;
_pauseCondition.signalAll();
} finally {
_pauseLock.unlock();
}
}
/**
* Return an new worker instance with the given parameter and for the given
* image.
*
* @param param the GA engine parameter
* @param image the image to polygonize
* @return a new evolving image instance
*/
public static EvolvingImagesWorker of(
final EngineParam param,
final BufferedImage image
) {
return new EvolvingImagesWorker(param, image);
}
}