`
jishublog
  • 浏览: 868925 次
文章分类
社区版块
存档分类
最新评论

借@阿里巴巴 耍了个帅——HTML5 JavaScript实现图片文字识别与提取

 
阅读更多

写在前面


8月底的时候,@阿里巴巴 推出了一款名为“拯救斯诺克”的闯关游戏,作为前端校园招聘的热身,做的相当不错,让我非常喜欢。后来又传出了一条消息,阿里推出了A-star(阿里星)计划,入职阿里的技术培训生,将接受CTO等技术大牛的封闭培训,并被安排到最有挑战的项目中,由技术带头人担任主管。于是那几天关注了一下阿里巴巴的消息,结果看到这么一条微博(http://e.weibo.com/1897953162/A79Lpcvhi):

此刻,@阿里足球队 可爱的队员们已经出征北上。临走前,后防线的队员们留下一段亲切的问候,送给对手,看@新浪足球队 的前锋们如何破解。@袁甲 @蓝耀栋 #阿里新浪足球世纪大战#

阿里足球队

目测是一段Base64加密过的信息,但无奈的是这段信息是写在图片里的,我想看到解密后的内容难道还一个字一个字地打出来?这么懒这么怕麻烦的我肯定不会这么做啦→_→想到之前有看到过一篇关于HTML5实现验证码识别的文章,于是顿时觉得也应该动手尝试一下,这才是极客的风范嘛!


Demo与截图


先来一个大家最喜欢的Demo地址(识别过程需要一定时间,请耐心等待,识别结果请按F12打开Console控制台查看):

http://www.clanfei.com/demos/recognition/


再来张效果图:
HTML5 JavaScript实现图片文字提取


思路


实现一个算法,思路是最重要的,而实现不过是把思想转化为能够运行的代码。

简单地说,要进行文本识别,自然是拿图片的数据与文字的图形数据进行对比,找到与图片数据匹配程度最高的字符。

首先,先确定图片中文本所用的字体、字号、行距等信息,打开PhotoShop,确定了字体为微软雅黑,16像素,行距为24,Base64文字的开始坐标为(8, 161)。

然后,确定要进行匹配的字库,Base64编码中可能出现的字符为26个字母大小写、10个数字、加号、斜杠,但目测在图片中没有斜杠出现,因此字库应该为:

0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+

接着,是确定如何判断字符是否匹配,由于只需要对字型进行匹配,因此颜色值对算法并无用处,因此将其灰度化(详见百度百科),并使用01数组表示,1代表该像素点落在此字符图形上,0反之,而如何确定该某个灰度值在数组中应该表示为0还是1,这个转换公式更是算法中的关键。

最后,将字型的灰度化数据与图片中文字部分的灰度化数据进行对比,将误差最小的字型作为匹配到的字符,然后进行下一个字符的匹配,直到图片中所有字符匹配完毕为止。


递归实现


详细的思路于代码注释中,个人觉得这样结合上下文更为容易理解(注:代码应运行于服务器环境,否则会出现跨域错误,代码行数虽多,但注释就占了大半,有兴趣可以耐心看完,图片资源于上方“写在前面”)。

  1. <!doctype html>
  2. <htmllang="zh-CN">
  3. <head>
  4. <metacharset="UTF-8">
  5. <title>文字识别</title>
  6. </head>
  7. <body>
  8. <canvasid="canvas"width="880"height="1500"></canvas>
  9. <scripttype="text/javascript">
  10. varimage=newImage();
  11. image.onload=recognition;
  12. image.src='image.jpg';
  13. functionrecognition(){
  14. // 开始时间,用于计算耗时
  15. varbeginTime=newDate().getTime();
  16. // 获取画布
  17. varcanvas=document.getElementById('canvas');
  18. // 字符库
  19. varletters='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+';
  20. // 字型数据
  21. varletterData={};
  22. // 获取context
  23. varcontext=canvas.getContext('2d');
  24. // 设置字体、字号
  25. context.font='16px 微软雅黑';
  26. // 设置文字绘制基线为文字顶端
  27. context.textBaseline='top';
  28. // 一个循环获取字符库对应的字型数据
  29. for(vari=0;i<letters.length;++i){
  30. varletter=letters[i];
  31. // 获取字符绘制宽度
  32. varwidth=context.measureText(letter).width;
  33. // 绘制白色背景,与图片背景对应
  34. context.fillStyle='#fff';
  35. context.fillRect(0,0,width,22);
  36. // 绘制文字,以获取字型数据
  37. context.fillStyle='#000';
  38. context.fillText(letter,0,0);
  39. // 缓存字型灰度化0-1数据
  40. letterData[letter]={
  41. width:width,
  42. data:getBinary(context.getImageData(0,0,width,22).data)
  43. }
  44. // 清空该区域以获取下个字符字型数据
  45. context.clearRect(0,0,width,22);
  46. }
  47. // console.log(letterData);
  48. // 绘制图片
  49. context.drawImage(this,0,0);
  50. // 要识别的文字开始坐标
  51. varx=beginX=8;
  52. vary=beginY=161;
  53. // 行高
  54. varlineHeight=24;
  55. // 递归次数
  56. varcount=0;
  57. // 结果文本
  58. varresult='';
  59. // 递归开始
  60. findLetter(beginX,beginY,'');
  61. // 递归函数
  62. functionfindLetter(x,y,str){
  63. // 找到结果文本,则递归结束
  64. if(result){
  65. return;
  66. }
  67. // 递归次数自增1
  68. ++count;
  69. // console.log(str);
  70. // 队列,用于储存可能匹配的字符
  71. varqueue=[];
  72. // 循环匹配字符库字型数据
  73. for(varletter in letterData){
  74. // 获取当前字符宽度
  75. varwidth=letterData[letter].width;
  76. // 获取该矩形区域下的灰度化0-1数据
  77. vardata=getBinary(context.getImageData(x,y,width,22).data);
  78. // 当前字符灰度化数据与当前矩形区域下灰度化数据的偏差量
  79. vardeviation=0;
  80. // 一个临时变量以确定是否到了行末
  81. varisEmpty=true;
  82. // 如果当前矩形区域已经超出图片宽度,则进行下一个字符匹配
  83. if(x+width>440){
  84. continue;
  85. }
  86. // 计算偏差
  87. for(vari=0,l=data.length;i<l;++i){
  88. // 如果发现存在的有效像素点,则确定未到行末
  89. if(isEmpty&&data[i]){
  90. isEmpty=false;
  91. }
  92. // 不匹配的像素点,偏差量自增1
  93. if(data[i]!=letterData[letter].data[i]){
  94. ++deviation;
  95. }
  96. }
  97. // 由于调试时是在猎豹浏览器下进行的,而不同浏览器下的绘图API表现略有不同
  98. // 考虑到用Chrome的读者应该也不少,故简单地针对Chrome对偏差进行一点手动微调
  99. // (好吧,我承认我是懒得重新调整getBinary方法的灰度化、0-1化公式=_=||)
  100. // 下面这段if分支在猎豹浏览器下可以删除
  101. if(letter=='F'||letter=='E'){
  102. deviation-=6;
  103. }
  104. // 如果匹配完所有17行数据,则递归结束
  105. if(y>beginY+lineHeight*17){
  106. result=str;
  107. break;
  108. }
  109. // 如果已经到了行末,重置匹配坐标
  110. if(isEmpty){
  111. x=beginX;
  112. y+=lineHeight;
  113. str+='\n';
  114. }
  115. // 如果偏差量与宽度的比值小于3,则纳入匹配队列中
  116. // 这里也是算法中的关键点,怎样的偏差量可以纳入匹配队列中
  117. // 刚开始是直接用绝对偏差量判断,当偏差量小于某个值的时候则匹配成功,但调试过程中发现不妥之处
  118. // 字符字型较小的绝对偏差量自然也小,这样l,i等较小的字型特别容易匹配成功
  119. // 因此使用偏差量与字型宽度的比值作为判断依据较为合理
  120. // 而这个判断值3的确定也是难点之一,大了递归的复杂度会大为增长,小了很可能将正确的字符漏掉
  121. if(deviation/width<3){
  122. queue.push({
  123. letter:letter,
  124. width:width,
  125. deviation:deviation
  126. });
  127. }
  128. }
  129. // 如果匹配队列不为空
  130. if(queue.length){
  131. // 对队列进行排序,同样是根据偏差量与字符宽度的比例
  132. queue.sort(compare);
  133. // console.log(queue);
  134. // 从队头开始进行下一个字符的匹配
  135. for(vari=0;i<queue.length&&!result;++i){
  136. varitem=queue[i];
  137. // 下一步递归
  138. findLetter(x+item.width,y,str+item.letter);
  139. }
  140. }else{
  141. returnfalse;
  142. }
  143. }
  144. // 递归结束
  145. // 两个匹配到的字符的比较方法,用于排序
  146. functioncompare(letter1,letter2){
  147. returnletter1.deviation/letter1.width-letter2.deviation/letter2.width;
  148. }
  149. // 图像数据的灰度化及0-1化
  150. functiongetBinary(data){
  151. varbinaryData=[];
  152. for(vari=0,l=data.length;i<l;i+=4){
  153. // 尝试过三种方式
  154. // 一种是正常的灰度化公式,无论系数如何调整都无法与绘制的文字字型数据很好地匹配
  155. // binaryData[i / 4] = (data[i] * 0.3 + data[i + 1] * 0.59 + data[i + 2] * 0.11) < 90;
  156. // 一种是自己是通过自己手动调整系数,结果虽然接近但总是不尽人意
  157. // binaryData[i / 4] = data[i] < 250 && data[i + 1] < 203 && data[i + 2] < 203;
  158. // 最后使用了平均值,结果比较理想
  159. binaryData[i/4]=(data[i]+data[i+1]+data[i+2])/3<200;
  160. }
  161. returnbinaryData;
  162. }
  163. console.log(result);
  164. // 输出耗时
  165. console.log(count,(newDate().getTime()-beginTime)/1000+' s');
  166. // 将文字绘制到图片对应位置上,以方便查看提取是否正确
  167. context.drawImage(this,this.width,0);
  168. vartextArray=result.split('\n');
  169. for(vari=0;i<textArray.length;++i){
  170. context.fillText(textArray[i],this.width+beginX,beginY+lineHeight*i);
  171. }
  172. }
  173. </script>
  174. </body>
  175. </html>


运行环境

Win7 64位,i3-3220 CPU 3.30 GHz,8G内存


运行结果


  1. yv66vgAAADIAHQoABgAPCQAQABEIABIKABMAFAcAF
  2. QcAFgEABjxpbml0PgEAAygpVgEABENvZGUB
  3. AA9MaW5lTnVtYmVyVGFibGUBAARtYWluAQAWKFtMa
  4. mF2YS9sYW5nL1N0cmluZzspVgEAClNvdXJj
  5. ZUZpbGUBAAlNYWluLmphdmEMAAcACAcAFwwAGAA
  6. ZAQBv5paw5rWq6Laz55CD6Zif5a6e5Yqb6LaF
  7. 576k77yM6Zi15a656LGq5Y2O44CC5LmF5Luw5aSn5ZC
  8. N77yM5ZGo5pel5LiA5oiY77yM6L+Y5pyb
  9. 5LiN6YGX5L2Z5Yqb77yM5LiN5ZCd6LWQ5pWZ44CCBw
  10. AaDAAbABwBAARNYWluAQAQamF2YS9sYW5n
  11. L09iamVjdAEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdX
  12. QBABVMamF2YS9pby9QcmludFN0cmVhbTsB
  13. ABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgE
  14. AFShMamF2YS9sYW5nL1N0cmluZzspVgAh
  15. AAUABgAAAAAAAgABAAcACAABAAkAAAAdAAEAAQA
  16. AAAUqtwABsQAAAAEACgAAAAYAAQAAAAEACQAL
  17. AAwAAQAJAAAAJQACAAEAAAAJsgACEgO2AASxAAAA
  18. AQAKAAAACgACAAAAAwAIAAQAAQANAAAAAgAO
  19. 7151.984s(猎豹)
  20. 77215.52sChrome

(递归次数谷歌只比猎豹多几十,耗时却对了十几秒,看来猎豹真的比Chrome快?)


非递归实现


其实非递归实现只是递归实现前做的一点小尝试,只在猎豹下调试完成,因为不舍得删,所以顺便贴出来了,使用Chrome的各位就不要跑了(我真的不是在给猎豹做广告= =||)。

  1. <!doctype html>
  2. <htmllang="zh-CN">
  3. <head>
  4. <metacharset="UTF-8">
  5. <title>文字识别</title>
  6. </head>
  7. <body>
  8. <canvasid="canvas"width="880"height="1500"></canvas>
  9. <scripttype="text/javascript">
  10. varimage=newImage();
  11. image.onload=recognition;
  12. image.src='image.jpg';
  13. functionrecognition(){
  14. // 开始时间,用于计算耗时
  15. varbeginTime=newDate().getTime();
  16. // 获取画布
  17. varcanvas=document.getElementById('canvas');
  18. // 字符库
  19. varletters='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+';
  20. // 字型数据
  21. varletterData={};
  22. // 获取context
  23. varcontext=canvas.getContext('2d');
  24. // 设置字体、字号
  25. context.font='16px 微软雅黑';
  26. // 设置文字绘制基线为文字顶端
  27. context.textBaseline='top';
  28. // 一个循环获取字符库对应的字型数据
  29. for(vari=0;i<letters.length;++i){
  30. varletter=letters[i];
  31. // 获取字符绘制宽度
  32. varwidth=context.measureText(letter).width;
  33. // 绘制白色背景,与图片背景对应
  34. context.fillStyle='#fff';
  35. context.fillRect(0,0,width,22);
  36. // 绘制文字,以获取字型数据
  37. context.fillStyle='#000';
  38. context.fillText(letter,0,0);
  39. // 缓存字型灰度化0-1数据
  40. letterData[letter]={
  41. width:width,
  42. data:getBinary(context.getImageData(0,0,width,22).data)
  43. }
  44. // 清空该区域以获取下个字符字型数据
  45. context.clearRect(0,0,width,22);
  46. }
  47. // console.log(letterData);
  48. // 绘制图片
  49. context.drawImage(this,0,0);
  50. // 要识别的文字开始坐标
  51. varx=beginX=8;
  52. vary=beginY=161;
  53. // 行高
  54. varlineHeight=24;
  55. // 结果文本
  56. varresult='';
  57. // 非递归开始
  58. varcount=0;
  59. while(y<=569&&++count<1000){
  60. // 当前最匹配的字符
  61. vartrueLetter={letter:null,width:null,deviation:100};
  62. // 循环匹配字符
  63. for(varletter in letterData){
  64. // 获取当前字符宽度
  65. varwidth=letterData[letter].width;
  66. // 获取该矩形区域下的灰度化0-1数据
  67. vardata=getBinary(context.getImageData(x,y,width,22).data);
  68. // 当前字符灰度化数据与当前矩形区域下灰度化数据的偏差量
  69. vardeviation=0;
  70. // 一个临时变量以确定是否到了行末
  71. varisEmpty=true;
  72. // 如果当前矩形区域已经超出图片宽度,则进行下一个字符匹配
  73. if(x+width>this.width){
  74. continue;
  75. }
  76. // 计算偏差
  77. for(vari=0,l=data.length;i<l;++i){
  78. // 如果发现存在的有效像素点,则确定未到行末
  79. if(isEmpty&&data[i]){
  80. isEmpty=false;
  81. }
  82. // 不匹配的像素点,偏差量自增1
  83. if(data[i]!=letterData[letter].data[i]){
  84. ++deviation;
  85. }
  86. }
  87. // 非递归无法遍历所有情况,因此针对某些字符进行一些微调(这里只针对猎豹,Chrome的没做)
  88. // 因为其实非递归实现只是在递归实现前做的一点小尝试,因为不舍得删,就顺便贴出来了
  89. if(letter=='M'){
  90. deviation-=6;
  91. }
  92. // 如果偏差量与宽度的比值小于3,则视为匹配成功
  93. if(deviation/width<3){
  94. // 将偏差量与宽度比值最小的作为当前最匹配的字符
  95. if(deviation/width<trueLetter.deviation/trueLetter.width){
  96. trueLetter.letter=letter;
  97. trueLetter.width=width;
  98. trueLetter.deviation=deviation;
  99. }
  100. }
  101. }
  102. // 如果已经到了行末,重置匹配坐标,进行下一轮匹配
  103. if(isEmpty){
  104. x=beginX;
  105. y+=lineHeight;
  106. result+='\n';
  107. continue;
  108. }
  109. // 如果匹配到的字符不为空,则加入结果字符串,否则输出匹配结果
  110. if(trueLetter.letter){
  111. result+=trueLetter.letter;
  112. // console.log(x, y, trueLetter.letter);
  113. }else{
  114. console.log(x,y,result.length);
  115. break;
  116. }
  117. // 调整坐标至下一个字符匹配位置
  118. x+=trueLetter.width;
  119. }
  120. // 非递归结束
  121. // 图像数据的灰度化及0-1化
  122. functiongetBinary(data){
  123. varbinaryData=[];
  124. for(vari=0,l=data.length;i<l;i+=4){
  125. // 尝试过三种方式
  126. // 一种是正常的灰度化公式,无论系数如何调整都无法与绘制的文字字型数据很好地匹配
  127. // binaryData[i / 4] = (data[i] * 0.3 + data[i + 1] * 0.59 + data[i + 2] * 0.11) < 90;
  128. // 一种是自己是通过自己手动调整系数,结果虽然接近但总是不尽人意
  129. // binaryData[i / 4] = data[i] < 250 && data[i + 1] < 203 && data[i + 2] < 203;
  130. // 最后使用了平均值,结果比较理想
  131. binaryData[i/4]=(data[i]+data[i+1]+data[i+2])/3<200;
  132. }
  133. returnbinaryData;
  134. }
  135. console.log(result);
  136. // 输出耗时
  137. console.log(count,(newDate().getTime()-beginTime)/1000+' s');
  138. // 将文字绘制到图片对应位置上,以方便查看提取是否正确
  139. context.drawImage(this,this.width,0);
  140. vartextArray=result.split('\n');
  141. for(vari=0;i<textArray.length;++i){
  142. context.fillText(textArray[i],this.width+beginX,beginY+lineHeight*i);
  143. }
  144. }
  145. </script>
  146. </body>
  147. </html>


运行结果


  1. yv66vgAAADIAHQoABgAPCQAQABEIABIKABMAFAcAF
  2. QcAFgEABjxpbml0PgEAAygpVgEABENvZGUB
  3. AA9MaW5lTnVtYmVyVGFibGUBAARtYWluAQAWKFtMa
  4. mF2YS9sYW5nL1N0cmluZzspVgEAClNvdXJj
  5. ZUZpbGUBAAlNYWluLmphdmEMAAcACAcAFwwAGAA
  6. ZAQBv5paw5rWq6Laz55CD6Zif5a6e5Yqb6LaF
  7. 576k77yM6Zi15a656LGq5Y2O44CC5LmF5Luw5aSn5ZC
  8. N77yM5ZGo5pel5LiA5oiY77yM6L+Y5pyb
  9. 5LiN6YGX5L2Z5Yqb77yM5LiN5ZCd6LWQ5pWZ44CCBw
  10. AaDAAbABwBAARNYWluAQAQamF2YS9sYW5n
  11. L09iamVjdAEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdX
  12. QBABVMamF2YS9pby9QcmludFN0cmVhbTsB
  13. ABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgE
  14. AFShMamF2YS9sYW5nL1N0cmluZzspVgAh
  15. AAUABgAAAAAAAgABAAcACAABAAkAAAAdAAEAAQA
  16. AAAUqtwABsQAAAAEACgAAAAYAAQAAAAEACQAL
  17. AAwAAQAJAAAAJQACAAEAAAAJsgACEgO2AASxAAAA
  18. AQAKAAAACgACAAAAAwAIAAQAAQANAAAAAgAO
  19. 7021.931s(猎豹)

真正的结果


找了个在线的Base64解码工具将上面的提取结果进行了一下解码,发现是一个Java编译后的.class文件,大概内容是:“新浪足球队实力超群,阵容豪华。久仰大名,周日一战,还望不遗余力,不吝赐教。”


写在最后


这个只是一个最浅层次的文字识别提取算法,不够通用,性能也一般,权当兴趣研究之用,不过我想,勇于实践、敢于尝试的精神才是最重要的。。

因为最近实习工作略忙,再加上学校开学事情也多,拖了两个星期才把这边文章写出来,除此之外还有不少计划都落下了,还得继续努力啊>_<

还有最近的一些思考的结果和感触也要找个时间写下来。

PS:写这篇博客的时候精神略差,之后有想到什么再作补充吧,如果写的不好还请多多指教!




=======================签 名 档=======================

原文地址(我的博客):http://www.clanfei.com/2013/09/1723.html
欢迎访问交流,至于我为什么要多弄一个博客,因为我热爱前端,热爱网页,我更希望有一个更加自由、真正属于我自己的小站,或许并不是那么有名气,但至少能够让我为了它而加倍努力。。
=======================签 名 档=======================




分享到:
评论
1 楼 再_见孙悟空 2014-02-03  
能识别中文不?

相关推荐

Global site tag (gtag.js) - Google Analytics