大家好,又见面了,我是你们的朋友全栈君。
最近做了一款Android应用需要输入大量的数据,为了提高体验我想了很多种输入数据的方式,最终采用了两种:二维码扫描和图片识别。前者顾名思义有个短板,就是需要生成二维码,下面就介绍下图片文字识别实现。
本应用是基于是OCR引擎,故需要第三方的jar包 tess-two.tesseract3.01-leptonica1.68-LibJPEG6b.jar 下载链接:点击打开链接
另外tessdata是语言包(我只下载了中文和英语包)下载链接:点击打开链接,需要放到手机SD卡根目录,我的应用中直接打包进apk中,免得需要拷贝的麻烦,但是造成的结果就是apk体积变得非常大,各位根据各自的情况做取舍,后面我会贴出打包进apk的方法。
首先介绍下布局文件,本应用为一个简单地实现,界面上就没有多做处理,主界面如下图:
如上图,可以选择是否二值化处理图片再识别文字,然后选泽需要识别的文字种类,紧接着可以选择拍摄或者相片选取,识别后文字显示在编辑框内,可修改识别错误后,点击复制到安卓粘贴板,具体的代码如下,就不多说了:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".TesMainActivity"
android:background="@drawable/beijing6" >
<LinearLayout
android:id="@+id/bottombar1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" >
<Button
android:id="@+id/btn_capy"
android:layout_weight="4"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:text="复制" />
<Button
android:id="@+id/btn_select"
android:layout_weight="4"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:text="相册选取" />
<Button
android:id="@+id/btn_camera"
android:layout_weight="4"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:text="拍照" />
</LinearLayout>
<RelativeLayout
android:id="@+id/bottombar"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_above="@id/bottombar1"
>
<RadioGroup
android:id="@+id/radiogroup"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_alignParentLeft="true"
android:orientation="horizontal" >
<RadioButton
android:id="@+id/rb_en"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:checked="true"
android:text="英" />
<RadioButton
android:id="@+id/rb_ch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="中" />
</RadioGroup>
</RelativeLayout>
<ScrollView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_above="@id/bottombar"
android:layout_alignParentTop="true" >
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<CheckBox
android:id="@+id/ch_pretreat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="二值化处理" />
<TextView
android:id="@+id/tv_result1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="result" />
<LinearLayout
android:focusable="true"
android:focusableInTouchMode="true"
android:layout_width="0px"
android:layout_height="0px"/>
<EditText
android:id="@+id/tv_result"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="选取的图片:" />
<ImageView
android:id="@+id/iv_selected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:maxHeight="300dp" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="预处理后的图片:" />
<ImageView
android:id="@+id/iv_treated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:maxHeight="300dp" />
</LinearLayout>
</ScrollView>
</RelativeLayout>
接着说明下Activity,在界面初始化是会对语言包文件夹进行判断,如果没有该文件进行复制,另外还会初始化各种控价,代码如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tesmain);
// 若文件夹不存在 首先创建文件夹
File path = new File(IMG_PATH);
if (!path.exists()) {
path.mkdirs();
}
tvResult = (EditText) findViewById(R.id.tv_result);
tvResult1 = (TextView) findViewById(R.id.tv_result1);
ivSelected = (ImageView) findViewById(R.id.iv_selected);
ivTreated = (ImageView) findViewById(R.id.iv_treated);
btnCamera = (Button) findViewById(R.id.btn_camera);
btnSelect = (Button) findViewById(R.id.btn_select);
btnCapy = (Button) findViewById(R.id.btn_capy);
chPreTreat = (CheckBox) findViewById(R.id.ch_pretreat);
radioGroup = (RadioGroup) findViewById(R.id.radiogroup);
btnCamera.setOnClickListener(new cameraButtonListener());
btnSelect.setOnClickListener(new selectButtonListener());
btnCapy.setOnClickListener(new capyButtonListener());
if(!isDirExist("tessdata")){
Toast.makeText(getApplicationContext(), "SD卡缺少语言包,复制中。。。",Toast.LENGTH_LONG).show();
new SaveFile_Thread().start();
}
// 用于设置解析语言
radioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
switch (checkedId) {
case R.id.rb_en:
LANGUAGE = "eng";
break;
case R.id.rb_ch:
LANGUAGE = "chi_sim";
break;
}
}
});
}
其中文件复制线程的代码如下:
public boolean SaveFileToSDCard(){
SDUtils sdutils_Chinese = new SDUtils("tessdata","chi_sim.traineddata",this,R.raw.chi_sim);
SDUtils sdutils_English = new SDUtils("tessdata","eng.traineddata",this,R.raw.eng);
try {
sdutils_Chinese.getSQLiteDatabase();
sdutils_English.getSQLiteDatabase();
} catch (IOException e) {
return false;
}
return true;
}
public class SaveFile_Thread extends Thread {
public SaveFile_Thread(){
}
public void run(){
synchronized (this) {
boolean iret;
do {
iret = SaveFileToSDCard();
} while (false);
if(iret){
ShowMsg(1);
}else
ShowMsg(2);
}
}
}
public void ShowMsg(int what) {
mLoadKeyHandler.sendEmptyMessage(what);
}
public Handler mLoadKeyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if(msg.what==1){
Toast.makeText(getApplicationContext(), "复制成功",Toast.LENGTH_LONG).show();
}
else if(msg.what==2)
Toast.makeText(getApplicationContext(), "复制失败",Toast.LENGTH_LONG).show();
}
};
对SD卡进行文件操作我编辑了一个SDUtils 类,具体如下:
package com.mikewong.tool.tesseract;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.os.Environment;
import android.util.Log;
/**
* 工具类 , 用于将RAW 目录下的文件写入到数据库中
*
* @author Administrator
*
*/
public class SDUtils {
private String file; // 设置文件存放路径
private String fileName; // 存放文件名称
private Context context; // 获取到Context 上下文
private int rawid; // 资源文件ID ,需要COPY 的文件
private String DATABASE_PATH = "";
private String DATABASE_NAME = "";
public String getFile() {
return file;
}
public void setFile(String file) {
this.file = file;
this.DATABASE_PATH = Environment.getExternalStorageDirectory()
.getAbsolutePath() + "/" + file;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
this.DATABASE_NAME = fileName;
}
public int getRawid() {
return rawid;
}
public void setRawid(int rawid) {
this.rawid = rawid;
}
public SDUtils() {
}
/**
*
* @param file
* 文件夹例如: aa/bb
* @param fileName
* 文件名
* @param context
* 上下文
* @param rawid
* 资源ID
*/
public SDUtils(String file, String fileName, Context context, int rawid) {
super();
this.file = file;
this.fileName = fileName;
this.context = context;
this.rawid = rawid;
this.DATABASE_PATH = Environment.getExternalStorageDirectory()
.getAbsolutePath() + "/" + file;
this.DATABASE_NAME = fileName;
}
/**
* 将文件复制到SD卡
*
* @return
* @throws IOException
*/
public boolean getSQLiteDatabase() throws IOException {
// 首先判断该目录下的文件夹是否存在
File dir = new File(DATABASE_PATH);
String filename1 = DATABASE_PATH + "/" + DATABASE_NAME;
if (!dir.exists()) {
// 文件夹不存在 , 则创建文件夹
dir.mkdirs();
}
// 判断目标文件是否存在
File file1 = new File(dir, DATABASE_NAME);
if (!file1.exists()) {
Log.i("msg", "没有文件,开始创建");
file1.createNewFile(); // 创建文件
}
Log.i("msg", "准备开始进行文件的复制");
// 开始进行文件的复制
InputStream input = context.getResources().openRawResource(rawid); // 获取资源文件raw
// 标号
try {
FileOutputStream out = new FileOutputStream(file1); // 文件输出流、用于将文件写到SD卡中
// -- 从内存出去
byte[] buffer = new byte[1024];
int len = 0;
while ((len = (input.read(buffer))) != -1) { // 读取文件,-- 进到内存
out.write(buffer, 0, len); // 写入数据 ,-- 从内存出
}
input.close();
out.close(); // 关闭流
return true;
} catch (Exception e) {
Log.i("msg", "复制异常");
return false;
}
}
}
三个按钮所对应的操作代码:
// 拍照识别
class cameraButtonListener implements OnClickListener {
@Override
public void onClick(View arg0) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(new File(IMG_PATH, "temp.jpg")));
startActivityForResult(intent, PHOTO_CAPTURE);
}
};
// 复制数据到剪切板
class capyButtonListener implements OnClickListener {
@Override
public void onClick(View arg0) {
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
// 将文本内容放到系统剪贴板里。
if(tvResult.length() == 0){
Toast.makeText(getApplicationContext(), "无数据", Toast.LENGTH_SHORT).show();
return;
}
cm.setText(tvResult.getText());
Toast.makeText(getApplicationContext(), "复制成功", Toast.LENGTH_SHORT).show();
}
};
// 从相册选取照片并裁剪
class selectButtonListener implements OnClickListener {
@Override
public void onClick(View v) {
// 激活系统图库,选择一张图片
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
// 开启一个带有返回值的Activity,请求码为PHOTO_REQUEST_GALLERY
boolean dele= delete(new File(IMG_PATH));
startActivityForResult(intent, PHOTO_REQUEST_GALLERY);
}
}
处理的回调函数
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == Activity.RESULT_CANCELED)
return;
if (requestCode == PHOTO_CAPTURE) {
tvResult1.setText("abc");
startPhotoCrop(Uri.fromFile(new File(IMG_PATH, "temp.jpg")));
}
if (requestCode == PHOTO_REQUEST_GALLERY) {
startPhotoCrop(data.getData());
}
// 处理结果
if (requestCode == PHOTO_RESULT) {
bitmapSelected = decodeUriAsBitmap(Uri.fromFile(new File(IMG_PATH,
"temp_cropped.jpg")));
if (chPreTreat.isChecked())
tvResult1.setText("预处理中......");
else
tvResult1.setText("识别中......");
// 显示选择的图片
showPicture(ivSelected, bitmapSelected);
// 新线程来处理识别
new Thread(new Runnable() {
@Override
public void run() {
if (chPreTreat.isChecked()) {
bitmapTreated = ImgPretreatment
.doPretreatment(bitmapSelected);
Message msg = new Message();
msg.what = SHOWTREATEDIMG;
myHandler.sendMessage(msg);
textResult = doOcr(bitmapTreated, LANGUAGE);
} else {
bitmapTreated = ImgPretreatment
.converyToGrayImg(bitmapSelected);
Message msg = new Message();
msg.what = SHOWTREATEDIMG;
myHandler.sendMessage(msg);
textResult = doOcr(bitmapTreated, LANGUAGE);
}
Message msg2 = new Message();
msg2.what = SHOWRESULT;
myHandler.sendMessage(msg2);
}
}).start();
}
super.onActivityResult(requestCode, resultCode, data);
}
负责剪切图片的函数
/**
* 调用系统图片编辑进行裁剪
*/
public void startPhotoCrop(Uri uri) {
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("scale", true);
intent.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(new File(IMG_PATH, "temp_cropped.jpg")));
intent.putExtra("return-data", false);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("noFaceDetection", true); // no face detection
startActivityForResult(intent, PHOTO_RESULT);
}
主要的功能实现函数如上,代码源码贴上:点击打开链接(辛苦手打收两个积分,如果积分不够可在下面留下邮箱,我看到后第一时间发送源码)
因上传源码有大小限制,故吧源码中的语言库删掉了,下载后只需把文章开始的tessdata语言包下的两个文件拷贝进res/raw下即可,如上图。
源码需求的人比较多,现在放在github上免费下载。地址为:https://github.com/cuilonglong/AndroidApp_ImageToText
纯手打,有帮助就回复下,有什么问题也可留言,我们共同讨论下。
转载请注明出处!谢谢!
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/131323.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...