javascripttvosquantizationtvmltvjs

Generate the Dominant Colors for an RGB image with XMLHttpRequest


A Note For Readers: This is a long question, but it needs a background to understand the question asked.

The color quantization technique is commonly used to get the dominant colors of an image. One of the well-known libraries that do color quantization is Leptonica through the Modified Median Cut Quantization (MMCQ) and octree quantization (OQ) Github's Color-thief by @lokesh is a very simple implementation in JavaScript of the MMCQ algorithm:

var colorThief = new ColorThief();
colorThief.getColor(sourceImage);

Technically, the image on a <img/> HTML element is backed on a <canvas/> element:

var CanvasImage = function (image) {
    this.canvas  = document.createElement('canvas');
    this.context = this.canvas.getContext('2d');

    document.body.appendChild(this.canvas);

    this.width  = this.canvas.width  = image.width;
    this.height = this.canvas.height = image.height;

    this.context.drawImage(image, 0, 0, this.width, this.height);
};

And that is the problem with TVML, as we will see later on.

Another implementation I recently came to know was linked on this article Using imagemagick, awk and kmeans to find dominant colors in images that links to Using python to generate awesome linux desktop themes. The author posted an article about Using python and k-means to find the dominant colors in images that was used there (sorry for all those links, but I'm following back my History...).

The author was super productive, and added a JavaScript version too that I'm posting here: Using JavaScript and k-means to find the dominant colors in images

In this case, we are generating the dominant colors of an image, not using the MMCQ (or OQ) algorithm, but K-Means. The problem is that the image must be a as well:

<canvas id="canvas" style="display: none;" width="200" height="200"></canvas>

and then

function analyze(img_elem) {
        var ctx = document.getElementById('canvas').getContext('2d')
          , img = new Image();
        img.onload = function() {
          var results = document.getElementById('results');
          results.innerHTML = 'Waiting...';
          var colors = process_image(img, ctx)
            , p1 = document.getElementById('c1')
            , p2 = document.getElementById('c2')
            , p3 = document.getElementById('c3');
          p1.style.backgroundColor = colors[0];
          p2.style.backgroundColor = colors[1];
          p3.style.backgroundColor = colors[2];
          results.innerHTML = 'Done';
        }
        img.src = img_elem.src;
      }

This is because the Canvas has a getContext() method, that expose 2D image drawing APIs - see An introduction to the Canvas 2D API

This context ctx is passed to the image processing function

  function process_image(img, ctx) {
    var points = [];
    ctx.drawImage(img, 0, 0, 200, 200);
    data = ctx.getImageData(0, 0, 200, 200).data;
    for (var i = 0, l = data.length; i < l;  i += 4) {
      var r = data[i]
        , g = data[i+1]
        , b = data[i+2];
      points.push([r, g, b]);
    }
    var results = kmeans(points, 3, 1)
     , hex = [];
    for (var i = 0; i < results.length; i++) {
      hex.push(rgbToHex(results[i][0]));
    }
    return hex;
  }

So you can draw an image on the Canvas through the Context and get image data:

ctx.drawImage(img, 0, 0, 200, 200);
data = ctx.getImageData(0, 0, 200, 200).data;

Another nice solution is in CoffeeScript, ColorTunes, but this is using a as well:

ColorTunes.getColorMap = function(canvas, sx, sy, w, h, nc) {
    var index, indexBase, pdata, pixels, x, y, _i, _j, _ref, _ref1;
    if (nc == null) {
      nc = 8;
    }
    pdata = canvas.getContext("2d").getImageData(sx, sy, w, h).data;
    pixels = [];
    for (y = _i = sy, _ref = sy + h; _i < _ref; y = _i += 1) {
      indexBase = y * w * 4;
      for (x = _j = sx, _ref1 = sx + w; _j < _ref1; x = _j += 1) {
        index = indexBase + (x * 4);
        pixels.push([pdata[index], pdata[index + 1], pdata[index + 2]]);
      }
    }
    return (new MMCQ).quantize(pixels, nc);
  };

But, wait, we have no <canvas/> element in TVML!

Of course, there are native solutions like Objective-C ColorCube, DominantColor - this is using K-means

and the very nice and reusable ColorArt by @AaronBrethorst from CocoaControls.

Despite the fact that this could be used in a TVML application through a native to JavaScriptCore bridge - see How to bridge TVML/JavaScriptCore to UIKit/Objective-C (Swift)?

my aim is to make this work completely in TVJS and TVML.

The simplest MMCQ JavaScript implementation does not need a Canvas: see Basic Javascript port of the MMCQ (modified median cut quantization) by Nick Rabinowitz, but needs the RGB array of the image:

var cmap = MMCQ.quantize(pixelArray, colorCount);

that is taken from the HTML <canvas/> and that is the reason for it!

function createPalette(sourceImage, colorCount) {

    // Create custom CanvasImage object
    var image = new CanvasImage(sourceImage),
        imageData = image.getImageData(),
        pixels = imageData.data,
        pixelCount = image.getPixelCount();

    // Store the RGB values in an array format suitable for quantize function
    var pixelArray = [];
    for (var i = 0, offset, r, g, b, a; i < pixelCount; i++) {
        offset = i * 4;
        r = pixels[offset + 0];
        g = pixels[offset + 1];
        b = pixels[offset + 2];
        a = pixels[offset + 3];
        // If pixel is mostly opaque and not white
        if (a >= 125) {
            if (!(r > 250 && g > 250 && b > 250)) {
                pixelArray.push([r, g, b]);
            }
        }
    }

    // Send array to quantize function which clusters values
    // using median cut algorithm

    var cmap = MMCQ.quantize(pixelArray, colorCount);
    var palette = cmap.palette();

    // Clean up
    image.removeCanvas();

    return palette;
}

[QUESTION] How to generate the dominant colors of a RGB image without using the HTML5 <canvas/>, but in pure JavaScript from an image's ByteArray fetched with XMLHttpRequest?

[UPDATE] I have posted this question to Color-Thief github repo, adapting the RGB array calculations to the latest codebase. The solution I have tried was this

ColorThief.prototype.getPaletteNoCanvas = function(sourceImageURL, colorCount, quality, done) {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', sourceImageURL, true);
  xhr.responseType = 'arraybuffer';
  xhr.onload = function(e) {
    if (this.status == 200) {

      var uInt8Array = new Uint8Array(this.response);
      var i = uInt8Array.length;
      var biStr = new Array(i);
      while (i--)
      { biStr[i] = String.fromCharCode(uInt8Array[i]);
      }

      if (typeof colorCount === 'undefined') {
          colorCount = 10;
      }
      if (typeof quality === 'undefined' || quality < 1) {
          quality = 10;
      }

      var pixels     = uInt8Array;
      var pixelCount = 152 * 152 * 4 // this should be width*height*4

      // Store the RGB values in an array format suitable for quantize function
      var pixelArray = [];
      for (var i = 0, offset, r, g, b, a; i < pixelCount; i = i + quality) {
          offset = i * 4;
          r = pixels[offset + 0];
          g = pixels[offset + 1];
          b = pixels[offset + 2];
          a = pixels[offset + 3];
          // If pixel is mostly opaque and not white
          if (a >= 125) {
              if (!(r > 250 && g > 250 && b > 250)) {
                  pixelArray.push([r, g, b]);
              }
          }
      }

      // Send array to quantize function which clusters values
      // using median cut algorithm
      var cmap    = MMCQ.quantize(pixelArray, colorCount);
      var palette = cmap? cmap.palette() : null;
      done.apply(this,[ palette ])

    } // 200
  };
  xhr.send();
}

but it does not gives back the right RGB colors array.

[UPDATE] Thanks to all the suggestions I got it working. Now a full example is available on Github,


Solution

  • The canvas element is being used as a convenient way to decode the image into an RGBA array. You can also use pure JavaScript libraries to do the image decoding.

    jpgjs is a JPEG decoder and pngjs is a PNG decoder. It looks like the JPEG decoder will work with TVJS as is. The PNG decoder, however, looks like it's made to work in a Node or web browser environment, so you might have to tweak that one a bit.