最近学习了下侧窗滤波算法,这是一个保边滤波算法,据说该算法的论文是近几年CV行业为数不多的非深度学习论文,方法简单而高效,查阅资料及网上的代码实现了下算法,跑了一些图后,不太确定实现的效果是否正确。翻看论文的时候,看他们的论文中贴出的图效果很惊艳,甚至有些动物的毛发都可以保留下来,于是一开始不是太确定自己的代码是否正确,最后自己造了张图片跑了下,发现结果应该没什么问题。下面依次从原理分析、代码实现及效果分析三个方面来介绍吧。
原理分析
通常用常规图像算法做检测类的算法需要将图像特征增强,其中就需要滤波,把噪点去掉,如果直接用滤波,像高斯滤波,中值滤波,均值滤波等等,不仅会把噪点过滤掉,也会把图像的一些细节,包括边缘给模糊掉,从而造成图像信息丢失。有时候为了保留边缘及一些细节信息,就会用到保边滤波算法。之前用过的保边滤波包括双边滤波,表面模糊等算法。这两个算法都比较慢,对于检测类的算法来说,性价比不高。
传统滤波算法以滤波器的核中心与要处理的像素对齐进行计算,不考虑要处理的像素是否是在边缘区域,导致滤波窗口在跨越边缘时,引入对来自于边缘两侧像素值的加权计算。一旦经过这样的计算,将会导致边缘信息在沿边缘法线的方向出现扩散,导致边缘信息减弱或者丢失。比如对于均值滤波来说:
为了避免传统滤波算法对边缘的弱化,现在考虑将待处理的像素不一定非要与滤波器核心对齐。
下面拿均值滤波来说明。对于平坦区域,均值滤波不管滤波器用什么形状的滤波核,其计算结果都跟原像素相差不大。对于处在边缘的像素,若想尽可能的保留边缘,我们需要尽可能的保留跟当前要计算的像素相似的区块中的像素进行计算,而跟当前要计算的像素相差很大的区块中的像素要尽可能抛掉。这个思路跟表面模糊算法很像。这里稍微介绍下表面模糊算法:
从上面公式可以看出,表面模糊算法的主要思想还是计算当前像素X的邻域范围内不同像素的加权求和,如果跟中心像素差的越多,则权值越小,跟中心像素相差越小,则权值越大,以此来保留边缘信息。上面公式中如果Y值取得非常大,那么表面模糊算法跟均值滤波算法就是一样的。
如下图是典型的理想边缘模型,分别是阶跃边缘、斜坡边缘和屋顶型边缘。我们做均值滤波,计算下图中红点所在像素的新像素值时,肯定希望尽可能用到跟红点在同一个面上的区块像素,而抛掉跟红点不在同一个平面的区块像素,这样就可以尽可能的保留红点所在的边缘,而不是弱化红点所在的边缘。
为了达到上述目的,我们可以预设一些滤波器,这些滤波器内部就有边缘,并且边缘类型就是上图中阶跃边缘类型,它被一切为2块。这样在对图像的边缘块的像素进行计算时,就可以让这样滤波器模型跟实际要计算的像素块所处的边缘模型进行匹配,如果能够匹配上,那么红点处计算的像素值将不会有太大变化,如果匹配是相反的,那么红点处计算的新值将和原来的值差很多。
滤波器中边缘模型有很多种,为了便于计算,只取其中8种。其它形状的滤波器由于有锯齿形状,不好遍历计算(尤其是在滤波核半径如果比较大的时候,那时如果有锯齿形状就只能一行行遍历,不能for循环)因此没有被采用。
上图8类滤波器中每个滤波器被明显分成两个块,即是有边缘的,并且形状不一,最重要的是其中的值分布是呈正方形或者长方形,非常便于遍历计算。我们只需要用上述面8种滤波器分别跟待计算的像素做匹配,然后计算对应的新的像素值,当该新的像素值跟原像素值相差的最小时,说明对应的滤波器的边缘形状,跟待计算的像素所在区块的边缘形状最接近,这时采取的均值滤波对于边缘处的像素弱化最小。
上面是我根据自己的理解对侧窗滤波算法的介绍。原论文里面有公式推导,也有分析滤波器侧窗的形状的公式,比较严谨,但是感觉不太直观。我按照自己的思考提供一种理解方式吧,如果有疑问,或者认为我的理解有误的,欢迎指正哈,谢谢!
代码实现
#include
#include
#include
using namespace std;
using namespace cv;
#include
#include
#include
enum class SIDE_DIRECTIONS : int {
L = 0,
R = 1,
U = 2,
D = 3,
NW = 4,
NE = 5,
SW = 6,
SE = 7,
SIDE_DIRECTIONS_COUNT = 8
};
const int side_directions_count = 8;
enum class XY_START_END : int {
WIDTH_START = 0,
WIDTH_END = 1,
HEIGHT_START = 2,
HEIGHT_END = 3,
XY_START_END_COUNT = 4
};
//表面模糊处理
void SurfaceBlur(float *data, int height, int width, int r, int threshold,
float *result) {
for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; ++j) {
float sumx = 0.f;
float sumy = 0.f;
for (int m = i - r; m < i + r; ++m) {
for (int n = j - r; n < j + r; ++n) {
sumx = sumx + (1 - fabs(data[i * width + j] - data[m * width + n]) /
(2.5 * threshold)) *
data[m * width + n];
sumy = sumy + (1 - fabs(data[i * width + j] - data[m * width + n]) /
(2.5 * threshold));
}
}
result[i * width + j] = sumx / sumy;
}
}
}
std::vector > m_side_window_params;
std::vector > m_side_window_filter_results;
void init(int height, int width, int radius) {
int panel_size = height * width;
m_side_window_params.resize(side_directions_count);
m_side_window_params[static_cast(SIDE_DIRECTIONS::L)] = {
-radius, 0, -radius, radius};
m_side_window_params[static_cast(SIDE_DIRECTIONS::R)] = {
0, radius, -radius, radius};
m_side_window_params[static_cast(SIDE_DIRECTIONS::U)] = {-radius, radius,
-radius, 0};
m_side_window_params[static_cast(SIDE_DIRECTIONS::D)] = {-radius, radius,
0, radius};
m_side_window_params[static_cast(SIDE_DIRECTIONS::NW)] = {-radius, 0,
-radius, 0};
m_side_window_params[static_cast(SIDE_DIRECTIONS::NE)] = {0, radius,
-radius, 0};
m_side_window_params[static_cast(SIDE_DIRECTIONS::SW)] = {-radius, 0, 0,
radius};
m_side_window_params[static_cast(SIDE_DIRECTIONS::SE)] = {0, radius, 0,
radius};
m_side_window_filter_results.resize(side_directions_count);
for (int i = 0; i < side_directions_count; ++i) {
m_side_window_filter_results[i].resize(panel_size);
}
}
void sideWindowFilterImpl(const float *input, const int radius,
const int height, const int width,
const SIDE_DIRECTIONS direction, float *output) {
const int w_start =
m_side_window_params[static_cast(direction)]
[static_cast(XY_START_END::WIDTH_START)];
const int w_end =
m_side_window_params[static_cast(direction)]
[static_cast(XY_START_END::WIDTH_END)];
const int h_start =
m_side_window_params[static_cast(direction)]
[static_cast(XY_START_END::HEIGHT_START)];
const int h_end =
m_side_window_params[static_cast(direction)]
[static_cast(XY_START_END::HEIGHT_END)];
const int w_first_loop_end = std::max(0, -w_start);
const int w_second_loop_start = w_first_loop_end;
const int w_second_loop_end = width - 1 - w_end;
const int w_third_loop_start = width - w_end;
int nn = 0;
int remain_start = (nn << 2) + w_second_loop_start;
int out_idx = 0;
int threshold = 10000;
for (int h = 0; h < height; ++h) {
const int h_lower_bound = std::max(0, h + h_start);
const int h_upper_bound = std::min(height - 1, h + h_end);
const int h_interval = h_upper_bound - h_lower_bound + 1;
//第一个for循环是处理图像边缘
for (int w = 0; w < w_first_loop_end; ++w) {
const int w_left_bound = std::max(0, w + w_start);
const int w_right_bound = std::min(width - 1, w + w_end);
const int arr_len = h_interval * (w_right_bound - w_left_bound + 1);
int idx = 0;
float sumx = 0.f;
float sumy = 0.f;
float center_pixel = input[h * width + w];
for (int i = h_lower_bound; i <= h_upper_bound; ++i) {
const int h_idx = i * width;
for (int j = w_left_bound; j <= w_right_bound; ++j) {
sumx = sumx + (1 - fabs(center_pixel - input[h_idx + j]) /
(2.5 * threshold)) *
input[h_idx + j];
sumy = sumy + (1 - fabs(center_pixel - input[h_idx + j]) /
(2.5 * threshold));
}
}
output[out_idx++] = sumx / sumy;
}
//这里是处理图像中间的任意像素
for (int w = remain_start; w <= w_second_loop_end; ++w) {
const int w_left_bound = std::max(0, w + w_start);
const int w_right_bound = std::min(width - 1, w + w_end);
int idx = 0;
float sumx = 0.f;
float sumy = 0.f;
float center_pixel = input[h * width + w];
for (int i = h_lower_bound; i <= h_upper_bound; ++i) {
const int h_idx = i * width;
for (int j = w_left_bound; j <= w_right_bound; ++j) {
sumx = sumx + (1 - fabs(center_pixel - input[h_idx + j]) /
(2.5 * threshold)) *
input[h_idx + j];
sumy = sumy + (1 - fabs(center_pixel - input[h_idx + j]) /
(2.5 * threshold));
}
}
output[out_idx++] = sumx / sumy;//这里是基于表面模糊的侧窗滤波算法,
//如果你想改成基于均值滤波的侧窗滤波算法,那么就统计下上面那两个for循环的
//像素个数,及累加的像素值的和,然后在这里求下比值赋值给output[out_idx++]就行了
}
for (int w = w_third_loop_start; w < width; ++w) {
const int w_left_bound = std::max(0, w + w_start);
const int w_right_bound = std::min(width - 1, w + w_end);
const int arr_len = h_interval * (w_right_bound - w_left_bound + 1);
int idx = 0;
float sumx = 0.f;
float sumy = 0.f;
float center_pixel = input[h * width + w];
for (int i = h_lower_bound; i <= h_upper_bound; ++i) {
const int h_idx = i * width;
for (int j = w_left_bound; j <= w_right_bound; ++j) {
sumx = sumx + (1 - fabs(center_pixel - input[h_idx + j]) /
(2.5 * threshold)) *
input[h_idx + j];
sumy = sumy + (1 - fabs(center_pixel - input[h_idx + j]) /
(2.5 * threshold));
}
}
output[out_idx++] = sumx / sumy;
}
}
}
void SideWindowFilter(const float *input, const int radius, const int height,
const int width, float *output) {
int panel_size = height * width;
for (int i = 0; i < side_directions_count; ++i) {
sideWindowFilterImpl(input, radius, height, width,
static_cast(i),
m_side_window_filter_results[i].data());
}
for (int i = 0; i < panel_size; ++i) {
float tmp = m_side_window_filter_results[0][i] - input[i];
float min = tmp * tmp;
int min_idx = 0;
for (int j = 1; j < side_directions_count; ++j) {
tmp = m_side_window_filter_results[j][i] - input[i];
tmp = tmp * tmp;
if (min > tmp) {
min_idx = j;
min = tmp;
}
}
output[i] = m_side_window_filter_results[min_idx][i];
}
}
int main() {
cv::Mat sourceimg = imread("src_img/降噪test.jpg", 1);
cv::Mat resultimg = imread("src_img/降噪test.jpg", 1);
cv::imwrite("0_sourceimg.jpg", sourceimg);
int img_h = sourceimg.rows;
int img_w = sourceimg.cols;
cv::Mat temp_img;
temp_img.create(img_h, img_w, CV_32FC3);
sourceimg.convertTo(temp_img, CV_32FC3);
float *input_r = new float[img_h * img_w];
float *input_g = new float[img_h * img_w];
float *input_b = new float[img_h * img_w];
int radius = 10;
int iteration = 10;
for (int ite = 10; ite <= 10; ++ite) {
iteration = 5;
for (int rad = 3; rad <= 3; rad +=1) {
radius = 2;
for (int i = 0; i < img_h; ++i) {
for (int j = 0; j < img_w; ++j) {
input_r[i * img_w + j] = sourceimg.data[i * img_w * 3 + j * 3 + 0];
input_g[i * img_w + j] = sourceimg.data[i * img_w * 3 + j * 3 + 1];
input_b[i * img_w + j] = sourceimg.data[i * img_w * 3 + j * 3 + 2];
}
}
// SurfaceBlur(input,img_h,img_w, 1, 250, result);
init(img_h, img_w, radius);
for (int m = 0; m < iteration; ++m) {
float *temp_result_r = new float[img_h * img_w];
float *temp_result_g = new float[img_h * img_w];
float *temp_result_b = new float[img_h * img_w];
SideWindowFilter(input_r, radius, img_h, img_w, temp_result_r);
SideWindowFilter(input_g, radius, img_h, img_w, temp_result_g);
SideWindowFilter(input_b, radius, img_h, img_w, temp_result_b);
for (int i = 0; i < img_h; ++i) {
for (int j = 0; j < img_w; ++j) {
input_r[i * img_w + j] = temp_result_r[i * img_w + j];
input_g[i * img_w + j] = temp_result_g[i * img_w + j];
input_b[i * img_w + j] = temp_result_b[i * img_w + j];
}
}
delete (temp_result_r);
delete (temp_result_g);
delete (temp_result_b);
}
for (int i = 0; i < img_h; ++i) {
for (int j = 0; j < img_w; ++j) {
resultimg.data[i * img_w * 3 + j * 3 + 0] = input_r[i * img_w + j];
resultimg.data[i * img_w * 3 + j * 3 + 1] = input_g[i * img_w + j];
resultimg.data[i * img_w * 3 + j * 3 + 2] = input_b[i * img_w + j];
}
}
char str[100];
sprintf(str, "iteration_%d_radius_%d_threshold_50.jpg", iteration,
radius);
cv::imwrite(str, resultimg);
}
}
// cv::imwrite("SurfaceBlur.jpg", resultimg);
delete (input_r);
delete (input_g);
delete (input_b);
waitKey(-1);
}
上面的代码是基于表面模糊的侧窗滤波算法,表面模糊可以换成其他任意滤波核,像中值,均值,高斯等等滤波核。
效果分析
上面代码实现后,通过跑不同迭代次数,及设置不同的滤波半径,跑了一批图片,但是十分不确定效果是否正确,于是灵机一动,要不自己造点数据来跑算法,验证下看看吧?于是就有了如下动图:
上图中原图就是我造的数据,里面包含各种边缘类型,大致也包含论文中提出的三种边缘模型。
另外还有一些散点视作噪点,一些不同粗细的线条视作毛发,从结果图可以看出,很多边缘部分经过侧窗滤波后都可以原封不动的保留下来,这些边缘要是用高斯模糊,中值或者均值滤波,绝对会被模糊掉的。通过结果图可以看出,如果毛发太细了,那么很有可能还是会被模糊掉,或者被过滤掉,即无法保边。
上面的结果图也证实了,我实现的算法莫得太大的问题,基本上达到了保边效果。原论文里面能够实现一些动物毛发被保留,可能跟一定大小图像选择合适的滤波器大小及设置合适的参数有关,如果参数设置不合适,就得不到论文中贴出的图的效果。
从上面的介绍可以看出,表面模糊算法和基于均值滤波的侧窗滤波算法的思路比较像,就是在边缘处进行滤波的时候,尽可能去挑选跟要计算的中心像素像素值接近的(或者处于差不多梯度水平的)像素来参与新的像素值的计算,个人认为不同之处在于表面模糊算法挑选的像素点可能是分散的像素点(这些分散的像素点可能是噪声),而侧窗滤波挑选出来的像素点肯定是连成一块的,所以相对来说单独做基于均值滤波的侧窗滤波算法的保边效果应该要优于表面模糊算法。