opencv 图像上的filter操作

96 阅读4分钟

矩阵上的filter操作非常简单。就是根据filter矩阵(也称内核)重新计算图像中每个像素的值。此filter包含的值将调整相邻像素(和当前像素)对新像素值的影响程度。从数学的角度来看,就是对指定的值进行加权平均。

1. 测试用例

考虑一个增强图像对比度方法。对图像的每个像素应用以下公式:

image.png

第一种表示是使用公式,而第二种表示法是第一种使用filter的压缩版本。可以通过将filter矩阵的中心(M0,0M_{0,0})放在要计算的像素上并将对应像素相乘然后相加。虽然第一种和第二种表示完成一样的功能,但是在大型矩阵的情况下,后一种表示法更容易查看。

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 毫秒。相当大的差异。

例如:

resultMatMaskFilter2D.png

原文地址