当前位置: 首页 > news >正文

安卓OCR使用(Google ML Kit)

OCR是一个很常用的功能,Google ML Kit提供了OCR能力,用起来也很简单,本文介绍一下使用方法。

1. 相关概念

名词概念解释
TextBlock一个段落
Line一行文本
Element元素单词;对汉字来说,类似"开头 (分隔符)中间(分隔符) 结尾"这样含有明显分隔符的才会有多个字在一个Element中,否则就是单个字
Symbol字符字母;对汉字来说就是单个字

2. 代码实现

在build.gradle中添加相关依赖:

// To recognize Chinese script
implementation 'com.google.mlkit:text-recognition-chinese:16.0.1'

添加布局文件activity_ocr.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><FrameLayoutandroid:layout_width="wrap_content"android:layout_height="wrap_content"><SurfaceViewandroid:id="@+id/camera_preview"android:layout_width="wrap_content"android:layout_height="wrap_content" /><com.example.study.views.DrawViewandroid:id="@+id/ocr_area"android:layout_width="wrap_content"android:layout_height="wrap_content" /></FrameLayout><Buttonandroid:id="@+id/ocr_switch"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_gravity="center_horizontal|bottom"android:layout_marginBottom="80dp"android:background="@color/夏云灰"android:text="stop" />
</LinearLayout>

绘制文字的OCRDrawView.java:

package com.example.study.views;import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.View;import androidx.annotation.Nullable;import java.util.ArrayList;
import java.util.List;public class OCRDrawView extends View {private final Object lock = new Object();protected Paint paint = new Paint();protected Path path = new Path();private final List<ShapeInfo> cornerPointsList = new ArrayList<>();public OCRDrawView(Context context) {super(context);}public OCRDrawView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);}public void clear() {synchronized (lock) {cornerPointsList.clear();}postInvalidate();}public void add(Point[] cornerPoints, String text) {synchronized (lock) {cornerPointsList.add(new ShapeInfo(cornerPoints, text));}}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);synchronized (lock) {for (ShapeInfo shapeInfo : cornerPointsList) {drawBackground(shapeInfo, canvas);drawText(shapeInfo, canvas);}}}private void drawText(ShapeInfo shapeInfo, Canvas canvas) {Point[] points = shapeInfo.points;// 根据矩形区域的高度设置文字大小double height = calDistance(points[0], points[3]);double width = calDistance(points[2], points[3]);float textSize = (float) Math.min(height, width / shapeInfo.text.length());paint.setColor(Color.BLUE);paint.setTextSize(textSize);path.reset();path.moveTo(points[3].x, points[3].y);path.lineTo(points[2].x, points[2].y);canvas.drawTextOnPath(shapeInfo.text, path, 0, 0, paint);}private double calDistance(Point start, Point end) {return Math.sqrt(Math.pow(start.x - end.x, 2) + Math.pow(start.y - end.y, 2));}private void drawBackground(ShapeInfo shapeInfo, Canvas canvas) {Point[] shape = shapeInfo.points;path.reset();path.moveTo(shape[3].x, shape[3].y);for (int i = 0; i < shape.length; i++) {path.lineTo(shape[i].x, shape[i].y);}path.close();paint.setColor(Color.WHITE);canvas.drawPath(path, paint);}static class ShapeInfo {Point[] points;String text;public ShapeInfo(Point[] shape, String text) {this.points = shape;this.text = text;}}
}

activity类:

package com.example.study.activities;import android.Manifest;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.hardware.Camera;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.Toast;import androidx.activity.ComponentActivity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;import com.example.study.R;
import com.example.study.views.OCRDrawView;
import com.google.mlkit.vision.text.Text;
import com.google.mlkit.vision.text.TextRecognition;
import com.google.mlkit.vision.text.TextRecognizer;
import com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions;import java.io.ByteArrayOutputStream;public class OCRActivity extends ComponentActivity implements Camera.PreviewCallback, SurfaceHolder.Callback {private static final String TAG = "CameraDemoActivity";private static final int REQUEST_CAMERA = 1000;private static final int HEIGHT = 1920;private static final int WIDTH = 1080;private static final int ORIENTATION = 90;private SurfaceView preview;private OCRDrawView ocrArea;private Button ocrSwitch;private Camera camera;private Camera.Parameters parameters;private TextRecognizer recognizer;private Matrix matrix;private boolean isRecognizering = false;private boolean stopRecognizer = false;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);this.setContentView(R.layout.activity_ocr);initView();initVar();// 检查权限if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);} else {preview.getHolder().addCallback(this);}}private void initVar() {recognizer = TextRecognition.getClient(new ChineseTextRecognizerOptions.Builder().build());matrix = new Matrix();matrix.setRotate(ORIENTATION);// 4个角的坐标是没有旋转过的,所以HEIGHT、WIDTH是反的matrix.preTranslate(-HEIGHT >> 1, -WIDTH >> 1);}private void initView() {preview = findViewById(R.id.camera_preview);ocrArea = findViewById(R.id.ocr_area);ocrSwitch = findViewById(R.id.ocr_switch);ocrSwitch.setOnClickListener(view -> {stopRecognizer = !stopRecognizer;ocrSwitch.setText(stopRecognizer ? "start" : "stop");if (camera == null) {return;}if (stopRecognizer) {camera.stopPreview();} else {camera.startPreview();}});adjustSurface(preview, ocrArea);}private void adjustSurface(SurfaceView cameraPreview, OCRDrawView ocrArea) {FrameLayout.LayoutParams cameraPreviewParams = (FrameLayout.LayoutParams) cameraPreview.getLayoutParams();cameraPreviewParams.width = WIDTH;cameraPreviewParams.height = HEIGHT;ViewGroup.LayoutParams ocrAreaParams = ocrArea.getLayoutParams();ocrAreaParams.width = WIDTH;ocrAreaParams.height = HEIGHT;}@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);if (requestCode == REQUEST_CAMERA && grantResults.length > 0) {if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {preview.getHolder().addCallback(this);surfaceCreated(preview.getHolder());camera.setPreviewCallback(this);camera.startPreview();} else {finish();}}}@Overridepublic void onPreviewFrame(byte[] data, Camera camera) {if (isRecognizering || stopRecognizer) {return;}Bitmap bitmap = convertToBitmap(camera, data);isRecognizering = true;recognizer.process(bitmap, ORIENTATION).addOnSuccessListener(text -> {parseOCRResult(text);}).addOnFailureListener(exception -> {Toast.makeText(this, "Failure", Toast.LENGTH_SHORT).show();isRecognizering = false;}).addOnCompleteListener(task -> {isRecognizering = false;}).addOnCanceledListener(() -> {Toast.makeText(this, "Canceled", Toast.LENGTH_SHORT).show();isRecognizering = false;});}private void parseOCRResult(Text text) {// 所有识别到的内容,下同String textContent = text.getText();if (textContent == null || textContent.trim().length() == 0) {return;}ocrArea.clear();// 块,段落for (Text.TextBlock textBlock : text.getTextBlocks()) {// 一行文本for (Text.Line line : textBlock.getLines()) {drawResult(line);// 元素:单词,对汉字来说,需要"开头 (分隔符)中间(分隔符) 结尾"之类比较强烈的分隔符去分隔for (Text.Element element : line.getElements()) {// symbol:字符,字母,字for (Text.Symbol symbol : element.getSymbols()) {symbol.getText();}}}}}private void drawResult(Text.Line line) {// line的旋转角度(以度为单位,顺时针为正,范围为[-180, 180])float angle = line.getAngle() + ORIENTATION;// 检测到的文本的轴对齐边界矩形Rect boundingBox = line.getBoundingBox();// 从左上角开始顺时针方向的四个角点。不带旋转角度,如果设置过旋转角度camera.setDisplayOrientation,需要进行旋转Point[] cornerPoints = line.getCornerPoints();// 置信度float confidence = line.getConfidence();// 获取文本中的主要语言(如果有的话)String recognizedLanguage = line.getRecognizedLanguage();// 置信度太低的过滤掉if (confidence < 0.3f) {return;}for (Point cornerPoint : cornerPoints) {float[] floats = {cornerPoint.x, cornerPoint.y};matrix.mapPoints(floats);cornerPoint.x = (int) floats[0] + (WIDTH >> 1);cornerPoint.y = (int) floats[1] + (HEIGHT >> 1);}ocrArea.add(cornerPoints, line.getText());ocrArea.postInvalidate();}/*** Convert camera data into bitmap data.*/private Bitmap convertToBitmap(Camera camera, byte[] data) {int width = camera.getParameters().getPreviewSize().width;int height = camera.getParameters().getPreviewSize().height;YuvImage yuv = new YuvImage(data, ImageFormat.NV21, width, height, null);ByteArrayOutputStream stream = new ByteArrayOutputStream();yuv.compressToJpeg(new Rect(0, 0, width, height), 100, stream);return BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.toByteArray().length);}@Overrideprotected void onResume() {super.onResume();}@Overrideprotected void onRestart() {super.onRestart();}@Overrideprotected void onDestroy() {super.onDestroy();if (recognizer != null) {recognizer.close();}}@Overridepublic void surfaceCreated(@NonNull SurfaceHolder holder) {try {camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);parameters = camera.getParameters();// 旋转了90度,所以height、width互换parameters.setPictureSize(HEIGHT, WIDTH);parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);parameters.setPictureFormat(ImageFormat.NV21);camera.setPreviewDisplay(holder);camera.setDisplayOrientation(ORIENTATION);camera.setParameters(parameters);} catch (Exception exception) {Log.i(TAG, exception.getMessage());}}@Overridepublic void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {if (camera != null) {camera.stopPreview();camera.setPreviewCallback(null);camera.startPreview();camera.setPreviewCallback(this);ocrArea.clear();stopRecognizer = true;ocrSwitch.performClick();}}@Overridepublic void surfaceDestroyed(@NonNull SurfaceHolder holder) {if (camera != null) {camera.stopPreview();camera.setPreviewCallback(null);camera.release();}}
}

参考文章

  1. 文字识别 v2

相关文章:

安卓OCR使用(Google ML Kit)

OCR是一个很常用的功能&#xff0c;Google ML Kit提供了OCR能力&#xff0c;用起来也很简单&#xff0c;本文介绍一下使用方法。 1. 相关概念 名词概念解释TextBlock块一个段落Line行一行文本Element元素单词&#xff1b;对汉字来说&#xff0c;类似"开头 (分隔符)中间&…...

《机器学习》——贝叶斯算法

贝叶斯简介 贝叶斯公式&#xff0c;又称贝叶斯定理、贝叶斯法则&#xff0c;最初是用来描述两个事件的条件概率间的关系的公式&#xff0c;后来被人们发现具有很深刻的实际意义和应用价值。该公式的实际内涵是&#xff0c;支持某项属性的事件发生得愈多&#xff0c;则该属性成…...

【博主推荐】 Microi吾码开源低代码平台,快速建站,提高开发效率

&#x1f36c;引言 &#x1f36c;什么是低代码平台&#xff1f; 低代码平台&#xff08;Low-Code Platform&#xff09;是一种使开发人员和业务用户可以通过图形化界面和少量的编程来创建应用程序的开发工具。与传统的编程方式相比&#xff0c;低代码平台大大简化了开发过程&a…...

网站自动签到

我研究生生涯面临两个问题&#xff0c;一是写毕业论文&#xff0c;二是找工作&#xff0c;这两者又有很大的冲突。怎么解决这两个冲突呢&#xff1f;把python学好是一个路子&#xff0c;因此从今天我要开一个专栏就是学python 其实我的本意不是网站签到&#xff0c;我喜欢在起点…...

C 语言奇幻之旅 - 第16篇:C 语言项目实战

目录 引言1. 项目规划1.1 需求分析与设计1.1.1 项目目标1.1.2 功能需求1.1.3 技术实现方案 2. 代码实现2.1 模块化编程2.1.1 学生信息模块2.1.2 成绩管理模块 2.2 调试与测试2.2.1 调试2.2.2 测试2.2.4 测试结果 3. 项目总结3.1 代码优化与重构3.1.1 代码优化3.1.2 代码重构 3.…...

项目实战——使用python脚本完成指定OTA或者其他功能的自动化断电上电测试

前言 在嵌入式设备的OTA场景测试和其他断电上电测试过程中&#xff0c;有的场景发生在夜晚或者随时可能发生&#xff0c;这个时候不可能24h人工盯着&#xff0c;需要自动化抓取串口日志处罚断电上电操作。 下面的python脚本可以实现自动抓取串口指定关键词&#xff0c;然后触发…...

04、Redis深入数据结构

一、简单动态字符串SDS 无论是Redis中的key还是value&#xff0c;其基础数据类型都是字符串。如&#xff0c;Hash型value的field与value的类型&#xff0c;List型&#xff0c;Set型&#xff0c;ZSet型value的元素的类型等都是字符串。redis没有使用传统C中的字符串而是自定义了…...

【MySQL学习笔记】MySQL的索引

MySQL索引 1、索引概述2、 索引的数据结构2.1 BTree索引结构2.2 Hash索引结构2.3 InnoDB选择BTree的原因 3、索引分类4、索引的语法5、SQL性能分析5.1 SQL执行频率5.2 慢查询日志5.3 profile详情5.4 explain执行计划 6、索引使用规则6.1 最左前缀法则6.2 范围查询6.3索引失效情…...

利用ArcGIS快速准确地统计出地块的现状容积率

研究目的 根据建筑.dwg、建筑.dwg Annotation、建筑.dwg Polygon&#xff0c;地籍边界.shp等数据&#xff0c;利用GIS快速准确地统计出地块的现状容积率。 研究思路 加载数据图层&#xff1a;建筑.dwg Polygon、建筑.dwg Annotation&#xff0c;使用空间连接功能把建筑层数数…...

C++类的引入

C中类的前身 1> 面向对象三大特征&#xff1a;封装、继承、多态 2> 封装&#xff1a;将能够实现某一事物的所有万事万物都封装到一起&#xff0c;包括成员属性&#xff08;成员变量&#xff09;&#xff0c;行为&#xff08;功能函数&#xff09;都封装在一起&#xff…...

【跨域问题】

跨域问题 官方概念&#xff1a; 当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域本质来说&#xff0c;是前端请求给到后端时候&#xff0c;请求头里面&#xff0c;有一个 Origin &#xff0c;会带上 协议域名端口号等&#xff1b;后端接受到请求&…...

“深入浅出”系列之FFmpeg:(1)音视频开发基础

我的音视频开发大部分内容是跟着雷霄骅大佬学习的&#xff0c;所以笔记也是跟雷老师的博客写的。 一、音视频相关的基础知识 首先播放一个视频文件的流程如下所示&#xff1a; FFmpeg的作用就是将H.264格式的数据转换成YUV格式的数据&#xff0c;然后SDL将YUV显示到电脑屏幕上…...

Springboot3.4整合jsp

文章目录 环境 springboot3.4 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency> <!--用于编译jsp--> <!-- Tomcat Embed Jasper --> <dependency>…...

CSS:背景样式、盒子模型与文本样式

背景样式 背景样式用于设置网页元素的背景&#xff0c;包括颜色、图片等。 背景颜色 使用 background-color 属性设置背景颜色&#xff0c;支持多种格式&#xff08;颜色英文、十六进制、RGB等&#xff09;。 div {background-color: lightblue; }格式示例十六进制#ff5733R…...

算法:线性查找

线性查找算法是一种简单的查找算法,用于在一个数组或列表中查找一个特定的元素。它从数组的第一个元素开始,逐个检查每个元素,直到找到所需的元素或搜索完整个数组。线性查找的时间复杂度为O(n),其中n是数组中的元素数量。 实现原理 从列表的第一个元素开始,逐个检查每个…...

【计算机网络】什么是网关(Gateway)?

网上冲浪多了&#xff0c;你可以听到过网关&#xff08;Gateway&#xff09;这个词&#xff0c;但是却不太清楚网关&#xff08;Gateway&#xff09;到底是干什么的、负责网络当中的什么任务&#xff0c;本篇文字将会为你介绍网关&#xff08;Gateway&#xff09;的作用&#x…...

20250106面试

rabbitmq如何保证消息不丢失 my&#xff1a; 持久化&#xff0c;包括消息持久化和队列持久化&#xff0c;重启不丢失。持久化到磁盘中的。 消息确认 死信队列&#xff1a;消费失败&#xff08;业务异常/未确认&#xff0c;重试后&#xff0c;会放死信队列&#xff09;&…...

Java 分布式锁:Redisson、Zookeeper、Spring 提供的 Redis 分布式锁封装详解

&#x1f4da; Java 分布式锁&#xff1a;Redisson、Zookeeper、Spring 提供的 Redis 分布式锁封装详解 在分布式系统中&#xff0c;分布式锁 用于解决多个服务实例同时访问共享资源时的 数据一致性 问题。Java 生态中&#xff0c;有多种成熟的框架可以实现分布式锁&#xff0…...

智能汽车的数字钥匙安全

数字钥匙作为汽车智能化变革下的一项创新技术&#xff0c;利用蓝牙定位、NFC等近场通信技术进行钥匙与汽车的匹配继而开锁&#xff0c;可以让车主通过智能手机、可穿戴设备等解锁汽车&#xff0c;并对汽车实施相关的操作&#xff0c;提升用车便利性&#xff0c;受到越来越多车企…...

YangQG 面试题汇总

一、交叉链表 问题&#xff1a; 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点&#xff0c;返回 null 。 解题思想&#xff1a; 双指针 备注&#xff1a;不是快慢指针&#xff0c;如果两个长度相…...

springboot 百货中心供应链管理系统小程序

一、前言 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;百货中心供应链管理系统被用户普遍使用&#xff0c;为方…...

脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)

一、数据处理与分析实战 &#xff08;一&#xff09;实时滤波与参数调整 基础滤波操作 60Hz 工频滤波&#xff1a;勾选界面右侧 “60Hz” 复选框&#xff0c;可有效抑制电网干扰&#xff08;适用于北美地区&#xff0c;欧洲用户可调整为 50Hz&#xff09;。 平滑处理&…...

将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?

Otsu 是一种自动阈值化方法&#xff0c;用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理&#xff0c;能够自动确定一个阈值&#xff0c;将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...

2025 后端自学UNIAPP【项目实战:旅游项目】6、我的收藏页面

代码框架视图 1、先添加一个获取收藏景点的列表请求 【在文件my_api.js文件中添加】 // 引入公共的请求封装 import http from ./my_http.js// 登录接口&#xff08;适配服务端返回 Token&#xff09; export const login async (code, avatar) > {const res await http…...

C++.OpenGL (10/64)基础光照(Basic Lighting)

基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...

相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)

【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...

tree 树组件大数据卡顿问题优化

问题背景 项目中有用到树组件用来做文件目录&#xff0c;但是由于这个树组件的节点越来越多&#xff0c;导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多&#xff0c;导致的浏览器卡顿&#xff0c;这里很明显就需要用到虚拟列表的技术&…...

es6+和css3新增的特性有哪些

一&#xff1a;ECMAScript 新特性&#xff08;ES6&#xff09; ES6 (2015) - 革命性更新 1&#xff0c;记住的方法&#xff0c;从一个方法里面用到了哪些技术 1&#xff0c;let /const块级作用域声明2&#xff0c;**默认参数**&#xff1a;函数参数可以设置默认值。3&#x…...

中科院1区顶刊|IF14+:多组学MR联合单细胞时空分析,锁定心血管代谢疾病的免疫治疗新靶点

中科院1区顶刊|IF14&#xff1a;多组学MR联合单细胞时空分析&#xff0c;锁定心血管代谢疾病的免疫治疗新靶点 当下&#xff0c;免疫与代谢性疾病的关联研究已成为生命科学领域的前沿热点。随着研究的深入&#xff0c;我们愈发清晰地认识到免疫系统与代谢系统之间存在着极为复…...

篇章一 论坛系统——前置知识

目录 1.软件开发 1.1 软件的生命周期 1.2 面向对象 1.3 CS、BS架构 1.CS架构​编辑 2.BS架构 1.4 软件需求 1.需求分类 2.需求获取 1.5 需求分析 1. 工作内容 1.6 面向对象分析 1.OOA的任务 2.统一建模语言UML 3. 用例模型 3.1 用例图的元素 3.2 建立用例模型 …...