图像处理里面比较基本的操作是在空间域的滤波处理。最常见的模糊啊锐化啊的都可以归于这类。其实质就是邻域间的组合运算,在shader技术上的乒乓也就差不多这个样子,而且操作纹理要更简捷。——ZwqXin.com
本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
原文地址:http://www.zwqxin.com/archives/image-processing/image-process-spatial-domain-filter.html
比较直方图类那种“单点”操作,空间域滤波中的每个像素都得多少顾及一下邻里情况,因而依其关注面大小而出现了不同大小的“邻域”的定义。至于不同的空间域滤波对应不同类型的矩阵式,就是所谓的“滤波”(FILTER),或者说模板。简单的就是3*3大小的高斯平滑,中值平滑,一阶梯度锐化/边缘,二阶拉普拉斯锐化/边缘等等,还有实现各种不同效果的FILTER,可上网找找,其实运用都是差不多的,但是要得出一个“有价值”的FILTER,可不是随随便便就能行的。
在[基于亮度的图像二值化处理] 里对8BIT,16BIT,24/32BIT的BMP分别应用“单点”的计算,一提取BMP数据区的像素信息,马上就作转换操作了,而在“邻域”计算里,把全部像素信息提取出来后,再进行转换操作比较合适:
- void SpatialDomainTemplate::BmpProcess24_32(OperateBMP& image, int Hdis, int Wdis, int step)
- {
- int index;
- int Wmax = (Wdis-1)*step;
- FilterColor *color;
- FilterColor *result_color;
- color = new FilterColor[Hdis*Wdis];
- result_color = new FilterColor[Hdis*Wdis];
- memset(color, 0, Hdis*Wdis*sizeof(FilterColor));
- memset(result_color, 0, Hdis*Wdis*sizeof(FilterColor));
- for(int j = 0; j < Hdis; j++)
- for(int i = 0; i < Wdis; i++)
- {
- index = j*Wdis + i;
- color[index].blue = image.GetImageData().buffer[i*step + 0 + Wmax * j];
- color[index].green = image.GetImageData().buffer[i*step + 1 + Wmax * j];
- color[index].red = image.GetImageData().buffer[i*step + 2 + Wmax * j];
- }
- ApplyTemplate(color, result_color, Hdis, Wdis);
- if(GetReadBack())
- {
- for(int k = 0; k < Hdis; k++)
- for(int l = 0; l < Wdis; l++)
- {
- index = k*Wdis + l;
- image.GetImageData().buffer[l*step + 0 + Wmax * k] = result_color[index].blue;
- image.GetImageData().buffer[l*step + 1 + Wmax * k] = result_color[index].green;
- image.GetImageData().buffer[l*step + 2 + Wmax * k] = result_color[index].red;
- }
- }
- delete []color;
- delete []result_color;
- }
这是24/32BIT版本,其他BIT的可参照上篇文章,类似的。color,resultcolor数组分别作为操作函数ApplyTemplate的输入和输出,模板操作就在此函数里完成——它无关BMP是多少BIT的,进来的是一个代表原图像的RGB数组,出来的是代表结果图像的RGB数组,就这样。当然可以看出为了结构我还是牺牲了不少“效率”的。
- void SpatialDomainTemplate::ApplyTemplate(FilterColor* oColor, FilterColor* rColor, int Hdis, int Wdis)
- {
- if(FilterType == AverageBlur)
- ApplyAverageBlur(oColor, rColor, Hdis, Wdis);
- if(FilterType == MedianBlur)
- ApplyMedianBlur(oColor, rColor, Hdis, Wdis);
- if(FilterType == FirstOrderSharp)
- ApplyFirstOrderSharp(oColor, rColor, Hdis, Wdis);
- if(FilterType == SecondOrderSharp)
- ApplySecondOrderSharp(oColor, rColor, Hdis, Wdis);
- if(FilterType == SelfDefine)
- ApplySelfDefine(oColor, rColor, Hdis, Wdis);
- }
根据客户端的要求,调用不同的FILTER。举最后一个(自定义)为例:
- void SpatialDomainTemplate::ApplySelfDefine(FilterColor* oColor, FilterColor* rColor, int Hdis, int Wdis)
- {
- for(int j = 0; j < Hdis; j++)
- for(int i = 0; i < Wdis; i++)
- {
- AdjustImageEdge(i, j, Wdis, Hdis);
- rColor[Index[1][1]]
- = oColor[ Index[0][0] ] * templateParam[2]
- + oColor[ Index[0][1] ] * templateParam[3]
- + oColor[ Index[0][2] ] * templateParam[4]
- + oColor[ Index[1][0] ] * templateParam[5]
- + oColor[ Index[1][1] ] * templateParam[6]
- + oColor[ Index[1][2] ] * templateParam[7]
- + oColor[ Index[2][0] ] * templateParam[8]
- + oColor[ Index[2][1] ] * templateParam[9]
- + oColor[ Index[2][2] ] * templateParam[10];
- rColor[Index[1][1]] = rColor[Index[1][1]] * templateParam[0] / templateParam[1];
- if(rColor[Index[1][1]].blue < 0) rColor[Index[1][1]].blue = 0;
- if(rColor[Index[1][1]].green < 0)rColor[Index[1][1]].green = 0;
- if(rColor[Index[1][1]].red < 0) rColor[Index[1][1]].red = 0;
- }
- }
对每个像素,以它为中心的3邻域每个邻接像素都乘以一个系数,相加后结果乘以一个主调节量,判断边界后得到该元素的结果。加上类型转换的截断,可以保证结果在0~255之间——为免去计算过程越界的危险,FilerColor间是整型的运算:
- class FilterColor {
- public:
- int red;
- int green;
- int blue;
- ............
- }
只要最后结果的类型是UCHAR(或BYTE),VC6就能自动类型转换从INT转换到UCHAR。AdjustImageEdge函数是调节边界的,这里对于边界元素,它的那些“不存在的邻居”都被虚拟成一个与该像素等值的像素。边界问题总是那样需要掂量啊。
高斯平滑:
原图片:
处理后:
数学形态学(Mathematical Morphology),是分析几何形状和结构的数学方法,包括膨胀、腐蚀、开、闭等运算。在图像处理里面,用于“保持基本形状,去除不相关特征”——ZwqXin.com
什么意思呢?图像中有一个貌似正方形的区域,然后你通过处理,可以把“貌似”去掉;图像是一个不清晰的指纹,然后你通过处理,也把“不”字去掉;图像中有很多大小不一的圆形区域,然后你通过处理,找出大者祛除小者(去噪)......应用是很多的,但是没有一个完整的范畴,就是很多时候要做某种图像处理实际应用的时候,会想起它吧。
本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
原文地址:http://www.zwqxin.com/archives/image-processing/morphologic-process.html
因为处理的都是二值图像(用来分清目标和背景),因此有必要预先把图片转化为8BIT的二值图像,便于处理操作。我是把黑色像素作为目标区域,白色像素作为背景的(当然可以反相)。
结构元素:
其实就是自己定义的一块像素区域,形态学处理的时候用这个元素与原图像作与或运算,生成结果。
膨胀:
设A为原图像。B为结构元素(C为B的映像,没用),那么D的整块“灰+黑”区域就是新图像了(反映在图像里都是黑像素),可以看到图中用黑色标识的是“膨胀”出来的。它就是把结构元素B的中心(+)分别与原图像A“重合”,然后考虑结构元素里的“中心以外”的像素将在原图像中出现的位置,并与原图像该位置的像素进行0-1值的“或运算”(设黑色为0,白色为1,下同)
腐蚀:
腐蚀的解释与膨胀相同,不过执行的是“与运算”。如图,最终图像是C中的纯黑色部分。由此可见膨胀与腐蚀的关系与不同点。
至于“开”与“闭”,无非是结合膨胀腐蚀的二次操作。前者是先腐蚀后膨胀,后者是先膨胀后腐蚀。要注意的是第一次是用结构元素,第二次则是用结构元素的倒置(相当于矩阵的转置咯,这是为了最后图像的“平衡”,如果两次用同一个的话,结果只会是等同双重“膨胀”或“腐蚀”。注意,概念跟“映像”是不同 di,见下图,元素中心也换位置了):
开运算效果:
- 删除小物体;
–将物体拆分为小物体;
–平滑大物体边界而不明显改变它们的面积;
闭运算效果:
- 填充物体的小洞;
–连接相近的物体;
–平滑物体的边界而不明显改变它们的面积。
(前者为“开”,后者为“闭”)
[注:以上图片多转自冈萨雷斯那本图像处理的书,老师的教学用PPT中截取]
总的操作代码见下。
- void MorphologicProcess::BmpProcess8(OperateBMP& image, int Hdis, int Wdis)
- {
- GetMinimumStructureElement(MorphElement, LineGrid, StructLineGridX, StructLineGridY);
- if(MorphType == MorphErosion)
- MorphDilaEroProcess(image, Hdis, Wdis, MorphErosion);
- else if(MorphType == MorphDilation)
- MorphDilaEroProcess(image, Hdis, Wdis, MorphDilation);
- else if(MorphType == MorphOpen)
- {
- MorphDilaEroProcess(image, Hdis, Wdis, MorphErosion);
- GetRotInverseStructureElement();
- GenBinaryMorph(image);
- MorphDilaEroProcess(image, Hdis, Wdis, MorphDilation);
- GetRotInverseStructureElement();
- }
- else if(MorphType == MorphClose)
- {
- MorphDilaEroProcess(image, Hdis, Wdis, MorphDilation);
- GetRotInverseStructureElement();
- GenBinaryMorph(image);
- MorphDilaEroProcess(image, Hdis, Wdis, MorphErosion);
- GetRotInverseStructureElement();
- }
- }
在细节上不想多说了。难处不在形态学算法本身,而在“自定义结构元素”(MorphElement)这块,包括它与原图像进行形态学运算时必须要考虑的边界条件处理。有需要的同学可以留言给我[http://www.zwqxin.com]。(当然如果自己连算法怎么样也不懂,只是为了交作业交任务而索要的话,恕不理会- -)
(原始测试图片,处理对话框)
(自定义任意的结构元素,约定中间红点为结构元素中心)
(膨胀,处理结果图片)
(任意定义新的结构元素,作为腐蚀例子)
(腐蚀,处理结果图片)
开和闭运算就不截图了,免得一篇日志里图太多。
2009-4-27 / 2009-5-16