前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >canvas 处理图像(下)

canvas 处理图像(下)

作者头像
用户8921923
发布2022-10-24 19:08:19
1.7K0
发布2022-10-24 19:08:19
举报
文章被收录于专栏:全栈私房菜

canvas 处理图像(下)

1. 访问像素值

虽然「调整尺寸」「裁剪」「变形」可用来创建有趣的图像效果,但画布还有另一个更强大的特性:「像素处理」。通过访问 2D 渲染上下文的各个像素,我们就能够得到每一个像素的颜色和阿尔法值等信息。我们还能够修改每一个像素的颜色,使之显示出截然不同的效果,后续将介绍这个功能。

在画布中访问像素的方法是getImageData。这个方法有 4 个参数:要访问的像素区域原点坐标(x, y)、像素区域的宽度和高度。它可以用代码表示为:

代码语言:javascript
复制
context.getImageData(x, y, width, height);

调用getImageData不会出现任何可见的效果,但是它会返回一个 2D 渲染上下文ImageData对象。这个ImageData对象包含3个属性:width表示所访问像素区域的宽度,height表示像素区域的高度,data是一个包含所访问区域中全部像素信息的CanvasPixelArray

我认为widthheight属性不需要多做解释了,此处我们真正关注的是data属性。data属性存储的是一个CanvasPixelArray,它是-个JavaScript一维数组。每一个像素用 4 个整数值表示,范围从 0 至 255,分别表示红(r)、绿(g)、蓝(b)和阿尔法值(a)。所以,数组的前 4 项(0-3)是第一个像素的颜色值,接下来 4 项(4-7)是第二个像素的颜色值,以此类推。CanvasPixelArray在这里是关键,所以一定要正确理解它的工作原理。

在详细解释之前,我们先看一个简单示例。我们使用索引数字来访问CanvasPixelArray中第一个像素的RGBA值。

代码语言:javascript
复制
const imageData = context.getImageData(0, 0, 3, 3); // 3×3 栅格
const pixel = imageData.data; // CanvaspixelArray

const red = pixel[0];
const green = pixel[1];
const blue = pixe1[2];
const alpha = pixel[3];

CanvasPixelArray本身绝对不知道所访问的像素区域的尺寸。相反,返回的数组实际上只是一长串 RGBA 颜色值,它的长度等于所访问区域的像素个数乘以 4(每个像素有4个颜色值)。例如,如果访问一个宽度和高度均为 3 个像素的像素栅格,那么CanvasPixelArray的长度就是36(3×3×4),宽度和高度为200时,则长度为160000(200×200×4),以此类推。

CanvasPixelArray中的像素排列顺序很简单:左上角像素位于数组开头(从位置 0 红色到位置 3 阿尔法值),而右下角像素位于数组末尾。这意味着,在所访问的区域中,每一行像素是从左到右访问的,直至到达行尾,然后再同样从左到右访问下一行。

所以,如果CanvasPixelArray只是一长串颜色值,而不知道像素区域的尺寸,那么应该如何从数组访问一个具体像素呢?幸好,一些聪明的人已经帮我们计算出一个公式,我们可以用这个公式准确地计算出你需要从CanvasPixelArray中访问的像素,而且它非常简单:

代码语言:javascript
复制
const imageData = context.getImageData(0, 0, 3, 3); // 3 x 3 栅格

const width = imageData.width;
const x = 2;
const y = 2;

const pixelRed = ((y - 1) * (width * 4)) + ((x - 1) * 4);
const pixelGreen = pixelRed + 1;
const pixelBlue = pixelRed + 2;
const pixelAlpha = pixelRed + 3;

现在,我们最关注的地方是计算像素红色值索引位置的公式。我们拆解分析这个公式,以了解它的计算原理:

(y-1)因为我们使用非0坐标值定义像素的(x, y)坐标位置,所以需要将坐标值减1。它的作用只是将画布所使用的坐标系统转换为数组所使用的从0开始的坐标系统。

(width*4)这会得到图像中每一行的颜色值个数。通过将(y-1)的结果与这个数相乘,就能够得到所访问行的开头位置的数组索引值(y坐标位置)。在这个例子中,索引值是12。

(×-1)*4这里我们对 y 坐标位置重复相同的计算——将它转换成从0开始的坐标系统。然后,将列(x左位置)乘以4,得到所访问列的前一行颜色值个数。

将列索引值与行索引值相加,最终可以得到所访问像素的第一个颜色(红色)的索引值。在这个例子中,它应该是16。

一旦得到红色像素的索引值,其他部分就很简单了。只需要给红色索引值分别加上1、2 或 3,就可以得到其他三种颜色——绿、蓝和阿尔法值。

我相信,这一步不难理解,我希望通过这样的解释,你已经能够理解访问画布像素的方法和原因了。

在继续学习其他内容之前,我们来创建一个有趣的「颜色拾取器」

代码语言:javascript
复制
const canvas = document.getElementById('myCanvas');
const context = canvas.getContext('2d');

const image = new Image();
image.src = "picture.jpg";
image.onload = function () {
  context.drawImage(image, 0, 0, 500, 333);
}

canvas.onclick = function (e) {
  const canvasX = Math.floor(e.pageX - canvas.offsetLeft);
  const canvasY = Math.floor(e.pageY - canvas.offsetTop);

  const imageData = context.getImageData(canvasX, canvasY, 1, 1);
  const pixel = imageData.data;
  const pixelColor =  `rgba(${pixel.join(',')})`;
  
  document.getElementsByTagName('body')[0].style.backgroundColor = pixelColor;
}

我们指定元素上发生鼠标点击事件。在这里,元素就是画布。onc1ick事件的处理函数会传递给你一个包含事件信息的参数,这里是。这个参数包含了相对于整个浏览器窗口的鼠标点击位置的(x, y)坐标,它可用来处理画布上发生的点击事件。

通过使用offsetLeftoffsetTop方法,我们就能够得到画布与浏览器窗口顶部和左边的像素距离。然后,用鼠标点击位置的 x 坐标(pageX)减去画布的左侧偏移量,就可以得到点击位置在画布上的 x 坐标。如果对鼠标点击位置 y 坐标和顶部偏移量进行相同的计算,将得到鼠标点击位置相对于画布原点的(x, y)坐标值。

image.png

现在,我们得到了点击位置在画布中的(x, y)位置,下一步是查询该点的颜色值。为此,我们将canvasXcanvasY传人getImageData方法。我们只需要一个像素的数据,这就是把getImageData调用的宽度和高度都设为 1 的原因,这样可以保持数据尽可能小。

一旦得到ImageData对象,就可以将它保存在一个变量中,然后访问data属性中的CanvasPixelArray。由于只得到一个像素的数据,所以检索颜色值就简单到只需访问CanvasPixelArray中的前 4 个索引。我们将修改整个网页的CSS背景,所以要用这些值创建一个表示CSS RGBA颜色值的字符串。

最后一步是修改HTML body元素的background-color CSS属性。如果一切正常,这会把网页的背景颜色设置为你在画布中点击的那个像素的颜色。

2. 从零绘制图像

现在可以开始制作一些真正漂亮的图像了,例如从创建像素开始制作自己的图像。

要创建一些像素,需要调用 2D 渲染上下文的createImageData方法。通过传人宽度和高度,它会返回一个包含所有常规属性的ImageData对象:widthheight和(最重要的)datadata 属性所包含的CanvasPixelArray将保存新的像素,此时它们是不可见的,因为它们都被设置为透明黑色。

在下一个例子中,我们将创建一个包含 200×200 透明像素区域的ImageData对象,然后将它们全部修改成红色。

代码语言:javascript
复制
const imageData = context.createImageData(200, 200);
const pixels = imageData.data;

变量pixels仅用作访问CanvasPixelArray中的像素的快捷方式。

修改颜色值与查询颜色值一样简单:都是读写CanvasPixelArray中的颜色值。如果想将所有像素修改为红色,那么需要使用for循环语句遍历每一个像素。

代码语言:javascript
复制
const numpixels = imageData.width * imageData.height;

for (let i = 0; i < numpixels; i++) {
  pixels[i * 4] = 255; // 红
  pixels[i * 4 + 1] = 0; // 绿
  pixels[1 * 4 + 2] = 0; // 蓝
  pixels[i * 4 + 3] = 255; // 透明度
}

变量numPixels保存了ImageData对象中的像素个数,它就是for循环的执行次数。在每一次循环过程中,我们都使用一个简单算法给每个像素赋予颜色值。每个像素都有4个颜色值,所以将像素个数乘以4就能够得到该像素的红色颜色值在CanvasPixelArray中的索引位置.然后,就可以将红色颜色值设置为255(全色),绿色和蓝色设置为0,而阿尔法值设置为255,这样它就变成不透明的了。非常简单!

按照目前情况,我们所做的就是创建一个ImageData,然后将像素修改为红色。现在画布上还看不见任何效果,因为我们还没有将新像素画到上面。为此,我们需要调用 2D 渲染上下文的putImageData方法。这个方法可以接受 3 个或 7 个参数:ImageData对象、绘制像素数据的原点坐标(x, y)、所谓脏矩形的原点坐标(x, y)、脏矩形的宽度和高度。在这个例子中,你暂时可以不考虑脏矩形的用途,它的作用只是定义ImageData对象中需要绘制的像素。

代码语言:javascript
复制
context.putImageData(imageData, 0, 0);

这样会在画布原点绘制新的红色像素。

image.png

2.1 随机绘制像素

只有红色像素似乎太单调,让我们更进一步,绘制一些完全随机的颜色。这也很简单。

代码语言:javascript
复制
for (let i = 0; i < numpixels; i++) {
  pixels[i * 4] = Math.floor(Math.random()*255); // 红
  pixels[i * 4 + 1] = Math.floor(Math.random()*255); // 绿
  pixels[1 * 4 + 2] = Math.floor(Math.random()*255); // 蓝
  pixels[i * 4 + 3] = 255; // 透明度
}

通过修改前-一个例子中设置颜色值的代码,我们可以插入0至255之间的随机数。我们仍然保持阿尔法值为255,否则有一些像素会变成透明的。注意,我们使用了Math.floor来向下舍人产生的随机数(例如,150.456会变成150)。

结果,我们得到一些杂乱的像素点。

❝注意:Math.random可以产生 0 到 1 之间的随机小数。将它与另一个数字相乘,就可以得到0与该数字(乘数)之间的随机数。例如,Math.random()*255将得到0与255之间的一个随机数。 ❞

2.2 创建马赛克效果

但是,杂乱的像素并不是画布的最佳用途。那么创建一个马赛克效果呢?肯定更有意思一些。它的实现方法是,创建一个新像素区域,然后将它分割到一个栅格中,并为栅格每个片段设置随机颜色。最复杂的部分是计算出每个像素应该落到哪个片段,这样相同的片段就可以设置相同的颜色。在图5-19中,我们会看到每个片段实际上是由许多像素构成的。

稍后,我会介绍如何计算出每个片段的像素。现在,先来做一些基础性工作。

代码语言:javascript
复制
const imageData = context.createImageData(500, 500);
const pixels = imageData.data;

//马赛克块的个数
const numTileRows = 4;
const numtileCols = 4;

//每个块的尺寸
const tileWidth = imageData.width / numTileCols;
const tileHeight = imageData.height / numTileRows;

前两行代码现在你应该很熟悉了,它们创建了一个500×500像素的ImageData对象,然后将CanvasPixelArray保存在一个变量中。后面的代码是定义两个变量,用于声明像素区域划分的片段数,其中包括每行每列的马赛克数。从现在起,我们将片段称为块,因为这个词更能说明它们的实际作用。最后两行代码是根据ImageData对象的尺寸和各行各列的块数计算出每个块的宽度和高度(以像素为单位)。

现在,我们有了足够信息,可以开始遍历这些块和修改像素的颜色值。

代码语言:javascript
复制
for (let r = 0; r < numTileRows; r++) {
  for (let c = 0; c < numTileCols; c++) {
    //为每个块设置像素的颜色值
    const red = Math.floor(Math.random() * 255);
    const green = Math.floor(Math.random() * 255);
    const blue = Math.floor(Math.random() * 255);
  }
}

这是一个嵌套循环,第一个循环遍历每一行的块,第二个循环遍历当前行的每一列块。每一个块都赋了新的颜色值,这些值都是 0 至 255 的随机数。到现在为止,所有代码都是非常基础的。

现在,在列循环中颜色值的下方,我们要声明另外两个循环:

代码语言:javascript
复制
for (let tr = 0; tr < tileHeight; tr++) {
  for (let tc = 0; tc < tileWidth; tc++) {
    
  }
}

根据之前计算的块尺寸,这些循环遍历的次数与每个块中的像素个数相同。变量trtc表示当前访问块的像素行(基于块的高度)和像素列(基于块的宽度)。在这个例子中,每一个块的宽和高都是125像素,所以tr将会循环125次,而在每一次循环中,tc将会再循环125次。

然而,我们现在仍然还无法访问每一个块中的实际像素。我们现在得到的是所访问的块的行和列(变量 rc ),以及你在该块中所处的像素的行和列(变量trtc )对于它们本身而言,这些变量并不足以用来访问CanvasPixelArray中的像素。为此,需要将它们转换为以 0 开始的像素位置坐标 (x, y),就像是没有块存在时那样。

将下面的代码添加到第二个循环中,然后我将解释会出现什么结果,这事实上是很简单的:

代码语言:javascript
复制
const trueX = (c * tileWidth) + tc;
const trueY = (r * tileHeight) + tr;

这两个变量可以计算出像素的真实位置。例如,要计算轴位置,首先要将当前块的列数(2)乘以每个块的宽度(125),这样就得到所访问块的左边缘的x坐标位置(2×125=250)。然后,再加上所访问的块中像素的列数(例如,10),这样就得到没有块时的x轴确切坐标(250+10=260)。对y轴重复这个过程,就可以得到开始修改像素颜色值的位置坐标(x, y)

将下面的代码加到trueXtrueY的赋值语句后面:

代码语言:javascript
复制
const pos = (trueY * (imageData.width * 4)) + (trueX * 4);

pixels[pos] = red;
pixels[pos + 1] = green;
pixels[pos + 2] = blue;
pixels[pos + 3] = 255;

这里并没有出现新代码,它只是访问像素的红色颜色值,然后使用之前设置的颜色值进行赋值。因为这里从0开始计算,所以必须将trueXtrueY减1,就像前面第一次看到这个公式时的做法一样。

最后一步是将像素绘制到画布上,所以要将下面的putImageData调用放到4个循环之外:

代码语言:javascript
复制
context.putImageData(imageData, 0, 0);

如果一切正常,画布上就会出现生动的马赛克效果。

通过修改每行和每列的块数,还能创建出更有趣的效果。

3. 基本图像效果

修改像素的颜色值并不意味着必须从零开始创建整个图像,已经存在的图像也是可以修改的。有一个例子就是基本照片处理——通过修改图像中的像素来修改它的显示效果。这种效果在画布中实现是很简单的,特别是现在你已经掌握了像素的操作方法。

3.1 灰度

将彩色图像变为灰色(有时候也称为黑白色;但是这种说法并不准确),除了访问和修改颜色值。

代码语言:javascript
复制
for (let i = 0; i < numPixels; i++) {
  const average = (pixels[i * 4] + pixels[i * 4 + 1] + pixels[i * 4 + 2]) / 3;
  pixels[i * 4] = average; // 红色
  pixels[i * 4 + 1] = average; // 绿色
  pixels[i * 4 + 2] = average; // 蓝色
}

将彩色转换为灰度要求计算出现有颜色值的平均值,即将它们加在一起然后除以颜色个数。这个平均颜色将作为三种颜色(红、绿和蓝)的值。其结果是将每一种颜色转换为灰度。

3.2 像素化

你是否曾经看到过新闻或文件中人物脸孔被像素化的情况?这是一种强大的特效,它可以将图像变得不可识别,但并不真正删除整个部分。实际上重新在画布上创建会相对简单一些,只需要将图像按栅格分割,或者对每个片段的颜色取平均值,或者选取每个片段的颜色。

代码语言:javascript
复制
const image = new Image();
image.src = "picture.jpg";
image.onload = function () {
  context.drawImage(image, 0, 0, 1024, 683, 0, 0, 500, 500);

  const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
  const pixels = imageData.data;

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

  const numTileRows = 20;
  const numTileCols = 20;

  const tileWidth = imageData.width / numTileCols;
  const tileHeight = imageData.height / numTileRows;
  
  for (let r = 0; r < numTileRows; r++) {
    for (let c = 0; c < numTileCols; c++) {

    }
  }
}

访问图像,等待图像加载,将它绘制到画布中,保存ImageData对象,从画布清除该图像,然后给分割的图像赋值确定块(片段)的数量和尺寸。

这两个循环的工作方式与马赛克的例子是一样的:第一个循环处理每一行块,第二个循环则处理当前行中的每一个块。而新的代码位于循环中,访问颜色值和创建像素化效果。

这里获取像素化效果的颜色值,为每一个块选择一种颜色。最简单的方法是使用块的中心位置像素,将以下代码添加到第二个循环中,就可以得到这个信息:

代码语言:javascript
复制
const x = (c * tileWidth) + (tileWidth / 2);
const y = (r * tileHeight) + (tileHeight / 2);
const pos = (Math.floor(y) * (imageData.width * 4)) + (Math.floor(x) * 4);

前两行将得到当前块中心像素从 0 开始表示的(x, y)坐标。这个计算方法与马赛克例子非常相似,先找到块边缘的(x, y)坐标位置,然后加上一半宽度或高度,从而确定中心。然后将(x, y)坐标传入标准公式,这样就得到CanvasPixelArray中该像素的索引值。但你可能注意到了,(x, y)坐标值在Math对象的floor方法中进行了取整处理。其原因是,除非(x, y)是整数,否则这个返回的素引将是错误的,所以我们使用floor方法将值取整为下一个最小整数(例如,3.567取整后变成3)。

最后,我们得到了访问颜色值和绘制像素化效果所需要的全部信息。将下面的代码插入到变量pos的声明语句之后。

代码语言:javascript
复制
const red = pixels[pos];
const green = pixels[pos + 1];
const blue = pixels[pos + 2];
context.fillStyle = "rgb(" + red + "," + green + "," + blue + ")";
context.fillRect(x - (tileWidth / 2), y - (tileHeight / 2), tileWidth, tileHeight);

这里没有新代码,它只是访问红色、绿色和蓝色值,然后使用这些值来设置fillSty1e。最后一步是在块的位置上绘制一个正方形,它是使用所访问的颜色填充的。

我们可以进一步将正方形修改为圆形。

代码语言:javascript
复制
context.beginPath();
context.arc(x, y, tileWidth / 2, 0, Math.PI * 2, false);
context.closePath();
context.fill();
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 全栈私房菜 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • canvas 处理图像(下)
    • 1. 访问像素值
      • 2. 从零绘制图像
        • 2.1 随机绘制像素
        • 2.2 创建马赛克效果
      • 3. 基本图像效果
        • 3.1 灰度
        • 3.2 像素化
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档