tools/hhtracer/tracer.cpp
changeset 16063 ec4fc7eb6acd
parent 16056 9ad74696ddec
--- 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();
+}