2

I have to achieve the following task:

divides the image into tiles, computes the average color of each tile, fetches a tile from the server for that color, and composites the results into a photomosaic of the original image.

What would be the best strategy? the first solution coming to my mind is using canvas.

CodeYogi
  • 1,352
  • 1
  • 18
  • 41

3 Answers3

1

A simple way to get pixel data and finding the means of tiles. The code will need more checks for images that do not have dimensions that can be divided by the number of tiles.

var image = new Image();
image.src = ??? // the URL if the image is not from your domain you will have to move it to your server first

// wait for image to load
image.onload = function(){
    // create a canvas
    var canvas = document.createElement("canvas");
    //set its size to match the image
    canvas.width = this.width;
    canvas.height = this.height;
    var ctx = canvas.getContext("2d"); // get the 2d interface
    // draw the image on the canvas
    ctx.drawImage(this,0,0);
    // get the tile size
    var tileSizeX = Math.floor(this.width / 10);
    var tileSizeY = Math.floor(this.height / 10);
    var x,y;
    // array to hold tile colours
    var tileColours = [];
    // for each tile
    for(y = 0; y < this.height; y += tileSizeY){
        for(x = 0; x < this.width; x += tileSizeX){
            // get the pixel data
            var imgData = ctx.getImageData(x,y,tileSizeX,tileSizeY);
            var r,g,b,ind;
            var i = tileSizeY * tileSizeX; // get pixel count
            ind = r = g = b = 0;
            // for each pixel (rgba 8 bits each)
            while(i > 0){
                // sum the channels
                r += imgData.data[ind++];
                g += imgData.data[ind++];
                b += imgData.data[ind++];
                ind ++;
                i --;
            }
            i = ind /4; // get the count again
            // calculate channel means
            r /= i;
            g /= i;
            b /= i;
            //store the tile coords and colour
            tileColours[tileColours.length] = {
                rgb : [r,g,b],
                x : x,
                y : y,
            }
        }
        // all done now fetch the images for the found tiles.
    }
Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • `ctx.getImageData(x,y,tileSizeX,tileSizeY)` should return the mean color since the tile size is not a single pixel, right? – CodeYogi Jul 02 '16 at 08:23
  • @CodeYogi getImageData returns the pixel data as an array, It does not process the data. For more info see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getImageData – Blindman67 Jul 02 '16 at 08:38
  • so, `getImageData` returns the pixel data for a particular tile and what about the `ind` variable? – CodeYogi Jul 02 '16 at 11:27
  • 1
    @CodeYogi `ind` (short for index) is the index into the pixel array, Each pixel has 4 items in the array one each for the red, green, blue and alpha channels, Each is a byte with 256 values ranging from 0 to 255 . The totl number of bytes is the number of pixels `tileSizeX` times `tileSizeY` times the number of bytes per pixel 4 ( `pixelDataArrayLength = tileSizeX * tileSizeY * 4`) – Blindman67 Jul 02 '16 at 14:29
  • Nearly done http://codepen.io/vivekimsit/pen/yJbwLP?editors=1010 but there seems to be some corner cases. For demo click on the canvas. I need to load the svgs from the server which seems not a big issue. – CodeYogi Jul 04 '16 at 16:15
  • Also, the application seems too slow, some hint to make it faster? – CodeYogi Jul 04 '16 at 16:16
  • @CodeYogi To increase the speed, when you create the canvas (see snippet `document.createElement("canvas")`) at a smaller size by multiples of 2. then draw the image the image `ctx.drawImage(canvas,0,0,canvas.width,canvas.height)` the render will automatically average the colours as it downsizes, then just sample the smaller tiles to get the average. This ONLY works if the original image if a perfect multiple of the tiles ie w = 100 and tiles = 10 then you can reduce by 10 but if the w = 100 and tiles = 9 then you can not use this method as tiles will bleed into each other. two part comment – Blindman67 Jul 04 '16 at 16:31
  • For very large images the bleeding becomes insignificant if you first step down to a multiple ie w = 1003 and tiles = 9 the first step down to 900 then you can step down to 90 or even 9 (one pixel per tile) NOTE the render does not use full floating point to find mean colours, reducing down to one pixel will introduce some colour loss. But this method will let you do it in real time. – Blindman67 Jul 04 '16 at 16:32
  • Sorry, I am getting confused, can you please restate your statements? – CodeYogi Jul 04 '16 at 16:37
  • @CodeYogi see second answer – Blindman67 Jul 04 '16 at 16:53
0

I created a solution for this (I am not getting the tile images from back end)

  // first function call to create photomosaic 
  function photomosaic(image) {

      // Dimensions of each tile
      var tileWidth = TILE_WIDTH;
      var tileHeight = TILE_HEIGHT;

      //creating the canvas for photomosaic
      var canvas = document.createElement('canvas');
      var context = canvas.getContext("2d");
      canvas.height = image.height;
      canvas.width = image.width;

      var imageData = context.getImageData(0, 0, image.width, image.height);
      var pixels = imageData.data;

      // Number of mosaic tiles
      var numTileRows = image.width / tileWidth;
      var numTileCols = image.height / tileHeight;


      //canvas copy of image
      var imageCanvas = document.createElement('canvas');
      var imageCanvasContext = canvas.getContext('2d');
      imageCanvas.height = image.height;
      imageCanvas.width = image.width;
      imageCanvasContext.drawImage(image, 0, 0);


      //function for finding the average color
      function averageColor(row, column) {
          var blockSize = 1, // we can set how many pixels to skip

              data, width, height,
              i = -4,
              length,
              rgb = {
                  r: 0,
                  g: 0,
                  b: 0
              },
              count = 0;          

          try {
              data = imageCanvasContext.getImageData(column * TILE_WIDTH, row * TILE_HEIGHT, TILE_HEIGHT, TILE_WIDTH);
          } catch (e) {
              alert('Not happening this time!');
              return rgb;
          }

          length = data.data.length;

          while ((i += blockSize * 4) < length) {
              ++count;
              rgb.r += data.data[i];
              rgb.g += data.data[i + 1];
              rgb.b += data.data[i + 2];
          }

          // ~~ used to floor values
          rgb.r = ~~(rgb.r / count);
          rgb.g = ~~(rgb.g / count);
          rgb.b = ~~(rgb.b / count);

          return rgb;

      }

      // Loop through each tile
      for (var r = 0; r < numTileRows; r++) {
          for (var c = 0; c < numTileCols; c++) {
              // Set the pixel values for each tile
              var rgb = averageColor(r, c)
              var red = rgb.r;
              var green = rgb.g;
              var blue = rgb.b;

              // Loop through each tile pixel
              for (var tr = 0; tr < tileHeight; tr++) {
                  for (var tc = 0; tc < tileWidth; tc++) {

                      // Calculate the true position of the tile pixel
                      var trueRow = (r * tileHeight) + tr;
                      var trueCol = (c * tileWidth) + tc;

                      // Calculate the position of the current pixel in the array
                      var pos = (trueRow * (imageData.width * 4)) + (trueCol * 4);

                      // Assign the colour to each pixel
                      pixels[pos + 0] = red;
                      pixels[pos + 1] = green;
                      pixels[pos + 2] = blue;
                      pixels[pos + 3] = 255;
                  };
              };
          };
      };

      // Draw image data to the canvas
      context.putImageData(imageData, 0, 0);
      return canvas;
  }

  function create() {
      var image = document.getElementById('image');
      var canvas = photomosaic(image);
      document.getElementById("output").appendChild(canvas);
  };

DEMO:https://jsfiddle.net/gurinderiitr/sx735L5n/

0

Try using the JIMP javascript library to read the pixel color and use invert, normalize or similar property for modifying the image.

Have a look on the jimp library https://github.com/oliver-moran/jimp

Jose G Varanam
  • 767
  • 8
  • 19