├── 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 |
59 |

60 |

61 |
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 | --------------------------------------------------------------------------------