使用Canvas把照片转换成素描画
原文:http://www.alloyteam.com/2012/07/convert-picture-to-sketch-by-canvas/
腾讯的alloy team写的一个素描效果,挺不错的。
<img title="sketch" src="http://www.alloyteam.com/wp-content/uploads/auto_save_image/2012/07/0208516qY.png" alt="" width="534" height="398" />
一、引子
话说前阵子想把一张照片转换成素描,然后发个微博。结果发现mac上没找到能直接转换素描的软件(PS不算,可要好几步呢),坑爹啊~~google 了下,Web上竟然也是没有直接把照片转换成素描的东西,连让我包含期望的美图秀秀(Web版)竟然都没有素描功能,T_T。
手机上是有很多这类app,但是我只是想一键转换下,发个微博嗟,至于这么折腾么……
所以自己动手整一个在线版的吧,没怎么用过canvas,正好可以顺道熟悉下。等不及的童鞋可以先到这里看看效果(http://apps.imatlas.com/sketching/)。
二、怎么转换
刚冒出这个想法的时候,简直是一头雾水诶~数学不行、PS不懂、图形学忘光了……
还好有万能的google,翻了几页,找到一个ps制作素描图片的步骤——虽然我不懂,但是如果按照这个步骤用PS能做成素描,我用代码也一定可以的。嗯,一定是的。
PS里面最简单的一个转换素描的步骤为:
- 去色(黑白化)
- 复制一份,反相
- 把复制后的图层叠加方式设为颜色减淡
- 高斯模糊
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;
} })();
拖动加入图片(可以获取到图片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按钮,生成新的素描图。
如果你还不明白,下面来看图说明(点击图片可以查看大图)。
![](http://www.alloyteam.com/wp-content/uploads/auto_save_image/2012/07/020848a60.png)
sketching 图示
斋说都没益啦,实牙实齿效果才是王道,看看下面的原图:
![](http://www.alloyteam.com/wp-content/uploads/auto_save_image/2012/07/020850Svs.jpg)
原图
转换后的素描图:
![](http://www.alloyteam.com/wp-content/uploads/auto_save_image/2012/07/0208516qY.png)
素描
怎么样,效果是不是还不错咧,嘎嘎嘎。当然,这个算法未必是最好的,欢迎各位童鞋踊跃拍砖,^_^
使用Canvas把照片转换成素描画的更多相关文章
- Joyoshare HEIC Converter for Mac将HEIC照片转换成其他格式的方法
如何把HEIC格式的照片转换成其JPEG,PNG,GIF他格式呢?使用Joyoshare HEIC Converter for Mac破解版就可以,Joyoshare HEIC Converter是可 ...
- 女神说拍了一套写真集想弄成素描画?很简单,用Python就行了!
素描作为一种近乎完美的表现手法有其独特的魅力,随着数字技术的发展,素描早已不再是专业绘画师的专利,今天这篇文章就来讲一讲如何使用python批量获取小姐姐素描画像.文章共分两部分: 第一部分介绍两种使 ...
- 如何将你拍摄的照片转换成全景图及六面体(PTGui)
在完成全景照片的拍摄之后,接下来,我们需要的是进行全景图的拼接.全景图片分为两种类型1.立方体全景图(6面体)制作全景时通常使用该种格式 如下图 2.球形图(2:1的单张全景图片)2:1全景图宽高比例 ...
- 使用的是html5的canvas将文字转换成图片
当前功能的运用场景是:用户需要传文件给他人,在用户选择文件之后需要显示一个文件图标和所选文件的名称. 当前代码部分是摘自网上,但是已经忘记在什么地方获取的,如有侵权联系小弟后自当删除. 注意:必须在h ...
- python 实用技巧:几十行代码将照片转换成素描图、随后打包成可执行文件(源码分享)
效果展示 原始效果图 素描效果图 相关依赖包 # 超美观的打印库 from pprint import pprint # 图像处理库 from PIL import Image # 科学计算库 imp ...
- Canvas将图片转换成base64形式展示的常见问题及解决方案
导航1:https://blog.csdn.net/weixin_30668887/article/details/98822699 导航2:https://stackoverflow.com/que ...
- 办公室文员必备python神器,将PDF文件表格转换成excel表格!
[阅读全文] 第三方库说明 # PDF读取第三方库 import pdfplumber # DataFrame 数据结果处理 import pandas as pd 初始化DataFrame数据对象 ...
- Swift - 从ALAsset中获取照片的原图并转换成NSData
ALAsset类代表相册中的每个资源文件,可以通过它获取照片的相关信息,及其对应的原图,全屏图,缩略图等. 当我们想通过一个照片的ALAsset对象,来获取这张照片的原图并将其转换成NSData数据, ...
- 使用canvas给图片添加水印, canvas转换base64,,canvas,图片,base64等转换成二进制文档流的方法,并将合成的图片上传到服务器,
一,前端合成带水印的图片 一般来说,生成带水印的图片由后端生成,但不乏有时候需要前端来处理.当然,前端处理图片一般不建议,一方面js的处理图片的方法不全,二是有些老版本的浏览器对canvas的支持度不 ...
随机推荐
- python练习程序(c100经典例13)
题目: 打印出所有的“水仙花数”,所谓“水仙花数”是指一个三位数,其各位数字立方和等于该数. for i in range(100,1000): a=i/100; b=(i/10)%10; c=i%1 ...
- 【英语】Bingo口语笔记(34) - Hit系列
hit it off 合得来 hit the bottle 喝醉酒 hit the spot 正合要求,恰到好处
- 虚拟机安装centos 6 报错Erro processing drive
错误提示: Error processing drive: pci-0000:00:10-scsi-0:0:0:0 20480MB VMware,VMware Virtual S This devic ...
- 微软官方的一段JavaScript判断.net环境
<HTML> <HEAD> <TITLE>Test for the .NET Framework 3.5</TITLE> <META HTTP-E ...
- 编译及load mydqli.so文件
(1)cd /usr/local/php-5.2.17/ext/mysqli(2)输入/usr/local/php/bin/phpize 回车(3)./configure --prefix=/usr/ ...
- Linux makefile教程之make运行八[转]
make 的运行 —————— 一 般来说,最简单的就是直接在命令行下输入make命令,make命令会找当前目录的makefile来执行,一切都是自动的.但也有时你也许只想让 make重编译某些文件, ...
- 二叉树的基本操作(C)
实现二叉树的创建(先序).递归及非递归的先.中.后序遍历 请按先序遍历输入二叉树元素(每个结点一个字符,空结点为'='): ABD==E==CF==G== 先序递归遍历: A B D E C F G ...
- JDT入门
1.打开Java类型 要打开一个Java类或Java接口以进行编辑,可以执行以下操作之一: 在编辑器中所显示的源代码里选择所要编辑的Java类或Java接口的名字(或者简单地将插入光标定位到所要编辑的 ...
- python 映射列表 学习
列表映射是个非常有用的方法,通过对列表的每个元素应用一个函数来转换数据,可以使用一种策略或者方法来遍历计算每个元素. 例如: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ...
- Leetcode 210 Course Schedule II
here are a total of n courses you have to take, labeled from 0 to n - 1. Some courses may have prere ...