--- a/tools/hhtracer/Main.qml Sun Jan 26 21:29:54 2025 +0100
+++ b/tools/hhtracer/Main.qml Mon Jan 27 13:08:58 2025 +0100
@@ -36,6 +36,10 @@
Label {
text: "Best: %1".arg(tracer.bestSolution)
}
+
+ Label {
+ text: "Gen: %1".arg(tracer.generation)
+ }
}
}
@@ -45,17 +49,43 @@
nameFilters: ["Hedgehog images (*.png)"]
onAccepted: {
- console.log("Hello")
+ console.log("Hello");
baseImage.source = selectedFile;
tracer.start(fileDialog.selectedFile);
+ tracer.generation = 0;
}
}
Tracer {
id: tracer
+
+ property int generation: 0
+
+ atoms: [
+ {
+ "type": "polygon",
+ "length": 3,
+ "pens": ["#9f086e", "#54a2fa"],
+ "brushes": ["#2c78d2", "#54a2fa"]
+ },
+ {
+ "type": "circle",
+ "pens": ["#9f086e", "#f29ce7"],
+ "brushes": ["#d66bcc", "#f29ce7"]
+ },
+ {
+ "type": "circle",
+ "pens": ["#000000"],
+ "brushes": [ "#000000"]
+ },
+ {
+ "type": "circle",
+ "pens": ["#ffffff"],
+ "brushes": [ "#ffffff"]
+ }
+ ]
}
-
Timer {
id: stepTimer
@@ -64,7 +94,15 @@
running: false
triggeredOnStart: true
- onTriggered: tracer.step()
+ onTriggered: {
+ tracer.generation = tracer.generation + 1;
+ tracer.step();
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: "#a0c0a0"
}
ColumnLayout {
@@ -79,24 +117,24 @@
}
GridLayout {
+ Layout.fillHeight: true
Layout.fillWidth: true
- Layout.fillHeight: true
- columns: 50
+ columns: 30
Repeater {
model: tracer.solutions
Image {
- width: 32
+ fillMode: Image.PreserveAspectFit
height: 32
source: "file://" + modelData
- fillMode: Image.PreserveAspectFit
+ width: 32
Rectangle {
- border.width: 1
+ anchors.fill: parent
border.color: "black"
+ border.width: 1
color: "transparent"
- anchors.fill: parent
}
}
}
--- a/tools/hhtracer/tracer.cpp Sun Jan 26 21:29:54 2025 +0100
+++ b/tools/hhtracer/tracer.cpp Mon Jan 27 13:08:58 2025 +0100
@@ -1,24 +1,10 @@
#include "tracer.h"
+#include <QJsonObject>
#include <QRandomGenerator>
#include <QSvgGenerator>
-Tracer::Tracer(QObject *parent)
- : QObject{parent},
- palette_{{Qt::black,
- Qt::white,
- {"#9f086e"},
- {"#f29ce7"},
- {"#54a2fa"},
- {"#2c78d2"}}} {}
-
-QList<QColor> Tracer::palette() const { return palette_; }
-
-void Tracer::setPalette(const QList<QColor> &newPalette) {
- if (palette_ == newPalette) return;
- palette_ = newPalette;
- emit paletteChanged();
-}
+Tracer::Tracer(QObject *parent) : QObject{parent} {}
double Tracer::bestSolution() const { return bestSolution_; }
@@ -30,11 +16,6 @@
generation_.clear();
referenceImage_ = QImage{};
- if (palette_.isEmpty()) {
- qDebug("Empty palette");
- return;
- }
-
referenceImage_.load(QUrl(fileName).toLocalFile());
if (referenceImage_.isNull()) {
@@ -42,25 +23,30 @@
return;
}
+ referenceImage_ = referenceImage_.convertedTo(QImage::Format_RGBA8888);
+
for (int i = 0; i < 600; ++i) {
- generation_.append(Solution{{32, 32}, palette_});
+ generation_.append(Solution{referenceImage_.size(), atoms_});
}
}
void Tracer::step() {
const auto size = generation_.size();
- const auto keepSize = 10;
- const auto replaceSize = 50;
+ const auto keepSize = 1;
+ const auto replaceSize = 10;
const auto kept = generation_.mid(0, keepSize);
generation_ = generation_.mid(0, size - replaceSize);
+ std::for_each(std::begin(generation_), std::end(generation_),
+ [](auto &s) { s.mutate(); });
+
for (int i = 0; i < replaceSize; ++i) {
- generation_.append(Solution{{32, 32}, palette_});
+ generation_.append(Solution{referenceImage_.size(), atoms_});
}
auto rg = QRandomGenerator::global();
- for (qsizetype i = 0; i < size; i += 4) {
+ for (qsizetype i = 0; i < size; i += 6) {
const auto first = rg->bounded(size);
const auto second = rg->bounded(size);
@@ -69,11 +55,8 @@
}
}
- std::for_each(std::begin(generation_), std::end(generation_),
- [this](auto &s) { s.mutate(palette_); });
-
std::for_each(std::begin(solutions_), std::end(solutions_),
- [this](const auto &fn) { QFile::remove(fn); });
+ [](const auto &fn) { QFile::remove(fn); });
solutions_.clear();
generation_.append(kept);
@@ -83,7 +66,7 @@
solution.calculateFitness(referenceImage_);
- solution.fitness += solution.cost() * 100;
+ solution.fitness += solution.cost() * 1e4;
}
std::sort(std::begin(generation_), std::end(generation_),
@@ -112,9 +95,12 @@
QStringLiteral("hedgehog_%1.svg").arg(counter, 3, 32, QChar(u'_')));
}
-Solution::Solution(QSizeF size, const QList<QColor> &palette) : size{size} {
+Solution::Solution(QSizeF size, const QJsonArray &atoms) : size{size} {
fitness = 0;
- primitives = {Primitive(size, palette), Primitive(size, palette)};
+
+ std::transform(std::begin(atoms), std::end(atoms),
+ std::back_inserter(primitives),
+ [&](const auto &a) { return Primitive{size, a.toObject()}; });
}
void Solution::calculateFitness(const QImage &target) {
@@ -125,14 +111,16 @@
return;
}
+ candidate = candidate.convertedTo(QImage::Format_RGBA8888);
+
// Both images assumed same size, same format
double diffSum = 0;
int width = target.width();
int height = target.height();
for (int y = 0; y < height; ++y) {
- auto candScan = reinterpret_cast<const QRgb *>(candidate.scanLine(y));
- auto targScan = reinterpret_cast<const QRgb *>(target.scanLine(y));
+ const auto candScan = reinterpret_cast<const QRgb *>(candidate.scanLine(y));
+ const auto targScan = reinterpret_cast<const QRgb *>(target.scanLine(y));
for (int x = 0; x < width; ++x) {
// Compare RGBA channels
const QRgb cPix = candScan[x];
@@ -143,8 +131,7 @@
const auto dg = qGreen(cPix) - qGreen(tPix);
const auto db = qBlue(cPix) - qBlue(tPix);
const auto da = qAlpha(cPix) - qAlpha(tPix);
- diffSum +=
- qMax(qMax(qMax(dr * dr, dg * dg), db * db) * ta, da * da * 1.0);
+ diffSum += (dr * dr + dg * dg + db * db) * ta + da * da;
}
}
@@ -197,43 +184,38 @@
[](auto a, auto p) { return a + p.cost(); });
}
-void Solution::mutate(const QList<QColor> &palette) {
+void Solution::mutate() {
if (primitives.isEmpty()) {
return;
}
auto rg = QRandomGenerator::global();
- double mutationRate = 0.05;
-
- if (rg->bounded(1.0) > mutationRate) {
- return;
- }
+ double mutationRate = 0.1;
for (auto &prim : primitives) {
// Pen width
if (rg->bounded(1.0) < mutationRate) {
- prim.pen.setWidthF(prim.pen.widthF() * (rg->bounded(1.5) + 0.5) + 0.05);
+ prim.pen.setWidthF(prim.pen.widthF() * (rg->bounded(0.5) + 0.8) + 0.01);
}
// Origin
if (rg->bounded(1.0) < mutationRate) {
- prim.origin += QPointF(rg->bounded(10.0) - 5.0, rg->bounded(10.0) - 5.0);
+ prim.origin += QPointF(rg->bounded(4.0) - 2.0, rg->bounded(4.0) - 2.0);
}
if (prim.type == Polygon) {
// Points
for (auto &pt : prim.points) {
if (rg->bounded(1.0) < mutationRate) {
- prim.origin +=
- QPointF(rg->bounded(10.0) - 5.0, rg->bounded(10.0) - 5.0);
+ pt += QPointF(rg->bounded(2.0) - 1.0, rg->bounded(2.0) - 1.0);
}
}
} else { // Circle/ellipse
if (rg->bounded(1.0) < mutationRate) {
- prim.radius1 *= rg->bounded(0.4) + 0.8;
+ prim.radius1 *= rg->bounded(0.5) + 0.8;
}
if (rg->bounded(1.0) < mutationRate) {
- prim.radius2 *= rg->bounded(0.4) + 0.8;
+ prim.radius2 *= rg->bounded(0.5) + 0.8;
}
if (rg->bounded(1.0) < mutationRate) {
prim.rotation = rg->bounded(90.0);
@@ -242,64 +224,54 @@
}
if (rg->bounded(1.0) < mutationRate) {
- auto i = rg->bounded(primitives.size());
+ const auto i = rg->bounded(primitives.size());
- Primitive p{size, palette};
- primitives.insert(i, p);
+ primitives.insert(i, primitives[i]);
}
if (rg->bounded(1.0) < mutationRate) {
- auto i = rg->bounded(primitives.size());
+ const auto a = rg->bounded(primitives.size());
+ const auto b = rg->bounded(primitives.size());
+
+ qSwap(primitives[a], primitives[b]);
+ }
+
+ if (rg->bounded(1.0) < mutationRate) {
+ const auto i = rg->bounded(primitives.size());
primitives.remove(i);
}
}
void Solution::crossover(Solution &other) {
- const auto n = qMin(primitives.size(), other.primitives.size());
-
auto rg = QRandomGenerator::global();
- if (rg->bounded(1.0) < 0.02) {
- if (n <= 1) {
- return;
- }
- // swap tails
- const auto cp = rg->bounded(1, primitives.size());
- const auto ocp = rg->bounded(1, other.primitives.size());
+ const auto n = qMin(primitives.size(), other.primitives.size());
- const auto tail = primitives.mid(cp);
- const auto otherTail = other.primitives.mid(ocp);
+ if (n <= 1) {
+ return;
+ }
- primitives.remove(cp, primitives.size() - cp);
- other.primitives.remove(ocp, other.primitives.size() - ocp);
+ // swap one element
+ const auto cp = rg->bounded(n);
+ const auto ocp = rg->bounded(n);
- primitives.append(otherTail);
- other.primitives.append(tail);
- } else {
- if (n < 1) {
- return;
- }
- // swap one element
- const auto cp = rg->bounded(primitives.size());
- const auto ocp = rg->bounded(other.primitives.size());
-
- qSwap(primitives[cp], other.primitives[ocp]);
- }
+ qSwap(primitives[cp], other.primitives[ocp]);
}
-Primitive::Primitive(QSizeF size, const QList<QColor> &palette) {
+Primitive::Primitive(QSizeF size, const QJsonObject &atom) {
auto rg = QRandomGenerator::global();
auto randomPoint = [&]() -> QPointF {
return {rg->bounded(size.width()), rg->bounded(size.height())};
};
- if (rg->bounded(2) == 0) {
+ if (atom["type"] == "polygon") {
type = Polygon;
- points.append(randomPoint());
- points.append(randomPoint());
- } else {
+ for (int i = 1; i < atom["length"].toInt(3); ++i) {
+ points.append(randomPoint());
+ }
+ } else if (atom["type"] == "circle") {
type = Circle;
radius1 = rg->bounded(size.width() * 0.2) + 2;
@@ -307,11 +279,23 @@
rotation = rg->bounded(90);
}
- pen = QPen(palette[rg->bounded(palette.length())]);
- pen.setWidthF(rg->bounded(size.width() * 0.1));
- brush = QBrush(palette[rg->bounded(palette.length())]);
+ const auto pens = atom["pens"].toVariant().toStringList();
+ pen = QPen(pens[rg->bounded(pens.length())]);
+ pen.setWidthF(rg->bounded(size.width() * 0.05));
+ pen.setJoinStyle(Qt::RoundJoin);
+
+ const auto brushes = atom["brushes"].toVariant().toStringList();
+ brush = QBrush(QColor(brushes[rg->bounded(brushes.length())]));
origin = randomPoint();
}
double Primitive::cost() const { return 1.0 + 0.1 * points.length(); }
+
+QJsonArray Tracer::atoms() const { return atoms_; }
+
+void Tracer::setAtoms(const QJsonArray &newAtoms) {
+ if (atoms_ == newAtoms) return;
+ atoms_ = newAtoms;
+ emit atomsChanged();
+}
--- a/tools/hhtracer/tracer.h Sun Jan 26 21:29:54 2025 +0100
+++ b/tools/hhtracer/tracer.h Mon Jan 27 13:08:58 2025 +0100
@@ -1,5 +1,6 @@
#pragma once
+#include <QJsonArray>
#include <QObject>
#include <QPainter>
#include <QQmlEngine>
@@ -15,21 +16,22 @@
QList<QPointF> points; // polygon
double radius1{}, radius2{}, rotation{}; // ellipse
- explicit Primitive(QSizeF size, const QList<QColor>& palette);
+ explicit Primitive(QSizeF size, const QJsonObject& atom);
double cost() const;
};
struct Solution {
QList<Primitive> primitives;
- double fitness;
+ double fitness{1e64};
QSizeF size;
QString fileName;
+ quint32 gen;
- explicit Solution(QSizeF size, const QList<QColor>& palette);
+ explicit Solution(QSizeF size, const QJsonArray& atoms);
void calculateFitness(const QImage& target);
void render(const QString& fileName);
double cost() const;
- void mutate(const QList<QColor>& palette);
+ void mutate();
void crossover(Solution &other);
};
@@ -37,8 +39,8 @@
Q_OBJECT
QML_ELEMENT
- Q_PROPERTY(QList<QColor> palette READ palette WRITE setPalette NOTIFY
- paletteChanged FINAL)
+ Q_PROPERTY(
+ QJsonArray atoms READ atoms WRITE setAtoms NOTIFY atomsChanged FINAL)
Q_PROPERTY(
double bestSolution READ bestSolution NOTIFY bestSolutionChanged FINAL)
Q_PROPERTY(QStringList solutions READ solutions NOTIFY solutionsChanged FINAL)
@@ -46,9 +48,6 @@
public:
explicit Tracer(QObject *parent = nullptr);
- QList<QColor> palette() const;
- void setPalette(const QList<QColor>& newPalette);
-
double bestSolution() const;
Q_INVOKABLE void start(const QString& fileName);
@@ -56,18 +55,21 @@
QStringList solutions() const;
+ QJsonArray atoms() const;
+ void setAtoms(const QJsonArray& newAtoms);
+
Q_SIGNALS:
- void paletteChanged();
void bestSolutionChanged();
void solutionsChanged();
+ void atomsChanged();
private:
- QList<QColor> palette_;
double bestSolution_;
QStringList solutions_;
QList<Solution> generation_;
QTemporaryDir tempDir_;
QImage referenceImage_;
+ QJsonArray atoms_;
QString newFileName();
};