图像可以看作一个矩阵,在矩阵上面做掩模操作是一个很普遍的事情,它实际上可以看做利用一个小矩阵对一个大矩阵进行卷积运算,这个小矩阵便是掩模,也称核(kernel)。很多功能的实现都依赖于掩模操作,如图像平滑,锐化,以及腐蚀膨胀等形态学的一些操作。下面我们来看下Opencv中的例程——(TUTORIAL) mat_mask_operations。
例程中使用了一个3x3的掩模对图像进行操作,实现锐化效果。先简单介绍下使用到的掩模,如下所示。
可以看出,为得到新图像中(i,j)位置的灰度值,需要借助一个卷积窗口,窗口大小与掩模大小一致,由以(i,j)为中心的图像像素组成,则卷积运算可以看做是卷积窗口中各像素点的加权和,每个像素点的加权大小对应于掩模中相应位置的值。上图中,一次卷积需要1次乘法,4次加减法。由于不只是对当前像素点遍历,还需要借助相邻像素进行计算,所以例程中使用的图像遍历方法是C的[ ] 下标访问方式。相应的,opencv对于掩模操作有自己的函数,filter2D,下面便是性能对比。
源代码中每种方法只允许了一遍,为了测试的准确性,我在程序中加了一个循环,这个结果是100次运算的平均时间。可以看出使用opencv的filter2D函数比自己手工打造的掩模操作性能上提高了39%
下面我们看下手工打造的掩模操作是如何实现的
void Sharpen(const Mat& myImage,Mat& Result)
{
CV_Assert(myImage.depth() == CV_8U); // accept only uchar images // CV_Assert 是cv定义的断言格式,如果条件不符合,会中断程序,抛出异常
const int nChannels = myImage.channels();
Result.create(myImage.size(),myImage.type());
for(int j = 1 ; j < myImage.rows-1; ++j) // 第一行和最后一行无法计算
{
const uchar* previous = myImage.ptr<uchar>(j - 1); // 上一行数据的指针
const uchar* current = myImage.ptr<uchar>(j ); // 当前行数据的指针
const uchar* next = myImage.ptr<uchar>(j + 1); // 下一行数据的指针
uchar* output = Result.ptr<uchar>(j); // 输出图像当前列数据的指针
for(int i= nChannels;i < nChannels*(myImage.cols-1); ++i) // 同理,忽略第一列和最后一列像素
{
*output++ = saturate_cast<uchar>(5*current[i]
-current[i-nChannels] - current[i+nChannels] - previous[i] - next[i]); // 计算新值
}
}
// 将边界设为0
Result.row(0).setTo(Scalar(0));
Result.row(Result.rows-1).setTo(Scalar(0));
Result.col(0).setTo(Scalar(0));
Result.col(Result.cols-1).setTo(Scalar(0));
}
可以看出,例程中利用了3个指针,分别用于定位在3行中的像素,实现对相邻像素的操作。输入图像(my