Search code examples
javascripthtmlcanvasfilterphoto

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:

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

Artist's Touch App


Solution

  • 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);
    }