vImage学习笔记——卷积(Convolution)
卷积(Convolution)是一个常用的图像处理技术,可以改变像素强度,从而影响周围其他像素的强度。卷积的常用技术是创建滤镜,使用卷积技术,你可以获取一些流行的图像效果,比如模糊(blur)、锐化(sharpen)及边缘检测(edge detection),这些效果在Photo Booth、iPhoto和Aperture都有广泛使用。
如果你对图像滤镜和实时处理有兴趣的话,你会发现vImage函数集的好处。用图像滤镜,卷积操作可以完成一些常用的滤镜效果,比如浮雕、模糊及色调分离。
vImage卷积技术对锐化或增强图像质量也很有用。当处理一些科学图像时,增强图像质量很有用。此外,由于科学图像通常都很大,就很有必要使用这些vImage技术来达到合适的性能需求。这种情况下你需要用到的技术有边缘检测(edge detection)、锐化、描绘外观轮廓(surface contour outlining)、平滑、及动作检测(motion detection)。
本章节讲述了卷积技术,以及如何使用vImage提供的卷积函数。通过本文,你可以:
了解卷积技术可以实现哪些效果;
学习什么是卷积核以及如何构建卷积核;
通过代码示例,学习如何对一个图像使用卷积技术。
卷积核(Convolution Kernels)
图1展示了一个图像通过vImage卷积函数添加了浮雕效果前后的对比图。为了达到这个效果,vImage使用一个类网格的数学概念,称为核(kernel),来完成卷积操作。
图1 浮雕
图1 浮雕
图2是一个3×3的kernel。kernel的高度和宽度不必一样,但必须是奇数。kernel内部的数值会影响卷积的整体效果。这些数值决定了初始图像像素会如何转换成目标图像像素,这看起来可能不是很直观,9个数字会如何影响到滤镜效果呢?卷积技术经过一系列的操作,根据周围像素的强度改变当前像素的强度。vImage根据kernel执行卷积操作,这种通过kernel执行卷积计算的过程就称为kernel convolution(核卷积)。
图2 3×3卷积
卷积是像素单位的操作,即对每个像素都要执行同样的算法。因此,大图像比小图像需要更多的卷积操作。一个kernel可以被看做一个二维的网格数据,而图像也可以被看做一个二维网格数据(如图3),对一个图像应用kernel可以想象成把一个小格子(kernel)平铺在大格子(图像)上。
图3 图像是二维网格数据
kernel内部的数值会作为与它下面的数值相乘的乘数,下面的数值指的是被kernel数值覆盖的像素的强度。在进行卷积计算时,把kernel的中心值覆盖在待转换的像素上,然后将kernel的每个值与其正下方的像素值相乘,最后将所有的结果相加,相加后的结果就是新的像素强度。图4展示了kernel是如何转化像素的。
图4 核卷积
虽然kernel会覆盖到一些其它的像素,但是最终只有kernel中心值正下方的原始像素会发生变化。kernel和图像之间所有乘积相加后的和被称为加权和。为了确保处理后的图像对比原图不会过于饱和,vImage有一个常用的方法,就是设置一个除数因子,把加权和进行拆分。因为用周围像素的加权和来代替原始像素时常导致像素强度过大(并且图像整体也过于明亮),拆分加权和可以按比例降低滤镜效果的强度,并确保维持原始亮度,这个过程称为标准化。这个行为是可选的,被拆分后的加权和会代替原始像素值。kernel对每个像素重复这个过程。
注:如果要执行标准化,你必须向卷积函数提供你要使用的因子。因子最好是2的幂次方。你也可以在图像像素值为整数的时候再提供因子。浮点型不需要使用,因为你可以直接依比例决定kernel的浮点型数值来达到标准化。
kernel的数据类型和图像的数据类型必须保持一致,比如,如果图像像素数据类型是浮点型,那么kernel中的数据类型也必须是浮点型。
记住以上所述的算法vImage都已经帮你做好了,你不需要牢记卷积算法的步骤。当然,你也可以在自定义的核中实现该算法。
反卷积
反卷积指的是解除先前的卷积效果——一般是原始图像中物理携带的卷积效果,比如镜头中的衍射效果。通常,反卷积是一个锐化操作。
反卷积的算法比较多,vImage用的是Richardson-Lucy deconvolution算法。
Richardson-Lucy deconvolution算法的目标是根据卷积后的像素值找到原始的像素值,以及kernel数据。
基于以上需求,在使用反卷积函数时必须提供卷积后的图像及卷积使用的kernel值。
vImage会自行处理反卷积的每一步操作,因此不需要牢记这些步骤。使用反卷积的时候,必须提供初始的卷积kernel(如果该kernel不对称的话,还要额外提供一个对角线翻转的kernel2)。
使用卷积核
现在你最好了解一下核的结构以及卷积的处理过程,是时候使用几个vImage函数来看看了。本章节展示了如何实现图1中的浮雕效果,并解释了无偏差卷积和带偏差卷积之间的差别。
卷积
vImage可以自动进行卷积计算,而你的工作是提供kernel,即描述卷积应该生成什么效果。表1展示了如何使用卷积去生成浮雕效果。你也可以通过合适的kernel,利用同样的代码来生成一个不同的效果,比如锐化。
表1 生成浮雕效果
int myEmboss(void *inData,
unsigned int inRowBytes,
void *outData,
unsigned int outRowBytes,
unsigned int height,
unsigned int width,
void *kernel,
unsigned int kernel_height,
unsigned int kernel_width,
int divisor ,
vImage_Flags flags )
{
uint_8 kernel = {-2, -2, 0, -2, 6, 0, 0, 0, 0}; // 1
vImage_Buffer src = { inData, height, width, inRowBytes }; // 2
vImage_Buffer dest = { outData, height, width, outRowBytes }; // 3
unsigned char bgColor[4] = { 0, 0, 0, 0 }; // 4
vImage_Error err; // 5
err = vImageConvolve_ARGB8888( &src, //const vImage_Buffer *src
&dest, //const vImage_Buffer *dest,
NULL,
0, //unsigned int srcOffsetToROI_X,
0, //unsigned int srcOffsetToROI_Y,
kernel, //const signed int *kernel,
kernel_height, //unsigned int
kernel_width, //unsigned int
divisor, //int
bgColor,
flags | kvImageBackgroundColorFill
//vImage_Flags flags
);
return err;
}
上述代码都做了哪些工作呢?
声明一个浮雕kernel,即int型数组。kernel的数据类型要与相应的vImage函数所需数据类型相匹配。示例中使用了vImageConvolve_ARGB8888函数,因此kernel的数据类型应该是uint_8(无符号,8-bit,整数)。kernel的数组元素从左至右,依次是第一行、第二行、第三行;
声明一个vImage_Buffer变量,用来存储原始图像信息。图像数据以数组的形式进行存储,元素图像二进制数据(inData),另外还存储高度、宽度和每行的字节数。这样vImage会知道要处理的图像大小,并如何合适地处理它。
声明一个vImage_Buffer变量,用来存储目标图像信息。
声明一个Pixel8888-格式的像素来表示目标图像的背景色(示例中用的是黑色)。
声明一个vImage_Err变量来存储卷积函数的返回值。
然后,将这些声明的变量值传入vImageConvolve_ARGB8888函数中,由vImage来处理后续的计算,并把结果存储到dest变量中。vImageConvolve_ARGB8888函数是vImage中仅有的几个卷积函数之一。通常,vImage会为每种图像格式提供4种函数变体,ARGB8888前缀表示该函数处理的是交叉型图像(full-color),每个像素由四个8字节的整数构成一组,代表alpha(A),red(R),green(G)和blue(B)四个通道。想了解vImage支持的图像格式的更多细节,请看vImage概述
示例myEmboss中还使用了vImage_Flags参数。该参数由1个或多个flags(用或逻辑运算符 | 连接)组成。kvImageBackgroundColorFill表示vImage要使用预先提供的背景色。
为了熟悉kernel效果的使用方法,请在你自己的代码中使用以下两个kernel。图6的kernel可以生成高斯模糊效果,图7的kernel可以生成边缘检测效果。
图6 高斯模糊
图7 边缘检测
带偏差的卷积
在执行卷积操作时,可以选择是否带偏差。偏差是指在卷积结果上再额外添加一个来自周围像素的影响。由于某些卷积计算得到的结果可能为负值,偏差可以避免信号溢出。可以把偏差设为127或128,来允许负值也被描绘出来。偏差可能使整体图像效果变亮或变暗。
每个标准的vImage函数(比如vImageConvolve_PlanarF)都有一个对应的带偏差的函数(vImageConvolveWithBias_PlanarF)。偏差函数的使用方法和无偏差函数一样,除了必须设置bias参数来使用偏差。bias的数据类型必须与图像像素数据类型一致。
图8 带偏差与无偏差
使用高速滤镜
vImage提供了一些特定的卷积函数,这些比一般的卷积函数具有更快的处理速度。OS X v10.4以上的系统,对于Planar_8和ARGB8888数据类型可以使用box滤镜和tent滤镜。这些滤镜可以得到模糊效果,函数是根据他们在笛卡尔坐标系的形状命名的。调用这些函数不需要提供kernel,效果等同于需要提供kernel的一般的卷积函数。但是这些函数比一般的函数性能上要快大约1个量级。
注:由于这些函数需要一个稳健精确的算法,vImage规定这些函数不支持浮点型。浮点型的计算误差会导致图像高密度区域附近的低密度区域显得人工化或粗糙化。
box滤镜用周围像素的未加权的平均数来代替被处理的像素值,相当于通过所有值都为1的kernel来进行卷积处理。对应的函数是vImageBoxConvolve_Planar8和vImageBoxConvolve_ARGB8888。每个转换后的像素都是其周围像素的平均值(周围像素的宽、高即kernel的宽、高)。
图9 box滤镜
tent滤镜用周围像素的加权平均值来代替被处理的像素值。对应的函数是vImageTentConvolve_Planar8和vImageTentConvolve_ARGB8888。tent滤镜的模糊操作相当于使用值不为1的kernel进行的卷积操作。和vImageBoxConvolve_Planar8和vImageBoxConvolve_ARGB8888一样,不需要向函数提供kernel值,只需要宽高即可。
图10 tent滤镜
假设kernel的大小是3×5。那么第一个矩阵是
第二个矩阵是
那么生成的kernel是
3×5的tent滤镜操作相当于使用上图中的滤镜来进行卷积操作。
使用多核
vImage允许在单个卷积操作中使用多个kernel。可以使用vImageConvolveMultiKernel函数,分别定义四个kernel,每个kernel对应一个图像通道。一个kernel控制一个通道的话,你就可以对图像进行更高级别的处理。例如,你可以利用多核卷积对图像的颜色通道分别重新采样,抵消屏幕上的RGB荧光效果。由于四个kernel可以分别对单个颜色通道进行处理,vImageConvolveMultiKernel函数只能应用于交叉型图像。
这些函数的使用方法与单核卷积函数使用方法相同,唯一不同的是,需要提供一个指针数组,数组中每个元素指向一个kernel地址。
反卷积
与卷积计算相同的是,vImage同样在内部封装了反卷积的计算过程,你只需要提供一个kernel即可。表2展示了如何把浮雕效果通过反卷积消除的过程。你可以用同样的代码、合适的kernel去反卷积各种效果(比如模糊效果)。但是不同于卷积操作的是,反卷积函数还需要另一个kernel参数。除非kernel的宽和高相同,否则这个参数不能为NULL。如果kernel的宽和高不相等,必须再提供一个行列反转的kernel。
表2是一段示例代码,描述了如何使用vImage对一个ARGB8888-格式的图像进行反卷积。
int myDeconvolve(void *inData,
unsigned int inRowBytes,
void *outData,
unsigned int outRowBytes,
unsigned int height,
unsigned int width,
void *kernel,
unsigned int kernel_height,
unsigned int kernel_width,
int divisor,
int iterationCount,
vImage_Flags flags )
{
//Prepare data structures
uint_8 kernel = {-2, -2, 0, -2, 6, 0, 0, 0, 0}; // 1
vImage_Error err; // 2
unsigned char bgColor[4] = { 0, 0, 0, 0 }; // 3
vImage_Buffer src = { inData, height, width, inRowBytes }; // 4
vImage_Buffer dest = { outData, height, width, outRowBytes }; // 5
//Send data to vImage for processing
err = vImageRichardsonLucyDeConvolve_ARGB8888( &src, // 6
&dest, //const vImage_Buffer *dest,
NULL,
0, //unsigned int srcOffsetToROI_X,
0, //unsigned int srcOffsetToROI_Y,
kernel, //const signed int *kernel,
NULL, //assumes symmetric kernel
kernel_height, //unsigned int kernel_height,
kernel_width, //unsigned int kernel_width,
0, //height of second kernel
0, //width of second kernel
divisor, //int
0, //for second kernel
bgColor,
iterationCount, //uint32_t
kvImageBackgroundColorFill | flags
//vImage_Flags
);
//Report result
return err; // 7
}
这段代码都做了什么?
定义vImage要进行反卷积处理的初始卷积图像的kernel值。示例使用的是对称的kernel(宽高都为3),因此不用再定义第二个kernel;
声明一个vImage_Error结构体,来存储反卷积结果;
声明一个Pixel8888-类型的像素,用于表示转换后图像的背景色;
声明一个vImage_Buffer结构体,用于存储初始图像信息。图像数据是作为一个字节型数组存储的,包括图像数据inData、宽、高、行字节等信息。这些信息可以让vImage知道它要处理的图像有多大,从而更好的执行操作;
声明一个vImage_Buffer结构体,用于存储目标图像信息;
把上述声明的变量传给vImage函数。注意示例中在调用vImageRichardsonDeConvolve_ARGB8888函数时,第二个kernel为NULL,这是因为第一个kernel是对称的,没有必要再进行翻转。
反卷积是一个迭代过程。你可以设置迭代次数,次数不同,得到的结果会不一样,因此,为了得到最理想的结果,可以多试几次。