4.4 图像卷积
Last updated
Last updated
卷积在图像处理中经常用到,其会根据每个像素周围的像素点修改当前像素点的值。卷积核就是用来描述每个像素点如何被附近的像素点所影响。例如,模糊滤波中,使用平均的权重方式来进行计算,这样差异比较大的像素点会减少差异。对于相同的图像,如果想要做不同的操作,我们只需要变化滤波器即可,这样就能做锐化、模糊、边缘增强和图像压花。
卷积算法会遍历原始图像中的每一个像素点。对于每个原始像素点,滤波器中心点会为于像素点的上方,然后将中心点以及周围点的像素点与滤波器中对应的权重值相乘。然后将乘积的结果值相加后,产生出新的值作为输出。图4.3中就展示了该算法的具体过程。
图4.3 卷积滤波如何对原图像进行处理。
图4.4a是原始图,图4.4b中展示了原图经过模糊滤波后的结果,图4.4c展示了经过一个压花滤波器处理后的结果。
图4.4 不同卷积核对同一张图像进行处理:a)为原图;b)为模糊处理;c)为压花处理。
程序清单4.6中使用C/C++
实现了一个串行的卷积操作。两层外部循环以遍历原始图像中所有的像素点。每一次滤波操作,每个原始点和其附近的点都要参与计算。需要注意的是,滤波器有可能访问到原始图像之外的区域。为了解决这个问题,我们在最内层循环中添加了四个显式的检查,当滤波器对应的坐标点位于原始图像之外,我们将使用与其最近的原始图像的边界值。
程序清单4.6 图像卷积的串行版本
OpenCL中,使用图像内存对象处理卷积操作要比使用数组内存对象更有优势。图像采样方式会自动对访问到图像之外的区域进行处理(类似于上节图像转换中所提到的),并且访问缓存中二维数据在硬件上也会有所优化(将会在第7章讨论)。
OpenCL上实现卷积操作几乎没有什么难度,并且写法类似于卷积操作的C版本。OpenCL版本中,我们为每一个输出的像素点创建了一个工作项,使用并行的方式将最外层两个循环去掉。那么每个工作项的任务就是完成最里面的两个循环,这两个循环完成的就是滤波操作。前面的例子中,读取源图像数据,需要配置一个OpenCL结构体,来指定数据的类型。本节的例子中,将继续使用read_imagef()
。完整的内核代码将在代码清单4.7中展示。
kerenl void convolution( read_only image2d_t inputImage, write_only image2d_t outputImage, int rows, int cols, consetant float filter, int filterWidth, sampler_t sampler) { / Store each work-item's unique row and column */ int column = get_global_id(0); int row = get_global_id(1);
/* Half the width of the filter is needed for indexing
memory later */ int halfWidth = (int)(filterWidth / 2);
/* All accesses to images return data as four-element vectors
(i.e., float4), although only the x component will contain
meaningful data in this code */ float4 sum = {0.0f,0.0f,0.0f,0.0f};
/ Iterator for the filter / int filterIdx = 0;
/* Each work-item iterates around its local area on the basis of the
size of the filter*/ int2 coords; // Coordinates for accessing the image
/ Iterate the filter rows/ for (int i = -halfWidth; i <= halfWidth; i++) { coords.y = row + i; / Iterate over the filter columns / for (int j = -halfWidth; j <= halfWidth; j++) { coords.x - column + 1;
/*Read a pixel from the image. A single-channel image
stores the pixel in the x coordinate of the reatured
vector. */
pixel = read_imagef(inputImage, sampler, coords);
sum.x += pixel.x * filter[filterIdx++];
} }
/ Copy the data to the output image / coords.x = column; coords.y = row; write_imagef(outputImage, coords, sum); }
程序清单4.7 使用OpenCL C实现的图像卷积
访问图像总会返回具有四个值的向量(每个通道一个值)。前节例子中,我们加了.x
来表示只需要返回值的第一个元素。本节例子中,我们会申明pixel(图像访问之后返回的值)和sum(存储结果数据,以拷贝到输出图像),其类型都为float4。当然,我们仅对x元素进行累加滤波像素值的计算(第45行)。
卷积核很适合放置到常量内存上,因为所有工作项所要用到的卷积核都是相同的。简单的添加关键字"__costant"在函数参数列表中(第7行),用于表示卷积核存放在常量内存中。
之前的例子中,我们是直接在内核内部创建了一个采样器。本节例子中,我们将使用主机端API创建一个采样器,并将其作为内核的一个参数传入。同样,本节例子中我们将使用C++ API(C++采样器构造函数需要相同的参数)。
主机端创建采样器的API如下所示:
其C++ API如下所示:
使用C++创建采样器的方式如下所示:
图像旋转例子中,采样器使用非标准化坐标。这里我们将展示不同于内部使用时的另外两个采样器参数:滤波模式将使用最近的像素点的值,而非进行差值后的值(CL_FILTER_NEAREST),并且寻址模式将在访问到图像区域之外时,将其值置为最接近的图像边界值(CL_ADDRESS_CLAMP_TO_EDGE)。(注:这里要注意一下CL和CLK的区别。CL开头的是使用在主机端API上,CLK则直接使用在OpenCL内核代码中。)
使用C++ API,创建二维图像使用image2D类进行创建,创建这个类需要一个ImageFormat对象作为参数。C API则不需要图像描述器的传入。
Image2D和ImageFormat的构造函数如下:
这样我们就能创建卷积所需要的输入和输出图像,其调用方式如下所示:
使用C++ API实现图像卷积的主机端代码在代码清单4.8中展示。主程序中,使用了一个5x5的高斯模糊滤波核用于卷积处理。
程序清单4.8 图像卷积主机端完整代码