2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > Android OCR文字识别 实时扫描手机号(极速扫描单行文本方案)

Android OCR文字识别 实时扫描手机号(极速扫描单行文本方案)

时间:2022-03-09 22:30:31

相关推荐

Android OCR文字识别 实时扫描手机号(极速扫描单行文本方案)

身份证识别:/wenchaosong/OCR_identify

遇到一个需求,要用手机扫描纸质面单,获取面单上的手机号,最后决定用tesseract这个开源OCR库,移植到Android平台是tess-two

Android平台tess-two地址:/tesseract-ocr

本文Demo地址:/mr_sk/article/details/79077271

评论里有人想要我训练的数字字库,这里贴出来(只训练了 黑体、微软雅黑、宋体 0-9的数字,其他字体识别率会降低)

数字字库地址:/download/mr_sk/10186145(现在上传资源好像不能免费下载了,至少要收两个积分….)

这篇博客主要是记录我的思路,大多是散乱的笔记,所以大家遇到报错什么的不要急,看看Log总能找到问题,接下来我也准备写一个library,直接封装好 手机号扫描、身份证扫描、邮箱扫描等,写好后我会更新

我遇到的坑(只想了解用法的可以跳过)

Tesseract虽然是个很强大的库,但直接使用的话,并不适用于连续识别的需求,因为tess-two对解析图像的清晰度文字规范度有很高的要求,用相机随便获取的一张预览图扫出来错误率非常高(如果用电脑截图文字区域,识别很高),手写的就更不用说了,几乎全是乱码,而且识别速度很慢,一张200*300的图片都要好几秒

所以在没有优化的情况下,直接用tess-two 来作文字识别,只能是拍一张照,然后等待识别结果,比如识别文章、扫描身份证等,如果像我的需求,需要识别面单上的手机号,可能一分钟需要扫描几十个手机号,那就必须要达到毫秒级的解析速度,直接使用常规的方法肯定是不行的,那怎么办呢?

tess-two的识别算法当然是没办法处理了,那就得从其他方面去想办法

第一个:是在字库方面,官方的一个英文字库30M,但是你面临的需求需要这么重量级的字库吗?比如我扫描手机号的功能,面单上都是黑体字,手机号只有纯数字, 就这么点识别范围去检索一个30M的字库,显然多了很多无用功

解决办法就是:

训练自己的字库,如果你需要毫秒级的扫描速度,那你的需求涉及的扫描内容 范围一定很小(前面说过,如果你要做文章识别之类的,那就用官方字库,拍一张照片,等几秒钟,完全是可以接受的),这样就可以根据需求范围内 常见的 ”字体“ 和 ”字符“来训练专门的字库,这样你就能使用一个轻量级的定制字库,极大的减少了解析时间,比如我手机号的数字子库,只有100KB,识别我处理后的图片,从官方字库的1.5-3秒,减少到了300-500ms

字库训练 详情参考/cnlian/p/5765871.html

第二个:就是在把图片交给tess-two解析之前,先进行简单的内容过滤,如上面所说的,即便是我把一张图片的解析速度压缩到了300-500ms,依然存在一个问题,那就是识别频率,要做连续扫描,相机肯定是一直开着的,那一秒钟几十帧的图片,你该解析哪一张呢?

每一张都解析的话,对性能是很大的消耗,也要考虑一些用低端机的用户,而且每次解析的时间不等,识别结果也很混乱,那就只有每次取一帧解析,拿到解析结果后,再去解析下一帧

那么问题又来了:相机一秒几十帧,一打开相机,第一帧就开始解析了,这样下一次开始解析就在300-500ms之后了,如果用户在对准手机号的前一刻,正好开始了一帧画面的解析,那等到开始解析手机号,至少也在几百毫秒以后了,加上手机号本身的解析时间,从对准到拿到结果,随随便便就超过了1秒,加上每次识别速度不定,可能特殊情况耗时更久,这样必然会感到很明显的延迟,那该怎么处理呢?

解决办法就是:

在图片交给tess-two之前,先进行图片二级裁切,第一次裁切就是利用界面的扫描框,拿到需要扫描的区域,然后进行内容过滤,把明显不可能包含手机号的图像直接忽略,不进行解析,这个过程需要遍历图片的像素,用jni处理时间不超过10ms,即便是用java处理,也只有10-50ms,只要能忽略大部分的无用的图像,那就解决了这个延迟的问题,并且在过滤的同时,如果被判断为有用图片,那就能同时拿到需要解析的文字块,然后进行第二次裁切,拿到更小的图片,进一步提升解析速度

至于过滤的方式,我写了针对手机号的过滤,在文章最下面的单行文本优化方案部分,有相似需求的可以看看,然后针对自己的需求,来写过滤算法

至于最后扫描的内容的提取,可以用正则公式来筛选关键信息如:手机号、网址、邮箱、身份证、银行卡号

Demo截图

图一

图二

图三

水印清除

图四

图五

图一:是扫描线没有对准手机号码,未捕捉到手机号的状态,这种状态下,每一帧都会在10-30ms之内被确定扫描线没有对准一个手机号而被过滤掉,不交给tess-two解析,直接放弃这一帧数据

图二:是扫描线对准了手机号,经过过滤算法后,捕捉到一个包含11位字符的蚊子块,基本确认存在手机号

图三:是 图二 状态下的识别结果

图四:是被水印干扰的手机号所得到的二值化图片

图五:是清除水印后取到的手机号区域(只适用于图五这种文字底部的干扰)

tess-two基本使用

这里是基本用法,我最早写的,效率不高但代码易读,是tess-two的使用方法,识别还是有明显延迟,优化方案我放在了文章后面的优化部分,Demo也更新了最新的优化方案,如果对这方面比较熟练,可以从后面开始看,这里由简入繁

集成很简单,build.gradle中加入:

compile ‘com.rmtheis:tess-two:6.0.0’

//后面我已经换到8.0.0,上传的demo是在6.0.0下运行的

compile ‘com.rmtheis:tess-two:8.0.0’

编译一下,框架的集成就ok了,不过tess-two的文字库是需要另外下载的,我们一般只需要中文和英文两种就可以了,特殊需求可以自己训练

字体库下载地址:/tesseract-ocr/tessdata

英文:eng.traineddata

简体中文:chi_sim.traineddata

将这两个字体库文件,放到sd卡,路径必须为**/tessdata/

路径为什么一定要为**/tessdata/呢?在TessBaseApi类的初始化方法中会检查你的文字库目录,代码如下

/*** datapath是你传入的文字库路径,可以看到这里在传入的datapath后加了一个"tessdata"目录* 然后验证了这个目录是否存在,如果不在,就会报错"数据目录必须包含tessdata目录"*/File tessdata = new File(datapath + "tessdata");//tessdata是否存在且是个目录if (!tessdata.exists() || !tessdata.isDirectory()) throw new IllegalArgumentException("Data path must contain subfolder tessdata!");

12345678

然后就是使用了,这里我的字体库文件都放在 “根目录/Download/tessdata“中

解析图片代码如下:

public class OcrUtil {//字体库路径,此路径下必须包含tessdata文件夹,但不用把tessdata写上 static final String TESSBASE_PATH = Environment.getExternalStorageDirectory() + File.separator + "Download" + File.separator; //英文 static final String ENGLISH_LANGUAGE = "eng"; //简体中文 static final String CHINESE_LANGUAGE = "chi_sim"; /** * 识别英文 * * @param bmp 需要识别的图片 * @param callBack 结果回调(携带一个String 参数即可) */ public static void ScanEnglish(final Bitmap bmp, final MyCallBack callBack) { new Thread(new Runnable() { @Override public void run() { TessBaseAPI baseApi = new TessBaseAPI(); //初始化OCR的字体数据,TESSBASE_PATH为路径,ENGLISH_LANGUAGE指明要用的字体库(不用加后缀) if (baseApi.init(TESSBASE_PATH, ENGLISH_LANGUAGE)) { //设置识别模式 baseApi.setPageSegMode(TessBaseAPI.PageSegMode.PSM_AUTO); //设置要识别的图片 baseApi.setImage(bmp); //开始识别 String result = baseApi.getUTF8Text(); baseApi.clear(); baseApi.end(); callBack.response(result); } } }).start(); } }

12345678910111213141516171819222324252627282930313233343536

好了,识别工具写好了,接下要做的就是,打开相机、获取预览图、裁切出需要的区域,然后交给tess-two识别,这里我直接吧SurfaceView封装了一下,自动打开相机开始预览,下面是扫描手机号的代码:

public class CameraView extends SurfaceView implements SurfaceHolder.Callback, Camera.PreviewCallback { private final String TAG = "CameraView"; private SurfaceHolder mHolder; private Camera mCamera; private boolean isPreviewOn; //默认预览尺寸 private int imageWidth = 1920; private int imageHeight = 1080; //帧率 private int frameRate = 30; public CameraView(Context context) { super(context); init(); } public CameraView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public CameraView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mHolder = getHolder(); //设置SurfaceView 的SurfaceHolder的回调函数 mHolder.addCallback(this); mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); } @Override public void surfaceCreated(SurfaceHolder holder) { //Surface创建时开启Camera openCamera(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { //设置Camera基本参数 if (mCamera != null) initCameraParams(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { try { release(); } catch (Exception e) { } } private boolean isScanning = false; /** * Camera帧数据回调用 */ @Override public void onPreviewFrame(byte[] data, Camera camera) { //识别中不处理其他帧数据 if (!isScanning) { isScanning = true; new Thread(new Runnable() { @Override public void run() { try { //获取Camera预览尺寸 Camera.Size size = camera.getParameters().getPreviewSize(); //将帧数据转为bitmap YuvImage image = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null); if (image != null) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); //将帧数据转为图片(new Rect()是定义一个矩形提取区域,我这里是提取了整张图片,然后旋转90度后再才裁切出需要的区域,效率会较慢,实际使用的时候,照片默认横向的,可以直接计算逆向90°时,left、top的值,然后直接提取需要区域,提出来之后再压缩、旋转 速度会快一些) pressToJpeg(new Rect(0, 0, size.width, size.height), 80, stream); Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size()); //这里返回的照片默认横向的,先将图片旋转90度 bmp = rotateToDegrees(bmp, 90); //然后裁切出需要的区域,具体区域要和UI布局中配合,这里取图片正中间,宽度取图片的一半,高度这里用的适配数据,可以自定义 bmp = bitmapCrop(bmp, bmp.getWidth() / 4, bmp.getHeight() / 2 - (int) getResources().getDimension(R.dimen.x25), bmp.getWidth() / 2, (int) getResources().getDimension(R.dimen.x50)); if (bmp == null) return; //将裁切的图片显示出来(测试用,需要为CameraView setTag(ImageView)) ImageView imageView = (ImageView) getTag(); imageView.setImageBitmap(bmp); stream.close(); //开始识别 OcrUtil.ScanEnglish(bmp, new MyCallBack() { @Override public void response(String result) { //这是区域内扫除的所有内容 Log.d("scantest", "扫描结果: " + result); //检索结果中是否包含手机号 Log.d("scantest", "手机号码: " + getTelnum(result)); isScanning = false; } }); } } catch (Exception ex) { isScanning = false; } }).start(); } } /** * 获取字符串中的手机号 */ public String getTelnum(String sParam) { if (sParam.length() <= 0) return ""; Pattern pattern = pile("(1|861)(3|5|8)\\d{9}$*"); Matcher matcher = pattern.matcher(sParam); StringBuffer bf = new StringBuffer(); while (matcher.find()) { bf.append(matcher.group()).append(","); } int len = bf.length(); if (len > 0) { bf.deleteCharAt(len - 1); } return bf.toString(); } /** * Bitmap裁剪 * * @param bitmap 原图 * @param width 宽 * @param height 高 */ public static Bitmap bitmapCrop(Bitmap bitmap, int left, int top, int width, int height) { if (null == bitmap || width <= 0 || height < 0) { return null; } int widthOrg = bitmap.getWidth(); int heightOrg = bitmap.getHeight(); if (widthOrg >= width && heightOrg >= height) { try { bitmap = Bitmap.createBitmap(bitmap, left, top, width, height); } catch (Exception e) { return null; } } return bitmap; } /** * 图片旋转 * * @param tmpBitmap * @param degrees * @return */ public static Bitmap rotateToDegrees(Bitmap tmpBitmap, float degrees) { Matrix matrix = new Matrix(); matrix.reset(); matrix.setRotate(degrees); return Bitmap.createBitmap(tmpBitmap, 0, 0, tmpBitmap.getWidth(), tmpBitmap.getHeight(), matrix, true); } /** * 摄像头配置 */ public void initCameraParams() { stopPreview(); //获取camera参数 Camera.Parameters camParams = mCamera.getParameters(); List<Camera.Size> sizes = camParams.getSupportedPreviewSizes(); //确定前面定义的预览宽高是camera支持的,不支持取就更大的 for (int i = 0; i < sizes.size(); i++) { if ((sizes.get(i).width >= imageWidth && sizes.get(i).height >= imageHeight) || i == sizes.size() - 1) { imageWidth = sizes.get(i).width; imageHeight = sizes.get(i).height; // break; } } //设置最终确定的预览大小 camParams.setPreviewSize(imageWidth, imageHeight); //设置帧率 camParams.setPreviewFrameRate(frameRate); //启用参数 mCamera.setParameters(camParams); mCamera.setDisplayOrientation(90); //开始预览 startPreview(); } /** * 开始预览 */ public void startPreview() { try { mCamera.setPreviewCallback(this); mCamera.setPreviewDisplay(mHolder);//set the surface to be used for live preview mCamera.startPreview(); mCamera.autoFocus(autoFocusCB); } catch (IOException e) { mCamera.release(); mCamera = null; } } /** * 停止预览 */ public void stopPreview() { if (mCamera != null) { mCamera.setPreviewCallback(null); mCamera.stopPreview(); } } /** * 打开指定摄像头 */ public void openCamera() { Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); for (int cameraId = 0; cameraId < Camera.getNumberOfCameras(); cameraId++) { Camera.getCameraInfo(cameraId, cameraInfo); if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) { try { mCamera = Camera.open(cameraId); } catch (Exception e) { if (mCamera != null) { mCamera.release(); mCamera = null; } } break; } } } /** * 摄像头自动聚焦 */ Camera.AutoFocusCallback autoFocusCB = new Camera.AutoFocusCallback() { public void onAutoFocus(boolean success, Camera camera) { postDelayed(doAutoFocus, 1000); } }; private Runnable doAutoFocus = new Runnable() { public void run() { if (mCamera != null) { try { mCamera.autoFocus(autoFocusCB); }

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。