第一次尝试识别验证码,先从简单的开始。
上图为某处验证码。收集数张图片后可发现,图中字符只涉及移位这一种变换。但是,字符被允许粘连,或超出边界。
一般对于验证码的识别,基本的思路应该是:
此处验证码并没有人为加的干扰线等障碍。所以只需将图像二值化处理,便可进行下一步。
利用PIL(Pillow)进行二值化处理
PIL支持一些图像模式。此处需要了解的有:
1 (1-bit pixels, black and white, stored with one pixel per byte)
L(指Luminance) (8-bit pixels, black and white)
RGB (3x8-bit pixels, true colour)
用PIL打开验证码图像的时候一般是RGB模式。先使用
img . convert ( 'L' )
将图像转换为灰度模式。此时每个像素上储存的信息为该点的亮度,0为最暗(黑色),255为最亮(白色)。但是这时仍然可以看到,原图上一些因为压缩而产生的色块仍然存在。接下来用
img = img . point ( lambda x : 1 if x >= 160 else 0 , mode = '1' )
来二值化图像。point
可以按照指定的lookup table或函数映射图像。mode
参数可以直接转换图像模式(仅支持部分模式)。此处lambda表达式中设定的阈值为160,该值需要根据不同图像的实际情况来设定。(亮度)高于该值的点会被map成1(白色),其余0(黑色)。
此时若要输出图像,可选择bmp格式,jpg格式的压缩可能给二值化后的图像带来干扰。
切割字符
虽然这个验证码比较低级,但是要切割出里面的字符,也不是最简单的方法(平均分)就可以搞定的。
def append_to_char_boxes_LR ( lst , left , right ):
if right - left > 8 : # maximum char width == 8(此处的8是完成切割部分代码并建立了字符库后重新填上的)
mid = ( left + right ) // 2
lst . append (( left , mid ))
lst . append (( mid , right ))
else :
lst . append (( left , right ))
for x in range ( img_width ): # 找出每个字符的左、右边界
empty_column = True
for y in range ( img_height ):
if px [ x , y ] == 0 : # black
empty_column = False
if stage == STAGE_SEARCHING_FOR_CHAR :
stage = STAGE_PROCESSING_CHAR
left = x
break
if stage == STAGE_PROCESSING_CHAR :
if empty_column :
stage = STAGE_SEARCHING_FOR_CHAR
right = x
append_to_char_boxes_LR ( char_boxes_LR , left , right )
else :
if x == img_width - 1 : # last column(字符可以超出边界,所以要注意这种情况)
stage = STAGE_SEARCHING_FOR_CHAR
right = img_width
append_to_char_boxes_LR ( char_boxes_LR , left , right )
if len ( char_boxes_LR ) != 4 : # 验证码是4个字符。如果没有4组边界,一定是错的。
return None
上面的代码将图片中的像素一列一列地遍历,试图找出由白色(无字符)到黑色(有字符)的分界。同时,它处理两种特殊情况:
若已经进入STAGE_PROCESSING_CHAR
状态,但搜索到最后一列仍未能退出该状态,则判定为字符已经超出图片边界,直接将起点到图片右边缘的范围作为一组边界。
若某次搜索过程中,已经处于STAGE_PROCESSING_CHAR
状态超过8列,则判定为字符粘连。此时代码会继续搜索,直到找出右边界,并从中间平均切开,生成两组边界。
# TODO: 寻找比“从中间平均切开”更合适的处理方法
另,此处须注意PIL库的crop
函数的使用方法:
The region is defined by a 4-tuple, where coordinates are
(left, upper, right, lower). The Python Imaging Library
uses a coordinate system with (0, 0) in the upper left corner.
四个参数分别是裁切得到的区域的左、上、下、右四条边界线的“位置”。而且,此函数裁切时“含上不含下,含左不含右“。以上述代码为例,则可发现左边界的“位置”是一个字符包含的首个黑色像素的x值,而右边界的“位置”是一个字符右边首个空列(完全不包含黑色字符的一列)的x值。只有这样,才能不多不少地裁切出目标区域。PIL对超出图片范围的裁切参数也会接受,所以此处若不小心则可能无法发现bug。此规则亦适用于上、下边界。
依法炮制上、下边界,再调用crop
函数,即可获得四个字母的图像。
建立字符库
完成裁切部分代码后,只要收集足够的验证码图像,裁切,并手工从中找出一组包含验证码使用的所有字符的完整的(无粘连、无缺损)的字符图片,为每张图片标识好对应的字符,就可以建立用于识别这个验证码图像的样本库。
# TODO: 扩大样本库适用范围
识别验证码
需要识别的验证码图片也应该按照上面的处理方法处理,并得到四个字符的图像。下面需要对四个字符分别进行识别。
首先,通过观察可以发现,对于那些可以完整地切出来(无粘连、无缺损)的字符,甚至根本不需要使用到KNN算法,因为这个例子中的验证码生成器不会对字符进行变形,即只要是两个完整的、相同的字符,它们必定拥有相同的图像数据(其实即使有粘连/缺损,只要粘连/缺损的方式、位置一致,也是可以的,但这样要求有更多的样本)。在这个例子中,我储存了样本库中字符的hash值;识别时,会先比较待识别字符是否与库中字符拥有相同hash值。然后只对未能通过hash匹配识别的字符使用KNN算法识别。
因为字符可以缺损、粘连,所以在使用KNN时,需要计算待识别字符与样本拥有不同相对位置时的相似度,并取最高得分作为该样本与待识别字符的相似度。但是,此处有一点可以简化,即,因为该验证码中字符不会被缩放、旋转等,故若待识别字符的宽度(或高度)大于某个样本的宽度(或高度),则可以直接排除该样本。
def calculate_similarity ( cdata , csize , sdata , ssize ):
cwidth , cheight = csize # size: (width, height)
swidth , sheight = ssize
if csize == ssize :
score = 0
for x in range ( cwidth ):
for y in range ( cheight ):
cdxy = cdata [ x , y ]
sdxy = sdata [ x , y ]
if cdxy == sdxy :
if cdxy == 0 : # We do not increase score if the pixels are white.
score += 1
else :
score -= 1
return score
else : # return the highest score obtained among all positions
if cwidth > swidth or cheight > sheight : return - 100 # This captcha generator does not zoom characters. Hence csize must not exceed ssize.
score_list = []
for xoffset in range ( swidth - cwidth + 1 ): # all possible positions on x axis
for yoffset in range ( sheight - cheight + 1 ):
score = 0
for x in range ( cwidth ):
for y in range ( cheight ):
cdxy = cdata [ x , y ]
sdxy = sdata [ x , y ]
if cdxy == sdxy :
if cdxy == 0 : # We do not do score++ if the pixels are white.
score += 1
else :
score -= 1
score_list . append ( score )
return max ( score_list )
def visual_recognise_char_img ( cimg ):
cdata = cimg . load ()
csize = cimg . size
score_list = [] # Stores score for each possible char. item: (score, char_name)
for char_name , sdata , ssize in samples :
score_list . append (( calculate_similarity ( cdata , csize , sdata , ssize ), char_name ))
return max ( score_list , key = lambda x : x [ 0 ])[ 1 ]
上面的代码中,对待识别字符和样本字符的对应点的分数计算方法为:
若两者为一黑一白,则扣1分
若两者均为白色,则不增不减
若两者均为黑色,则加1分
References