现在,Megaupload站点提供的CAPTCHA在上述代码面前已经败下阵来,说实话,这里的验证码设计的不不太好。但更有趣的是:
1.HTML 5中的Canvas应用程序接口getImageData可以用来从验证码图像中取得像素数据。利用Canvas,我们不仅可以将一个图像嵌入一个画布中,而且之后还可以再从中重新提取出来。
2.上述的脚本中包含一个完全使用JavaScript实现的神经网络。
3.使用Canvas从图像中提取出像素数据后,将其送入神经网络,通过一种简单的光学字符识别技术来推测验证码中到底使用了哪些字符。
通过阅读源代码,我们不仅可以更好地理解其工作原理,也可以领会这个验证码究竟是如何实现的。就像前面看到的那样,这里使用的验证码不是很复杂——每个验证码有三个字符组成,每个字符使用一种不同的颜色,并且只使用26个字母中的字符,而所有字符都使用同一种字体。
第一步的用意很明显,那就是把验证码拷贝到画布上,并且把它转化为灰度图。
function convert_grey(image_data){
for (var x = 0; x < image_data.width; x++){
for (var y = 0; y < image_data.height; y++){
var i = x*4+y*4*image_data.width;
var luma = Math.floor(image_data.data[i] * 299/1000 +
image_data.data[i+1] * 587/1000 +
image_data.data[i+2] * 114/1000);
image_data.data[i] = luma;
image_data.data[i+1] = luma;
image_data.data[i+2] = luma;
image_data.data[i+3] = 255;
}
}
}
然后,将画布分成三个单独的像素矩阵,每个矩阵包含一个字符。这一步实现起来非常容易,因为每个字符都使用一种单独的颜色,所以通过颜色就可以将其区分开来。
filter(image_data[0], 105);
filter(image_data[1], 120);
filter(image_data[2], 135);
function filter(image_data, colour){
for (var x = 0; x < image_data.width; x++){
for (var y = 0; y < image_data.height; y++){
var i = x*4+y*4*image_data.width;
// Turn all the pixels of the certain colour to white
if (image_data.data[i] == colour) {
image_data.data[i] = 255;
image_data.data[i+1] = 255;
image_data.data[i+2] = 255;
// Everything else to black
} else {
image_data.data[i] = 0;
image_data.data[i+1] = 0;
image_data.data[i+2] = 0;
}
}
}
}
最终,所有无关的干扰像素都被剔除出去。为此,可以先查找那些前面或者后面被黑色(未匹配的)像素围绕的白色(匹配过的)像素,然后将匹配过的像素删除即可。
var i = x*4+y*4*image_data.width;
var above = x*4+(y-1)*4*image_data.width;
var below = x*4+(y+1)*4*image_data.width;
if (image_data.data[i] == 255 &&
image_data.data[above] == 0 &&
image_data.data[below] == 0) {
image_data.data[i] = 0;
image_data.data[i+1] = 0;
image_data.data[i+2] = 0;
}
现在我们已经得到了字符的大约图形,但在将其载入神经网络之前,脚本还会进一步对它进行必要的边缘检测。脚本会寻找图形最左、右、上、下方的像素,并将其转化为一个矩形,接着把矩形重新转换为一个20*25像素的矩阵。
cropped_canvas.getContext("2d").fillRect(0, 0, 20, 25);
var edges = find_edges(image_data[i]);
cropped_canvas.getContext("2d").drawImage(canvas, edges[0], edges[1],
edges[2]-edges[0], edges[3]-edges[1], 0, 0,
edges[2]-edges[0], edges[3]-edges[1]);
image_data[i] = cropped_canvas.getContext("2d").getImageData(0, 0,
cropped_canvas.width, cropped_canvas.height);
经过上面的处理,我们得到了什么呢? 一个20*25的矩阵,其中包含单个矩形,其中填由黑白色。真是太好了!
然后,会对这个矩形做进一步的简化。我们策略性地从矩阵中提取一些点,作为“光感受器”,这些光感受器将输送到神经网络。举例而言,某个光感受器具体对应的可能是位于9*6位置像素,有像素或者没有像素。脚本会提取一系列这样的状态(远少于对 20*25矩阵整个计算的次数——只提取64种状态),并将这些状态送入神经网络。
您可能要问,为什么不直接对像素进行比较?有必要使用神经网络吗?问题的关键在于,我们要去掉那些模棱两可的情况。如果您试过前面的演示就会发现,直接进行像素比较比通过神经网络比较,更容易出错,尽管出错的时候不多。但我们必须承认,对于大部分用户来说,直接的像素比较应该已经够用了。
下一步就是尝试猜字母了。神经网络中导入了64个布尔值(由其中的一个字符图像获取而来),同时包含一系列预先计算好的数据。神经网络的理念之一,就是我们希望得的结果事先就是知道的,所以我们可以针对结果对神经网络进行相关的训练。脚本作者可以多次运行脚本,并收集了一系列最佳评分,这些评分能帮助倒推出产生它们的那些值,从而帮神经网络猜出答案,除此之外,这些评分没有任何特殊意义。
当神经网络对验证码中一个字母对应的64个布尔值进行计算以后,和一个预先计算好的字母表相比较,然后为和每个字母的匹配都给出一个分数。(最后的结果可能类似:98%的可能是字母A,36%的可能是字母B等。)
当对验证码中的三个字母都经过处理以后,最终的结果也就出来了。需要注意的是,该脚本无法达到100%正确性(不知道如果在开始的时候不将字母转换成矩形,是不是可以提高评分的精度),但这已经相当好了,至少对于当前的用途来说是这样。而且所有的操作都是在基于标准的客户端技术实现的浏览器中完成的!