使用Canvas把照片转换成素描画

原文:http://www.alloyteam.com/2012/07/convert-picture-to-sketch-by-canvas/

腾讯的alloy team写的一个素描效果,挺不错的。

 

 

 

使用Canvas把照片转换成素描画

一、引子

话说前阵子想把一张照片转换成素描,然后发个微博。结果发现mac上没找到能直接转换素描的软件(PS不算,可要好几步呢),坑爹啊~~google 了下,Web上竟然也是没有直接把照片转换成素描的东西,连让我包含期望的美图秀秀(Web版)竟然都没有素描功能,T_T。

手机上是有很多这类app,但是我只是想一键转换下,发个微博嗟,至于这么折腾么……

所以自己动手整一个在线版的吧,没怎么用过canvas,正好可以顺道熟悉下。等不及的童鞋可以先到这里看看效果(http://apps.imatlas.com/sketching/)。

二、怎么转换

刚冒出这个想法的时候,简直是一头雾水诶~数学不行、PS不懂、图形学忘光了……

还好有万能的google,翻了几页,找到一个ps制作素描图片的步骤——虽然我不懂,但是如果按照这个步骤用PS能做成素描,我用代码也一定可以的。嗯,一定是的。

PS里面最简单的一个转换素描的步骤为:

  1. 去色(黑白化)
  2. 复制一份,反相
  3. 把复制后的图层叠加方式设为颜色减淡
  4. 高斯模糊

PS里面的具体步骤我就不详说了,可以看这篇文章。既然知道了实现步骤,我只要用JS把这些算法都实现了就行啦,哇哈哈哈~

三、原理什么的

去色:把图片变成黑白图,只要把每个像素的R、G、B设为亮度(Y)的值就行了。关于R、G、B、Y的关系可以看到这里看看,这里只要记住这条公式:Y = 0.299R + 0.587G + 0.114B。

反相:就是将一个颜色换成它的补色。补色就是用255(8位通道模式下,255即2的8次方,16位要用65535去减,即2的16次方)减去它本身得到的值:R(补) = 255 – R。

颜色减淡:其计算公式是:结果色 = 基色 + (混合色 * 基色) / (255 – 混合色),在这里找到的这条公式,原理我就不多说了,因为我也不大懂(^_^,图形学睡过去了……)。

高斯模糊:嗯,这个是最让我抓头摸脑的。一开始没怎么理解到这个算法,纠结了两天。最后终于灵光一闪,想通了(还好没晕过去大睡三天~.~)!网上有很多C++的实现,但是基本没找到JS的。一开始不想去理解高斯模糊,就尝试把C++代码改成JS的,改了半天,终于放弃了~想明白之后,自己照原理写了个,想不到还挺容易的,呃……具体的高斯模糊原理,就在这里这里这里看吧,老衲就不误人子弟了。

本项目已经托管到了Github(https://github.com/iazrael/sketching),这几个方法的源码可以到上面查看。稍微提下实现素描的一个注意事项:去色之后需要拷贝一份像素数组备用,开始是用数组的slice方法来拷贝像素数组的,结果经常需要800ms左右的时间;后来尝试了直接用canvas,putImageData之后再调用getImageData来“曲线救国”,结果只用10几毫秒就可完成,简直让老衲老泪纵横诶~其代码如下:

/**

 * 素描

 * @param  {Object} imgData 

 * @param  {Number} radius 取样区域半径, 正数, 可选, 默认为 3.0

 * @param  {Number} sigma 标准方差, 可选, 默认取值为 radius / 3

 * @return {Array}

 */

function sketch(imgData, radius, sigma){

    var pixes = imgData.data,

        width = imgData.width,

        height = imgData.height,

        copyPixes;

 

    discolor(pixes);//去色

    canvas.width = width, canvas.height = height;

    //复制一份

    ctx.clearRect(0, 0, width, height);

    ctx.putImageData(imgData, 0, 0);

    copyPixes = ctx.getImageData(0, 0, width, height).data;

    // 拷贝数组太慢

    // copyPixes = Array.prototype.slice.call(pixes, 0);

    invert(copyPixes);//反相

    gaussBlur(copyPixes, width, height, radius, sigma);//高斯模糊

    dodgeColor(pixes, copyPixes);//颜色减淡

    return pixes;

}

 

(function() {



    /**

     * 把图像变成黑白色

     * Y = 0.299R + 0.587G + 0.114B

     * @param  {Array} pixes pix array

     * @return {Array}

     * @link {http://www.61ic.com/Article/DaVinci/DM64X/200804/19645.html}

     */

    function discolor(pixes) {

        var grayscale;

        for (var i = 0, len = pixes.length; i < len; i += 4) {

            grayscale = pixes[i] * 0.299 + pixes[i + 1] * 0.587 + pixes[i + 2] * 0.114;

            pixes[i] = pixes[i + 1] = pixes[i + 2] = grayscale;

        }

        return pixes;

    }



    /**

     * 把图片反相, 即将某个颜色换成它的补色

     * @param  {Array} pixes pix array

     * @return {Array}

     */

    function invert(pixes) {

        for (var i = 0, len = pixes.length; i < len; i += 4) {

            pixes[i] = 255 - pixes[i]; //r

            pixes[i + 1] = 255 - pixes[i + 1]; //g

            pixes[i + 2] = 255 - pixes[i + 2]; //b

        }

        return pixes;

    }

    /**

     * 颜色减淡,

     * 结果色 = 基色 + (混合色 * 基色) / (255 - 混合色)

     * @param  {Array} basePixes 基色

     * @param  {Array} mixPixes  混合色

     * @return {Array}

     */

    function dodgeColor(basePixes, mixPixes) {

        for (var i = 0, len = basePixes.length; i < len; i += 4) {

            basePixes[i] = basePixes[i] + (basePixes[i] * mixPixes[i]) / (255 - mixPixes[i]);

            basePixes[i + 1] = basePixes[i + 1] + (basePixes[i + 1] * mixPixes[i + 1]) / (255 - mixPixes[i + 1]);

            basePixes[i + 2] = basePixes[i + 2] + (basePixes[i + 2] * mixPixes[i + 2]) / (255 - mixPixes[i + 2]);

        }

        return basePixes;

    }



    /**

     * 高斯模糊

     * @param  {Array} pixes  pix array

     * @param  {Number} width 图片的宽度

     * @param  {Number} height 图片的高度

     * @param  {Number} radius 取样区域半径, 正数, 可选, 默认为 3.0

     * @param  {Number} sigma 标准方差, 可选, 默认取值为 radius / 3

     * @return {Array}

     */

    function gaussBlur(pixes, width, height, radius, sigma) {

        var gaussMatrix = [],

            gaussSum = 0,

            x, y,

            r, g, b, a,

            i, j, k, len;



        radius = Math.floor(radius) || 3;

        sigma = sigma || radius / 3;

        

        a = 1 / (Math.sqrt(2 * Math.PI) * sigma);

        b = -1 / (2 * sigma * sigma);

        //生成高斯矩阵

        for (i = 0, x = -radius; x <= radius; x++, i++){

            g = a * Math.exp(b * x * x);

            gaussMatrix[i] = g;

            gaussSum += g;

        

        }

        //归一化, 保证高斯矩阵的值在[0,1]之间

        for (i = 0, len = gaussMatrix.length; i < len; i++) {

            gaussMatrix[i] /= gaussSum;

        }

        //x 方向一维高斯运算

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

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

                r = g = b = a = 0;

                gaussSum = 0;

                for(j = -radius; j <= radius; j++){

                    k = x + j;

                    if(k >= 0 && k < width){//确保 k 没超出 x 的范围

                        //r,g,b,a 四个一组

                        i = (y * width + k) * 4;

                        r += pixes[i] * gaussMatrix[j + radius];

                        g += pixes[i + 1] * gaussMatrix[j + radius];

                        b += pixes[i + 2] * gaussMatrix[j + radius];

                        // a += pixes[i + 3] * gaussMatrix[j];

                        gaussSum += gaussMatrix[j + radius];

                    }

                }

                i = (y * width + x) * 4;

                // 除以 gaussSum 是为了消除处于边缘的像素, 高斯运算不足的问题

                // console.log(gaussSum)

                pixes[i] = r / gaussSum;

                pixes[i + 1] = g / gaussSum;

                pixes[i + 2] = b / gaussSum;

                // pixes[i + 3] = a ;

            }

        }

        //y 方向一维高斯运算

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

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

                r = g = b = a = 0;

                gaussSum = 0;

                for(j = -radius; j <= radius; j++){

                    k = y + j;

                    if(k >= 0 && k < height){//确保 k 没超出 y 的范围

                        i = (k * width + x) * 4;

                        r += pixes[i] * gaussMatrix[j + radius];

                        g += pixes[i + 1] * gaussMatrix[j + radius];

                        b += pixes[i + 2] * gaussMatrix[j + radius];

                        // a += pixes[i + 3] * gaussMatrix[j];

                        gaussSum += gaussMatrix[j + radius];

                    }

                }

                i = (y * width + x) * 4;

                pixes[i] = r / gaussSum;

                pixes[i + 1] = g / gaussSum;

                pixes[i + 2] = b / gaussSum;

                // pixes[i] = r ;

                // pixes[i + 1] = g ;

                // pixes[i + 2] = b ;

                // pixes[i + 3] = a ;

            }

        }

        //end

        return pixes;

    }



    var canvas = document.createElement('canvas'),

        ctx = canvas.getContext('2d');



    /**

     * 素描

     * @param  {Object} imgData  

     * @param  {Number} radius 取样区域半径, 正数, 可选, 默认为 3.0

     * @param  {Number} sigma 标准方差, 可选, 默认取值为 radius / 3

     * @return {Array}

     */

    function sketch(imgData, radius, sigma){

        var pixes = imgData.data,

            width = imgData.width, 

            height = imgData.height,

            copyPixes;



        discolor(pixes);//去色

        canvas.width = width, canvas.height = height;

        //复制一份

        ctx.clearRect(0, 0, width, height);

        ctx.putImageData(imgData, 0, 0);

        copyPixes = ctx.getImageData(0, 0, width, height).data;

        // 拷贝数组太慢

        // copyPixes = Array.prototype.slice.call(pixes, 0);

        invert(copyPixes);//反相

        gaussBlur(copyPixes, width, height, radius, sigma);//高斯模糊

        dodgeColor(pixes, copyPixes);//颜色减淡

        return pixes;

    }



    window.sketching = {

        discolor: discolor,

        invert: invert,

        dodgeColor: dodgeColor,

        gaussBlur: gaussBlur,

        sketch: sketch

    };



    if(typeof window.sk === 'undefined'){

        window.sk = window.sketching;

    }



})();
View Code

 

拖动加入图片(可以获取到图片base64串)

(function(){

    var $ = window.$ || function(id){

        return document.getElementById(id);

    }



    var toggleActionButton = function(status){

        if(status){

            action.classList.add('btn-primary');

            action.disabled = false;

        }else{

            action.classList.remove('btn-primary');

            action.disabled = true;

        }

    }



    var doSketch = function(){

        var st = Math.abs(strangth.value || 5);

        var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);

        sk.sketch(imgData, st);

        ctx.putImageData(imgData, 0, 0);

    }



    var defaultWidth = 640, defaultHeight = 480;

    var setCanvasSize = function(width, height){

        var scale = height / width,

            defaultScale = defaultHeight / defaultWidth;

        if(scale >= defaultScale && height >= defaultHeight){

            height = defaultHeight;

            width = height / scale;

        }

        if(scale <= defaultScale && width >= defaultWidth){

            width = defaultWidth;

            height = width * scale;

        }

        // console.log(width, height);

        canvas.width = width;

        canvas.height = height;

    }



    var drawImage = function(img){

        toggleActionButton(false);

        setTimeout(function(){

            //set the width/height will clear the canvas

            // canvas.width = img.width;

            // canvas.height = 640 * img.height / img.width;

            setCanvasSize(img.width, img.height);

            ctx.clearRect(0, 0, canvas.width, canvas.height);

            ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

            doSketch();

            download.href = canvas.toDataURL();

            toggleActionButton(true);

        }, 0);

    }



    var canvas = $('canvas'),

        action = $('action'),

        download = $('download'),

        strangth = $('strength'),

        dropper = $('dropper'),



        ctx = canvas.getContext('2d'),

        cacheImg;



    dropper.addEventListener('drop', function(e){

        e.preventDefault();

        dropper.innerHTML = '';

        var file = e.dataTransfer.files[0]; var reader = new FileReader();

        reader.onload = function(e){

            var img = new Image();

            img.onload = function(){

                cacheImg = this;

                drawImage(this);

            }

            img.src = e.target.result;

        }

        reader.onerror = function(e){

            var code = e.target.error.code;

            if(code === 2){

                alert('please don\'t open this page using protocol fill:///');

            }else{

                alert('error code: ' + code);

            }

        }

        reader.readAsDataURL(file);

    }, false);

    dropper.addEventListener('dragover', function(e){

        e.preventDefault();

    }, false);

    dropper.addEventListener('dragenter', function(e){

        e.preventDefault();

    }, false);



    action.addEventListener('click', function(e){

        if(cacheImg){

            drawImage(cacheImg);

        }else{

            alert('please select a picture first')

        }

    }, false);



})();

 

四、怎么用

说起用法啊,那你可以问对人了,哈哈。狠狠的敲入app的网址:http://apps.imatlas.com/sketching/(注意只能用现代浏览器(Chrome,Firefox,Opera,Safari等)打开哦,IE9以前的老古董就甭来啦),然后拖拽一张图片到画布区(就是下面打开的灰色地带~),然后……就没有然后啦,最多2秒之后自动生成素描画。点击download按钮可以下载生成的图片。

如果感觉效果不太好,可以改下取样的半径(Sample size),为正整数,最小为1。如果你一定要填负数、小数,也会被取正取整(抠鼻)。之后点下action按钮,生成新的素描图。

如果你还不明白,下面来看图说明(点击图片可以查看大图)。

sketching

sketching 图示

斋说都没益啦,实牙实齿效果才是王道,看看下面的原图:

使用Canvas把照片转换成素描画

原图

转换后的素描图:

使用Canvas把照片转换成素描画

素描

怎么样,效果是不是还不错咧,嘎嘎嘎。当然,这个算法未必是最好的,欢迎各位童鞋踊跃拍砖,^_^

 

你可能感兴趣的:(canvas)