Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

monochrome dithering in JavaScript (Bayer, Atkinson, Floyd–Steinberg)

I'm playing with webcam filters in HTML5. Got an Atkinson dither working pretty well for that old-school Mac feeling.

meemoo cam to dither
Live | Code

Now I'm trying to make a Bayer ordered dithering option for a 1989 Gameboy feeling.

I read up on the algorithm, but I'm having trouble converting this pseudocode to JavaScript:

for each y
  for each x
    oldpixel := pixel[x][y] + threshold_map_4x4[x mod 4][y mod 4]
    newpixel := find_closest_palette_color(oldpixel)
    pixel[x][y] := newpixel

Are there any examples out there in AS3, PHP, or JS? Could you explain what is happening with threshold_map_4x4[x mod 4][y mod 4]?


gameboy style gif (made with the Meemoo Gameboy GIFerizer)

Figured it out. In Wikipedia it says "For example, in monochrome rendering, if the value of the pixel (scaled into the 0-9 range) is less than the number in the corresponding cell of the matrix, plot that pixel black, otherwise, plot it white." In js I got good results by averaging the current pixel (0-255) and the map's value (15-240) and comparing it to the threshold (normally 129):

var map = (imageData.data[currentPixel] + bayerThresholdMap[x%4][y%4]) / 2;
imageData.data[currentPixel] = (map < threshold) ? 0 : 255;

Here is my whole monochrome function with different algorithms:

var bayerThresholdMap = [
  [  15, 135,  45, 165 ],
  [ 195,  75, 225, 105 ],
  [  60, 180,  30, 150 ],
  [ 240, 120, 210,  90 ]
];

var lumR = [];
var lumG = [];
var lumB = [];
for (var i=0; i<256; i++) {
  lumR[i] = i*0.299;
  lumG[i] = i*0.587;
  lumB[i] = i*0.114;
}

function monochrome(imageData, threshold, type){

  var imageDataLength = imageData.data.length;

  // Greyscale luminance (sets r pixels to luminance of rgb)
  for (var i = 0; i <= imageDataLength; i += 4) {
    imageData.data[i] = Math.floor(lumR[imageData.data[i]] + lumG[imageData.data[i+1]] + lumB[imageData.data[i+2]]);
  }

  var w = imageData.width;
  var newPixel, err;

  for (var currentPixel = 0; currentPixel <= imageDataLength; currentPixel+=4) {

    if (type === "none") {
      // No dithering
      imageData.data[currentPixel] = imageData.data[currentPixel] < threshold ? 0 : 255;
    } else if (type === "bayer") {
      // 4x4 Bayer ordered dithering algorithm
      var x = currentPixel/4 % w;
      var y = Math.floor(currentPixel/4 / w);
      var map = Math.floor( (imageData.data[currentPixel] + bayerThresholdMap[x%4][y%4]) / 2 );
      imageData.data[currentPixel] = (map < threshold) ? 0 : 255;
    } else if (type === "floydsteinberg") {
      // Floyd–Steinberg dithering algorithm
      newPixel = imageData.data[currentPixel] < 129 ? 0 : 255;
      err = Math.floor((imageData.data[currentPixel] - newPixel) / 16);
      imageData.data[currentPixel] = newPixel;

      imageData.data[currentPixel       + 4 ] += err*7;
      imageData.data[currentPixel + 4*w - 4 ] += err*3;
      imageData.data[currentPixel + 4*w     ] += err*5;
      imageData.data[currentPixel + 4*w + 4 ] += err*1;
    } else {
      // Bill Atkinson's dithering algorithm
      newPixel = imageData.data[currentPixel] < threshold ? 0 : 255;
      err = Math.floor((imageData.data[currentPixel] - newPixel) / 8);
      imageData.data[currentPixel] = newPixel;

      imageData.data[currentPixel       + 4 ] += err;
      imageData.data[currentPixel       + 8 ] += err;
      imageData.data[currentPixel + 4*w - 4 ] += err;
      imageData.data[currentPixel + 4*w     ] += err;
      imageData.data[currentPixel + 4*w + 4 ] += err;
      imageData.data[currentPixel + 8*w     ] += err;
    }

    // Set g and b pixels equal to r
    imageData.data[currentPixel + 1] = imageData.data[currentPixel + 2] = imageData.data[currentPixel];
  }

  return imageData;
}

I'd appreciate optimization hints.

like image 699
forresto Avatar asked Sep 14 '12 10:09

forresto


2 Answers

Here are all of my monochrome dither functions, usable as a web worker: https://github.com/meemoo/meemooapp/blob/master/src/nodes/image-monochrome-worker.js

Live demo with webcam: http://meemoo.org/iframework/#gist/3721129

like image 150
forresto Avatar answered Oct 12 '22 23:10

forresto


I do this as a debug code:

var canvas = document.createElement('canvas');
var ctx    = canvas.getContext('2d');

var img    = new Image();
img.src    = "http://i.stack.imgur.com/tHDY8.png";
img.onload = function() {
    canvas.width  = this.width;
    canvas.height = this.height;
    ctx.drawImage( this, 0, 0, this.width, this.height );

    var imageData  = ctx.getImageData( 0, 0, this.width, this.height);
    var depth      = 32;


    // Matrix
    var threshold_map_4x4 = [
        [  1,  9,  3, 11 ],
        [ 13,  5, 15,  7 ],
        [  4, 12,  2, 10 ],
        [ 16,  8, 14,  6 ]
    ];

    // imageData
    var width  = imageData.width;
    var height = imageData.height;
    var pixel  = imageData.data;
    var x, y, a, b;

    // filter
    for ( x=0; x<width; x++ )
    {
        for ( y=0; y<height; y++ )
        {
            a    = ( x * height + y ) * 4;
            b    = threshold_map_4x4[ x%4 ][ y%4 ];
            pixel[ a + 0 ] = ( (pixel[ a + 0 ]+ b) / depth | 0 ) * depth;
            pixel[ a + 1 ] = ( (pixel[ a + 1 ]+ b) / depth | 0 ) * depth;
            pixel[ a + 2 ] = ( (pixel[ a + 2 ]+ b) / depth | 0 ) * depth;
            //pixel[ a + 3 ] = ( (pixel[ a + 3 ]+ b) / depth | 3 ) * depth;
        }
    }

    ctx.putImageData( imageData, 0, 0);

};

document.body.appendChild(canvas);

And it seems to work fine, you can change the depth variable to change the posterization.

like image 30
Vincent Thibault Avatar answered Oct 12 '22 23:10

Vincent Thibault