找回密码
 注册
搜索
热搜: 超星 读书 找书
查看: 6091|回复: 26

[【原创】] 简易验证码识别实作

[复制链接]
发表于 2006-4-13 18:44:19 | 显示全部楼层 |阅读模式

格式化图例:
┏━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
┃颜色┃含义                      ┃
┣━━╋━━━━━━━━━━━━━━━━━━━━━━━━┫
┃黑色┃普通的内容                   ┃
┠──╂────────────────────────┨
灰色┃说明内容. 通常出现在不太重要, 或较令人费解的地方┃
┠──╂────────────────────────┨
彩色┃孔南希望您能够格外注意的地方.          ┃
┗━━┻━━━━━━━━━━━━━━━━━━━━━━━━┛

您仅在遵循以下声明的情况下方可使用本文中介绍的方法:
1.本文正文及源码仅供学习研究使用, 禁止用于其它任何目的.
2.本文由孔南原创, 首发于 readfree.net, 转载请注明出处.
因违反上述声明所导致的各种后果均与作者无关.



==============前言=============

以前从网上看到过识别验证码的文章,
但具体操作过程较少涉及,
不太适合吾等菜鸟.
考虑到本坛性质的特殊性,
及 zhang706 \"软件版块是结果,而这里是基地.辅导站\" 的伟大思想,
故作此文, 讨论简易验证码的编程识别方法, 与广大坛友共勉.

预期读者:
喜欢编程, 又对验证码识别感兴趣, 还苦于无法入门的坛友.
您需要熟悉 Delphi 基础知识以实现本文中介绍的编程内容.
如果您是高手, 欢迎指正本文中的错误和纰漏, 孔南表示感谢.

附加说明:
本文代码针对可读性进行优化, 以尽量使其浅显易懂.
并非针对性能进行优化, 故执行效率可能很低.
也没有做任何错误处理, 甚至没有析构对象以回收资源.
这些只是为了保持简单. 当然孔南很欢迎大家提出易懂而高效的方案!
另外, 如果图片较大但非常模糊, 请点击它以查看完整图片.

用到的软件:
Delphi 7.0; PhotoShop 8.0; FireFox 1.5; EditPlus 2.21.


============正文开始===========

零. 接触验证码.

1.什么是验证码
我们知道不少表单在提交的时候都必须填写\"验证码\"这一项,
而它通常是一张画有肉眼可辨字符的图片(也有音频视频的).
我们需要手动将其内容填写到对应的文本框内才可提交,
如不填写或填写错误, 则您的表单也不被系统接受.

2.验证码存在的目的
验证码的妙处就在于可以区分表单的提交者是人还是机器.
因为对于设计良好的验证码来说, 只有人眼才能识别出来.
(不过有些设计变态的验证码, 就连人眼也识别不出来)
计算机不认识验证码, 所以无法正确地完成并提交表单,
故而避免了来自网络的一些攻击, 也保护了一些资源.

3.我们应该怎么办
对于受保护的东西, 我们当然更想得到. 所以我们要识别.
上文提到计算机是不可识别验证码的, 所以需要我们\"教\"它.
就像和认证会员沟通要用火星语一样, 和计算机沟通要用计算机语.
本文的选择是 OOP Pascal, 也就是 Delphi 使用的编程语言.
理论上人眼能识别的, 计算机也能识别, 这依赖于代码的质量.
出于教学目的及受本人所限, 本文仅针对最简易的验证码展开讨论.



一.分析验证码
上文我们发现了问题, 现在该分析一下了.
我们以著名读书网站 http://www.firstdrs.com/ 上的验证码为例.
用 FireFox 打开该页, 可在右侧偏下的部分找到验证码, 如图所示.
[图]

如果我们查看该图片的属性, 会发现它来源于:
http://www.firstdrs.com/authCode.jsp, 尺寸为 60*20 pixels.
如图所示.[图]

为方便研究, 我们将该图片保存下来, 比如保存为 authCode.jsp.jpeg.
[图]


然后在 PhotoShop 中打开该图片, 放大到 1600 倍,
这时我们可以看清该图片中的每个像素点. 如图所示.
[图]

我们可以看到, 该图片的背景为一些浅色的纹理,
另有两条深色的线横贯整张图片,
作为验证码的 4 位数字均匀地水平分布在横线的前方.
对于每一个数字, 都可能具有不同的宽度,
但所有数字的高度都是固定的.

一张图片无法得出所有信息, 所以按照上述方法多下载几张.
对比后您会发现这些图片中, 纹理是随机的, 线条是固定的,
并且各位验证码的位置也是固定的.
用鼠标指向某像素(pixels)时可查看其坐标.
我们需要每位验证码四个角的坐标值. 取坐标方法如图所示.
图中标圆圈的位置是需取坐标值的点, 右侧的色块指示当前坐标.
[图]

对多张图片进行分析后, 我们可将有验证码的区域标记出来.
经分析发现它们纵坐标相同, 均为: 4-15, 而横坐标不同(当然不同),
分别为: 6-14, 19-27, 32-40, 45-53. 每位验证码占: 8 * 12 pixels.
如图所示的蓝色区域即为验证码显示的位置.
[图]  

我们发现上述横坐标码是有规可循的, 运用数学归纳法总结一下.
各起始横坐标可表示为:  x = 13 * i - 7, 其中 i 为第几位.

我们同时发现背景色较亮而前景要暗一些(废话, 要一样的话谁认得出来),
这使我们想到: 如果能够把前景和背景 \"分开\", 事情可能就好办了.

想法不错, 按 Ctrl + L 打开\"色阶\"对话框, 如图所示.
[图]

我们可以看到中间的色阶图形可被分为左右两座\"山\",
它们各自具有自已的山峰, 而中间部分是一个明显的\"山谷\".
该图形下方有一\"输入色阶\"滑动条, 我们移动右侧的滑块, 看会发现什么.
[图]

我们惊奇地发现, 随着滑块向左移动, 背景的干扰渐渐消失了!
这是怎么回事?

原来图像在计算机中是以数字格式表示的, 而每一像素都有自己的\"灰度\".
该值越高, 该像素点就越亮. 最亮的颜色是白色, 它对应的值为 255.
与之相反, 黑色的值为 0. 因为背景色较亮, 我们直接把它们\"过滤\"掉了.
(因为我们调整了\"输入色阶\", 超出该范围的点都没有被\"输入\", 所以OK).

看来这里确实有一个能够分化前景和背景的值, 它的名字叫\"阈值\".
(听过\"全或无定律\"吗? 嘿嘿, 零号肯定听过 )
如果我们将灰度高于该值的点认为是白(1), 反之认定为黑(1),
就可以得到\"两极分化\"的映像了(可以是图片或其它, 取决于您的处理).

但是, 该值是多少? 这得由阁下自己寻找. 它并不需要十分精确.
只要您看着该点可以区分前景和背景, 就可以使用它作为阈值.
因为所有的验证码都在相同的位置打上了黑线, 所以该线虽明显却可忽略.
同一特征无法区分不同对象的(孔南: 1和1的区别是啥 众小妖: @#$%?!^&..)
[图]  

对于该图像而言, 最佳阈值应该在 128 左右, 我们假设您选择的是 140.
(为何不选最佳值? 因为此值不好确定, 且选择非最佳值有利于我们演示\"容错\")


现在有了阈值. 但若想分开 0.1, 还需取得每个点的灰度. 这要引入另一知识点.
正如大家所知道的那样, windows 以 RGB 模式来处理位图,
也就是说像素点(Pixels)的颜色(TColor)实际上包含 R.G.B 三个分量.
(懒得画图了, 顺手扯下一张来意思一下吧 )
[图]  

因为人眼对红绿蓝各色的敏感程度是不一样的, 所以转换时所占的份量也不一致.
红色(R)占 30%, 绿色(G)占 59%, 蓝色(B)占 11%.(怎么得出来的? 这可别问我)
我们可以得到灰度计算公式: Brightness := (R * 30 + G * 59 + B * 11) Div 100;
这样, 我们得到了灰度及其阈值, 就可以进行上文所述的处理了.


二. 设计
经上文的简要分析以后, 我们就可以着手进行程序设计了.

我们设想的程序如下:
该程序具有一个打开图像或/和下载图像的按钮,
点击\"打开\"可从对话框中选择本地图片, 点击\"下载\"时直接获取网络图片.
使用一种方式获取到图片后, 在预览图像框中显示出来, 也可以放大一份,
另外该程序有一个\"二值化\"按钮, 可以把源图像各点按照某个阈值分成0.1两种值,
并将结果显示在一个文本框中. 为了方便查看/处理, 可以同时显示行号.
为了实现对上述\"阈值\"的调整, 添加一下滑动条可能是不错的方法.
点击\"识别\"按钮时, 可以通过\"字典法\"对源图像进行识别, 结果在标题栏显示出来.

根据上文的描述, 大致可以确定程序所用的控件及单元:[图]
  

几乎任何窗体都需要的 Button 和 Label 组件 (Standard 页)
需要显示图像, 故需要 Image 组件(Additional 页)
需要显示处理结果, 故需要 Memo 组件(Standard 页)
可选处理\"阈值\", 该值在 0-255 之间变化. 故可选 TrackBar 组件(Win32 页)
可选\"打开图像\"功能, 故可选 OpenDialog/OpenPictureDialog 组件 (Dialogs 页)
可选\"下载图像\"功能, 故可选 IdHttp 组件 (Indy Clients 页)
需要处理的图像为 Jpeg 格式, 故需要 Jpeg 单元(在 Uses 节段添加)

我们为这些控件的一些属性及事件赋值以符合我们的需要.
对它们的赋值均在对象检视器(Object Inspector)中进行,
但分属性(Properties)页和事件(Events)页.
它们的设置方法是相同的, 在某控件的相应属性/事件页中找到列表中指定的属性/事件,
在其后面填写或选择相应的属性值或事件处理程序名称, 按回车键确定即可.
列表中并未包含所有属性而仅是重要的部分. 其它可按个人爱好进行设置, 如窗体标题等.


先说属性:
┏━━━━━┯━━━━━━━━━┯━━━━━━━┯━━━━━━━━━━━━┓
┃控件类型 │控件名称     │属性名    │属性值         ┃
┠─────┼─────────┼───────┼────────────┨
┃Form   │FrmMain      │Font[Name]  │宋体          ┃
┃     │         ├───────┼────────────┨
┃     │         │Font[Size]  │9            ┃
┠─────┼─────────┼───────┼────────────┨
┃Button  │BtnOpen      │Caption    │打开(&O)        ┃
┃     ├─────────┼───────┼────────────┨
┃     │BtnDownload    │Caption    │下载(&D)        ┃
┃     ├─────────┼───────┼────────────┨
┃     │BtnThreshold   │Caption    │二值化(&B)       ┃
┃     ├─────────┼───────┼────────────┨
┃     │BtnOCR      │Caption    │识别(&R)        ┃
┠─────┼─────────┼───────┼────────────┨
┃Image   │ImgPreview    │Stretch    │False          ┃
┃     ├─────────┼───────┼────────────┨
┃     │ImgSrc      │Stretch    │True          ┃
┠─────┼─────────┼───────┼────────────┨
┃Label   │LblThresholdTitle │Caption    │阈值          ┃
┃     ├─────────┼───────┼────────────┨
┃     │LblThreshold   │Caption    │140           ┃
┠─────┼─────────┼───────┼────────────┨
┃TrackBar │TrkThreshold   │Max      │255           ┃
┃     │         ├───────┼────────────┨
┃     │         │Frequency   │25           ┃
┃     │         ├───────┼────────────┨
┃     │         │Posotion   │140           ┃
┠─────┼─────────┼───────┼────────────┨
┃Memo   │TxtDst      │Lines     │            ┃
┠─────┼─────────┼───────┼────────────┨
┃OpenDialog│DlgOpen      │Filter    │识别码图片(*.jpg)|*.jpg ┃
┠─────┼─────────┼───────┼────────────┨
┃IdHTTP  │IdHTTP      │       │            ┃
┗━━━━━┷━━━━━━━━━┷━━━━━━━┷━━━━━━━━━━━━┛

您可以按照您的意愿对它们进行布局. 也可以参考我的截图.[图]  


再说事件:
┏━━━━━━┯━━━━┯━━━━━━━┯━━━━━━━━┓
┃控件名称  │事件名称│处理程序名称 │处理程序含义  ┃
┠──────┼────┼───────┼────────┨
┃FrmMain   │OnCreate│init     │执行初始化   ┃
┠──────┼────┼───────┼────────┨
┃BtnOpen   │OnClick │openImage   │打开本地图像  ┃
┠──────┼────┼───────┼────────┨
┃BtnDownload │OnClick │downloadImage │下载网络图像  ┃
┠──────┼────┼───────┼────────┨
┃BtnThreshold│OnClick │doThreshold  │执行二值化   ┃
┠──────┼────┼───────┼────────┨
┃BtnOCR   │OnClick │doOCR     │执行识别    ┃
┠──────┼────┼───────┼────────┨
┃TrkThreshold│OnChange│setThreshold │设置阈值    ┃
┠──────┴────┴───────┴────────┨
┃注: OnCreate-创建事件.OnClick-点击事件.OnChange-更改事件┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
提示: 如果您设置了\"事件\"的值, 但并没有为此事件编写任何代码,
则您在编译程序时, 该\"事件处理程序\"会被编译器清理掉. 故请在需用时填写\"事件\"栏.
作为替代, 您也可以仅在\"事件处理程序\"中写上一对注释符号, 就不会被清理了.
而本文将事件集中列表在这里的目的仅是为了使它看上去简明一些.


在您做好上述设置后, 该项目的框架就建好了.
如果您有问题, 可以对照以下源码: [0_UIOnly.rar]




三. 实现
我们设想的程序比较复杂,所以我们计划逐步来实现.
(其实功能一点也不复杂, 只是描述起来复杂)

(一). 在运行时动态加载 JPEG 图像
这可以分为两部分, 打开本地图像, 或下载网络图像. 现分述如下.

1. 打开本地图像. 触发方式: 点击\"打开\"按钮.
(注意: 此处未使用最直接的方式. 目的是为下载图像作铺垫)
为了在点击打开按钮时打开本地图像, 我们需要编写该按钮点击事件的处理程序.
双击\"打开\"按钮进入代码编辑窗口, 提示 procedure TfrmMain.openImage(Sender: TObject);

因为要打开的图像为 JPEG 格式, 所以可以使用 TJpegImage 的方法来实现.
(更不要忘记在代码窗口顶部的 Uses 节段中添加 JPEG 单元啊! )
在使用其方法之前, 需要事先声明变量, 并完成初始化. 关键代码如下:
  1. var JPG : TJpegImage;
  2. Begin
  3.    JPG := TJpegImage.Create();
  4. End;
复制代码

然后就可以调用其 Loadxxxx. 方法来加载图像了.
输入 Jpg.Load 会看到代码提示窗口, 看一下有何方法:
[图]

我们可以看到三种方法, 分别是:
LoadFromStream(从流对象中加载).
LoadFromClipboardFormat(从剪贴板中加载).
LoadFromFile(从文件中加载).
因为我们需要从本地打开图像, 所以我们使用其 LoadFromFile 方法.
该方法需要传入欲打开图像的文件名, 所以得通过一个对话框提示用户选择文件
如果用户选择了文件, 我们就执行打开操作, 否则忽视这一操作. 代码如下:
  1. if dlgOpen.Execute then //如果选择了图像文件
  2. Begin
  3.    Jpg := TJpegImage.Create();    //创建 JpegImage 的实例
  4.    Jpg.LoadFromFile(dlgOpen.FileName); //使用该实例加载选中的图像文件
  5. End;
复制代码

另外我们还要将图片显示在两个图像框中.
加载到第一个图像框时时需使用其位图对象的 Assign 方法来指派(请参见文档),
而加载到另一个图像框时仅需同步上个图像框中的 Picture 属性即可.
完整代码如下:
  1. //打开图像 - 点击"打开"按钮的事件处理程序
  2. procedure TfrmMain.openImage(Sender: TObject);
  3. var
  4.    JPG: TJpegImage;   //JPEG图像格式
  5. Begin
  6.    if dlgOpen.Execute then //如果选择了图像文件
  7.    Begin
  8.       Jpg := TJpegImage.Create();    //创建 JpegImage 的实例
  9.       Jpg.LoadFromFile(dlgOpen.FileName); //使用该实例加载选中的图像文件
  10.       imgPreview.Picture.Bitmap.Assign(JPG);  //从 Jpg 中获取位图格式的图像
  11.       imgSrc.Picture := imgPreview.Picture;  //同时显示在 imgSrc 里面
  12.    End;
  13. End;
复制代码
按 F9 调试, 应该可以正常运行了. 点击\"打开\",
选择一幅我们刚才下载的验证码图片, 它将以两种尺寸显示出来.
可参见源码: [1_LoadImage.rar]   

说明: 如果至此仍不能正确运行, 说明您可能没有严格按照上文的说明进行操作.
请仔细检查您是否已准确设置各项参数及输入代码, 如仍有问题请跟贴询问.
因为这是编码的第一步骤, 故详细说明, 后续步骤则会从简.



2.下载网络图像
刚才我们选择打开了本地图像. 但对于网络图像而言就没这么直接了.
首先我们需要一个指定的验证码图片网址, 然后用 Indy 的 IdHttp Get 获取它.
因为这个地址是固定的(http://www.firstdrs.com/authCode.jsp), 我们可以把它设置为常量(const).
双击\"下载\"按钮进入代码编辑器. 提示: procedure TfrmMain.downloadImage(Sender: TObject);

IdHttp 组件使用 Get 方法来获取网络图像, 此方法有两种形式:
procedure Get(AURL: string; const AResponseContent: TStream); overload;
function Get(AURL: string): string; overload;
通过阅读文档可以知道后者返回一个字符串而前者将取回的数据写入流(TStream).
很明显我们要用第一个方法, 也就同时需要一个 TStream 类的实例, 我们选择的是 TMemoryStream.
TStream 为抽象类, 它不应直接被实例化, 所以应该用它的子类来执行此操作.
其子类有: TFileStream(文件流).TStringStream(字符流).TMemoryStream(内存流).
TBlobStream(Blob流).TWinSocketStream(WinSock流)和TOleStream(OLE流).
很明显 TMemory 适于此目的, 因为我们需要处理一个内存缓冲区.

实例化的过程同上文中的 TJpegImage 类似. 具体代码如下:
  1. const
  2.    sUrl = 'http://www.firstdrs.com/authCode.jsp';  //生成验证码的页面地址
  3. var
  4.    mstrm : TMemoryStream;  //流
  5.    Jpg : TJpegImage;  //Jpeg 图像
  6. begin
  7.    Jpg := TJpegImage.Create(); //创建 JpegImage 对象的实例
  8.    mstrm := TMemoryStream.Create();   //创建内存流对象的实例
  9.   //......
  10. end;
复制代码

然后就可以使用 Get 方法将从上述 URL 中获取的内容保存到内存流中了.
上文提到过 JpegImage 可以从文件或流中获取 Jpeg 图片, 这儿就要用到从流中获取.
需要注意的是在读取之前, 需将\"流指针\"移动到流的起始位置. 其它操作同前. 完整代码如下:
  1. procedure TfrmMain.downloadImage(Sender: TObject);
  2. const
  3.    sUrl = 'http://www.firstdrs.com/authCode.jsp';  //生成验证码的页面地址
  4. var
  5.    mstrm : TMemoryStream;  //流
  6.    Jpg : TJpegImage;  //Jpeg 图像
  7. begin
  8.    Jpg := TJpegImage.Create(); //创建 JpegImage 对象的实例
  9.    mstrm := TMemoryStream.Create();   //创建内存流对象的实例
  10.    IdHttp.Get(sUrl, mstrm);  //使用 Http Get 获取图像, 并保存到内存流中
  11.    mstrm.Position := 0;   //将"流指针"移动到到该内存流的起始位置
  12.    Jpg.LoadFromStream(mstrm);  //从内存流中加载 Jpeg 图像
  13.    ImgPreview.Picture.Bitmap.Assign(Jpg);  //将已加载的图像指派给预览框
  14.    ImgSrc.Picture := ImgPreview.Picture;  //同时在 ImgSrc 图像框中显示图片
  15. end;
复制代码

至此我们已经可以通过打开和下载两种方式获取验证码图片了.
代码包:[2_DownImage.rar]
  


3.二值化处理
网文: 二值化处理是一种灰度处理。对于给定的阈值,程序将灰度大于给定阈值的点变成白点,
另外的点变为黑点。图像经处理后变为一位的只有黑白二色的二值图像。二值化操作将使信息丢失,
但是却是某些处理的不可缺少的步骤。

有了图片我们就可以进行二值化处理了. 其关键是根据坐标(x,y)判断该点是黑('0')还是白('1').

因为该步骤具有通用性, 所以我们考虑将其写成一个函数 GetBiValue.
该函数接受一个位图(TBitmap)及点坐标(x,y)作为参数, 返回 '0' 或者 '1' 表示该点的颜色.
根据该设想, 我们在单元的 interface 部分添加函数声明: 添加到 private 部分即可
  1. function getBivalue(srcBitmap: TBitmap; x, y: Integer) : char;   //确认单点黑白
复制代码
然后按 Ctrl + Shift + C, 系统将为您写入函数\"框架\", 并转到该部分.
为描述方便我们先给出完整代码:
  1. //判断指定位图的相应坐标点颜色对应为黑还是白
  2. function TfrmMain.getBivalue(srcBitmap: TBitmap; x, y: Integer): char;
  3. var
  4.    byBrightness : byte;   //灰度
  5.    r, g, b: byte;  //红绿蓝值
  6.    Color: TColor;  //颜色值
  7. begin
  8.    Color := srcBitmap.Canvas.Pixels[x,y];  //获取指定点的坐标颜色
  9.    R := GetRValue(Color);  //分别获取红. 绿. 蓝色的值
  10.    G := GetGValue(Color);  //
  11.    B := GetBValue(Color);  //
  12.    byBrightness := (30 * r + 59 * g + 11 * b) div 100; //依公式计算灰度
  13.    if byBrightness < trkThreshold.Position then Result := &#39;1&#39;
  14.    else Result := &#39;0&#39;; //如果灰度低于阈值, 则为黑色(&#39;1&#39;) 否则为白色(&#39;0&#39;)
  15. end;
复制代码
前文我们已经知道了灰度的计算公式 Brightness := (R * 30 + G * 59 + B * 11) Div 100;
公式中需要 RGB 三个分量, 它们都需要从该点的颜色值中通过 GetXValue 提取出来.
所以我们需要事先获取指定点的颜色值, 使用的方法是: srcBitmap.Canvas.Pixels[x,y].
然后我们如代码所示套用公式得到灰度, 最后判断该灰度是否大于阈值即可.

作完该判断, 二值化的任务也就作完了一半.
因为上述函数仅针对一个点进行处理, 我们通过循环让它可以处理所有点就可以了.
双击 \"二值化\" 按钮, 系统提示: procedure TfrmMain.doThreshold(Sender: TObject);
仍先给出完整代码:
  1. //执行二值化 - 点击"二值化"按钮的事件处理程序
  2. procedure TfrmMain.doThreshold(Sender: TObject);
  3. var
  4.    x, y: integer;  //坐标
  5.    sBuffer: String;   //缓冲字符串, 用于向文本框中写入文字
  6.    srcBitmap: TBitmap;   //源图片
  7. begin
  8.    srcBitmap := ImgSrc.Picture.Bitmap;   //为源图片赋值
  9.    txtDst.Lines.Clear();  //清空文本框
  10.    for y := 0 to srcBitmap.Height-1 do //按源图形的"行"进行循环
  11.    Begin
  12.       sBuffer := format(&#39;%3d  :  &#39;,[y]);  //使用格式化函数打印当前行号
  13.       for x := 0 to srcBitmap.Width-1 do    //按源图形的"列"进行循环
  14.         sBuffer := sBuffer + getBivalue(srcBitmap, x, y);  //将当前点的值添加到缓存行
  15.       txtDst.Lines.Append(sBuffer);  //将缓存行添加到文本框中以显示结果
  16.    End;
  17. end;
复制代码
该代码遍历源图片每一行每一列的各个点, 取得它们的值, 并显示在文本区里.
代码中通过 format() 实现了行号的格式化显示, 通过 Lines(TStrings 类) 的 Append 方法添加字符串.
编译执行完毕之后, 点击\"二值化\"按钮, 我们可以看到类似下面的二值化\"图像\".
(我们需要这些值, 故不直接将原图处理为真正的二值化图像).
  0  :  000000000000000000000000000000000000000000000000000000000000
  1  :  000000000000000000000000000000000000000000000000000000000000
  2  :  000000000000000000000000000000000000000000000000000000000000
  3  :  000000000000000000000000000000000000000000000000000000000000
  4  :  000000001111000000000011000000000001110000000000111100000000
  5  :  000000011000110000001111000000000011011000000000111100000000
  6  :  000000110000110000000011000000000010001000000001000000000000
  7  :  011111111111111111111111111111111111111111111111111111111111
  8  :  000000011101100000000011000000000110001100000011111000000000
  9  :  000000001110000000000011000000000110001100000000011100000000
 10  :  000000001111000000000011000000000110001100000000001100000000
 11  :  000000010001100000000011000000000110001100000000000100000000
 12  :  000000110000110000000011000000000110001100000000000100000000
 13  :  011111111111111111111111111111111111111111111111111111111111
 14  :  000000011001100000000011000000000011011000000010001000000000
 15  :  000000001111000000001111110000000001110000000011110000000000
 16  :  000000000000000000000000000000000000000000000000000000000000
 17  :  000000000000000000000000000000000000000000000000000000000000
 18  :  000000000000000000000000000000000000000000000000000000000000
 19  :  000000000000000000000000000000000000000000000000000000000000

此步骤的演示及代码 [3_Threshold.rar]   



4. 同步显示阈值
经过上文的努力我们已经进行了二值化这一基本的\"图像\"处理
但如果我们拖动滑块, 标签上并不能显示出当前阈值, 这不爽.
为了让程序更符合我们的需要, 我们需要对标签文本作一下同步.
因为非常简单, 故只给出代码. (对应 trkThreshold 的更改事件)
  1. //设置阈值 - 滑动滑块的事件处理程序
  2. procedure TfrmMain.setThreshold(Sender: TObject);
  3. begin
  4.    lblThreshold.Caption := IntToStr(trkThreshold.position); //将阈值设置为滑动条的值
  5. end;
复制代码
这样就可实现显示的同步了. 调整几次, 并分别进行二值化, 以查看阈值对二值化结果的影响.
可见高阈值情况下会出现全\"1\", 而低阈值会出现全\"0\", 只有在适当的阈值下才能精确区分前景背景.
因为单张图片效果未必明显, 请大家多试验几张图片以最终确定一个您认为合适的阈值.
重复一次, 它并无需十分精确. 但若您选定了一个值, 就不要更改它了.
[4_SynDisplay.rar]   

5. 准备字典
当我们得到一幅二值化的\"图像\"以后, 就可以对其通过\"字典法\"识别了.
想用字典法识别, 需要预先准备\"字典\"(好像是废话 )
因为工作量并不大, 我们可以手工取出上面的数字部分, 并处理为单行形式(主要是懒得讲代码 ).
注意需要按照前文描述的区域进行截取哦! 比如上图中的 8:
001111000
011000110
110000110
111111111
011101100
001110000
001111000
010001100
110000110
111111111
011001100
001111000
组成一行就是: 001111000011000110110000110111111111011101100001110000001111000010001100110000110111111111011001100001111000
这些工作用 EditPlus 处理起来相当轻松, 因为 EditPlus 支持列块模式,
可以用鼠标把上文那一个区块框起来选定, 到新文档中粘贴,
然后用正则表达式替换掉所有回车符就搞定了.

按照相同的方式制作 0-9 计十个数字的字典, 然后把它们添加到程序中.
因为我们不想每次点击\"识别\"按钮都重新初始化字典, 所以最好放在窗体加载事件中.
这样我们需要声明一个在窗体范围内均可识别的数组, 比如叫 sNumCode, 放在 private 节中.
声明如下:
  1. sNumCode : array[0..9] of String;  //二值化的标准数字码
复制代码

然后在窗体加载事件中对其进行初始化, 代码如下:
  1.    //初始化二值化的标准数字码
  2.    sNumCode[0] := &#39;000111000001101100001000100111111111011000110011000110011000110011000110011000110111111111001101100000111000&#39;;
  3.    sNumCode[1] := &#39;000110000011110000000110000111111111000110000000110000000110000000110000000110000111111111000110000011111100&#39;;
  4.    sNumCode[2] := &#39;001111000010011100100001100111111111000001100000001000000011000000010000000100000111111111011111110111111100&#39;;
  5.    sNumCode[3] := &#39;001111000010011100100001100111111111000011000000111000000011100000001100000001100111111111110011000111110000&#39;;
  6.    sNumCode[4] := &#39;000001100000001100000011100111111111001001100001001100010001100100001100111111110111111111000001100000001100&#39;;
  7.    sNumCode[5] := &#39;000111100000111100001000000111111111011111000000011100000001100000000100000000100111111111010001000011110000&#39;;
  8.    sNumCode[6] := &#39;000001110000111000001100000111111111010111000111001100110000110110000110110000110111111111011001100001111000&#39;;
  9.    sNumCode[7] := &#39;001111111001111110010000010111111111000000100000000100000001000000001000000010000111111111000010000000100000&#39;;
  10.    sNumCode[8] := &#39;001111000011000110110000110111111111011101100001110000001111000010001100110000110111111111011001100001111000&#39;;
  11.    sNumCode[9] := &#39;001111000011001100110000110111111111110000110110000110011000110001111100000001100111111111000110000111000000&#39;;
复制代码
字典就做好了.
[5_PrepareDict.rar]   

6. 执行识别
我们的设想是这样: 既然字典中已经有了相应位置字符的标准码,
则我们只需取得源图像的特定区域内的字符, 然后和字典中的字符一一对照,
结果差不太多即可.(\"容错\"的体现. 假设最大允许误差为 10)
因为验证码中共有 4 个字符, 对每个字符的识别过程均相同, 所以可写成一个函数.
该函数接受一幅位图(TBitMap)和表示获取第几位验证码的数字, 返回表示验证码的字符.
请在 private 节中添加:
  1. function getNumber(srcBitMap: TBitmap; iPart: Integer) : char; //获取单个验证码
复制代码
然后编写代码如下:
  1. //识别单个验证码. 参数 iPart 指第几位验证码
  2. function TfrmMain.getNumber(srcBitmap: TBitmap;iPart: Integer): char;
  3. const
  4.    iMaxMismatch = 10;  //允许的不匹配最大值(误差)
  5. var
  6.    x, y: integer;  //坐标
  7.    sAuthChar: String[108]; //验证码串
  8.    i, j, iStartX, iEndX: Integer;  //一些循环变量
  9.    byMismatch: Integer;   //不匹配计数器
  10. begin
  11.    iStartX := iPart * 13 - 7;  //计算起始横坐标
  12.    iEndX := iStartX + 8;  //计算终止横坐标
  13.    sAuthChar := &#39;&#39;;   //初始化验证码串
  14.    for y := 4 to 15 do //依据位图"行"进行循环, 仅限有验证码的部位
  15.       for x := iStartX to iEndX do   //依据位图"列"进行循环, 仅限有验证码的部位
  16.         sAuthchar := sAuthChar + getBiValue(srcBitmap, x, y);  //依次获取所有点的黑/白值
  17.    getNumber := &#39;X&#39;;  //初始化返回值
  18.    for j := 0 to 9 do  //遍历二值化的标准验证码数组
  19.    begin
  20.       byMismatch := 0;   //初始化不匹配计数器
  21.       for i := 1 to 109 do   //遍历标准串/验证串中的每个字符
  22.       begin
  23.         if sAuthChar[i] <> sNumCode[j,i] then  //如果相应位置的字符不一致
  24.         Begin
  25.            inc(byMismatch);   //不匹配计数器自增 1
  26.            if (byMismatch >= iMaxmismatch) then break; //达到误差上限则终止
  27.         End;
  28.       end;
  29.       if byMismatch < iMaxmismatch then  //如果经上述循环后仍未达到误差上限
  30.       begin
  31.         getNumber := chr(j+48);    //将此已识别值通过ascii转换为相应字符
  32.         break;  //跳出循环
  33.       end;
  34.    end;
  35. end;
复制代码

上述代码虽长, 但理解并不困难. 浅析如下:
首先我们需要一个值说明我们接受的最大误差是多少. (const iMaxMismatch = 10;)
然后我们可以使用代表第几位验证码的参数(iPart), 通过前面的公式计算出该验证码块的位置,
再通过双层循环遍历每一个点, 执行 getBiValue 然后串联即可得到 108 位(12行*9列)的验证码串.
然后我们再通过一个双层循环对验证码串和 0-9 的字典串进行比较,
如果验证码串与某一个字典串匹配, 则认为验证码为该字典串对应的字符.
因为我们需要返回一个字符, 所以需要执行数字向字符的转换, 即加 48 后调用 chr() 函数.

通过上述代码完成了单个字符的识别, 而要识别全部验证码不过是循环4次而已,
双击\"识别\"按钮, 完成以下代码: (因这段代码过于简单, 就不显式讲述了)
  1. //执行识别 - 点击"下载"按钮的事件处理程序
  2. procedure TfrmMain.doOCR(Sender: TObject);
  3. var
  4.    i : Integer;   //循环变量
  5.    sOcrResult : String; //验证码识别字符串
  6. begin
  7.    sOcrResult := &#39;识别为: &#39;;   //初始化验证码字符串
  8.    for i := 1 to 4 do  //执行 4 次循环以获取 4 位验证码
  9.       sOcrResult := sOcrResult + getNumber(imgSrc.Picture.Bitmap, i);  //获取当前位置上的验证码
  10.    self.Caption :=  sOcrResult;   //显示结果
  11. end;
复制代码

[6_OCR.rar ]   

编译执行程序, 打开或下载图片后点击\"识别\"按钮, 就能够识别出正确的验证码了.
我们拖动滑块改变阈值, 在进行二值化时会看到有些文字周围已经出现了错误点,
但识别仍然正常(这就是上文提到的容错), 但如果阈值改变过大, 就只能得到\"XXXX\"了.

最后给出完整的源码和 EXE.  

程序运行结果如图所示.


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
回复

使用道具 举报

发表于 2006-4-13 20:23:37 | 显示全部楼层
期待下文……
回复

使用道具 举报

 楼主| 发表于 2006-4-17 17:33:31 | 显示全部楼层
终于完成, 封锁了4天了. 现在解禁.
回复

使用道具 举报

发表于 2006-4-17 17:47:06 | 显示全部楼层
真不错     
回复

使用道具 举报

发表于 2006-4-17 18:16:42 | 显示全部楼层
很适合想学习delphi编程的朋友。
回复

使用道具 举报

 楼主| 发表于 2006-4-17 18:19:41 | 显示全部楼层
引用第4楼coolman2006-04-17 18:16发表的“”:
很适合想学习delphi编程的朋友。

呵呵, 感谢 Coolman 的捧场!
看来写得太浅了
回复

使用道具 举报

发表于 2006-4-17 19:24:59 | 显示全部楼层
非常谢谢,介绍非常详细,学到了不少思路!
回复

使用道具 举报

发表于 2006-4-17 19:57:58 | 显示全部楼层
好东西.等我把VC完全了解了再来看你的,基本编程思想能理解.期待中.......
回复

使用道具 举报

发表于 2006-4-17 20:42:05 | 显示全部楼层
支持一下。
回复

使用道具 举报

发表于 2006-4-17 23:06:31 | 显示全部楼层
学习图像处理编程
回复

使用道具 举报

发表于 2006-4-18 00:53:52 | 显示全部楼层
受教受教, 多谢孔南兄.

建议风影版版推荐孔南兄为计算机版热心会员如何?
http://www.readfree.net/bbs/read.php?tid=113090&fpage=1
回复

使用道具 举报

发表于 2006-4-18 02:53:33 | 显示全部楼层
支持楼上建议
回复

使用道具 举报

hjmsolar 该用户已被删除
发表于 2006-4-18 09:13:45 | 显示全部楼层
用PHP写,It is so easy!

<?php
/*
  显示验证图片
*/
header("Content-type: image/png");
$img_height  = 50;
$img_width = 17;
$auth_code = &#39;&#39;;
//if (isset($_GET[&#39;num&#39;])) { $auth_code = $_GET[&#39;num&#39;]; }
for($i=0; $i<4; $i++) {
  srand((double)microtime()*1000000);
  $auth_code .= strval(rand(0, 9));
}
$auth_img = imagecreate($img_height, $img_width);
//if (!session_is_registered(&#39;AuthCode&#39;)) { session_register(&#39;AuthCode&#39;); }
imagecolorallocate($auth_img, 255, 255, 255);
for ($i=1; $i<=30; $i++) {
  $color_star = imagecolorallocate($auth_img,mt_rand(200,255),mt_rand(200,255),mt_rand(200,255));
  imagestring($auth_img,1,mt_rand(1,$img_height),mt_rand(1,$img_width),"*", $color_star);
}
$color_char = imagecolorallocate($auth_img,mt_rand(0,100),mt_rand(0,150),mt_rand(0,200));
imagestring($auth_img, 5, 7, 1, $auth_code, $color_char);
imagepng($auth_img);
imagedestroy($auth_img);
?>

很久没写过PHP了,不直到好不好用,吼吼!!!
回复

使用道具 举报

 楼主| 发表于 2006-4-18 09:53:02 | 显示全部楼层
引用第12楼hjmsolar2006-04-18 09:13发表的“”:
用PHP写,It is so easy!

   /*
     显示验证图片
   */
   
header ("Content-type: image/png");
   
$img_height = 50;
   
$img_width = 17;
   
$auth_code = &#39;&#39;;
   
//if (isset($_GET[&#39;num&#39;])) { $auth_code = $_GET[&#39;num&#39;]; }
   
for( $i = 0; $i 4; $i++ )
   {
      
srand ((double)microtime() * 1000000);
      
$auth_code .= strval( rand( 0, 9 ) );
   }
   
$auth_img = imagecreate( $img_height, $img_width );
   
//if (!session_is_registered(&#39;AuthCode&#39;)) { session_register(&#39;AuthCode&#39;); }
   
imagecolorallocate( $auth_img, 255, 255, 255 );
   
for( $i = 1; $i 30; $i++ )
   {
      
$color_star = imagecolorallocate( $auth_img, mt_rand( 200, 255 ), mt_rand( 200, 255 ), mt_rand( 200, 255 ) );
      
imagestring( $auth_img, 1, mt_rand( 1, $img_height ), mt_rand( 1, $img_width ), "*", $color_star );
   }
   
$color_char = imagecolorallocate( $auth_img, mt_rand( 0, 100 ), mt_rand( 0, 150 ), mt_rand( 0, 200 ) );
   
imagestring( $auth_img, 5, 7, 1, $auth_code, $color_char );
   
imagepng ($auth_img);
   
imagedestroy ($auth_img);

.......


怎么我看着像是在生成验证码?
本文的主题是识别哟

不过看来 h 兄很厉害, 指教一下吧.
回复

使用道具 举报

发表于 2006-4-18 10:44:02 | 显示全部楼层
看来孔兄应该是一个老师,将其问题来脉络非常清晰哦!!
回复

使用道具 举报

hjmsolar 该用户已被删除
发表于 2006-4-18 10:53:21 | 显示全部楼层
吼吼,我的生成验证码的!俺们是对头,吼吼!       


现在要用程序识别验证码是有局限性的,因为各个网站的验证码生成程序是不一样的!

就算能识别,我只要稍微改动一下识别码生成程序,甚至好几个生成程序随机生成识别码图片,你就不能识别了!咯咯!

等什么时候计算机的识别技术再先进一点,可能识别码就用不上了,不过可能要等很久!

指教不敢当,自己去看php的手册!
http://www.php.net/docs.php
回复

使用道具 举报

发表于 2006-4-18 18:47:02 | 显示全部楼层
第一次在论坛上看到写的这么工整的教学帖!支持一下!!!
回复

使用道具 举报

发表于 2006-4-18 20:13:10 | 显示全部楼层
我正想学编程,不过听人说VC最有前途,Dephi最容易出成果,现在计算机里下载了一大批的教,看园地这么多DEPHIE高手,呵呵,我不妨从Dephi入手,有问题也好请教!
回复

使用道具 举报

hjmsolar 该用户已被删除
发表于 2006-4-19 08:19:27 | 显示全部楼层
引用第17楼yishui01682006-04-18 20:13发表的“”:
我正想学编程,不过听人说VC最有前途,Dephi最容易出成果,现在计算机里下载了一大批的教,看园地这么多DEPHIE高手,呵呵,我不妨从Dephi入手,有问题也好请教!


VC极其强大,但是比较麻烦!
Dephi控件多,适合快速编程!
去看看新闻,好像最近Dephi情况不太好!
回复

使用道具 举报

 楼主| 发表于 2006-4-19 19:14:26 | 显示全部楼层
引用第18楼hjmsolar2006-04-19 08:19发表的“”:
去看看新闻,好像最近Dephi情况不太好!
.......

是啊, 要出售整条 IDE 生产线吧,
可是副总裁那句话很耐人寻味:
泪水将离开我们的眼睛,而微笑将留在我们的脸上!

程序的部分功能已经失效了, 但到现在也没人询问, 看来都没研究, 真失败啊 (-_-);;
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|手机版|小黑屋|网上读书园地

GMT+8, 2026-4-6 01:16 , Processed in 0.109718 second(s), 7 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表