2024-11-30 15:58:06 | 来源: 互联网整理
先说一下发展背景。今年搬家找房(2020年应该叫去年),我发现每天都要给各种租房广告打很多电话。 (当然,我在网上和亲自搜索过)。每次基本上都是看墙上的电话号码然后拨打,拨打的次数多了就变得很麻烦。如果没看清楚的话,很容易输入错误的数字。
图片来自网络
当时我就想,现在OCR技术这么流行,我们为什么不能做一个程序来解决这个问题呢。由于有些出租电话号码仍然是手写的,所以手写识别的问题也必须解决。同时,很多租房信息实际上是来自中介或其他骗局。所以有些并不是我们需要的。为什么这些信息不能在平台上共享,就像在手机上识别和提交欺诈电话一样。然后我也搜索了微信小程序里是否有类似的小程序,发现基本没有。一些OCR相关程序使用起来比较麻烦,而且大多需要付费或者会员激活。
所以我想自己开发一个相关的小程序会更好。功能很简单。您可以通过相机拍照并识别来获取电话号码;您可以直接拨打电话拨打该号码。同时允许将识别出的号码作为标记提交,以标记其为欺诈、中介等。当号码被标记后,下次有人识别该号码时,将显示相应的标记信息。
这次的核心问题可能是手写识别,普通印刷文字识别。它已经变得非常普遍和成熟。我们主要识别数字,所以我们只关注数字手写识别。近年来,BAT等第三方实际上也提供了类似的服务接口。我也研究了一下,发现连接很简单。识别率还不错,达到70%左右。毕竟每个人的笔迹都不一样。不过第三方服务最终还是要收费的,当然也有一定的免费额度。开发这个软件我并不打算收取任何费用,所以暂时放弃使用第三方软件。
MNIST 机器学习
基于机器学习的手写识别目前很流行。常用的机器学习框架有很多,比如TensorFlow。在手写数字识别中,MNIST数据集是最常用的。它甚至非常流行,以至于通用机器学习框架都使用MNIST 作为入门教程。这里我就不详细介绍MNIST了。有兴趣的可以上网了解一下。
然后就在网上找了Python机器学习识别手写数字之类的,研究了很久。最后,我觉得使用MNIST数据集进行识别更多的是识别算法的比较。因为MNIST本身包含样本和测试数据集。利用机器学习对样本进行学习并生成模型数据,然后根据模型数据读取样本数据进行比较,检查识别成功率。而网上的案例基本类似,就是基于数据集学习识别准确率。
我还发现.NET Core 使用ML.NET 基于MNIST 数据集进行手写数字识别。运行结果如下图所示。原理和过程与Python处理相同,包括输出结果。
MNIST数据集是一张28*28像素的黑白图片,一个字符占一张图片。我们平时进行OCR识别时,都是用一张图片识别所有的字符(数字)。所以如果我们使用MNIST,我们必须精确分割图像文本(数字),然后将图像二值化并保存为20*28像素。许多手写数字可能会序列在一起,因此在此基础上对其进行分段仍然很困难。
所以这里暂时放弃使用,以后继续研究使用MNIST进行识别。
超立方体
Tesseract是一款比较知名的开源OCR识别软件,早期由HP实验室开发,现在由Google开发和维护。支持的平台包括Windows、linux 和macos。支持数十种常用语言识别;您也可以自己训练字库。如果使用手写识别,则需要自己训练字库进行识别。
具体我就不过多阐述了,有兴趣的可以自行了解。在本次开发中我选择了Tesseract进行识别。
GitHub: https://github.com/tesseract-ocr/tesseract
前端使用微信小程序。该小程序无需安装,完全跨平台。我不是专业的前端开发人员,所以不会过多介绍。我只介绍如何在小程序中使用相机拍照和裁剪指定区域。
1. 拍照
拍照的相机接口不需要很大,因为它只识别一个数字。如果单纯缩小相机的尺寸,就会显得很难看。所以有点像微信扫描,需要的部分全亮,其他部分半透明。
如下图
一开始我就想小程序的相机会不会提供类似的功能,让我直接通过设置就可以实现这样的效果。但遗憾的是我没能找到。在相机上添加文字和图片就可以了。其实这部分透明度一般分为半透明,是一张图片。整个背景就是一张图片,我想了很久,然后用PS把图片做成透明半透明的。
1 个摄像头设备- 位置='宽度' 闪光灯='关闭' 样式='height:{{height}}px;' 2 cover-view class='camerabgImage-view' 3 cover-image class='bgImage' src='./images/bg2.png' /cover-image 4 cover-view class='cameraTips'请扫描手机号/cover-view 5 cover-view class='cameraTips2'*支持打印和手写数字识别/cover-view 6 cover-view class='cameraBgView' 7 cover-image class='cancelphoto' src='./images/cancelPhoto2.png'bindtap='cancelPhoto'/cover-image 8 cover-view class='cameraButton-view'9 cover-image class='takephoto'src='./images/takephoto.png'bindtap='takePhoto' /cover-image10 /cover-view11 /cover-view12 /cover-view13 /cameraDetails 代码请见文末github地址。
2. 切割
摄像头接口已经实现了,下一步就是实现摄像头功能了。相机代码很简单,我就不做过多阐述了。因为我们要拍摄完全透明区域的图像。也就是照片的指定位置。在照片API中,微信并没有提供类似的获取指定区域图片的功能,所以要实现这个功能,我们需要自己裁剪一张完整的图片信息。
如何捕捉这部分图像?我目前的做法是根据帧的位置,即对应的X,Y找出整个图像中的位置。因为我们的整个帧本来就是背景图像的一部分,所以它没有实际的坐标位置在代码中。我们需要显示裁剪后的图像,因此我们需要一个画布来在另一个页面上显示裁剪后的图像。
在下面的代码中,可以看到我使用了延迟和错误重试机制,因为在实际的真机测试中,canvasToTempFilePath方法偶尔还是会报错。原因是在调用canvasToTempFilePath之前,需要绘制一个矩形框来显示我们截取的图像。但是canvas.draw()方法是异步的,这会导致下面的canvasToTempFilePath方法在绘制完成之前就报错。
1 canvasToTempFile: function() { 2 var that=this; 3 setTimeout(function() { 4 wx.canvasToTempFilePath({ //裁剪对参数5 canvasId: 'image-canvas', 6 x: that.data.image_x, //画布x轴起始点7 y: that.data.image_y, //画布y轴起点8 width: that.data.width, //画布宽度9 height: that.data.image_height, //画布高度10 destWidth: that.data.width , //输出图像宽度11 destHeight: that.data .image_height, //输出图像高度12 canvasId: 'image-canvas',13 success: function(res) {14 that.filePath=res.tempFilePath;15 //清除画布上矩形区域内的内容16 that.canvas。clearRect(0, 0, that.data.width, that.data.height);17 that.canvas.drawImage(that.filePath, that.data.image_x, that.data.image_y, that.data.width - 20 , that.data.image_height);18 that.canvas.draw();19 wx.hideLoading();20 //开始请求识别接口21 that.startDiscern(res.tempFilePath );22 //开始获取标记类型23 that.getMarkType();24 },25 failure: function(e) {26 //console.log('错误:' + e);27 wx.hideLoading()28 wx .showToast({29 title: '请稍候.',30 icon: 'loading'31 })32 //出错后继续执行。 33 that.canvasToTempFile();34 }35 });36 }, 1000);37 }详细代码请见文末github地址。
首先,为了提高识别效果,Tesseract允许我们训练自己的字体。也就是当Tesseract识别不正确时,我们手动修正。或者当完全无法识别的时候,我们可以自己标记需要识别的文本坐标和正确的结果值。
使用的工具是jTessBoxEditor,使用前需要安装Java。
1.将图片转换为tif格式
训练模板必须是TIFF格式,因此第一步是将其转换为TIFF格式文件。同时,可以将多张图片合并为一种TIFF 文件格式。这样您就可以将多张图像保存在一个文件中。你可以在jTessBoxEditor中的Tools-Merge中进行,只需选择多张图片即可。不过,保存为TIFF 文件时必须注意格式。
[语言].[字体名称].exp[数字].tif
lang为语言名称(即训练后的语言名称),
fontname 是字体名称,
num 是序列号,可以自定义。
2.生成BOX文件
下一步是生成BOX 文件。这一步其实就是利用Tesseract来做一个基础的识别。识别后会生成一个box文件。该文件保存了识别结果以及结果对应的坐标信息。
此步骤必须安装在电脑上,否则无法执行。
生成BOX文件命令(注意一定要在刚刚生成tif文件的目录下)
3.jTessBoxEditor纠错并训练
打开jTessBoxEditor,选择Box Editor-Open,打开刚刚生成的tif文件所在目录。该目录必须包含上一步中生成的box 文件。选择打开的是tif图像文件,而不是box文件。
打开后,您将看到初步的识别结果。如果不正确,您可以自行修改。一是修改坐标,即蓝框对应的矩形位置,二是修改识别到的字符。
修改修正完成后,按Ctrl+S或者上面的保存按钮保存。
4. 创建字体文件
创建一个名为font_properties 的文件并将其放在同一目录中。请注意,没有扩展名。
内容为:字体0 0 0 0 0
如下图
每个0对应不同的字体。
他们是:
斜体、粗体、默认字体、衬线字体、德文黑色字体
0 表示否,1 表示是
5.执行批处理
1 echo 运行Tesseract 进行训练. 2 tesseract.exe num.font.exp0.tif num.font.exp0 nobatch box.train 3 4 echo 计算字符集. 5 unicharset_extractor.exe num.font.exp0. box 6 mftraining -F font_properties -U unicharset - 7 O num.unicharset num.font.exp0.tr 8 9 10 echo 聚类. 11 cntraining.exe num.font.exp0.tr 12 13 echo 重命名文件. 14 重命名normproto num.normproto 15 重命名inttemp num.inttemp 16 重命名pffmtable num.pffmtable 17 重命名shapetable num.shapetable 18 19 echo 创建Tessdata. 20 merge_tessdata.exe num. 21 22 echo.pause 最后执行这个批处理,会生成很多文件,如下图。我们只需要把目录下的num.traineddata文件复制到项目的tessdata目录下即可。
后端主要使用.NET Core编写webapi。小程序将捕获和截获的图像转换为base64格式并传递给后端。后端通过调用Tesserac进行识别。前面我提到,识别出来的号码可以标记为提交、获取等,因此目前后端使用MongoDB来存储和读取相关数据。
常用接口和MongoDB的操作都很简单,这里不再介绍。主要讲一下Tesseract识别的实现。
安装Tesseract 依赖项
Tesseract 的最新版本是4.1.0。
Nuget 中的最新版本是Genesis.Tesseract4。
所以下载的时候要注意不要下载错了。
Tesseract 主要通过两种方式提高识别率。第一个是训练更多相关字体,第二个是图像处理。原始图像是最重要的。在我们的例子中,是用户拍摄照片。最好图片清晰可见,并且文本后面没有其他干扰。
在图像识别领域,最常用的图像处理方法有灰度化、二值化、图像校正等。
灰度
在RGB模型中,如果R=G=B,则该颜色代表灰度颜色,R=G=B的值称为灰度值。简单来说,就是将图像处理成黑白图像。
1 ///摘要2 ///图像灰度3 ////summary 4 ///param name='bmp'/param 5 ///returns/returns 6 public static Bitmap ToGray(Bitmap bmp) 7 { 8 for (int i=0; i bmp.Width; i++) 9 { 10 for (int j=0; j bmp.Height; j++) 11 { 12 //获取该点像素的RGB颜色13 Color color=bmp.GetPixel(i, j); 14 //利用公式计算灰度值15 int grey=(int)(color.R * 0.3 + color.G * 0.59 + color.B * 0.11); 16 颜色newColor=Color.FromArgb(灰色, 灰色, 灰色); 17 bmp.SetPixel(i, j, newColor); 18 } 19 } 20 返回bmp; 21 }详细代码请见文末github地址。
二值化
二值化就是将大于某个值的像素修改为255,将小于该值的像素修改为0。
也就是说,0和1实际上是灰度图像的0~255的简化版本。 0代表白色,1代表黑色。
二值化最重要的是阈值的选择,一般分为固定阈值和自适应阈值。比较常用的二值化方法有:双峰法、P参数法、迭代法和OTSU法。
C#代码如下:
1 ///摘要2 ///图像二值化(迭代方法) 3 ////summary 4 ///param name='bmp'/param 5 ///returns/returns 6 public static Bitmap ToBinaryImage(Bitmap bmp ) 7 { 8 int[] 直方图=new int[256]; 9 int minGrayValue=255,maxGrayValue=0; 10 //获取直方图11 for (int i=0; i bmp.Width; i++) 12 { 13 for (int j=0; j bmp.Height; j++) 14 { 15 Color PixelColor=bmp.GetPixel(i, j); 16 直方图[pixelColor.R]++; 17 if (pixelColor.R maxGrayValue) maxGrayValue=PixelColor.R; 18 if (pixelColor.R minGrayValue) minGrayValue=PixelColor.R; 19 } 20 } 21 //迭代计算阈值22 int Threshold=-1; 23 int 新阈值=(minGrayValue + maxGrayValue)/2; 24 for ( int iterationTimes=0; 阈值!=newThreshold iterationTimes 100; iterationTime 25 { 26 阈值=newThreshold; 27 int lP1=0; 28 int lP2=0; 29 int lS1=0; 30 int lS2=0; 31 //求两个区域的平均灰度值32 for (int i=minGrayValue; i Threshold; i++) 33 { 34 lP1 +=histogram[i] * i; 35 lS1 +=histogram[i] } 37 int Mean1GrayValue=(lP1/lS1); 38 for (int i=阈值+ 1; i maxGrayValue; i++) 39 { 40 lP2 +=histogram[i] * i; 41 lS2 +=histogram[i] } 43 int Mean2GrayValue=( lP2/lS2); 44 newThreshold=(mean1GrayValue + Mean2GrayValue)/45 } 46 47 //计算二值化48 for (int i=0; i bmp.Width; i++) 49 { 50 for (int j=0; j bmp.Height; j++) 51 { 52 颜色PixelColor=bmp.GetPixel(i, j); 53 if (pixelColor.R 阈值) bmp.SetPixel(i, j, Color.FromArgb(255, 255, 255)); else bmp.SetPixel(i, j, Color.FromArgb(0, 0, 0)); 55 } 56 } 57 return bmp; 58 }详细代码请见文末github地址。
下图是我这段时间写的一个工具,用来测试图像处理效果和对比。
灰度
二值化
完整的图像处理代码https://github.com/cfan1236/ImageManipulation
灰度化和二值化都是为了加强和突出要识别的区域,例如文本。削弱背景和其他干扰项。在最后的测试实验中发现,并不是所有的原始图像经过一系列的图像处理后都能得到更好的结果。有些图像无需任何处理即可获得更好的效果。
所以在实际处理时,我使用三个线程来同时处理和识别三种情况。第一个使用原始图像识别,第二个使用灰度识别,第三个使用二值化识别。最后选择识别结果最多的那个。
代码如下:
1 ///摘要2 ///数字识别3 ////摘要4 ///
param name="base64_image"></param> 5 /// <param name="image_url"></param> 6 /// <returns></returns> 7 public PhoneDiscernResult DiscernNumber(string base64_image, string image_url) 8 { 9 PhoneDiscernResult result = new PhoneDiscernResult(); 10 string imageFile = GetImageFileName(); 11 if (!string.IsNullOrEmpty(image_url)) 12 { 13 Utils.DownLoadWebImage(image_url, imageFile); 14 } 15 else 16 { 17 Utils.SaveBase64Image(base64_image, imageFile); 18 } 19 if (File.Exists(imageFile)) 20 { 21 string[] taskResult = new string[3]; 22 // 三个线程同时去处理执行 23 // 每个线程处理的图片都不一样 取结果最好的一个 24 Task[] tk = new Task[] { 25 Task.Factory.StartNew(()=> 26 { 27 // 原图识别 28 taskResult[0]=Discern(imageFile); 29 }), 30 Task.Factory.StartNew(()=> 31 { 32 // 灰度处理后识别 33 taskResult[1]=GrayDiscern(imageFile); 34 }), 35 Task.Factory.StartNew(()=> 36 { 37 // 二值化处理后识别 38 taskResult[2]=BinaryzationDiscern(imageFile); 39 }), 40 }; 41 // 超时1分钟 42 int timeout = (1000 * 60) * 1; 43 Task.WaitAll(tk, timeout); 44 var number_str = taskResult[0]; 45 if (taskResult[1].Length > number_str.Length) 46 { 47 number_str = taskResult[1]; 48 } 49 if (taskResult[2].Length > number_str.Length) 50 { 51 number_str = taskResult[2]; 52 } 53 result.text = number_str; 54 if (number_str.Length == 11) 55 { 56 result.message = "识别成功"; 57 } 58 else 59 { 60 result.message = "当前识别的电话可能有误,请注意辨别"; 61 } 62 63 } 64 return result; 65 } 66 67 68 /// <summary> 69 /// 直接识别 70 /// </summary> 71 /// <param name="filePath"></param> 72 /// <returns></returns> 73 private string Discern(string imageFile) 74 { 75 string number_str = ""; 76 // 这里可以选择不同的语言包 可以是自己训练的 可以是Tesseract 训练好的语言包 77 TesseractEngine te_ocr = new TesseractEngine(@"tessdata", "chi_sim", EngineMode.TesseractAndLstm); 78 var img = Pix.LoadFromFile(imageFile); 79 var page = te_ocr.Process(img, PageSegMode.Auto); 80 string text = page.GetText().Trim().Replace("\r", "").Replace("\n", ""); 81 _logger.Info("识别的原始数据:"+text); 82 page.Dispose(); 83 // 只提取数字 84 number_str = System.Text.RegularExpressions.Regex.Replace(text, @"[^0-9]+", ""); 85 _logger.Info("只提取数字结果:" + number_str); 86 return number_str; 87 } 88 89 /// <summary> 90 /// 灰度识别 91 /// </summary> 92 /// <param name="imageFile"></param> 93 /// <returns></returns> 94 private string GrayDiscern(string imageFile) 95 { 96 string number_str = ""; 97 using (Bitmap bmp = new Bitmap(imageFile)) 98 { 99 // 灰度处理100 var bmps = Utils.ToGray(bmp);101 var tempFile = GetImageFileName(1);102 bmps.Save(tempFile);103 number_str = Discern(tempFile);104 File.Delete(tempFile);105 }106 return number_str;107 }108 /// <summary>109 /// 二值化识别110 /// </summary>111 /// <param name="imageFile"></param>112 /// <returns></returns>113 private string BinaryzationDiscern(string imageFile)114 {115 string number_str = "";116 using (Bitmap bmp = new Bitmap(imageFile))117 {118 // 灰度处理119 var bmps = Utils.ToGray(bmp);120 // 处理自动校正121 gmseDeskew sk = new gmseDeskew(bmps);122 double skewangle = sk.GetSkewAngle();123 Bitmap bmpOut = Utils.RotateImage(bmps, -skewangle);124 var tempFile = GetImageFileName(1);125 // 将二值化后的图像保存下 126 Utils.ToBinaryImage(bmpOut).Save(tempFile);127 number_str = Discern(tempFile);128 File.Delete(tempFile);129 }130 return number_str;131 }具体详细代码,请看文章结尾github地址。
用户评论
这也太酷了!我一直想找个工具识别图上的文字。
有15位网友表示赞同!
我经常会遇到照片里写着奇怪字面的情况,现在终于有解决方法了!
有13位网友表示赞同!
这个小程序功能强大,能识别的文字种类很多啊!
有13位网友表示赞同!
以前要识别图片上的文字还得去电脑下载软件,现在直接用手机就行。
有12位网友表示赞同!
这功能太棒了,以后扫描文档和文件就不用再打字了。
有19位网友表示赞同!
界面设计简单易用,操作起来也毫不费力。
有18位网友表示赞同!
希望未来能支持更多语言识别呢!
有9位网友表示赞同!
这款APP真是我的省时神器,简直太实用啦!
有16位网友表示赞同!
以前识别图片上的文字都是找人工,现在自己就能搞定,效率高多了!
有20位网友表示赞同!
速度还挺快,识别出来的文字也很准确。
有9位网友表示赞同!
学习一下Tesseract的原理吧,以后开发自己的项目可以用到。
有20位网友表示赞同!
这个小程序有无限可能啊,也许还能用到其他领域呢!
有18位网友表示赞同!
分享给所有需要识别图片上文字的朋友们!
有11位网友表示赞同!
非常期待未来会有更多新功能加入!
有6位网友表示赞同!
使用体验很棒,好评!
有18位网友表示赞同!
太适合学习OCR技术的入门者了!
有20位网友表示赞同!