Search code examples
javascripthtml5-canvasfabricjs

How to write a convolution filter for Edge detection in FabricJS


I want to write a convolution filter for edge detection in Fabricjs , but the matrix doesn't work.

Here's the Matrix (Idea taken from http://homepages.inf.ed.ac.uk/rbf/HIPR2/sobel.htm): new fabric.Image.filters.Convolute({ // edge detect matrix: [[ -1, 0, 1, -2, 0, 2, -1, 0, 1 ], [1, 2, 1, 0 ,0 ,0, -1, -2, 1 ]] })

heres the Fiddle

NOTE:- You need to click on the image to get the check boxes for image processing.


Solution

  • Since a native html5 canvas element can be an image for Fabric.Image, you can find an Edge detection solution for native canvas and then use that native canvas on a Fabric.Image like this image:myNativeCanvas.

    [Addition: added a demo]

    Here's example code and a demo showing how to:

    • Use a native canvas to apply Sobel Edge Detection to an image and
    • Use that native canvas as the image source for a Fabric.Image

    enter image description here

    // Attribution: http://www.html5rocks.com/en/tutorials/canvas/imagefilters/
    
    //---------------- Script 'filters.js' ----------------
    
    
    Filters = {};
    Filters.getPixels = function(img) {
      var c,ctx;
      if (img.getContext) {
        c = img;
        try { ctx = c.getContext('2d'); } catch(e) {}
      }
      if (!ctx) {
        c = this.getCanvas(img.width, img.height);
        ctx = c.getContext('2d');
        ctx.drawImage(img, 0, 0);
      }
      return ctx.getImageData(0,0,c.width,c.height);
    };
    
    Filters.getCanvas = function(w,h) {
      var c = document.createElement('canvas');
      c.width = w;
      c.height = h;
      return c;
    };
    
    Filters.filterImage = function(filter, image, var_args) {
      var args = [this.getPixels(image)];
      for (var i=2; i<arguments.length; i++) {
        args.push(arguments[i]);
      }
      return filter.apply(null, args);
    };
    
    Filters.grayscale = function(pixels, args) {
      var d = pixels.data;
      for (var i=0; i<d.length; i+=4) {
        var r = d[i];
        var g = d[i+1];
        var b = d[i+2];
        // CIE luminance for the RGB
        var v = 0.2126*r + 0.7152*g + 0.0722*b;
        d[i] = d[i+1] = d[i+2] = v
      }
      return pixels;
    };
    
    Filters.brightness = function(pixels, adjustment) {
      var d = pixels.data;
      for (var i=0; i<d.length; i+=4) {
        d[i] += adjustment;
        d[i+1] += adjustment;
        d[i+2] += adjustment;
      }
      return pixels;
    };
    
    Filters.threshold = function(pixels, threshold) {
      var d = pixels.data;
      for (var i=0; i<d.length; i+=4) {
        var r = d[i];
        var g = d[i+1];
        var b = d[i+2];
        var v = (0.2126*r + 0.7152*g + 0.0722*b >= threshold) ? 255 : 0;
        d[i] = d[i+1] = d[i+2] = v
      }
      return pixels;
    };
    
    Filters.tmpCanvas = document.createElement('canvas');
    Filters.tmpCtx = Filters.tmpCanvas.getContext('2d');
    
    Filters.createImageData = function(w,h) {
      return this.tmpCtx.createImageData(w,h);
    };
    
    Filters.convolute = function(pixels, weights, opaque) {
      var side = Math.round(Math.sqrt(weights.length));
      var halfSide = Math.floor(side/2);
    
      var src = pixels.data;
      var sw = pixels.width;
      var sh = pixels.height;
    
      var w = sw;
      var h = sh;
      var output = Filters.createImageData(w, h);
      var dst = output.data;
    
      var alphaFac = opaque ? 1 : 0;
    
      for (var y=0; y<h; y++) {
        for (var x=0; x<w; x++) {
          var sy = y;
          var sx = x;
          var dstOff = (y*w+x)*4;
          var r=0, g=0, b=0, a=0;
          for (var cy=0; cy<side; cy++) {
            for (var cx=0; cx<side; cx++) {
              var scy = Math.min(sh-1, Math.max(0, sy + cy - halfSide));
              var scx = Math.min(sw-1, Math.max(0, sx + cx - halfSide));
              var srcOff = (scy*sw+scx)*4;
              var wt = weights[cy*side+cx];
              r += src[srcOff] * wt;
              g += src[srcOff+1] * wt;
              b += src[srcOff+2] * wt;
              a += src[srcOff+3] * wt;
            }
          }
          dst[dstOff] = r;
          dst[dstOff+1] = g;
          dst[dstOff+2] = b;
          dst[dstOff+3] = a + alphaFac*(255-a);
        }
      }
      return output;
    };
    
    if (!window.Float32Array)
      Float32Array = Array;
    
    Filters.convoluteFloat32 = function(pixels, weights, opaque) {
      var side = Math.round(Math.sqrt(weights.length));
      var halfSide = Math.floor(side/2);
    
      var src = pixels.data;
      var sw = pixels.width;
      var sh = pixels.height;
    
      var w = sw;
      var h = sh;
      var output = {
        width: w, height: h, data: new Float32Array(w*h*4)
      };
      var dst = output.data;
    
      var alphaFac = opaque ? 1 : 0;
    
      for (var y=0; y<h; y++) {
        for (var x=0; x<w; x++) {
          var sy = y;
          var sx = x;
          var dstOff = (y*w+x)*4;
          var r=0, g=0, b=0, a=0;
          for (var cy=0; cy<side; cy++) {
            for (var cx=0; cx<side; cx++) {
              var scy = Math.min(sh-1, Math.max(0, sy + cy - halfSide));
              var scx = Math.min(sw-1, Math.max(0, sx + cx - halfSide));
              var srcOff = (scy*sw+scx)*4;
              var wt = weights[cy*side+cx];
              r += src[srcOff] * wt;
              g += src[srcOff+1] * wt;
              b += src[srcOff+2] * wt;
              a += src[srcOff+3] * wt;
            }
          }
          dst[dstOff] = r;
          dst[dstOff+1] = g;
          dst[dstOff+2] = b;
          dst[dstOff+3] = a + alphaFac*(255-a);
        }
      }
      return output;
    };
    //
    function runFilter(id, filter, arg1, arg2, arg3) {
      var c = document.getElementById(id);
      var s = c.previousSibling.style;
      var b = c.parentNode.getElementsByTagName('button')[0];
      if (b.originalText == null) {
        b.originalText = b.textContent;
      }
      if (s.display == 'none') {
        s.display = 'inline';
        c.style.display = 'none';
        b.textContent = b.originalText;
      } else {
        var idata = Filters.filterImage(filter, img, arg1, arg2, arg3);
        c.width = idata.width;
        c.height = idata.height;
        var ctx = c.getContext('2d');
        ctx.putImageData(idata, 0, 0);
        s.display = 'none';
        c.style.display = 'inline';
        b.textContent = 'Restore original image';
      }
    }
    //
    sobel = function() {
      runFilter('sobel', function(px){
        px = Filters.grayscale(px);
        var vertical = Filters.convoluteFloat32(px,
          [-1,-2,-1,
            0, 0, 0,
            1, 2, 1]);
        var horizontal = Filters.convoluteFloat32(px,
          [-1,0,1,
           -2,0,2,
           -1,0,1]);
        var id = Filters.createImageData(vertical.width, vertical.height);
        for (var i=0; i<id.data.length; i+=4) {
          var v = Math.abs(vertical.data[i]);
          id.data[i] = v;
          var h = Math.abs(horizontal.data[i]);
          id.data[i+1] = h
          id.data[i+2] = (v+h)/4;
          id.data[i+3] = 255;
        }
        return id;
      });
    }
    
    //---------------- Calling script ---------------- 
    
    
    
    // load an image 
    var image=new Image();
    image.crossOrigin='anonymous';
    image.onload=start;
    image.src="https://upload.wikimedia.org/wikipedia/commons/7/74/Boy_and_Turtle.png";
    function start(){
    
      // apply Sobel Edge Detection
      // return ImageData of the filtered canvas
      var grayscale = Filters.filterImage(Filters.grayscale, image);
      var vertical = Filters.convoluteFloat32(grayscale,
                                              [ -1, 0, 1,
                                               -2, 0, 2,
                                               -1, 0, 1 ]);
      var horizontal = Filters.convoluteFloat32(grayscale,
                                                [ -1, -2, -1,
                                                 0,  0,  0,
                                                 1,  2,  1 ]);
      var final_image = Filters.createImageData(vertical.width, vertical.height);
      for (var i=0; i<final_image.data.length; i+=4){
        // make the vertical gradient red
        var v = Math.abs(vertical.data[i]);
        final_image.data[i] = v;
        // make the horizontal gradient green
        var h = Math.abs(horizontal.data[i]);
        final_image.data[i+1] = h;
        // and mix in some blue for aesthetics
        final_image.data[i+2] = (v+h)/4;
        final_image.data[i+3] = 255; // opaque alpha
      }
    
      // put the filtered imageData on an in-memory canvas
      var memCanvas = document.createElement('canvas');
      memCanvas.width=image.width;
      memCanvas.height=image.height;
      memCanvas.getContext('2d').putImageData(final_image,0,0);
    
      // use the in-memory canvas as an image source for a Fabric.Image
      var canvas = new fabric.Canvas('canvas');
      var imgElement = document.getElementById('my-image');
      var imgInstance = new fabric.Image(memCanvas,{left:0,top:0});
      canvas.add(imgInstance);
    }
    body{ background-color: ivory; }
    canvas, img {border:1px solid red; margin:0 auto; }
    <script src="http://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.5.0/fabric.min.js"></script>
    <h4>Original Image</h4>
    <img src='https://upload.wikimedia.org/wikipedia/commons/7/74/Boy_and_Turtle.png' width="250px" height="232px">
    <h4>FabricJS image with Sobel filter applied</h4>
    <canvas id="canvas" width="798px" height="746px"></canvas>