鸿蒙实战:快递地址图片识别与自动填充

完整代码:SmartRecognitionDemo

在寄件、收件或填写订单信息时,手动输入姓名、电话、地址往往繁琐易错。本文将实现一个快递地址图片识别工具:用户上传一张包含地址信息的图片(如名片、快递单截图),系统自动识别文字,并从中智能提取姓名、电话号码、地址,自动填充到对应表单中。

一、最终效果

  • 选择本地图片或拍照,调用 Core Vision Kit 的文字识别(OCR)提取图中所有文字。
  • 利用 Natural Language Kit 的实体抽取 API,从文字中自动分离出姓名、电话号码、地址。
  • 将抽取结果自动填入姓名、电话、地址输入框,支持手动修正。
  • 所有图片获取均通过安全控件实现,无需申请存储或相机权限。

二、实现思路

  1. 安全控件选图/拍照 :使用 PhotoViewPicker 选择相册图片,或 CameraPicker 拍照,返回 PixelMap
  2. 文字识别 :初始化 textRecognition,调用 recognizeText 获取识别文本。
  3. 实体抽取 :调用 textProcessing.getEntity,指定 EntityType.NAMEPHONE_NOLOCATION,获取结构化信息。
  4. 表单填充 :将抽取结果更新到 @State 变量,界面自动刷新。

运行效果

不支持模拟器,图片中的收货信息是虚拟生成的,相册选图的视频删除只保留结果。

三、完整代码

1. 权限声明 (module.json5)

使用安全控件,不需要 申请 ohos.permission.READ_MEDIAohos.permission.CAMERA

2. 工具类:AddressRecognizer.ets

封装文字识别与实体抽取逻辑

javascript 复制代码
import { textRecognition } from '@kit.CoreVisionKit';
import { textProcessing, EntityType } from '@kit.NaturalLanguageKit';
import { image } from '@kit.ImageKit';
import { AddressInfo } from '../model/AddressInfo';

export class AddressRecognizer {
  private static isInit = false;

  static async init(): Promise<void> {
    if (AddressRecognizer.isInit) return;
    try {
      await textRecognition.init();
      AddressRecognizer.isInit = true;
      console.info('Text recognition init success');
    } catch (err) {
      console.error('Init failed', err);
      throw new Error(err);
    }
  }

  static async release(): Promise<void> {
    if (!AddressRecognizer.isInit) return;
    try {
      await textRecognition.release();
      AddressRecognizer.isInit = false;
    } catch (err) {
      console.error('Release failed', err);
    }
  }

  static async recognizeAndExtract(pixelMap: image.PixelMap): Promise<AddressInfo | null> {
    try {
      // 1. 文字识别

      let visionInfo: textRecognition.VisionInfo = {
        pixelMap: pixelMap
      };
      const config: textRecognition.TextRecognitionConfiguration = {
        isDirectionDetectionSupported: false
      };
      const result = await new Promise<textRecognition.TextRecognitionResult>((resolve, reject) => {
        textRecognition.recognizeText(visionInfo, config,  (error: BusinessError, data: textRecognition.TextRecognitionResult) => {
          if (data.value) {
           resolve(data);
          }else {
            reject(error)
          }
        });
      });
      const fullText = result.value;
      console.info('识别文本:', fullText);
      if (!fullText) return null;

      // 2. 实体抽取
      const entities = await textProcessing.getEntity(fullText, {
        entityTypes: [EntityType.NAME, EntityType.PHONE_NO, EntityType.LOCATION]
      });
      return AddressRecognizer.parseEntities(entities);
    } catch (err) {
      console.error('识别或抽取失败', err);
      return null;
    }
  }

  private static parseEntities(entities: textProcessing.Entity[]): AddressInfo {
    let name = '';
    let phone = '';
    let address = '';
    let detailAddress = '';

    for (const entity of entities) {
      if (entity.type === EntityType.NAME) {
        name = entity.text;
      } else if (entity.type === EntityType.PHONE_NO) {
        phone = entity.text;
      } else if (entity.type === EntityType.LOCATION) {
        // 简单处理:以"区"或"县"为界拆分为主地址和详细地址
        const parts = entity.text.split(/[区县]/);
        if (parts.length >= 2) {
          address = parts[0] + '区';
          detailAddress = parts.slice(1).join('');
        } else {
          address = entity.text;
          detailAddress = '';
        }
      }
    }
    return { name, phone, address, detailAddress };
  }
}

3. 图片选择工具:ImagePicker.ets

使用安全控件无需申请权限

javascript 复制代码
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { camera, cameraPicker } from '@kit.CameraKit';
import { image } from '@kit.ImageKit';
import { fileIo } from '@kit.CoreFileKit';

export class ImagePicker {
  static async pickFromGallery(): Promise<image.PixelMap | null> {
    try {
      const picker = new photoAccessHelper.PhotoViewPicker();
      const result = await picker.select({
        MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
      });
      if (result.photoUris.length === 0) return null;
      const uri = result.photoUris[0];
      const file = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY);
      const imageSource = image.createImageSource(file.fd);
      const pixelMap = await imageSource.createPixelMap();
      await fileIo.close(file.fd);
      return pixelMap;
    } catch (err) {
      console.error('选择图片失败', err);
      return null;
    }
  }

  // 使用 CameraPicker 拍照(安全控件,无需权限)
  static async takePhoto(context: Context): Promise<image.PixelMap | null> {
    try {
      const mediaTypes: Array<cameraPicker.PickerMediaType> = [cameraPicker.PickerMediaType.PHOTO];
      const pickerProfile: cameraPicker.PickerProfile = {
        cameraPosition:  camera.CameraPosition.CAMERA_POSITION_BACK
      };
      const pickerResult = await cameraPicker.pick(context, mediaTypes, pickerProfile);
      if (!pickerResult || pickerResult.resultCode !== 0) return null;
      const photoUri = pickerResult.resultUri;
      if (!photoUri) return null;
      const file = await fileIo.open(photoUri, fileIo.OpenMode.READ_ONLY);
      const imageSource = image.createImageSource(file.fd);
      const pixelMap = await imageSource.createPixelMap();
      await fileIo.close(file.fd);
      return pixelMap;
    } catch (err) {
      console.error('拍照失败', err);
      return null;
    }
  }
}

4. 主页面:Index.ets

javascript 复制代码
import { textRecognition } from '@kit.CoreVisionKit';
import { textProcessing, EntityType } from '@kit.NaturalLanguageKit';
import { image } from '@kit.ImageKit';
import { AddressInfo } from '../model/AddressInfo';

export class AddressRecognizer {
  private static isInit = false;

  static async init(): Promise<void> {
    if (AddressRecognizer.isInit) return;
    try {
      await textRecognition.init();
      AddressRecognizer.isInit = true;
      console.info('Text recognition init success');
    } catch (err) {
      console.error('Init failed', err);
      throw new Error(err);
    }
  }

  static async release(): Promise<void> {
    if (!AddressRecognizer.isInit) return;
    try {
      await textRecognition.release();
      AddressRecognizer.isInit = false;
    } catch (err) {
      console.error('Release failed', err);
    }
  }

  static async recognizeAndExtract(pixelMap: image.PixelMap): Promise<AddressInfo | null> {
    try {
      // 1. 文字识别

      let visionInfo: textRecognition.VisionInfo = {
        pixelMap: pixelMap
      };
      const config: textRecognition.TextRecognitionConfiguration = {
        isDirectionDetectionSupported: false
      };
      const result = await new Promise<textRecognition.TextRecognitionResult>((resolve, reject) => {
        textRecognition.recognizeText(visionInfo, config,  (error: BusinessError, data: textRecognition.TextRecognitionResult) => {
          if (data.value) {
           resolve(data);
          }else {
            reject(error)
          }
        });
      });
      const fullText = result.value;
      console.info('识别文本:', fullText);
      if (!fullText) return null;

      // 2. 实体抽取
      const entities = await textProcessing.getEntity(fullText, {
        entityTypes: [EntityType.NAME, EntityType.PHONE_NO, EntityType.LOCATION]
      });
      return AddressRecognizer.parseEntities(entities);
    } catch (err) {
      console.error('识别或抽取失败', err);
      return null;
    }
  }

  private static parseEntities(entities: textProcessing.Entity[]): AddressInfo {
    let name = '';
    let phone = '';
    let address = '';
    let detailAddress = '';

    for (const entity of entities) {
      if (entity.type === EntityType.NAME) {
        name = entity.text;
      } else if (entity.type === EntityType.PHONE_NO) {
        phone = entity.text;
      } else if (entity.type === EntityType.LOCATION) {
        // 简单处理:以"区"或"县"为界拆分为主地址和详细地址
        const parts = entity.text.split(/[区县]/);
        if (parts.length >= 2) {
          address = parts[0] + '区';
          detailAddress = parts.slice(1).join('');
        } else {
          address = entity.text;
          detailAddress = '';
        }
      }
    }
    return { name, phone, address, detailAddress };
  }
}

四、关键 API 说明

API 用途 安全性
PhotoViewPicker.select() 安全选择相册图片 无需权限,系统弹出选择界面
CameraPicker.pick() 安全拍照 无需相机权限,系统提供拍照界面
textRecognition.recognizeText() 识别图片中的文字 -
textProcessing.getEntity() 从文本中抽取实体 -

五、常见问题与优化建议

问题 解决方案
识别结果不准确 确保图片清晰、文字可读;可增加图片预处理
地址解析粒度不够细 parseEntities 中利用正则进一步拆分
首次调用耗时较长 在应用启动时提前初始化 textRecognition
多次识别后内存增大 确保 PixelMap 及时释放(pixelMap.release()

六、总结

本文利用鸿蒙的安全控件和 Core Vision Kit、Natural Language Kit,实现了一个端侧快递地址图片识别与自动填充工具。整个过程无需申请敏感权限,保护用户隐私的同时大幅提升输入效率。可在此基础上扩展至名片识别、发票识别等更多场景。

如果觉得本文对你有帮助,请点赞、收藏、转发支持!

相关推荐
G_dou_9 小时前
Flutter三方库适配OpenHarmony【countdown_timer】倒计时器项目完整实战
flutter·harmonyos
特立独行的猫a11 小时前
Tauri 应用移植到 OpenHarmony/鸿蒙PC完整指南
华为·rust·harmonyos·tauri·移植·鸿蒙pc
互联网散修11 小时前
鸿蒙实战:文字放大镜精确跟随手指放大
华为·harmonyos
金启攻14 小时前
【鸿蒙应用开发实战·食光篇】第二篇:首页与菜系导航——圆形封面与美食榜单
华为·harmonyos
JohnnyDeng9415 小时前
【鸿蒙】ArkUI 列表性能优化:LazyForEach 与组件复用深度解析
性能优化·harmonyos·arkts·鸿蒙·arkui
●VON16 小时前
AtomGit Flutter鸿蒙客户端:设置页面
flutter·华为·跨平台·harmonyos·鸿蒙
FrameNotWork16 小时前
HarmonyOS6.1 AI 模型管理架构设计与最佳实践
人工智能·harmonyos
wordbaby16 小时前
rn-cross-calendar:一个兼容 React 18/19、RN/RNOH 的跨平台日历组件
前端·react native·harmonyos
●VON17 小时前
AtomGit Flutter鸿蒙客户端:用户资料
flutter·华为·架构·跨平台·harmonyos·鸿蒙