图片是由一个个像素点组成。每一个像素点包含四个值,决定了渲染出来的状态。这四个值为rgba(red, green, blue, alpha)
。
前三个值是 红绿蓝,值的大小范围从 0到255 ,或者从 0%到100% 之间。
第四个值 alpha,规定了色彩的透明度,它的范围为0到1之间。其中0代表完全透明,1代表完全可见。通过getImageData
方法得到的alpha值范围为0~255
红绿蓝是色彩中的三元色,通过设置这三种颜色所占的比重,可以变幻出其他所有颜色。
既然每个像素点可以通过rgba的值来表达,那么一张图片所包含的所有像素点都可以转换成数据。如果修改某部分像素点的rgba值,那该图片渲染出来的效果就会发生变化,这样便实现了图片的编辑。
我们通过一个换肤功能来理清具体过程。
首先我们定义一些全局信息
// 定义图像宽高
let imgWidth = null
let imgHeight = null
// 定义canvas画布
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d');
那怎么把图片转化成由像素点组成的数据呢?
首先编写一个getImageData函数将原始图片转化成数据(代码如下)。
图片转换成像素数据按以下两步操作。
/**
* 获取图片像素点信息
*
* @param { String } picPath 图片路径
*
* @returns Promise
**/
function getImageData(picPath) {
const image = new Image();
image.src = picPath;
return new Promise((resolve) => {
image.onload = (e) => {
imgWidth = e.target.width
imgHeight = e.target.height
canvas.width = imgWidth
canvas.height = imgHeight
ctx.drawImage(image, 0, 0, imgWidth, imgHeight); // 将图片绘制到画布上
const imgData = ctx.getImageData(0, 0, imgWidth, imgHeight); // 获取画布上的图像像素
resolve(imgData.data) // 获取到的数据为一维数组,包含图像的RGBA四个通道数据
ctx.clearRect(0, 0, imgWidth, imgHeight);
}
})
}
在上一步我们获取到了图片像素点信息,数据结果(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(代码如下)实现数据格式的转换。
/**
* 将一维数组化为矩阵结构数组
*
* @param { Array } data 一维数组
**/
function normalize(data) {
const pixelPointArr = []
const result = []
const len = data.length / 4
// 将rgba数据形成一个数组
for (let i = 0; i < len; i++) {
const start = i * 4;
pixelPointArr.push([data[start], data[start + 1], data[start + 2], data[start + 3]]);
}
// 将像素数据与视图形成对应
for (let h = 0; h < imgHeight; h++) {
const temp = []
for (let w = 0; w < imgWidth; w++) {
temp.push(pixelPointArr[h * imgWidth + w])
}
result.push(temp)
}
return result
}
对矩阵数组进行操作,从而实现具体功能。实现代码如下,peeling函数负责变换图片的颜色。
观察代码,实现思路如下案例:
由于 黑色的rgb值是(0,0,0) 。那么只需要判断出像素点是黑色,就重置其 rgb值为(255,255,0) 便能将图片中所有的黑色换成黄色。
此处所写是个通用的换肤函数。由用户决定需要转换的像素RGB,以及转换后的像素RGB。
/**
* 图片换肤
*
* @param { Array } data 矩阵结构数组
* @param { Array } originRGB 需要转换的原始RGB,结构类似[255, 255, 255]
* @param { Array } endedRGB 转换后的RGB,结构如上
**/
function peeling(data, originRGB, endedRGB) {
for (let h = 0; h < data.length; h++) {
for (let w = 0; w < data[h].length; w++) {
// 排除透明度的比较
if (data[h][w].slice(0, 3).join() === originRGB.join()) {
data[h][w] = [...endedRGB, data[h][w][3]];
}
}
}
return data
}
矩阵的数据操作完了,还需要调用restoreData函数将多维数组再转回一维数组传给putImageData
方法进行图形渲染。
/**
* 转化为一维数组
*
* @param { Array } data 矩阵结构数组
**/
function restoreData(data) {
const result = [];
for (let h = 0; h < data.length; h++) {
for (let w = 0; w < data[h].length; w++) {
result.push(...data[h][w]);
}
}
return result;
}
数据处理完毕后,将处理完的数据data传递给drawImage函数渲染成新图片(代码如下)。
渲染图像主要调用以下两个api。
/**
* 绘制图片
*
* @param { Array } data 一维数组
**/
function drawImage(data) {
const matrixObj = ctx.createImageData(imgWidth, imgHeight);
matrixObj.data.set(data);
ctx.putImageData(matrixObj, 0, 0);
document.body.appendChild(canvas)
}
这样整体流程就结束了,总体函数调用逻辑如下
getImageData('./images/xxx.png')
.then((data) => {
data = normalize(data); // 转化成多维数组
data = peeling(data, [183, 183, 183], [255, 255, 255]); // 换肤
data = restoreData(data); // 转化成一维数组
drawImage(data); // 绘制图像
})
至此新图片便成功渲染了出来。可以看下效果对比图。左图为原图,右图为处理之后的图片。将灰色背景(rgb值183, 183, 183)替换为白色背景(rgb值255, 255, 255)
回顾上述操作,编辑图像主要分解成以下四步。
上述第二步操作是图像编辑的核心,很多复杂的变换效果可以通过编写矩阵算法实现。
为了加深理解,利用上述知识点实现一个图片旋转的需求。
假定存在最简单的情况如下图所示,其中左图存在四个像素点。第一行有两个像素点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],刚好索引顺序颠倒。
通过以上描述规律便可编写下面函数实现图片的旋转。下面的旋转算法只适用于正方形且长宽为偶数(长方形的图片要另外编写)。
/**
* 图片旋转90度
*
* @param { Array } data 矩阵结构数组
**/
function rotate90(data) {
// 围绕中间行上下颠倒
const mid = imgHeight / 2; // 找出中间行
for (let h = 0; h < mid; h++) {
for (let w = 0; w < imgWidth; w++) {
const correspondingLine = imgHeight - 1 - h;
[data[h][w], data[correspondingLine][w]] = [data[correspondingLine][w], data[h][w]]
}
}
// 根据对角线进行值交换
for (let h = 0; h < imgHeight; h++) {
for (let w = h + 1; w < imgWidth; w++) {
[data[h][w], data[w][h]] = [data[w][h], data[h][w]]
}
}
return data
}
实现效果如下,左图为原图,右图为旋转90后的图片
实现思路是将图片画到canvas上,获取canvas的ImageData对象,对每个像素的颜色值进行反相处理。
/**
* 对图片进行反相处理
*
* @param { Array } data 一维数组
**/
function revertImg(data) {
const pixelLen = data.length / 4
for (let i = 0; i < pixelLen; i++) {
const start = i * 4
data[start] = 255 - data[start]
data[start + 1] = 255 - data[start + 1]
data[start + 2] = 255 - data[start + 2]
}
return data
}
实现效果如下,左图为原图,右图为反相后的图片
若引用的图片域名与当前不一致,则存在跨域的问题,会出现如下报错
chrome:Uncaught SecurityError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.
指向错误原因来自于getImageData
只能操作与脚本位于同一个域中的图片。
解决方法是将文件放到同域名服务器目录下,通过服务器访问,这样就不会报错了。