Il reste à voir deux fonctions techniques pour faire tourner notre programme: la copie d’un individu et le rendu d’une image. Je les appelle techniques car elles n’ont rien à voir avec l’algorithme lui-même. La copie d’un individu est rendue nécessaire par le langage utilisé, Javascript, et le rendu d’une image est nécessaire puisqu’on veut …et bien… afficher des images, quoi…

Voyons d’abord la copie d’un individu:

function copy(individual) {
  var indiCopy = [];
  for(var i = 0; i < TOTAL_SQUARES; i++) {
    var objectCopy = {},
        prop;
    for(prop in individual[i]) {
      objectCopy[prop] = individual[i][prop];
    }
    indiCopy.push(objectCopy);
  }
  return indiCopy;
}

Un individu est un tableau, contenant des objets, chaque objets contenants des propriétés… Là, je m’interroge et je demande l’avis de spécialistes: est-ce-qu’il ne vaudrait pas mieux utiliser une librairie pour faire ça, comme jQuery ou Underscore.js ?

Maintenant le rendu d’une image:

function renderIndividual(individual, ctx) {
  ctx.fillStyle = "white";
  ctx.fillRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
  for (var i = 0; i < TOTAL_SQUARES; i++) {
    ctx.globalAlpha = individual[i].alpha;
    ctx.fillStyle = 'rgb(' + individual[i].red + ',' +
      individual[i].green + ',' + individual[i].blue + ')';
    ctx.fillRect(individual[i].x, individual[i].y,
      individual[i].size, individual[i].size);
  }
}

ctx est un contexte de Canvas. Je vois ça tout simplement comme un objet dans lequel on peut dessiner. Tout d’abord on efface l’image en la remplissant de blanc:

ctx.fillStyle = "white";
ctx.fillRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);

Puis on dessine chacuns des carrés:

for (var i = 0; i < TOTAL_SQUARES; i++) {

Pour chaque carré il faut sélectionner sa transparence:

ctx.globalAlpha = individual[i].alpha;

Puis sa couleur:

ctx.fillStyle = 'rgb(' + individual[i].red + ',' +
  individual[i].green + ',' + individual[i].blue + ')';

On peut alors dessiner un carré:

ctx.fillRect(individual[i].x, individual[i].y,
  individual[i].size, individual[i].size);

Voilà. Reste à voir maintenant le programme dans son ensemble. Voici les fichiers HTML et CSS:

<!DOCTYPE html>
<html lang="fr">
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" type="text/css" href="picture.css" /> 
  </head>
  <body>
    <canvas width="400" height="400" id="canvas1"></canvas>
    <canvas width="400" height="400" id="canvas2"></canvas>
    <p id="generation">0</p>
    <p id="quality">0</p>
    <script src="picture.js"></script>
  </body>
</html>
body {
  background-color: #222;
}

p {
  color: #ccc;
}

Et voici le programme Javascript complet:

var canvasImgOrigin = document.getElementById('canvas1');
var canvasGenetic = document.getElementById('canvas2');
var ctxOrigin = canvas1.getContext('2d');
var ctx = canvas2.getContext('2d');
var TOTAL_SQUARES = 400;
var IMAGE_WIDTH = 400;
var IMAGE_HEIGHT = 400;
var SQUARE_MAX_SIZE = 40;
var img = new Image();
var generation = 0;
var htmlGeneration = document.getElementById("generation");
var htmlQuality = document.getElementById("quality");
var solution = [];
var canvasBuffer = document.createElement('canvas');
canvasBuffer.width = IMAGE_WIDTH;
canvasBuffer.height = IMAGE_HEIGHT;
var ctxBuffer = canvasBuffer.getContext('2d');

img.onload = function() { ctxOrigin.drawImage(img, 0, 0); };
img.src = 'photo.jpg';
solution = makeIndividual();

var interval = setInterval(hillClimb, 150);

function makeIndividual() {
  var individual = [];
  for (var i = 0; i < TOTAL_SQUARES; i++) {
    individual.push({
      x: Math.floor(Math.random() * IMAGE_WIDTH),
      y: Math.floor(Math.random() * IMAGE_HEIGHT),
      size: Math.floor(Math.random() * SQUARE_MAX_SIZE),
      red: Math.floor(Math.random() * 256),
      green: Math.floor(Math.random() * 256),
      blue: Math.floor(Math.random() * 256),
      alpha: Math.random()
    });
  }
  return individual;
}

function renderIndividual(individual, ctx) {
  ctx.fillStyle = "white";
  ctx.fillRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
  for (var i = 0; i < TOTAL_SQUARES; i++) {
    ctx.globalAlpha = individual[i].alpha;
    ctx.fillStyle = 'rgb(' + individual[i].red + ',' +
      individual[i].green + ',' + individual[i].blue + ')';
    ctx.fillRect(individual[i].x, individual[i].y,
      individual[i].size, individual[i].size);
  }
}

function quality(individual) {
  var imgOrigin = ctxOrigin.getImageData(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
  var pixelArrayOrigin = imgOrigin.data;
  var score = 0;
  renderIndividual(individual, ctxBuffer);
  var imgBuffer = ctxBuffer.getImageData(0, 0, 400, 400);
  var pixelArrayCandidate = imgBuffer.data;
  for (var i = 0, n = pixelArrayOrigin.length; i < n; i += 4) {
    score += Math.abs(pixelArrayOrigin[i] - pixelArrayCandidate[i]);
    score += Math.abs(pixelArrayOrigin[i+1] - pixelArrayCandidate[i+1]);
    score += Math.abs(pixelArrayOrigin[i+2] - pixelArrayCandidate[i+2]);
  }
  return 1 / score;
}

function hillClimb() {
  var opponent = mutate(copy(solution));
  var score_opponent = quality(opponent);
  var score_solution = quality(solution);
  if (score_opponent > score_solution) {
    solution = opponent;
  }
  generation++;
  if (generation % 100 == 0) renderIndividual(solution, ctx);
  htmlGeneration.innerHTML = generation;
  htmlQuality.innerHTML = score_solution;
  if (generation >= 100000) {
    clearInterval(interval);
  }
}

function copy(individual) {
  var indiCopy = [];
  for(var i = 0; i < TOTAL_SQUARES; i++) {
    var objectCopy = {},
        prop;
    for(prop in individual[i]) {
      objectCopy[prop] = individual[i][prop];
    }
    indiCopy.push(objectCopy);
  }
  return indiCopy;
}

function mutate(individual) {
  var gene = Math.floor(Math.random() * TOTAL_SQUARES),
      squareProperty = Math.floor(Math.random() * 7);
  switch (squareProperty) {
    case 0:
      individual[gene].x = Math.floor(Math.random() * IMAGE_WIDTH);
      break;
    case 1:
      individual[gene].y = Math.floor(Math.random() * IMAGE_HEIGHT);
      break;
    case 2:
      individual[gene].size = Math.floor(Math.random() * SQUARE_MAX_SIZE);
      break;
    case 3:
      individual[gene].red = Math.floor(Math.random() * 256);
      break;
    case 4:
      individual[gene].green = Math.floor(Math.random() * 256);
      break;
    case 5:
      individual[gene].blue = Math.floor(Math.random() * 256);
      break;
    case 6:
      individual[gene].alpha = Math.random();
      break;
  }
  return individual;
}

Pour le faire tourner vous aurez besoin d’une photo de 400x400 pixels et de beaucoup de patience… Avec Firefox, ça marche tout seul mais avec Chrome il faudra passer par un serveur Web. Si Ruby est installé sur votre machine, vous pouvez démarrer un serveur en entrant ceci dans un terminal (même répertoire que votre fichier HTML):

ruby -rwebrick -e'WEBrick::HTTPServer.new(:Port => 3000, :DocumentRoot => Dir.pwd).start'

Le code se trouve aussi sur Github: github.com/lkdjiin/picture_genetic_algorithm. Je suis sûr que certains d’entre-vous connaissent Javascript bien mieux que moi et peuvent l’améliorer, alors n’hésitez pas.

À demain.