Add some further work on hhtracer
authorunC0Rr
Mon, 27 Jan 2025 13:08:58 +0100
changeset 16092 ec4fc7eb6acd
parent 16091 288df7b85efc
child 16093 07cb6dbc8444
Add some further work on hhtracer
tools/hhtracer/Main.qml
tools/hhtracer/tracer.cpp
tools/hhtracer/tracer.h
--- 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();
 };