基于Simple Image Statistics(简单图像统计,SIS)的图像二值化算法。
这是个简单的算法,是全局二值算法的一种,算法执行速度快。
算法过程简单描述如下:
对于每一个像素,做如下处理
1、计算当前像素水平和垂直方向的梯度。 (two gradients are calculated |I(x + 1, y) - I(x - 1, y)| and |I(x, y + 1) - I(x, y - 1)|);
2、取两个梯度的最大值作为权重。(weight is calculated as maximum of two gradients);
3、更新权重的和。(sum of weights is updated (weightTotal += weight));
4、更新加权像素之和 (sum of weighted pixel values is updated (total += weight * I(x, y)));
之后,最终的阈值去加权像素之和和权重之和相除的值。
这个算法在 Image Processing Lab in c# 的代码中有相关的说明。
从实际的操作上讲,我认为二值处理应该只针对灰度图像进行处理,这样才意义明确,因此,我在代码中给出了判断一副图像是否是灰度图像的一个函数:
private bool IsGrayBitmap(Bitmap Bmp) { bool IsGray; if (Bmp.PixelFormat == PixelFormat.Format8bppIndexed) // .net中灰度首先必然是索引图像 { IsGray = true; if (Bmp.Palette.Entries.Length != 256) // 这个要求其实在PS中是不存在的 IsGray = false; else { for (int X = 0; X < Bmp.Palette.Entries.Length; X++) // 看看调色板的每一个分两值是不是都相等,且必须还要等于其在调色板中出现的顺序 { if (Bmp.Palette.Entries[X].R != X || Bmp.Palette.Entries[X].G != X || Bmp.Palette.Entries[X].B != X) { IsGray = false; break; } } } } else { IsGray = false; } return IsGray; }
实际上,在PS的概念中,灰度图像的调色板个数不一定是256,只要调色板的每个元素的分量值都相等,并且都等于其在调色板中出现的顺序,PS就认为他是灰度图像。
为了处理方便,我加入了一个将其他模式的图像转换为灰度模式图像的函数:
private Bitmap ConvertToGrayModeBitmap(Bitmap Bmp) { int X, Y, SrcStride, DestStride, Width, Height; byte* SrcData, DestData; BitmapData BmpData = Bmp.LockBits(new Rectangle(0, 0, Bmp.Width, Bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); Bitmap GrayBmp = new Bitmap(Bmp.Width, Bmp.Height, PixelFormat.Format8bppIndexed); ColorPalette Pal = GrayBmp.Palette; for (Y = 0; Y < Pal.Entries.Length; Y++) Pal.Entries[Y] = Color.FromArgb(255, Y, Y, Y); // 设置灰度图像的调色板 GrayBmp.Palette = Pal; // LockBits 在第一个参数和图像一样大,以及读取格式和原始一样的情况下,调用函数的时间为0,且每次调用后BitmapData的Scan0都相同,而在 // 其他的大部分情况下同样参数调用该函数返回的Scan0都不同,这就说明在在程序内部,GDI+为在创建图像时还是分配了和对应位图一样大小内存空间, // 这样我们就可以再加载时调用一次该函数,并记住Scan0的值,然后直接用指针操作这一片区域,就相当于操作了图像。而不用每次都LOCK和UNLOCK了 // 从这个层次上说,该函数和GetDibits类似。 BitmapData GrayBmpData = GrayBmp.LockBits(new Rectangle(0, 0, GrayBmp.Width, GrayBmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format8bppIndexed); Width = BmpData.Width; Height = BmpData.Height; SrcStride = BmpData.Stride; DestStride = GrayBmpData.Stride; // 这个值并不一定就等于width*height*色深/8 for (Y = 0; Y < Height; Y++) { SrcData = (byte*)BmpData.Scan0 + Y * SrcStride; // 必须在某个地方开启unsafe功能,其实C#中的unsafe很safe,搞的好吓人。 DestData = (byte*)GrayBmpData.Scan0 + Y * DestStride; for (X = 0; X < Width; X++) { *DestData = (byte)((*SrcData * 7472 + *(SrcData + 1) * 38469 + *(SrcData + 2) * 19595) >> 16); //这里可以有不同的算法 SrcData += 3; DestData++; } } Bmp.UnlockBits(BmpData); GrayBmp.UnlockBits(GrayBmpData); return GrayBmp; }
在很多人心目中所谓的灰度图像就是R=G=B这样的图像,只能说这些人还是门外汉,太不专业了。 这样的图像只能算是颜色分量相同的彩色图像罢了,再次予以纠正。
由于上述所描述的算法涉及到了图像的四领域,因此我们采用类似PhotoShop算法原理解析系列 - 风格化---》查找边缘 一文中的哨兵算法,对备份的图像扩充边界,扩充部分的数据以原始图像边界处的值填充。因为只涉及到了四领域,因此需要在图像宽度和高度上分别增加2个像素即可。
关于填充数据,我还是喜欢自己分配内存,而且我更倾向于直接使用API,这个可能与个人习惯有关吧,你们也可以按照自己的方式来处理。
private byte GetSimpleStatisticsThreshold(Bitmap GrayBmp) { int Width, Height, Stride, X, Y; int CloneStride, Ex, Ey; int Weight = 0; long SumWeight = 0; // 对于大图像这个数字会溢出,所以用long类型的变量 byte* Pointer, Scan0, CloneData; BitmapData GrayBmpData = GrayBmp.LockBits(new Rectangle(0, 0, GrayBmp.Width, GrayBmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed); Width = GrayBmp.Width; Height = GrayBmp.Height; Stride = GrayBmpData.Stride; CloneStride = Width + 2; Scan0 = (byte*)GrayBmpData.Scan0; CloneData = (byte*)GlobalAlloc(GPTR, CloneStride * (Height * 2)); for (Y = 0; Y < Height; Y++) { *(CloneData + (Y + 1) * CloneStride) = *(Scan0 + Y * Stride); // 填充左侧第一列像素(不包括第一个和最后一个点) CopyMemory(CloneData + CloneStride * (Y + 1) + 1, Scan0 + Y * Stride, Width); *(CloneData + (Y + 1) * CloneStride + Width + 1) = *(Scan0 + Y * Stride + Width - 1); // 填充最右侧那一列的数据 } CopyMemory(CloneData, CloneData + CloneStride, CloneStride); // 第一行 CopyMemory(CloneData + (Height + 1) * CloneStride, CloneData + Height * CloneStride, CloneStride); // 最后一行 for (Y = 0; Y < Height; Y++) { Pointer = CloneData + (Y + 1) * CloneStride + 1; for (X = 0; X < Width; X++) { Ex = *(Pointer - 1) - *(Pointer + 1); if (Ex < 0) Ex = -Ex; Ey = *(Pointer - CloneStride) - *(Pointer + CloneStride); if (Ey < 0) Ey = -Ey; if (Ex > Ey) { Weight += Ex; SumWeight += *Pointer * Ex; } else { Weight += Ey; SumWeight += *Pointer * Ey; } Pointer++; } } GlobalFree((IntPtr)CloneData); GrayBmp.UnlockBits(GrayBmpData); if (Weight == 0) return *(Scan0); // 说明所有的颜色值都相同 return (byte)(SumWeight / Weight); }
一般情况下,为了程序的速度考虑,对于一些小函数我建议直接自己展开,比如上面的ABS函数,直接写成if (Ex < 0) Ex = -Ex会快一些的。你通过下面的反汇编可以看出不同:
Ex = Math.Abs(Ex); 00000161 js 00000167 00000163 mov eax,esi 00000165 jmp 0000016E 00000167 mov ecx,esi 00000169 call 638C54E4 0000016e mov esi,eax if (Ex < 0) Ex = -Ex; 00000170 test eax,eax 00000172 jge 00000176 00000174 neg esi
分割的效果可能还是要拿具体的图像说事,这里不做过多评论。
工程下载地址:https://files.cnblogs.com/Imageshop/ThresholdUseSIS.rar
*****************************基本上我不提供源代码,但是我会尽量用文字把对应的算法描述清楚或提供参考文档**************************
*******************************因为靠自己的努力和实践写出来的效果才真正是自己的东西,人一定要靠自己****************************
***************************作者: laviewpbt 时间: 2013.7.15 联系QQ: 33184777 转载请保留本行信息*************************