Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Apply a Oil Paint/Sketch effect to a photo using Javascript

I want to simulate an human drawing effect starting from a photo, using javascript.

I've been searching trough several js libraries that do image manipulation (mostly on canvas). But seems that no one even attempted what I'm looking for.

photoshop oil paint filter

I don't think it's impossible to achieve such effects with javascript. So I wonder why I can't find anything already done.

On the native side there are several alternatives to photoshop to achieve such effects, as seen in several apps in the App Store:

  • Oil Painting Booth Free
  • Brushstroke
  • Autopainter
  • Artist's Touch
  • Autographo

Here's other examples of possible result (from Artist's Touch App):

Artist's Touch App

like image 438
Francesco Frapporti Avatar asked Jun 14 '14 17:06

Francesco Frapporti


2 Answers

Alright so I found a great explanation of the algorithm used here and adapted it to JS and canvas.

Live Demo

CodePen Demo with controls to mess with the effect

effect result

How it works is you average all the pixels around your center pixel, then you multiply that average by the intensity level you want, you then divide it by 255. Then you increment the r/g/b's related to the intensity level. Then you check which intensity level was most common among the pixels neighbors, and assign the target pixel that intensity level.

edit worked on it a bit more and rewrote a lot of it, gained some really huge performance gains, works with decent sized images now pretty well.

var canvas = document.getElementById("canvas"),
    ctx = canvas.getContext("2d"),
    img = new Image();

img.addEventListener('load', function () {
    canvas.width = this.width;
    canvas.height = this.height;
    ctx.drawImage(this, 0, 0, canvas.width, canvas.height);
    oilPaintEffect(canvas, 4, 55);
});

img.crossOrigin = "Anonymous";
img.src = "https://fbcdn-sphotos-h-a.akamaihd.net/hphotos-ak-xpa1/v/t1.0-9/1379992_10202357787410559_1075078295_n.jpg?oh=5b001e9848796dd942f47a0b2f3df6af&oe=542F3FEF&__gda__=1412145968_4dbb7f75b385770ecc3f4b88105cb0f8";

function oilPaintEffect(canvas, radius, intensity) {
    var width = canvas.width,
        height = canvas.height,
        imgData = ctx.getImageData(0, 0, width, height),
        pixData = imgData.data,
        destCanvas = document.createElement("canvas"),
        dCtx = destCanvas.getContext("2d"),
        pixelIntensityCount = [];

    destCanvas.width = width;
    destCanvas.height = height;

    // for demo purposes, remove this to modify the original canvas
    document.body.appendChild(destCanvas);

    var destImageData = dCtx.createImageData(width, height),
        destPixData = destImageData.data,
        intensityLUT = [],
        rgbLUT = [];

    for (var y = 0; y < height; y++) {
        intensityLUT[y] = [];
        rgbLUT[y] = [];
        for (var x = 0; x < width; x++) {
            var idx = (y * width + x) * 4,
                r = pixData[idx],
                g = pixData[idx + 1],
                b = pixData[idx + 2],
                avg = (r + g + b) / 3;

            intensityLUT[y][x] = Math.round((avg * intensity) / 255);
            rgbLUT[y][x] = {
                r: r,
                g: g,
                b: b
            };
        }
    }


    for (y = 0; y < height; y++) {
        for (x = 0; x < width; x++) {

            pixelIntensityCount = [];

            // Find intensities of nearest pixels within radius.
            for (var yy = -radius; yy <= radius; yy++) {
                for (var xx = -radius; xx <= radius; xx++) {
                    if (y + yy > 0 && y + yy < height && x + xx > 0 && x + xx < width) {
                        var intensityVal = intensityLUT[y + yy][x + xx];

                        if (!pixelIntensityCount[intensityVal]) {
                            pixelIntensityCount[intensityVal] = {
                                val: 1,
                                r: rgbLUT[y + yy][x + xx].r,
                                g: rgbLUT[y + yy][x + xx].g,
                                b: rgbLUT[y + yy][x + xx].b
                            }
                        } else {
                            pixelIntensityCount[intensityVal].val++;
                            pixelIntensityCount[intensityVal].r += rgbLUT[y + yy][x + xx].r;
                            pixelIntensityCount[intensityVal].g += rgbLUT[y + yy][x + xx].g;
                            pixelIntensityCount[intensityVal].b += rgbLUT[y + yy][x + xx].b;
                        }
                    }
                }
            }

            pixelIntensityCount.sort(function (a, b) {
                return b.val - a.val;
            });

            var curMax = pixelIntensityCount[0].val,
                dIdx = (y * width + x) * 4;

            destPixData[dIdx] = ~~ (pixelIntensityCount[0].r / curMax);
            destPixData[dIdx + 1] = ~~ (pixelIntensityCount[0].g / curMax);
            destPixData[dIdx + 2] = ~~ (pixelIntensityCount[0].b / curMax);
            destPixData[dIdx + 3] = 255;
        }
    }

    // change this to ctx to instead put the data on the original canvas
    dCtx.putImageData(destImageData, 0, 0);
}
like image 57
Loktar Avatar answered Oct 13 '22 05:10

Loktar


Paste a demo of @Loktar's answer here for someone looking for this algorithm.

var canvas = document.getElementById("canvas"),
    ctx = canvas.getContext("2d"),
    img = new Image(),
    effectEl = document.getElementById("effect"),
    settings = {
      radius : 4,
      intensity : 25,
      ApplyFilter : function(){
         doOilPaintEffect();
      }
    }

img.addEventListener('load', function () {
    // reduced the size by half for pen and performance.
    canvas.width = (this.width/2);
    canvas.height = (this.height/2);
    ctx.drawImage(this, 0, 0, canvas.width, canvas.height);
    doOilPaintEffect();
});

img.crossOrigin = "Anonymous";
img.src = "https://codropspz-tympanus.netdna-ssl.com/codrops/wp-content/uploads/2013/02/ImageTechniques.jpg";

var gui = new dat.GUI();
gui.add(settings, 'intensity');
gui.add(settings, 'radius');
gui.add(settings, 'ApplyFilter');

function doOilPaintEffect(){
  oilPaintEffect(canvas, settings.radius, settings.intensity);
}

function oilPaintEffect(canvas, radius, intensity) {
    var width = canvas.width,
        height = canvas.height,
        imgData = ctx.getImageData(0, 0, width, height),
        pixData = imgData.data,
        // change to createElement getting added element just for the demo
        destCanvas = document.getElementById("dest-canvas"),
        dCtx = destCanvas.getContext("2d"),
        pixelIntensityCount = [];
    
    destCanvas.width = width;
    destCanvas.height = height;
    
    var destImageData = dCtx.createImageData(width, height),
        destPixData = destImageData.data,
        intensityLUT = [],
        rgbLUT = [];
    
    for (var y = 0; y < height; y++) {
        intensityLUT[y] = [];
        rgbLUT[y] = [];
        for (var x = 0; x < width; x++) {
            var idx = (y * width + x) * 4,
                r = pixData[idx],
                g = pixData[idx + 1],
                b = pixData[idx + 2],
                avg = (r + g + b) / 3;
            
            intensityLUT[y][x] = Math.round((avg * intensity) / 255);
            rgbLUT[y][x] = {
                r: r,
                g: g,
                b: b
            };
        }
    }
    
    
    for (y = 0; y < height; y++) {
        for (x = 0; x < width; x++) {
            pixelIntensityCount = [];
            
            // Find intensities of nearest pixels within radius.
            for (var yy = -radius; yy <= radius; yy++) {
                for (var xx = -radius; xx <= radius; xx++) {
                  if (y + yy > 0 && y + yy < height && x + xx > 0 && x + xx < width) {
                      var intensityVal = intensityLUT[y + yy][x + xx];

                      if (!pixelIntensityCount[intensityVal]) {
                          pixelIntensityCount[intensityVal] = {
                              val: 1,
                              r: rgbLUT[y + yy][x + xx].r,
                              g: rgbLUT[y + yy][x + xx].g,
                              b: rgbLUT[y + yy][x + xx].b
                          }
                      } else {
                          pixelIntensityCount[intensityVal].val++;
                          pixelIntensityCount[intensityVal].r += rgbLUT[y + yy][x + xx].r;
                          pixelIntensityCount[intensityVal].g += rgbLUT[y + yy][x + xx].g;
                          pixelIntensityCount[intensityVal].b += rgbLUT[y + yy][x + xx].b;
                      }
                  }
                }
            }
            
            pixelIntensityCount.sort(function (a, b) {
                return b.val - a.val;
            });
            
            var curMax = pixelIntensityCount[0].val,
                dIdx = (y * width + x) * 4;
            
            destPixData[dIdx] = ~~ (pixelIntensityCount[0].r / curMax);
            destPixData[dIdx + 1] = ~~ (pixelIntensityCount[0].g / curMax);
            destPixData[dIdx + 2] = ~~ (pixelIntensityCount[0].b / curMax);
            destPixData[dIdx + 3] = 255;
        }
    }
    
    // change this to ctx to instead put the data on the original canvas
    dCtx.putImageData(destImageData, 0, 0);
}
body{text-align:center;background:#ececec;font-family:Tahoma, Geneva, sans-serif}
section{display:inline-block}
canvas{border:1px solid #000}
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5/dat.gui.min.js"></script>
<section>
  <h2>Original</h2>
  <canvas id="canvas"></canvas>
</section>
<section>
  <h2>Oil Painting Effect</h2>
   <canvas id="dest-canvas"></canvas>
</section>
like image 34
mihnsen Avatar answered Oct 13 '22 04:10

mihnsen