矩阵上的filter操作非常简单。就是根据filter矩阵(也称内核)重新计算图像中每个像素的值。此filter包含的值将调整相邻像素(和当前像素)对新像素值的影响程度。从数学的角度来看,就是对指定的值进行加权平均。
1. 测试用例
考虑一个增强图像对比度方法。对图像的每个像素应用以下公式:
第一种表示是使用公式,而第二种表示法是第一种使用filter的压缩版本。可以通过将filter矩阵的中心()放在要计算的像素上并将对应像素相乘然后相加。虽然第一种和第二种表示完成一样的功能,但是在大型矩阵的情况下,后一种表示法更容易查看。
2. 代码
您可以从此处下载此源代码或查看 OpenCV 源代码库示例目录samples/cpp/tutorial_code/core/mat_mask_operations/mat_mask_operations.cpp。
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>
using namespace std;
using namespace cv;
static void help(char* progName)
{
cout << endl
<< "This program shows how to filter images with mask: the write it yourself and the"
<< "filter2d way. " << endl
<< "Usage:" << endl
<< progName << " [image_path -- default lena.jpg] [G -- grayscale] " << endl << endl;
}
// 锐化
void Sharpen(const Mat& myImage,Mat& Result);
// 主函数
int main( int argc, char* argv[])
{
help(argv[0]);
// 获取文件名称
const char* filename = argc >=2 ? argv[1] : "lena.jpg";
Mat src, dst0, dst1;
if (argc >= 3 && !strcmp("G", argv[2]))
src = imread( samples::findFile( filename ), IMREAD_GRAYSCALE);
else
src = imread( samples::findFile( filename ), IMREAD_COLOR);
if (src.empty())
{
cerr << "Can't open image [" << filename << "]" << endl;
return EXIT_FAILURE;
}
namedWindow("Input", WINDOW_AUTOSIZE);
namedWindow("Output", WINDOW_AUTOSIZE);
imshow( "Input", src );
double t = (double)getTickCount();
Sharpen( src, dst0 );
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Hand written function time passed in seconds: " << t << endl;
imshow( "Output", dst0 );
waitKey();
// 增强图像对比度fiter的核
Mat kernel = (Mat_<char>(3,3) << 0, -1, 0,
-1, 5, -1,
0, -1, 0);
t = (double)getTickCount();
// 应用filter
filter2D( src, dst1, src.depth(), kernel );
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Built-in filter2D time passed in seconds: " << t << endl;
imshow( "Output", dst1 );
waitKey();
return EXIT_SUCCESS;
}
// 锐化
void Sharpen(const Mat& myImage,Mat& Result)
{
CV_Assert(myImage.depth() == CV_8U); // accept only uchar images
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]);
}
}
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. 基本方法即公式法
现在看看如何通过使用基本像素访问方法或使用**filter2D()** 函数来实现这一点。
这是一个可以执行此操作的函数:
void Sharpen(const Mat& myImage,Mat& Result)
{
CV_Assert(myImage.depth() == CV_8U); // accept only uchar images
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]);
}
}
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));
}
首先,确保输入图像数据是无符号字符格式。为此,使用cv::CV_Assert函数,该函数在其中的表达式为假时抛出错误。
CV_Assert(myImage.depth() == CV_8U); // accept only uchar images
创建一个与输入具有相同大小和相同类型的输出图像。正如您在存储部分中看到的,根据通道的数量,可能有一个或多个子列。
通过指针遍历它们,因此元素的总数取决于这个数字。
const int nChannels = myImage.channels();
Result.create(myImage.size(),myImage.type());
使用普通的 C [] 运算符来访问像素。因为需要同时访问多行,所以获取每一行的指针(前一行、当前行和下一行)。需要另一个指向将保存计算的位置的指针。然后只需使用 [] 运算符访问正确的项目。为了将输出指针向前移动,需在每次操作后增加它(一个字节):
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]);
}
}
在图像的边界上,上面的符号导致不存在的像素位置(如(-1,-1))。在这些点上,公式是未定义的。一个简单的解决方案是不在这些点中应用内核,例如,将边界上的像素设置为零:
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));
4. filter2D 函数
应用此类过滤器在图像处理中非常常见,以至于在 OpenCV 中有一个函数可以处理应用filter(在某些地方也称为内核)。为此,首先需要定义一个包含filter的对象:
Mat kernel = (Mat_<char>(3,3) << 0, -1, 0,
-1, 5, -1,
0, -1, 0);
然后调用**filter2D()** 函数指定输入、输出图像和要使用的内核:
filter2D( src, dst1, src.depth(), kernel );
该函数甚至有第五个可选参数来指定内核的中心,第六个用于在将过滤像素存储到 K 之前添加一个可选值,第七个用于确定在未定义操作的区域中做什么(边界)。
filter2D()函数更短,更简洁,并且因为有一些优化,它通常比手动编码的方法更快。例如,在测试中,第二个只用了 13 毫秒,而第一个用了 31 毫秒。相当大的差异。
例如: