├── .gitattributes ├── .gitignore ├── CAPTCHA ├── CAPTCHA.sln ├── CAPTCHA │ ├── App.config │ ├── CAPTCHA.csproj │ ├── CaptchaSegment.cs │ ├── Preprocess.cs │ ├── Program.cs │ └── Properties │ │ └── AssemblyInfo.cs └── Orgin │ ├── OTSU │ ├── 10_OTSU .jpg │ ├── 11_OTSU .jpg │ ├── 1_OTSU .jpg │ ├── 2_OTSU .jpg │ ├── 3_OTSU .jpg │ ├── 4_OTSU .jpg │ ├── 5_OTSU .jpg │ ├── 6_OTSU .jpg │ ├── 7_OTSU .jpg │ ├── 8_OTSU .jpg │ └── 9_OTSU .jpg │ ├── Sauvla │ ├── 10_sauvola.jpg │ ├── 11_sauvola.jpg │ ├── 12_sauvola.jpg │ ├── 13_sauvola.jpg │ ├── 1_sauvola.jpg │ ├── 2_sauvola.jpg │ ├── 3_sauvola.jpg │ ├── 4_sauvola.jpg │ ├── 5_sauvola.jpg │ ├── 6_sauvola.jpg │ ├── 7_sauvola.jpg │ ├── 8_sauvola.jpg │ └── 9_sauvola.jpg │ ├── img0.jpg │ ├── img1.jpg │ ├── img10.jpg │ ├── img11.jpg │ ├── img12.jpg │ ├── img13.jpg │ ├── img2.jpg │ ├── img3.jpg │ ├── img4.jpg │ ├── img5.jpg │ ├── img6.jpg │ ├── img7.jpg │ ├── img8.jpg │ └── img9.jpg └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | 11 | [Dd]ebug/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | [Bb]in/ 16 | [Oo]bj/ 17 | 18 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 19 | !packages/*/build/ 20 | 21 | # MSTest test Results 22 | [Tt]est[Rr]esult*/ 23 | [Bb]uild[Ll]og.* 24 | 25 | *_i.c 26 | *_p.c 27 | *.ilk 28 | *.meta 29 | *.obj 30 | *.pch 31 | *.pdb 32 | *.pgc 33 | *.pgd 34 | *.rsp 35 | *.sbr 36 | *.tlb 37 | *.tli 38 | *.tlh 39 | *.tmp 40 | *.tmp_proj 41 | *.log 42 | *.vspscc 43 | *.vssscc 44 | .builds 45 | *.pidb 46 | *.log 47 | *.scc 48 | 49 | # Visual C++ cache files 50 | ipch/ 51 | *.aps 52 | *.ncb 53 | *.opensdf 54 | *.sdf 55 | *.cachefile 56 | 57 | # Visual Studio profiler 58 | *.psess 59 | *.vsp 60 | *.vspx 61 | 62 | # Guidance Automation Toolkit 63 | *.gpState 64 | 65 | # ReSharper is a .NET coding add-in 66 | _ReSharper*/ 67 | *.[Rr]e[Ss]harper 68 | 69 | # TeamCity is a build add-in 70 | _TeamCity* 71 | 72 | # DotCover is a Code Coverage Tool 73 | *.dotCover 74 | 75 | # NCrunch 76 | *.ncrunch* 77 | .*crunch*.local.xml 78 | 79 | # Installshield output folder 80 | [Ee]xpress/ 81 | 82 | # DocProject is a documentation generator add-in 83 | DocProject/buildhelp/ 84 | DocProject/Help/*.HxT 85 | DocProject/Help/*.HxC 86 | DocProject/Help/*.hhc 87 | DocProject/Help/*.hhk 88 | DocProject/Help/*.hhp 89 | DocProject/Help/Html2 90 | DocProject/Help/html 91 | 92 | # Click-Once directory 93 | publish/ 94 | 95 | # Publish Web Output 96 | *.Publish.xml 97 | 98 | # NuGet Packages Directory 99 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 100 | #packages/ 101 | 102 | # Windows Azure Build Output 103 | csx 104 | *.build.csdef 105 | 106 | # Windows Store app package directory 107 | AppPackages/ 108 | 109 | # Others 110 | sql/ 111 | *.Cache 112 | ClientBin/ 113 | [Ss]tyle[Cc]op.* 114 | ~$* 115 | *~ 116 | *.dbmdl 117 | *.[Pp]ublish.xml 118 | *.pfx 119 | *.publishsettings 120 | 121 | # RIA/Silverlight projects 122 | Generated_Code/ 123 | 124 | # Backup & report files from converting an old project file to a newer 125 | # Visual Studio version. Backup files are not needed, because we have git ;-) 126 | _UpgradeReport_Files/ 127 | Backup*/ 128 | UpgradeLog*.XML 129 | UpgradeLog*.htm 130 | 131 | # SQL Server files 132 | App_Data/*.mdf 133 | App_Data/*.ldf 134 | 135 | 136 | #LightSwitch generated files 137 | GeneratedArtifacts/ 138 | _Pvt_Extensions/ 139 | ModelManifest.xml 140 | 141 | # ========================= 142 | # Windows detritus 143 | # ========================= 144 | 145 | # Windows image file caches 146 | Thumbs.db 147 | ehthumbs.db 148 | 149 | # Folder config file 150 | Desktop.ini 151 | 152 | # Recycle Bin used on file shares 153 | $RECYCLE.BIN/ 154 | 155 | # Mac desktop service store files 156 | .DS_Store 157 | -------------------------------------------------------------------------------- /CAPTCHA/CAPTCHA.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CAPTCHA", "CAPTCHA\CAPTCHA.csproj", "{EDB125E1-4978-4E8C-86AF-FD047FE9831B}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|Any CPU = Debug|Any CPU 9 | Release|Any CPU = Release|Any CPU 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {EDB125E1-4978-4E8C-86AF-FD047FE9831B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {EDB125E1-4978-4E8C-86AF-FD047FE9831B}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {EDB125E1-4978-4E8C-86AF-FD047FE9831B}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {EDB125E1-4978-4E8C-86AF-FD047FE9831B}.Release|Any CPU.Build.0 = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | EndGlobal 21 | -------------------------------------------------------------------------------- /CAPTCHA/CAPTCHA/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CAPTCHA/CAPTCHA/CAPTCHA.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {EDB125E1-4978-4E8C-86AF-FD047FE9831B} 8 | Exe 9 | Properties 10 | CAPTCHA 11 | CAPTCHA 12 | v4.5 13 | 512 14 | 15 | 16 | AnyCPU 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | AnyCPU 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 61 | -------------------------------------------------------------------------------- /CAPTCHA/CAPTCHA/CaptchaSegment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Drawing; 5 | using System.Drawing.Imaging; 6 | using System.Linq; 7 | using System.Runtime.InteropServices; 8 | using System.Text; 9 | using CAPTCHA; 10 | using Preprocess; 11 | 12 | 13 | namespace CaptchaSegment 14 | { 15 | public class Boundary 16 | { 17 | private Point _rightUpPoint; 18 | 19 | public Point rightUpPoint 20 | { 21 | get { return _rightUpPoint; } 22 | set { _rightUpPoint = value; } 23 | } 24 | 25 | private Point _leftDownPoint; 26 | 27 | public Point leftDownPoint 28 | { 29 | get { return _leftDownPoint; } 30 | set { _leftDownPoint = value; } 31 | } 32 | } 33 | 34 | public class SegAreaInfo 35 | { 36 | //已分割区域包含的有效字符像素的数量 37 | private int _blackPixCount; 38 | 39 | public int blackPixCount 40 | { 41 | get { return _blackPixCount; } 42 | set { _blackPixCount = value; } 43 | } 44 | 45 | private Boundary _segAreaB; 46 | 47 | public Boundary segAreaB 48 | { 49 | get { return _segAreaB; } 50 | set { _segAreaB = value; } 51 | } 52 | 53 | private byte[,] _binaryArr; 54 | 55 | public byte[,] binaryArr 56 | { 57 | get { return _binaryArr; } 58 | set { _binaryArr = value; } 59 | } 60 | } 61 | 62 | 63 | 64 | 65 | public class CaptchaSegment 66 | { 67 | //记录当前处理验证码字符有效区域 68 | public static Boundary ImgBoundary = new Boundary(); 69 | 70 | public static int ImageHeight = CAPTCHA.Program.Height; 71 | 72 | public static int ImageWidth = CAPTCHA.Program.Width; 73 | 74 | //当前验证码图片中包含的有效字符的总像素数 75 | public static int bPixSum = 0; 76 | 77 | //记录当前验证码的二值化数组 78 | public static Byte[,] BinaryArray = new Byte[ImageHeight, ImageWidth]; 79 | 80 | //记录当前验证码的分割路径 81 | public static ArrayList SegPathList= new ArrayList(); 82 | 83 | //记录当前已分割区域的相关信息:分割区域所包含的黑色像素数目;已分割区域的与baseline的交点;已分割区域的与meanine的交点; 84 | //已分割区域的最高点(离原点最远的,X坐标较大值);已分割区域的最低点(离原地最低点,X坐标较小值) 85 | public static ArrayList SegAreaList = new ArrayList(); 86 | 87 | //记录当前处理验证码的字符笔画宽度 88 | public static int StrokeWid = 0; 89 | 90 | //记录baseline与meanline的X坐标,//Point.x: basePoint; Point.y: meanPoint 91 | public static Point GuidePoint = new Point(); 92 | /// 93 | /// 将输入的含粘粘字符的图片转换为分割后的字符图片 94 | /// 95 | /// 粘粘字符图片的路径 96 | /// 分割后图片的存储路径 97 | /// 字符分割后的图片为位图 98 | public static Bitmap CaptchaSegmentFun(Bitmap srcBmp)//, string imageDestPath) 99 | { 100 | Byte[,] grayArraySrc = Preprocess.Preprocess.ToGrayArray(srcBmp); 101 | 102 | BinaryArray = Preprocess.Preprocess.Sauvola(grayArraySrc); 103 | 104 | FixBrokenCharacter(); 105 | 106 | StrokeWid = Preprocess.Preprocess.GetStrokeWid(ImageHeight, ImageWidth, BinaryArray); 107 | 108 | ImgBoundary = Preprocess.Preprocess.getImgBoundary(ImageHeight, ImageWidth, BinaryArray); 109 | 110 | GetGuidePoint(); 111 | 112 | Byte[,] grayArray = new Byte[ImageHeight, ImageWidth]; 113 | 114 | for (int i = 0; i < ImageHeight; i++) 115 | { 116 | for (int j = 0; j < ImageWidth; j++) 117 | { 118 | grayArray[i, j] = BinaryArray[i, j]; 119 | 120 | if (0 == BinaryArray[i, j]) 121 | { 122 | bPixSum++; 123 | } 124 | } 125 | } 126 | 127 | grayArray = Preprocess.Preprocess.FillConnectedArea(1, 1, ImageHeight, ImageWidth, grayArray); 128 | 129 | ArrayList loopArray = GetLoopBoundary(grayArray); 130 | 131 | SegByLoop(loopArray, grayArray); 132 | 133 | 134 | int times = 0;//在loop特征分割完成后,允许根据guideline分割的次数,现暂定为5次 135 | while ((SegPathList.Count < 5) && (times < 5)) 136 | { 137 | GetMergedArea(); 138 | 139 | SegAreaList.Clear(); 140 | 141 | for (int k = 0; k < (SegPathList.Count - 1); k++) 142 | { 143 | ArrayList pathLeft = (ArrayList)SegPathList[k]; 144 | ArrayList pathRight = (ArrayList)SegPathList[k + 1]; 145 | 146 | SegAreaInfo aInfo = GetSegInfo(pathLeft, pathRight); 147 | 148 | SegAreaList.Add(aInfo); 149 | } 150 | 151 | 152 | times++; 153 | } 154 | 155 | 156 | return (GetSegImg()); 157 | } 158 | 159 | 160 | /// 161 | /// 修补有笔画缺陷的字符,修补后的二值化数组仍存储在BinaryArray中 162 | /// 163 | public static void FixBrokenCharacter() 164 | { 165 | /* 166 | 5 7 8 167 | 5 X 1 168 | 4 3 2 169 | */ 170 | // 1 2 3 4 5 6 7 8 171 | // 1 0 0 1 0 0 0 0 144 172 | // 1 0 0 0 1 0 0 0 136 173 | // 1 0 0 0 0 1 0 0 132 174 | // 0 1 0 0 1 0 0 0 72 175 | // 0 1 0 0 0 1 0 0 68 176 | // 0 1 0 0 0 0 1 0 66 177 | // 0 0 1 0 0 1 0 0 36 178 | // 0 0 1 0 0 0 1 0 34 179 | // 0 0 1 0 0 0 0 1 33 180 | // 0 0 0 1 0 0 1 0 18 181 | // 0 0 0 1 0 0 0 1 17 182 | // 0 0 0 0 1 0 0 1 9 183 | 184 | int[] templateFix = new int[] {34,136 }; 185 | //{9,17,18,33,34,36,66,68,72,132,136,144}; 186 | 187 | // Byte[,] fixArray = new Byte[ImageHeight, ImageWidth]; 188 | 189 | int i = 0, j = 0; 190 | 191 | for(i = 0; i < ImageHeight; i ++) 192 | { 193 | for (j = 0; j < ImageWidth; j ++) 194 | { 195 | if (0 == BinaryArray[i, j]) 196 | { 197 | BinaryArray[i, j] = 1; 198 | } 199 | else 200 | { 201 | BinaryArray[i, j] = 0; 202 | } 203 | } 204 | } 205 | 206 | Byte point1 = 0; 207 | Byte point2 = 0; 208 | Byte point3 = 0; 209 | Byte point4 = 0; 210 | Byte point5 = 0; 211 | Byte point6 = 0; 212 | Byte point7 = 0; 213 | Byte point8 = 0; 214 | 215 | for(i = 1; i < (ImageHeight - 1); i ++) 216 | { 217 | for (j = 1; j < (ImageWidth - 1); j ++ ) 218 | { 219 | if (0 == BinaryArray[i, j]) 220 | { 221 | point1 = BinaryArray[i, j + 1]; 222 | point2 = BinaryArray[i + 1, j + 1]; 223 | point3 = BinaryArray[i + 1, j]; 224 | point4 = BinaryArray[i + 1, j - 1]; 225 | 226 | point5 = BinaryArray[i, j - 1]; 227 | point6 = BinaryArray[i - 1, j - 1]; 228 | point7 = BinaryArray[i - 1, j]; 229 | point8 = BinaryArray[i - 1, j + 1]; 230 | 231 | int nValue = (point1 << 7) + (point2 << 6) + (point3 << 5) + (point4 << 4) + (point5 << 3) 232 | + (point6 << 2) + (point7 << 1) + point8; 233 | if (templateFix.Contains(nValue)) 234 | { 235 | BinaryArray[i, j] = 1; 236 | } 237 | } 238 | } 239 | } 240 | 241 | for (i = 0; i < ImageHeight; i++) 242 | { 243 | for (j = 0; j < ImageWidth; j++) 244 | { 245 | if (0 == BinaryArray[i, j]) 246 | { 247 | BinaryArray[i, j] = 255; 248 | } 249 | else 250 | { 251 | BinaryArray[i, j] = 0; 252 | } 253 | } 254 | } 255 | } 256 | 257 | 258 | /// 259 | /// 获取验证码图片的baseline和meanline在X轴上的坐标 260 | /// 261 | public static void GetGuidePoint() 262 | { 263 | int[] pointArray = new int[ImgBoundary.rightUpPoint.Y]; 264 | 265 | int i = 0, j = 0; 266 | 267 | //找到每一列从字符底部到字符顶部遇到的第一个字符像素的X坐标值 268 | for (j = ImgBoundary.leftDownPoint.Y; j < ImgBoundary.rightUpPoint.Y; j++) 269 | { 270 | pointArray[j] = 0; 271 | 272 | for (i = ImgBoundary.rightUpPoint.X; i > ImgBoundary.leftDownPoint.X; i--) 273 | { 274 | if (0 == BinaryArray[i,j]) 275 | { 276 | pointArray[j] = i; 277 | break; 278 | } 279 | } 280 | } 281 | 282 | //找出上述X坐标值中数量最多的X值作为baseline 283 | Int32[] Histogram = new Int32[ImgBoundary.rightUpPoint.Y]; 284 | Array.Clear(Histogram, 0, ImgBoundary.rightUpPoint.Y); 285 | foreach (Int32 b in pointArray) 286 | { 287 | if(b != 0) 288 | { 289 | Histogram[b]++; 290 | } 291 | } 292 | 293 | //根据研究对象的特征,高于baseline三个像素以上的区域才会被认为是在baseline以下区域,因此将得到的坐标值+3个像素点作为baseline的值 294 | GuidePoint.X = Array.IndexOf(Histogram, Histogram.Max());// +3; 295 | 296 | //找到每一列从字符顶部到字符底部遇到的第一个字符像素的X坐标值 297 | for (j = ImgBoundary.leftDownPoint.Y; j < ImgBoundary.rightUpPoint.Y; j++) 298 | { 299 | pointArray[j] = 0; 300 | 301 | for (i = ImgBoundary.leftDownPoint.X; i < ImgBoundary.rightUpPoint.X; i++) 302 | { 303 | if (0 == BinaryArray[i,j]) 304 | { 305 | pointArray[j] = i; 306 | break; 307 | } 308 | } 309 | } 310 | 311 | //找到上述X值中数量最多的点作为meanline的参考点 312 | Array.Clear(Histogram, 0, ImgBoundary.rightUpPoint.X); // 初始化 313 | 314 | foreach (Int32 b in pointArray) 315 | { 316 | if(b != 0) 317 | { 318 | Histogram[b]++; // 统计直方图 319 | } 320 | } 321 | 322 | //小于meanline 2个像素的会被认为是meanline以上的区域,故直接将meanline减2个像素 323 | Int32 midValIndex = Array.IndexOf(Histogram, Histogram.Max()); 324 | 325 | int tmpVal = ImgBoundary.leftDownPoint.X + (ImgBoundary.rightUpPoint.X - ImgBoundary.leftDownPoint.X) / 5; 326 | 327 | //防止meanline以上没有有效像素,故并不直接以计算值作为guidePoint 328 | if(midValIndex > tmpVal) 329 | { 330 | GuidePoint.Y = midValIndex; 331 | } 332 | else 333 | { 334 | GuidePoint.Y = tmpVal; 335 | } 336 | } 337 | 338 | 339 | /// 340 | /// 根据仅loop为白色的灰度数组定位每个loop的坐标范围 341 | /// 342 | public static ArrayList GetLoopBoundary(Byte[,] grayArray) 343 | { 344 | int i = 0, j = 0, k = 0; 345 | 346 | int[] wHistogram = new int[ImageWidth]; 347 | int[] hHistogram = new int[ImageHeight]; 348 | 349 | //对字符所包含的封闭区间进行竖直投影 350 | for (i = ImgBoundary.leftDownPoint.X; i < ImgBoundary.rightUpPoint.X; i++) 351 | { 352 | for (j = ImgBoundary.leftDownPoint.Y; j < ImgBoundary.rightUpPoint.Y; j++) 353 | { 354 | if (255 == grayArray[i, j]) 355 | { 356 | wHistogram[j] ++; 357 | } 358 | } 359 | } 360 | 361 | ArrayList loopArray = new ArrayList();//compose of rightUpPoint and leftDownPoint 362 | 363 | j = ImgBoundary.leftDownPoint.Y; 364 | 365 | //根据封闭区间的竖直投影获取封闭区间的左右与上下极限位置 366 | while (j < ImgBoundary.rightUpPoint.Y) 367 | { 368 | if(wHistogram[j] != 0) 369 | { 370 | Boundary loop = new Boundary(); 371 | 372 | //高为X轴,宽为Y轴,图片的左上角为原点。 373 | //Y轴方向离原点较远的顶点,X轴方向离原点较远的顶点,这两个点为rightUp 374 | Point posRightUp = new Point(); 375 | //Y轴方向离原点较近的顶点,X轴方向离原点较近的顶点,这两个点为LeftDown 376 | Point posLeftDown = new Point(); 377 | 378 | posLeftDown.Y = j; 379 | 380 | while ((wHistogram[j] != 0) && (j < ImgBoundary.rightUpPoint.Y)) 381 | { 382 | j ++; 383 | } 384 | 385 | posRightUp.Y = j - 1; 386 | 387 | Array.Clear(hHistogram, 0, ImageHeight); 388 | 389 | //在已确定的左右区间内对封闭区域进行水平方向的投影 390 | for (j = posLeftDown.Y; j < posRightUp.Y; j++) 391 | { 392 | for (i = ImgBoundary.leftDownPoint.X; i < ImgBoundary.rightUpPoint.X; i++) 393 | { 394 | if (255 == grayArray[i, j]) 395 | { 396 | hHistogram[i]++; 397 | } 398 | } 399 | } 400 | 401 | //寻找水平投影起点的i的初始值 402 | i = ImgBoundary.leftDownPoint.X; 403 | 404 | while ((0 == hHistogram[i]) && (i < ImgBoundary.rightUpPoint.X)) 405 | { 406 | i++; 407 | } 408 | 409 | posLeftDown.X = i; 410 | 411 | i = ImgBoundary.rightUpPoint.X; 412 | 413 | while ((0 == hHistogram[i]) && (i > ImgBoundary.leftDownPoint.X)) 414 | { 415 | i--; 416 | } 417 | 418 | posRightUp.X = i; 419 | 420 | 421 | loop.leftDownPoint = posLeftDown; 422 | loop.rightUpPoint = posRightUp; 423 | 424 | loopArray.Add(loop); 425 | 426 | } 427 | j ++; 428 | } 429 | 430 | //遍历找到的封闭区间内的像素,至少包含一个8邻域的区域被认为是有效封闭区间,否则将被滤除 431 | //此处的处理方式需要根据实际验证码类型中loop的大小酌情调整, 432 | for (k = 0; k < loopArray.Count; k ++ ) 433 | { 434 | Boundary tmpLoop = (Boundary)loopArray[k]; 435 | 436 | bool tmpFlg = true; 437 | 438 | for (i = tmpLoop.leftDownPoint.X + 1; (i < tmpLoop.rightUpPoint.X - 1) && (tmpFlg); i ++ ) 439 | { 440 | for (j = tmpLoop.leftDownPoint.Y + 1; (j < tmpLoop.rightUpPoint.Y - 1) && (tmpFlg); j++) 441 | { 442 | if ((255 == grayArray[i,j]) && (255 == grayArray[i,j - 1]) && (255 == grayArray[i,j + 1]) 443 | && (255 == grayArray[i + 1, j]) && (255 == grayArray[i + 1, j - 1]) && (255 == grayArray[i + 1, j + 1]) 444 | && (255 == grayArray[i - 1, j]) && (255 == grayArray[i - 1, j - 1]) && (255 == grayArray[i - 1, j + 1])) 445 | { 446 | tmpFlg = false; 447 | } 448 | } 449 | } 450 | 451 | if (tmpFlg) 452 | { 453 | loopArray.RemoveAt(k); 454 | 455 | k--; 456 | } 457 | } 458 | 459 | return loopArray; 460 | } 461 | 462 | /// 463 | /// 根据两条分割路径获取已分割路径之间区域的如下信息: 464 | /// 1.两个路径之间包含的有效字符的像素点数(黑色像素的数量) 465 | /// 2.已分割区域的与baseline的交点 466 | /// 3.已分割区域的与meanine的交点 467 | /// 4.已分割区域的最高点(离原点最远的,X坐标较大值) 468 | /// 5.已分割区域的最低点(离原地最低点,X坐标较小值) 469 | /// 470 | public static SegAreaInfo GetSegInfo(ArrayList pathLeft, ArrayList pathRight) 471 | { 472 | //遍历两条路径之间的区域用 473 | Point posRight = new Point(); 474 | Point posLeft = new Point(); 475 | 476 | int widthRight = 0; 477 | int widthLeft = 0; 478 | 479 | int heightRight = 0; 480 | int heightLeft = 0; 481 | 482 | int indexLeft = 0; 483 | int indexRight = 0; 484 | 485 | int posCountLeft = 0; 486 | int posCountRight = 0; 487 | 488 | int bPixCount = 0; 489 | 490 | SegAreaInfo segArea = new SegAreaInfo(); 491 | 492 | Boundary tmpBoundary = new Boundary(); 493 | 494 | //用于存储已分割区域的二值化数组 495 | Byte[,] binaryArr = new Byte[ImageHeight, ImageWidth]; 496 | 497 | for(int m = 0; m < ImageHeight; m++) 498 | { 499 | for (int n = 0; n < ImageWidth;n ++ ) 500 | { 501 | binaryArr[m, n] = 255; 502 | } 503 | } 504 | 505 | bPixCount = 0; 506 | 507 | posCountLeft = pathLeft.Count; 508 | posCountRight = pathRight.Count; 509 | 510 | indexLeft = 0; 511 | indexRight = 0; 512 | 513 | //两条路径之间的点数可能不一致,可能会出现同一个X值对应多个Y值的状况 514 | while ((indexLeft < posCountLeft) && (indexRight < posCountRight)) 515 | { 516 | posRight = (Point)pathRight[indexRight]; 517 | posLeft = (Point)pathLeft[indexLeft]; 518 | 519 | indexLeft++; 520 | indexRight++; 521 | 522 | widthLeft = posLeft.Y; 523 | widthRight = posRight.Y; 524 | 525 | heightRight = posLeft.X; 526 | heightLeft = posRight.X; 527 | 528 | //路径中可能会出现多个宽度值对应同一高度值的状况,找到同一高度值下最左侧的宽度值 529 | while (indexLeft < posCountLeft) 530 | { 531 | posLeft = (Point)pathLeft[indexLeft]; 532 | 533 | if (widthLeft != posLeft.X) 534 | { 535 | break; 536 | } 537 | else 538 | { 539 | if (widthLeft > posLeft.Y) 540 | { 541 | widthLeft = posLeft.Y; 542 | } 543 | indexLeft++; 544 | } 545 | } 546 | 547 | //路径中可能会出现多个宽度值对应同一高度值的状况,找到同一高度值下最右侧的宽度值 548 | while (indexRight < posCountRight) 549 | { 550 | posRight = (Point)pathRight[indexRight]; 551 | 552 | if (heightRight != posRight.X) 553 | { 554 | break; 555 | } 556 | else 557 | { 558 | if (widthRight < posRight.Y) 559 | { 560 | widthRight = posRight.Y; 561 | } 562 | indexRight++; 563 | } 564 | } 565 | 566 | //从左至右,从上至下,遍历两条路径之间的像素点 567 | for (Int32 y = widthLeft; y < widthRight; y++) 568 | { 569 | binaryArr[heightLeft, y] = BinaryArray[heightLeft, y]; 570 | 571 | if (0 == BinaryArray[heightLeft, y]) 572 | { 573 | //将两条路径之间的有效像素点的总数 574 | bPixCount++; 575 | } 576 | } 577 | } 578 | 579 | tmpBoundary = Preprocess.Preprocess.getImgBoundary(ImageHeight, ImageWidth, binaryArr); 580 | 581 | segArea.blackPixCount = bPixCount; 582 | 583 | segArea.segAreaB = tmpBoundary; 584 | 585 | segArea.binaryArr = binaryArr; 586 | 587 | return segArea; 588 | } 589 | 590 | 591 | /// 592 | /// 根据侦测到的LOOP区域进行分割 593 | /// 594 | /// 当前验证码图片中LOOP区域的信息集合 595 | /// 有效字符为黑色,loop区域为白色,背景为灰色的灰度数组 596 | public static void SegByLoop(ArrayList loopArray, Byte[,] grayArray) 597 | { 598 | int width = ImgBoundary.rightUpPoint.Y - ImgBoundary.leftDownPoint.Y; 599 | 600 | //若loop左侧与图像左侧的距离,或者loop右侧与图像右侧的距离小于该值,那么该loop位于最左侧或最右侧,它将只有一个分割点 601 | int minW = width / 7; 602 | 603 | //当前分割点 604 | Point pos = new Point(); 605 | 606 | //上一个分割点,若两个分割点之间的距离小于minW,那么新的分割点将无效 607 | Point oldPos = new Point(); 608 | 609 | //ArrayList SegPathList = new ArrayList(); 610 | 611 | int i = 0; 612 | 613 | ArrayList pathListFirst = new ArrayList(); 614 | //将图像最左侧的点所在直线作为第一条分割路径 615 | for (i = ImgBoundary.leftDownPoint.X; i < ImgBoundary.rightUpPoint.X; i++) 616 | { 617 | pos.X = i; 618 | 619 | pos.Y = ImgBoundary.leftDownPoint.Y; 620 | 621 | pathListFirst.Add(pos); 622 | } 623 | 624 | SegPathList.Add(pathListFirst); 625 | 626 | oldPos = pos; 627 | 628 | for(int k = 0; k < loopArray.Count; k ++) 629 | { 630 | ArrayList pathListUp = new ArrayList(); 631 | 632 | ArrayList pathListDown = new ArrayList(); 633 | 634 | Boundary tmpLoop = (Boundary)loopArray[k]; 635 | 636 | //判断当前loop所在位置与baseline的关系,正常状况下loop应该都在baseline以上区域,位于baseline以上区域的部分按正常方式取其分割点 637 | if (tmpLoop.rightUpPoint.X <= GuidePoint.X) 638 | { 639 | //此处minW阈值可以进行调节,调节的条件是确保最左侧与最右侧的点可以被包含进相应的条件,否则程序有可能出错 640 | if ((tmpLoop.leftDownPoint.Y - StrokeWid - ImgBoundary.leftDownPoint.Y) < minW) 641 | { 642 | pos = GetSementLoopPoint(true, tmpLoop, grayArray); 643 | } 644 | else if ((ImgBoundary.rightUpPoint.Y - StrokeWid - tmpLoop.rightUpPoint.Y) < minW) 645 | { 646 | pos = GetSementLoopPoint(false, tmpLoop, grayArray); 647 | } 648 | else 649 | { 650 | pos = GetSementLoopPoint(false, tmpLoop, grayArray); 651 | //应该是新分割点左侧点与旧分割点右侧点的距离?_20150814 652 | if ((pos != oldPos) && (pos.Y - oldPos.Y >= minW)) 653 | { 654 | pathListUp = dropFallUp(pos); 655 | 656 | pathListDown = dropFallDown(pos); 657 | 658 | for (i = 0; i < pathListUp.Count; i++) 659 | { 660 | pathListDown.Add(pathListUp[i]); 661 | } 662 | 663 | SegPathList.Add(pathListDown); 664 | } 665 | 666 | oldPos = pos; 667 | 668 | pos = GetSementLoopPoint(true, tmpLoop, grayArray); 669 | } 670 | 671 | if ((pos != oldPos) && (pos.Y - oldPos.Y < minW)) 672 | { 673 | continue; 674 | } 675 | 676 | pathListUp = dropFallUp(pos); 677 | 678 | pathListDown = dropFallDown(pos); 679 | 680 | for (i = 0; i < pathListUp.Count; i++) 681 | { 682 | pathListDown.Add(pathListUp[i]); 683 | } 684 | 685 | SegPathList.Add(pathListDown); 686 | 687 | oldPos = pos; 688 | } 689 | else//若其有部分区域位于baseline以下,那么说明当前loop倾斜严重,分割点将直接取该loop与baseline的交叉点作为分割点 690 | { 691 | Point leftP = new Point(); 692 | Point rightP = new Point(); 693 | 694 | for (i = tmpLoop.leftDownPoint.Y; i < tmpLoop.rightUpPoint.Y; i ++ ) 695 | { 696 | if (255 == grayArray[GuidePoint.X,i]) 697 | { 698 | if(i - StrokeWid < 0) 699 | { 700 | leftP.Y = 0; 701 | } 702 | else 703 | { 704 | leftP.Y = i - StrokeWid; 705 | } 706 | 707 | break; 708 | } 709 | } 710 | 711 | for (i = tmpLoop.rightUpPoint.Y; i > tmpLoop.leftDownPoint.Y; i--) 712 | { 713 | if (255 == grayArray[GuidePoint.X, i]) 714 | { 715 | if (i + StrokeWid > ImageWidth) 716 | { 717 | rightP.Y = ImageWidth - 1; 718 | } 719 | else 720 | { 721 | rightP.Y = i + StrokeWid; 722 | } 723 | break; 724 | } 725 | } 726 | 727 | leftP.X = GuidePoint.X; 728 | rightP.X = GuidePoint.X; 729 | 730 | //此处minW阈值可以进行调节,调节的条件是确保最左侧与最右侧的点可以被包含进相应的条件,否则程序有可能出错 731 | if ((leftP.Y - ImgBoundary.leftDownPoint.Y) < minW) 732 | { 733 | pos = rightP; 734 | } 735 | else if ((ImgBoundary.rightUpPoint.Y - rightP.Y) < minW) 736 | { 737 | pos = leftP; 738 | } 739 | else 740 | { 741 | pos = leftP; 742 | 743 | if ((pos != oldPos) && (pos.Y - oldPos.Y >= minW)) 744 | { 745 | pathListUp = dropFallUp(pos); 746 | 747 | pathListDown = dropFallDown(pos); 748 | 749 | for (i = 0; i < pathListUp.Count; i++) 750 | { 751 | pathListDown.Add(pathListUp[i]); 752 | } 753 | 754 | SegPathList.Add(pathListDown); 755 | } 756 | 757 | oldPos = pos; 758 | 759 | pos = rightP; 760 | } 761 | 762 | if ((pos != oldPos) && (pos.Y - oldPos.Y < minW)) 763 | { 764 | continue; 765 | } 766 | 767 | pathListUp = dropFallUp(pos); 768 | 769 | pathListDown = dropFallDown(pos); 770 | 771 | for (i = 0; i < pathListUp.Count; i++) 772 | { 773 | pathListDown.Add(pathListUp[i]); 774 | } 775 | 776 | SegPathList.Add(pathListDown); 777 | 778 | oldPos = pos; 779 | } 780 | } 781 | 782 | ArrayList pathListLast = new ArrayList(); 783 | //将图像最右侧的点所在直线作为最后一条分割路径 784 | for (i = ImgBoundary.leftDownPoint.X; i < ImgBoundary.rightUpPoint.X; i++) 785 | { 786 | Point newPos = new Point(); 787 | 788 | newPos.X = i; 789 | 790 | newPos.Y = ImgBoundary.rightUpPoint.Y; 791 | 792 | pathListLast.Add(newPos); 793 | } 794 | SegPathList.Add(pathListLast); 795 | 796 | //若分割路径小于5,那么获取已分割区域的相关信息,用于指导后续根据Guideline继续分割 797 | if (SegPathList.Count < 5) 798 | { 799 | for (i = 0; i < (SegPathList.Count - 1); i++ ) 800 | { 801 | ArrayList pathLeft = (ArrayList)SegPathList[i]; 802 | ArrayList pathRight = (ArrayList)SegPathList[i + 1]; 803 | 804 | SegAreaInfo aInfo = GetSegInfo(pathLeft, pathRight); 805 | 806 | SegAreaList.Add(aInfo); 807 | } 808 | } 809 | } 810 | 811 | /// 812 | /// 根据指定loop的信息获取相应的分割点 813 | /// 814 | /// 用于标记当前需要确认的分割点在LOOP的左侧或右侧,true:右侧;false:左侧 815 | /// 当前研究的LOOP的坐标范围 816 | /// 有效字符为黑色,loop区域为白色,背景为灰色的灰度数组 817 | public static Point GetSementLoopPoint(bool beRight, Boundary Loop, Byte[,] grayArray) 818 | { 819 | Point segPoint = new Point(); 820 | 821 | int tmpH = 0; 822 | 823 | int tmpW = 0; 824 | 825 | if (beRight) 826 | { 827 | //确认LOOP右侧的分割点,先找到LOOP位于最右侧点的坐标 828 | for (tmpH = Loop.leftDownPoint.X; tmpH < Loop.rightUpPoint.X; tmpH++) 829 | { 830 | if (255 == grayArray[tmpH, Loop.rightUpPoint.Y]) 831 | { 832 | break; 833 | } 834 | } 835 | 836 | //以LOOP最右侧点的高度坐标为分割点的高度坐标;以右侧点的宽度坐标+笔画宽度作为分割点的宽度坐标 837 | segPoint.X = tmpH; 838 | 839 | tmpW = Loop.rightUpPoint.Y + StrokeWid; 840 | 841 | //若确认的分割点处为灰色,那么从该点开始逐点后退找到紧贴LOOP的灰色像素作为分割点 842 | while(128 == grayArray[tmpH,tmpW]) 843 | { 844 | tmpW --; 845 | } 846 | 847 | segPoint.Y = tmpW + 1; 848 | } 849 | else 850 | { 851 | //确认LOOP左侧的分割点,先找到LOOP位于最左侧点的坐标 852 | for (tmpH = Loop.leftDownPoint.X; tmpH < Loop.rightUpPoint.X; tmpH++) 853 | { 854 | if (255 == grayArray[tmpH, Loop.leftDownPoint.Y]) 855 | { 856 | break; 857 | } 858 | } 859 | 860 | //以LOOP最左侧点的高度坐标为分割点的高度坐标;以左侧点的宽度坐标-笔画宽度作为分割点的宽度坐标 861 | segPoint.X = tmpH; 862 | 863 | tmpW = Loop.leftDownPoint.Y - StrokeWid; 864 | 865 | //若确认的分割点处为灰色,那么从该点开始逐点前进找到紧贴LOOP的灰色像素作为分割点 866 | while(128 == grayArray[tmpH,tmpW]) 867 | { 868 | tmpW ++; 869 | } 870 | 871 | segPoint.Y = tmpW - 1; 872 | } 873 | 874 | return segPoint; 875 | } 876 | 877 | /// 878 | ///根据分割路径分割粘粘字符并另存为位图 879 | /// 880 | public static Bitmap GetSegImg() 881 | { 882 | int segCount = SegPathList.Count; 883 | 884 | int locationVal = ImageWidth / (segCount - 1); 885 | 886 | int locationPos = 0; 887 | 888 | int iniWidthLeft = 0; 889 | 890 | Byte[,] divideArray = new Byte[ImageHeight, ImageWidth]; 891 | 892 | for (Int32 x = 0; x < ImageHeight; x++) 893 | { 894 | for (Int32 y = 0; y < ImageWidth; y++) 895 | { 896 | divideArray[x, y] = 255; 897 | } 898 | } 899 | 900 | Point divPosRight = new Point(); 901 | 902 | Point divPosLeft = new Point(); 903 | 904 | ArrayList pathListLeft = new ArrayList(); 905 | ArrayList pathListRight = new ArrayList(); 906 | 907 | int indexWidthRight = 0; 908 | int indexWidthLeft = 0; 909 | 910 | int indexHeightRight = 0; 911 | int indexHeightLeft = 0; 912 | 913 | int pointIndexLeft = 0; 914 | int pointIndexRight = 0; 915 | 916 | int posCountLeft = 0; 917 | int posCountRight = 0; 918 | 919 | for (int i = 0; i < segCount - 1; i++) 920 | { 921 | //用于记录分割区域所在的范围,Point.X为分割范围的左侧,Point.Y为分割范围的右侧 922 | // Point segArea = new Point(); 923 | 924 | pathListLeft = (ArrayList)SegPathList[i]; 925 | pathListRight = (ArrayList)SegPathList[i + 1]; 926 | 927 | posCountLeft = pathListLeft.Count; 928 | posCountRight = pathListRight.Count; 929 | 930 | //遍历左侧路径中的点,找到左侧路径点中最小的宽度值,用于辅助分割后字符的定位 931 | divPosRight = (Point)pathListRight[0]; 932 | iniWidthLeft = divPosRight.Y; 933 | 934 | divPosLeft = (Point)pathListLeft[0]; 935 | 936 | for (pointIndexLeft = 0; pointIndexLeft < posCountLeft; pointIndexLeft++) 937 | { 938 | divPosLeft = (Point)pathListLeft[pointIndexLeft]; 939 | 940 | if (iniWidthLeft > divPosLeft.Y) 941 | { 942 | iniWidthLeft = divPosLeft.Y; 943 | } 944 | } 945 | 946 | locationPos = 5 + locationVal * i; 947 | 948 | pointIndexLeft = 0; 949 | pointIndexRight = 0; 950 | 951 | while ((pointIndexLeft < posCountLeft) && (pointIndexRight < posCountRight)) 952 | { 953 | //遍历路径中每一个高度值,找到每一个高度值对应的宽度左右极限,将此范围内的像素点重新进行定位以分割字符 954 | divPosRight = (Point)pathListRight[pointIndexRight]; 955 | divPosLeft = (Point)pathListLeft[pointIndexLeft]; 956 | 957 | indexWidthLeft = divPosLeft.Y; 958 | indexWidthRight = divPosRight.Y; 959 | 960 | indexHeightRight = divPosRight.X; 961 | indexHeightLeft = divPosLeft.X; 962 | 963 | pointIndexLeft++; 964 | pointIndexRight++; 965 | 966 | //路径中可能会出现多个宽度值对应同一高度值的状况,找到同一高度值下最左侧的宽度值 967 | while (pointIndexLeft < posCountLeft) 968 | { 969 | divPosLeft = (Point)pathListLeft[pointIndexLeft]; 970 | 971 | if (indexHeightLeft != divPosLeft.X) 972 | { 973 | break; 974 | } 975 | else 976 | { 977 | if (indexWidthLeft > divPosLeft.Y) 978 | { 979 | indexWidthLeft = divPosLeft.Y; 980 | } 981 | pointIndexLeft++; 982 | } 983 | } 984 | 985 | //路径中可能会出现多个宽度值对应同一高度值的状况,找到同一高度值下最右侧的宽度值 986 | while (pointIndexRight < posCountRight) 987 | { 988 | divPosRight = (Point)pathListRight[pointIndexRight]; 989 | 990 | if (indexHeightRight != divPosRight.X) 991 | { 992 | break; 993 | } 994 | else 995 | { 996 | if (indexWidthRight < divPosRight.Y) 997 | { 998 | indexWidthRight = divPosRight.Y; 999 | } 1000 | pointIndexRight++; 1001 | } 1002 | } 1003 | 1004 | int tmpVal = 0; 1005 | 1006 | for (Int32 y = indexWidthLeft; y < indexWidthRight; y++) 1007 | { 1008 | tmpVal = y - iniWidthLeft + locationPos; 1009 | 1010 | if (tmpVal < ImageWidth) 1011 | { 1012 | divideArray[indexHeightRight, tmpVal] = BinaryArray[indexHeightRight, y]; 1013 | } 1014 | else 1015 | { 1016 | //error; 1017 | } 1018 | } 1019 | } 1020 | } 1021 | 1022 | Bitmap GrayBmp = Preprocess.Preprocess.BinaryArrayToBinaryBitmap(divideArray); 1023 | 1024 | return GrayBmp; 1025 | } 1026 | 1027 | /// 1028 | ///根据loop分割后的区块判断是否还有粘连字符,若有则根据guideLine原则继续分割 1029 | /// 1030 | public static void GetMergedArea()//divideInfo divideData, Point guideLine, Byte[,] BinaryArray, ImgBoundary Boundary) 1031 | { 1032 | int areaNum = SegAreaList.Count; 1033 | 1034 | //记录单个粘连区域内独立有效的竖直投影区域 1035 | ArrayList proLoops = new ArrayList(); 1036 | 1037 | //记录当前粘连区域的Y轴范围 1038 | SegAreaInfo mAreaRange = new SegAreaInfo(); 1039 | SegAreaInfo mAreaRange_1 = new SegAreaInfo(); 1040 | SegAreaInfo mAreaRange_2 = new SegAreaInfo(); 1041 | 1042 | //记录不同区块的黑色像素数,用于辅助判断粘连字符区域 1043 | int blackCount_0 = 0; 1044 | int blackCount_1 = 0; 1045 | int blackCount_2 = 0; 1046 | 1047 | //记录当前验证码图片中单个字符的平均像素 1048 | int bThVal = bPixSum / 4; 1049 | 1050 | // int newPathPos = 0;//记录新的分割路径该插入的位置 1051 | 1052 | switch (areaNum) 1053 | { 1054 | case 1://当前验证码仍有4个字符粘连 1055 | 1056 | mAreaRange = (SegAreaInfo)SegAreaList[0]; 1057 | 1058 | SegByGuideLine(mAreaRange, 1, 4); 1059 | 1060 | break; 1061 | 1062 | case 2://当前验证码有3个字符粘连与1个分割完成的字符;或者2个字符与2个字符粘连 1063 | //根据两个区域的黑色像素数来判断哪个区块为粘连区 1064 | mAreaRange = (SegAreaInfo)SegAreaList[0]; 1065 | 1066 | blackCount_0 = mAreaRange.blackPixCount; 1067 | 1068 | mAreaRange_1 = (SegAreaInfo)SegAreaList[1]; 1069 | 1070 | blackCount_1 = mAreaRange_1.blackPixCount; 1071 | 1072 | if (blackCount_0 > (2 * bThVal + bThVal / 2))//dArray[0]为3个字符粘连区域 1073 | { 1074 | SegByGuideLine(mAreaRange, 1, 3); 1075 | } 1076 | else if (blackCount_1 > (2 * bThVal + bThVal / 2))//dArray[1]为3个字符粘连区域 1077 | { 1078 | SegByGuideLine(mAreaRange_1, 2, 3); 1079 | } 1080 | else 1081 | { 1082 | SegByGuideLine(mAreaRange, 1, 2); 1083 | } 1084 | 1085 | break; 1086 | 1087 | case 3://当前验证码有2个字符粘连与2个独立字符,根据区域黑色像素数来找出粘连区 1088 | mAreaRange = (SegAreaInfo)SegAreaList[0]; 1089 | 1090 | blackCount_0 = mAreaRange.blackPixCount; 1091 | 1092 | mAreaRange_1 = (SegAreaInfo)SegAreaList[1]; 1093 | 1094 | blackCount_1 = mAreaRange_1.blackPixCount; 1095 | 1096 | mAreaRange_2 = (SegAreaInfo)SegAreaList[2]; 1097 | 1098 | blackCount_2 = mAreaRange_2.blackPixCount; 1099 | 1100 | if ((blackCount_0 > blackCount_1) && (blackCount_0 > blackCount_2))//dArray[0]为2个字符粘连区域 1101 | { 1102 | SegByGuideLine(mAreaRange,1, 2); 1103 | } 1104 | else if ((blackCount_1 > blackCount_0) && (blackCount_1 > blackCount_2))//dArray[1]为2个字符粘连区域 1105 | { 1106 | SegByGuideLine(mAreaRange_1, 2, 2); 1107 | } 1108 | else 1109 | { 1110 | SegByGuideLine(mAreaRange_2, 3, 2); 1111 | } 1112 | 1113 | break; 1114 | 1115 | default: 1116 | break; 1117 | } 1118 | 1119 | } 1120 | 1121 | /// 1122 | ///根据粘连区域与Guideline的关系对粘连区域进行分割 1123 | /// 1124 | /// 粘连区域的信息 1125 | /// 此次分割得到的路径在总路径中的位置 1126 | /// 当前粘连区域包含的字符数 1127 | public static void SegByGuideLine(SegAreaInfo mergedArea, int newPos, int num) 1128 | { 1129 | //循环用变量 1130 | int k = 0; //SegAreaInfo 1131 | 1132 | Boundary mBoundary = mergedArea.segAreaB; 1133 | 1134 | //当前粘连区域粘连字符宽度判定阈值 1135 | int thVal = (mBoundary.rightUpPoint.Y - mBoundary.leftDownPoint.Y) / (num + 1); 1136 | 1137 | int bThVal = mergedArea.blackPixCount / (num + 2); 1138 | 1139 | Point segP = new Point(); 1140 | 1141 | ArrayList pathListA = new ArrayList(); 1142 | 1143 | ArrayList pathListB = new ArrayList(); 1144 | 1145 | //用于标注占据三格的字符根据meanline分割的路径是否为有效路径 1146 | bool bValid = false; 1147 | 1148 | //获取当前粘连区域在meanline之上区域的竖直投影 1149 | // Point point1 = new Point(); 1150 | 1151 | //获取meanline以上区域竖直投影的右上角的点 1152 | segP.X = GuidePoint.Y; 1153 | segP.Y = mBoundary.rightUpPoint.Y; 1154 | VerPro meanArr = Preprocess.Preprocess.VerticalProjection(mBoundary.leftDownPoint, segP, 2, mergedArea.binaryArr); 1155 | int mCount = meanArr.vRange.Count; 1156 | 1157 | segP.X = GuidePoint.X; 1158 | segP.Y = mBoundary.leftDownPoint.Y; 1159 | VerPro baseArr = Preprocess.Preprocess.VerticalProjection(segP, mBoundary.rightUpPoint, 1, mergedArea.binaryArr); 1160 | int bCount = baseArr.vRange.Count; 1161 | 1162 | segP.X = GuidePoint.Y; 1163 | segP.Y = mBoundary.leftDownPoint.Y; 1164 | Point tmpPoint = new Point(); 1165 | tmpPoint.X = GuidePoint.X; 1166 | tmpPoint.Y = mBoundary.rightUpPoint.Y; 1167 | VerPro midArr = Preprocess.Preprocess.VerticalProjection(segP, tmpPoint, 0, mergedArea.binaryArr); 1168 | int midCount = midArr.vRange.Count; 1169 | 1170 | //以GuideLine作为分割依据主要是粘连区域占据四线三格中格数的状况 1171 | if ((mCount > 0) && (bCount > 0)) 1172 | { 1173 | //1.若占据三格,则meanline与粘连区域的左侧交点若不位于边界线,则将其作为一个分割点进行分割 1174 | if (meanArr.vInter.X > (mBoundary.leftDownPoint.Y + thVal)) 1175 | { 1176 | segP.X = GuidePoint.Y; 1177 | segP.Y = meanArr.vInter.X; 1178 | 1179 | bValid = SegByMeanLeft(segP, meanArr, mBoundary, bThVal, newPos); 1180 | } 1181 | 1182 | //若meanline区域分割无效,或者meanline无有效分割点,将以baseline为分割依据 1183 | //取baseline与字符在左侧的第一个交点,若该交点不位于边界,则以该点为分割点 1184 | //若该点位于边界,则以baseline和字符在右侧的第一个交点为分割点 1185 | if ((!bValid) && (baseArr.vInter.X > (mBoundary.leftDownPoint.Y + thVal))) 1186 | { 1187 | segP.X = GuidePoint.X; 1188 | segP.Y = baseArr.vInter.X; 1189 | 1190 | bValid = SegByBaseLine(segP, meanArr, mBoundary, bThVal, newPos, true); 1191 | } 1192 | 1193 | //若baseline与meanline的左侧交点均未得到有效分割线,则以baseline和meanline的右侧交点来进行分割 1194 | //以meanline的右侧交点为分割点 1195 | if ((!bValid) && (meanArr.vInter.Y < (mBoundary.rightUpPoint.Y - thVal))) 1196 | { 1197 | segP.X = GuidePoint.Y; 1198 | segP.Y = meanArr.vInter.Y; 1199 | 1200 | bValid = SegByMeanRight(segP, meanArr, mBoundary, bThVal, newPos); 1201 | } 1202 | 1203 | //以baseline的右侧交点为分割点 1204 | if ((!bValid) && (baseArr.vInter.Y < (mBoundary.leftDownPoint.Y - thVal))) 1205 | { 1206 | segP.X = GuidePoint.X; 1207 | segP.Y = baseArr.vInter.Y; 1208 | 1209 | bValid = SegByBaseLine(segP, meanArr, mBoundary, bThVal, newPos, false); 1210 | } 1211 | 1212 | //理论上而言,经过上述几项分割后,应该会得到一条有效路径,若仍未得到有效路径,将考虑用投影特征来分割,目前暂不处理此状况 1213 | 1214 | } 1215 | else if ((mCount > 0) && (0 == bCount)) 1216 | { 1217 | //目前粘连区域位于baseline以上的两个格 1218 | //1.若meanline以上区域位于右侧区域,则以meanline与字符的左侧交点为分割点 1219 | if (meanArr.vInter.X > (mBoundary.leftDownPoint.Y + thVal)) 1220 | { 1221 | segP.X = GuidePoint.Y; 1222 | segP.Y = meanArr.vInter.X; 1223 | 1224 | bValid = SegByMeanLeft(segP, meanArr, mBoundary, bThVal, newPos); 1225 | } 1226 | 1227 | //2.若meanline以上区域位于左侧区域,则以meanline与字符的右侧交点为分割点 1228 | if ((!bValid) && (meanArr.vInter.Y < (mBoundary.rightUpPoint.Y - thVal))) 1229 | { 1230 | segP.X = GuidePoint.Y; 1231 | segP.Y = meanArr.vInter.Y; 1232 | 1233 | bValid = SegByMeanRight(segP, meanArr, mBoundary, bThVal, newPos); 1234 | } 1235 | 1236 | //3.若上述两个条件均不成立,则以竖直投影为分割依据 1237 | thVal = (mBoundary.rightUpPoint.Y + mBoundary.leftDownPoint.Y) / 2; 1238 | //3.1 meanline以上区域粘连,以下区域分割,则以其分割区域靠近中间的投影点作为分割点 1239 | if (!bValid) 1240 | { 1241 | if ((1 == mCount) && (midCount > 1)) 1242 | { 1243 | segP = (Point)midArr.vRange[0]; 1244 | tmpPoint = (Point)midArr.vRange[1]; 1245 | 1246 | if (Math.Abs(thVal - segP.Y) < Math.Abs(thVal - tmpPoint.X)) 1247 | { 1248 | Point pt1 = new Point(); 1249 | 1250 | pt1.X = GuidePoint.Y; 1251 | pt1.Y = segP.Y; 1252 | 1253 | pathListA = dropFallUp(pt1); 1254 | 1255 | pathListB = dropFallDown(pt1); 1256 | } 1257 | else 1258 | { 1259 | Point pt1 = new Point(); 1260 | pt1.X = GuidePoint.Y; 1261 | pt1.Y = tmpPoint.X; 1262 | 1263 | pathListA = dropFallUp(pt1); 1264 | 1265 | pathListB = dropFallDown(pt1); 1266 | } 1267 | } 1268 | else if ((1 == midCount) && (mCount > 1)) 1269 | { 1270 | //3.2 meanline以上区域分割,以下区域粘连,则以其分割区域靠近中间的投影点作为分割点 1271 | segP = (Point)meanArr.vRange[0]; 1272 | tmpPoint = (Point)meanArr.vRange[1]; 1273 | 1274 | if (Math.Abs(thVal - segP.Y) < Math.Abs(thVal - tmpPoint.X)) 1275 | { 1276 | Point pt1 = new Point(); 1277 | 1278 | pt1.X = GuidePoint.Y; 1279 | pt1.Y = segP.Y; 1280 | 1281 | pathListA = dropFallUp(pt1); 1282 | 1283 | pathListB = dropFallDown(pt1); 1284 | } 1285 | else 1286 | { 1287 | Point pt1 = new Point(); 1288 | pt1.X = GuidePoint.Y; 1289 | pt1.Y = tmpPoint.X; 1290 | 1291 | pathListA = dropFallUp(pt1); 1292 | 1293 | pathListB = dropFallDown(pt1); 1294 | } 1295 | } 1296 | else 1297 | { 1298 | //3.3 上下均粘连,则暂不处理 1299 | } 1300 | if ((pathListA.Count > 0) && (pathListB.Count > 0)) 1301 | { 1302 | for (k = 0; k < pathListA.Count; k++) 1303 | { 1304 | pathListB.Add(pathListA[k]); 1305 | } 1306 | SegPathList.Insert(newPos, pathListB); 1307 | 1308 | bValid = true; 1309 | } 1310 | 1311 | } 1312 | } 1313 | else if ((0 == mCount) && (bCount > 0)) 1314 | { 1315 | //目前粘连区域位于meanline以下的两个格 1316 | //若baseline以下区域位于粘连区域的右侧,则以baseline和字符在左侧的交点为分割点 1317 | if ((!bValid) && (baseArr.vInter.X > (mBoundary.leftDownPoint.Y + thVal))) 1318 | { 1319 | segP.X = GuidePoint.X; 1320 | segP.Y = baseArr.vInter.X; 1321 | 1322 | bValid = SegByBaseLine(segP, meanArr, mBoundary, bThVal, newPos, true); 1323 | } 1324 | 1325 | //若baseline以下区域位于粘连区域的左侧,则以baseline和字符在右侧的交点为分割点 1326 | if ((!bValid) && (baseArr.vInter.Y < (mBoundary.rightUpPoint.Y - thVal))) 1327 | { 1328 | segP.X = GuidePoint.X; 1329 | segP.Y = baseArr.vInter.Y; 1330 | 1331 | bValid = SegByBaseLine(segP, meanArr, mBoundary, bThVal, newPos, false); 1332 | } 1333 | 1334 | //3.若上述两个条件均不成立,则以竖直投影为分割依据 1335 | if (!bValid) 1336 | { 1337 | //3.1 baseline以上区域粘连,以下区域分割,则以其分割区域靠近中间的投影点作为分割点 1338 | //3.2 baseline以上区域分割,以下区域粘连,则以其分割区域靠近中间的投影点作为分割点 1339 | //3.3 上下均粘连,则暂不处理 1340 | //3.若上述两个条件均不成立,则以竖直投影为分割依据 1341 | thVal = (mBoundary.rightUpPoint.Y + mBoundary.leftDownPoint.Y) / 2; 1342 | //3.1 meanline以上区域粘连,以下区域分割,则以其分割区域靠近中间的投影点作为分割点 1343 | if (!bValid) 1344 | { 1345 | if ((1 == bCount) && (midCount > 1)) 1346 | { 1347 | segP = (Point)midArr.vRange[0]; 1348 | tmpPoint = (Point)midArr.vRange[1]; 1349 | 1350 | if (Math.Abs(thVal - segP.Y) < Math.Abs(thVal - tmpPoint.X)) 1351 | { 1352 | segP.X = GuidePoint.X; 1353 | pathListA = dropFallUp(segP); 1354 | 1355 | pathListB = dropFallDown(segP); 1356 | } 1357 | else 1358 | { 1359 | tmpPoint.X = GuidePoint.X; 1360 | pathListA = dropFallUp(tmpPoint); 1361 | 1362 | pathListB = dropFallDown(tmpPoint); 1363 | } 1364 | } 1365 | else if ((1 == midCount) && (bCount > 1)) 1366 | { 1367 | //3.2 meanline以上区域分割,以下区域粘连,则以其分割区域靠近中间的投影点作为分割点 1368 | segP = (Point)baseArr.vRange[0]; 1369 | tmpPoint = (Point)baseArr.vRange[1]; 1370 | 1371 | if (Math.Abs(thVal - segP.Y) < Math.Abs(thVal - tmpPoint.X)) 1372 | { 1373 | pathListA = dropFallUp(segP); 1374 | 1375 | pathListB = dropFallDown(segP); 1376 | } 1377 | else 1378 | { 1379 | pathListA = dropFallUp(tmpPoint); 1380 | 1381 | pathListB = dropFallDown(tmpPoint); 1382 | } 1383 | } 1384 | else 1385 | { 1386 | //3.3 上下均粘连,则暂不处理 1387 | } 1388 | 1389 | for (k = 0; k < pathListA.Count; k++) 1390 | { 1391 | pathListB.Add(pathListA[k]); 1392 | } 1393 | 1394 | // pathListA = (ArrayList)SegPathList[newPos - 1]; 1395 | 1396 | // SegAreaInfo tmpInfo = GetSegInfo(pathListA, pathListB); 1397 | // SegAreaList.Add(tmpInfo); 1398 | SegPathList.Insert(newPos, pathListB); 1399 | 1400 | bValid = true; 1401 | } 1402 | } 1403 | else 1404 | { 1405 | //目前粘连区域位于中间一格,暂不处理 1406 | } 1407 | } 1408 | } 1409 | 1410 | public static bool SegByMeanLeft(Point segP, VerPro meanArr, Boundary mBoundary, int bThVal, int newPos) 1411 | { 1412 | bool bValid = false; 1413 | 1414 | //此类交点仅执行从上向下滴落,取meanline以上区域均归为左侧区域 1415 | ArrayList pathListA = dropFallUp(segP); 1416 | 1417 | ArrayList pathListB = new ArrayList(); 1418 | 1419 | Point point1 = (Point)meanArr.vRange[0]; 1420 | 1421 | int k = 0; 1422 | 1423 | //从meanline以上区域第一个区块的左下角至与meanline交点在竖直方向以竖直线分割 1424 | for (k = mBoundary.leftDownPoint.X; k < GuidePoint.Y; k++) 1425 | { 1426 | Point tmpP = new Point(); 1427 | 1428 | tmpP.X = k; 1429 | tmpP.Y = point1.X; 1430 | 1431 | pathListB.Add(tmpP); 1432 | } 1433 | 1434 | //从从meanline以上区域第一个区块的左下角至meanline交点处在水平方向以横线分割 1435 | for (k = point1.X; k < meanArr.vInter.X; k++) 1436 | { 1437 | Point tmpP = new Point(); 1438 | 1439 | tmpP.X = GuidePoint.Y; 1440 | tmpP.Y = k; 1441 | 1442 | pathListB.Add(tmpP); 1443 | } 1444 | 1445 | for (k = 0; k < pathListA.Count; k++) 1446 | { 1447 | pathListB.Add(pathListA[k]); 1448 | } 1449 | 1450 | //按字符的规则而言,符合该条件的可能是字符A,Z,7,若为字符Z/7,那么分割完成后,左侧区域有一定几率没有有效字符 1451 | //确认按照该规则分割出的左侧区域是否包含有效字符像素 1452 | pathListA.Clear(); 1453 | 1454 | //当前粘连区域左侧竖线作为判断基准 1455 | for (k = mBoundary.leftDownPoint.X; k < mBoundary.rightUpPoint.X; k++) 1456 | { 1457 | Point tmpP = new Point(); 1458 | 1459 | tmpP.X = k; 1460 | tmpP.Y = mBoundary.leftDownPoint.Y; 1461 | 1462 | pathListA.Add(tmpP); 1463 | } 1464 | 1465 | SegAreaInfo tmpInfo = GetSegInfo(pathListA, pathListB); 1466 | 1467 | if (tmpInfo.blackPixCount > bThVal)//当前分割路径为有效路径 1468 | { 1469 | //SegAreaList.Add(tmpInfo); 1470 | SegPathList.Insert(newPos, pathListB); 1471 | 1472 | bValid = true; 1473 | } 1474 | 1475 | return bValid; 1476 | } 1477 | 1478 | public static bool SegByMeanRight(Point segP, VerPro meanArr, Boundary mBoundary, int bThVal, int newPos) 1479 | { 1480 | bool bValid = false; 1481 | 1482 | ArrayList pathListA = dropFallUp(segP); 1483 | 1484 | ArrayList pathListB = dropFallDown(segP); 1485 | 1486 | int k = 0; 1487 | 1488 | for (k = 0; k < pathListA.Count; k++) 1489 | { 1490 | pathListB.Add(pathListA[k]); 1491 | } 1492 | 1493 | pathListA.Clear(); 1494 | 1495 | for (k = mBoundary.leftDownPoint.X; k < mBoundary.rightUpPoint.X; k++) 1496 | { 1497 | Point tmpP = new Point(); 1498 | 1499 | tmpP.X = k; 1500 | tmpP.Y = mBoundary.rightUpPoint.Y; 1501 | 1502 | pathListA.Add(tmpP); 1503 | } 1504 | 1505 | SegAreaInfo tmpInfo = GetSegInfo(pathListB, pathListA); 1506 | 1507 | if (tmpInfo.blackPixCount > bThVal)//当前分割路径为有效路径 1508 | { 1509 | //SegAreaList.Add(tmpInfo); 1510 | SegPathList.Insert(newPos, pathListB); 1511 | 1512 | bValid = true; 1513 | } 1514 | 1515 | return bValid; 1516 | } 1517 | 1518 | public static bool SegByBaseLine(Point segP, VerPro baseArr, Boundary mBoundary, int bThVal, int newPos, bool bLeft) 1519 | { 1520 | bool bValid = false; 1521 | 1522 | //以交点为起点分别执行向上滴落和向下滴落 1523 | ArrayList pathListA = dropFallUp(segP); 1524 | 1525 | ArrayList pathListB = dropFallDown(segP); 1526 | 1527 | SegAreaInfo tmpInfo = new SegAreaInfo(); 1528 | 1529 | int k = 0; 1530 | 1531 | for (k = 0; k < pathListA.Count; k++) 1532 | { 1533 | pathListB.Add(pathListA[k]); 1534 | } 1535 | 1536 | //按字符的规则而言,符合该条件的可能是字符y,j,那么分割完成后,左侧区域有一定几率没有有效字符 1537 | //确认按照该规则分割出的左侧区域是否包含有效字符像素 1538 | pathListA.Clear(); 1539 | 1540 | //当前粘连区域左侧竖线作为判断基准 1541 | if (bLeft) 1542 | { 1543 | for (k = mBoundary.leftDownPoint.X; k < mBoundary.rightUpPoint.X; k++) 1544 | { 1545 | Point tmpP = new Point(); 1546 | 1547 | tmpP.X = k; 1548 | tmpP.Y = mBoundary.leftDownPoint.Y; 1549 | 1550 | pathListA.Add(tmpP); 1551 | } 1552 | 1553 | tmpInfo = GetSegInfo(pathListA, pathListB); 1554 | } 1555 | else 1556 | { 1557 | for (k = mBoundary.leftDownPoint.X; k < mBoundary.rightUpPoint.X; k++) 1558 | { 1559 | Point tmpP = new Point(); 1560 | 1561 | tmpP.X = k; 1562 | tmpP.Y = mBoundary.rightUpPoint.Y; 1563 | 1564 | pathListA.Add(tmpP); 1565 | } 1566 | 1567 | tmpInfo = GetSegInfo(pathListB, pathListA); 1568 | } 1569 | 1570 | if (tmpInfo.blackPixCount > bThVal)//当前分割路径为有效路径 1571 | { 1572 | //SegAreaList.Add(tmpInfo); 1573 | SegPathList.Insert(newPos, pathListB); 1574 | 1575 | bValid = true; 1576 | } 1577 | 1578 | return bValid; 1579 | } 1580 | 1581 | public static ArrayList GetProBoundary(Point proRange, bool beBaseline,int iniX, int endX) 1582 | { 1583 | int i = 0, j = 0, k = 0; 1584 | 1585 | int[] wHistogram = new int[ImageWidth]; 1586 | int[] hHistogram = new int[ImageHeight]; 1587 | 1588 | //对字符所包含的封闭区间进行竖直投影 1589 | for (i = iniX; i < endX; i++) 1590 | { 1591 | for (j = proRange.X; j < proRange.Y; j++) 1592 | { 1593 | if (0 == BinaryArray[i, j]) 1594 | { 1595 | wHistogram[j]++; 1596 | } 1597 | } 1598 | } 1599 | 1600 | ArrayList loopArray = new ArrayList();//compose of rightUpPoint and leftDownPoint 1601 | 1602 | j = proRange.X; 1603 | 1604 | //根据封闭区间的竖直投影获取封闭区间的左右与上下极限位罿 1605 | while (j < proRange.Y) 1606 | { 1607 | if (wHistogram[j] != 0) 1608 | { 1609 | Boundary loop = new Boundary(); 1610 | 1611 | //高为X轴,宽为Y轴,图片的左上角为原点 1612 | //Y轴方向离原点较远的顶点,X轴方向离原点较远的顶点,这两个点组成rightUp 1613 | Point posRightUp = new Point(); 1614 | //Y轴方向离原点较近的顶点,X轴方向离原点较近的顶点,这两个点组成LeftDown 1615 | Point posLeftDown = new Point(); 1616 | 1617 | posLeftDown.Y = j; 1618 | 1619 | while ((wHistogram[j] != 0) && (j < proRange.Y)) 1620 | { 1621 | j++; 1622 | } 1623 | 1624 | posRightUp.Y = j - 1; 1625 | 1626 | Array.Clear(hHistogram, 0, ImageHeight); 1627 | 1628 | //在已确定的左右区间内对封闭区域进行水平方向的投影 1629 | for (j = posLeftDown.Y; j < posRightUp.Y; j++) 1630 | { 1631 | for (i = iniX; i < endX; i++) 1632 | { 1633 | if (0 == BinaryArray[i, j]) 1634 | { 1635 | hHistogram[i]++; 1636 | } 1637 | } 1638 | } 1639 | //投影区域的左下角的X轴坐标为baseline的X值 1640 | posLeftDown.X = iniX; 1641 | 1642 | i = endX; 1643 | 1644 | //寻找投影区域右上角的X轴坐标 1645 | while ((0 == hHistogram[i]) && (i > iniX)) 1646 | { 1647 | i--; 1648 | } 1649 | 1650 | posRightUp.X = i; 1651 | 1652 | 1653 | loop.leftDownPoint = posLeftDown; 1654 | loop.rightUpPoint = posRightUp; 1655 | 1656 | loopArray.Add(loop); 1657 | 1658 | } 1659 | j++; 1660 | } 1661 | 1662 | if (beBaseline)//若是以baseline为基准的投影,则需要判断该投影区域是否有效 1663 | { 1664 | //遍历找到的封闭区间内的像素,至少包含一丿邻域的区域被认为是有效封闭区间,否则将被滤除 1665 | for (k = 0; k < loopArray.Count; k++) 1666 | { 1667 | Boundary tmpLoop = (Boundary)loopArray[k]; 1668 | 1669 | //若投影区域的X轴最大值离baseline的距离小于3个像素,则认为该投影区域为无效区域 1670 | if (tmpLoop.rightUpPoint.X - iniX < 3) 1671 | { 1672 | loopArray.RemoveAt(k); 1673 | 1674 | k--; 1675 | } 1676 | } 1677 | } 1678 | return loopArray; 1679 | } 1680 | 1681 | 1682 | 1683 | 1684 | 1685 | 1686 | /// 1687 | ///根据谷点获取图片的分割路径,水滴从上向下滴落 1688 | /// 1689 | /// 谷点列表 1690 | /// 图片中字符所在区域的边界 1691 | /// 二值化图片数组 1692 | /// 返回粘粘图片的分割路径 1693 | public static ArrayList dropFallUp(Point iniPos)//,int ImageHeight,int ImageWidth, ImgBoundary Boundary, Byte[,] BinaryArray) 1694 | { 1695 | int yIndex2 = ImgBoundary.rightUpPoint.Y; 1696 | 1697 | int yIndex1 = ImgBoundary.leftDownPoint.Y; 1698 | 1699 | int xIndex2 = ImgBoundary.rightUpPoint.X; 1700 | 1701 | int xIndex1 = ImgBoundary.leftDownPoint.X; 1702 | 1703 | ArrayList pathList = new ArrayList(); 1704 | 1705 | int iniPointY = iniPos.Y; 1706 | 1707 | int iniPointX = iniPos.X;//任一分割路径的高度起始点均为有效字符出现的第一个点 1708 | 1709 | byte leftRightFlg = 0;//该标志位置位时,表示情况5与情况6可能会循环出现 1710 | 1711 | while ((iniPointX <= xIndex2) && (iniPointY < yIndex2)) 1712 | { 1713 | Point newPos = new Point(); 1714 | 1715 | newPos.Y = iniPointY; 1716 | newPos.X = iniPointX; 1717 | 1718 | pathList.Add(newPos); 1719 | 1720 | if (((iniPointX + 1) >= ImageHeight) || ((iniPointX - 1) <= 0) || ((iniPointY + 1) >= ImageWidth) || ((iniPointY - 1) <= 0)) 1721 | { 1722 | break; 1723 | } 1724 | 1725 | Byte pointLeft = BinaryArray[iniPointX, (iniPointY - 1)]; 1726 | Byte pointRight = BinaryArray[iniPointX, (iniPointY + 1)]; 1727 | Byte pointDown = BinaryArray[(iniPointX + 1), iniPointY]; 1728 | Byte pointDownLeft = BinaryArray[(iniPointX + 1), (iniPointY - 1)]; 1729 | Byte pointDownRight = BinaryArray[(iniPointX + 1), (iniPointY + 1)]; 1730 | 1731 | 1732 | 1733 | if (((0 == pointLeft) && (0 == pointRight) && (0 == pointDown) && (0 == pointDownLeft) && ((0 == pointDownRight)))// all black:11111 1734 | || ((255 == pointLeft) && (255 == pointRight) && (255 == pointDown) && (255 == pointDownLeft) && ((255 == pointDownRight))//all white:00000 1735 | || ((0 == pointDownLeft) && (255 == pointDown))))//情况1与情况3 // **10* 1736 | {//down 1737 | iniPointX = iniPointX + 1; 1738 | leftRightFlg = 0; 1739 | } 1740 | else if (((255 == pointLeft) && (0 == pointDown) && (255 == pointDownLeft) && (0 == pointDownRight))//0*011 1741 | || ((0 == pointDown) && (255 == pointDownLeft) && (0 == pointDownRight) && (0 == pointLeft) && (0 == pointRight))//11011 1742 | || ((255 == pointLeft) && (0 == pointDown) && (255 == pointDownLeft) && (255 == pointDownRight)))//0*010 1743 | {//left down //情况2 1744 | iniPointX = iniPointX + 1; 1745 | iniPointY = iniPointY - 1; 1746 | 1747 | leftRightFlg = 0; 1748 | } 1749 | else if (((0 == pointDown) && (0 == pointDownLeft) && (255 == pointDownRight))//**110 1750 | || ((0 == pointLeft) && (255 == pointRight) && (0 == pointDown) && (255 == pointDownLeft) && (255 == pointDownRight)))//10010 1751 | {//情况4 1752 | iniPointX = iniPointX + 1; 1753 | iniPointY = iniPointY + 1; 1754 | 1755 | leftRightFlg = 0; 1756 | } 1757 | else if (((255 == pointRight) && (0 == pointDown) && (0 == pointDownLeft) && (0 == pointDownRight))//*0111 1758 | || ((0 == pointLeft) && (255 == pointRight) && (0 == pointDown) && (255 == pointDownLeft) && (0 == pointDownRight)))//10011 1759 | {//情况5 1760 | if (0 == leftRightFlg) 1761 | { 1762 | iniPointY = iniPointY + 1; 1763 | 1764 | leftRightFlg = 1; 1765 | 1766 | } 1767 | else//标志位为1时说明上一个点出现在情况6,而本次循环的点出现在情况5,此种情况将垂直渗透 1768 | { 1769 | iniPointX = iniPointX + 1; 1770 | 1771 | 1772 | leftRightFlg = 0; 1773 | } 1774 | } 1775 | else if ((255 == pointLeft) && (0 == pointDown) && (0 == pointDownLeft) && (0 == pointDownRight))//01111 1776 | {//情况6 1777 | if (0 == leftRightFlg) 1778 | { 1779 | iniPointY = iniPointY - 1; 1780 | 1781 | leftRightFlg = 1; 1782 | } 1783 | else//标志位为1时说明上一个点出现在情况5,而本次循环的点出现在情况6,此种情况将垂直渗透 1784 | { 1785 | iniPointX = iniPointX + 1; 1786 | 1787 | 1788 | leftRightFlg = 0; 1789 | } 1790 | } 1791 | else 1792 | { 1793 | iniPointX = iniPointX + 1; 1794 | } 1795 | 1796 | } 1797 | 1798 | return pathList; 1799 | } 1800 | 1801 | /// 1802 | ///根据谷点获取图片的分割路径,水滴从下向上滴落 1803 | /// 1804 | /// 谷点列表 1805 | /// 图片中字符所在区域的边界 1806 | /// 二值化图片数组 1807 | /// 返回粘粘图片的分割路径 1808 | public static ArrayList dropFallDown(Point iniPos)//, int ImageHeight, int ImageWidth, ImgBoundary Boundary, Byte[,] BinaryArray) 1809 | { 1810 | int yIndex2 = ImgBoundary.rightUpPoint.Y; 1811 | 1812 | int yIndex1 = ImgBoundary.leftDownPoint.Y; 1813 | 1814 | int xIndex2 = ImgBoundary.rightUpPoint.X; 1815 | 1816 | int xIndex1 = ImgBoundary.leftDownPoint.X; 1817 | 1818 | ArrayList pathList = new ArrayList(); 1819 | 1820 | int iniPointY = iniPos.Y; 1821 | 1822 | int iniPointX = iniPos.X;//任一分割路径的高度起始点均为有效字符出现的第一个点 1823 | 1824 | byte leftRightFlg = 0;//该标志位置位时,表示情况5与情况6可能会循环出现 1825 | 1826 | while ((iniPointX >= xIndex1) && (iniPointY < yIndex2)) 1827 | { 1828 | Point newPos = new Point(); 1829 | newPos.Y = iniPointY; 1830 | newPos.X = iniPointX; 1831 | 1832 | pathList.Add(newPos); 1833 | 1834 | 1835 | if (((iniPointX - 1) <= 0) || ((iniPointY + 1) >= ImageWidth) || ((iniPointY - 1) <= 0)) 1836 | { 1837 | break; 1838 | } 1839 | 1840 | Byte pointLeft = BinaryArray[iniPointX, (iniPointY - 1)]; 1841 | Byte pointRight = BinaryArray[iniPointX, (iniPointY + 1)]; 1842 | Byte pointDown = BinaryArray[(iniPointX - 1), iniPointY]; 1843 | Byte pointDownLeft = BinaryArray[(iniPointX - 1), (iniPointY - 1)]; 1844 | Byte pointDownRight = BinaryArray[(iniPointX - 1), (iniPointY + 1)]; 1845 | 1846 | 1847 | 1848 | if (((0 == pointLeft) && (0 == pointRight) && (0 == pointDown) && (0 == pointDownLeft) && ((0 == pointDownRight)))// all black 11111 1849 | || ((255 == pointLeft) && (255 == pointRight) && (255 == pointDown) && (255 == pointDownLeft) && ((255 == pointDownRight))//all white 00000 1850 | || ((0 == pointDownLeft) && (255 == pointDown))))//情况1与情况3 //**10* 1851 | {//down 1852 | iniPointX = iniPointX - 1; 1853 | leftRightFlg = 0; 1854 | } 1855 | else if (((255 == pointLeft) && (0 == pointDown) && (255 == pointDownLeft) && (0 == pointDownRight))//0*011 1856 | || ((0 == pointDown) && (255 == pointDownLeft) && (0 == pointDownRight) && (0 == pointLeft) && (0 == pointRight))//11011 1857 | || ((255 == pointLeft) && (0 == pointDown) && (255 == pointDownLeft) && (255 == pointDownRight)))//0*010 1858 | 1859 | {//left down //情况2 1860 | iniPointX = iniPointX - 1; 1861 | iniPointY = iniPointY - 1; 1862 | 1863 | leftRightFlg = 0; 1864 | } 1865 | else if (((0 == pointDown) && (0 == pointDownLeft) && (255 == pointDownRight))//**110 1866 | || ((0 == pointLeft) && (255 == pointRight) && (0 == pointDown) && (255 == pointDownLeft) && (255 == pointDownRight)))//10010 1867 | {//情况4 1868 | iniPointX = iniPointX - 1; 1869 | iniPointY = iniPointY + 1; 1870 | 1871 | leftRightFlg = 0; 1872 | } 1873 | else if (((255 == pointRight) && (0 == pointDown) && (0 == pointDownLeft) && (0 == pointDownRight))//*0111 1874 | || ((0 == pointLeft) && (255 == pointRight) && (0 == pointDown) && (255 == pointDownLeft) && (0 == pointDownRight)))//10011 1875 | {//情况5 1876 | if (0 == leftRightFlg) 1877 | { 1878 | iniPointY = iniPointY + 1; 1879 | 1880 | leftRightFlg = 1; 1881 | 1882 | } 1883 | else//标志位为1时说明上一个点出现在情况6,而本次循环的点出现在情况5,此种情况将垂直渗透 1884 | { 1885 | iniPointX = iniPointX - 1; 1886 | 1887 | 1888 | leftRightFlg = 0; 1889 | } 1890 | } 1891 | else if ((255 == pointLeft) && (0 == pointDown) && (0 == pointDownLeft) && (0 == pointDownRight))//01111 1892 | {//情况6 1893 | if (0 == leftRightFlg) 1894 | { 1895 | iniPointY = iniPointY - 1; 1896 | 1897 | leftRightFlg = 1; 1898 | } 1899 | else//标志位为1时说明上一个点出现在情况5,而本次循环的点出现在情况6,此种情况将垂直渗透 1900 | { 1901 | iniPointX = iniPointX - 1; 1902 | 1903 | 1904 | leftRightFlg = 0; 1905 | } 1906 | } 1907 | else 1908 | { 1909 | iniPointX = iniPointX - 1; 1910 | } 1911 | } 1912 | 1913 | pathList.Reverse(); 1914 | 1915 | return pathList; 1916 | } 1917 | 1918 | } 1919 | } 1920 | -------------------------------------------------------------------------------- /CAPTCHA/CAPTCHA/Preprocess.cs: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------- 2 | * 作者:livezingy 3 | * 4 | * 博客:http://www.livezingy.com 5 | * 6 | * 开发环境: 7 | * Visual Studio V2012 8 | * .NET Framework 4.5 9 | * 10 | * 版本历史: 11 | * V1.0 2015年07月14日 12 | 获取验证码二值化数组,灰度数组;有二值化数组,灰度数组等转换为位图 13 | --------------------------------------------------------- */ 14 | 15 | using System; 16 | using System.Collections.Generic; 17 | using System.Collections; 18 | using System.Linq; 19 | using System.Text; 20 | using System.Drawing; 21 | using System.Drawing.Imaging; 22 | 23 | using System.Runtime.InteropServices; 24 | 25 | using CaptchaSegment; 26 | 27 | 28 | namespace Preprocess 29 | { 30 | public class VerPro 31 | { 32 | //字符在参考线以上区域的投影区域集合 33 | private ArrayList _vRange; 34 | 35 | public ArrayList vRange 36 | { 37 | get { return _vRange; } 38 | set { _vRange = value; } 39 | } 40 | 41 | //字符与参考线的交点 42 | private Point _vInter; 43 | 44 | public Point vInter 45 | { 46 | get { return _vInter; } 47 | set { _vInter = value; } 48 | } 49 | } 50 | 51 | public static class Preprocess 52 | { 53 | /// 54 | /// 在二值化数组中寻找与给定坐标点连通的区域,并用涂上灰色 55 | /// 56 | /// 给定坐标行 57 | /// 给定坐标列 58 | /// 二值化图像的高度 59 | /// 二值化图像的宽度 60 | /// 二值化数组 61 | /// 返回与给定坐标点连通区域设定为灰色的灰度数组 62 | public static Byte[,] FillConnectedArea(int nRow, int nCol, int imageHeight, int imageWidth, Byte[,] BinaryArray) 63 | { 64 | for (int x = nRow - 1;x < nRow + 2;x ++) 65 | { 66 | for (int y = nCol - 1;y < nCol + 2;y ++) 67 | { 68 | if ((x >= 0) && (x < imageHeight) && (y >= 0) && (y < imageWidth)) 69 | { 70 | // if (255 == BinaryArray[x, y])//8邻域的方式寻找连通域 71 | if ((255 == BinaryArray[x, y]) && ((x == nRow) || (y == nCol)))//4邻域的方式寻找连通域 72 | { 73 | BinaryArray[x, y] = 128; 74 | 75 | FillConnectedArea(x, y, imageHeight, imageWidth, BinaryArray); 76 | } 77 | } 78 | } 79 | } 80 | 81 | return BinaryArray; 82 | } 83 | 84 | /// 85 | ///获取指定区域下黑色像素的竖直投影 86 | /// 87 | /// 待处理区域的左下角坐标 88 | /// 待处理区域的右上角坐标 89 | /// 参考线,1:baseline为参考线,2:meanline为参考线,0:无参考线 90 | /// 待处理的二值化数组 91 | /// 返回指定区域投影信息 92 | public static VerPro VerticalProjection(Point leftdown, Point rightUp, byte flg, Byte[,] srcArray) 93 | { 94 | int tmpNum = CAPTCHA.Program.Width; 95 | 96 | Int32[] vProjection = new Int32[tmpNum]; 97 | 98 | Array.Clear(vProjection, 0, tmpNum); 99 | 100 | Point guideP = CaptchaSegment.CaptchaSegment.GuidePoint; 101 | 102 | //记录当前投影区域的区块数目,相关信息在区块中以Point的形式存在,Point.x为区块数目,Point.y为该区块的起始位置 103 | ArrayList vpArea = new ArrayList(); 104 | 105 | VerPro infoPro = new VerPro(); 106 | 107 | Point vPoint = new Point(); 108 | Point interPoint = new Point(); 109 | 110 | int x = 0; 111 | int y = 0; 112 | 113 | //获取指定区域的竖直投影 114 | for (y = leftdown.Y; y < rightUp.Y; y++) 115 | { 116 | if(2 == flg) 117 | { 118 | x = leftdown.X; 119 | } 120 | else 121 | { 122 | x = leftdown.X + 1; 123 | } 124 | for (x = leftdown.X; x < rightUp.X; x++) 125 | { 126 | if (0 == srcArray[x, y]) 127 | { 128 | vProjection[y]++; 129 | } 130 | } 131 | } 132 | 133 | //获取竖直投影的区块 134 | int k = leftdown.Y; 135 | 136 | while (k < rightUp.Y) 137 | { 138 | int tmpMax = 0; 139 | 140 | if (vProjection[k] != 0) 141 | { 142 | vPoint.X = k - 1; 143 | 144 | while ((vProjection[k] != 0) && (k < rightUp.Y)) 145 | { 146 | k ++; 147 | } 148 | 149 | vPoint.Y = k; 150 | 151 | if(2 == flg) 152 | { 153 | tmpMax = rightUp.X; 154 | //若meanline为基准线,则找到当前区域字符像素X值最小的点 155 | for (y = vPoint.X; y < vPoint.Y; y++) 156 | { 157 | for (x = leftdown.X; x < rightUp.X; x++) 158 | { 159 | if ((0 == srcArray[x, y]) && (tmpMax > x)) 160 | { 161 | tmpMax = x; 162 | break; 163 | } 164 | } 165 | } 166 | } 167 | else if(1 == flg) 168 | { 169 | tmpMax = leftdown.X; 170 | //若baseline为基准线,则找到当前区域字符像素X值最大的点 171 | for (y = vPoint.X; y < vPoint.Y; y++) 172 | { 173 | for (x = rightUp.X; x > leftdown.X; x--) 174 | { 175 | if ((0 == srcArray[x, y]) && (tmpMax < x)) 176 | { 177 | tmpMax = x; 178 | break; 179 | } 180 | } 181 | } 182 | } 183 | 184 | //在meanline或baseline之外2个像素的区域被记录,否则被认定为无效区域 185 | if ((((tmpMax - guideP.X) > 1) && (1 == flg)) || (((guideP.Y - tmpMax) > 2)) && (2 == flg) || (0 == flg)) 186 | { 187 | vpArea.Add(vPoint); 188 | } 189 | } 190 | k ++; 191 | } 192 | 193 | if ((flg != 0) && (vpArea.Count > 0)) 194 | { 195 | if (2 == flg) 196 | { 197 | tmpNum = guideP.Y; 198 | } 199 | else 200 | { 201 | tmpNum = guideP.X; 202 | } 203 | 204 | //从第一个有效投影区域开始从左至右找字符与参考线的左侧交点 205 | vPoint = (Point)vpArea[0]; 206 | y = vPoint.X; 207 | while (y < vPoint.Y) 208 | { 209 | if (0 == srcArray[tmpNum,y]) 210 | { 211 | interPoint.X = y; 212 | break; 213 | } 214 | y++; 215 | } 216 | 217 | 218 | //从最后一个有效投影区域开始从右至左找字符与参考线的右侧交点 219 | vPoint = (Point)vpArea[vpArea.Count - 1]; 220 | y = vPoint.Y; 221 | while (y > vPoint.X) 222 | { 223 | if (0 == srcArray[tmpNum, y]) 224 | { 225 | interPoint.Y = y; 226 | break; 227 | } 228 | y--; 229 | } 230 | } 231 | 232 | infoPro.vRange = vpArea; 233 | infoPro.vInter = interPoint; 234 | return infoPro; 235 | } 236 | 237 | 238 | /// 239 | ///获取字符所在区域的边界 240 | /// 241 | /// 二值化图像的高度 242 | /// 二值化图像的宽度 243 | /// 二值化数组 244 | /// 返回字符所在区域的边界 245 | public static Boundary getImgBoundary(int imageHeight, int imageWidth, Byte[,] BinaryArray) 246 | { 247 | Boundary boundary = new Boundary(); 248 | 249 | Point leftDownP = new Point(); 250 | Point rightUpP = new Point(); 251 | 252 | Int32[] verticalPoints = new Int32[imageWidth]; 253 | Array.Clear(verticalPoints, 0, imageWidth); 254 | 255 | int x = 0, y = 0; 256 | 257 | for (x = 0; x < imageHeight; x++) 258 | { 259 | for (y = 0; y < imageWidth; y++) 260 | { 261 | if (0 == BinaryArray[x, y]) 262 | { 263 | verticalPoints[y]++; 264 | } 265 | } 266 | } 267 | 268 | //用于存储当前横坐标水平方向上的有效像素点数量(组成字符的像素点) 269 | Int32[] horPoints = new Int32[imageHeight]; 270 | Array.Clear(horPoints, 0, imageHeight); 271 | 272 | //统计源图像的二值化数组中在每一个横坐标的水平方向所包含的像素点数 273 | 274 | for (y = 0; y < imageWidth; y++) 275 | { 276 | for (x = 0; x < imageHeight; x++) 277 | { 278 | if (0 == BinaryArray[x, y]) 279 | { 280 | horPoints[x]++; 281 | } 282 | } 283 | } 284 | 285 | //从原点开始,在竖直投影中找到左下角的Y坐标 286 | for(y = 0; y < imageWidth; y++) 287 | { 288 | if(verticalPoints[y] != 0) 289 | { 290 | if(y != 0) 291 | { 292 | leftDownP.Y = y - 1; 293 | } 294 | else 295 | { 296 | leftDownP.Y = y; 297 | } 298 | break; 299 | } 300 | } 301 | 302 | //从离原点最远的点开始,在竖直投影中找到右上角的Y坐标 303 | for(y = imageWidth - 1; y > 0; y--) 304 | { 305 | if(verticalPoints[y] != 0) 306 | { 307 | if (y != imageWidth - 1) 308 | { 309 | rightUpP.Y = y + 1; 310 | } 311 | else 312 | { 313 | rightUpP.Y = y; 314 | } 315 | break; 316 | } 317 | } 318 | 319 | //从原点开始,在竖直投影中找到左下角的X坐标 320 | for (x = 0; x < imageHeight; x++) 321 | { 322 | if (horPoints[x] != 0) 323 | { 324 | if(x != 0) 325 | { 326 | leftDownP.X = x - 1; 327 | } 328 | else 329 | { 330 | leftDownP.X = x; 331 | } 332 | break; 333 | } 334 | } 335 | 336 | //从离原点最远的点开始,在竖直投影中找到右上角的X坐标 337 | for (x = imageHeight - 1; x > 0; x--) 338 | { 339 | if (horPoints[x] != 0) 340 | { 341 | if (x != imageHeight - 1) 342 | { 343 | rightUpP.X = x + 1; 344 | } 345 | else 346 | { 347 | rightUpP.X = x; 348 | } 349 | break; 350 | } 351 | } 352 | 353 | boundary.leftDownPoint = leftDownP; 354 | boundary.rightUpPoint = rightUpP; 355 | 356 | return boundary; 357 | } 358 | 359 | /// 360 | /// 获取验证码字符的笔画宽度。横向与纵向分别统计黑色像素的宽度,默认在这两个方向上最多的宽度值为字符笔画宽度。 361 | /// 该方法默认有效字符为黑色,不适用于统计倾斜严重的验证码, 362 | /// 363 | /// 二值化图像的高度 364 | /// 二值化图像的宽度 365 | /// 二值化数组 366 | /// 返回字符所在区域的边界 367 | public static Int32 GetStrokeWid(int imageHeight, int imageWidth, Byte[,] BinaryArray) 368 | { 369 | int i, j; 370 | int nCount = 0; 371 | ArrayList nCountArray = new ArrayList(); 372 | 373 | //从宽度方向上统计黑色像素的宽度 374 | for (i = 0; i < imageHeight; i++) 375 | { 376 | for (j = 0; j < imageWidth; j++) 377 | { 378 | if (0 == BinaryArray[i, j]) 379 | { 380 | nCount++; 381 | } 382 | else 383 | { 384 | if (nCount != 0) 385 | { 386 | nCountArray.Add(nCount); 387 | } 388 | 389 | nCount = 0; 390 | } 391 | } 392 | } 393 | 394 | //从高度方向上统计黑色像素的宽度 395 | for (j = 0; j < imageWidth; j++) 396 | { 397 | for (i = 0; i < imageHeight; i++) 398 | { 399 | if (0 == BinaryArray[i, j]) 400 | { 401 | nCount++; 402 | } 403 | else 404 | { 405 | if (nCount != 0) 406 | { 407 | nCountArray.Add(nCount); 408 | } 409 | 410 | nCount = 0; 411 | } 412 | } 413 | } 414 | //将nCountArray中的数值按照从小到大的顺序进行排列 415 | nCountArray.Sort(); 416 | 417 | int tmpNum = nCountArray.Count; 418 | 419 | tmpNum = (int)nCountArray[tmpNum - 1] + 1; 420 | 421 | //统计宽度数最多的值 422 | int[] Histogram = new int[tmpNum]; 423 | Array.Clear(Histogram, 0, tmpNum); 424 | foreach (int val in nCountArray) 425 | { 426 | Histogram[val]++; 427 | } 428 | 429 | tmpNum = Histogram.Max(); 430 | int StrokeWid = 0; 431 | 432 | for (i = 0; i < 2; i ++ ) 433 | { 434 | tmpNum = Histogram.Max(); 435 | 436 | j = Array.IndexOf(Histogram, tmpNum); 437 | 438 | Histogram.SetValue(0, j); 439 | 440 | StrokeWid = StrokeWid + j; 441 | } 442 | 443 | return (int)Math.Round((Double)StrokeWid / 2); 444 | 445 | } 446 | 447 | /// 448 | /// 实现Sauvola算法实现图像二值化 449 | /// 450 | /// 用于存储二值化完成的图像 451 | /// 用于存储等待二值化完成的灰度图像 452 | public static Byte[,] Sauvola(Byte[,] gray_image) 453 | { 454 | //以当前像素点为中心的邻域的宽度 455 | int w = 40; 456 | 457 | //使用者自定义的修正系数 458 | double k = 0.3; 459 | 460 | //邻域边界距离中心点的距离 461 | int whalf = w >> 1; 462 | int MAXVAL = 256; 463 | 464 | int image_width = gray_image.GetLength(0); 465 | int image_height = gray_image.GetLength(1); 466 | 467 | Byte[,] bin_image = new Byte[image_width, image_height]; 468 | 469 | 470 | int[,] integral_image = new int[image_width, image_height]; 471 | int[,] integral_sqimg = new int[image_width, image_height]; 472 | int[,] rowsum_image = new int[image_width, image_height]; 473 | int[,] rowsum_sqimg = new int[image_width, image_height]; 474 | 475 | 476 | int xmin,ymin,xmax,ymax; 477 | double diagsum,idiagsum,diff,sqdiagsum,sqidiagsum,sqdiff,area; 478 | double mean,std,threshold; 479 | 480 | for (int j = 0; j < image_height; j++) 481 | { 482 | rowsum_image[0, j] = gray_image[0, j]; 483 | rowsum_sqimg[0, j] = gray_image[0, j] * gray_image[0, j]; 484 | } 485 | for (int i = 1; i < image_width; i++) 486 | { 487 | for (int j = 0; j < image_height; j++) 488 | { 489 | //计算图像范围内任意宽度窗口(邻域)的灰度值之和 490 | rowsum_image[i, j] = rowsum_image[i - 1, j] + gray_image[i, j]; 491 | 492 | //计算图像范围内任意宽度窗口(邻域)的灰度值平方之和 493 | rowsum_sqimg[i, j] = rowsum_sqimg[i - 1, j] + gray_image[i, j] * gray_image[i, j]; 494 | } 495 | } 496 | 497 | for (int i = 0; i < image_width; i++) 498 | { 499 | integral_image[i, 0] = rowsum_image[i, 0]; 500 | integral_sqimg[i, 0] = rowsum_sqimg[i, 0]; 501 | } 502 | for (int i = 0; i < image_width; i++) 503 | { 504 | for (int j = 1; j < image_height; j++) 505 | { 506 | //计算图像范围内任意宽度窗口(邻域)的灰度值的积分 507 | integral_image[i, j] = integral_image[i, j - 1] + rowsum_image[i, j]; 508 | 509 | //计算图像范围内任意宽度窗口(邻域)的灰度值平方的积分 510 | integral_sqimg[i, j] = integral_sqimg[i, j - 1] + rowsum_sqimg[i, j]; 511 | } 512 | } 513 | 514 | //Calculate the mean and standard deviation using the integral image 515 | 516 | for(int i=0; i= 0): 525 | // we'll prove that (xmax-xmin+1) > 0, 526 | // (ymax-ymin+1) is analogous 527 | // It's the same as to prove: xmax >= xmin 528 | // image_width - 1 >= 0 since image_width > i >= 0 529 | // i + whalf >= 0 since i >= 0, whalf >= 0 530 | // i + whalf >= i - whalf since whalf >= 0 531 | // image_width - 1 >= i - whalf since image_width > i 532 | // --IM 533 | if (area <= 0) 534 | throw new Exception("Binarize: area can't be 0 here"); 535 | if (xmin == 0 && ymin == 0) 536 | { // Point at origin 537 | diff = integral_image[xmax, ymax]; 538 | sqdiff = integral_sqimg[xmax, ymax]; 539 | } 540 | else if (xmin == 0 && ymin > 0) 541 | { // first column 542 | diff = integral_image[xmax, ymax] - integral_image[xmax, ymin - 1]; 543 | sqdiff = integral_sqimg[xmax, ymax] - integral_sqimg[xmax, ymin - 1]; 544 | } 545 | else if (xmin > 0 && ymin == 0) 546 | { // first row 547 | diff = integral_image[xmax, ymax] - integral_image[xmin - 1, ymax]; 548 | sqdiff = integral_sqimg[xmax, ymax] - integral_sqimg[xmin - 1, ymax]; 549 | } 550 | else 551 | { // rest of the image 552 | diagsum = integral_image[xmax, ymax] + integral_image[xmin - 1, ymin - 1]; 553 | idiagsum = integral_image[xmax, ymin - 1] + integral_image[xmin - 1, ymax]; 554 | //以(i,j)为中心点的w邻域内灰度值的积分 555 | diff = diagsum - idiagsum; 556 | 557 | sqdiagsum = integral_sqimg[xmax, ymax] + integral_sqimg[xmin - 1, ymin - 1]; 558 | sqidiagsum = integral_sqimg[xmax, ymin - 1] + integral_sqimg[xmin - 1, ymax]; 559 | //以(i,j)为中心点的w邻域内灰度值平方的积分 560 | sqdiff = sqdiagsum - sqidiagsum; 561 | } 562 | 563 | //以(i,j)为中心点的w邻域内的灰度均值 564 | mean = diff/area; 565 | 566 | //以(i,j)为中心点的w邻域内的标准方差 567 | std = Math.Sqrt((sqdiff - diff*diff/area)/(area-1)); 568 | 569 | //根据Sauvola计算公式和以(i,j)为中心点的w邻域内的灰度均值与标准方差来计算当前点(i,j)的二值化阈值 570 | threshold = mean*(1+k*((std/128)-1)); 571 | 572 | //根据当前点的阈值对当前像素点进行二值化 573 | if(gray_image[i,j] < threshold) 574 | bin_image[i,j] = 0; 575 | else 576 | bin_image[i,j] = (byte)(MAXVAL-1); 577 | } 578 | } 579 | 580 | return bin_image; 581 | } 582 | 583 | /// 584 | /// 图像二值化方法:大津法和迭代法 585 | /// 586 | public enum BinarizationMethods 587 | { 588 | Otsu, // 大津法 589 | Iterative // 迭代法 590 | } 591 | 592 | /// 593 | /// 全局阈值图像二值化 594 | /// 595 | /// 原始图像 596 | /// 二值化方法 597 | /// 输出:全局阈值 598 | /// 二值化图像 599 | public static Bitmap ToBinaryBitmap(this Bitmap bmp, BinarizationMethods method, out Int32 threshold) 600 | { // 位图转换为灰度数组 601 | Byte[,] GrayArray = bmp.ToGrayArray(); 602 | 603 | // 计算全局阈值 604 | if (method == BinarizationMethods.Otsu) 605 | threshold = OtsuThreshold(GrayArray); 606 | else 607 | threshold = IterativeThreshold(GrayArray); 608 | 609 | // 将灰度数组转换为二值数据 610 | Int32 PixelHeight = bmp.Height; 611 | Int32 PixelWidth = bmp.Width; 612 | Int32 Stride = ((PixelWidth + 31) >> 5) << 2; 613 | Byte[] Pixels = new Byte[PixelHeight * Stride]; 614 | for (Int32 i = 0; i < PixelHeight; i++) 615 | { 616 | Int32 Base = i * Stride; 617 | for (Int32 j = 0; j < PixelWidth; j++) 618 | { 619 | if (GrayArray[i, j] > threshold) 620 | { 621 | Pixels[Base + (j >> 3)] |= Convert.ToByte(0x80 >> (j & 0x7)); 622 | } 623 | } 624 | } 625 | 626 | // 从二值数据中创建黑白图像 627 | Bitmap BinaryBmp = new Bitmap(PixelWidth, PixelHeight, PixelFormat.Format1bppIndexed); 628 | 629 | // 设置调色表 630 | ColorPalette cp = BinaryBmp.Palette; 631 | cp.Entries[0] = Color.Black; // 黑色 632 | cp.Entries[1] = Color.White; // 白色 633 | BinaryBmp.Palette = cp; 634 | 635 | // 设置位图图像特性 636 | BitmapData BinaryBmpData = BinaryBmp.LockBits(new Rectangle(0, 0, PixelWidth, PixelHeight), ImageLockMode.WriteOnly, PixelFormat.Format1bppIndexed); 637 | Marshal.Copy(Pixels, 0, BinaryBmpData.Scan0, Pixels.Length); 638 | BinaryBmp.UnlockBits(BinaryBmpData); 639 | 640 | return BinaryBmp; 641 | } 642 | 643 | /// 644 | /// 全局阈值图像二值化 645 | /// 646 | /// 原始图像 647 | /// 二值化方法 648 | /// 输出:全局阈值 649 | /// 二值化后的图像数组 650 | public static Byte[,] ToBinaryArray(this Bitmap bmp, BinarizationMethods method, out Int32 threshold) 651 | { // 位图转换为灰度数组 652 | Byte[,] GrayArray = bmp.ToGrayArray(); 653 | 654 | // 计算全局阈值 655 | if (method == BinarizationMethods.Otsu) 656 | threshold = OtsuThreshold(GrayArray); 657 | else 658 | threshold = IterativeThreshold(GrayArray); 659 | 660 | // 根据阈值进行二值化 661 | Int32 PixelHeight = bmp.Height; 662 | Int32 PixelWidth = bmp.Width; 663 | Byte[,] BinaryArray = new Byte[PixelHeight, PixelWidth]; 664 | for (Int32 i = 0; i < PixelHeight; i++) 665 | { 666 | for (Int32 j = 0; j < PixelWidth; j++) 667 | { 668 | BinaryArray[i, j] = Convert.ToByte((GrayArray[i, j] > threshold) ? 255 : 0); 669 | } 670 | } 671 | 672 | return BinaryArray; 673 | } 674 | 675 | /// 676 | /// 大津法计算阈值 677 | /// 678 | /// 灰度数组 679 | /// 二值化阈值 680 | public static Int32 OtsuThreshold(Byte[,] grayArray) 681 | { // 建立统计直方图 682 | Int32[] Histogram = new Int32[256]; 683 | Array.Clear(Histogram, 0, 256); // 初始化 684 | foreach (Byte b in grayArray) 685 | { 686 | Histogram[b]++; // 统计直方图 687 | } 688 | 689 | // 总的质量矩和图像点数 690 | Int32 SumC = grayArray.Length; // 总的图像点数 691 | Double SumU = 0; // 双精度避免方差运算中数据溢出 692 | for (Int32 i = 1; i < 256; i++) 693 | { 694 | SumU += i * Histogram[i]; // 总的质量矩 695 | } 696 | 697 | // 灰度区间 698 | Int32 MinGrayLevel = Array.FindIndex(Histogram, NonZero); // 最小灰度值 699 | Int32 MaxGrayLevel = Array.FindLastIndex(Histogram, NonZero); // 最大灰度值 700 | 701 | // 计算最大类间方差 702 | Int32 Threshold = MinGrayLevel; 703 | Double MaxVariance = 0.0; // 初始最大方差 704 | Double U0 = 0; // 初始目标质量矩 705 | Int32 C0 = 0; // 初始目标点数 706 | for (Int32 i = MinGrayLevel; i < MaxGrayLevel; i++) 707 | { 708 | if (Histogram[i] == 0) continue; 709 | 710 | // 目标的质量矩和点数 711 | U0 += i * Histogram[i]; 712 | C0 += Histogram[i]; 713 | 714 | // 计算目标和背景的类间方差 715 | Double Diference = U0 * SumC - SumU * C0; 716 | Double Variance = Diference * Diference / C0 / (SumC - C0); // 方差 717 | if (Variance > MaxVariance) 718 | { 719 | MaxVariance = Variance; 720 | Threshold = i; 721 | } 722 | } 723 | 724 | // 返回类间方差最大阈值 725 | return Threshold; 726 | } 727 | 728 | /// 729 | /// 检测非零值 730 | /// 731 | /// 要检测的数值 732 | /// 733 | /// true:非零 734 | /// false:零 735 | /// 736 | private static Boolean NonZero(Int32 value) 737 | { 738 | return (value != 0) ? true : false; 739 | } 740 | 741 | /// 742 | /// 迭代法计算阈值 743 | /// 744 | /// 灰度数组 745 | /// 二值化阈值 746 | public static Int32 IterativeThreshold(Byte[,] grayArray) 747 | { // 建立统计直方图 748 | Int32[] Histogram = new Int32[256]; 749 | Array.Clear(Histogram, 0, 256); // 初始化 750 | foreach (Byte b in grayArray) 751 | { 752 | Histogram[b]++; // 统计直方图 753 | } 754 | 755 | // 总的质量矩和图像点数 756 | Int32 SumC = grayArray.Length; // 总的图像点数 757 | Int32 SumU = 0; 758 | for (Int32 i = 1; i < 256; i++) 759 | { 760 | SumU += i * Histogram[i]; // 总的质量矩 761 | } 762 | 763 | // 确定初始阈值 764 | Int32 MinGrayLevel = Array.FindIndex(Histogram, NonZero); // 最小灰度值 765 | Int32 MaxGrayLevel = Array.FindLastIndex(Histogram, NonZero); // 最大灰度值 766 | Int32 T0 = (MinGrayLevel + MaxGrayLevel) >> 1; 767 | if (MinGrayLevel != MaxGrayLevel) 768 | { 769 | for (Int32 Iteration = 0; Iteration < 100; Iteration++) 770 | { // 计算目标的质量矩和点数 771 | Int32 U0 = 0; 772 | Int32 C0 = 0; 773 | for (Int32 i = MinGrayLevel; i <= T0; i++) 774 | { // 目标的质量矩和点数 775 | U0 += i * Histogram[i]; 776 | C0 += Histogram[i]; 777 | } 778 | 779 | // 目标的平均灰度值和背景的平均灰度值的中心值 780 | Int32 T1 = (U0 / C0 + (SumU - U0) / (SumC - C0)) >> 1; 781 | if (T0 == T1) break; else T0 = T1; 782 | } 783 | } 784 | 785 | // 返回最佳阈值 786 | return T0; 787 | } 788 | 789 | /// 790 | /// 将原始图像转换成格式为Bgr565的16位图像 791 | /// 792 | /// 用于转换的原始图像 793 | /// 转换后格式为Bgr565的16位图像 794 | public static Bitmap ToBgr565(this Bitmap bmp) 795 | { 796 | Int32 PixelHeight = bmp.Height; // 图像高度 797 | Int32 PixelWidth = bmp.Width; // 图像宽度 798 | Int32 Stride = ((PixelWidth * 3 + 3) >> 2) << 2; // 跨距宽度 799 | Byte[] Pixels = new Byte[PixelHeight * Stride]; 800 | 801 | // 锁定位图到系统内存 802 | BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, PixelWidth, PixelHeight), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); 803 | Marshal.Copy(bmpData.Scan0, Pixels, 0, Pixels.Length); // 从非托管内存拷贝数据到托管内存 804 | bmp.UnlockBits(bmpData); // 从系统内存解锁位图 805 | 806 | // Bgr565格式为 RRRRR GGGGGG BBBBB 807 | Int32 TargetStride = ((PixelWidth + 1) >> 1) << 2; // 每个像素占2字节,且跨距要求4字节对齐 808 | Byte[] TargetPixels = new Byte[PixelHeight * TargetStride]; 809 | for (Int32 i = 0; i < PixelHeight; i++) 810 | { 811 | Int32 Index = i * Stride; 812 | Int32 Loc = i * TargetStride; 813 | for (Int32 j = 0; j < PixelWidth; j++) 814 | { 815 | Byte B = Pixels[Index++]; 816 | Byte G = Pixels[Index++]; 817 | Byte R = Pixels[Index++]; 818 | 819 | TargetPixels[Loc++] = (Byte)(((G << 3) & 0xe0) | ((B >> 3) & 0x1f)); 820 | TargetPixels[Loc++] = (Byte)((R & 0xf8) | ((G >> 5) & 7)); 821 | } 822 | } 823 | 824 | // 创建Bgr565图像 825 | Bitmap TargetBmp = new Bitmap(PixelWidth, PixelHeight, PixelFormat.Format16bppRgb565); 826 | 827 | // 设置位图图像特性 828 | BitmapData TargetBmpData = TargetBmp.LockBits(new Rectangle(0, 0, PixelWidth, PixelHeight), ImageLockMode.WriteOnly, PixelFormat.Format16bppRgb565); 829 | Marshal.Copy(TargetPixels, 0, TargetBmpData.Scan0, TargetPixels.Length); 830 | TargetBmp.UnlockBits(TargetBmpData); 831 | 832 | return TargetBmp; 833 | } 834 | 835 | /// 836 | /// 将原始图像转换成格式为Bgr555的16位图像 837 | /// 838 | /// 用于转换的原始图像 839 | /// 转换后格式为Bgr555的16位图像 840 | public static Bitmap ToBgr555(this Bitmap bmp) 841 | { 842 | Int32 PixelHeight = bmp.Height; // 图像高度 843 | Int32 PixelWidth = bmp.Width; // 图像宽度 844 | Int32 Stride = ((PixelWidth * 3 + 3) >> 2) << 2; // 跨距宽度 845 | Byte[] Pixels = new Byte[PixelHeight * Stride]; 846 | 847 | // 锁定位图到系统内存 848 | BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, PixelWidth, PixelHeight), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); 849 | Marshal.Copy(bmpData.Scan0, Pixels, 0, Pixels.Length); // 从非托管内存拷贝数据到托管内存 850 | bmp.UnlockBits(bmpData); // 从系统内存解锁位图 851 | 852 | // Bgr555格式为 X RRRRR GGGGG BBBBB 853 | Int32 TargetStride = ((PixelWidth + 1) >> 1) << 2; // 每个像素占2字节,且跨距要求4字节对齐 854 | Byte[] TargetPixels = new Byte[PixelHeight * TargetStride]; 855 | for (Int32 i = 0; i < PixelHeight; i++) 856 | { 857 | Int32 Index = i * Stride; 858 | Int32 Loc = i * TargetStride; 859 | for (Int32 j = 0; j < PixelWidth; j++) 860 | { 861 | Byte B = Pixels[Index++]; 862 | Byte G = Pixels[Index++]; 863 | Byte R = Pixels[Index++]; 864 | 865 | TargetPixels[Loc++] = (Byte)(((G << 2) & 0xe0) | ((B >> 3) & 0x1f)); 866 | TargetPixels[Loc++] = (Byte)(((R >> 1) & 0x7c) | ((G >> 6) & 3)); 867 | } 868 | } 869 | 870 | // 创建Bgr555图像 871 | Bitmap TargetBmp = new Bitmap(PixelWidth, PixelHeight, PixelFormat.Format16bppRgb555); 872 | 873 | // 设置位图图像特性 874 | BitmapData TargetBmpData = TargetBmp.LockBits(new Rectangle(0, 0, PixelWidth, PixelHeight), ImageLockMode.WriteOnly, PixelFormat.Format16bppRgb555); 875 | Marshal.Copy(TargetPixels, 0, TargetBmpData.Scan0, TargetPixels.Length); 876 | TargetBmp.UnlockBits(TargetBmpData); 877 | 878 | return TargetBmp; 879 | } 880 | 881 | 882 | /// 883 | /// 将位图转换为彩色数组 884 | /// 885 | /// 原始位图 886 | /// 彩色数组 887 | public static Color[,] ToColorArray(this Bitmap bmp) 888 | { 889 | Int32 PixelHeight = bmp.Height; // 图像高度 890 | Int32 PixelWidth = bmp.Width; // 图像宽度 891 | Int32[] Pixels = new Int32[PixelHeight * PixelWidth]; 892 | 893 | // 锁定位图到系统内存 894 | BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, PixelWidth, PixelHeight), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); 895 | Marshal.Copy(bmpData.Scan0, Pixels, 0, Pixels.Length); // 从非托管内存拷贝数据到托管内存 896 | bmp.UnlockBits(bmpData); // 从系统内存解锁位图 897 | 898 | // 将像素数据转换为彩色数组 899 | Color[,] ColorArray = new Color[PixelHeight, PixelWidth]; 900 | for (Int32 i = 0; i < PixelHeight; i++) 901 | { 902 | for (Int32 j = 0; j < PixelWidth; j++) 903 | { 904 | ColorArray[i, j] = Color.FromArgb(Pixels[i * PixelWidth + j]); 905 | } 906 | } 907 | 908 | 909 | 910 | return ColorArray; 911 | } 912 | 913 | /// 914 | /// 将位图转换为灰度数组(256级灰度) 915 | /// 916 | /// 原始位图 917 | /// 灰度数组 918 | public static Byte[,] ToGrayArray(this Bitmap bmp) 919 | { 920 | Int32 PixelHeight = bmp.Height; // 图像高度 921 | Int32 PixelWidth = bmp.Width; // 图像宽度 922 | Int32 Stride = ((PixelWidth * 3 + 3) >> 2) << 2; // 跨距宽度 923 | Byte[] Pixels = new Byte[PixelHeight * Stride]; 924 | 925 | // 锁定位图到系统内存 926 | BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, PixelWidth, PixelHeight), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); 927 | Marshal.Copy(bmpData.Scan0, Pixels, 0, Pixels.Length); // 从非托管内存拷贝数据到托管内存 928 | bmp.UnlockBits(bmpData); // 从系统内存解锁位图 929 | 930 | // 将像素数据转换为灰度数组 931 | Byte[,] GrayArray = new Byte[PixelHeight, PixelWidth]; 932 | for (Int32 i = 0; i < PixelHeight; i++) 933 | { 934 | Int32 Index = i * Stride; 935 | for (Int32 j = 0; j < PixelWidth; j++) 936 | { 937 | GrayArray[i, j] = Convert.ToByte((Pixels[Index + 2] * 19595 + Pixels[Index + 1] * 38469 + Pixels[Index] * 7471 + 32768) >> 16); 938 | Index += 3; 939 | } 940 | } 941 | 942 | return GrayArray; 943 | } 944 | 945 | /// 946 | /// 位图灰度化 947 | /// 948 | /// 原始位图 949 | /// 灰度位图 950 | public static Bitmap ToGrayBitmap(this Bitmap bmp) 951 | { 952 | Int32 PixelHeight = bmp.Height; // 图像高度 953 | Int32 PixelWidth = bmp.Width; // 图像宽度 954 | Int32 Stride = ((PixelWidth * 3 + 3) >> 2) << 2; // 跨距宽度 955 | Byte[] Pixels = new Byte[PixelHeight * Stride]; 956 | 957 | // 锁定位图到系统内存 958 | BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, PixelWidth, PixelHeight), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); 959 | Marshal.Copy(bmpData.Scan0, Pixels, 0, Pixels.Length); // 从非托管内存拷贝数据到托管内存 960 | bmp.UnlockBits(bmpData); // 从系统内存解锁位图 961 | 962 | // 将像素数据转换为灰度数据 963 | Int32 GrayStride = ((PixelWidth + 3) >> 2) << 2; 964 | Byte[] GrayPixels = new Byte[PixelHeight * GrayStride]; 965 | for (Int32 i = 0; i < PixelHeight; i++) 966 | { 967 | Int32 Index = i * Stride; 968 | Int32 GrayIndex = i * GrayStride; 969 | for (Int32 j = 0; j < PixelWidth; j++) 970 | { 971 | GrayPixels[GrayIndex++] = Convert.ToByte((Pixels[Index + 2] * 19595 + Pixels[Index + 1] * 38469 + Pixels[Index] * 7471 + 32768) >> 16); 972 | Index += 3; 973 | } 974 | } 975 | 976 | // 创建灰度图像 977 | Bitmap GrayBmp = new Bitmap(PixelWidth, PixelHeight, PixelFormat.Format8bppIndexed); 978 | 979 | // 设置调色表 980 | ColorPalette cp = GrayBmp.Palette; 981 | for (int i = 0; i < 256; i++) cp.Entries[i] = Color.FromArgb(i, i, i); 982 | GrayBmp.Palette = cp; 983 | 984 | // 设置位图图像特性 985 | BitmapData GrayBmpData = GrayBmp.LockBits(new Rectangle(0, 0, PixelWidth, PixelHeight), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); 986 | Marshal.Copy(GrayPixels, 0, GrayBmpData.Scan0, GrayPixels.Length); 987 | GrayBmp.UnlockBits(GrayBmpData); 988 | 989 | return GrayBmp; 990 | } 991 | 992 | /// 993 | /// 将灰度数组转换为灰度图像(256级灰度) 994 | /// 995 | /// 灰度数组 996 | /// 灰度图像 997 | public static Bitmap GrayArrayToGrayBitmap(Byte[,] grayArray) 998 | { // 将灰度数组转换为灰度数据 999 | Int32 PixelHeight = grayArray.GetLength(0); // 图像高度 1000 | Int32 PixelWidth = grayArray.GetLength(1); // 图像宽度 1001 | Int32 Stride = ((PixelWidth + 3) >> 2) << 2; // 跨距宽度 1002 | Byte[] Pixels = new Byte[PixelHeight * Stride]; 1003 | for (Int32 i = 0; i < PixelHeight; i++) 1004 | { 1005 | Int32 Index = i * Stride; 1006 | for (Int32 j = 0; j < PixelWidth; j++) 1007 | { 1008 | Pixels[Index++] = grayArray[i, j]; 1009 | } 1010 | } 1011 | 1012 | // 创建灰度图像 1013 | Bitmap GrayBmp = new Bitmap(PixelWidth, PixelHeight, PixelFormat.Format8bppIndexed); 1014 | 1015 | // 设置调色表 1016 | ColorPalette cp = GrayBmp.Palette; 1017 | for (int i = 0; i < 256; i++) cp.Entries[i] = Color.FromArgb(i, i, i); 1018 | GrayBmp.Palette = cp; 1019 | 1020 | // 设置位图图像特性 1021 | BitmapData GrayBmpData = GrayBmp.LockBits(new Rectangle(0, 0, PixelWidth, PixelHeight), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); 1022 | Marshal.Copy(Pixels, 0, GrayBmpData.Scan0, Pixels.Length); 1023 | GrayBmp.UnlockBits(GrayBmpData); 1024 | 1025 | return GrayBmp; 1026 | } 1027 | 1028 | /// 1029 | /// 将二值化数组转换为二值化图像 1030 | /// 1031 | /// 二值化数组 1032 | /// 二值化图像 1033 | public static Bitmap BinaryArrayToBinaryBitmap(Byte[,] binaryArray) 1034 | { // 将二值化数组转换为二值化数据 1035 | Int32 PixelHeight = binaryArray.GetLength(0); 1036 | Int32 PixelWidth = binaryArray.GetLength(1); 1037 | Int32 Stride = ((PixelWidth + 31) >> 5) << 2; 1038 | Byte[] Pixels = new Byte[PixelHeight * Stride]; 1039 | for (Int32 i = 0; i < PixelHeight; i++) 1040 | { 1041 | Int32 Base = i * Stride; 1042 | for (Int32 j = 0; j < PixelWidth; j++) 1043 | { 1044 | if (binaryArray[i, j] != 0) 1045 | { 1046 | Pixels[Base + (j >> 3)] |= Convert.ToByte(0x80 >> (j & 0x7)); 1047 | } 1048 | } 1049 | } 1050 | 1051 | // 创建黑白图像 1052 | Bitmap BinaryBmp = new Bitmap(PixelWidth, PixelHeight, PixelFormat.Format1bppIndexed); 1053 | 1054 | // 设置调色表 1055 | ColorPalette cp = BinaryBmp.Palette; 1056 | cp.Entries[0] = Color.Black; // 黑色 1057 | cp.Entries[1] = Color.White; // 白色 1058 | BinaryBmp.Palette = cp; 1059 | 1060 | // 设置位图图像特性 1061 | BitmapData BinaryBmpData = BinaryBmp.LockBits(new Rectangle(0, 0, PixelWidth, PixelHeight), ImageLockMode.WriteOnly, PixelFormat.Format1bppIndexed); 1062 | Marshal.Copy(Pixels, 0, BinaryBmpData.Scan0, Pixels.Length); 1063 | BinaryBmp.UnlockBits(BinaryBmpData); 1064 | 1065 | return BinaryBmp; 1066 | } 1067 | } 1068 | } -------------------------------------------------------------------------------- /CAPTCHA/CAPTCHA/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Drawing; 5 | using System.Drawing.Imaging; 6 | using System.Linq; 7 | using System.Runtime.InteropServices; 8 | using System.Text; 9 | 10 | using Preprocess; 11 | 12 | namespace CAPTCHA 13 | { 14 | class Program 15 | { 16 | public static int Height = 0; 17 | public static int Width = 0; 18 | 19 | static void Main(string[] args) 20 | { 21 | Bitmap bmp = new Bitmap(@"D:\CSharp\CAPTCHA\Orgin\img3.jpg"); 22 | 23 | Height = bmp.Height; 24 | 25 | Width = bmp.Width; 26 | 27 | Bitmap segBmp = CaptchaSegment.CaptchaSegment.CaptchaSegmentFun(bmp); 28 | 29 | segBmp.Save(@"D:\CSharp\CAPTCHA\DST\b3.bmp", System.Drawing.Imaging.ImageFormat.Jpeg); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /CAPTCHA/CAPTCHA/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // 有关程序集的常规信息通过以下 6 | // 特性集控制。更改这些特性值可修改 7 | // 与程序集关联的信息。 8 | [assembly: AssemblyTitle("CAPTCHA")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("CAPTCHA")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // 将 ComVisible 设置为 false 使此程序集中的类型 18 | // 对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型, 19 | // 则将该类型上的 ComVisible 特性设置为 true。 20 | [assembly: ComVisible(false)] 21 | 22 | // 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID 23 | [assembly: Guid("08dd8a8d-cb40-44e1-b265-babc0ef658e2")] 24 | 25 | // 程序集的版本信息由下面四个值组成: 26 | // 27 | // 主版本 28 | // 次版本 29 | // 生成号 30 | // 修订号 31 | // 32 | // 可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值, 33 | // 方法是按如下所示使用“*”: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /CAPTCHA/Orgin/OTSU/10_OTSU .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/OTSU/10_OTSU .jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/OTSU/11_OTSU .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/OTSU/11_OTSU .jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/OTSU/1_OTSU .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/OTSU/1_OTSU .jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/OTSU/2_OTSU .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/OTSU/2_OTSU .jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/OTSU/3_OTSU .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/OTSU/3_OTSU .jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/OTSU/4_OTSU .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/OTSU/4_OTSU .jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/OTSU/5_OTSU .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/OTSU/5_OTSU .jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/OTSU/6_OTSU .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/OTSU/6_OTSU .jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/OTSU/7_OTSU .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/OTSU/7_OTSU .jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/OTSU/8_OTSU .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/OTSU/8_OTSU .jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/OTSU/9_OTSU .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/OTSU/9_OTSU .jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/Sauvla/10_sauvola.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/Sauvla/10_sauvola.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/Sauvla/11_sauvola.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/Sauvla/11_sauvola.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/Sauvla/12_sauvola.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/Sauvla/12_sauvola.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/Sauvla/13_sauvola.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/Sauvla/13_sauvola.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/Sauvla/1_sauvola.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/Sauvla/1_sauvola.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/Sauvla/2_sauvola.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/Sauvla/2_sauvola.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/Sauvla/3_sauvola.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/Sauvla/3_sauvola.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/Sauvla/4_sauvola.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/Sauvla/4_sauvola.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/Sauvla/5_sauvola.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/Sauvla/5_sauvola.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/Sauvla/6_sauvola.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/Sauvla/6_sauvola.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/Sauvla/7_sauvola.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/Sauvla/7_sauvola.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/Sauvla/8_sauvola.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/Sauvla/8_sauvola.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/Sauvla/9_sauvola.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/Sauvla/9_sauvola.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/img0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/img0.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/img1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/img1.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/img10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/img10.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/img11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/img11.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/img12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/img12.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/img13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/img13.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/img2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/img2.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/img3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/img3.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/img4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/img4.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/img5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/img5.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/img6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/img6.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/img7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/img7.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/img8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/img8.jpg -------------------------------------------------------------------------------- /CAPTCHA/Orgin/img9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livezingy/merged-characters-segmentation/c9e7f3962b8ccef2f4b8d7b09bde4602d3ec1418/CAPTCHA/Orgin/img9.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 1.This repository created in C# VS2013-x64, It contains the following function: 2 | * merged characters segmentation algorithm according "The Robustness of “Connecting Characters Together” CAPTCHAs". 3 | * Sauvola binarization 4 | * OTSU binarization 5 | * Iteration binarization 6 | * Zhang-Suen skeletonization 7 | 8 | 2.The test data set is the TAOBAO CAPTCHA that composed of blue character and white background. 9 | --------------------------------------------------------------------------------