图像处理之vImage(二)——卷积

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    浮雕
图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;
}

上述代码都做了哪些工作呢?

  1. 声明一个浮雕kernel,即int型数组。kernel的数据类型要与相应的vImage函数所需数据类型相匹配。示例中使用了vImageConvolve_ARGB8888函数,因此kernel的数据类型应该是uint_8(无符号,8-bit,整数)。kernel的数组元素从左至右,依次是第一行、第二行、第三行;
  2. 声明一个vImage_Buffer变量,用来存储原始图像信息。图像数据以数组的形式进行存储,元素图像二进制数据(inData),另外还存储高度、宽度和每行的字节数。这样vImage会知道要处理的图像大小,并如何合适地处理它。
  3. 声明一个vImage_Buffer变量,用来存储目标图像信息。
  4. 声明一个Pixel8888-格式的像素来表示目标图像的背景色(示例中用的是黑色)。
  5. 声明一个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_8ARGB8888数据类型可以使用box滤镜和tent滤镜。这些滤镜可以得到模糊效果,函数是根据他们在笛卡尔坐标系的形状命名的。调用这些函数不需要提供kernel,效果等同于需要提供kernel的一般的卷积函数。但是这些函数比一般的函数性能上要快大约1个量级。

注:由于这些函数需要一个稳健精确的算法,vImage规定这些函数不支持浮点型。浮点型的计算误差会导致图像高密度区域附近的低密度区域显得人工化或粗糙化。

box滤镜用周围像素的未加权的平均数来代替被处理的像素值,相当于通过所有值都为1的kernel来进行卷积处理。对应的函数是vImageBoxConvolve_Planar8vImageBoxConvolve_ARGB8888。每个转换后的像素都是其周围像素的平均值(周围像素的宽、高即kernel的宽、高)。

图9 box滤镜

tent滤镜用周围像素的加权平均值来代替被处理的像素值。对应的函数是vImageTentConvolve_Planar8vImageTentConvolve_ARGB8888。tent滤镜的模糊操作相当于使用值不为1的kernel进行的卷积操作。和vImageBoxConvolve_Planar8vImageBoxConvolve_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
}

这段代码都做了什么?

  1. 定义vImage要进行反卷积处理的初始卷积图像的kernel值。示例使用的是对称的kernel(宽高都为3),因此不用再定义第二个kernel;
  2. 声明一个vImage_Error结构体,来存储反卷积结果;
  3. 声明一个Pixel8888-类型的像素,用于表示转换后图像的背景色;
  4. 声明一个vImage_Buffer结构体,用于存储初始图像信息。图像数据是作为一个字节型数组存储的,包括图像数据inData、宽、高、行字节等信息。这些信息可以让vImage知道它要处理的图像有多大,从而更好的执行操作;
  5. 声明一个vImage_Buffer结构体,用于存储目标图像信息;
  6. 把上述声明的变量传给vImage函数。注意示例中在调用vImageRichardsonDeConvolve_ARGB8888函数时,第二个kernel为NULL,这是因为第一个kernel是对称的,没有必要再进行翻转。

反卷积是一个迭代过程。你可以设置迭代次数,次数不同,得到的结果会不一样,因此,为了得到最理想的结果,可以多试几次。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容