@Lenciel

记一次快乐的生日

Don't touch me

今天收到很多「生日快乐」的祝福。

蓼虫不知辛,容易有快乐时光。

但作为人类,能把生日完全过得快乐,要么是小孩儿,要么就是在过别人的。毕竟二十五岁之后,生日就像个没法取消的闹钟,响起时只让你觉得人生在途,白驹过隙。

这道理中国人其实最懂:所以主要负责祝寿的八仙,看着个个喜庆,其实都很惆怅。项目经理吕洞宾和何仙姑的失败恋情开了个坏头。「能开顷刻花」的韩湘子,「春风一拐」的铁拐李,他们的快乐也是转瞬即逝。边走边唱的蓝采和,算是最逍遥的,一开口也是「红颜一春树,流年一掷梭」,歌词再美,也是令人心灰的消极意味。

谁无痼疾难相笑,各有风流两不知。

所以看了很多书睡了很多姑娘的伍迪.艾伦说,变老没好处。「岁月流逝,你也不会变得更聪明,只会渐渐崩裂。人们总会说得很好听,你成熟了,你开始了解到生命的意义,也学着接受了。但若给你一个变回年轻的机会,这些你都可以不要。」

你看,得道仙人也好风流才子也罢,大都看起来清狂奇峻,骨子里敏感沉郁。书读得更好,才智更高的,一般都凋残得快。不然为什么是沈周常笑笑,而伯虎总怅怅。

毕竟人生忧患识字始,要承受世界被扩大的痛苦,难免寂寞。

所以钱钟书说,快乐在这狗屁倒灶的生活里,就好比诱引小孩吃药的方糖。

这样算起来,我很幸运。

作为一名创业中的大龄程序员,和一群志趣相投的伙伴混在一起,每天都在学习新的东西,处理新的问题。我们很多人都已经工作超过十年了,还是对技术有好奇心,绝大多数人都还在写代码。

在编程这个行当里面,大家好像都觉得到年纪了就不该「亲自上阵」了。说白了,就算打算一辈子做技术的人,也是把「做更好的 Engineer」当成目标的多,把「做更好的 Programmer 当成目标」的很少。

这大概是 Thorstein Veblen 在《有闲阶级论》里面讲的那个道理:脱离与谋生直接相关的工作,成为掌握着世界的主动权,控制着别人的节奏和命运,指挥别人去从事具体生产工作的「有闲阶级」,是从狩猎时代开始,人类社会普遍的奋斗目标。

用他的原话说,很多时候我们追逐的往往不是 utility(功用)而是 prestige(声望),这是一种心理上根深蒂固的需求:人类解决欲望的方式,就是不断地从各个层面满足它,而不是克服它。

因为克服起来太难了。

这态度本身也没有问题,但总是有些人选择比较忙碌的生活的。

这就好比有的人用自杀对抗命运,有的人用活着对抗命运。

于是有的人用闲适对抗命运,有的人用忙碌对抗命运。

我很喜欢的Knuth大叔是站在活到老写到老这边的,他说过:

People who discover the power and beauty of high-level, abstract ideas often make the mistake of believing that concrete ideas at lower levels are relatively worthless and might as well be forgotten. (…) on the contrary, the best computer scientists are thoroughly grounded in basic concepts of how computers actually work, and indeed that the essence of computer science is an ability to understand many levels of abstraction simultaneously.

当然,软件开发这职场里面,大多数人每天都很忙。有的是忙着开虚头巴脑的会;有的人是忙着混各种论坛、讲座和圈子;有的是忙着假装加班好不用回家带孩子,在公司里面看会儿视频打会儿游戏。

这些忙,既骗别人,也骗自己。

与之相反,年轻的时候,找到值得自己投入的方向,干一些真正在创造价值的事情,就不妨好好忙一下。

这是不是就算,但行好事,莫问前程。

简单验证码的快速识别

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

其实验证码的识别,技术上来说可以作为古老的 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 验证码识别