1 #include "tracer.h" |
1 #include "tracer.h" |
2 |
2 |
3 #include <QRandomGenerator> |
3 #include <QRandomGenerator> |
4 #include <QSvgGenerator> |
4 #include <QSvgGenerator> |
5 |
5 |
6 Tracer::Tracer(QObject* parent) |
6 Tracer::Tracer(QObject *parent) |
7 : QObject{parent}, |
7 : QObject{parent}, |
8 palette_{{Qt::black, |
8 palette_{{Qt::black, |
9 Qt::white, |
9 Qt::white, |
|
10 {"#9f086e"}, |
10 {"#f29ce7"}, |
11 {"#f29ce7"}, |
11 {"#9f086e"}, |
|
12 {"#54a2fa"}, |
12 {"#54a2fa"}, |
13 {"#2c78d2"}}} {} |
13 {"#2c78d2"}}} {} |
14 |
14 |
15 QList<QColor> Tracer::palette() const { return palette_; } |
15 QList<QColor> Tracer::palette() const { return palette_; } |
16 |
16 |
17 void Tracer::setPalette(const QList<QColor>& newPalette) { |
17 void Tracer::setPalette(const QList<QColor> &newPalette) { |
18 if (palette_ == newPalette) return; |
18 if (palette_ == newPalette) return; |
19 palette_ = newPalette; |
19 palette_ = newPalette; |
20 emit paletteChanged(); |
20 emit paletteChanged(); |
21 } |
21 } |
22 |
22 |
23 double Tracer::bestSolution() const { return bestSolution_; } |
23 double Tracer::bestSolution() const { return bestSolution_; } |
24 |
24 |
25 void Tracer::start(const QString& fileName) { |
25 void Tracer::start(const QString &fileName) { |
26 qDebug() << "Starting using" << fileName; |
26 qDebug() << "Starting using" << fileName; |
27 |
27 |
28 bestSolution_ = 0; |
28 bestSolution_ = 0; |
29 solutions_.clear(); |
29 solutions_.clear(); |
30 generation_.clear(); |
30 generation_.clear(); |
31 image_ = QImage{}; |
31 referenceImage_ = QImage{}; |
32 |
32 |
33 if (palette_.isEmpty()) { |
33 if (palette_.isEmpty()) { |
34 qDebug("Empty palette"); |
34 qDebug("Empty palette"); |
35 return; |
35 return; |
36 } |
36 } |
37 |
37 |
38 image_.load(QUrl(fileName).toLocalFile()); |
38 referenceImage_.load(QUrl(fileName).toLocalFile()); |
39 |
39 |
40 if (image_.isNull()) { |
40 if (referenceImage_.isNull()) { |
41 qDebug("Failed to load image"); |
41 qDebug("Failed to load image"); |
42 return; |
42 return; |
43 } |
43 } |
44 |
44 |
45 for (int i = 0; i < 100; ++i) { |
45 for (int i = 0; i < 600; ++i) { |
46 generation_.append(Solution{{32, 32}, palette_}); |
46 generation_.append(Solution{{32, 32}, palette_}); |
47 } |
47 } |
48 } |
48 } |
49 |
49 |
50 void Tracer::step() { |
50 void Tracer::step() { |
|
51 const auto size = generation_.size(); |
|
52 const auto keepSize = 10; |
|
53 const auto replaceSize = 50; |
|
54 const auto kept = generation_.mid(0, keepSize); |
|
55 generation_ = generation_.mid(0, size - replaceSize); |
|
56 |
|
57 for (int i = 0; i < replaceSize; ++i) { |
|
58 generation_.append(Solution{{32, 32}, palette_}); |
|
59 } |
|
60 |
|
61 auto rg = QRandomGenerator::global(); |
|
62 |
|
63 for (qsizetype i = 0; i < size; i += 4) { |
|
64 const auto first = rg->bounded(size); |
|
65 const auto second = rg->bounded(size); |
|
66 |
|
67 if (first != second) { |
|
68 generation_[first].crossover(generation_[second]); |
|
69 } |
|
70 } |
|
71 |
|
72 std::for_each(std::begin(generation_), std::end(generation_), |
|
73 [this](auto &s) { s.mutate(palette_); }); |
|
74 |
|
75 std::for_each(std::begin(solutions_), std::end(solutions_), |
|
76 [this](const auto &fn) { QFile::remove(fn); }); |
51 solutions_.clear(); |
77 solutions_.clear(); |
52 |
78 |
53 for (auto& solution : generation_) { |
79 generation_.append(kept); |
54 const auto fileName = newFileName(); |
80 |
55 solutions_.append(fileName); |
81 for (auto &solution : generation_) { |
56 |
82 solution.render(newFileName()); |
57 solution.render(fileName); |
83 |
58 } |
84 solution.calculateFitness(referenceImage_); |
59 |
85 |
60 qDebug() << solutions_; |
86 solution.fitness += solution.cost() * 100; |
61 |
87 } |
|
88 |
|
89 std::sort(std::begin(generation_), std::end(generation_), |
|
90 [](const auto &a, const auto &b) { return a.fitness < b.fitness; }); |
|
91 |
|
92 std::for_each(std::begin(generation_) + size, std::end(generation_), |
|
93 [](const auto &s) { QFile::remove(s.fileName); }); |
|
94 generation_.remove(size, kept.size()); |
|
95 |
|
96 bestSolution_ = generation_[0].fitness; |
|
97 |
|
98 std::transform(std::begin(generation_), std::end(generation_), |
|
99 std::back_inserter(solutions_), |
|
100 [](const auto &a) { return a.fileName; }); |
|
101 |
|
102 emit bestSolutionChanged(); |
62 emit solutionsChanged(); |
103 emit solutionsChanged(); |
63 } |
104 } |
64 |
105 |
65 QStringList Tracer::solutions() const { return solutions_; } |
106 QStringList Tracer::solutions() const { return solutions_; } |
66 |
107 |
69 counter += 1; |
110 counter += 1; |
70 return tempDir_.filePath( |
111 return tempDir_.filePath( |
71 QStringLiteral("hedgehog_%1.svg").arg(counter, 3, 32, QChar(u'_'))); |
112 QStringLiteral("hedgehog_%1.svg").arg(counter, 3, 32, QChar(u'_'))); |
72 } |
113 } |
73 |
114 |
74 Solution::Solution(QSizeF size, const QList<QColor>& palette) : size{size} { |
115 Solution::Solution(QSizeF size, const QList<QColor> &palette) : size{size} { |
75 fitness = 0; |
116 fitness = 0; |
76 primitives = {Primitive(size, palette)}; |
117 primitives = {Primitive(size, palette), Primitive(size, palette)}; |
77 } |
118 } |
78 |
119 |
79 void Solution::render(const QString& fileName) const { |
120 void Solution::calculateFitness(const QImage &target) { |
|
121 QImage candidate{fileName}; |
|
122 |
|
123 if (candidate.isNull()) { |
|
124 fitness = 1e32; |
|
125 return; |
|
126 } |
|
127 |
|
128 // Both images assumed same size, same format |
|
129 double diffSum = 0; |
|
130 int width = target.width(); |
|
131 int height = target.height(); |
|
132 |
|
133 for (int y = 0; y < height; ++y) { |
|
134 auto candScan = reinterpret_cast<const QRgb *>(candidate.scanLine(y)); |
|
135 auto targScan = reinterpret_cast<const QRgb *>(target.scanLine(y)); |
|
136 for (int x = 0; x < width; ++x) { |
|
137 // Compare RGBA channels |
|
138 const QRgb cPix = candScan[x]; |
|
139 const QRgb tPix = targScan[x]; |
|
140 // const auto ca = qAlpha(cPix) / 255.0; |
|
141 const auto ta = qAlpha(tPix) / 255.0; |
|
142 const auto dr = qRed(cPix) - qRed(tPix); |
|
143 const auto dg = qGreen(cPix) - qGreen(tPix); |
|
144 const auto db = qBlue(cPix) - qBlue(tPix); |
|
145 const auto da = qAlpha(cPix) - qAlpha(tPix); |
|
146 diffSum += |
|
147 qMax(qMax(qMax(dr * dr, dg * dg), db * db) * ta, da * da * 1.0); |
|
148 } |
|
149 } |
|
150 |
|
151 fitness = diffSum; |
|
152 } |
|
153 |
|
154 void Solution::render(const QString &fileName) { |
|
155 this->fileName = fileName; |
|
156 |
80 const auto imageSize = size.toSize(); |
157 const auto imageSize = size.toSize(); |
81 |
158 |
82 QSvgGenerator generator; |
159 QSvgGenerator generator; |
83 generator.setFileName(fileName); |
160 generator.setFileName(fileName); |
84 generator.setSize(imageSize); |
161 generator.setSize(imageSize); |
118 double Solution::cost() const { |
195 double Solution::cost() const { |
119 return std::accumulate(primitives.constBegin(), primitives.constEnd(), 0, |
196 return std::accumulate(primitives.constBegin(), primitives.constEnd(), 0, |
120 [](auto a, auto p) { return a + p.cost(); }); |
197 [](auto a, auto p) { return a + p.cost(); }); |
121 } |
198 } |
122 |
199 |
123 Primitive::Primitive(QSizeF size, const QList<QColor>& palette) { |
200 void Solution::mutate(const QList<QColor> &palette) { |
|
201 if (primitives.isEmpty()) { |
|
202 return; |
|
203 } |
|
204 |
|
205 auto rg = QRandomGenerator::global(); |
|
206 double mutationRate = 0.05; |
|
207 |
|
208 if (rg->bounded(1.0) > mutationRate) { |
|
209 return; |
|
210 } |
|
211 |
|
212 for (auto &prim : primitives) { |
|
213 // Pen width |
|
214 if (rg->bounded(1.0) < mutationRate) { |
|
215 prim.pen.setWidthF(prim.pen.widthF() * (rg->bounded(1.5) + 0.5) + 0.05); |
|
216 } |
|
217 |
|
218 // Origin |
|
219 if (rg->bounded(1.0) < mutationRate) { |
|
220 prim.origin += QPointF(rg->bounded(10.0) - 5.0, rg->bounded(10.0) - 5.0); |
|
221 } |
|
222 |
|
223 if (prim.type == Polygon) { |
|
224 // Points |
|
225 for (auto &pt : prim.points) { |
|
226 if (rg->bounded(1.0) < mutationRate) { |
|
227 prim.origin += |
|
228 QPointF(rg->bounded(10.0) - 5.0, rg->bounded(10.0) - 5.0); |
|
229 } |
|
230 } |
|
231 } else { // Circle/ellipse |
|
232 if (rg->bounded(1.0) < mutationRate) { |
|
233 prim.radius1 *= rg->bounded(0.4) + 0.8; |
|
234 } |
|
235 if (rg->bounded(1.0) < mutationRate) { |
|
236 prim.radius2 *= rg->bounded(0.4) + 0.8; |
|
237 } |
|
238 if (rg->bounded(1.0) < mutationRate) { |
|
239 prim.rotation = rg->bounded(90.0); |
|
240 } |
|
241 } |
|
242 } |
|
243 |
|
244 if (rg->bounded(1.0) < mutationRate) { |
|
245 auto i = rg->bounded(primitives.size()); |
|
246 |
|
247 Primitive p{size, palette}; |
|
248 primitives.insert(i, p); |
|
249 } |
|
250 |
|
251 if (rg->bounded(1.0) < mutationRate) { |
|
252 auto i = rg->bounded(primitives.size()); |
|
253 |
|
254 primitives.remove(i); |
|
255 } |
|
256 } |
|
257 |
|
258 void Solution::crossover(Solution &other) { |
|
259 const auto n = qMin(primitives.size(), other.primitives.size()); |
|
260 |
|
261 auto rg = QRandomGenerator::global(); |
|
262 |
|
263 if (rg->bounded(1.0) < 0.02) { |
|
264 if (n <= 1) { |
|
265 return; |
|
266 } |
|
267 // swap tails |
|
268 const auto cp = rg->bounded(1, primitives.size()); |
|
269 const auto ocp = rg->bounded(1, other.primitives.size()); |
|
270 |
|
271 const auto tail = primitives.mid(cp); |
|
272 const auto otherTail = other.primitives.mid(ocp); |
|
273 |
|
274 primitives.remove(cp, primitives.size() - cp); |
|
275 other.primitives.remove(ocp, other.primitives.size() - ocp); |
|
276 |
|
277 primitives.append(otherTail); |
|
278 other.primitives.append(tail); |
|
279 } else { |
|
280 if (n < 1) { |
|
281 return; |
|
282 } |
|
283 // swap one element |
|
284 const auto cp = rg->bounded(primitives.size()); |
|
285 const auto ocp = rg->bounded(other.primitives.size()); |
|
286 |
|
287 qSwap(primitives[cp], other.primitives[ocp]); |
|
288 } |
|
289 } |
|
290 |
|
291 Primitive::Primitive(QSizeF size, const QList<QColor> &palette) { |
124 auto rg = QRandomGenerator::global(); |
292 auto rg = QRandomGenerator::global(); |
125 auto randomPoint = [&]() -> QPointF { |
293 auto randomPoint = [&]() -> QPointF { |
126 return {rg->bounded(size.width()), rg->bounded(size.height())}; |
294 return {rg->bounded(size.width()), rg->bounded(size.height())}; |
127 }; |
295 }; |
128 |
296 |