一、滤波卷积基础原理
1、卷积处理过程
空间滤波的理论基础是空间卷积和空间相关,参考信号处理中频域处理的原理,允许一定频率的分量通过,从而过滤掉一部分不需要的频率分量。(详见《信号系统》)在图像处理中,空间滤波就是针对特定的一个像素,使用周围领域的像素对其进行卷积等操作,最终产生新的颜色值作为特定像素的颜色值。 在滤波处理过程中的流程:
1)将当前像素作为中心像素点,对应滤波器模板的中心点;
2)对周边领域的像素点的颜色值与模板中权值相乘后进行累加,作为新的颜色值
3)图像中的每一个像素点,都按照1~2两个步骤进行处理
其中的领域我们通常选择 3*3、5*5、7*7大小的区域,这个区域我们通常称之为 卷积模板 或者 滤波器模板。这里 f(x,y) 是以(x,y)为坐标的像素值,而w(s,t)我们则看成卷积滤波器。
使用卷积的表达方式:
简单的理解:某个像素点的颜色值,依赖于当前像素点颜色值与其周围像素点颜色值进行线性累加后得到新值。
卷积模板的选择:
I)模板大小。我们通常使用奇数作为卷积模板的宽高值,这样便于对称处理。并且卷积模板的宽高通常一样,也即通常使用正方形卷积模板, a=b。
II)模板系数。由于我们卷积处理只是希望在原先图像基础上做一些基本调整处理,所以通常模板中所有卷积系数之和为1,即:
2、边缘像素的卷积处理
图像卷积过程可以看成使用一个模板窗口,其中红色中心点对应原始图像每个像素不断的处理。但是对于图像边缘的像素点进行卷积操作时,卷积模板的区域会超过图像本身内容区域,这个时候如何选取领域像素?通常是采用填充补边的方式,即超出区域直接使用边缘像素替代即可。
例如:在如下的图像中,使用5*5大小的卷积模板,当对于图像第一行像素进行卷积操作时,w(x-1, y-1); w(x-2, y-2); 等权值都没有对应的领域像素,那么就直接用最上面一样像素值替代即可。
3、卷积处理实现代码:
如下是图像卷积的通用操作代码(这里直接是浮点计算)
//
// 功能: 卷积操作
// 参数: pInBmp : 卷积输入图像
// lWndSize: 模板窗口大小,这里就是模板的宽和高,当前只支持宽高一致的情况,需要是奇数
// lUnitStride : 卷积模板中每个元素对应的像素长度,默认为1
// pTemplate: 卷积模板数据,其数据大小为 (lWndSize*lWndSize)
// pOutBmp: 卷积结果输出图像
// 返回:错误值
//
XRESULT XBmpConvolute(
XBITMAP* pInBmp,
XLONG lWndSize,
XLONG lUnitStride,
XFLOAT* pTemplateData,
XBITMAP* pOutBmp )
{
XLONG lTemplateSize = lWndSize * lWndSize; // 模板元素数量
XLONG lHalfWndSize = lWndSize / 2; // 半个窗口的宽高
XLONG lPaddingPxls = lHalfWndSize * lUnitStride;
XPOINT* pLocateOffset = XNULL; // 各个卷积元素的偏移位置
XBYTE* pOutLine = XNULL;
XBYTE* pOutData = XNULL;
XLONG lPxlBytes = 0;
XLONG i, j, k;
XLONG x, y, z;
XRESULT res = XOK;
if( (pInBmp->lWidth != pOutBmp->lWidth)
|| (pInBmp->lHeight != pOutBmp->lHeight)
|| (pInBmp->dwPixelArrayFormat != pOutBmp->dwPixelArrayFormat) )
{
return XERR_INVALID_PARAM;
}
lPxlBytes = XBmpGetPxlBytes(pInBmp->dwPixelArrayFormat);
if (lPxlBytes < 3) // 这里只考虑24位色或者32位色的图像,对于Gray灰度图稍作调整即可
{
return XERR_INVALID_PARAM;
}
// 计算每个卷积元素相对于中心的偏移位置
pLocateOffset = (XPOINT*)XMemAlloc(XNULL, lTemplateSize*sizeof(XPOINT));
XASSERT(pLocateOffset);
z = 0;
for (y = -lHalfWndSize; y <= lHalfWndSize; y++)
{
for (x = -lHalfWndSize; x <= lHalfWndSize; x++)
{
pLocateOffset[z].x = x * lUnitStride;
pLocateOffset[z].y = y * lUnitStride;
z++;
}
}
//
// 对输入图像中每个像素点进行卷积操作,结果保存到输出图像中
//
pOutLine = pOutBmp->pPlane[0];
for (i = 0; i < pOutBmp->lHeight; i++)
{
pOutData = pOutLine;
for (j = 0; j < pOutBmp->lWidth; j++)
{
XFLOAT fSumR = 0;
XFLOAT fSumG = 0;
XFLOAT fSumB = 0;
for (k = 0; k < lTemplateSize; k++)
{
// 计算模板元素对应在原图中的像素位置, 超出边缘的直接以边缘图像数据为准
y = j + lPaddingPxls + pLocateOffset[k].y;
x = i + lPaddingPxls + pLocateOffset[k].x;
if (x < 0) x = 0;
if (x >= pInBmp->lHeight) x = pInBmp->lHeight - 1;
if (y < 0) y = 0;
if (y > pInBmp->lWidth) y = pInBmp->lWidth - 1;
// 找到模板元素对应的原图中像素数据
XBYTE* pSourceData = pInBmp->pPlane[0] + (x * pInBmp->lPitch[0]) + (y * lPxlBytes);
// 进行卷积操作
fSumR += (XFLOAT)pSourceData[0] * pTemplateData[k];
fSumG += (XFLOAT)pSourceData[1] * pTemplateData[k];
fSumB += (XFLOAT)pSourceData[2] * pTemplateData[k];
}
// 将输出RGB颜色值控制在 [0, 255]值域内
pOutData[0] = (XBYTE)(MAX(MIN(255, fSumR), 0));
pOutData[1] = (XBYTE)(MAX(MIN(255, fSumG), 0));
pOutData[2] = (XBYTE)(MAX(MIN(255, fSumB), 0));
pOutData += lPxlBytes;
}
pOutLine += pOutBmp->lPitch[0];
}
return XOK;
}
//
// 具体调用尝试
//
void TestConvolute()
{
XBITMAP* pRgbBmp = XNULL;
XBITMAP* pConvoluteBmp = XNULL;
// 加载原始输入图像
XBmpLoad("d:/baby.bmp", &pRgbBmp);
// 创建一个卷积输出的图像
pConvoluteBmp = XBmpAlloc(pRgbBmp->dwPixelArrayFormat, pRgbBmp->lWidth, pRgbBmp->lHeight);
// 这个一个 5*5窗口大小的卷积模板
// 实际上是LOG锐化算子的模板,具体参数意义后续详细讲解
XFLOAT szTemplate[25] = {
-0.0357f, -0.0714f, -0.0714f, -0.0714f, -0.0357f,
-0.0714f, 0.0000f, 0.1429f, 0.0000f, -0.0714f,
-0.0714f, 0.1429f, 1.4286f, 0.1429f, -0.0714f,
-0.0714f, 0.0000f, 0.1429f, 0.0000f, -0.0714f,
-0.0357f, -0.0714f, -0.0714f, -0.0714f, -0.0357f
};
// 卷积操作
XBmpConvolute(pRgbBmp, 5, 1, szTemplate, pConvoluteBmp);
// 保存卷积结果
XBmpSave(pConvoluteBmp, "d:/baby_sharp.bmp");
}
4、卷积过程的优化处理:
上面的卷积处理过程中,可以看到整个时间复杂度是 O(width*height*templateSize),并且每个像素的处理是浮点计算,比较消耗性能。通常会将卷积模板中的系数由浮点转换为定点整数处理,即:将卷积模板中的系数全部使用整型值表示,在卷积完成累加计算后统一除以相应的一个整数,这样就可以加速计算速度,同时对于卷积模板系数的表示也更加清晰。
整型计算优化后的代码
//
// 功能: 卷积操作
// 参数: pInBmp : 卷积输入图像
// lWndSize: 模板窗口大小,这里就是模板的宽和高,当前只支持宽高一致的情况,需要是奇数
// lUnitStride : 卷积模板中每个元素对应的像素长度,默认为1
// pTemplate: 卷积模板数据,整型数据,其数据大小为 (lWndSize*lWndSize)
// pOutBmp: 卷积结果输出图像
// 返回:错误值
//
XRESULT XBmpConvoluteInt(
XBITMAP* pInBmp,
XLONG lWndSize,
XLONG lUnitStride,
XLONG* pTemplateData,
XBITMAP* pOutBmp)
{
XLONG lTemplateSize = lWndSize * lWndSize; // 模板元素数量
XLONG lHalfWndSize = lWndSize / 2; // 半个窗口的宽高
XLONG lPaddingPxls = lHalfWndSize * lUnitStride;
XPOINT* pLocateOffset = XNULL; // 各个卷积元素的偏移位置
XBYTE* pOutLine = XNULL;
XBYTE* pOutData = XNULL;
XLONG lCoeffSum = 0; // 统计系数累加和
XLONG lPxlBytes = 0;
XLONG i, j, k;
XLONG x, y, z;
XRESULT res = XOK;
if ((pInBmp->lWidth != pOutBmp->lWidth)
|| (pInBmp->lHeight != pOutBmp->lHeight)
|| (pInBmp->dwPixelArrayFormat != pOutBmp->dwPixelArrayFormat))
{
return XERR_INVALID_PARAM;
}
lPxlBytes = XBmpGetPxlBytes(pInBmp->dwPixelArrayFormat);
if (lPxlBytes < 3)
{
return XERR_INVALID_PARAM;
}
// 计算每个卷积元素相对于中心的偏移位置
pLocateOffset = (XPOINT*)XMemAlloc(XNULL, lTemplateSize * sizeof(XPOINT));
XASSERT(pLocateOffset);
z = 0;
for (y = -lHalfWndSize; y <= lHalfWndSize; y++)
{
for (x = -lHalfWndSize; x <= lHalfWndSize; x++)
{
pLocateOffset[z].x = x * lUnitStride;
pLocateOffset[z].y = y * lUnitStride;
lCoeffSum += pTemplateData[z]; // 统计模板系数之和
z++;
}
}
// 输入图像中每个像素点进行卷积操作
pOutLine = pOutBmp->pPlane[0];
for (i = 0; i < pOutBmp->lHeight; i++)
{
pOutData = pOutLine;
for (j = 0; j < pOutBmp->lWidth; j++)
{
XLONG lSumR = 0;
XLONG lSumG = 0;
XLONG lSumB = 0;
for (k = 0; k < lTemplateSize; k++)
{
// 计算模板元素对应在原图中的像素位置, 超出边缘的直接以边缘图像数据为准
y = j + lPaddingPxls + pLocateOffset[k].y;
x = i + lPaddingPxls + pLocateOffset[k].x;
if (x < 0) x = 0;
if (x >= pInBmp->lHeight) x = pInBmp->lHeight - 1;
if (y < 0) y = 0;
if (y > pInBmp->lWidth) y = pInBmp->lWidth - 1;
// 找到模板元素对应的原图中像素数据
XBYTE* pSourceData = pInBmp->pPlane[0] + (x * pInBmp->lPitch[0]) + (y * lPxlBytes);
// 进行卷积操作
lSumR += pSourceData[0] * pTemplateData[k];
lSumG += pSourceData[1] * pTemplateData[k];
lSumB += pSourceData[2] * pTemplateData[k];
}
// 卷积累加后统一整除系数之和,进行正常化处理
lSumR /= lCoeffSum;
lSumG /= lCoeffSum;
lSumB /= lCoeffSum;
pOutData[0] = (XBYTE)(MAX(MIN(255, lSumR), 0));
pOutData[1] = (XBYTE)(MAX(MIN(255, lSumG), 0));
pOutData[2] = (XBYTE)(MAX(MIN(255, lSumB), 0));
pOutData += lPxlBytes;
}
pOutLine += pOutBmp->lPitch[0];
}
return XOK;
}
//
// 具体调用尝试
//
void TestConvolute()
{
XBITMAP* pRgbBmp = XNULL;
XBITMAP* pConvoluteBmp = XNULL;
// 加载原始输入图像
XBmpLoad("d:/baby.bmp", &pRgbBmp);
// 创建一个卷积输出的图像
pConvoluteBmp = XBmpAlloc(pRgbBmp->dwPixelArrayFormat, pRgbBmp->lWidth, pRgbBmp->lHeight);
// 这个一个 5*5窗口大小的卷积模板,与上面例子系数一样,只是转成整型数据
// 实际上是LOG锐化算子的模板,具体参数意义后续详细讲解
XLONG szTemplateInt[25] = {
-2, -4, -4, -4, -2,
-4, 0, 8, 0, -4,
-4, 8, 80, 8, -4,
-4, 0, 8, 0, -4,
-2, -4, -4, -4, -2
};
// 卷积操作
XBmpConvoluteInt(pRgbBmp, 5, 1, szTemplateInt, pConvoluteBmp);
// 保存卷积结果
XBmpSave(pConvoluteBmp, "d:/baby_sharp.bmp");
}
二、平滑滤波
平滑滤波器主要用于进行模糊处理和降低图像中的噪声,通常在图像预处理时使用,例如:将图像中的一些轻微的污点、缝隙等像素点通过平滑来掩盖掉。从灰度图像的3D坐标中可以知道,所谓平滑就是:尽可能减少当前像素点的颜色值 与 周边像素点颜色值的差异。
当前像素点颜色值比较凸出时,将其截断一部分平摊到周边像素点;
当前像素点颜色值比较凹陷时,使用周边的像素颜色值分别截断一些来填充;
常见的平滑滤波器有:
1、均值滤波器:
当前像素的颜色值由周围像素颜色值取平均值。常用的3*3的卷积模板如下:
通过均值滤波后的图像可以取得比较好的平滑效果,特别是卷积模板窗口越大其平滑效果越好,但是也带来一个问题:过多平滑后图像显得比较模糊。
2、中值滤波器
将周围卷积窗口区域内像素进行排序,找到中间的颜色值直接替换当前像素颜色值。中值滤波的实现不方便直接使用系数乘法,而是要将卷积窗口内的像素值进行排序操作,然后取中间的颜色值。
例如:使用3*3的卷积窗口,对上面的红色像素点的数据进行中值滤波。对应的像素值为:140、131、132、151、251、142、101、146、108,经过按照从小到大排序后:101、108、131、132、140、142、146、151、251,那么就取中间值140直接来替换,经过中值滤波后的数据如下
根据不同的场景,中值滤波还可以有多种变化,例如:最大值滤波(选取排序后的最大值)、最小值滤波(选取排序后的最小值)。
中值滤波的主要特点:
1)对大的边缘高度,中值滤波较邻域均值好,对于较小边缘高度,两种滤波有很少的差别
2)中值滤波是非线性的,整个处理过程也稍微麻烦一些
3)在抑制图像随机脉冲噪声方面特别是椒盐噪声等比较好,去除孤立线或污点效果比较好
3、高斯滤波器
前面提到在均值滤波处理经过均值平滑处理后的图像会有比较模糊的问题,那么采用高斯平滑滤波则可以相对减少这种模糊程度。如下是一个3*3的简单的高斯卷积模板,对比均值模板同样实现了平均的效果,但是高斯卷积模板考虑了卷积像素点 与 当前中心像素点的距离关系,越接近中心像素点权值越高,距离中心像素点越远则权值越低,从直观角度也可以理解,越是靠近中心像素点的邻域像素内容通常也是越接近中心点的内容。
那么根据不同的权重来计算到卷积滤波器呢,接下来我们重点讲解高斯卷积模板的生成原理。
在数学上有一维高斯分布函数,其基本函数和公式如下
这里σ是标准差,代表着数据的离散程度,如果σ较小,那么生成的模板的中心系数较大,而周围的系数较小,这样对图像的平滑效果就不是很明显;反之,σ较大,则生成的模板的各个系数相差就不是很大。从函数图像上来看σ越大,则图形越宽,尖峰越小,图形较为平缓;σ越小,则图形越窄,越集中,中间部分也就越尖,图形变化比较剧烈。
对于二维高斯分布函数,也是类似的图形,只是在3D空间坐标系中,同样σ是标准差,可以调整概率分布程度。
利用二维高斯分布函数,我们可以用来生成我们的高斯卷积模板。其中(x,y)坐标就相当于我们卷积模板的两个方向,高斯坐标原点相当于卷积模板的中心元素,然后对于卷积模板中其他元素作为原点中心的领域值进行计算
浮点卷积模板生成代码
void XBmpCalcuGaussCoeff(
XLONG lWndSize,
XFLOAT fSigma,
XDOUBLE* pGaussCoeff )
{
XLONG lHalfWndSize = lWndSize / 2;
XLONG x, y, z;
XDOUBLE fSigma2 = 2.0 * fSigma * fSigma;
XDOUBLE fConstCoeff = sqrtf(2.0 * X_PI) * fSigma;
XDOUBLE fCoeffSum = 0;
XDOUBLE fElmSum = 0;
z = 0;
for (y = -lHalfWndSize; y <= lHalfWndSize; y++)
{
for (x = -lHalfWndSize; x <= lHalfWndSize; x++)
{
pGaussCoeff[z] = exp(-(x*x + y*y) / fSigma2) / fConstCoeff;
fCoeffSum += pGaussCoeff[z];
z++;
}
}
// 所有系数归一化操作
z = 0;
for (y = -lHalfWndSize; y <= lHalfWndSize; y++)
{
for (x = -lHalfWndSize; x <= lHalfWndSize; x++)
{
pGaussCoeff[z] /= fCoeffSum;
fElmSum += pGaussCoeff[z];
z++;
}
}
// 最后统计到的所有元素之和 fElmSum应该是1.0
}
这是使用Sigma=0.8 窗口大小为5生成的卷积模板值
三、锐化滤波
锐化滤波主要是为了突出图像内容中对象边缘等处理
总结
空域滤波主要是采用卷积模板的方式对图像进行处理
文章系列目录
华叔-视觉魔术师:图像算法原理与实践——绪论zhuanlan.zhihu.com