拍照识别文字识别不准?华为 Core Vision Kit 让你三步搞定

前两天有个朋友找我吐槽,说他们公司要做个发票识别功能,用了第三方的 OCR 服务,结果识别准确率感人,尤其是那种拍摄角度有点偏的图片,基本就是瞎猜。我问他为啥不用 HarmonyOS 自带的文字识别能力,他一脸懵逼:“还有这玩意儿?”

说实话,HarmonyOS 的 Core Vision Kit 提供的通用文字识别能力,在很多场景下比第三方服务更靠谱。而且它直接集成在系统里,不用额外申请 API Key,也不用担心网络延迟问题。

今天我就把这套能力从原理到实战给你盘一遍,顺便把踩过的坑都列出来,让你少走弯路。


一、这玩意儿能干啥?

Core Vision Kit 的文字识别能力,说白了就是把图片里的文字提取出来。听起来简单,但实际场景里要考虑的东西还挺多:

  • 支持多种来源的图片:相机拍照、图库选择、甚至是网络下载的图片
  • 能处理复杂场景:文本倾斜、拍摄角度偏移、光照不均匀、背景复杂这些情况都能应付
  • 提供位置信息:不光给你文字内容,还能告诉你每个字、每行、每段在图片上的坐标位置
  • 支持多语言:简体中文、英文、日文、韩文、繁体中文这五种语言都能识别

适用场景也很多,比如:

  • 扫描文档、票据、卡证这些印刷品
  • 街景招牌识别
  • 翻译应用里的文字提取
  • 搜索应用里的文字搜索

二、使用前先搞清楚这些限制

别急着上手写代码,有些限制你得先心里有数,不然到时候踩坑了都不知道为啥。

图片格式要求

  • 只支持 JPEG、JPG、PNG 这三种格式
  • 图片质量建议 720p 以上
  • 尺寸范围:100px < 高度 < 15210px,100px < 宽度 < 10000px
  • 高宽比例建议 10:1 以下,接近手机屏幕比例最好

语言和文本限制

  • 支持的语言就那五种,其他语言识别不了
  • 文本长度不超过 10000 字符
  • 只能识别印刷体,手写体识别能力有限

拍摄角度要求

  • 拍摄角度与文本平面的夹角要小于 30 度
  • 超过这个角度,识别准确率会直线下降

设备限制

  • 当前不支持模拟器,只能在真机上测试
  • 支持的设备:Phone、PC/2in1、Tablet

三、核心 API 速览

HarmonyOS 文字识别的 API 设计还挺清晰的,主要就这几个关键类:

VisionInfo

这是待识别的入参,目前只支持 PixelMap 类型的视觉信息,而且颜色格式必须是 RGBA_8888。

import { image } from '@kit.ImageKit';

const visionInfo: textRecognition.VisionInfo = {
  pixelMap: pixelMap  // 待识别的图片 PixelMap
};

TextRecognitionConfiguration

配置项,主要用来控制是否开启朝向检测。

const config: textRecognition.TextRecognitionConfiguration = {
  isDirectionDetectionSupported: true  // 是否支持文字朝向检测,默认 true
};

这里有个小技巧:如果你确定图片是正向的,可以把这个设为 false,能提升性能。但如果你不确定,还是保持 true 比较稳妥。

TextRecognitionResult

这是识别结果的返回值,层级结构是这样的:

TextRecognitionResult (识别结果)
  └── TextBlock[] (文本块/段落)
      └── TextLine[] (文本行)
          └── TextWord[] (单词)
              └── PixelPoint[] (外框坐标点)

每个层级都包含文本内容和外框坐标信息,你可以根据需要选择合适的层级来处理数据。


四、实战:从零实现文字识别功能

下面我给你一个完整的实战案例,从图片选择到文字识别,再到结果展示,全流程走一遍。

1. 配置页面布局

先搞个简单的 UI,一个按钮触发图片选择,一个文本框展示识别结果。

// Index.ets
@Entry
@Component
struct Index {
  @State recognizedText: string = '识别结果将显示在这里';

  build() {
    Column() {
      Button('选择图片并识别')
        .onClick(() => {
          this.selectAndRecognizeImage();
        })
        .margin({ top: 20 })

      Text(this.recognizedText)
        .fontSize(16)
        .margin({ top: 20 })
        .padding(10)
        .border({ width: 1, color: Color.Gray })
        .width('90%')
    }
    .width('100%')
    .height('100%')
  }
}

2. 实现图片选择和识别逻辑

核心代码来了,这里涉及几个关键步骤:

import { textRecognition } from '@kit.CoreVisionKit';
import { image } from '@kit.ImageKit';
import { picker } from '@kit.CoreFileKit';
import { fileIo as fs } from '@kit.CoreFileKit';

async selectAndRecognizeImage() {
  try {
    // 步骤1:拉起图库选择图片
    const photoSelectOptions = new picker.PhotoSelectOptions();
    photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
    photoSelectOptions.maxSelectNumber = 1;

    const photoPicker = new picker.PhotoViewPicker();
    const photoSelectResult = await photoPicker.select(photoSelectOptions);

    if (photoSelectResult.photoUris.length === 0) {
      this.recognizedText = '未选择图片';
      return;
    }

    // 步骤2:将图片转换为 PixelMap
    const imageSource = image.createImageSource(photoSelectResult.photoUris[0]);
    const pixelMap = await imageSource.createPixelMap();

    // 步骤3:实例化 VisionInfo
    const visionInfo: textRecognition.VisionInfo = {
      pixelMap: pixelMap
    };

    // 步骤4:配置识别参数
    const config: textRecognition.TextRecognitionConfiguration = {
      isDirectionDetectionSupported: true
    };

    // 步骤5:调用识别接口
    const result = await textRecognition.recognizeText(visionInfo, config);

    // 步骤6:处理识别结果
    if (result && result.length > 0) {
      let fullText = '';
      result.forEach((block: textRecognition.TextBlock) => {
        block.lines.forEach((line: textRecognition.TextLine) => {
          fullText += line.value + '\n';
        });
      });
      this.recognizedText = fullText;
    } else {
      this.recognizedText = '未识别到文字';
    }

    // 步骤7:释放资源
    pixelMap.release();

  } catch (error) {
    this.recognizedText = `识别失败: ${JSON.stringify(error)}`;
  }
}

3. 完整代码整合

把上面的代码整合到一起,就是一个完整的文字识别功能了。

// Index.ets
import { textRecognition } from '@kit.CoreVisionKit';
import { image } from '@kit.ImageKit';
import { picker } from '@kit.CoreFileKit';

@Entry
@Component
struct Index {
  @State recognizedText: string = '点击下方按钮选择图片进行文字识别';

  async selectAndRecognizeImage() {
    try {
      // 拉起图库选择图片
      const photoSelectOptions = new picker.PhotoSelectOptions();
      photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
      photoSelectOptions.maxSelectNumber = 1;

      const photoPicker = new picker.PhotoViewPicker();
      const photoSelectResult = await photoPicker.select(photoSelectOptions);

      if (photoSelectResult.photoUris.length === 0) {
        this.recognizedText = '未选择图片';
        return;
      }

      // 将图片转换为 PixelMap
      const imageSource = image.createImageSource(photoSelectResult.photoUris[0]);
      const pixelMap = await imageSource.createPixelMap();

      // 实例化 VisionInfo
      const visionInfo: textRecognition.VisionInfo = {
        pixelMap: pixelMap
      };

      // 配置识别参数
      const config: textRecognition.TextRecognitionConfiguration = {
        isDirectionDetectionSupported: true
      };

      // 调用识别接口
      const result = await textRecognition.recognizeText(visionInfo, config);

      // 处理识别结果
      if (result && result.length > 0) {
        let fullText = '';
        result.forEach((block: textRecognition.TextBlock) => {
          block.lines.forEach((line: textRecognition.TextLine) => {
            fullText += line.value + '\n';
          });
        });
        this.recognizedText = fullText;
      } else {
        this.recognizedText = '未识别到文字';
      }

      // 释放资源
      pixelMap.release();

    } catch (error) {
      this.recognizedText = `识别失败: ${JSON.stringify(error)}`;
    }
  }

  build() {
    Column() {
      Button('选择图片并识别')
        .onClick(() => {
          this.selectAndRecognizeImage();
        })
        .margin({ top: 20 })

      Text(this.recognizedText)
        .fontSize(16)
        .margin({ top: 20 })
        .padding(10)
        .border({ width: 1, color: Color.Gray })
        .width('90%')
    }
    .width('100%')
    .height('100%')
  }
}

五、实战中遇到的坑

上面的代码看起来挺简单,但实际用起来还是会遇到一些问题。我把踩过的坑给你列出来,希望能帮你省点时间。

坑1:图片格式不对

有时候你从图库选的图片,格式可能不是 JPEG/JPG/PNG,或者颜色格式不是 RGBA_8888,这时候就会报错。

解决方法:在创建 PixelMap 之前,先检查图片格式,如果不支持就提示用户重新选择。

// 检查图片格式
const imageInfo = await imageSource.getImageInfo();
if (imageInfo.size.width < 100 || imageInfo.size.height < 100) {
  this.recognizedText = '图片尺寸过小,请选择 100px 以上的图片';
  return;
}

坑2:拍摄角度太大

如果你选的图片拍摄角度超过 30 度,识别准确率会明显下降。这时候可以在 UI 上加个提示,引导用户重新拍摄。

// 在识别结果为空时提示
if (!result || result.length === 0) {
  this.recognizedText = '未识别到文字,可能原因:\n1. 图片中没有文字\n2. 拍摄角度过大(建议小于30度)\n3. 光照不足或背景复杂';
}

坑3:内存泄漏

PixelMap 是个需要手动释放的资源,如果你忘了调用 pixelMap.release(),会导致内存泄漏。特别是在循环处理多张图片时,这个问题会更明显。

建议在 try-catch-finally 里统一处理资源释放:

let pixelMap: image.PixelMap | null = null;
try {
  // ... 识别逻辑
} catch (error) {
  // ... 错误处理
} finally {
  if (pixelMap) {
    pixelMap.release();
  }
}

坑4:模拟器不支持

这个最坑,我在模拟器上测试了半天,一直报错,后来才发现官方文档明确说了"该能力当前不支持模拟器"。

所以记得用真机测试,别在模拟器上浪费时间。


六、进阶玩法:获取文字位置信息

除了文字内容,你还能获取每个字、每行、每段在图片上的坐标位置。这个功能在做文字标注、文字高亮等场景特别有用。

// 获取文字位置信息
if (result && result.length > 0) {
  result.forEach((block: textRecognition.TextBlock, blockIndex: number) => {
    console.log(`段落 ${blockIndex + 1}: ${block.value}`);

    block.lines.forEach((line: textRecognition.TextLine, lineIndex: number) => {
      console.log(`${lineIndex + 1}: ${line.value}`);

      // 获取行的外框坐标
      const corners = line.cornerPoints;
      console.log(`    坐标: (${corners[0].x}, ${corners[0].y}) -> (${corners[2].x}, ${corners[2].y})`);

      line.words.forEach((word: textRecognition.TextWord, wordIndex: number) => {
        console.log(`    单词 ${wordIndex + 1}: ${word.value}`);
      });
    });
  });
}

七、性能优化建议

如果你的应用需要频繁调用文字识别功能,可以考虑下面这些优化点:

1. 关闭朝向检测

如果你确定图片都是正向的,可以把 isDirectionDetectionSupported 设为 false,能提升识别速度。

const config: textRecognition.TextRecognitionConfiguration = {
  isDirectionDetectionSupported: false  // 已知图片正向,关闭朝向检测
};

2. 图片预处理

在识别前对图片进行预处理,比如调整大小、增强对比度、去噪等,能提升识别准确率。

// 调整图片大小
const targetWidth = 1920;
const targetHeight = 1080;
const scaledPixelMap = await pixelMap.scale(targetWidth, targetHeight);

3. 批量处理

如果有多张图片需要识别,可以考虑批量处理,减少重复的初始化开销。


八、总结

HarmonyOS 的文字识别能力,说实话,在大多数场景下已经够用了。特别是对于印刷体识别,准确率还挺高的。而且它是系统级能力,不用依赖第三方服务,也不用担心隐私问题。

但也有一些局限性,比如手写体识别能力有限,不支持模拟器测试,这些你在做技术选型时得考虑清楚。

如果你在做发票识别、文档扫描、翻译应用这类功能,HarmonyOS 的文字识别能力值得试试。至少比用第三方 OCR 服务省不少事。

有问题的话,欢迎在评论区交流,我看到了都会回复。

Logo

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

更多推荐