简介
高斯滤波是一种线性平滑滤波,用于消除高斯噪声。高斯滤波的中心点权重更大,原理中心的权重较小,其维度为奇数。高斯核函数用于将有限维数据映射到高维空间。通常定义为空间中任意一点 x 到某一中心点 μ 之间的欧式距离的单调函数。
高斯核的计算公式为:
一维高斯核:
G
(
x
)
=
1
2
π
σ
e
−
(
x
−
μ
)
2
2
σ
2
G(x)=\frac{1}{\sqrt{2\pi }\sigma } e^{-\frac{(x-\mu )^{2}}{2\sigma^{2} } }
G(x)=2πσ1e−2σ2(x−μ)2
二维高斯核:
G
(
x
,
y
)
=
1
2
π
σ
1
σ
2
e
−
(
(
(
x
−
μ
1
)
2
2
σ
1
2
)
+
(
(
y
−
μ
2
)
2
2
σ
2
2
)
)
G(x, y)=\frac{1}{\sqrt{2\pi }\sigma_{1} \sigma_{2} } e^{-((\frac{(x-\mu_{1} )^{2}}{2\sigma_{1}^{2} })+(\frac{(y-\mu_{2})^{2} }{2\sigma_{2}^{2} })) }
G(x,y)=2πσ1σ21e−((2σ12(x−μ1)2)+(2σ22(y−μ2)2))
其中,
x 为空间中的点;
μ \mu μ为核函数中心;
控制高斯核函数的作用范围,其值越大,高斯核函数的局部影响范围就越大,其值不要选太小,否则在分类任务中容易过拟合。
计算高斯核
根据上述公式可以计算二维高斯核,高斯核最终需要归一化,在归一化过程中常熟部分
1
2
π
σ
1
σ
2
\frac{1}{\sqrt{2\pi }\sigma_{1} \sigma_{2} }
2πσ1σ21
会被消除,所以在实际计算时只需计算指数部分即可。因为高斯滤波可以进行核分离计算,假设高斯核宽 2a + 1, 高 2b + 1,其推导公式如下:
g
(
x
,
y
)
=
∑
s
=
−
a
a
∑
t
=
−
b
b
[
G
(
x
,
y
)
×
f
(
x
+
s
,
y
+
t
)
]
=
∑
s
=
−
a
a
∑
t
=
−
b
b
[
1
2
π
σ
1
σ
2
e
−
(
(
s
−
μ
1
)
2
2
σ
1
2
+
(
t
−
μ
2
)
2
2
σ
2
2
)
×
f
]
=
∑
s
=
−
a
a
∑
t
=
−
b
b
[
1
2
π
σ
1
e
−
(
s
−
μ
1
)
2
2
σ
1
2
1
2
π
σ
2
e
−
(
t
−
μ
2
)
2
2
σ
2
2
×
f
]
=
∑
s
=
−
a
a
{
1
2
π
σ
1
e
−
(
s
−
μ
1
)
2
2
σ
1
2
∑
t
=
−
b
b
[
1
2
π
σ
2
e
−
(
t
−
μ
2
)
2
2
σ
2
2
×
f
]
⏟
水平卷积
}
⏟
竖直卷积
\begin{matrix} g(x,y)& = \sum_{s=-a}^{a}\sum_{t=-b}^{b} [G(x,y)\times f(x+s,y+t)]\\ & = \sum_{s=-a}^{a}\sum_{t=-b}^{b} [\frac{1}{2\pi\sigma_{1} \sigma{2}}e^{-(\frac{(s-\mu_{1})^{2}}{2\sigma_{1}^{2}} + \frac{(t-\mu_{2})^{2}}{2\sigma_{2}^{2}})} \times f ]\\ & =\sum_{s=-a}^{a}\sum_{t=-b}^{b} [\frac{1}{\sqrt{2\pi} \sigma_{1}}e^{-\frac{(s-\mu_{1})^{2}}{2\sigma_{1}^{2}}} \frac{1}{\sqrt{2\pi} \sigma_{2}} e^{-\frac{(t-\mu_{2})^{2}}{2\sigma_{2}^{2}}} \times f ]\\ & = \underbrace{\sum_{s=-a}^{a}\{\frac{1}{\sqrt{2\pi} \sigma_{1}}e^{-\frac{(s-\mu_{1})^{2}}{2\sigma_{1}^{2}}} \underbrace{\sum_{t=-b}^{b}[\frac{1}{\sqrt{2\pi} \sigma_{2}} e^{-\frac{(t-\mu_{2})^{2}}{2\sigma_{2}^{2}}} \times f]}_{水平卷积} \}}_{竖直卷积} \end{matrix}
g(x,y)=∑s=−aa∑t=−bb[G(x,y)×f(x+s,y+t)]=∑s=−aa∑t=−bb[2πσ1σ21e−(2σ12(s−μ1)2+2σ22(t−μ2)2)×f]=∑s=−aa∑t=−bb[2πσ11e−2σ12(s−μ1)22πσ21e−2σ22(t−μ2)2×f]=竖直卷积
s=−a∑a{2πσ11e−2σ12(s−μ1)2水平卷积
t=−b∑b[2πσ21e−2σ22(t−μ2)2×f]}
因此高斯卷积可以进行分离卷积。所以只需计算一维的高斯核:
cv::Mat window;
window.create(1 , wsize, CV_32F);
int center = (wsize - 1) / 2;
float sum = 0.0;
for (int i = 0; i < wsize; ++i){
float g = exp(-(pow(i - center, 2)) / (2 * sigma*sigma));
window.at<float>(0, i) = g;
sum += g;
}
window = window / sum;
std::vector<uint16_t> kernel(wsize);
for(int i = 0; i < wsize; ++i){
kernel[i] = window.at<float>(0, i) < 1e-5 ? 0 : (uint16_t)round(window.at<float>(0, i) * float((int32_t)(1 << 8)));
在计算一维高斯核的时候使用定点优化
卷积
接下来就是卷积操作,卷积操作需要我们注意边界问题,opencv的编辑采用的镜像反射,即 abcdefg 反射为 gfedcb| abcdefg | fedcba 。我们在计算时需要注意边界问题。
int boder = (wsize - 1) / 2;
uint16_t buf[src.rows][src.channels() * src.cols];
//高斯滤波--水平方向
for (int i = 0; i < src.rows; ++i){
for (int j = 0; j < src.cols; ++j){
uint16_t sum[3] = { 0 };
for (int r = -boder; r <= boder; ++r){
int new_j = j + r < 0 ? -(j + r) : (j + r < src.cols ? j + r : 2 * (src.cols - 1) - j - r);
if (src.channels() == 1){
sum[0] = saturate_cast<uint16_t>(sum[0] + (uint8_t)src.at<uchar>(i, new_j) * kernel[r + boder]); //行不变列变
}
else if (src.channels() == 3){
cv::Vec3b rgb = src.at<cv::Vec3b>(i, new_j);
sum[0] = saturate_cast<uint16_t>(sum[0] + (uint32_t)(rgb[0]) * kernel[r + boder]); //B
sum[1] = saturate_cast<uint16_t>(sum[1] + (uint32_t)(rgb[1]) * kernel[r + boder]); //G
sum[2] = saturate_cast<uint16_t>(sum[2] + (uint32_t)(rgb[2]) * kernel[r + boder]); //R
}
}
if (src.channels() == 1){
buf[i][j] = sum[0];
}
else if (src.channels() == 3){
buf[i][j * 3 + 0] = sum[0];
buf[i][j * 3 + 1] = sum[1];
buf[i][j * 3 + 2] = sum[2];
}
}
}
// 高斯滤波--垂直方向
// 对水平方向处理后的dst边界填充
// cv::Size 的参数为 宽,高,注意顺序
dst = cv::Mat::zeros(src.size(), src.type());
for (int j = 0; j < src.cols; ++j){
for (int i = 0; i < src.rows; ++i){
uint32_t sum[3] = { 0 };
for (int r = -boder; r <= boder; ++r){
int new_i = i + r < 0 ? -(i + r) : (i + r < src.rows ? i + r : 2 * (src.rows - 1) - i - r);
if (src.channels() == 1){
sum[0] = sum[0] + (uint32_t)buf[new_i][j * 3 + 0] * (uint32_t)kernel[r + boder]; //列不变行变
}
else if (src.channels() == 3){
sum[0] = sum[0] + (uint32_t)buf[new_i][j * 3 + 0] * (uint32_t)kernel[r + boder];//B
sum[1] = sum[1] + (uint32_t)buf[new_i][j * 3 + 1] * (uint32_t)kernel[r + boder];//G
sum[2] = sum[2] + (uint32_t)buf[new_i][j * 3 + 2] * (uint32_t)kernel[r + boder];//R
}
}
// std::cout << cv::saturate_cast<uint8_t>((sum[0] + ((1 << 16) >> 1)) >> 16) << std::endl;
// std::cin.get();
if (src.channels() == 1){
dst.at<uchar>(i, j) = cv::saturate_cast<uint8_t>(sum[0]);
}
else if (src.channels() == 3){
cv::Vec3b rgb = { cv::saturate_cast<uint8_t>((sum[0] + ((1 << 16) >> 1)) >> 16),
cv::saturate_cast<uint8_t>((sum[1] + ((1 << 16) >> 1)) >> 16),
cv::saturate_cast<uint8_t>((sum[2] + ((1 << 16) >> 1)) >> 16) };
dst.at<cv::Vec3b>(i, j) = rgb;
}
}
}
卷积时进行了定点优化,需要注意饱和度转换,因为计算的中间结果很可能大于255。
定点转换的原理
浮点数转换为定点数
val = _val.getSign() ? 0 : (uint16_t)cvRound(_val * cv::softdouble((int32_t)(1 << fixedShift)));
1、如果浮点数 val 为 0,则将定点数设为 0;
2、1 << fixedShift 是定点数表示中的缩放因子,它将浮点数乘以 2^fixedShift,即对应于将小数部分的 fixedShift 位移到整数部分。
3、接下来,使用 cvRound 函数对结果进行四舍五入,将其转换为整数。
4、将转换后的整数值赋给 val
定点数转换为 uint8_t, 即 CV_8U
static CV_ALWAYS_INLINE uint16_t fixedround(const uint16_t& _val) { return (_val + ((1 << fixedShift) >> 1)); }
cv::saturate_cast<ET>(fixedround(val) >> fixedShift);
fixedround函数将定点数 _val 进行四舍五入取整,并返回一个 uint16_t 类型的结果。在定点数表示中,小数部分的位数由 fixedShift 定义。这个函数的目的是在 _val 的小数部分上加上一个适当的偏移量,以实现四舍五入的效果。具体来说,假设 fixedShift 为 n,那么 (1 << fixedShift) 表示 2^n,即定点数的精度。该函数中的 (1 << fixedShift) >> 1 表示将精度减半,也就是将小数部分的最高位右移一位,这相当于将原来定点数的小数部分加上0.5的效果。函数将 _val 加上这个半精度偏移量,并返回结果,从而实现四舍五入的效果。
第二行代码负责将定点数转换我 CV_8U 类型,因为 fixedShift 为小数部分,所有右移 fixedShift 位,即将小数部分丢弃,只保留整数部分。然后使用饱和度算法将定点数转换为指定的目标类型,即CV_8U。