圖像處理基礎(5):雙邊濾波器

雙邊濾波(Bilateral filter)是一種非線性的濾波方法,是結合圖像的空間鄰近度和像素值類似度的一種折衷處理,同時考慮空域信息和灰度類似性,達到保邊去噪的目的。算法

雙邊濾波器之因此可以作到在平滑去噪的同時還可以很好的保存邊緣(Edge Preserve),是因爲其濾波器的核由兩個函數生成:數組

  • 一個函數由像素歐式距離決定濾波器模板的係數
  • 另外一個函數由像素的灰度差值決定濾波器的係數

其綜合了高斯濾波器(Gaussian Filter)和\(\alpha\)-截尾均值濾波器(Alpha-Trimmed mean Filter)的特色。高斯濾波器只考慮像素間的歐式距離,其使用的模板係數隨着和窗口中心的距離增大而減少;Alpha截尾均值濾波器則只考慮了像素灰度值之間的差值,去掉\(\alpha\%\)的最小值和最大值後再計算均值。函數

雙邊濾波器使用二維高斯函數生成距離模板,使用一維高斯函數生成值域模板。
距離模板係數的生成公式以下:優化

\[ d(i,j,k,l) = exp(-\frac{(i-k)^2 + (j -l)^2}{2\sigma_d^2}) \]
其中,\((k,l)\)爲模板窗口的中心座標;\((i,j)\)爲模板窗口的其餘係數的座標;\(\sigma_d\)爲高斯函數的標準差。 使用該公式生成的濾波器模板和高斯濾波器使用的模板是沒有區別的。spa

值域模板係數的生成公式以下:
\[ r(i,j,k,l)= exp(-\frac{||f(i,j)-f(k,l)||^2}{2\sigma_r^2}) \]
其中,函數\(f(x,y)\)表示要處理的圖像,\(f(x,y)\)表示圖像在點\((x,y)\)處的像素值;\((k,l)\)爲模板窗口的中心座標;\((i,j)\)爲模板窗口的其餘係數的座標;\(\sigma_r\)爲高斯函數的標準差。指針

將上述兩個模板相乘就獲得了雙邊濾波器的模板
\[ w(i,j,k,l) = d(i,j,k,l) * r(i,j,k,l) = exp(-\frac{(i-k)^2 + (j -l)^2}{2\sigma_d^2}-\frac{||f(i,j)-f(k,l)||^2}{2\sigma_r^2}) \]code

實現(參考OpenCV源代碼)

這裏的實現主要參考OpenCV中的bilateralFilter實現,其實現主要有兩個優化:blog

  • 使用查表的方式計算灰度值模板係數
  • 將二維的模板轉換爲一維,下降算法複雜度。

在濾波以前,首先將灰度值模板係數計算出來。opencv

double color_coeff = -0.5 / (color_sigma * color_sigma);
vector<double> _color_weight(channels * 256); // 存放差值的平方
double *color_weight = &_color_weight[0];
for (int i = 0; i < channels * 256; i++)
        color_weight[i] = exp(i * i * color_coeff);

灰度值的模板係數計算公式參見上面的公式,是兩個灰度值的差值的平方。這裏表的長度是channels * 256沒有想通,應該255的長度就足夠了。在使用的時候,首先取出模板中心的灰度值val0,而後依次取出模板其餘位置的灰度值val,使用abs(val - val0)的差值從color_weight查表獲得灰度值模板的係數。模板

距離的模板是二維的,這裏使用的方法就i比較巧妙,將其化爲了一維。

vector<double> _space_weight(ksize * ksize); // 空間模板係數
vector<int> _space_ofs(ksize * ksize); // 模板窗口的座標

// 生成空間模板
    int maxk = 0;
    for (int i = -radius; i <= radius; i++)
    {
        for (int j = -radius; j <= radius; j++)
        {
            double r = sqrt(i*i + j * j);
            if (r > radius)
                continue;
            space_weight[maxk] = exp(r * r * space_coeff); // 存放模板係數
            space_ofs[maxk++] = i * temp.step + j * channels; // 存放模板的位置,和模板係數相對應
        }
    }

使用一維數組存放空間模板係數,同時使用另外一個一維數組存放模板位置,和係數相對應。
整個代碼的實現以下:

void myBilateralFilter(const Mat &src, Mat &dst, int ksize, double space_sigma, double color_sigma)
{
    int channels = src.channels();
    CV_Assert(channels == 1 || channels == 3);
    double space_coeff = -0.5 / (space_sigma * space_sigma);
    double color_coeff = -0.5 / (color_sigma * color_sigma);
    int radius = ksize / 2;
    Mat temp;
    copyMakeBorder(src, temp, radius, radius, radius, radius, BorderTypes::BORDER_REFLECT);
    vector<double> _color_weight(channels * 256); // 存放差值的平方
    vector<double> _space_weight(ksize * ksize); // 空間模板係數
    vector<int> _space_ofs(ksize * ksize); // 模板窗口的座標
    double *color_weight = &_color_weight[0];
    double *space_weight = &_space_weight[0];
    int    *space_ofs = &_space_ofs[0];
    for (int i = 0; i < channels * 256; i++)
        color_weight[i] = exp(i * i * color_coeff);
    // 生成空間模板
    int maxk = 0;
    for (int i = -radius; i <= radius; i++)
    {
        for (int j = -radius; j <= radius; j++)
        {
            double r = sqrt(i*i + j * j);
            if (r > radius)
                continue;
            space_weight[maxk] = exp(r * r * space_coeff); // 存放模板係數
            space_ofs[maxk++] = i * temp.step + j * channels; // 存放模板的位置,和模板係數相對應
        }
    }
    // 濾波過程
    for (int i = 0; i < src.rows; i++)
    {
        const uchar *sptr = temp.data + (i + radius) * temp.step + radius * channels;
        uchar *dptr = dst.data + i * dst.step;
        if (channels == 1)
        {
            for (int j = 0; j < src.cols; j++)
            {
                double sum = 0, wsum = 0;
                int val0 = sptr[j]; // 模板中心位置的像素
                for (int k = 0; k < maxk; k++)
                {
                    int val = sptr[j + space_ofs[k]];
                    double w = space_weight[k] * color_weight[abs(val - val0)]; // 模板係數 = 空間係數 * 灰度值係數
                    sum += val * w;
                    wsum += w;
                }
                dptr[j] = (uchar)cvRound(sum / wsum);
            }
        }
        else if (channels == 3)
        {
            for (int j = 0; j < src.cols * 3; j+=3)
            {
                double sum_b = 0, sum_g = 0, sum_r = 0, wsum = 0;
                int b0 = sptr[j];
                int g0 = sptr[j + 1];
                int r0 = sptr[j + 2];
                for (int k = 0; k < maxk; k++)
                {
                    const uchar *sptr_k = sptr + j + space_ofs[k];
                    int b = sptr_k[0];
                    int g = sptr_k[1];
                    int r = sptr_k[2];
                    double w = space_weight[k] * color_weight[abs(b - b0) + abs(g - g0) + abs(r - r0)];
                    sum_b += b * w;
                    sum_g += g * w;
                    sum_r += r * w;
                    wsum += w;
                }
                wsum = 1.0f / wsum;
                b0 = cvRound(sum_b * wsum);
                g0 = cvRound(sum_g * wsum);
                r0 = cvRound(sum_r * wsum);
                dptr[j] = (uchar)b0;
                dptr[j + 1] = (uchar)g0;
                dptr[j + 2] = (uchar)r0;
            }
        }
    }
}

須要注意圖像像素值的獲取,首先獲取到每行的座標指針

const uchar *sptr = temp.data + (i + radius) * temp.step + radius * channels;
uchar *dptr = dst.data + i * dst.step;

在濾波循環中,從space_ofs中取出每一個模板位置偏移地址

int val = sptr[j + space_ofs[k]];

這種實現方法,大大的下降濾波的時間複雜度。

結果對比:

實現的結果和OpenCV的實現相差無幾。sigma = 80,模板大小爲20

總結

雙邊濾波器,在平滑圖像的同時,還可以很好的保護圖像的邊緣信息,例如上圖中,圖像的平滑效果很是明顯了,可是頭髮的髮絲仍是很明顯的。 雙邊濾波器的最重要參數仍然是標準差sigma,其值小於10時,平滑效果不是很明顯。

相關文章
相關標籤/搜索