Explorar el Código

Initial commit.

simondevyoutube hace 5 años
padre
commit
7b0f6a16bb
Se han modificado 5 ficheros con 627 adiciones y 0 borrados
  1. BIN
      assets/square.jpg
  2. 137 0
      genetic-worker.js
  3. 68 0
      index.html
  4. 306 0
      main.js
  5. 116 0
      render.js

BIN
assets/square.jpg


+ 137 - 0
genetic-worker.js

@@ -0,0 +1,137 @@
+
+function lerp(x, a, b) {
+  return x * (b - a) + a;
+}
+
+function CalculateFitness(srcData, dstData) {
+  let fitness = 0;
+  const D1 = srcData.data;
+  const D2 = dstData.data;
+  for (let i = 0; i < D1.length; i+=4) {
+    for (let j = 0; j < 3; j++) {
+      const c1 = D1[i + j] / 255.0;
+      const c2 = D2[i + j] / 255.0;
+      fitness += (c1 - c2) ** 2;
+    }
+  }
+
+  fitness /= (srcData.width * srcData.height * 3);
+  fitness = Math.max(fitness, 0.001);
+  return 1.0 / fitness;
+}
+
+function DrawTexture_ELLIPSE(genotype, dstWidth, dstHeight) {
+  const canvas = new OffscreenCanvas(dstWidth, dstHeight);
+  const ctx = canvas.getContext('2d');
+
+  ctx.fillStyle = 'rgba(0, 0, 0, 1)';
+  ctx.fillRect(0, 0, dstWidth, dstHeight);
+
+  for (let gene of genotype) {
+    const r = gene[0] * 255;
+    const g = gene[1] * 255;
+    const b = gene[2] * 255;
+    const a = lerp(gene[3], 0.05, 0.25);
+    const x1 = gene[4] * dstWidth;
+    const y1 = gene[5] * dstHeight;
+    const w = lerp(gene[6], 0.01, 0.25) * dstWidth;
+    const h = lerp(gene[7], 0.01, 0.25) * dstHeight;
+    ctx.beginPath();
+    ctx.ellipse(x1, y1, w, h, 0, 0, 2 * Math.PI);
+    ctx.closePath();
+    ctx.fillStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
+    ctx.fill();
+  }
+
+  const data = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
+
+  return data;
+}
+
+function DrawTexture_LINE(genotype, dstWidth, dstHeight) {
+  const key = dstWidth + '-' + dstHeight;
+  if (!(key in _CONTEXTS)) {
+    const canvas = new OffscreenCanvas(dstWidth, dstHeight);
+    _CONTEXTS[key] = canvas.getContext('2d');
+  }
+
+  const ctx = _CONTEXTS[key];
+
+  ctx.fillStyle = 'rgba(0, 0, 0, 1)';
+  ctx.fillRect(0, 0, dstWidth, dstHeight);
+
+  for (let gene of genotype) {
+    const r = gene[0] * 255;
+    const g = gene[1] * 255;
+    const b = gene[2] * 255;
+    const a = lerp(gene[3], 0.05, 0.25);
+    const lw = gene[4] * dstWidth * 0.25;
+    const x1 = gene[5] * dstWidth;
+    const y1 = gene[6] * dstHeight;
+    const x2 = gene[7] * dstWidth;
+    const y2 = gene[8] * dstHeight;
+    ctx.strokeStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
+    ctx.lineWidth = lw;
+    ctx.beginPath();
+    ctx.moveTo(x1, y1);
+    ctx.lineTo(x2, y2);
+    ctx.closePath();
+    ctx.stroke();
+  }
+
+  const data = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
+
+  return data;
+}
+
+function ProcessWorkItem(e) {
+  const data = e.data;
+
+  if (data.action == 'setup') {
+    _FRAME_PARAMS = data;
+    return {action: 'ready'};
+  } else if (data.action == 'work') {
+    const fitnesses = [];
+
+    for (const workItem of data.work) {
+      let resultData = null;
+      if (data.type == 'line') {
+        resultData = DrawTexture_LINE(
+            workItem.genotype,
+            _FRAME_PARAMS.srcData.width, _FRAME_PARAMS.srcData.height);
+      } else if (data.type == 'ellipse') {
+        resultData = DrawTexture_ELLIPSE(
+            workItem.genotype,
+            _FRAME_PARAMS.srcData.width, _FRAME_PARAMS.srcData.height);
+      }
+
+      fitnesses.push({
+          fitness: CalculateFitness(_FRAME_PARAMS.srcData, resultData),
+          index: workItem.index,
+      });
+    }
+
+    return {
+        action: 'work-complete',
+        result: fitnesses
+    };
+  } else if (data.action == 'draw') {
+    let resultData = null;
+    if (data.type == 'line') {
+      resultData = DrawTexture_LINE(data.genotype, data.width, data.height);
+    } else if (data.type == 'ellipse') {
+      resultData = DrawTexture_ELLIPSE(data.genotype, data.width, data.height);
+    }
+    return {
+        action: 'work-complete',
+        result: {imageData: resultData}
+    };
+  }
+}
+
+let _FRAME_PARAMS = null;
+const _CONTEXTS = {};
+
+onmessage = function(e) {
+  postMessage(ProcessWorkItem(e));
+}

+ 68 - 0
index.html

@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<head>
+  <link href="https://fonts.googleapis.com/css?family=Roboto+Condensed&display=swap" rel="stylesheet">
+</head>
+<html>
+  <body>
+    <style>
+
+      canvas {
+        margin: 0.25em;
+      }
+
+      p {
+        font-family: 'Roboto Condensed', sans-serif;
+        font-size: 2em;
+        color: white;
+        width: 256px;
+        margin: 0.25em;
+      }
+
+      .right {
+        text-align:right;
+      }
+
+      .vertical {
+        display:flex;
+        justify-content: center;
+        align-items: center;
+        flex-direction: column;
+        height:100%;
+      }
+
+      .horizontal {
+        display:flex;
+        justify-content: center;
+        align-items: center;
+        flex-direction: row;
+      }
+
+      img {
+        display: none;
+      }
+
+      html, body {
+        padding: 0;
+        margin: 0;
+        background-color: black;
+        width: 100%;
+        height: 100%;
+      }
+    </style>
+
+    <div class="vertical">
+      <div class="horizontal">
+        <canvas id="source"></canvas>
+        <canvas id="target"></canvas>
+      </div>
+      <div class="horizontal">
+        <p id="statsText" class="right"></p>
+        <p id="numbersText"></p>
+      </div>
+    </div>
+
+    <script src="main.js" type="module"></script>
+
+    <img id="sourceImg"></img>
+  </body>
+</html>

+ 306 - 0
main.js

@@ -0,0 +1,306 @@
+
+import {render} from "./render.js";
+
+function rand_range(a, b) {
+  return Math.random() * (b - a) + a;
+}
+
+function rand_normalish() {
+  const r = Math.random() + Math.random() + Math.random() + Math.random();
+  return (r / 4.0) * 2.0 - 1;
+}
+
+function lerp(x, a, b) {
+  return x * (b - a) + a;
+}
+
+function clamp(x, a, b) {
+  return Math.min(Math.max(x, a), b);
+}
+
+function sat(x) {
+  return Math.min(Math.max(x, 0.0), 1.0);
+}
+
+
+class Population {
+  constructor(params) {
+    this._params = params;
+    this._population = [...Array(this._params.population_size)].map(
+        _ => ({fitness: 1, genotype: this._CreateRandomgenotype()}));
+    this._lastGeneration = null;
+    this._generations = 0;
+    this._callback = null;
+  }
+
+  _CreateRandomGene() {
+    return this._params.gene.ranges.map(r => rand_range(r[0], r[1]));
+  }
+
+  _CreateRandomgenotype() {
+    return [...Array(this._params.genotype.size)].map(
+        _ => this._CreateRandomGene());
+  }
+
+  Fittest() {
+    return this._lastGeneration.parents[0];
+  }
+
+  async Run(srcData) {
+    await render.setup(srcData);
+
+    while (true) {
+      await this._Step(srcData);
+    }
+  }
+
+  async _Step(tgtImgData) {
+    await this._StepPopulation(tgtImgData);
+
+    const parents = this._population.sort((a, b) => (b.fitness - a.fitness));
+
+    this._lastGeneration = {parents: parents};
+    this._generations += 1;
+
+    // Draw the main canvas on the worker while breeding next population.
+    const cbPromise = this._callback(this, this._lastGeneration.parents[0]);
+
+    this._population = this._BreedNewPopulation(parents);
+
+    if (this._params.genotype.growth_per_increase > 0 ||
+        this._params.genotype.size < this._params.genotype.max_size) {
+      const increase = (
+          (this._generations + 1) % this._params.genotype.generations_per_increase) == 0;
+      if (increase) {
+        const geneIncrease = this._params.genotype.growth_per_increase;
+        this._params.genotype.size += geneIncrease;
+
+        for (let i = 0; i < geneIncrease; i++) {
+          for (let p of this._population) {
+            p.genotype.push([...this._CreateRandomGene()]);
+          }
+        }
+      }
+    }
+    await cbPromise;
+  }
+
+  async _StepPopulation(tgtImgData) {
+    // Wait for them all to be done
+    const promises = render.calculateFitnesses(
+        this._params.gene.type, this._population.map(p => p.genotype));
+    const responses = await Promise.all(promises);
+
+    for (const r of responses) {
+      for (const f of r.result) {
+        this._population[f.index].fitness = f.fitness;
+      }
+    }
+  }
+
+  _BreedNewPopulation(parents) {
+    function _RouletteSelection(sortedParents, totalFitness) {
+      const roll = Math.random() * totalFitness;
+      let sum = 0;
+      for (let p of sortedParents) {
+        sum += p.fitness;
+        if (roll < sum) {
+          return p;
+        }
+      }
+      return sortedParents[sortedParents.length - 1];
+    }
+
+    function _RandomParent(sortedParents, otherParent, totalFitness) {
+      const p = _RouletteSelection(sortedParents, totalFitness);
+      return p;
+    }
+
+    function _CopyGenotype(g) {
+      return ({
+          fitness: g.fitness,
+          genotype: [...g.genotype].map(gene => [...gene])
+      });
+    }
+
+    const newPopulation = [];
+    const totalFitness = parents.reduce((t, p) => t + p.fitness, 0);
+    const numChildren = Math.ceil(parents.length * 0.8);
+
+    const top = [...parents.slice(0, Math.ceil(parents.length * 0.25))];
+    for (let j = 0; j < numChildren; j++) {
+      const i = j % top.length;
+      const p1 = top[i];
+      const p2 = _RandomParent(parents, p1, totalFitness);
+
+      const g = [];
+      for (let r = 0; r < p1.genotype.length; r++ ) {
+        const roll = Math.random();
+        g.push(roll < 0.5 ? p1.genotype[r] : p2.genotype[r]);
+      }
+      newPopulation.push(_CopyGenotype({fitness: 1, genotype: g}));
+    }
+
+    // Let's say keep top X% go through, but with mutations
+    const top5 = [...parents.slice(0, Math.ceil(parents.length * 0.05))];
+
+    newPopulation.push(...top5.map(x => _CopyGenotype(x)));
+
+    // Mutations!
+    for (let p of newPopulation) {
+      const genotypeLength = p.genotype.length;
+      const mutationOdds = this._params.mutation.odds;
+      const mutationMagnitude = this._params.mutation.magnitude;
+      const mutationDecay = this._params.mutation.decay;
+      function _Mutate(x, i) {
+        const roll = Math.random();
+
+        if (roll < mutationOdds) {
+          const xi = genotypeLength - i;
+          const mutationMod = Math.E ** (-1 * xi * mutationDecay);
+          if (mutationMod <= 0.0001) {
+            return x;
+          }
+          const magnitude = mutationMagnitude * mutationMod * rand_normalish();
+          return sat(x + magnitude);
+        }
+        return x;
+      }
+
+      p.genotype = p.genotype.map(
+          (g, i) => g.map(
+              (x, xi) => _Mutate(x, i)));
+    }
+
+    // Immortality granted to the winners from the last life. May the odds be
+    // forever in your favour.
+    newPopulation.push(...top5.map(x => _CopyGenotype(x)));
+
+    // Create a bunch of random crap to fill out the rest.
+    while (newPopulation.length < parents.length) {
+      newPopulation.push(
+          {fitness: 1, genotype: this._CreateRandomgenotype()});
+    }
+
+    return newPopulation;
+  }
+}
+
+
+class GeneticAlgorithmDemo {
+  constructor() {
+    this._Init();
+  }
+
+  _Init(scene) {
+    this._statsText1 = document.getElementById('statsText');
+    this._statsText2 = document.getElementById('numbersText');
+    this._sourceImg = document.getElementById('sourceImg');
+    this._sourceImg.src = 'assets/square.jpg';
+    this._sourceImg.onload = () => {
+      const ctx = this._sourceCanvas.getContext('2d');
+
+      this._sourceCanvas.width = 128;
+      this._sourceCanvas.height = this._sourceCanvas.width * (
+          this._sourceImg.height / this._sourceImg.width);
+
+      ctx.drawImage(
+          this._sourceImg,
+          0, 0, this._sourceImg.width, this._sourceImg.height,
+          0, 0, this._sourceCanvas.width, this._sourceCanvas.height);
+
+      this._sourceLODData = ctx.getImageData(
+          0, 0, this._sourceCanvas.width, this._sourceCanvas.height);
+
+      this._sourceCanvas.width = 800;
+      this._sourceCanvas.height = this._sourceCanvas.width * (
+          this._sourceImg.height / this._sourceImg.width);
+      this._targetCanvas.width = this._sourceCanvas.width;
+      this._targetCanvas.height = this._sourceCanvas.height;
+
+      ctx.drawImage(
+          this._sourceImg,
+          0, 0, this._sourceImg.width, this._sourceImg.height,
+          0, 0, this._sourceCanvas.width, this._sourceCanvas.height);
+
+      this._InitPopulation();
+    };
+
+    this._sourceCanvas = document.getElementById('source');
+    this._targetCanvas = document.getElementById('target');
+  }
+
+  _InitPopulation() {
+    const GENE_ELLIPSE = {
+      type: 'ellipse',
+      ranges: [
+        [0, 1],
+        [0, 1],
+        [0, 1],
+        [0.01, 0.1],
+        [0, 1],
+        [0, 1],
+        [0.05, 0.5],
+        [0, 1],
+      ]
+    };
+
+    const GENE_LINE = {
+      type: 'line',
+      ranges: [
+        [0, 1],
+        [0, 1],
+        [0, 1],
+        [0.05, 0.2],
+        [0, 1],
+        [0, 1],
+        [0, 1],
+        [0, 1],
+        [0, 1],
+      ]
+    };
+
+    const params = {
+      population_size: 512,
+      genotype: {
+        size: 64,
+        max_size: 1000,
+        generations_per_increase: 50,
+        growth_per_increase: 1
+      },
+      gene: GENE_LINE,
+      mutation: {
+        magnitude: 0.25,
+        odds: 0.1,
+        decay: 0,
+      }
+    };
+
+    this._population = new Population(params);
+    this._population._callback = async (population, fittest) => {
+      const p1 = render.draw(
+          population._params.gene.type, fittest.genotype,
+          this._targetCanvas.width, this._targetCanvas.height);
+
+      const hd = await p1;
+
+      const ctx = this._targetCanvas.getContext('2d');
+      ctx.putImageData(hd.result.imageData, 0, 0);
+
+      this._statsText2.innerText =
+          this._population._generations + '\n' +
+          this._population.Fittest().fitness.toFixed(3) + '\n' +
+          this._population._population.length + '\n' +
+          this._population._params.genotype.size;
+    };
+    this._population.Run(this._sourceLODData);
+
+    this._statsText1.innerText =
+        'Generation:\n' +
+        'Fitness:\n' +
+        'Population:\n' +
+        'Genes:';
+  }
+}
+
+const _DEMO = new GeneticAlgorithmDemo();

+ 116 - 0
render.js

@@ -0,0 +1,116 @@
+export const render = (function() {
+
+  let _IDs = 0;
+
+  class PWorker {
+    constructor(s) {
+      this._worker = new Worker(s);
+      this._worker.onmessage = (e) => {
+        this._OnMessage(e);
+      };
+      this._resolve = null;
+      this._id = _IDs++;
+    }
+
+    _OnMessage(e) {
+      const resolve = this._resolve;
+      this._resolve = null;
+      resolve(e.data);
+    }
+
+    get id() {
+      return this._id;
+    }
+
+    sendAsync(s) {
+      return new Promise((resolve) => {
+        this._resolve = resolve;
+        this._worker.postMessage(s);
+      });
+    }
+  }
+
+  class PWorkerPool {
+    constructor(sz, entry) {
+      this._workers = [...Array(sz)].map(_ => new PWorker(entry));
+      this._free = [...this._workers];
+      this._busy = {};
+      this._queue = [];
+    }
+
+    get length() {
+      return this._workers.length;
+    }
+
+    Broadcast(msg) {
+      return Promise.all(this._workers.map(w => w.sendAsync(msg)));
+    }
+
+    Enqueue(workItem) {
+      return new Promise(resolve => {
+          this._queue.push([workItem, resolve]);
+          this._PumpQueue();
+      });
+    }
+
+    _PumpQueue() {
+      while (this._free.length > 0 && this._queue.length > 0) {
+        const w = this._free.pop();
+        this._busy[w.id] = w;
+
+        const [workItem, workResolve] = this._queue.shift();
+
+        w.sendAsync(workItem).then((v) => {
+          delete this._busy[w.id];
+          this._free.push(w);
+          workResolve(v);
+          this._PumpQueue();
+        });
+      }
+    }
+  }
+  const _POOL = new PWorkerPool(
+      navigator.hardwareConcurrency, 'genetic-worker.js');
+
+
+  return {
+    setup: function(srcData) {
+      const setupMsg = {
+          action: 'setup',
+          srcData: srcData,
+      };
+      return _POOL.Broadcast(setupMsg);
+    },
+
+    draw: function(type, genotype, width, height) {
+      const p = _POOL.Enqueue({
+          action: 'draw',
+          type: type,
+          genotype: genotype,
+          width: width,
+          height: height
+      });
+      return p;
+    },
+
+    calculateFitnesses: function(type, genotypes) {
+      // Wait for them all to be done
+      const workItems = genotypes.map((g, i) => ({genotype: g, index: i}));
+
+      const chunkSize = genotypes.length / _POOL.length;
+      const promises = [];
+
+      while (workItems.length > 0) {
+        const workSet = workItems.splice(0, chunkSize);
+        const workItem = {
+          action: 'work',
+          work: workSet,
+          type: type,
+        };
+        promises.push(_POOL.Enqueue(workItem));
+      }
+
+      return promises;
+    },
+  };
+})();