准备工作:
1.下载OpenCV:https://opencv.org/releases/
2.添加tess-two依赖:
建议直接在app的build.gradle下添加tess-two依赖库就可以了:
implementation 'com.rmtheis:tess-two:9.1.0'
最新版本:tess-two
也可以通过以上网址下载自行加入
3.识别库:
用到两个,一个是chi_sim 代表中文,一个是eng代表英文,资源中assets下没有识别库,需要自己添加chi_sim和eng。
识别库下载:识别库
4.图片压缩:
implementation 'top.zibin:Luban:1.1.3'
OpenCV图像处理:
/**
* 图像处理
* 二值化
* 腐蚀
*/
public void proSrc2Gray() {
Mat rgbMat = new Mat();
Mat grayMat = new Mat();
Mat binaryMat = new Mat();
Mat cannyMat = new Mat();
//获取彩色图像所对应的像素数据
Utils.bitmapToMat(srcBitmap, rgbMat);
//图像灰度化,将彩色图像数据转换为灰度图像数据并存储到grayMat中
Imgproc.cvtColor(rgbMat, grayMat, Imgproc.COLOR_RGB2GRAY);
//得到边缘图,这里最后两个参数控制着选择边缘的阀值上限和下限
// Imgproc.Canny(grayMat, cannyMat, 50, 300);
//二值化 ADAPTIVE_THRESH_MEAN_C THRESH_BINARY
Imgproc.threshold(grayMat, binaryMat, Imgproc.THRESH_BINARY_INV, 255, 7);
//获取自定义核,参数MORPH_RECT表示矩形的卷积核,当然还可以选择椭圆形的、交叉型的
Mat strElement = Imgproc.getStructuringElement(Imgproc.MORPH_RECT,
new Size(2, 2));
//腐蚀
Imgproc.erode(binaryMat,cannyMat,strElement);
Imgproc.dilate(binaryMat,cannyMat,strElement);
//Hough变换倾斜校正
Imgproc.HoughLinesP(binaryMat,cannyMat,1,3.14/180,1);
//创建一个图像
mBitmap = Bitmap.createBitmap(grayMat.cols(), grayMat.rows(),
Bitmap.Config.RGB_565);
//将矩阵binaryMat转换为图像
Utils.matToBitmap(grayMat, mBitmap);
}
文字识别代码:
/**
* 识别图像
* @param activity
* @param bitmap
*/
public void recognition(final Activity activity, final Bitmap bitmap) {
new Thread(new Runnable() {
@Override
public void run() {
/**
* 检测sd卡是否存在语言库
* 若不存在,从assets获取到本地sd卡
*/
if (!checkTrainedDataExists()) {
SDUtils.assets2SD(activity.getApplicationContext(), PathUtils.LANGUAGE_PATH, PathUtils.DEFAULT_LANGUAGE_NAME);
}
TessBaseAPI tessBaseAPI = new TessBaseAPI();
tessBaseAPI.setDebug(true);
tessBaseAPI.init(PathUtils.DATAPATH, PathUtils.DEFAULT_LANGUAGE);
//识别的图片
tessBaseAPI.setImage(bitmap);
//获得识别后的字符串
String text = "";
text = "识别结果:" + "\n" + tessBaseAPI.getUTF8Text();
final String finalText = text;
tessBaseAPI.end();
}
}).start();
}
以上文字识别提取的过程就完成了。
接下来就从手机相册获取照片或者拍照,然后压缩处理,识别文字。
获取图片:
权限:
Android6.0+需要动态申请权限
AndroidManifest中:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- 创建/删除文件的权限 -->
<uses-permission
android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.CAMERA" />
java代码中:
/**
* 申请权限
*/
private void requestPermissions() {
if (Build.VERSION.SDK_INT >= 23) {
if ((ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) &&
(ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED)) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA}, 1);
}
}
}
由于本人比较懒,所以直接“取消严格模式”来获取图片,为什么要“取消严格模式”我就不多说了,可以自行百度。
/**
* 取消严格模式 FileProvider
*/
private void CancelFileProvider(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy( builder.build() );
}
}
从相册获取图片:
打开相册:
/**
* 打开相册
* @param activity
*/
public void openAlbum(Activity activity) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
activity.startActivityForResult(intent, MainActivity.PICK_PHOTO);
}
拿到照片裁剪:
/**
* 从相册获取照片返回,裁剪
* @param activity
*/
public void PickPhotoResult(Activity activity){
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(MainActivity.imageUri, "image/*");
//intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.putExtra("crop", true);
intent.putExtra(MediaStore.EXTRA_OUTPUT, MainActivity.imageUri);
activity.startActivityForResult(intent, MainActivity.CROP_PHOTO); // 启动裁剪程序
}
裁剪:
/**
* 裁剪照片
* @param activity
*/
public void CropPhotoResult(Activity activity){
try {
OpenCVUtils.getInstance().srcBitmap = BitmapFactory.decodeStream(activity.getContentResolver().
openInputStream(MainActivity.imageUri));
OpenCVUtils.getInstance().proSrc2Gray();
activity.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
Uri.fromFile(ImageUtils.getInstance().createImageFile())));
if (ImageUtils.getInstance().mBitmap != null) {
ImageUtils.getInstance().showPicFileByLuban(activity);
MainActivity.imgView.setImageBitmap(ImageUtils.getInstance().mBitmap); // 将裁剪后的照片显示出来
MainActivity.imgView.setVisibility(View.VISIBLE);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
}
执行startActivityForResult后的回调函数
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
if (requestCode == PICK_PHOTO) {
//将content类型的Uri转化为文件类型的Uri
imageUri = ImageUtils.getInstance().convertUri(this,data.getData());
CameraUtils.getInstance().PickPhotoResult(this);
} else if (requestCode == TAKE_PHOTO) {
CameraUtils.getInstance().TakePhotoResult(this);
} else if (requestCode == CROP_PHOTO) {
CameraUtils.getInstance().CropPhotoResult(this);
}
}
}
图像裁剪后,会得到一个灰度化的图像,如果直接识别拍照的图片耗费时间很长,所以我在这对裁剪后的图片进行了压缩处理。
/**
* 压缩图片
* @param activity
*/
public void showPicFileByLuban(final Activity activity) {
try {
Luban.with(activity)
.load(new File(createImageFile().getPath()))
.setCompressListener(new OnCompressListener() {
@Override
public void onStart() {
// TODO 压缩开始前调用,可以在方法内启动 loading UI
}
@Override
public void onSuccess(File file) {
// TODO 压缩成功后调用,返回压缩后的图片文件
mBitmap = BitmapFactory.decodeFile(file.getPath());
Toast.makeText(activity,
file.length() / 1024 + "K",Toast.LENGTH_LONG).show();
}
@Override
public void onError(Throwable e) {
// TODO 当压缩过去出现问题时调用
}
}).launch();//启动压缩
} catch (IOException e) {
e.printStackTrace();
}
}
保存图片:
/**
* 将content类型的Uri转化为文件类型的Uri
* @param activity
* @param uri
* @return
*/
public Uri convertUri(Activity activity,Uri uri){
InputStream is;
try {
//Uri ----> InputStream
is = activity.getContentResolver().openInputStream(uri);
//InputStream ----> Bitmap
Bitmap bm = BitmapFactory.decodeStream(is);
//关闭流
is.close();
return ImageUtils.getInstance().saveBitmap(bm);
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
/**
* 将Bitmap写入SD卡中的一个文件中
* 并返回写入文件的Uri
* @param bm
* @return
*/
public Uri saveBitmap(Bitmap bm) {
//新建文件夹用于存放裁剪后的图片
File appDir = new File(PathUtils.DATAPATH + "/DCIM/Camera/");
if (!appDir.exists()) {
appDir.mkdirs();
}
try {
File file = createImageFile();
//打开文件输出流
FileOutputStream fos = new FileOutputStream(file);
//将bitmap压缩后写入输出流(参数依次为图片格式、图片质量和输出流)
bm.compress(Bitmap.CompressFormat.JPEG, 100, fos);
//刷新输出流
fos.flush();
//关闭输出流
fos.close();
//返回File类型的Uri
return Uri.fromFile(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
调用相机拍照:
/**
* 启动相机
* @param activity
*/
public void openCamera(Activity activity) {
try {
MainActivity.imageUri = Uri.fromFile(ImageUtils.getInstance().createImageFile());
} catch (IOException e) {
e.printStackTrace();
}
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//传递你要保存的图片的路径
intent.putExtra(MediaStore.EXTRA_OUTPUT, MainActivity.imageUri);
activity.startActivityForResult(intent, MainActivity.TAKE_PHOTO);
}
裁剪的方法和从相册获取图片的一样!!
工具类:
将assets中的识别库复制到SD卡中
public class SDUtils {
/**
* 将assets中的识别库复制到SD卡中
*
* @param path 要存放在SD卡中的 完整的文件名。这里是"/storage/emulated/0/tessdata/chi_sim.traineddata"
* @param name assets中的文件名 这里是 "chi_sim.traineddata"
*/
public static void assets2SD(Context context, String path, String name) {
//如果存在就删掉
File f = new File(path);
if (f.exists()) {
f.delete();
}
if (!f.exists()) {
File p = new File(f.getParent());//返回此抽象路径名父目录的路径名字符串
if (!p.exists()) {
p.mkdirs();//建立多级文件夹
}
try {
f.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
InputStream is = null;
OutputStream os = null;
try {
//打开assets文件获得一个InputStream字节输入流
is = context.getAssets().open(name);
File file = new File(path);
// 创建一个向指定 File 对象表示的文件中写入数据的文件输出流
os = new FileOutputStream(file);
byte[] bytes = new byte[4096];
int len = 0;
//从输入流中读取一定数量的字节,并将其存储在缓冲区数组bytes中
//如果因为流位于文件末尾而没有可用的字节,则返回值-1
while ((len = is.read(bytes)) != -1) {
//将指定byte数组中从偏移量off开始的len个字节写入此缓冲的输出流
os.write(bytes, 0, len);
}
//java在使用流时,都会有一个缓冲区,按一种它认为比较高效的方法来发数据:把要发的数据先放到缓冲区,
//缓冲区放满以后再一次性发过去,而不是分开一次一次地发
//flush()强制将缓冲区中的数据发送出去,不必等到缓冲区满
os.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//关闭输入流和输出流
if (is != null)
is.close();
if (os != null)
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
保存的路径:
public class PathUtils {
//TessBaseAPI初始化用到的第一个参数,是个目录
public static final String DATAPATH = Environment.getExternalStorageDirectory()
.getAbsolutePath() + File.separator;
//在DATAPATH中新建这个目录,TessBaseAPI初始化要求必须有这个目录
public static final String tessdata = DATAPATH + File.separator + "tessdata";
//TessBaseAPI初始化测第二个参数,就是识别库的名字不要后缀名。
public static String DEFAULT_LANGUAGE = "chi_sim";
//assets中的文件名
public static String DEFAULT_LANGUAGE_NAME = DEFAULT_LANGUAGE + ".traineddata";
//保存到SD卡中的完整文件名
public static String LANGUAGE_PATH = tessdata + File.separator + DEFAULT_LANGUAGE_NAME;
}
最后,识别按钮操作:
case R.id.btn_rec:
if (imgView.getVisibility() != View.VISIBLE) {
Toast.makeText(getApplicationContext(), "请先拍照或者选一张图片", Toast.LENGTH_SHORT).show();
return;
} else {
resultTv.setText("");
txtFinal.setText("");
try {
ImageUtils.getInstance().mBitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
} catch (Exception e) {
e.printStackTrace();
}
OpenCVUtils.getInstance().recognition(this,ImageUtils.getInstance().mBitmap);
}
break;
完整demo下载
参考:https://blog.csdn.net/qq_35820350/article/details/78802276
感谢博主的分享!!