【计算摄影学】中值滤波的简单实现

概念

中值滤波是一种非线性平滑技术,它将每一像素点的灰度值设置为该点某邻域窗口内的所有像素点灰度值的中值。一般来说,高斯滤波用于去高斯噪声,而对于非高斯噪声污染的图像,高斯滤波并不能起到很好的效果。对于中值滤波而言,其更善于处理随机的01噪声,即椒盐噪声。

所谓椒盐噪声,分别指胡椒(pepper)噪声与盐(salt)噪声,前者指代像素值为0的黑色噪点,后者指代像素值为255的白色噪点。如下图所示,可以看到其中分布了很多的椒盐噪声。

(椒盐噪声污染图像)

中值滤波的基本思想是指将滤波窗口内的像素值,按照灰度的大小进行排序,取灰度值中位数的那一点的像素值作为中心点的像素值。在这里有几个重点:

  • 中值滤波比较的是灰度值,而非RGB像素值。
  • 如果原始图像为灰度图像,可以直接将中值灰度作为输出灰度。
  • 如果原始图像包含多个通道,必须将其转为灰度值再进行排序比较。之后输出时将中值灰度代表的那一点的像素值付给目标图像。

以上思想务必弄清楚,否则在代码阶段是无法正确输出的。

 

理论分析

求解中值滤波的步骤可以分为如下几步:

  • 读取原始图像。
  • 为原始图像添加随机的椒盐噪声。
  • 灰度图像直接用窗口内灰度值排序,多通道图像先将某点像素值转为灰度值再进行排序。
  • 取排序后的中位数所代表的那一点的像素值作为窗口中心点的像素值。

需要注意的是对于图像边缘的处理,在实验中我是将位于窗口内,且不超过图像边界的点全部排序。也就是没有扩充图像,直接将外部点排除。

 

实验细节

1、为原始图像添加椒盐噪声

// 该函数为图像矩阵添加盐噪声255
Mat salt(Mat image, int n) {
    Mat result = image.clone();
    for (int i = 0; i < n; ++i) {
        int row = rand() % result.rows;
        int col = rand() % result.cols;
        result.at<Vec3b>(row, col)[0] = 255;
        result.at<Vec3b>(row, col)[1] = 255;
        result.at<Vec3b>(row, col)[2] = 255;
    }
    return result;
}

// 该函数为图像矩阵添加胡椒噪声0
Mat pepper(Mat image, int n) {
    Mat result = image.clone();
    for (int i = 0; i < n; ++i) {
        int row = rand() % result.rows;
        int col = rand() % result.cols;
        result.at<Vec3b>(row, col)[0] = 0;
        result.at<Vec3b>(row, col)[1] = 0;
        result.at<Vec3b>(row, col)[2] = 0;
    }
    return result;
}

通过自定义函数,设置需要添加椒盐噪声的个数n,通过rand()函数取随机数,在随机得到的(row,col)点处将像素值进行修改为255或0。

2、RGB通道值转灰度值并进行排序

// 遍历图像,进行中值滤波处理
for (int i = 0; i < middle.rows; i++) {
    for (int j = 0; j < middle.cols; j++) {
        vector<uchar> gray;   // 记录灰度值
        for (int m = i - height; m <= i + height; m++) {
            for (int n = j - width; n <= j + width; n++) {
                if (!(m<0 || m>middle.rows - 1 || n<0 || n>middle.cols - 1)) {
                    // 该条件排除了窗口内的图像边界外部值
                    gray.push_back(middle.at<Vec3b>(m, n)[0] * 0.114 + middle.at<Vec3b>(m, n)[1] * 0.587 + middle.at<Vec3b>(m, n)[2] * 0.299);
                }
            }
        }
        int center = ceil(gray.size() / 2);
        sort(gray.begin(), gray.end());
    }
}

该步通过遍历整个图像矩阵,将每一点(i,j)作为中心点,先排除图像边界外部的点值,直接将内部点的像素值通过公式转为灰度值,并将该灰度值存储到vector向量中。利用C++内置的sort函数,对gray进行排序。由于边界值的存在,内部点的个数可能为偶数个,同时也有可能是奇数,这里直接用ceil进行向上取整定位排序后的中值的位置。之后,利用已定位的中值点,将其像素值赋值给需要输出的目标矩阵即可。

 

结果展示

(原始图像)

(椒盐噪声污染图像)

(中值滤波处理图像,滤波窗口大小为3 × 3)

(中值滤波处理图像,滤波窗口大小为11 × 11)

可以看出,窗口值越大,中值滤波处理的图像边缘更平滑。也因此模糊了图像的边缘信息。在下节,我将会实现双边滤波,来实现图像边缘信息的保留。

 

最后附上完整代码:

#include<opencv2/opencv.hpp>
#include<iostream>
#include<algorithm>
using namespace std;
using namespace cv;

// 该函数为图像矩阵添加盐噪声255
Mat salt(Mat image, int n) {
    Mat result = image.clone();
    for (int i = 0; i < n; ++i) {
        int row = rand() % result.rows;
        int col = rand() % result.cols;
        result.at<Vec3b>(row, col)[0] = 255;
        result.at<Vec3b>(row, col)[1] = 255;
        result.at<Vec3b>(row, col)[2] = 255;
    }
    return result;
}

// 该函数为图像矩阵添加胡椒噪声0
Mat pepper(Mat image, int n) {
    Mat result = image.clone();
    for (int i = 0; i < n; ++i) {
        int row = rand() % result.rows;
        int col = rand() % result.cols;
        result.at<Vec3b>(row, col)[0] = 0;
        result.at<Vec3b>(row, col)[1] = 0;
        result.at<Vec3b>(row, col)[2] = 0;
    }
    return result;
}

int main()
{
    Mat src, dst, middle;
    Mat kernel;
    string image_name;
    string output_name;
    int width, height, img_height, img_width;

    cout << "请输入需要读取的图像名称:";
    cin >> image_name;

    cout << "请输入输出的图像名称:";
    cin >> output_name;

    cout << "请输入卷积核中心到边界的宽度:";
    cin >> width;

    cout << "请输入卷积核中心到边界的高度:";
    cin >> height;

    cout << "正在处理,请稍后……" << endl;

    // 进行初始判断
    src = imread(image_name, IMREAD_COLOR);
    if (src.empty()) {
        cout << "图像读取失败!" << endl;
        return 0;
    }
    else {
        // 若读取成功,判断卷积核的大小与图像大小的关系
        img_height = src.rows;
        img_width = src.cols;
        imshow("原始图像", src);
        dst = src.clone();
        if (img_height < height * 2 + 1 || img_width < width * 2 + 1) {
            cout << "卷积核大小超过图像大小!" << endl;
            return 0;
        }
    }

    // 为原始图像添加椒盐噪声
    middle = salt(src, 3000);         //加入盐噪声255
    middle = pepper(middle, 3000);    //加入椒噪声0
    imshow("椒盐噪声图像", middle);
    imwrite("椒盐噪声.jpg", middle);

    // 遍历图像,进行中值滤波处理
    for (int i = 0; i < middle.rows; i++) {
        for (int j = 0; j < middle.cols; j++) {
            vector<uchar> gray;   // 记录灰度值
            for (int m = i - height; m <= i + height; m++) {
                for (int n = j - width; n <= j + width; n++) {
                    if (!(m<0 || m>middle.rows - 1 || n<0 || n>middle.cols - 1)) {
                        // 该条件排除了窗口内的图像边界外部值
                        gray.push_back(middle.at<Vec3b>(m, n)[0] * 0.114 + middle.at<Vec3b>(m, n)[1] * 0.587 + middle.at<Vec3b>(m, n)[2] * 0.299);
                    }
                }
            }

            int center = ceil(gray.size() / 2);
            int flag = 0;
            sort(gray.begin(), gray.end());
            for (int m = i - height; m <= i + height; m++) {
                for (int n = j - width; n <= j + width; n++) {
                    if (!(m<0 || m>middle.rows - 1 || n<0 || n>middle.cols - 1)) {
                        uchar a = middle.at<Vec3b>(m, n)[0] * 0.114 + middle.at<Vec3b>(m, n)[1] * 0.587 + middle.at<Vec3b>(m, n)[2] * 0.299;
                        if (gray.at(center) == a) {
                            flag = 1;
                            dst.at<Vec3b>(i, j) = middle.at<Vec3b>(m, n);
                            break;
                        }
                    }
                }
                if (flag == 1) {
                    break;
                }
            }
        }
    }

    imshow("中值滤波后图像", dst);
    imwrite(output_name, dst);
    waitKey(0);
}

 

posted @ 2020-03-13 13:34  Suubai  阅读(829)  评论(0编辑  收藏  举报