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

完整代码: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,实现了一个端侧快递地址图片识别与自动填充工具。整个过程无需申请敏感权限,保护用户隐私的同时大幅提升输入效率。可在此基础上扩展至名片识别、发票识别等更多场景。

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

相关推荐
世人万千丶25 分钟前
鸿蒙Flutter Flex多子组件权重分配
学习·flutter·华为·harmonyos·鸿蒙
拥抱太阳06164 小时前
HarmonyOS 应用开发《掌上英语》第23篇-图标与颜色系统构建可主题化视觉体系
华为·harmonyos
红烧大青虫5 小时前
HarmonyOS应用开发实战:小事记 - UIAbility 的冷启动/热启动/后台启动三种场景与 launchParam 解析
后端·华为·harmonyos·鸿蒙系统
b130538100496 小时前
HarmonyOS应用开发实战:小事记 - 多级页面路由的 back 逻辑与参数回传模式
华为·harmonyos·鸿蒙系统
MonkeyKing6 小时前
鸿蒙ArkTS Text组件全解析:全属性详解+富文本实战
harmonyos
花开彼岸天~6 小时前
鸿蒙原生开发手记:徒步迹 - 自定义组件开发规范
后端·华为·harmonyos·鸿蒙系统
zSD55rt5a7 小时前
方差在扩散模型保护中的作用
人工智能·harmonyos
AD02278 小时前
HarmonyOS应用实战-启示散页-05-随机抽答案要避免连续重复:把算法放进服务层
harmonyos·arkts·鸿蒙开发
世人万千丶8 小时前
鸿蒙Flutter Flexible与Expanded的区别
学习·flutter·harmonyos·鸿蒙
红烧大青虫8 小时前
setInterval 倒计时实现:60s 验证码发送逻辑
后端·华为·harmonyos·鸿蒙系统