# HG changeset patch # User unC0Rr # Date 1737979738 -3600 # Node ID ec4fc7eb6acdcde6014dd14776c1ec3abd9bffa9 # Parent 288df7b85efca38ae446d6d81b41cac7f3825e69 Add some further work on hhtracer diff -r 288df7b85efc -r ec4fc7eb6acd tools/hhtracer/Main.qml --- 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 } } } diff -r 288df7b85efc -r ec4fc7eb6acd tools/hhtracer/tracer.cpp --- 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 #include #include -Tracer::Tracer(QObject *parent) - : QObject{parent}, - palette_{{Qt::black, - Qt::white, - {"#9f086e"}, - {"#f29ce7"}, - {"#54a2fa"}, - {"#2c78d2"}}} {} - -QList Tracer::palette() const { return palette_; } - -void Tracer::setPalette(const QList &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 &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(candidate.scanLine(y)); - auto targScan = reinterpret_cast(target.scanLine(y)); + const auto candScan = reinterpret_cast(candidate.scanLine(y)); + const auto targScan = reinterpret_cast(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 &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 &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(); +} diff -r 288df7b85efc -r ec4fc7eb6acd tools/hhtracer/tracer.h --- 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 #include #include #include @@ -15,21 +16,22 @@ QList points; // polygon double radius1{}, radius2{}, rotation{}; // ellipse - explicit Primitive(QSizeF size, const QList& palette); + explicit Primitive(QSizeF size, const QJsonObject& atom); double cost() const; }; struct Solution { QList primitives; - double fitness; + double fitness{1e64}; QSizeF size; QString fileName; + quint32 gen; - explicit Solution(QSizeF size, const QList& 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& palette); + void mutate(); void crossover(Solution &other); }; @@ -37,8 +39,8 @@ Q_OBJECT QML_ELEMENT - Q_PROPERTY(QList 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 palette() const; - void setPalette(const QList& 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 palette_; double bestSolution_; QStringList solutions_; QList generation_; QTemporaryDir tempDir_; QImage referenceImage_; + QJsonArray atoms_; QString newFileName(); };