拍发票自动提取金额、拍名片秒存通讯录、拍书页直接转文本——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 到合适尺寸
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐