├── LICENSE
├── assets
└── square.jpg
├── genetic-worker.js
├── index.html
├── main.js
└── render.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 simondevyoutube
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/assets/square.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/GeneticAlgorithm-Picture/7b0f6a16bb75a9f3cce2a259804759ca4ed7a677/assets/square.jpg
--------------------------------------------------------------------------------
/genetic-worker.js:
--------------------------------------------------------------------------------
1 |
2 | function lerp(x, a, b) {
3 | return x * (b - a) + a;
4 | }
5 |
6 | function CalculateFitness(srcData, dstData) {
7 | let fitness = 0;
8 | const D1 = srcData.data;
9 | const D2 = dstData.data;
10 | for (let i = 0; i < D1.length; i+=4) {
11 | for (let j = 0; j < 3; j++) {
12 | const c1 = D1[i + j] / 255.0;
13 | const c2 = D2[i + j] / 255.0;
14 | fitness += (c1 - c2) ** 2;
15 | }
16 | }
17 |
18 | fitness /= (srcData.width * srcData.height * 3);
19 | fitness = Math.max(fitness, 0.001);
20 | return 1.0 / fitness;
21 | }
22 |
23 | function DrawTexture_ELLIPSE(genotype, dstWidth, dstHeight) {
24 | const canvas = new OffscreenCanvas(dstWidth, dstHeight);
25 | const ctx = canvas.getContext('2d');
26 |
27 | ctx.fillStyle = 'rgba(0, 0, 0, 1)';
28 | ctx.fillRect(0, 0, dstWidth, dstHeight);
29 |
30 | for (let gene of genotype) {
31 | const r = gene[0] * 255;
32 | const g = gene[1] * 255;
33 | const b = gene[2] * 255;
34 | const a = lerp(gene[3], 0.05, 0.25);
35 | const x1 = gene[4] * dstWidth;
36 | const y1 = gene[5] * dstHeight;
37 | const w = lerp(gene[6], 0.01, 0.25) * dstWidth;
38 | const h = lerp(gene[7], 0.01, 0.25) * dstHeight;
39 | ctx.beginPath();
40 | ctx.ellipse(x1, y1, w, h, 0, 0, 2 * Math.PI);
41 | ctx.closePath();
42 | ctx.fillStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
43 | ctx.fill();
44 | }
45 |
46 | const data = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
47 |
48 | return data;
49 | }
50 |
51 | function DrawTexture_LINE(genotype, dstWidth, dstHeight) {
52 | const key = dstWidth + '-' + dstHeight;
53 | if (!(key in _CONTEXTS)) {
54 | const canvas = new OffscreenCanvas(dstWidth, dstHeight);
55 | _CONTEXTS[key] = canvas.getContext('2d');
56 | }
57 |
58 | const ctx = _CONTEXTS[key];
59 |
60 | ctx.fillStyle = 'rgba(0, 0, 0, 1)';
61 | ctx.fillRect(0, 0, dstWidth, dstHeight);
62 |
63 | for (let gene of genotype) {
64 | const r = gene[0] * 255;
65 | const g = gene[1] * 255;
66 | const b = gene[2] * 255;
67 | const a = lerp(gene[3], 0.05, 0.25);
68 | const lw = gene[4] * dstWidth * 0.25;
69 | const x1 = gene[5] * dstWidth;
70 | const y1 = gene[6] * dstHeight;
71 | const x2 = gene[7] * dstWidth;
72 | const y2 = gene[8] * dstHeight;
73 | ctx.strokeStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
74 | ctx.lineWidth = lw;
75 | ctx.beginPath();
76 | ctx.moveTo(x1, y1);
77 | ctx.lineTo(x2, y2);
78 | ctx.closePath();
79 | ctx.stroke();
80 | }
81 |
82 | const data = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
83 |
84 | return data;
85 | }
86 |
87 | function ProcessWorkItem(e) {
88 | const data = e.data;
89 |
90 | if (data.action == 'setup') {
91 | _FRAME_PARAMS = data;
92 | return {action: 'ready'};
93 | } else if (data.action == 'work') {
94 | const fitnesses = [];
95 |
96 | for (const workItem of data.work) {
97 | let resultData = null;
98 | if (data.type == 'line') {
99 | resultData = DrawTexture_LINE(
100 | workItem.genotype,
101 | _FRAME_PARAMS.srcData.width, _FRAME_PARAMS.srcData.height);
102 | } else if (data.type == 'ellipse') {
103 | resultData = DrawTexture_ELLIPSE(
104 | workItem.genotype,
105 | _FRAME_PARAMS.srcData.width, _FRAME_PARAMS.srcData.height);
106 | }
107 |
108 | fitnesses.push({
109 | fitness: CalculateFitness(_FRAME_PARAMS.srcData, resultData),
110 | index: workItem.index,
111 | });
112 | }
113 |
114 | return {
115 | action: 'work-complete',
116 | result: fitnesses
117 | };
118 | } else if (data.action == 'draw') {
119 | let resultData = null;
120 | if (data.type == 'line') {
121 | resultData = DrawTexture_LINE(data.genotype, data.width, data.height);
122 | } else if (data.type == 'ellipse') {
123 | resultData = DrawTexture_ELLIPSE(data.genotype, data.width, data.height);
124 | }
125 | return {
126 | action: 'work-complete',
127 | result: {imageData: resultData}
128 | };
129 | }
130 | }
131 |
132 | let _FRAME_PARAMS = null;
133 | const _CONTEXTS = {};
134 |
135 | onmessage = function(e) {
136 | postMessage(ProcessWorkItem(e));
137 | }
138 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 |
2 | import {render} from "./render.js";
3 |
4 | function rand_range(a, b) {
5 | return Math.random() * (b - a) + a;
6 | }
7 |
8 | function rand_normalish() {
9 | const r = Math.random() + Math.random() + Math.random() + Math.random();
10 | return (r / 4.0) * 2.0 - 1;
11 | }
12 |
13 | function lerp(x, a, b) {
14 | return x * (b - a) + a;
15 | }
16 |
17 | function clamp(x, a, b) {
18 | return Math.min(Math.max(x, a), b);
19 | }
20 |
21 | function sat(x) {
22 | return Math.min(Math.max(x, 0.0), 1.0);
23 | }
24 |
25 |
26 | class Population {
27 | constructor(params) {
28 | this._params = params;
29 | this._population = [...Array(this._params.population_size)].map(
30 | _ => ({fitness: 1, genotype: this._CreateRandomgenotype()}));
31 | this._lastGeneration = null;
32 | this._generations = 0;
33 | this._callback = null;
34 | }
35 |
36 | _CreateRandomGene() {
37 | return this._params.gene.ranges.map(r => rand_range(r[0], r[1]));
38 | }
39 |
40 | _CreateRandomgenotype() {
41 | return [...Array(this._params.genotype.size)].map(
42 | _ => this._CreateRandomGene());
43 | }
44 |
45 | Fittest() {
46 | return this._lastGeneration.parents[0];
47 | }
48 |
49 | async Run(srcData) {
50 | await render.setup(srcData);
51 |
52 | while (true) {
53 | await this._Step(srcData);
54 | }
55 | }
56 |
57 | async _Step(tgtImgData) {
58 | await this._StepPopulation(tgtImgData);
59 |
60 | const parents = this._population.sort((a, b) => (b.fitness - a.fitness));
61 |
62 | this._lastGeneration = {parents: parents};
63 | this._generations += 1;
64 |
65 | // Draw the main canvas on the worker while breeding next population.
66 | const cbPromise = this._callback(this, this._lastGeneration.parents[0]);
67 |
68 | this._population = this._BreedNewPopulation(parents);
69 |
70 | if (this._params.genotype.growth_per_increase > 0 ||
71 | this._params.genotype.size < this._params.genotype.max_size) {
72 | const increase = (
73 | (this._generations + 1) % this._params.genotype.generations_per_increase) == 0;
74 | if (increase) {
75 | const geneIncrease = this._params.genotype.growth_per_increase;
76 | this._params.genotype.size += geneIncrease;
77 |
78 | for (let i = 0; i < geneIncrease; i++) {
79 | for (let p of this._population) {
80 | p.genotype.push([...this._CreateRandomGene()]);
81 | }
82 | }
83 | }
84 | }
85 | await cbPromise;
86 | }
87 |
88 | async _StepPopulation(tgtImgData) {
89 | // Wait for them all to be done
90 | const promises = render.calculateFitnesses(
91 | this._params.gene.type, this._population.map(p => p.genotype));
92 | const responses = await Promise.all(promises);
93 |
94 | for (const r of responses) {
95 | for (const f of r.result) {
96 | this._population[f.index].fitness = f.fitness;
97 | }
98 | }
99 | }
100 |
101 | _BreedNewPopulation(parents) {
102 | function _RouletteSelection(sortedParents, totalFitness) {
103 | const roll = Math.random() * totalFitness;
104 | let sum = 0;
105 | for (let p of sortedParents) {
106 | sum += p.fitness;
107 | if (roll < sum) {
108 | return p;
109 | }
110 | }
111 | return sortedParents[sortedParents.length - 1];
112 | }
113 |
114 | function _RandomParent(sortedParents, otherParent, totalFitness) {
115 | const p = _RouletteSelection(sortedParents, totalFitness);
116 | return p;
117 | }
118 |
119 | function _CopyGenotype(g) {
120 | return ({
121 | fitness: g.fitness,
122 | genotype: [...g.genotype].map(gene => [...gene])
123 | });
124 | }
125 |
126 | const newPopulation = [];
127 | const totalFitness = parents.reduce((t, p) => t + p.fitness, 0);
128 | const numChildren = Math.ceil(parents.length * 0.8);
129 |
130 | const top = [...parents.slice(0, Math.ceil(parents.length * 0.25))];
131 | for (let j = 0; j < numChildren; j++) {
132 | const i = j % top.length;
133 | const p1 = top[i];
134 | const p2 = _RandomParent(parents, p1, totalFitness);
135 |
136 | const g = [];
137 | for (let r = 0; r < p1.genotype.length; r++ ) {
138 | const roll = Math.random();
139 | g.push(roll < 0.5 ? p1.genotype[r] : p2.genotype[r]);
140 | }
141 | newPopulation.push(_CopyGenotype({fitness: 1, genotype: g}));
142 | }
143 |
144 | // Let's say keep top X% go through, but with mutations
145 | const top5 = [...parents.slice(0, Math.ceil(parents.length * 0.05))];
146 |
147 | newPopulation.push(...top5.map(x => _CopyGenotype(x)));
148 |
149 | // Mutations!
150 | for (let p of newPopulation) {
151 | const genotypeLength = p.genotype.length;
152 | const mutationOdds = this._params.mutation.odds;
153 | const mutationMagnitude = this._params.mutation.magnitude;
154 | const mutationDecay = this._params.mutation.decay;
155 | function _Mutate(x, i) {
156 | const roll = Math.random();
157 |
158 | if (roll < mutationOdds) {
159 | const xi = genotypeLength - i;
160 | const mutationMod = Math.E ** (-1 * xi * mutationDecay);
161 | if (mutationMod <= 0.0001) {
162 | return x;
163 | }
164 | const magnitude = mutationMagnitude * mutationMod * rand_normalish();
165 | return sat(x + magnitude);
166 | }
167 | return x;
168 | }
169 |
170 | p.genotype = p.genotype.map(
171 | (g, i) => g.map(
172 | (x, xi) => _Mutate(x, i)));
173 | }
174 |
175 | // Immortality granted to the winners from the last life. May the odds be
176 | // forever in your favour.
177 | newPopulation.push(...top5.map(x => _CopyGenotype(x)));
178 |
179 | // Create a bunch of random crap to fill out the rest.
180 | while (newPopulation.length < parents.length) {
181 | newPopulation.push(
182 | {fitness: 1, genotype: this._CreateRandomgenotype()});
183 | }
184 |
185 | return newPopulation;
186 | }
187 | }
188 |
189 |
190 | class GeneticAlgorithmDemo {
191 | constructor() {
192 | this._Init();
193 | }
194 |
195 | _Init(scene) {
196 | this._statsText1 = document.getElementById('statsText');
197 | this._statsText2 = document.getElementById('numbersText');
198 | this._sourceImg = document.getElementById('sourceImg');
199 | this._sourceImg.src = 'assets/square.jpg';
200 | this._sourceImg.onload = () => {
201 | const ctx = this._sourceCanvas.getContext('2d');
202 |
203 | this._sourceCanvas.width = 128;
204 | this._sourceCanvas.height = this._sourceCanvas.width * (
205 | this._sourceImg.height / this._sourceImg.width);
206 |
207 | ctx.drawImage(
208 | this._sourceImg,
209 | 0, 0, this._sourceImg.width, this._sourceImg.height,
210 | 0, 0, this._sourceCanvas.width, this._sourceCanvas.height);
211 |
212 | this._sourceLODData = ctx.getImageData(
213 | 0, 0, this._sourceCanvas.width, this._sourceCanvas.height);
214 |
215 | this._sourceCanvas.width = 800;
216 | this._sourceCanvas.height = this._sourceCanvas.width * (
217 | this._sourceImg.height / this._sourceImg.width);
218 | this._targetCanvas.width = this._sourceCanvas.width;
219 | this._targetCanvas.height = this._sourceCanvas.height;
220 |
221 | ctx.drawImage(
222 | this._sourceImg,
223 | 0, 0, this._sourceImg.width, this._sourceImg.height,
224 | 0, 0, this._sourceCanvas.width, this._sourceCanvas.height);
225 |
226 | this._InitPopulation();
227 | };
228 |
229 | this._sourceCanvas = document.getElementById('source');
230 | this._targetCanvas = document.getElementById('target');
231 | }
232 |
233 | _InitPopulation() {
234 | const GENE_ELLIPSE = {
235 | type: 'ellipse',
236 | ranges: [
237 | [0, 1],
238 | [0, 1],
239 | [0, 1],
240 | [0.01, 0.1],
241 | [0, 1],
242 | [0, 1],
243 | [0.05, 0.5],
244 | [0, 1],
245 | ]
246 | };
247 |
248 | const GENE_LINE = {
249 | type: 'line',
250 | ranges: [
251 | [0, 1],
252 | [0, 1],
253 | [0, 1],
254 | [0.05, 0.2],
255 | [0, 1],
256 | [0, 1],
257 | [0, 1],
258 | [0, 1],
259 | [0, 1],
260 | ]
261 | };
262 |
263 | const params = {
264 | population_size: 512,
265 | genotype: {
266 | size: 64,
267 | max_size: 1000,
268 | generations_per_increase: 50,
269 | growth_per_increase: 1
270 | },
271 | gene: GENE_LINE,
272 | mutation: {
273 | magnitude: 0.25,
274 | odds: 0.1,
275 | decay: 0,
276 | }
277 | };
278 |
279 | this._population = new Population(params);
280 | this._population._callback = async (population, fittest) => {
281 | const p1 = render.draw(
282 | population._params.gene.type, fittest.genotype,
283 | this._targetCanvas.width, this._targetCanvas.height);
284 |
285 | const hd = await p1;
286 |
287 | const ctx = this._targetCanvas.getContext('2d');
288 | ctx.putImageData(hd.result.imageData, 0, 0);
289 |
290 | this._statsText2.innerText =
291 | this._population._generations + '\n' +
292 | this._population.Fittest().fitness.toFixed(3) + '\n' +
293 | this._population._population.length + '\n' +
294 | this._population._params.genotype.size;
295 | };
296 | this._population.Run(this._sourceLODData);
297 |
298 | this._statsText1.innerText =
299 | 'Generation:\n' +
300 | 'Fitness:\n' +
301 | 'Population:\n' +
302 | 'Genes:';
303 | }
304 | }
305 |
306 | const _DEMO = new GeneticAlgorithmDemo();
307 |
--------------------------------------------------------------------------------
/render.js:
--------------------------------------------------------------------------------
1 | export const render = (function() {
2 |
3 | let _IDs = 0;
4 |
5 | class PWorker {
6 | constructor(s) {
7 | this._worker = new Worker(s);
8 | this._worker.onmessage = (e) => {
9 | this._OnMessage(e);
10 | };
11 | this._resolve = null;
12 | this._id = _IDs++;
13 | }
14 |
15 | _OnMessage(e) {
16 | const resolve = this._resolve;
17 | this._resolve = null;
18 | resolve(e.data);
19 | }
20 |
21 | get id() {
22 | return this._id;
23 | }
24 |
25 | sendAsync(s) {
26 | return new Promise((resolve) => {
27 | this._resolve = resolve;
28 | this._worker.postMessage(s);
29 | });
30 | }
31 | }
32 |
33 | class PWorkerPool {
34 | constructor(sz, entry) {
35 | this._workers = [...Array(sz)].map(_ => new PWorker(entry));
36 | this._free = [...this._workers];
37 | this._busy = {};
38 | this._queue = [];
39 | }
40 |
41 | get length() {
42 | return this._workers.length;
43 | }
44 |
45 | Broadcast(msg) {
46 | return Promise.all(this._workers.map(w => w.sendAsync(msg)));
47 | }
48 |
49 | Enqueue(workItem) {
50 | return new Promise(resolve => {
51 | this._queue.push([workItem, resolve]);
52 | this._PumpQueue();
53 | });
54 | }
55 |
56 | _PumpQueue() {
57 | while (this._free.length > 0 && this._queue.length > 0) {
58 | const w = this._free.pop();
59 | this._busy[w.id] = w;
60 |
61 | const [workItem, workResolve] = this._queue.shift();
62 |
63 | w.sendAsync(workItem).then((v) => {
64 | delete this._busy[w.id];
65 | this._free.push(w);
66 | workResolve(v);
67 | this._PumpQueue();
68 | });
69 | }
70 | }
71 | }
72 | const _POOL = new PWorkerPool(
73 | navigator.hardwareConcurrency, 'genetic-worker.js');
74 |
75 |
76 | return {
77 | setup: function(srcData) {
78 | const setupMsg = {
79 | action: 'setup',
80 | srcData: srcData,
81 | };
82 | return _POOL.Broadcast(setupMsg);
83 | },
84 |
85 | draw: function(type, genotype, width, height) {
86 | const p = _POOL.Enqueue({
87 | action: 'draw',
88 | type: type,
89 | genotype: genotype,
90 | width: width,
91 | height: height
92 | });
93 | return p;
94 | },
95 |
96 | calculateFitnesses: function(type, genotypes) {
97 | // Wait for them all to be done
98 | const workItems = genotypes.map((g, i) => ({genotype: g, index: i}));
99 |
100 | const chunkSize = genotypes.length / _POOL.length;
101 | const promises = [];
102 |
103 | while (workItems.length > 0) {
104 | const workSet = workItems.splice(0, chunkSize);
105 | const workItem = {
106 | action: 'work',
107 | work: workSet,
108 | type: type,
109 | };
110 | promises.push(_POOL.Enqueue(workItem));
111 | }
112 |
113 | return promises;
114 | },
115 | };
116 | })();
117 |
--------------------------------------------------------------------------------