验证码识别

因为当初的的一个程序 需要模拟发包 但是发包的平率太高于是就会出现验证码、、然后 我看了一下那个验证码 感觉并不是好复杂的样子 然后就开始捣鼓了起来 验证码是百度推广的验证码

验证码地址:http://fengchao.baidu.com/nirvana/vcode?src=prv&nocache

源码(CSDN):http://download.csdn.net/download/crystal_lz/7647841

有90+%的识别率

这是个意外、、

当时在网上大概的搜索了一下 然后自己整理了一个思路 总结出几点

  • 二值化验证码 这么做主要目的是为了能够尽量让文字和背景分明、、当然截图中的验证码还有干扰线 那只是一个巧合 因为干扰线颜色分明 验证码文字颜色也分明 所以在二值化的时候 我直接去掉了干扰线
  • 然后就是在一张图上把文字区域提取出来 当然 如果一些验证码位置固定 就不用这一步了
  • 然后对每个字符 采样 收集样本
  • 识别的时候 将你采集到的字符 和样本进行比对 找出重合率最高的那个

这里请注意 这个方法并不是万能的、、只是正对于一些比较简单的验证码有效、、复杂的 压根这种方法就没戏、、比如验证码每个字符都黏在一起的 就像上面截图的WU一样、、程序直接当作一个处理了、、

代码调用很简单:

public static string GetCodeString(Bitmap bmpCode) {
    //获取二值化后并且去掉了干扰线的图片
    Bitmap bmpDarkImage = GetDarkImage(bmpCode);
    //获取字符所在的区域
    ListlstRect = GetCharRect(bmpDarkImage);
    bmpDarkImage.Dispose();
    //为什么要重新获取GetCharRect里面修改了原图 当初没有统一方法里面是否修改原图
    bmpDarkImage = GetDarkImage(bmpCode);
    string strCode = string.Empty;
    for (int i = 0; i < lstRect.Count; i++) { 
        //将每个区域的字符和样本进行对比 返回重合率最高字符 
        strCode += GetBestSameChar(bmpDarkImage, lstRect[i]);
    } 
    bmpDarkImage.Dispose();  
    return strCode; 
}
一个方法接收一张图然后返回字符串、、代码是很久以前写的了 快两年了 所以质量有点不咋的 但是现在也没有用他的必要了 所以也不去改他了 思路到位就行了、、用同样的思路 后来也写过几次其他网站的验证码识别、、当然都是简单的验证码 不是很复杂的

二值化也很简单 相信会一点图形知识的都知道:

public static Bitmap GetDarkImage(Bitmap bmpSrc) {
    Bitmap bmp = bmpSrc.Clone(
        new Rectangle(0, 0, bmpSrc.Width, bmpSrc.Height), PixelFormat.Format24bppRgb);
    BitmapData bmpData = bmp.LockBits(
        new Rectangle(0, 0, bmp.Width, bmp.Height), 
        ImageLockMode.ReadWrite, bmp.PixelFormat);
    byte[] byColorInfo = new byte[bmp.Height * bmpData.Stride];
    Marshal.Copy(bmpData.Scan0, byColorInfo, 0, byColorInfo.Length);
    for (int x = 0, xLen = bmp.Width; x < xLen; x++) { 
        for (int y = 0, yLen = bmp.Height; y < yLen; y++) { 
            byte byV = GetAvg(//取出像素RGB平均值 
                byColorInfo[y * bmpData.Stride + x * 3], 
                byColorInfo[y * bmpData.Stride + x * 3 + 1],
                byColorInfo[y * bmpData.Stride + x * 3 + 2]); 
            //这个判断是去干扰线的 
            if ((byColorInfo[y * bmpData.Stride + x * 3] <= 60 && 
                byColorInfo[y * bmpData.Stride + x * 3 + 1] <= 60 &&
                byColorInfo[y * bmpData.Stride + x * 3 + 2] <= 60) || byV <= 30) 
                byV = 255; 
            else byV = (byte)(byV >= 127 ? 255 : 0);
            //设置像素颜色
            byColorInfo[y * bmpData.Stride + x * 3] =
                byColorInfo[y * bmpData.Stride + x * 3 + 1] =
                byColorInfo[y * bmpData.Stride + x * 3 + 2] = byV;
        }
    }
    Marshal.Copy(byColorInfo, 0, bmpData.Scan0, byColorInfo.Length);
    bmp.UnlockBits(bmpData);
    return bmp;
}
其实关键在于 如何找出字符的区域和样本对比、、其实 要找出字符的区域也不是好复杂

因为图像已经二值化 只有两个颜色 然后开始遍历像素 从左到右从上往下的开始扫描 一旦遇到一个黑色的像素点 那就证明 十有八九碰到一个字符了、、然后记录下这个点并且把这个点标记成其他颜色比如红色 然后在开始从这个像素点开始向下找寻如果下面一个点也是黑色 表示十有八九是字符的一部分 标记 继续向下 如果发现向下是白色了 那么开始向右找 如果右边是黑色 标记 如果不是黑色 那就再换一个方向 向左。。然后一直重复 直到遇到上下左右都没有黑色像素点的时候 那就表明这个字符被扫描完了

什么意思呢 举个例子 假设把你放到一个迷宫里面 然你从迷宫的开始处 一直向右走 遇到岔路依然向右、、反正始终保持向右转、、如果说向右的某一条路是死路 那么你退回到上次一转弯的地方 然后用粉笔在向右的方向标记一下 这条路已经走过了 是死路 然后换一个左边的路口走进去 然后又依然保持向右的动作、、反正走不通 就回到上一个路口换一个方向继续、、这样一直下去 你必然会找到一条出去的路、如果没有路那你也会把能走的路 全部遍历完 最终回到起点 说白了 其实就是一个递归的过程 有意的人可以百度百科深度优先搜索

这个过程本应该用递归是很简单的 但是当时由于急着要用、所以一直在思考边思考边按照思路写代码、、结果完了之后才反映过来 用一个递归多简单的事情、、结果却写得很复杂 用的是非递归方式:

public static ListGetCharRect(Bitmap bmpDarkImage) {
  ListlstRect = new List();
  for (int y = 0, leny = bmpDarkImage.Height; y < leny; y++) { 
    for (int x = 0, lenx = bmpDarkImage.Width; x < lenx; x++) { 
      //如果遇到一个黑色点 调用GetRegionFromPoint得到一个区域 
      if (bmpDarkImage.GetPixel(x, y).ToArgb() == Color.Black.ToArgb()) { 
        Rectangle rectTemp = GetRegionFromPoint 
                    (bmpDarkImage, new Point(x, y), Color.Black, Color.Blue); 
        if (rectTemp != Rectangle.Empty) lstRect.Add(rectTemp); 
      } 
    } 
  } //将区域按照left属性排序 
  for (int i = 0; i < lstRect.Count; i++) { 
    for (int j = 1; j < lstRect.Count - i; j++) { 
      if (lstRect[j - 1].Left > lstRect[j].Left) {
        Rectangle rectTemp = lstRect[j];
        lstRect[j] = lstRect[j - 1];
        lstRect[j - 1] = rectTemp;
      }
    }
  }
  return lstRect;
}
public static Rectangle GetRegionFromPoint
  (Bitmap bmpDarkImage, Point ptStart, Color clrSrc, Color clrSet) {
  int nCount = 0;
  Rectangle rect = new Rectangle(ptStart.X, ptStart.Y, 0, 0);
  ListptRegList = new List();
  ptRegList.Add(ptStart);//标记颜色 这里的List其实相当于一个栈- -!其实.Net有Stack<>
  bmpDarkImage.SetPixel(ptStart.X, ptStart.Y, clrSet);
  while (ptRegList.Count != 0) {
    //获取下一个点 
    Point ptTemp = GetNextPoint(bmpDarkImage, ptRegList[ptRegList.Count - 1], clrSrc);
    if (ptTemp != Point.Empty) {
      ptRegList.Add(ptTemp);//存在下一个点 继续步骤
      bmpDarkImage.SetPixel(ptTemp.X, ptTemp.Y, clrSet);
      nCount++;//统计一下点数 连接在一起的点过少 就当作是干扰
      if (ptTemp.X < rect.Left) { rect.Width = rect.Right - ptTemp.X; rect.X = ptTemp.X; } 
      if (ptTemp.Y < rect.Top) { rect.Height = rect.Bottom - ptTemp.Y; rect.Y = ptTemp.Y; } 
      if (ptTemp.X > rect.Right) rect.Width = ptTemp.X - rect.Left;
      if (ptTemp.Y > rect.Bottom) rect.Height = ptTemp.Y - rect.Top;
    } else//不存在下一个点回到上一步继续 直到没有退步为止
      ptRegList.RemoveAt(ptRegList.Count - 1);
  }
  rect.Width += 1; rect.Height += 1;
  return nCount < 8 ? Rectangle.Empty : rect; 
} 
public static Point GetNextPoint(Bitmap bmpScr, Point ptStart, Color clr) { 
  if (GetDownPoint(bmpScr, ptStart, clr)) return new Point(ptStart.X, ptStart.Y + 1); 
  if (GetRightPoint(bmpScr, ptStart, clr)) return new Point(ptStart.X + 1, ptStart.Y);
  if (GetUpPoint(bmpScr, ptStart, clr)) return new Point(ptStart.X, ptStart.Y - 1); 
  if (GetLeftPoint(bmpScr, ptStart, clr)) return new Point(ptStart.X - 1, ptStart.Y); 
  if (GetLDPoint(bmpScr, ptStart, clr)) return new Point(ptStart.X - 1, ptStart.Y + 1); 
  if (GetRDPoint(bmpScr, ptStart, clr)) return new Point(ptStart.X + 1, ptStart.Y + 1); 
  if (GetRUPoint(bmpScr, ptStart, clr)) return new Point(ptStart.X + 1, ptStart.Y - 1); 
  if (GetLUPoint(bmpScr, ptStart, clr)) return new Point(ptStart.X - 1, ptStart.Y - 1); 
  return Point.Empty; 
} 
//剩下的几个同理 
public static bool GetDownPoint(Bitmap bmpScr, Point ptStart, Color clr) { 
  if (bmpScr.Height <= ptStart.Y + 1) return false; 
  return bmpScr.GetPixel(ptStart.X, ptStart.Y + 1).ToArgb() == clr.ToArgb(); 
}
这样就能够得到 字符所在区域了 但是如果验证码粘字了 就会悲剧的 然后 就可以把区域里面的字符抠出来 作为样本了 因为这个百度的字符 有些旋转什么的 所以当时是抱着试一试的心态 写个一个程序 用上面的方式把图抠出来 然后自己手动输入验证码 然后将抠出来的图片和输入的验证码配对 分别保存到ABCD。。的文件夹里面 每个文件夹里面是一个字符的样本 采集好了只有 写个程序筛选出那些重复的 然后所有图片序列化到一个文件中、、而序列化的类差不多是这个样子:
[Serializable]
public class CodeImageModel
{
    private Hashtable m_hsCodeImage;
    public CodeImageModel() {
        m_hsCodeImage = new Hashtable();
    }
    public ListGetCodeImages(char ch) {
        if (m_hsCodeImage.Contains(ch)) {
            return (List)(m_hsCodeImage[ch]);
        }
        return null;
    }
}
对比的话就比较简单了:
public static int CmpImage(Bitmap bmpA, Bitmap bmpB) {
    int nCount = 0;
    using (Bitmap bmpTemp = new Bitmap(bmpA.Width, bmpA.Height)) {
        using (Graphics g = Graphics.FromImage(bmpTemp)) {
            g.DrawImage(bmpB, 0, 0, bmpA.Width, bmpA.Height);
            BitmapData bmpDataA = bmpA.LockBits(
                new Rectangle(0, 0, bmpA.Width, bmpA.Height), 
                ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
            byte[] byColorInfoA = new byte[bmpA.Height * bmpDataA.Stride];
            Marshal.Copy(bmpDataA.Scan0, byColorInfoA, 0, byColorInfoA.Length);

            BitmapData bmpDataB = bmpTemp.LockBits(
                new Rectangle(0, 0, bmpTemp.Width, bmpTemp.Height), 
                ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
            byte[] byColorInfoB = new byte[bmpTemp.Height * bmpDataB.Stride];
            Marshal.Copy(bmpDataB.Scan0, byColorInfoB, 0, byColorInfoB.Length);
            for (int x = 0, xLen = bmpA.Width; x < xLen; x++) { 
                for (int y = 0, yLen = bmpA.Height; y < yLen; y++) { 
                    byte byA = (byte)(GetAvg( 
                        byColorInfoA[y * bmpDataA.Stride + x * 3], 
                        byColorInfoA[y * bmpDataA.Stride + x * 3 + 1],
                        byColorInfoA[y * bmpDataA.Stride + x * 3 + 2]) <= 30 ? 0 : 255); 
                    byte byB = (byte)(GetAvg( 
                        byColorInfoB[y * bmpDataB.Stride + x * 3], 
                        byColorInfoB[y * bmpDataB.Stride + x * 3 + 1], 
                        byColorInfoB[y * bmpDataB.Stride + x * 3 + 2]) <= 30 ? 0 : 255); 
                    if (byA == byB) nCount++; 
                } 
            } 
            bmpA.UnlockBits(bmpDataA); bmpTemp.UnlockBits(bmpDataB);
        } 
    } 
    return nCount; 
}
两张图进去 搞成一样大的 然后看是遍历像素。。看看像素重叠率、、返回一个重叠的数量 到时候就取出 这个值最大的就是、、其他代码就不多上了、、

添加时间:2014-07-18 02:18:53 编辑时间:2014-07-18 15:01:14 阅读:1830 
捣鼓 C#
还没有人留言 要不你来抢一个沙发?
  • 编写评论

      我觉得区分大小写是一个码农的基本素质
[访问统计] 今天:7 总数:262818 提示:未成年人 请在大人陪同下浏览本站内容 还有:世界上最帅的码农 -> 石头 RSS:http://st233.com/rss Powered by -> Crystal_lz