Start work on hedgehog tracer default tip
authorunC0Rr
Fri, 10 Jan 2025 17:37:34 +0100
changeset 16084 2d65bd46c92f
parent 16083 629d5123a979
Start work on hedgehog tracer
tools/hhtracer/CMakeLists.txt
tools/hhtracer/Main.qml
tools/hhtracer/main.cpp
tools/hhtracer/tracer.cpp
tools/hhtracer/tracer.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/hhtracer/CMakeLists.txt	Fri Jan 10 17:37:34 2025 +0100
@@ -0,0 +1,44 @@
+cmake_minimum_required(VERSION 3.16)
+
+project(hhtracer VERSION 0.1 LANGUAGES CXX)
+
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+find_package(Qt6 REQUIRED COMPONENTS Quick Svg)
+
+qt_standard_project_setup(REQUIRES 6.5)
+
+qt_add_executable(apphhtracer
+    main.cpp
+)
+
+qt_add_qml_module(apphhtracer
+    URI hhtracer
+    VERSION 1.0
+    QML_FILES
+        Main.qml
+        SOURCES tracer.h tracer.cpp
+)
+
+# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
+# If you are developing for iOS or macOS you should consider setting an
+# explicit, fixed bundle identifier manually though.
+set_target_properties(apphhtracer PROPERTIES
+#    MACOSX_BUNDLE_GUI_IDENTIFIER com.example.apphhtracer
+    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
+    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
+    MACOSX_BUNDLE TRUE
+    WIN32_EXECUTABLE TRUE
+)
+
+target_link_libraries(apphhtracer
+    PRIVATE Qt6::Quick
+    Qt6::Svg
+)
+
+include(GNUInstallDirs)
+install(TARGETS apphhtracer
+    BUNDLE DESTINATION .
+    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/hhtracer/Main.qml	Fri Jan 10 17:37:34 2025 +0100
@@ -0,0 +1,101 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Dialogs
+import QtQuick.Layouts
+
+ApplicationWindow {
+  height: 900
+  title: qsTr("Tracer")
+  visible: true
+  width: 1200
+
+  header: ToolBar {
+    RowLayout {
+      Button {
+        text: qsTr("Choose Image...")
+
+        onClicked: fileDialog.open()
+      }
+
+      Button {
+        text: qsTr("Start")
+
+        onClicked: {
+          stepTimer.start();
+        }
+      }
+
+      Button {
+        text: qsTr("Stop")
+
+        onClicked: {
+          stepTimer.stop();
+        }
+      }
+    }
+  }
+
+  FileDialog {
+    id: fileDialog
+
+    nameFilters: ["Hedgehog images (*.png)"]
+
+    onAccepted: {
+      console.log("Hello")
+      baseImage.source = selectedFile;
+      tracer.start(fileDialog.selectedFile);
+    }
+  }
+
+  Tracer {
+    id: tracer
+  }
+
+
+  Timer {
+    id: stepTimer
+
+    interval: 1500
+    repeat: true
+    running: false
+    triggeredOnStart: true
+
+    onTriggered: tracer.step()
+  }
+
+  ColumnLayout {
+    anchors.fill: parent
+
+    Image {
+      id: baseImage
+
+      Layout.fillWidth: true
+      Layout.preferredHeight: 128
+      fillMode: Image.PreserveAspectFit
+    }
+
+    GridLayout {
+      Layout.fillWidth: true
+      Layout.fillHeight: true
+      columns: 10
+
+      Repeater {
+        model: tracer.solutions
+
+        Image {
+          width: 32
+          height: 32
+          source: "file://" + modelData
+          fillMode: Image.PreserveAspectFit
+
+          Rectangle {
+            border.width: 1
+            border.color: "black"
+            color: "transparent"
+            anchors.fill: parent
+          }
+        }
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/hhtracer/main.cpp	Fri Jan 10 17:37:34 2025 +0100
@@ -0,0 +1,22 @@
+#include <QGuiApplication>
+#include <QQmlApplicationEngine>
+#include <QQmlContext>
+
+#include "tracer.h"
+
+int main(int argc, char *argv[]) {
+  QGuiApplication app(argc, argv);
+
+  QQmlApplicationEngine engine;
+
+  // Tracer tracer;
+  // engine.rootContext()->setContextProperty(QStringLiteral("tracer"),
+  // &tracer);
+
+  QObject::connect(
+      &engine, &QQmlApplicationEngine::objectCreationFailed, &app,
+      []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
+  engine.loadFromModule("hhtracer", "Main");
+
+  return app.exec();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/hhtracer/tracer.cpp	Fri Jan 10 17:37:34 2025 +0100
@@ -0,0 +1,149 @@
+#include "tracer.h"
+
+#include <QRandomGenerator>
+#include <QSvgGenerator>
+
+Tracer::Tracer(QObject* parent)
+    : QObject{parent},
+      palette_{{Qt::black,
+                Qt::white,
+                {"#f29ce7"},
+                {"#9f086e"},
+                {"#54a2fa"},
+                {"#2c78d2"}}} {}
+
+QList<QColor> Tracer::palette() const { return palette_; }
+
+void Tracer::setPalette(const QList<QColor>& newPalette) {
+  if (palette_ == newPalette) return;
+  palette_ = newPalette;
+  emit paletteChanged();
+}
+
+double Tracer::bestSolution() const { return bestSolution_; }
+
+void Tracer::start(const QString& fileName) {
+  qDebug() << "Starting using" << fileName;
+
+  bestSolution_ = 0;
+  solutions_.clear();
+  generation_.clear();
+  image_ = QImage{};
+
+  if (palette_.isEmpty()) {
+    qDebug("Empty palette");
+    return;
+  }
+
+  image_.load(QUrl(fileName).toLocalFile());
+
+  if (image_.isNull()) {
+    qDebug("Failed to load image");
+    return;
+  }
+
+  for (int i = 0; i < 100; ++i) {
+    generation_.append(Solution{{32, 32}, palette_});
+  }
+}
+
+void Tracer::step() {
+  solutions_.clear();
+
+  for (auto& solution : generation_) {
+    const auto fileName = newFileName();
+    solutions_.append(fileName);
+
+    solution.render(fileName);
+  }
+
+  qDebug() << solutions_;
+
+  emit solutionsChanged();
+}
+
+QStringList Tracer::solutions() const { return solutions_; }
+
+QString Tracer::newFileName() {
+  static qlonglong counter{0};
+  counter += 1;
+  return tempDir_.filePath(
+      QStringLiteral("hedgehog_%1.svg").arg(counter, 3, 32, QChar(u'_')));
+}
+
+Solution::Solution(QSizeF size, const QList<QColor>& palette) : size{size} {
+  fitness = 0;
+  primitives = {Primitive(size, palette)};
+}
+
+void Solution::render(const QString& fileName) const {
+  const auto imageSize = size.toSize();
+
+  QSvgGenerator generator;
+  generator.setFileName(fileName);
+  generator.setSize(imageSize);
+  generator.setViewBox(QRect(0, 0, imageSize.width(), imageSize.height()));
+  generator.setTitle("Hedgehog");
+  generator.setDescription("Approximation of a target image using primitives");
+
+  QPainter painter;
+  painter.begin(&generator);
+  painter.setRenderHint(QPainter::Antialiasing, true);
+
+  for (const auto& primitive : primitives) {
+    painter.setPen(primitive.pen);
+    painter.setBrush(primitive.brush);
+    painter.resetTransform();
+    painter.translate(primitive.origin);
+    painter.rotate(primitive.rotation);
+
+    switch (primitive.type) {
+      case Polygon: {
+        QPolygonF polygon;
+        polygon.append({0, 0});
+        polygon.append(primitive.points);
+
+        painter.drawPolygon(polygon);
+        break;
+      }
+      case Circle:
+        painter.drawEllipse({0, 0}, primitive.radius1, primitive.radius2);
+        break;
+    }
+  }
+
+  painter.end();
+}
+
+double Solution::cost() const {
+  return std::accumulate(primitives.constBegin(), primitives.constEnd(), 0,
+                         [](auto a, auto p) { return a + p.cost(); });
+}
+
+Primitive::Primitive(QSizeF size, const QList<QColor>& palette) {
+  auto rg = QRandomGenerator::global();
+  auto randomPoint = [&]() -> QPointF {
+    return {rg->bounded(size.width()), rg->bounded(size.height())};
+  };
+
+  if (rg->bounded(2) == 0) {
+    type = Polygon;
+
+    points.append(randomPoint());
+    points.append(randomPoint());
+  } else {
+    type = Circle;
+
+    radius1 = rg->bounded(size.width());
+    radius2 = rg->bounded(size.width());
+    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())]);
+
+  origin = randomPoint();
+}
+
+double Primitive::cost() const { return 1.0 + 0.1 * points.length(); }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/hhtracer/tracer.h	Fri Jan 10 17:37:34 2025 +0100
@@ -0,0 +1,70 @@
+#pragma once
+
+#include <QObject>
+#include <QPainter>
+#include <QQmlEngine>
+#include <QTemporaryDir>
+
+enum PrimitiveType { Polygon, Circle };
+
+struct Primitive {
+  PrimitiveType type;
+  QPen pen;
+  QBrush brush;
+  QPointF origin;
+  QList<QPointF> points;                    // polygon
+  double radius1{}, radius2{}, rotation{};  // ellipse
+
+  explicit Primitive(QSizeF size, const QList<QColor>& palette);
+  double cost() const;
+};
+
+struct Solution {
+  QList<Primitive> primitives;
+  double fitness;
+  QSizeF size;
+
+  explicit Solution(QSizeF size, const QList<QColor>& palette);
+  void calculateFitness(const QImage& image);
+  void render(const QString& fileName) const;
+  double cost() const;
+};
+
+class Tracer : public QObject {
+  Q_OBJECT
+  QML_ELEMENT
+
+  Q_PROPERTY(QList<QColor> palette READ palette WRITE setPalette NOTIFY
+                 paletteChanged FINAL)
+  Q_PROPERTY(
+      double bestSolution READ bestSolution NOTIFY bestSolutionChanged FINAL)
+  Q_PROPERTY(QStringList solutions READ solutions NOTIFY solutionsChanged FINAL)
+
+ 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);
+  Q_INVOKABLE void step();
+
+  QStringList solutions() const;
+
+ Q_SIGNALS:
+  void paletteChanged();
+  void bestSolutionChanged();
+  void solutionsChanged();
+
+ private:
+  QList<QColor> palette_;
+  double bestSolution_;
+  QStringList solutions_;
+  QList<Solution> generation_;
+  QTemporaryDir tempDir_;
+  QImage image_;
+
+  QString newFileName();
+};