@Lenciel

简单验证码的快速识别

昨天饭局上聊起来自动化测试或者是别的奇怪事业里经常需要面对的一个问题:验证码识别。

其实验证码的识别,技术上来说可以作为古老的 OCR(Optical Character Recognition)问题的一个子集:因为 OCR 其实就是从图片上把文字认出来嘛。

但它的有趣之处在于,验证码,也就是 CAPTCHA,本身就是’Completely Automated Public Turing test to tell Computers and Humans Apart’的缩写,也就是说在设计上它的目的就是要:

  1. 让人很容易认识出来
  2. 让机器很难认识出来

所以如果你电脑识别出来了验证码,要么就是它特别容易不符合#2 的要求,要么就是你实现了很不错的人工智能算法,这篇文章是讲第一种情况。

传统的做法来识别 OCR,主要需要处理的是下面三个环节:

  1. 图片二值化
  2. 字符的分割
  3. 字符的识别

二值化怎么做

所谓的「二值化」,就是图片上的像素要么灰度是 255(白),要么是 0(黑)。大致的思路就是把灰度大于或等于阈值的像素判为属于你关注的文字,置成 0;其他的像素点灰度置为 255。

具体的操作,我一般使用下面几种方式:

  1. 如果是特别简单地处理,用 PIL 库
  2. 如果是比较复杂的但是不需要很细致的控制,用ImageMagickconvert命令
  3. 如果是特别复杂,需要反复试验各种算法的,用 OpenCV

所以下面这两个验证码,哪个的难度大一些?

Don't touch me...

图1. 微林的验证码

Don't touch me...

图2. 饭局后J.Snow提供的验证码

如果你脑子里面没有二值化的概念大概会觉得第一个难度大一些,因为以人眼的视线去考虑,好像第一张要「难分辨」一些。

但其实第一张图所有的噪声都是花花绿绿的颜色,而验证码本身是纯粹的黑色,这种图片处理起来是相对容易的。只需要找到验证码像素点的颜色,用这种颜色选取这些像素点,拷贝到一张全白的图片上面即可。

要获取验证码的像素颜色可以参考这里的思路,把图片转成 256 色的,然后对所有的像素做一个统计然后标出它们在整个图片里面出现的频率。因为觉得原文里面的代码写得比较啰嗦(要学会写 lamda 啊)就做了一些修改:

import sys
from PIL import Image


def get_top_pixels(file_path, min_pt_num):
    im = Image.open(file_path)
    im = im.convert("P")
    top_pixels = []

    for index in enumerate(im.histogram()):
        if index[1] > int(min_pt_num):
            top_pixels.append(index)

    return sorted(top_pixels, key=lambda x: x[1], reverse=True)


if __name__ == '__main__':
    print(get_top_pixels(sys.argv[1], sys.argv[2]))

这个程序运行的结果如下:

$ python get_histdata.py regcode.png 30

[(0, 1471), (1, 214), (10, 110), (11, 97), (2, 85), (9, 83), (6, 66), (8, 58), (7, 49), (5, 37)]

拿到了颜色,就可以写一个简单的程序从图片里面拷贝这些像素到一张干净的图:

import sys
from PIL import Image


def clean_image(file_path, key_pix):
    im = Image.open(file_path)
    im = im.convert("P")
    im2 = Image.new("P", im.size, 255)

    for x in range(im.size[1]):
        for y in range(im.size[0]):
            pix = im.getpixel((y, x))
            # color of pixel to get
            if pix == key_pix:
                im2.putpixel((y, x), 0)

    im2.save("convert_%s.png" % key_pix)


if __name__ == '__main__':
    clean_image(sys.argv[1], sys.argv[2])

出现的最多的0显然是背景色,所以对110运行脚本:

$ python convert_grayscale.py regcode.png 1
$ python convert_grayscale.py regcode.png 10

结果如下:

Don't touch me... Don't touch me...

很明显目标像素是 1 而不是 10。

而 J. Snow 的这张图,首先验证码本身就是幻彩的而不是均匀一致的颜色,然后噪声又都是用这些幻彩颜色来生成的,所以如果只是简单的对颜色排序,会得到下面的结果:

[(225, 349), (139, 170), (182, 161), (219, 95), (224, 64), (189, 54), (175, 47), (218, 40), (90, 36), (96, 33)]

然后我们对排名靠前的像素进行提取会得到下面的结果:

Don't touch me... Don't touch me... Don't touch me... Don't touch me...

这种情况下怎么办?直观观察一下验证码,会发现背景噪声点相比验证码像素点来说很少(这也正常,都是一个颜色如果太多就没法看了), 很适合先做一些切割,然后进行模糊匹配(因为验证码的像素是幻彩的不是单一的,需要匹配相近像素点),然后再做二值化。

直接用 IM 的 convert 来处理比写代码简单:

$ convert 1.pic.jpg -gravity Center -crop 48x16+0+0  +repage -fuzz 50% -fill white -opaque white -fill black +opaque white resultimage.jpg

效果如下:

Don't touch me... Don't touch me...

字符怎么分割

其实整个验证码的识别里面,最难的是分割。特别是很多严肃的验证码,字体不是标准字体或者会变形,互相还可能粘连或者重叠,分割起来是非常难的。

但这里拿到的验证码相对简单,这部分不是问题就不展开了。

字符的识别

对于这里拿到的验证码而言,因为都是标准字体,可以直接使用 OCR 的开源工具读取,比如tesseract

$ tesseract resultimage.jpg -psm 7 output && cat output.txt

Tesseract Open Source OCR Engine v3.04.01 with Leptonica
Warning in pixReadMemJpeg: work-around: writing to a temp file

YLNU

如果不是标准字体的,因为分割完毕了就拿到了独立的字符,要识别就可以建一个模型,不断的训练它,来识别每个字符。

如果是更困难的呢?

可能你会觉得围棋电脑都会下了,那么认识验证码为什么还是比较难?

其实随便搜一下就会发现有很多人在做这方面的实验,主要的思路就是把 n 个字符组成的验证码当成有 n 个标签的图片来用 CNN 来解决。加上最近很多大公司开放了自己的人工智能平台,比如 Google 的 Tensorflow,我们这些没有大量计算资源的普通人也可以用它们实现自己的想法了。

推荐参考链接:

  1. CNN辨认车牌
  2. CNN 验证码识别

Starry Starry Night

Don't touch me

看 paper 是不是比较无聊的事情?

我试着在自己看的每篇论文标题前面加上「Harry Potter and The」,要不你也试试?

当然,更好的状态是,其实你不是因为要做某个事情在看 paper,而是出于好玩。

比如最近我看了一篇paper,名字叫「Seasonal Dating of Sappho’s ‘Midnight Poem’ Revisited」。

Sappho作为古希腊”第十繆斯”,在国内没有多显赫的名声,能找到的只有豆瓣上一篇和她相关艺术品的介绍文章。其实她的腕儿相当大,女同的称呼「lesbian」就取自她的居住地「Lesbos」。

而这篇论文是说,如何根据 Sappho 下面这首诗里面的描述,来判断这首诗写作于一年里面的什么时节。

The Moon hath left the sky;
Lost is the Pleiads' light;
It is midnight
And time slips by;
But on my couch alone I lie.

其中Pleiasds,如果你喜欢科幻的话肯定不会陌生,就是著名的金牛座七姐妹星团:昴宿星团(阿根廷一直有一帮人号称自己是来自这个星球,但其实这个星团有数不清的恒星组成)。

所以,Sappho 她:

  1. 住在 Lesbos
  2. 在午夜之前看到昴宿星团消失在地平线

在这些信息的帮助下,天文学家通过软件重建当时的星图,来得出了以下的结论:

Assuming that Sappho observed from Mytilene on the island of Lesbos, we determined that in 570 BC the Pleiades set before midnight from 25 January on, and were lost to the evening twilight completely by 6 April.

发了 paper 之后,作者还嘚瑟了一下,大概是说「这种准确描述星象和时间的作品实在是少,被我们抓到(发了篇有趣的 paper)哇哈哈哈」。

的确,要比较明确地描述,你首先得认识,能做到这点的诗人可不多。比如我Walt H. White枕边放的 Walt Whitman 是搞清楚了的:

Up through the darkness,
While ravening clouds, the burial clouds, in black masses spreading,
Lower sullen and fast athwart and down the sky,
Amid a transparent clear belt of ether yet left in the east,
Ascends large and calm the lord-star Jupiter,
And nigh at hand, only a very little above,
Swim the delicate sisters the Pleiades.

再比如我大杜甫是搞清楚了的:

人生不相见,动如参与商。
今夕复何夕,共此灯烛光。

参和商两个星,没法同时出现,用来形容人生不相见算是恰如其分。

还有流行歌选集《诗经》那些写词的是搞清楚了的:

七月流火,九月授衣。

「火」是指心宿二,所以「七月流火」是说这颗星逐渐从天空中消失

至于什么「七月流火,酷暑难耐」的用法,倒是现代人自己没有搞清楚了。