欢迎加入开源鸿蒙 PC社区

https://harmonypc.csdn.net/

效果截图

在这里插入图片描述

第6篇:百度OCR手写识别接入

系列教程导航

篇号 标题 状态
01 环境搭建与项目创建
02 数据模型与单词仓库
03 主入口页面与导航结构
04 极速划词页面实现
05 手写画布实现
06 百度OCR手写识别接入 本篇
07 答案比对与反馈UI 下一篇
08 单词切换与底部导航
09 词根分解与水印展示
10 项目总结与优化方向

源码仓库https://gitcode.com/qq_33247427/englishProject

一、为什么选择百度 OCR

1.1 华为端侧 OCR 的问题

HarmonyOS 提供了 @kit.CoreVisionKittextRecognition API,理论上可以在设备端完成文字识别,无需网络。但在实际开发中遇到了两个严重问题:

问题 错误信息 原因
服务未初始化 “The service is abnormal” 需要先调用 init()
识别超时 “Run timed out, please try again later” PixelMap 太大,端侧推理超时

即使加了 init() 和图片缩放,在部分真机上仍然不稳定。

1.2 百度手写 OCR 的优势
对比项 华为 CoreVisionKit 百度手写 OCR
网络依赖 无(端侧) 需要网络
稳定性 部分设备超时 稳定可靠
手写识别质量 一般 专门针对手写优化
首次使用 需下载模型 即用
免费额度 无限 每天 500 次(足够开发测试)
响应速度 不稳定 通常 1-2 秒
1.3 百度 OCR 申请步骤
  1. 注册 百度智能云账号
  2. 进入控制台 → 文字识别 → 创建应用
  3. 勾选「手写文字识别」能力
  4. 获取 API KeySecret Key

二、网络权限配置

百度 OCR 需要网络请求,必须在 module.json5 中声明权限:

{
  "module": {
    "requestPermissions": [
      { "name": "ohos.permission.INTERNET" }
    ]
  }
}

HarmonyOS 的 INTERNET 权限属于系统授权权限,声明即可使用,不需要动态申请。

三、BaiduOCRService 完整实现

3.1 文件结构

创建 electron/src/main/ets/services/BaiduOCRService.ets

import { image } from '@kit.ImageKit';
import { util } from '@kit.ArkTS';
import { http } from '@kit.NetworkKit';

const API_KEY = 'your_api_key_here';
const SECRET_KEY = 'your_secret_key_here';
let cachedToken: string = '';
3.2 获取 Access Token

百度 API 使用 OAuth 2.0 认证,需要先用 API Key + Secret Key 换取 Access Token:

async function getAccessToken(): Promise<string> {
  // Token 缓存,避免重复请求
  if (cachedToken) {
    return cachedToken;
  }

  const url = `https://aip.baidubce.com/oauth/2.0/token` +
    `?grant_type=client_credentials` +
    `&client_id=${API_KEY}` +
    `&client_secret=${SECRET_KEY}`;

  const req = http.createHttp();
  try {
    const resp = await req.request(url, {
      method: http.RequestMethod.POST
    });
    const data = JSON.parse(resp.result as string) as Record<string, string>;
    cachedToken = data['access_token'];
    console.log('BaiduOCR token 获取成功');
    return cachedToken;
  } finally {
    req.destroy();  // 必须销毁,否则内存泄漏
  }
}

关键点

  • Token 有效期 30 天,缓存后不需要每次都请求
  • http.createHttp() 创建的实例用完必须 destroy()
  • 生产环境应该把 Key 放在服务端,不要硬编码在客户端
3.3 PixelMap 转 Base64

百度 OCR 接受 Base64 编码的图片:

async function pixelMapToBase64(pixelMap: image.PixelMap): Promise<string> {
  const packer = image.createImagePacker();
  try {
    // 将 PixelMap 编码为 JPEG(比 PNG 小很多)
    const buffer = await packer.packing(pixelMap, {
      format: 'image/jpeg',
      quality: 90  // 90% 质量,平衡大小和清晰度
    });

    // ArrayBuffer → Uint8Array → Base64 字符串
    const helper = new util.Base64Helper();
    const uint8 = new Uint8Array(buffer);
    const b64 = helper.encodeToStringSync(uint8);
    return b64;
  } finally {
    packer.release();  // 释放 packer 资源
  }
}

为什么用 JPEG 而不是 PNG?

  • 手写内容是黑白线条,JPEG 90% 质量足够清晰
  • JPEG 文件通常比 PNG 小 3-5 倍
  • 上传更快,百度 API 有请求体大小限制(4MB)
3.4 英文手写识别
export async function baiduOCRRecognize(pixelMap: image.PixelMap): Promise<string> {
  try {
    const base64 = await pixelMapToBase64(pixelMap);
    const token = await getAccessToken();

    // 百度手写文字识别接口
    const url = `https://aip.baidubce.com/rest/2.0/ocr/v1/handwriting?access_token=${token}`;
    const body = `image=${encodeURIComponent(base64)}`;

    const req = http.createHttp();
    try {
      const resp = await req.request(url, {
        method: http.RequestMethod.POST,
        header: { 'Content-Type': 'application/x-www-form-urlencoded' },
        extraData: body
      });

      const result = JSON.parse(resp.result as string) as Record<string, Object>;
      const wordsResult = result['words_result'] as Array<Record<string, string>>;

      if (wordsResult && wordsResult.length > 0) {
        // 清洗结果:只保留英文字母和空格
        const tokens = wordsResult
          .map((w: Record<string, string>) =>
            w['words'].replace(/[^a-zA-Z\s]/g, '').trim().toLowerCase())
          .filter((s: string) => s.length > 0);

        if (tokens.length === 0) return '';

        // 去重(水印和手写可能被重复识别)
        const seen = new Set<string>();
        const unique: string[] = [];
        for (const t of tokens) {
          if (!seen.has(t)) {
            seen.add(t);
            unique.push(t);
          }
        }
        return unique.join(' ').trim();
      }
      return '';
    } finally {
      req.destroy();
    }
  } catch (e) {
    console.error('BaiduOCR 识别失败:', JSON.stringify(e));
    return '';
  }
}
3.5 中文手写识别
export async function baiduOCRRecognizeChinese(pixelMap: image.PixelMap): Promise<string> {
  try {
    const base64 = await pixelMapToBase64(pixelMap);
    const token = await getAccessToken();
    const url = `https://aip.baidubce.com/rest/2.0/ocr/v1/handwriting?access_token=${token}`;
    const body = `image=${encodeURIComponent(base64)}`;

    const req = http.createHttp();
    try {
      const resp = await req.request(url, {
        method: http.RequestMethod.POST,
        header: { 'Content-Type': 'application/x-www-form-urlencoded' },
        extraData: body
      });

      const result = JSON.parse(resp.result as string) as Record<string, Object>;
      const wordsResult = result['words_result'] as Array<Record<string, string>>;

      if (wordsResult && wordsResult.length > 0) {
        // 只保留中文字符
        return wordsResult
          .map((w: Record<string, string>) =>
            w['words'].replace(/[^\u4e00-\u9fa5]/g, '').trim())
          .filter((s: string) => s.length > 0)
          .join('');
      }
      return '';
    } finally {
      req.destroy();
    }
  } catch (e) {
    console.error('BaiduOCR 中文识别失败:', JSON.stringify(e));
    return '';
  }
}

四、在页面中调用

4.1 导入服务
import { componentSnapshot } from '@kit.ArkUI';
import { image } from '@kit.ImageKit';
import { baiduOCRRecognize, baiduOCRRecognizeChinese } from '../services/BaiduOCRService';
4.2 doRecognize 方法
async doRecognize() {
  if (this.isRecognizing || this.currentWord === null) {
    return;
  }
  this.isRecognizing = true;
  this.feedbackText = '识别中…';
  this.feedbackColor = '#6B7280';

  try {
    // 1. 截取画布组件为 PixelMap
    const pixelMap: image.PixelMap = await componentSnapshot.get('speedDictCanvas');

    // 2. 调用百度 OCR
    const text = await baiduOCRRecognize(pixelMap);

    // 3. 比对答案
    this.recognizedText = text;
    this.checkAnswer(text);
  } catch (e) {
    const err = e as Record<string, string>;
    this.feedbackText = '识别失败:' + (err['message'] ?? '');
    this.feedbackColor = '#B5533C';
  }

  this.isRecognizing = false;
}
4.3 Loading 状态

识别过程需要 1-2 秒,用 @State isRecognizing 控制按钮状态:

Button() {
  Row({ space: 4 }) {
    if (this.isRecognizing) {
      LoadingProgress()
        .width(14).height(14).color('#FFFFFF')
    }
    Text(this.isRecognizing ? '识别中' : '识别')
      .fontSize(13).fontColor('#FFFFFF')
  }
}
.enabled(!this.isRecognizing)  // 识别中禁用按钮

五、API 响应格式

百度手写 OCR 返回的 JSON 格式:

{
  "log_id": 1234567890,
  "words_result_num": 2,
  "words_result": [
    { "words": "apple" },
    { "words": "Apple" }
  ]
}
  • words_result:识别到的文字块数组
  • 每个块的 words 字段是识别出的文字
  • 手写体可能被分成多个块(多行书写)
  • 水印文字也可能被识别到(需要去重)

六、结果清洗策略

6.1 为什么需要清洗

百度 OCR 会识别画布上所有可见文字,包括:

  • 用户手写的内容(我们需要的)
  • 水印文字(需要过滤或去重)
  • 误识别的噪点
6.2 清洗流程
原始结果 → 去除非字母字符 → 转小写 → 去空格 → 去重 → 拼接

示例:

输入: ["Apple", "apple", "app le"]
处理: ["apple", "apple", "app le"] → 去非字母 → ["apple", "apple", "apple"]
去重: ["apple"]
输出: "apple"

七、错误处理

7.1 常见错误
错误码 含义 处理方式
110 Access Token 无效 清除缓存重新获取
216201 图片为空 提示用户先书写
17 每日调用量超限 提示明天再试
网络错误 无网络 提示检查网络
7.2 容错代码
try {
  const text = await baiduOCRRecognize(pixelMap);
  // ...
} catch (e) {
  const err = e as Record<string, string>;
  this.feedbackText = '识别失败:' + (err['message'] ?? '网络异常');
  this.feedbackColor = '#B5533C';
}

八、本篇小结

通过本篇教程,我们完成了:

  • 理解了选择百度 OCR 的原因(端侧 OCR 不稳定)
  • 完成了百度 OCR 服务申请和权限配置
  • 实现了 BaiduOCRService(Token 缓存 + PixelMap 转 Base64 + API 调用)
  • 掌握了 componentSnapshot 截图 + OCR 的完整链路
  • 实现了识别结果清洗和去重
  • 处理了 Loading 状态和错误情况

下一篇预告

第 7 篇:答案比对与反馈 UI — 我们将实现识别结果与正确答案的比对逻辑,以及画布上的大字体反馈浮层。

Logo

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

更多推荐