图片像素分析与功能实现

1,007 阅读7分钟

rgba概念

图片是由一个个像素点组成。每一个像素点包含四个值,决定了渲染出来的状态。这四个值为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函数将原始图片转化成数据(代码如下)。

图片转换成像素数据按以下两步操作。

  • 调用ctx.drawImage(img, x, y, width, height) 用于创建ImageData对象
  • 调用ctx.getImageData(x, y, width, height) 用于从canvas中获取ImageData对象
/**
 * 获取图片像素点信息
 * 
 * @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。

  • ctx.createImageData(width, height) 创建新的空白ImageData对象,通过.data.set重新赋值。
  • ctx.putImageData(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight) 将图像数据(从指定的 ImageData 对象)放回画布上,可以只输入前三个参数。参数分别是:
    • imagedata:包含像素值的数组对象。
    • dx:源图像数据在目标画布中的位置偏移量(x 轴方向的偏移量)。
    • dy:源图像数据在目标画布中的位置偏移量(y 轴方向的偏移量)。
    • dirtyX:(可选)在源图像数据中,矩形区域左上角的位置。默认是整个图像数据的左上角(x 坐标)。
    • dirtyY:(可选) 在源图像数据中,矩形区域左上角的位置。默认是整个图像数据的左上角(y 坐标)。
    • dirtyWidth:(可选) 在源图像数据中,矩形区域的宽度。默认是图像数据的宽度。
    • dirtyHeight:(可选) 在源图像数据中,矩形区域的高度。默认是图像数据的高度。
/**
 * 绘制图片
 * 
 * @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只能操作与脚本位于同一个域中的图片。

解决方法是将文件放到同域名服务器目录下,通过服务器访问,这样就不会报错了。