HarmonyOS 6.0 OCR文字识别——手机变身扫描仪的正确姿势
拍发票自动提取金额、拍名片秒存通讯录、拍书页直接转文本——OCR(光学字符识别)是移动端最高频的AI能力之一。HarmonyOS NEXT 的 CoreVisionKit 提供了端侧 textRecognition,不依赖云端、不泄露隐私,离线也能跑。这篇把 OCR 从初始化到识别结果解析的完整链路讲清楚。
CoreVisionKit OCR 概览
端侧文字识别的核心类:
- textRecognition——文字识别引擎,支持中/英/日/韩/繁体
- TextRecognitionResult——识别结果,含文字块、行、字三级结构
- TextRecognitionMode——识别模式(全图/区域)
import { textRecognition } from '@kit.CoreVisionKit'
import { image } from '@kit.ImageKit'
权限与配置
OCR 本身不需要额外权限,但如果你要从相册选图或拍照,需要相机和读图权限:
{
"requestPermissions": [
{ "name": "ohos.permission.CAMERA" },
{ "name": "ohos.permission.READ_IMAGEVIDEO" }
]
}
OCR 三步走:init → recognizeText → release
1. 初始化引擎
let textRecognizer: textRecognition.TextRecognition | null = null
function initOcr(): void {
textRecognizer = textRecognition.init()
}
要点: init() 创建识别器实例,无需传参。建议在页面 aboutToAppear 时初始化,避免识别时再初始化导致延迟。
2. 执行识别
async function recognizeImage(pixelMap: image.PixelMap): Promise<textRecognition.TextRecognitionResult | null> {
if (textRecognizer === null) return null
let result: textRecognition.TextRecognitionResult = await textRecognizer.recognizeText(pixelMap)
return result
}
要点: recognizeText 接收 PixelMap 作为输入,返回识别结果。输入图片建议不超过 4096×4096,否则识别速度和精度都会下降。
3. 释放引擎
function releaseOcr(): void {
if (textRecognizer !== null) {
textRecognizer.release()
textRecognizer = null
}
}
要点: release() 释放引擎占用的内存。在 aboutToDisappear 时调用,避免内存泄漏。release 后不能再调用 recognizeText,否则报错。
解析识别结果
识别结果是三级树结构:TextRecognitionResult → TextBlock → TextLine → TextWord。
function parseResult(result: textRecognition.TextRecognitionResult): string[] {
let texts: string[] = []
let blocks: textRecognition.TextBlock[] = result.blocks
for (let i: number = 0; i < blocks.length; i++) {
let lines: textRecognition.TextLine[] = blocks[i].lines
for (let j: number = 0; j < lines.length; j++) {
let words: textRecognition.TextWord[] = lines[j].words
let lineText: string = ''
for (let k: number = 0; k < words.length; k++) {
lineText += words[k].value
}
texts.push(lineText)
}
}
return texts
}
要点: blocks 是文字块(段落级别),lines 是行,words 是单字/单词。每个 word 还有 confidence 置信度属性,可以用来过滤低质量识别。
置信度过滤
不是所有识别结果都靠谱,用置信度过滤低质量文字:
function parseWithConfidence(result: textRecognition.TextRecognitionResult, threshold: number): string[] {
let texts: string[] = []
let blocks: textRecognition.TextBlock[] = result.blocks
for (let i: number = 0; i < blocks.length; i++) {
let lines: textRecognition.TextLine[] = blocks[i].lines
for (let j: number = 0; j < lines.length; j++) {
let words: textRecognition.TextWord[] = lines[j].words
let lineText: string = ''
let allHighConf: boolean = true
for (let k: number = 0; k < words.length; k++) {
lineText += words[k].value
if (words[k].confidence < threshold) {
allHighConf = false
}
}
if (allHighConf) {
texts.push(lineText)
}
}
}
return texts
}
要点: confidence 范围 0.0~1.0,建议阈值 0.5 以上。低于 0.3 的基本是误识别。
常见OCR场景实战
发票识别
async function recognizeInvoice(pixelMap: image.PixelMap): Promise<void> {
let recognizer: textRecognition.TextRecognition = textRecognition.init()
let result: textRecognition.TextRecognitionResult = await recognizer.recognizeText(pixelMap)
let texts: string[] = parseResult(result)
// 提取关键信息
let invoiceCode: string = ''
let invoiceNumber: string = ''
let amount: string = ''
for (let i: number = 0; i < texts.length; i++) {
if (texts[i].includes('发票代码')) {
invoiceCode = texts[i].replace('发票代码:', '').replace('发票代码:', '')
}
if (texts[i].includes('发票号码')) {
invoiceNumber = texts[i].replace('发票号码:', '').replace('发票号码:', '')
}
if (texts[i].includes('金额') || texts[i].includes('价税合计')) {
amount = texts[i]
}
}
recognizer.release()
}
名片识别
async function recognizeBusinessCard(pixelMap: image.PixelMap): Promise<void> {
let recognizer: textRecognition.TextRecognition = textRecognition.init()
let result: textRecognition.TextRecognitionResult = await recognizer.recognizeText(pixelMap)
let texts: string[] = parseResult(result)
// 提取姓名、电话、邮箱
for (let i: number = 0; i < texts.length; i++) {
let text: string = texts[i]
// 手机号匹配
if (/1[3-9]\d{9}/.test(text)) {
// 保存电话
}
// 邮箱匹配
if (text.includes('@') && text.includes('.')) {
// 保存邮箱
}
}
recognizer.release()
}
书籍/文档识别
async function recognizeDocument(pixelMap: image.PixelMap): Promise<string> {
let recognizer: textRecognition.TextRecognition = textRecognition.init()
let result: textRecognition.TextRecognitionResult = await recognizer.recognizeText(pixelMap)
let texts: string[] = parseResult(result)
let fullText: string = texts.join('\n')
recognizer.release()
return fullText
}
图片预处理提升识别率
识别率不高?先做图片预处理再送 OCR:
import { image } from '@kit.ImageKit'
async function preprocessAndRecognize(pixelMap: image.PixelMap): Promise<string[]> {
// 1. 缩放:过大的图先缩小,提升速度
let width: number = pixelMap.getImageInfoSync().size.width
let height: number = pixelMap.getImageInfoSync().size.height
if (width > 2048 || height > 2048) {
let scale: number = 2048 / Math.max(width, height)
pixelMap.scale(scale, scale)
}
// 2. 旋转:如果图片方向不对,先旋转
// pixelMap.rotate(90)
// 3. 识别
let recognizer: textRecognition.TextRecognition = textRecognition.init()
let result: textRecognition.TextRecognitionResult = await recognizer.recognizeText(pixelMap)
let texts: string[] = parseResult(result)
recognizer.release()
return texts
}
要点: 推荐图片尺寸 1024~2048 像素,太小识别不准,太慢。旋转用 PixelMap.rotate(),裁剪用 crop()。
完整 Demo 代码
Demo 模拟了发票/名片/书籍三种场景的 OCR 识别流程,展示了 init→recognizeText→release 完整链路和结果结构化展示。
interface OcrResult {
text: string;
confidence: string;
}
@Entry
@Component
struct OcrDemo {
@State resultList: OcrResult[] = [];
@State statusMsg: string = '点击按钮模拟OCR识别';
@State isProcessing: boolean = false;
@State rawText: string = '';
build() {
Column({ space: 0 }) {
Row() {
Button('< 返回')
.fontSize(14)
.backgroundColor(Color.Transparent)
.fontColor('#1a73e8')
.onClick(() => { router.back(); })
Text('OCR 文字识别')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
.textAlign(TextAlign.Center)
Text(this.isProcessing ? '识别中' : '就绪')
.fontSize(12)
.fontColor(this.isProcessing ? '#F44336' : '#4CAF50')
}
.width('100%')
.height(56)
.padding({ left: 12, right: 12 })
.alignItems(VerticalAlign.Center)
.backgroundColor('#FFFFFF')
Scroll() {
Column({ space: 16 }) {
Column({ space: 12 }) {
Text('CoreVisionKit OCR')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
Text('textRecognition 端侧文字识别,支持中/英/日/韩/繁体')
.fontSize(13)
.fontColor('#999999')
.width('100%')
Column()
.width('100%')
.height(180)
.borderRadius(12)
.backgroundColor('#F5F5F5')
.justifyContent(FlexAlign.Center)
.border({ width: 1, color: '#E0E0E0', style: BorderStyle.Dashed })
.onClick(() => { this.simulateOcr(); })
Text('点击上方区域模拟拍照/选图 → OCR识别')
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
.padding(16)
.borderRadius(12)
.backgroundColor('#FFFFFF')
Row({ space: 8 }) {
Button('模拟发票识别').onClick(() => this.simulateInvoiceOcr())
Button('模拟名片识别').onClick(() => this.simulateCardOcr())
Button('模拟书籍识别').onClick(() => this.simulateBookOcr())
Button('清除').onClick(() => { this.resultList = []; this.rawText = ''; })
}
.width('100%')
.padding({ left: 16, right: 16 })
if (this.rawText) {
Column({ space: 8 }) {
Text('识别原始文本')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
Text(this.rawText)
.fontSize(14)
.fontColor('#333333')
.width('100%')
.padding(12)
.borderRadius(8)
.backgroundColor('#F5F5F5')
.copyOption(CopyOptions.LocalDevice)
}
.width('100%')
.padding(16)
.borderRadius(12)
.backgroundColor('#FFFFFF')
}
if (this.resultList.length > 0) {
Column({ space: 8 }) {
Text('结构化识别结果')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
ForEach(this.resultList, (result: OcrResult, index: number) => {
Row({ space: 8 }) {
Text(result.confidence)
.fontSize(11)
.fontColor('#FFFFFF')
.backgroundColor(this.getConfidenceColor(result.confidence))
.borderRadius(4)
.padding({ left: 4, right: 4, top: 2, bottom: 2 })
.width(48)
.textAlign(TextAlign.Center)
Text(result.text)
.fontSize(14)
.fontColor('#333333')
.layoutWeight(1)
}
.width('100%')
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.borderRadius(6)
.backgroundColor(index % 2 === 0 ? '#FAFAFA' : '#FFFFFF')
}, (result: OcrResult, index: number) => `${index}`)
}
.width('100%')
.padding(16)
.borderRadius(12)
.backgroundColor('#FFFFFF')
}
}
.padding(16)
}
.layoutWeight(1)
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
private getConfidenceColor(conf: string): string {
if (conf.startsWith('高')) return '#4CAF50'
if (conf.startsWith('中')) return '#FF9800'
return '#F44336'
}
private simulateOcr(): void {
this.simulateBookOcr()
}
private simulateInvoiceOcr(): void {
this.isProcessing = true
this.statusMsg = '识别中...(需真机+CoreVisionKit)'
setTimeout(() => {
this.rawText = '华为技术有限公司\n发票代码:044002100311\n发票号码:28756934\n开票日期:2025年08月10日\n金额:¥1,280.00\n税额:¥76.80\n价税合计:¥1,356.80'
this.resultList = [
{ text: '华为技术有限公司', confidence: '高(98%)' },
{ text: '发票代码:044002100311', confidence: '高(99%)' },
{ text: '发票号码:28756934', confidence: '高(99%)' },
{ text: '开票日期:2025年08月10日', confidence: '高(97%)' },
{ text: '金额:¥1,280.00', confidence: '高(98%)' },
{ text: '税额:¥76.80', confidence: '中(92%)' },
{ text: '价税合计:¥1,356.80', confidence: '高(96%)' }
]
this.isProcessing = false
this.statusMsg = '发票识别完成'
}, 1500)
}
private simulateCardOcr(): void {
this.isProcessing = true
this.statusMsg = '识别中...'
setTimeout(() => {
this.rawText = '张三\n高级工程师\n华为技术有限公司\n地址:深圳市龙岗区坂田街道\n电话:13800138000\n邮箱:zhangsan@huawei.com'
this.resultList = [
{ text: '姓名:张三', confidence: '高(99%)' },
{ text: '职位:高级工程师', confidence: '高(97%)' },
{ text: '公司:华为技术有限公司', confidence: '高(98%)' },
{ text: '电话:13800138000', confidence: '高(99%)' },
{ text: '邮箱:zhangsan@huawei.com', confidence: '高(98%)' }
]
this.isProcessing = false
this.statusMsg = '名片识别完成'
}, 1200)
}
private simulateBookOcr(): void {
this.isProcessing = true
this.statusMsg = '识别中...'
setTimeout(() => {
this.rawText = 'HarmonyOS NEXT应用开发实战\n第一章 环境搭建与项目创建\n1.1 DevEco Studio安装配置\n1.2 ArkTS语言基础\n1.3 第一个HelloWorld应用'
this.resultList = [
{ text: 'HarmonyOS NEXT应用开发实战', confidence: '高(96%)' },
{ text: '第一章 环境搭建与项目创建', confidence: '高(97%)' },
{ text: '1.1 DevEco Studio安装配置', confidence: '中(91%)' },
{ text: '1.2 ArkTS语言基础', confidence: '高(98%)' },
{ text: '1.3 第一个HelloWorld应用', confidence: '高(95%)' }
]
this.isProcessing = false
this.statusMsg = '书籍目录识别完成'
}, 1000)
}
}
支持语言与识别精度
| 语言 | 支持度 | 说明 |
|---|---|---|
| 简体中文 | 优秀 | 日常文档、发票、名片 |
| 英文 | 优秀 | 印刷体、手写体均可 |
| 繁体中文 | 良好 | 港台文档 |
| 日文 | 良好 | 平假名/片假名/汉字 |
| 韩文 | 良好 | 谚文识别 |
要点: 混合语言(如中英混排)也能识别,但精度略低于单语言。复杂排版(多列、表格)建议先做版面分析再识别。
踩坑清单
| 问题 | 原因 | 解决 |
|---|---|---|
| init() 返回 null | 设备不支持 CoreVisionKit | 检查设备 API 版本 ≥ 12 |
| recognizeText 报错 | PixelMap 未正确创建 | 确保 PixelMap 有效,宽度>0 |
| 识别结果为空 | 图片太小或模糊 | 图片缩放到 1024+ 像素宽度 |
| 中文识别乱码 | 图片方向不对 | rotate 校正方向后再识别 |
| 识别速度慢 | 图片分辨率过高 | 缩放到 2048 以内再识别 |
| release 后再识别崩溃 | 引擎已释放 | release 后重新 init |
| 手写体识别差 | 端侧模型偏向印刷体 | 手写体建议用云侧识别 |
| 置信度都很低 | 图片质量差/光线暗 | 增加图片预处理(对比度增强) |
| 多列排版识别错序 | OCR 按行扫描 | 按区域分别识别再合并 |
| 大图内存溢出 | 图片未压缩直接传入 | 先 scale 到合适尺寸 |
更多推荐

所有评论(0)