开始之前

再说上一篇文章中, 我们想按照噪声产生, 然后将降噪的, 但是限于篇幅, 我就放在这一篇里面了,
说起图像的噪声问题就又回到了我们上一章的内容, 把噪声当作信号处理, 实际上数字图像处理实际上也是在进行数字信号的处理过程, 我们这一章就是将滤除信号的过程,

根据上一章的方式, 我们对图像添加噪声, 然后计算 PSNR 与 SSIM 参数, 然后通过降噪, 再从新计算参数值, 比较我们算法的效果 对比我们的算法效果, 看正文吧

目录

正文

我们在上一章给出了两种噪声的添加方式, 可以根据我们的需求添加椒盐噪声和高斯噪声, 但是由于我们的噪声添加使用了随机数 , 导致我们在每次进行的结果可能不一致, 所以我们提前设计好噪声情况, 将图片存储起来, 后面我们进行滤波的时候, 都使用一样的照片, 这样我们能够保证每次的结果是一致的, 这样就能进行算法的对比了,

生成噪声图像

我们在上一章给出了不同噪声情况下图像结果, 也给出了一个链接, 对比了更多情况下的图像噪声情况, 可以参考, 所以我们考虑五种情况吧 分别是低椒盐噪声, 高椒盐噪声,低高斯噪声, 高高斯噪声,低椒盐混合低高斯噪声, 高椒盐混合高高斯噪声. 我们分别将图片进行存储便能够得到结果

说明一下: 之前的算法使用的 lena 图像 忘记从那搞来的了, 今天对比了一下, 发现图像不太对, 我现在找到opencv的一个标准图像Lena.jpg, 我下载下来了, 转换成了 lena.png 的图像, 可以见lena.png 这幅图, 可以直接访问链接 https://gitee.com/schen00/BlogImage/raw/master/小书匠/1588298950276.png 直接下载即可..

这里的处理算法比较简单, 我们来看代码

void MainWindow::testFunc1(void)
{
    // 用于生成 测试图像 一共6幅图像
    std::vector<cv::Mat> noise_img(6);
    // 初始化为原始图像
    for(auto &m: noise_img)
        m = gSrcImg.clone();

    // 分别添加 低, 高, 低混合, 高混合 共6幅图像
    addSaltNoise(noise_img[0],1000);
    addSaltNoise(noise_img[1],10000);

    addGaussianNoise(noise_img[2],0,1);
    addGaussianNoise(noise_img[3],100,10);

    addSaltNoise(noise_img[4],1000);
    addGaussianNoise(noise_img[4],0,1);

    addSaltNoise(noise_img[5],10000);
    addGaussianNoise(noise_img[5],100,10);


    // 计算 6幅图像的  psnr 和 ssim 然后存储结果值
    std::vector<double> psnr(6);
    std::vector<cv::Scalar> mssim(6);

    QString res_temp = "image-%1: psnr:%2, mssim: B:%3 G:%4 R:%5 ";
    QString res_str;

    // 计算每个图像的 参数值, 然后存储起来
    for(int i=0;i<6;i++)
    {
        psnr[i] = getPSNR(gSrcImg, noise_img[i]);
        mssim[i] = getMSSIM(gSrcImg, noise_img[i]);
        res_str = res_temp.arg(i+1)
                            .arg(psnr[i])
                            .arg(mssim[i].val[0])
                            .arg(mssim[i].val[1])
                            .arg(mssim[i].val[2]);

        ui->pt_log->appendPlainText(res_str);
        cv::imwrite("../testimages/noise/lena-" + std::to_string(i+1) + ".png", noise_img[i]);
    }
}

我们将图片输出, 然后输出了每幅图的参数, 同时将结果图存储下来, 由于我们在实际进行图像处理的时候会有很多

image-1: psnr:29.4922, mssim: B:0.880587 G:0.888243 R:0.944992
image-2: psnr:19.4727, mssim: B:0.353134 G:0.383638 R:0.629353
image-3: psnr:46.8705, mssim: B:0.991138 G:0.991732 R:0.991185
image-4: psnr:9.15966, mssim: B:0.492354 G:0.482311 R:0.680167
image-5: psnr:29.2807, mssim: B:0.874794 G:0.881488 R:0.935624
image-6: psnr:8.92587, mssim: B:0.392531 G:0.393254 R:0.655795

我这里使用之前提到的 图像"拼接" 的方式将图像拼接起来, 这样我们可以更为直观的比较, 图像尺寸都是 \(512*512\), 如果需要可以裁剪出来,

opencv-10-图像滤波-噪声添加与均值滤波-含opencv C++ 代码实现-LMLPHP

传统图像降噪算法及其对比

之前提到过的【技术综述】一文道尽传统图像降噪方法
这篇文章讲的还比较详细, 大概的给我们讲了一下传统的降噪的方法,
这里我想将各种滤波分开进行实现, 但是比较麻烦, 我就直接在一篇文章中写了吧..

目前常用的降噪的方法主要可以分为空域降噪与频域降噪, 空域滤波也是我们常用的使用空间处理的方式,计算量小, 简单易用. 频域比较难理解,计算量也比较大, 但是在很多情况结果比较有效..

所以我们主要的部分也是空间域处理的方式, 也比较直观. opencv 的例程中Smoothing Images 章节大概讲了一下目前使用的模糊方式, 其实模糊是相对的, 也是进行降噪的一个有利手段, 在处理掉噪声的同时, 会导致原始图像的细节模糊, 进而丢失一部分图像信息,我们之后看下图像测试结果. 同时在例程中还提到了一本书Computer Vision: Algorithms and Applications, 1st ed. 有中译版本, 内容还不错, 可以学习

opencv 核表示的算法操作

在之前的内容中, 我们介绍了 opencv 核操作的方式, 对于图像的每个像素点的领域操作都可以使用 opencv 提供的 filter2D 方式进行指定核的运算, 我们能够很容易核的操作, 也就是说我们将图像的算法操作都可以转换成图像的矩阵相乘的运算, 可以表示成

\[g(x,y) = M \cdot f(x,y)\]

\(g(x,y)\) 用来表示结果图像, \(f(x,y)\) 表示原始图像, (x,y) 表示 列行座标, M 就是我们的图像运算矩阵,
我们后续都不再重复这些默认的操作, 希望能够明白

一般来说, 我们进行矩阵运算的时候都会选择方阵, 这样不会由于矩阵的方向性导致的处理结果不同, 所以我们在一般情况下都会选择
方阵, 比如上面进行的滤波 采用的就是 \(3x3\) 尺寸的图像, 而且由于我们的图像都是离散的, 所以 实际山采用的滤波的窗口边长也是奇数值, 类似于 \(3,5,7,9...(2k+1)\) 的形式

均值滤波及C++ 代码实现

算术均值滤波

均值滤波(Mean Filter)的算法就是对于每一个像素点, 将其设定为取其邻域窗口内的所有像素的平均值
我们考虑一般形式的均值滤波器

\[g(x,y) = \frac{1}{mn} \sum_{(i,j) \in S_{xy}} f(i,j)\]

那我们开始转换一下, 则可以得到下相应的 均值滤波的矩阵

\[M = \frac{1}{9} \left [ \begin{array}{c} 1 & 1 & 1 \\ 1& 1 & 1 \\ 1 & 1 & 1 \end{array}\right ]\]

加权均值滤波

上面给出的均值滤波让人容易的就会想一个问题, 对于不同的像素位置, 应该要赋予不同的权重值, 靠近中间的位置我们必须要考虑权重的问题, 这就是我们使用加权的均值滤波了, 一般来说我们最常用的矩阵为

\[M = \frac{1}{16} \left [ \begin{array}{c} 1 & 2 & 1 \\ 2& 4 & 2 \\ 1 & 2 & 1 \end{array}\right ]\]

这种矩阵对于中心元素的权重更高, 边缘的较弱, 符合人的感觉, 具体的参数值可以自己调整, 前面的系数为矩阵内各个元素的总和, 是为了保证系数的归一.

其实均值滤波器还有很多, 有兴趣的推荐看图像处理基础(3):均值滤波器及其变种 这篇文章, 写的很好,介绍的很详细,

C++手动实现均值滤波

我们这里还是使用基础的 算术均值滤波, 实现起来简单一点, 边界问题也不考虑, 这样的话,我们处理的图像区域就稍微内缩小一个像素(1,1)- (m-2,n-2), 至于边界问题, 处理起来还是要看
看起来还是比较简单的, 我们按照给出的方法写一下

// 默认 尺寸为3的  均值滤波 // 自定义实现 暂时不考虑参数异常等 处理
cv::Mat meanFilter(const cv::Mat src, int ksize = 3)
{
    // 边界不处理, 直接忽略掉 使用原始图, 拷贝, 避免直接修改
    cv::Mat dst = src.clone();

    // 直接出, 强制向下取整, // 暴力计算每一个 邻域区间的值
    int k0 = ksize/2;
    int sum[3] = {0,0,0};
    for(int i=k0;i<dst.rows-k0-1;i++)
    {
        for(int j=k0;j<dst.cols-k0-1;j++)
        {
            // 清空 和数组
            memset(sum,0, sizeof(sum));

            // 计算三个通道的结果 和值 并计算 均值写入目标图像
            for(int c = 0;c<3;c++)
            {
                for(int m = 0;m<ksize;m++)
                {
                    for (int n=0;n<ksize;n++)
                    {
                        sum[c] += src.at<cv::Vec3b>(i-k0+m,j-k0+n)[c];
                    }
                }
                // 计算均值写入
                dst.at<cv::Vec3b>(i,j)[c] = cv::saturate_cast<uchar>((float)sum[c] /(ksize*ksize));
            }
        }
    }
    return dst;
}

中间部分写的比较暴力,直接计算的窗口的和值, 然后进行均值得到的结果, 其实这里如果要考虑窗口的和值, 我们没必要重复计算一次, 每次计我们移动窗口后变化的两个边界差值即可, 这样计算上的一点点速度优化, 我们这里实现的只是一个 小小的demo , 有一定的效果即可

opencv 实现均值滤波

我们在之前的章节提到了 使用 filter2D 代替普通操作的方法,在这里自然而然的想到怎么去实现, 我们还是一样的构造一个核, 然后计算结果即可, 这里使用的核还是 上面提到的 算术均值滤波的核

// filter2D 实现 meanfilter
cv::Mat meanFilterByFilter2D(const cv::Mat src, int ksize = 3)
{
    cv::Mat kernel = (cv::Mat_<float>(ksize,ksize) << 1,1,1,1,1,1,1,1,1);
    kernel = kernel / 9.0f;
    cv::Mat dst;
    cv::filter2D(src,dst,src.depth(),kernel);
    return dst;
}

这里实现起来真的很简单, 这里的 Mat 可以直接进行矩阵的操作, 每个元素都除以了9,这样就简单很多了,

接下来呢, opencv 对于这种基础且常见的算法肯定自己去在做了实现呀, 在我们上面也提到了opencv 的例程Smoothing Images, 提到了 一个模糊的函数, cv::blur, 这个函数可以调用盒式滤波器, 其实也就是均值滤波的通用形式, 前面的系数不一定而已, 我们先实现一下看下效果, 这里跟上面写成一样的形式, 看起来好看一点, 其实只需要一句话便可以实现了 没什么难度, 至于效果, 我们马上来对比

// 使用 blur 均值滤波
cv::Mat meanFilterByBlur(const cv::Mat src, int ksize = 3)
{
    cv::Mat dst;
    cv::blur(src,dst,cv::Size(ksize,ksize));
    return dst;
}

均值滤波算法对比

我们上面提到了构造噪声图像, 然后我们存储了起来, 这里我们选择了一副图像进行直接给结果, 这里我们选择 高椒盐噪声的图像进行测试, 然后先看结果, 第一行表示噪声图像与原始图像的参数值, 后面的三行依次是我们进行上面提到的三种实现出来的滤波方式得到的图像与原始图像进行的对比分析, 这里还是能看到比较明显的结果的,

image-noise: psnr:19.4727, mssim: B:0.353134 G:0.383638 R:0.629353
image-1: psnr:26.505, mssim: B:0.603292 G:0.63888 R:0.806963
image-2: psnr:26.7208, mssim: B:0.605704 G:0.641344 R:0.809115
image-3: psnr:26.7208, mssim: B:0.605704 G:0.641344 R:0.809115

我们看一下测试的代码, 还是之前的界面里面的第二个按钮执行的函数, 这里我们第一个按钮是去读取我们之前存储的噪声图像, 按名称读取,
然后结果的时候, 我们是按照每幅图像进行的, 这里暂时 高椒盐噪声的图像, 可以在上面给出的图中看到

// 全局 噪声图像数组, psnr 数组 mssim 数组
const std::string IMAGE_DIR ="../testimages/noise/";
std::vector<cv::Mat> gNoiseImg(6);
double psnr[6];
cv::Scalar mssim[6];
void MainWindow::testFunc1(void)
{
    // 用于读取 测试图片
    for(int i=0;i<6;i++)
    {
        gNoiseImg[i] = cv::imread(IMAGE_DIR + "lena-" + std::to_string(i+1) + ".png");
    }

    qDebug("ReadOK");
}
void MainWindow::testFunc2(void)
{
    QString res_temp = "image-%1: psnr:%2, mssim: B:%3 G:%4 R:%5 ";
    QString res_str;

    // 测试 均值滤波 三种方式的不同
    const int TEST = 1; // 使用统一的图进行测试 暂时使用 高 椒盐噪声图像
    psnr[TEST] = getPSNR(gSrcImg, gNoiseImg[TEST]);
    mssim[TEST] = getMSSIM(gSrcImg,gNoiseImg[TEST]);

    res_str = res_temp.arg("noise")
            .arg(psnr[TEST])
            .arg(mssim[TEST].val[0])
            .arg(mssim[TEST].val[1])
            .arg(mssim[TEST].val[2]);

    // 噪声的参数值
    ui->pt_log->appendPlainText(res_str);

    cv::Mat dst[3];

    dst[0] = meanFilter(gNoiseImg[TEST]);
    dst[1] = meanFilterByFilter2D(gNoiseImg[TEST]);
    dst[2] = meanFilterByBlur(gNoiseImg[TEST]);

    // 分别计算三种方式得到的滤波的效果 (结果图与 原始图比较)
    for(int i=0;i<3;i++)
    {
        psnr[TEST] = getPSNR(gSrcImg, dst[i]);
        mssim[TEST] = getMSSIM(gSrcImg,dst[i]);

        res_str = res_temp.arg(i+1)
                .arg(psnr[TEST])
                .arg(mssim[TEST].val[0])
                .arg(mssim[TEST].val[1])
                .arg(mssim[TEST].val[2]);

        // 噪声的参数值
        ui->pt_log->appendPlainText(res_str);

        cv::imwrite(IMAGE_DIR + "dst_" + std::to_string(i+1)+".png",dst[i]);
    }
}

从上面的参数也能看出来, 后面两种方法得到的结果图像是一模一样的, 我们就不再进行展示, 先看 我们实现的均值滤波与自带的均值滤波的图像区别

opencv-10-图像滤波-噪声添加与均值滤波-含opencv C++ 代码实现-LMLPHP

其实吧, 总体的结果上是看不出来区别的, 主要是我们的算法上没有进行边界的处理部分, 能在图的中间部分看到稍微的几个噪声点没有处理掉, 这可能也是我们的结果参数要稍微小一点的原因, 总体来说, 我们的算法还是能够进行均值滤波的, 而且跟自带的处理结果也是一致的.

我这里就有了一个疑问, 为什么我们后面的结果就一模一样了呢,blur 去调用了 filter2D? 然后我去看两个函数的调用图, 感觉问题应该是出在 cv::FilterEngine::apply 函数上,在后面就没去研究了

opencv-10-图像滤波-噪声添加与均值滤波-含opencv C++ 代码实现-LMLPHP

opencv-10-图像滤波-噪声添加与均值滤波-含opencv C++ 代码实现-LMLPHP

总结

其实这里原本是计划一起写完的, 但是真的太伤了, 慢慢来吧, 中间容易跑偏, 因为这边还要做比较多的东西, 所以写的越来越慢, 不过至少目前兴致还是很高的 , 昨天发在博客园的文章还被 ImageShop 大佬点赞, 还是很开心的,

其实越写感觉自己越虚, 很多深入的东西自己都不能说摸透了, 还是要深入去研究了 但是写的深入了看得就少了一点, 其实我尽量写的浅一点, 因为很多人最开始就是搜索 blog 找答案的, 能看懂就行,
我会在后面将常用的都给写完的, 尽量更新的快, 现在每天要花大量的时间去查, 去看还要写, 希望我还能坚持下去, 加油.

广告

就是我这里还有之前的 函数调用图都是自己使用 doxygen 和graphiz 参考绘制函数调用图(call graph)(4):doxygen + graphviz 自己重新生成的opencv 的文档图, 至少在用起来还是比较简单的 这个就是一个静态的网页, 我把它放在了我的 服务器上, 这样别人也能访问有需要的可以看下, 基于 opencv 4.3.0 版本的文档图 http://schen.xyz:89/opencv

参考

  1. 《高斯噪声_百度百科》. 见于 2020年4月30日. https://baike.baidu.com/item/高斯噪声.
  2. 《绘制函数调用图(call graph)(4):doxygen + graphviz_运维_许振坪的专栏-CSDN博客》. 见于 2020年5月2日. https://blog.csdn.net/benkaoya/article/details/79763668.
  3. 知乎专栏. 《【技术综述】一文道尽传统图像降噪方法》. 见于 2020年4月29日. https://zhuanlan.zhihu.com/p/51403693.
  4. 知乎专栏. 《可复现的图像降噪算法总结》. 见于 2020年4月29日. https://zhuanlan.zhihu.com/p/32502816.
  5. 《图像噪声的成因分类与常见图像去噪算法简介_Java_qq_27606639的博客-CSDN博客》. 见于 2020年4月30日. https://blog.csdn.net/qq_27606639/article/details/80912071.
  6. 《最小均方滤波器》. 收入 维基百科,自由的百科全书, 2018年3月9日. https://zh.wikipedia.org/w/index.php?title=最小均方滤波器&oldid=48602322.
  7. 《Computer Vision: Algorithms and Applications, 1st ed.》 见于 2020年5月1日. http://szeliski.org/Book/.
  8. 《OpenCV: Smoothing Images》. 见于 2020年5月1日. https://docs.opencv.org/4.3.0/dc/dd3/tutorial_gausian_median_blur_bilateral_filter.html.
  9. 《openCV之中值滤波&均值滤波(及代码实现)_人工智能_林小默-CSDN博客》. 见于 2020年5月1日. https://blog.csdn.net/weixin_37720172/article/details/72627543.
05-02 18:16