基础概念
众所周知,图片是由一个个像素点组成.每一个像素点包含四个值,决定了渲染出来的状态.这四个值为rgba(red, green, blue, alpha)
.
前三个值是红绿蓝
,值的大小范围从0
到255
,或者从0%
到100%
之间.
第四个值alpha
,规定了色彩的透明度,它的范围为0
到1
之间.其中0
代表完全透明,1
代表完全可见.
红绿蓝
是色彩中的三元色,通过设置这三种颜色所占的比重,可以变幻出其他所有颜色.
比如某个标签的文字想设置为红色,就可以通过css
设置rgba
值(代码如下).
span {color: rgba(255, 0, 0, 1);
}
既然每个像素点可以通过rgba
的值来表达,那么一张图片所包含的所有像素点都可以转换成数据.如果修改某部分像素点的rgba
值,那该图片渲染出来的效果就会发生变化,这样便实现了图片的编辑.
那怎么把图片转化成由像素点组成的数据呢?
实现
图片转换数据
一段简单的html
结构如下,页面上放置一个原始图片和一个canvas
画布,宽高都为300
;
<body><p class="image"><img src="./img/rect.png" width="300" height="300" /></p><canvas id="myCanvas" width="300" height="300"></canvas>
<body>
首先编写一个getImageData
函数将原始图片转化成数据(代码如下).
图片转换成像素数据按以下两步操作.
- 调用
ctx.drawImage
将图片绘制到画布上 - 调用
ctx.getImageData
获取像素数据
const dom = document.getElementById("myCanvas"); // canvas画布getImageData(dom,"./img/rect.png").then((data)=>{console.log(data); // 打印输出像素数据})function getImageData(dom,url){const ctx = dom.getContext("2d"); // 设置在画布上绘图的环境const image = new Image();image.src= url;//获取画布宽高const w = dom.width;const h = dom.height;return new Promise((resolve)=>{image.onload = function(){ctx.drawImage(image, 0, 0 ,w,h); // 将图片绘制到画布上const imgData = ctx.getImageData(0,0,w,h); // 获取画布上的图像像素resolve(imgData.data) // 获取到的数据为一维数组,包含图像的RGBA四个通道数据ctx.clearRect(0,0,w,h);} })
}
最终的打印出来的数据结果(data
)如下:
data = [255, 255, 255, 255, 255, 61, 61, 255, 255, 0, 0, 255, 255,...]
data
是一维数组,数组的前四个值[255, 255, 255, 255]
为图片第一个像素点的rgba
值(ctx.getImageData
返回的透明度大小范围是从0 - 255
的),[255, 61, 61, 255]
是图片第二个像素点的rgba
值,后面依次类推.如此便成功的将图片转化成了数据.
数据格式化
虽然图片成功转化成了数据,但这样的数据结构很难操作,我们期待能够将数据结构的表现形式与图片展示效果保持一致.
假如存在四个都是黑色的像素点(如下图),总宽高都为2
,值为[0, 0, 0, 255,0, 0, 0, 255,0, 0, 0, 255,0, 0, 0, 255]
.
通过某个函数转换,数据就变成了下列格式.
[[[0, 0, 0, 255],[[0, 0, 0, 255]]], // 第一行[[0, 0, 0, 255],[[0, 0, 0, 255]]] // 第二行
]
上列数据格式和图片的展示结构保持了一致,可以很清晰的看出当前图形有多少行,每一行又有多少个像素点,
以及每一个像素点的rgba
值.
综合上面描述,可以编写函数normalize
(代码如下)实现数据格式的转换.
const dom = document.getElementById("myCanvas"); // canvas画布getImageData(dom,"./img/rect.png").then((data)=>{console.log(normalize(data,dom.width,dom.height)); // 打印格式化后的像素数据
})function normalize(data,width,height){const list = [];const result = [];const len = Math.ceil(data.length/4);// 将每一个像素点的rgba四个值组合在一起for(i = 0;i<len;i++){const start = i*4;list.push([data[start],data[start+1],data[start+2],data[start+3]]);}//根据图形的宽和高将数据进行分类for(hh = 0;hh < height;hh++){const tmp = [];for(ww = 0; ww < width;ww++){tmp.push(list[hh*width + ww]);}result.push(tmp);}return result;
}
换肤需求
通过normalize
函数的转换,一维数组的图片数据转换成了矩阵形式.有了矩阵,我们就可以更加方便的实现编辑图片的需求.
首选我们简单实现一个图片换肤的需求,将图片中的黑色全部变成黄色(最终效果图如下).
上方的原始图片包含红蓝绿黑
四种颜色,下方是换肤后生成的新图片.
实现代码如下,peeling
函数负责变换图片的颜色.
观察代码,由于黑色的rgb
值是(0,0,0)
.那么只需要判断出像素点是黑色,就重置其rgb
值为(255,255,0)
便能将图片中所有的黑色换成黄色.
const dom = document.getElementById("myCanvas"); // canvas画布getImageData(dom,"./img/rect.png").then((data)=>{data = peeling(data,dom.width,dom.height); // 换肤drawImage(dom,data); // 绘制图像
})function peeling(data,w,h){data = normalize(data,w,h); // 转化成多维数组// 将黑色变成黄色 (0,0,0) -> (255,255,0) for(let i = 0;i<data.length;i++){for(let j = 0;j<data[i].length;j++){//排除透明度的比较if(data[i][j].slice(0,3).join("") == "000"){data[i][j] = [255,255,0,data[i][j][3]];}}}return restoreData(data); // 转化成一维数组
}
矩阵的数据操作完了,还需要调用restoreData
函数将多维数组再转回一维数组传给浏览器渲染.
function restoreData(data){const result = [];for(let i = 0;i<data.length;i++){for(let j = 0;j<data[i].length;j++){result.push(data[i][j][0],data[i][j][1],data[i][j][2],data[i][j][3]);}}return result;}
渲染图片
数据处理完毕后,还需将处理完的数据data
传递给drawImage
函数渲染成新图片(代码如下).
渲染图像主要调用以下两个api
.
ctx.createImageData
.创建新的空白ImageData
对象,通过.data.set
重新赋值.ctx.putImageData
.将像素数据绘制到画布上.
const dom = document.getElementById("myCanvas"); // canvas画布getImageData(dom,"./img/rect.png").then((data)=>{data = peeling(data,dom.width,dom.height); // 换肤drawImage(dom,data); // 绘制图像
})function drawImage(dom,data){const ctx = dom.getContext("2d");const matrix_obj = ctx.createImageData(dom.width,dom.height);matrix_obj.data.set(data);ctx.putImageData(matrix_obj,0,0);
}
至此新图片便成功渲染了出来,效果图如下.
回顾上述操作,编辑图像主要分解成以下三步.
- 将原始图片转化成矩阵数据(多维数组)
- 依据需求操作矩阵
- 将修改后的矩阵数据渲染成新图片
上述第二步操作是图像编辑的核心,很多复杂的变换效果可以通过编写矩阵算法实现.
为了加深理解,利用上述知识点实现一个图片旋转的需求.
图片旋转
假定存在最简单的情况如下图所示,其中左图存在四个像素点.第一行有两个像素点1
和2
(这里用序号代替rgba
值).
第二行也有两个像素点3
和4
.数据源转换成矩阵data
后的值为 [[[1],[2]],[[3],[4]]]
.
如何将左图按顺时针旋转90
度变成右图?
通过观察图中位置关系,只需要将data
中的数据做位置变换,让data = [[[1],[2]],[[3],[4]]]
变成data = [[[3],[1]],[[4],[2]]]
,就可以实现图片变换.
四个像素点可以直接用索引交换数组的值,但一张图片动辄几十万个像素,那该如何进行操作?
这种情况下通常需要编写一个基础算法来实现图片的旋转.
首先从下图中寻找规律,图中有左 - 中 - 右
三种图片状态,为了从左图的1-2-3-4
变成右图的3-1-4-2
,可以通过以下两步实现.
-
寻找矩阵的高度的中心轴线,上下两侧按照轴线进行数据交换.比如左图
1 - 2
和3 - 4
之间可以画一条轴线,上下两侧围绕轴线交换数据,第一行变成了3 - 4
,第二行变成了1 - 2
.通过第一步操作变成了中图的样子. -
中图的对角线
3 - 2
和右图一致,剩下的将对角线两侧的数据对称交换就可以变成右图.比如将中图的1
和4
进行值交换.操作完后便实现了图片的旋转.值得注意的是4
的数组索引是[0][1]
,而1
的索引是[1][0]
,刚好索引顺序颠倒.
通过以上描述规律便可编写下面函数实现图片的旋转.
const dom = document.getElementById("myCanvas"); // canvas画布// getImageData 获取像素数据
getImageData(dom,"./img/rect.png").then((data)=>{data = rotate90(data,dom.width,dom.height); // 顺时针旋转90度drawImage(dom,data); // 绘制图像
})function rotate90(data,w,h){data = normalize(data,w,h); // 转化成矩阵// 围绕中间行上下颠倒const mid = h/2; // 找出中间行for(hh = 0;hh < mid;hh++){const symmetric_hh = h - hh -1; // 对称行的索引for(ww = 0;ww<w;ww++){let tmp = data[hh][ww];data[hh][ww] = data[symmetric_hh][ww];data[symmetric_hh][ww] = tmp;}}// 根据对角线进行值交换for(hh = 0;hh < h;hh++){for(ww = hh+1;ww<w;ww++){let tmp = data[hh][ww];data[hh][ww] = data[ww][hh];data[ww][hh] = tmp;}}return restoreData(data); // 转化成一维数组
}
由于我们定义的canvas
宽高都为300
,上面的旋转算法只适用于正方形(长方形的图片要另外编写).
最终页面展示的效果图如下(上面是原始图片,下面是画布生成的新图片).