最近刚好又开始搞这个,所以回答一发。
提前预警,这篇文章将会比较长!!!!!
针对题主的问题,我想说的是,只是你没有看到而已,针对验证码的识别,无数人在做,网络上的打码平台一搜一大把,很多人是靠这个赚钱的。
至于难度,针对单个网站的验证码,老实说,在当今深度学习的威力下,单个网站验证码的破解已经不是什么难事了,难就难在通用性,不同的字库、不同的扭曲干扰类型,会给训练带来极大的难度提升,所以你去github上看验证码识别项目,基本都是针对单一类型的验证码,然后下好样本做好标注,识别起来确实准确率高,但你让他们做一款通用的出来,他们估计只有摇摇头,这也是人家打码平台可以靠这个开起一家公司的原因。
举个例子,github上收藏最多的验证码项目,所识别的验证码
这种类型,乍一看感觉很牛逼,但是用它的原模型识别这种:
很可能就会识别错误,而且由于字体的影响,这种不经过预处理的验证码识别模型,通用性是极差的,甚至可能会连一些简单的验证码都识别错误,比如这种:
针对这一点,我想很多真正做过的朋友应该会有很深的感触,除非你能把所有网站的验证码都收集一波然后标注训练,不过那样做的成本也太大了。
接下来准备释放一些干货,分享一下我自己在做通用验证码识别过程中的一点心得,也和各位讨论一下以谋求进步。
首先说说很多程序员使用的pytesseract,老实说这个库我也用过,但是大家用过之后想必也知道效果,那就是简单的识别还可以,稍微复杂一点就直接gg,比如这篇文章讲的:
所以关于这个库我不打算多说,因为这离我们想要的效果还差得很远,即便是做了去干扰,识别效果也极为有限,针对入门级的验证码还可以。
接下来再说说使用深度学习做验证码识别的同学,其实网上也有很多类似的文章了,比如:
这种方式的问题我在之前也说过了,通用性较差,一旦出现不同类型的干扰或字体,识别率就会断崖式下跌。
那么,做通用的验证码识别难点究竟在哪些方面呢?我归纳了一下,大致如下:
我针对这几个问题也一一说一下难在那些方面:
验证码本身复杂性
验证码本身的复杂性在于,大小写字母数字混合,不同字体相似性带来的问题,编码的问题,举个例子:
一个四位数的验证码,我们在将它传入深度学习模型之前,怎么来转化他呢?肯定是想着将它进行one-hot编码,这个不懂的同学可以去搜一搜什么是one-hot编码,那么针对大小写+数字混合的四位数验证码编码有多复杂呢?
先说数字型,可以形成验证码的个数是0000~9999,如果强行用one-hot编码,即
[1,0,0,0,0,0,0……0,0,0]来表示0000
[0,1,0,0,0,0,0……0,0,0]来表示0001
这个数的话,那么这个数组的长度将是10000
如果加上大小写字母,按排列那么这个数组的长度将扩充到 62^4长,所以肯定是不能这么编码的,解决方案只有将62中不同类型的字符放入一个数组中
[0,1,2,3,4,5……a,b,c,d……A,B,C……]
然后
[1,0,0,0,0,0,0,0,0……] =》0
[0,0,0,0,0,0,0,0,0,0,1]=>a
这样来表示,即每一个62位长的数组代表一个字符,那么四位长的验证码形成的字符就是一个62*4=248位长的数组,这样即可表示所有四位长的验证码了。
验证码本身的复杂性除了这个其实还体现在字体,比如
这两个都是G,以哪个为准呢?
在这个字体中,大写的T是不是和r很像,生成验证码识别是不是容易出现错误呢?
I 1 7 i L l 这几个到底哪个是哪个生成验证码以后你确定你看得出来么?
说实话,有时候我都看不出来,真的太难为机器了。
去燥降噪的难度
这个在我之前的一篇关于验证码的文章中有提到过,大家可以去看看,但是,即便有了一些通用的降噪去燥手段,针对不同的平台验证码,乱七八糟的混淆干扰,效果也是极为有限的,大多都是针对单一平台特定验证码进行降噪,通用的还是非常有难度。
验证码切割的难度
关于验证码切割在我之前那篇文章中也有提到,采用连通域切割的方式来做,在知乎上有另一位答主写的更加详细,大家可以看一下
大家可以看到使用连通域方式分割验证码存在的几个问题就是,会将一些区域错误的划分,而且可能会出现n多乱七八糟的区域,比如将同一个位置划分两次,将两个数字划分到一起
并且返回的是一组算法自认为的连通域,就可能出现图中那样返回了六七组甚至几十组连通域,这种时候哪一组才是正确的呢?
另外,遇到字符粘连的,怎么办?
建模方面的问题
建模方面其实相对来说复杂度要好很多,主要是需要不断的去调参,不然很容易出现过拟合等情况,这样就会导致识别准确率急剧下降,当然这个大家有各自的建模方式,就不细说了。
下面放几张我自己做的模型对通用验证码的识别效果,涉及到切割等
可以看到,对不同类型干扰的验证码都有不错的识别效果,从图中可以看到打印出来的四个数组,那就是进行图像切割后切割出的区域,具体的切割效果以上面的smcs为例:
大家可以看到,M的切割效果较差,还有其它的字符也存在一些干扰,那是因为我还并没有做降噪处理,但即便如此,模型都已经可以将其识别出来了。
接下来说说我的整个模型识别所经历的一些步骤和对上述提出问题的解决方案:
(不好意思,鸽了挺久,今天来填坑了)
我看评论中有人提到CTC切割,这个我确实不太了解,后面会学习一下,感谢知友提供的思路。
说说我目前做到的验证码识别的整个流程。
1.验证码降噪灰度二值化
灰度和二值化这个对于玩验证码的人来说,应该算是基本操作,一般就是几个函数搞定
cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) ret, img = cv2.threshold(img, 180, 255, cv2.THRESH_BINARY)
只需要注意的是这个分割值180,使用这个可以滤掉很多颜色较浅的干扰,但同时也可能破坏正常验证码的形态,所以大家可以根据实际情况进行调节。
至于降噪算法,种类挺多,这里给大家提供四种较为简单的:
def img_salt_filter(img): """ 去除椒盐类干扰 :param img: :return: """ newIMG = cv2.medianBlur(img, 5) return newIMG def img_open_filter(img): """ 图像开运算,去除噪点类干扰以及补全验证码缺口 :param img: :return: """ kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (2, 2)) kernel2 = cv2.getStructuringElement(cv2.MORPH_CROSS, (2, 2)) newImg = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel) newImg = cv2.dilate(newImg, kernel2) return newImg def img_close_filter(img): """ 图像闭运算,去除干扰 :param img: :return: """ kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (2, 2)) kernel2 = cv2.getStructuringElement(cv2.MORPH_CROSS, (1, 1)) closed = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel) newImg = cv2.erode(closed, kernel2, iterations=1) return newImg def neighbo_filter(img) """ 邻域扫描运算,去除干扰 :param img: :return: """ 这个就是扫描一个像素点附近是否存在其他点,很简单,就不贴出来了
2.图像切割
这里也给大家送上我自己写的针对四位数验证码的切割算法,对于不粘连的验证码切割效果很好,返回值为四张图像的验证码数组。
def img_mser(img): """ 图像切割算法 :param img: :return: """ # 图像预处理 img = img_resize(img) box_content = list() box_mser = list() mser = cv2.MSER_create(_min_area=5, _max_area=300) img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, img = cv2.threshold(img, 200, 255, cv2.THRESH_BINARY) # img_show(img) # try: # 连通域划分及去重 regions, boxes = mser.detectRegions(img) print len(boxes) boxes = boxes.tolist() for i in boxes: if i not in box_content: box_content.append(i) # 使用欧氏距离算法去除近似连通域 for j in box_content: if box_mser: for k in xrange(len(box_mser)): if Euclidean(j, box_mser[k]) > 4 and k == len(box_mser) - 1: box_mser.append(j) else: box_mser.append(j) # 去除多余连通域 box_mser_len = len(box_mser) if box_mser_len != 4: if box_mser_len > 4: for i in box_mser: area = list() area.append(i[2] * i[3]) for j in xrange(box_mser_len - 4): pos = area.index(min(area)) - 1 box_mser.remove(box_mser[pos]) else: print u'切割出现问题,请改变设置值' # print box_mser cut_img = list() box_mser.sort() for z in xrange(len(box_mser)): k = box_mser[z] slice_img = img[k[1]-1:k[1] + k[3]+3, k[0]-2:k[0] + k[2] + 2] # img_show(slice_img) cut_img.append(cv2.resize(slice_img, (30, 40))) return cut_img def img_resize(img): img = cv2.resize(img, (50, 20)) return img def Euclidean(vec1, vec2): """ Euclidean_Distance,欧式距离 :param vec1: :param vec2: :return: """ npvec1, npvec2 = np.array(vec1), np.array(vec2) return math.sqrt(((npvec1 - npvec2) ** 2).sum())
这个算法中解决了系统本身自带联通划分中存在的
等乱七八糟的问题,但针对粘连较紧的验证码效果不好,另外有几个参数可能会根据实际情况调节。
3.模型训练
这个其实都很简单,我个人使用的三层卷积+两层全连接构建的模型,其中需要注意的坑是:
贴一下代码
model = Sequential() inputShape = (height, width, depth) # 第一层卷积 model.add( convolutional.Conv2D(filters=32, kernel_size=(5, 5), input_shape=inputShape, data_format="channels_last") ) model.add(Activation('relu')) model.add(MaxPooling2D( pool_size=(2, 2), strides=(2, 2), padding='same', )) # 第二层卷积 model.add( convolutional.Conv2D(filters=128, kernel_size=(3, 3), input_shape=inputShape, data_format="channels_last") ) model.add(Activation('relu')) model.add(MaxPooling2D( pool_size=(2, 2), padding='same', )) # 第三层卷积 model.add( convolutional.Conv2D(filters=128, kernel_size=(3, 3), input_shape=inputShape, data_format="channels_last") ) model.add(Activation('relu')) model.add(MaxPooling2D( pool_size=(2, 2), padding='same', )) # # 第四层卷积 # model.add( # convolutional.Conv2D(filters=128, kernel_size=(2, 2), input_shape=inputShape, data_format="channels_last") # ) # model.add(Activation('relu')) # model.add(MaxPooling2D( # pool_size=(2, 2), # padding='same', # )) # 三维向量一维化 model.add(Flatten()) ''' 第一层全连接层 ''' model.add(Dense(1024)) model.add(Activation('relu')) model.add(Dropout(0.2)) ''' 第二层全连接层 ''' model.add(Dense(CLASS_NUM)) model.add(Activation('softmax')) adam = Adam(1e-4) model.compile( optimizer=adam, loss='categorical_crossentropy', metrics=['accuracy'] ) return model
另外训练的样本大家最好多找一些类别,我目前供训练的验证码样本也很少,基本都是自己找的几种生成算法生成,其实这样很不好(主要还是自己打标记成本太大)
说说我这样做的一个思路,其实主要就是尽可能的排除干扰,还原数字或者字母本身最标准最原始的状态,尽量让模型识别起来难度最低。我这个训练样本就只有两万多张,对通用数字型小干扰验证码的识别率可以达到95%以上,字母型的90%以上,当然干扰太严重的就不行了。
当然有的大佬手握kw甚至上亿张标注好的各种类型的验证码,不用切割或者其他什么的,直接硬怼,那也没办法,拼不过。
还有一些上面遗留的问题:
对于粘连的验证码怎么办?
像素投影、骨架、滴水算法、拉伸切割,还不行的话我也找不到好的方案了。
对于不同字体间造成的字符类似怎么办(比如我上述举例的r和T)?
办法就是在训练样本中尽量避免这种特殊字体造成的问题,将这类剔除掉
对于l(L)和I(i)和1(有的就是直接一竖)这种怎么办?
没有办法
太复杂的干扰怎么办?
对于比较复杂的干扰,用我上述提到的几种通用的降噪方式可能就不行了,这种既需要针对性的分析,比如干扰线与字符线是否存在像素差,干扰是否有规律可循,是否可以通过固定位置切割排除一部分干扰等等。
另外提醒一下准备自己动手的朋友,通常验证码输入是不分大小写的
全文结束,感谢大家的关注,有什么问题可以在评论问我,我看到了就回复,希望能跟大家碰撞出更好的思路。
知乎首答一发!!!!
还记得从前,验证码还只是这样:
作为一名优秀的程序猿的你在想,为什么不做一个自动识别软件,验证码全是数字,你只要收集一个包含10个数字的训练库,把图片二值化然后分隔单个字符最后对比训练库里的数字最终识别,so easy!
作为一个有情怀的天才程序猿,你花了1天搞定了该程序,自豪感爆棚。
自己用了几天赶脚非常不错,于是你不仅自己用,还发给亲戚朋友们用。尽管它只是省了3秒钟的输入验证码的时间不过你的亲戚朋友们因为觉得它确实很高大上,纷纷夸你并把它分享给自己的朋友。
然而,终于有一天,你的作品被传到了另一个天才程序猿手里,非常不巧的是,他就是运营这个网站并且整出“验证码”这玩意儿的那个程序猿。他的工作任务就是确保在用户电脑前执行登陆或者注册操作的是一个“人”而不是某个黑科技刷子。
看完你的东西他瞬间觉得日了狗了,觉得你的软件是对他工作的侮辱,是在向他的智商发出挑战...
于是,这货大手一挥,后来的验证码变成了,这样:
次日你刚起床,发现你的邮箱已经炸了,里面全是大家在向你反馈你的软件不能用了。于是打开网页看到了新版的验证码。
你冷哼一声,当然,你是一个天才程序猿,你只简单的向你的训练库里又添加了52个大小写英文字母就解决了这个问题。完了以后你还顺手添加了几个日文平片假名,也没别的目的,就是多装个B。
于是你的软件又能用了。你觉得你的智商已经碾压了这个做网站的货。
然而,不可避免的,过了几天你发现,验证码开始丧心病狂了,它已经变成了这样:
现在你就觉得有点懵逼了,汉字那么多,你觉得这个对面那个做验证码的程序猿就是想玩儿死你。不过没关系,换汤不换药。于是你又花了一个礼拜,写了一个字符自动截取的代码,在网上整理出了常用汉字3000个,并且分别截取了这3000个字的黑体、宋体和楷体的图片并放进了训练库里。你的程序又能用了。
这次以后你觉得很累但是真的很有成就感,你觉得你就是全天下最diao的程序猿。然后你就去补睡了。
然而,你不会意识到的是,在你睡觉的时候,另一个程序猿正在被你逼疯。
几天以后,你发现验证码突然变得开始反人类:
有,这样的
这样的
这样的
这样的
还有这样的
- -|
等到某天你再从睡梦中醒过来的时候会发现,整个世界都已经不一样了,你的邮箱里全是愤怒的朋友、朋友的朋友还有朋友的朋友的朋友。
他们不单单只吐槽你的软件不能用了,而且更关键是!!喵了个咪的这TM啥玩意儿啊!直接用眼看都特么看不清这些验证码到底是个毛了啊!!!!
看到这些牛鬼蛇神我猜你整个人都已经斯巴达了!!!
但是怎么办,你是天才程序猿!你不能输啊!
于是你系上头巾,泡好咖啡,借了各种书籍撸起袖子准备开干,势要搞定这些验证码。
此时,电话响了。
是你的老妈。
你妈说:儿啊,你妈跟你爸打算出去度个假,想要在网上订个火车票,上了12306的网站,但是它最近不知道为什么突然换了一种验证码啊。你爸妈是真老了,看了看发现是真不会输这个验证码,听二姨说你搞了个什么软件能直接自动给输了,你给我看看呗。
你轻松的应承下来,不急不慢的打开12306网站。
终于
你惶恐的小眼神一眼就看到了如下玩意儿:
听说大部分程序狗高中的时候都学的理科!!!!!!!!!!
就问你服不服!
-----------------------------------------------------------分割一下-----------------------------------------------------
槽吐完了再给大家看一看最初的那个清纯简单的验证码
1秒辨别,2秒输入
所以想要表达的是,其实自动识别验证码这种东西,只要你不是黄牛党僵尸号出售员刷票专业户这种职业,花尽了心思去做完了也就只是装装X,并没有什么卵用。何必呢。
最后想一想人家对面哪位一直跟你杠正面,不停开脑洞搞出更加变态的验证码的小哥,你熬一个夜意味着他马上得熬一个夜,于是你又熬两个夜,他再熬四个夜...... 0.0 炸!
所以,大家都是程序狗,大学选专业的时候一不小心走了神才踏上这条不归路的,互相放一条生路吧!!!Q.Q
------------------------------------------------------------------再割一下------------------------------------------------
(8.29)感谢大家的点赞~ 知乎首答就这么多赞开心得不得了lol。
有同学问我说我似乎没有真正回答为什么程序猿不做自动识别的这个问题
答案就是,又不是没做出来过
好吧我想我们可以来个类比:
做验证码的那位程序猿A就像是拿了一个花瓶来让大家认,做自动识别程序的小哥B就像是一直试图教机器认识这是个花瓶一样。
起初,A为了反击B,给花瓶上色、用布包起来只留个轮廓或者干脆掰掉一个把手来试图让B的程序识别不了这个花瓶。
可是B是个很牛X的程序猿啊而且他调教的程序也相当的蒸汽!每每都能拆掉A出的奇招。
但是每次A只要随手给出一点变动,B就要花上一两天来继续优化他的程序!
可是各位,你们有没有发现再这样下去无非只有一个结局。
就是... ...
A说:"妈蛋!算你狠!LZ不跟你玩儿了!大不了咱俩鱼死网破!"
然后顺手直接把花瓶砸了......
"你丫倒是接着拼起来认啊!!"
所以事实就是,现在的很多奇葩验证码已经很难做到自动识别了,而且就算有人做到了,估计识别率暂时也无法保证,而有情怀的程序猿们一般都会把用户体验看得很重,残次品是不会到处去发给大家炫耀的。
况且
等到有一天程序猿B调教好了程序又能识别了
A只要把花瓶渣捡起来砸得更碎一点=、= 管你们自己用眼睛还能不能识别
----------------------------------------------------------------------
部分图片来源于网络,侵删。