getImageData
撰写时间:2025-04-03
修订时间:2025-04-03
设定目标
现在,我们准备取出一个Canvas所绘制的内容,然后居中绘制到另一个Canvas上面。
对此并非长方形的图形,由于它是使用内部数据来绘制的,因此较难直接居中。但我们可以通过ctx的getImageData方法来获取其内部图像像素数据,找到其4个顶点的位置,再精准定位。
getImageData初窥
getImageData方法返回一个ImageData的实例imgData。该对象有4个重要属性,分别是width, height, colorSpace及data。
width及height属性值分别对应于getImageData方法最后2个参数width及height,上面以canvas的width及height属性值传入。
由于CanvasEditor默认应用了devicePixelRatio(在笔者电脑上默认为2.0),因此,canvas的绘图环境ctx的大小分别为400px及400px。这两个值,即是canvas的width及height属性值。
colorSpace值为srgb,表示使用了sRGB的颜色空间,故此每个像素均分为rgba
共4个通道,因此,像素的总数量及imgData.data的数组元素数量的关系为:
getImageData详解
从上节看出,由于需要使用4个数组元素来表示一个像素的rgba通道成分,因此imgData.data这个数组的元素数量往往太大了。为简化问题,本节将只在一个尺寸为10px × 10px的Canvas上绘制一条水平直线,以专注考查imgData.data的存储结构,以及其与像素数量、像素索引值、像素内容、Canvas宽高之间的内在关系。
此外,也是出于简化问题之故,这里暂时不应用devicePixelRatio。
Canvas尺寸
Canvas的默认尺寸为300px × 150px,我们通过CSS将其尺寸改为100px × 100px,这将影响到canvas的clientWidth及clientHeight属性值。
而在initCanvas函数中,将影响绘图环境的canvas的width及height属性值分别取自于其clientWidth及clientHeight属性值。这可以避免图像在画布中可能产生偏移或模糊的问题。
因此,canvas的尺寸大小最终确定为100px × 100px。具体详见解决Canvas图像模糊的问题。
data属性
imgData的data属性的类型是Uint8ClampedArray,这是每个元素字长为1字节的类型化数组,每个元素的值域在[0, 255]的范围之内,正好对应于rgba的每个通道值。
上节谈到,对于每个像素的每个rgba通道值,需使用一个数组元素来存储其值,因此data这个数组的元素数量为:
data属性中的像素数量
在不应用devicePixelRatio的情况下,canvas中所有像素的数量值等于:
在调用getImageData方法时,可以只取出ctx的一部分数据,但上面的getData函数:
取出整个canvas的所有像素,则存储在data中所有像素的数量为:
而由于data的length是确定的,因此,用其除以rgba通道值,也可以得出存储在data中所有像素的数量:
data数组结构
data是一个一维数组,以线性的方式按顺序存储了每个像素的信息,且每4个相邻的元素存储了一个像素的rgba通道值。因此,根据特定像素索引值取出该像素数据的函数为:
getAllPixels函数用以取出所有像素:
所取出的每个像素的信息,均为一个数量为4的数组,分别对应于该像素的rgba通道值。因我们只使用一种颜色来绘图,因此,每个被点亮的像素,其值均为:
与我们在drawImg函数中所指定的颜色值完全一致:
从上面代码也可看出,我们只在Canvas的第一行绘制了一条直线,因此,在所返回的data数组中,只有前面10个像素有具体的颜色通道值,而后面其余的90个像素,则是完全透明的颜色:
取出有图像内容的像素索引值
函数getFirstHilightPixel用于取出第一个有图像内容的像素索引值:
从第0个像素开始遍历,如果各个rgba通道值中,只要有一个成分的值不为0,则表示该像素已被点亮,则返回其索引值。
同理,函数getLastHilightPixel用于取出最后一个有图像内容的像素索引值:
这次是从后面开始往前遍历。
取出第一个及最后一个有内容的索引值后,下面将用这两个值来确定有图像的内容在Canvas上的X轴与Y轴的坐标值。
一维数组与二维坐标值的相互转换
如前所述data是一个一维的线性数组,而要求出各个像素所对应的(x, y)坐标值,则是从一维数组转换为二维坐标值的过程。
在不应用devicePixelRatio的情况下,Canvas上每个坐标,均对应于每个像素。对于任意一个坐标(x, y),可使用函数getPixelIndexFromXY求出像素索引值:
此函数可应用在根据鼠标悬停的位置取出像素索引值的应用上面。在本站的Canvas ImageData Slider应用中,拉动Col (x)
及Row (y)
拉杆,可自动求出pixelIndex的值。
有时可根据特定像素索引值,将所在的像素转换为二维平面上的坐标值。getXYFromPixelIndex可用于此。
使用直线扫描法找出有图像内容的部分
现在,回到第一节的目标,在新的Canvas上居中绘制任意Canvas所绘制的内容。
对于任意的图形,我们可以为其绘制一个正好包裹其内容的矩形边界框,则该边界框内的所有内容即Canvas中有图像内容的部分。
注意,此边界框的确定,不能依靠在imgeData.data中的数据。它决定了内部图像数据的先后存储顺序,但与图像的四个端点无关。
而要确定任意图形的边界框,可使用直线扫描法来达成目标。
确定边界框的顶端直线的方法是,从Canvas的顶部开始,取一直线往其底部扫描。在扫描每行时,从左往右扫,从而确定各个扫描点。根据每个扫描点的位置,取出其像素,如果该位置的像素被点亮,则立即返回该位置作为Y轴上的最小坐标值。getMinY函数用于此目的。其逻辑是,对于每次的行扫描,一旦发现被点亮的像素,必定是Y轴上第一次发现的被点亮的像素,因为其上面若有被点亮的点,则函数早已经返回。
这种算法,只能确定所找到的像素必定是Y轴上的最小坐标值,但不能确定是否X轴上的最小坐标值。因此该函数只返回一个数值。
其余类似的3个函数的原理皆与此相同。
上面找到边界框的4个端点后,调用drawBoundingRect函数将其绘制出来。
确定边界框后,将其传给centerImg函数,该函数根据此边界框再次调用getImageData方法,但这次仅取出有图像内容的区域,然后再调用ctx2的putImageData方法,即可在新的Canvas居中整个图像。
适当修改drawImg的内容,再运行,观察居中效果。
通过这种方式,可居中任意形状、任意内容的图像。
应用devicePixelRatio
getImageData方法的参数
当应用了devicePixelRatio后,getImageData中的参数需使用:
才能取出所有的图像范围。可见,getImageData方法不像其它方法,它不会自动应用devicePixelRatio。
图像内部数据存储结构
设若当devicePixelRatio为2时,当应用了devicePixelRatio后,像素数量是原来的多少倍?2倍,答案显而易见。
我们会脱口而出。
现有一个canvas,CSS设置其宽高值为200px × 200px,在应用了devicePixelRatio后,canvas的width及height的值变为400px × 400px,则:
像素数量变成了原来的4倍。这也意味着imgData.data的数组元素总数也随之增长为原来的4倍。
下面,我们使用LiveEditor,先在一个不应用devicePixelRatio的canvas中,只绘制一个红色的点。然后取出所有点亮的像素,查看它们的情况。
只有一个像素被点亮。下面改为使用CanvasEditor,其默认应用了devicePixelRatio,使用与上面相同的绘图代码,只绘制一个红点。
从结果可以看出,对于原来的1个像素,当应用了值为2的devicePixelRatio后,将使用在视图上、上下左右相邻的4个像素来绘制它。这也是为何应用devicePixelRatio之后,图像将非常清晰的内在原因。
这同时也带来一个新的问题,即如果我们根据特定像素的索引值来查询其像素的颜色值时,索引值所在的像素可能是某个像素的复本。例如,上面索引值为1、400、401所在的像素,其实都是索引值为0所在像素的复本。
Canvas坐标与像素对应关系
现在,对于Canvas上每一个坐标,其与相应的像素数量不再是1:1的关系,而是1 : (devicePixelRatio * devicePixelRatio)的关系。
计算像素数量的公式不变,还是:
getPixelIndicesFromXY函数改为:
getXYFromPixelIndex函数改为:
注,上面这两个函数的坐标值,均是Canvas的CSS坐标值,而不是ctx绘图环境的坐标值。
应用devicePixelRatio后,因该值可能变化,且又是一对多的关系,因此上面这两个函数变为较为复杂了。
上面代码,先取出所有加亮的像素索引值,然后根据它们所对应的坐标值来分组,再列出它们的颜色值。
改变drawImg绘图函数的内容再运行,则可看出运行结果自动跟踪、并按坐标值来分类列出它们的像素索引值及其颜色值。但由于我们是跟底层像素数据在打交道,所绘制的图形不能过于复杂。绘制一个3px × 3px的矩形,则会产生9 × 4 = 36个像素被加亮,UI界面将被拉得比较长。
居中
当应用了devicePixelRatio后,在涉及到坐标值时,共有2套坐标系。第一套坐标系是Canvas的基于CSS的坐标系,宽高均为200px。getPixelIndicesFromXY, getMinX, getMaxX, getMinY, getMaxY等函数均使用这一套坐标系。
第二套坐标系是ctx的坐标系,宽高均为400px。在centerImg函数中,需将基于CSS的坐标系转换为此坐标系:
这是因为getImageData与putImageData方法均须使用第二套的坐标系。