中值滤波就是是基于排序统计理论的一种能有效抑制噪声的非线性信号处理技术,它的基本原理是把数字图像或数字序列中一点的值用该点的一个邻域中各点值的中值代替,让周围的像素值接近的值,从而消除孤立的噪声点。中值滤波一般使用模板的方法实现,对模板内的像素按照像素值的大小进行排序,生成单调上升(或下降)的二维数据序列,并使用下面的公式进行输出:
g(x,y)=med{f(x-m,y-n),(m,n∈W)}
其中,f(x,y)表示原始的图像,而g(x,y)表示处理后的图像。
中值滤波一般使用二维模板,滤波窗口通常为 3*3,5*5,7*7 区域,实际使用中,我们常常放大窗口长度,选取最合适的直到滤波效果满意为止,对于缓变长轮廓物体一般采用方形和圆形,对于尖角形一般采用十字形窗口。后面的程序采用 3*3 矩形区域。实现方法是通过从图像中的某个采样窗口取出奇数个数据进行排序。用排序后的中值取代要处理的数据即可。
中值滤波对椒盐噪声的抑制效果好,在抑制随机噪声的同时能有效保护边缘少受模糊。但它对点、线等细节较多的图像却不太合适。对中值滤波法来说,正确选择窗口尺寸的大小是很重要的环节。一般很难事先确定最佳的窗口尺寸,需通过从小窗口到大窗口的中值滤波试验,再从中选取最佳的。
OpenCV函数 medianBlur 执行中值滤波操作:
/************************************************************************* * * 函数名称: * MedianFilter() * * 参数: * LPSTR lpDIBBits - 指向源DIB图像指针 * LONG lWidth - 源图像宽度(象素数) * LONG lHeight - 源图像高度(象素数) * int iFilterH - 滤波器的高度 * int iFilterW - 滤波器的宽度 * int iFilterMX - 滤波器的中心元素X坐标 * int iFilterMY - 滤波器的中心元素Y坐标 * * 返回值: * BOOL - 成功返回TRUE,否则返回FALSE。 * * 说明: * 该函数对DIB图像进行中值滤波。 * ************************************************************************/ BOOL WINAPI MedianFilter(LPSTR lpDIBBits, LONG lWidth, LONG lHeight, int iFilterH, int iFilterW, int iFilterMX, int iFilterMY) { // 指向源图像的指针 unsigned char* lpSrc; // 指向要复制区域的指针 unsigned char* lpDst; // 指向复制图像的指针 LPSTR lpNewDIBBits; HLOCAL hNewDIBBits; // 指向滤波器数组的指针 unsigned char * aValue; HLOCAL hArray; // 循环变量 LONG i; LONG j; LONG k; LONG l; // 图像每行的字节数 LONG lLineBytes; // 计算图像每行的字节数 lLineBytes = WIDTHBYTES(lWidth * 8); // 暂时分配内存,以保存新图像 hNewDIBBits = LocalAlloc(LHND, lLineBytes * lHeight); // 判断是否内存分配失败 if (hNewDIBBits == NULL) { // 分配内存失败 return FALSE; } // 锁定内存 lpNewDIBBits = (char * )LocalLock(hNewDIBBits); // 初始化图像为原始图像 memcpy(lpNewDIBBits, lpDIBBits, lLineBytes * lHeight); // 暂时分配内存,以保存滤波器数组 hArray = LocalAlloc(LHND, iFilterH * iFilterW); // 判断是否内存分配失败 if (hArray == NULL) { // 释放内存 LocalUnlock(hNewDIBBits); LocalFree(hNewDIBBits); // 分配内存失败 return FALSE; } // 锁定内存 aValue = (unsigned char * )LocalLock(hArray); // 开始中值滤波 // 行(除去边缘几行) for(i = iFilterMY; i < lHeight - iFilterH + iFilterMY + 1; i++) { // 列(除去边缘几列) for(j = iFilterMX; j < lWidth - iFilterW + iFilterMX + 1; j++) { // 指向新DIB第i行,第j个象素的指针 lpDst = (unsigned char*)lpNewDIBBits + lLineBytes * (lHeight - 1 - i) + j; // 读取滤波器数组 for (k = 0; k < iFilterH; k++) { for (l = 0; l < iFilterW; l++) { // 指向DIB第i - iFilterMY + k行,第j - iFilterMX + l个象素的指针 lpSrc = (unsigned char*)lpDIBBits + lLineBytes * (lHeight - 1 - i + iFilterMY - k) + j - iFilterMX + l; // 保存象素值 aValue[k * iFilterW + l] = *lpSrc; } } // 获取中值 * lpDst = GetMedianNum(aValue, iFilterH * iFilterW); } } // 复制变换后的图像 memcpy(lpDIBBits, lpNewDIBBits, lLineBytes * lHeight); // 释放内存 LocalUnlock(hNewDIBBits); LocalFree(hNewDIBBits); LocalUnlock(hArray); LocalFree(hArray); // 返回 return TRUE; } /************************************************************************* * * 函数名称: * GetMedianNum() * * 参数: * unsigned char * bpArray - 指向要获取中值的数组指针 * int iFilterLen - 数组长度 * * 返回值: * unsigned char - 返回指定数组的中值。 * * 说明: * 该函数用冒泡法对一维数组进行排序,并返回数组元素的中值。 * ************************************************************************/ unsigned char WINAPI GetMedianNum(unsigned char * bArray, int iFilterLen) { // 循环变量 int i; int j; // 中间变量 unsigned char bTemp; // 用冒泡法对数组进行排序 for (j = 0; j < iFilterLen - 1; j ++) { for (i = 0; i < iFilterLen - j - 1; i ++) { if (bArray[i] > bArray[i + 1]) { // 互换 bTemp = bArray[i]; bArray[i] = bArray[i + 1]; bArray[i + 1] = bTemp; } } } // 计算中值 if ((iFilterLen & 1) > 0) { // 数组有奇数个元素,返回中间一个元素 bTemp = bArray[(iFilterLen + 1) / 2]; } else { // 数组有偶数个元素,返回中间两个元素平均值 bTemp = (bArray[iFilterLen / 2] + bArray[iFilterLen / 2 + 1]) / 2; } // 返回中值 return bTemp; }
从中值滤波的原理可以看出,平滑后图像的每个像素的值只与原图该点其邻域的像素值有关,并且每个像素值的中值滤波处理算法是完全相同的。并且算法为只涉及局部运算
的像素级处理,并且处理的数据量巨大,局部数据之间的相关性小,没有涉及知识推理和人工干预,算法本身的并行化程度很高,并且 CUDA 的 SIMT(单指令多线程)并行流式程序处理模式在进行这样的像素级的图像处理时具有明显的优势。设计好内核运行函数,使平滑图像中的每一个像素对应一个由一个线程产生。由此首先设计 kernel 函数MedianFilter 。
_global_ void MedianFilter(int In[N][N],int Out[N][N],int Width,int Height) { int window[9]; unsigned int x=blockIdx.x*blockDim.x+threadIdx.x; unsigned int y=blockIdx.y*blockDim.y+threadIdx.y; if(x>= Width && y>= Height) return; window[0]=(y==0||x==0)?0:In[(y-1)* Width +x-1]; window[1]=(y==0)?0:In[(y-1)* Width +x]; window[2]=(y==0||x==DATA_W-1)? 0:In[(y-1)* Width +x+1]; window[3]=(x==0)? 0:In[y* Width +x-1]; window[4]= In[y* Width +x]; window[5]=(x==DATA_W-1)? 0:In[y* Width +x+1]; window[6]=(y==DATA_H-1||x==0)? 0:In[(y+1)* Width +x-1]; window[7]=(y==DATA_H-1)? 0:In[(y+1)* Width +x]; window[8]=(y==DATA_H-1||x==DATA_W-1)? 0:In[(y+1)* Width +x+1]; for (unsigned int j=0; j<5; ++j) { int min=j; for (unsigned int l=j+1; l<9; ++l) if (window[l] < window[min]) min=l; const float temp=window[j]; window[j]=window[min]; window[min]=temp; } Out[y* Width + x]=window[4]; }说明:
根据 CUDA 程序定义的语法,_global_ 修饰符声明函数 MedianFilter 为kernel 函数,表明此类函数在设备上执行,仅可通过宿主调用。函数 MedianFilter 有四个
形参:int In[N][N]定义了指向输入的待处理的图像数据指针, int Out[N][N] 定义了指向输出的处理后的图像数据指针,int Width 定义了待处理的图像的宽度,int Height 定义
了待处理的图像的高度。int window[9]表示为线程定义了一个整形数组用于排序。int x和 y 求出当前线程的坐标,确定我们用该线程在生成结果像素的坐标。if 的判断线程超出
边界说明坐标不合法,退出。window[0]到 window[8] 读取当前点及其 8 邻域像素值。for循环对当前点及其 8 邻域像素值排序,最后输出中值滤波结果。
主机端代码如下
void main ( ) { //读入待锐化图像文件并初始化主机; //将图像数据传输到设备; dim3 dimBlock (16,16) ; dim3 dimGrid((Width+dimBlock.x–1)/dimBlock.x,(Height+dimBlock.y– 1)/dimBlock.y); clock_t begin = clock ( ) ;, end; MedianFilter<<<dimGrid,dimBlock>>>(inlp,outlp,Width, Height); cudaThreadSynchronize(); double cost; end = clock(); cost = (double)(end - begin) / CLOCKS_PER_SEC; }