Add some progress on hhtracer default tip
authorunC0Rr
Sun, 12 Jan 2025 22:48:47 +0100
changeset 16085 9ad74696ddec
parent 16084 2d65bd46c92f
Add some progress on hhtracer
tools/hhtracer/Main.qml
tools/hhtracer/main.cpp
tools/hhtracer/tracer.cpp
tools/hhtracer/tracer.h
--- a/tools/hhtracer/Main.qml	Fri Jan 10 17:37:34 2025 +0100
+++ b/tools/hhtracer/Main.qml	Sun Jan 12 22:48:47 2025 +0100
@@ -32,6 +32,10 @@
           stepTimer.stop();
         }
       }
+
+      Label {
+        text: "Best: %1".arg(tracer.bestSolution)
+      }
     }
   }
 
@@ -55,7 +59,7 @@
   Timer {
     id: stepTimer
 
-    interval: 1500
+    interval: 120
     repeat: true
     running: false
     triggeredOnStart: true
@@ -70,14 +74,14 @@
       id: baseImage
 
       Layout.fillWidth: true
-      Layout.preferredHeight: 128
+      Layout.preferredHeight: 32
       fillMode: Image.PreserveAspectFit
     }
 
     GridLayout {
       Layout.fillWidth: true
       Layout.fillHeight: true
-      columns: 10
+      columns: 50
 
       Repeater {
         model: tracer.solutions
--- a/tools/hhtracer/main.cpp	Fri Jan 10 17:37:34 2025 +0100
+++ b/tools/hhtracer/main.cpp	Sun Jan 12 22:48:47 2025 +0100
@@ -9,10 +9,6 @@
 
   QQmlApplicationEngine engine;
 
-  // Tracer tracer;
-  // engine.rootContext()->setContextProperty(QStringLiteral("tracer"),
-  // &tracer);
-
   QObject::connect(
       &engine, &QQmlApplicationEngine::objectCreationFailed, &app,
       []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
--- a/tools/hhtracer/tracer.cpp	Fri Jan 10 17:37:34 2025 +0100
+++ b/tools/hhtracer/tracer.cpp	Sun Jan 12 22:48:47 2025 +0100
@@ -3,18 +3,18 @@
 #include <QRandomGenerator>
 #include <QSvgGenerator>
 
-Tracer::Tracer(QObject* parent)
+Tracer::Tracer(QObject *parent)
     : QObject{parent},
       palette_{{Qt::black,
                 Qt::white,
+                {"#9f086e"},
                 {"#f29ce7"},
-                {"#9f086e"},
                 {"#54a2fa"},
                 {"#2c78d2"}}} {}
 
 QList<QColor> Tracer::palette() const { return palette_; }
 
-void Tracer::setPalette(const QList<QColor>& newPalette) {
+void Tracer::setPalette(const QList<QColor> &newPalette) {
   if (palette_ == newPalette) return;
   palette_ = newPalette;
   emit paletteChanged();
@@ -22,43 +22,84 @@
 
 double Tracer::bestSolution() const { return bestSolution_; }
 
-void Tracer::start(const QString& fileName) {
+void Tracer::start(const QString &fileName) {
   qDebug() << "Starting using" << fileName;
 
   bestSolution_ = 0;
   solutions_.clear();
   generation_.clear();
-  image_ = QImage{};
+  referenceImage_ = QImage{};
 
   if (palette_.isEmpty()) {
     qDebug("Empty palette");
     return;
   }
 
-  image_.load(QUrl(fileName).toLocalFile());
+  referenceImage_.load(QUrl(fileName).toLocalFile());
 
-  if (image_.isNull()) {
+  if (referenceImage_.isNull()) {
     qDebug("Failed to load image");
     return;
   }
 
-  for (int i = 0; i < 100; ++i) {
+  for (int i = 0; i < 600; ++i) {
     generation_.append(Solution{{32, 32}, palette_});
   }
 }
 
 void Tracer::step() {
+  const auto size = generation_.size();
+  const auto keepSize = 10;
+  const auto replaceSize = 50;
+  const auto kept = generation_.mid(0, keepSize);
+  generation_ = generation_.mid(0, size - replaceSize);
+
+  for (int i = 0; i < replaceSize; ++i) {
+    generation_.append(Solution{{32, 32}, palette_});
+  }
+
+  auto rg = QRandomGenerator::global();
+
+  for (qsizetype i = 0; i < size; i += 4) {
+    const auto first = rg->bounded(size);
+    const auto second = rg->bounded(size);
+
+    if (first != second) {
+      generation_[first].crossover(generation_[second]);
+    }
+  }
+
+  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); });
   solutions_.clear();
 
-  for (auto& solution : generation_) {
-    const auto fileName = newFileName();
-    solutions_.append(fileName);
+  generation_.append(kept);
 
-    solution.render(fileName);
+  for (auto &solution : generation_) {
+    solution.render(newFileName());
+
+    solution.calculateFitness(referenceImage_);
+
+    solution.fitness += solution.cost() * 100;
   }
 
-  qDebug() << solutions_;
+  std::sort(std::begin(generation_), std::end(generation_),
+            [](const auto &a, const auto &b) { return a.fitness < b.fitness; });
+
+  std::for_each(std::begin(generation_) + size, std::end(generation_),
+                [](const auto &s) { QFile::remove(s.fileName); });
+  generation_.remove(size, kept.size());
 
+  bestSolution_ = generation_[0].fitness;
+
+  std::transform(std::begin(generation_), std::end(generation_),
+                 std::back_inserter(solutions_),
+                 [](const auto &a) { return a.fileName; });
+
+  emit bestSolutionChanged();
   emit solutionsChanged();
 }
 
@@ -71,12 +112,48 @@
       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 QList<QColor> &palette) : size{size} {
   fitness = 0;
-  primitives = {Primitive(size, palette)};
+  primitives = {Primitive(size, palette), Primitive(size, palette)};
 }
 
-void Solution::render(const QString& fileName) const {
+void Solution::calculateFitness(const QImage &target) {
+  QImage candidate{fileName};
+
+  if (candidate.isNull()) {
+    fitness = 1e32;
+    return;
+  }
+
+  // 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));
+    for (int x = 0; x < width; ++x) {
+      // Compare RGBA channels
+      const QRgb cPix = candScan[x];
+      const QRgb tPix = targScan[x];
+      // const auto ca = qAlpha(cPix) / 255.0;
+      const auto ta = qAlpha(tPix) / 255.0;
+      const auto dr = qRed(cPix) - qRed(tPix);
+      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);
+    }
+  }
+
+  fitness = diffSum;
+}
+
+void Solution::render(const QString &fileName) {
+  this->fileName = fileName;
+
   const auto imageSize = size.toSize();
 
   QSvgGenerator generator;
@@ -90,7 +167,7 @@
   painter.begin(&generator);
   painter.setRenderHint(QPainter::Antialiasing, true);
 
-  for (const auto& primitive : primitives) {
+  for (const auto &primitive : primitives) {
     painter.setPen(primitive.pen);
     painter.setBrush(primitive.brush);
     painter.resetTransform();
@@ -120,7 +197,98 @@
                          [](auto a, auto p) { return a + p.cost(); });
 }
 
-Primitive::Primitive(QSizeF size, const QList<QColor>& palette) {
+void Solution::mutate(const QList<QColor> &palette) {
+  if (primitives.isEmpty()) {
+    return;
+  }
+
+  auto rg = QRandomGenerator::global();
+  double mutationRate = 0.05;
+
+  if (rg->bounded(1.0) > mutationRate) {
+    return;
+  }
+
+  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);
+    }
+
+    // Origin
+    if (rg->bounded(1.0) < mutationRate) {
+      prim.origin += QPointF(rg->bounded(10.0) - 5.0, rg->bounded(10.0) - 5.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);
+        }
+      }
+    } else {  // Circle/ellipse
+      if (rg->bounded(1.0) < mutationRate) {
+        prim.radius1 *= rg->bounded(0.4) + 0.8;
+      }
+      if (rg->bounded(1.0) < mutationRate) {
+        prim.radius2 *= rg->bounded(0.4) + 0.8;
+      }
+      if (rg->bounded(1.0) < mutationRate) {
+        prim.rotation = rg->bounded(90.0);
+      }
+    }
+  }
+
+  if (rg->bounded(1.0) < mutationRate) {
+    auto i = rg->bounded(primitives.size());
+
+    Primitive p{size, palette};
+    primitives.insert(i, p);
+  }
+
+  if (rg->bounded(1.0) < mutationRate) {
+    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 tail = primitives.mid(cp);
+    const auto otherTail = other.primitives.mid(ocp);
+
+    primitives.remove(cp, primitives.size() - cp);
+    other.primitives.remove(ocp, other.primitives.size() - ocp);
+
+    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]);
+  }
+}
+
+Primitive::Primitive(QSizeF size, const QList<QColor> &palette) {
   auto rg = QRandomGenerator::global();
   auto randomPoint = [&]() -> QPointF {
     return {rg->bounded(size.width()), rg->bounded(size.height())};
@@ -134,8 +302,8 @@
   } else {
     type = Circle;
 
-    radius1 = rg->bounded(size.width());
-    radius2 = rg->bounded(size.width());
+    radius1 = rg->bounded(size.width() * 0.2) + 2;
+    radius2 = rg->bounded(size.width() * 0.2) + 2;
     rotation = rg->bounded(90);
   }
 
--- a/tools/hhtracer/tracer.h	Fri Jan 10 17:37:34 2025 +0100
+++ b/tools/hhtracer/tracer.h	Sun Jan 12 22:48:47 2025 +0100
@@ -23,11 +23,14 @@
   QList<Primitive> primitives;
   double fitness;
   QSizeF size;
+  QString fileName;
 
   explicit Solution(QSizeF size, const QList<QColor>& palette);
-  void calculateFitness(const QImage& image);
-  void render(const QString& fileName) const;
+  void calculateFitness(const QImage& target);
+  void render(const QString& fileName);
   double cost() const;
+  void mutate(const QList<QColor>& palette);
+  void crossover(Solution &other);
 };
 
 class Tracer : public QObject {
@@ -64,7 +67,7 @@
   QStringList solutions_;
   QList<Solution> generation_;
   QTemporaryDir tempDir_;
-  QImage image_;
+  QImage referenceImage_;
 
   QString newFileName();
 };