36 图像预处理与裁剪:AnalyzingPage 中的图片处理

前言

在这里插入图片描述

图:36 图像预处理与裁剪:AnalyzingPage 中的图片处理 运行效果截图(HarmonyOS NEXT)

拍摄或选择图片后,应用通常需要对原始图片进行预处理——裁剪、缩放、格式转换等。这既是 UI 展示的需要,也是后续 AI 分析(OCR、特征提取)的前置条件。

本文以"鹿鹿·笔迹心理分析"项目中 [CapturePage.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/CapturePage.ets) 和 [AnalyzingPage.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/AnalyzingPage.ets) 之间的图片传递链路为例,解析鸿蒙应用中图像的处理路径和优化策略。

鸿蒙官方·图像处理文档:developer.huawei.com
项目源码仓库:harmony-app GitHub

图像预处理链路示意图

图:图像处理管道——原始图片 → ImageSource → PixelMap → 裁剪/缩放 → 编码输出

原始图片
Camera / Album

image.createImageSource
解码图片

PixelMap
像素图对象

crop 裁剪
提取手写区域

scale 缩放
统一到 512x512

rotate 旋转
修正 EXIF 方向

ImagePacker 编码
压缩为 JPEG/PNG

写入本地文件
缓存目录

一、图像处理链路

1.1 数据流

拍照/相册选择
  ↓
原始图片路径(URI / 文件路径)
  ↓
图片解码(PixelMap)
  ↓
图片裁剪 / 缩放(可选)
  ↓
保存至缓存目录
  ↓
传入 AnalyzingPage
  ↓
OCR 识别 + 特征提取(AI 服务)

1.2 项目中的图片传递

// CapturePage — 确认使用照片
private confirmPhoto() {
  AppStorage.setOrCreate<string>('capture_image_path', this.previewPath)
  this.getUIContext().getRouter().pushUrl({
    url: 'pages/AnalyzingPage',
    params: { imagePath: this.previewPath }
  })
}

// AnalyzingPage — 接收图片
private async startAnalysis() {
  const imagePath = AppStorage.get<string>('capture_image_path') ?? ''
  const source = AppStorage.get<string>('capture_source') ?? 'camera'
  // ...
}

二、图片解码:PixelMap

2.1 从文件加载 PixelMap

import { image } from '@kit.ImageKit'

async function loadPixelMap(filePath: string): Promise<image.PixelMap> {
  const source = image.createImageSource(filePath)
  const pixelMap = await source.createPixelMap({
    desiredPixelFormat: image.ImagePixelFormat.RGBA_8888,
    desiredSize: { width: 1080, height: 1920 },
    fitDensity: 0
  })
  source.release()
  return pixelMap
}

参数说明:

参数 作用 推荐值 说明
desiredPixelFormat 像素格式 RGBA_8888 最高质量
desiredSize 目标尺寸 1080x1920 限制最大分辨率
fitDensity 密度适配 0 不需要适配

2.2 从 PixelMap 保存为文件

async function savePixelMap(pixelMap: image.PixelMap, outputPath: string): Promise<void> {
  const packer = image.createImagePacker()
  const packedImage = await packer.packing(pixelMap, {
    format: 'image/jpeg',
    quality: 85
  })
  const file = fs.openSync(outputPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
  file.writeSync(packedImage.data.buffer)
  fs.closeSync(file)
  packer.release()
}

三、图片裁剪

3.1 裁剪功能

async function cropImage(
  sourcePath: string,
  outputPath: string,
  region: image.Region
): Promise<void> {
  const source = image.createImageSource(sourcePath)
  const pixelMap = await source.createPixelMap({
    cropRegion: region,      // 裁剪区域
    desiredPixelFormat: image.ImagePixelFormat.RGBA_8888,
  })

  // 保存裁剪后的图片
  const packer = image.createImagePacker()
  const packedImage = await packer.packing(pixelMap, {
    format: 'image/jpeg',
    quality: 85
  })
  const file = fs.openSync(outputPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
  file.writeSync(packedImage.data.buffer)
  fs.closeSync(file)

  source.release()
  packer.release()
}

// 使用示例
image.Region = {
  x: 50, y: 100,           // 左上角坐标
  width: 800, height: 1000 // 裁剪尺寸
}
await cropImage(inputPath, outputPath, region)

四、图片缩放

4.1 缩放到指定尺寸

async function resizeImage(
  sourcePath: string,
  outputPath: string,
  maxWidth: number,
  maxHeight: number
): Promise<void> {
  const source = image.createImageSource(sourcePath)

  // 获取原始尺寸
  const info = await source.getImageInfo(0) as {
    size?: { width: number, height: number }
  }
  const srcWidth = info.size?.width ?? 1920
  const srcHeight = info.size?.height ?? 1080

  // 等比缩放计算
  let targetWidth = srcWidth
  let targetHeight = srcHeight
  if (targetWidth > maxWidth) {
    targetHeight = Math.round(targetHeight * maxWidth / targetWidth)
    targetWidth = maxWidth
  }
  if (targetHeight > maxHeight) {
    targetWidth = Math.round(targetWidth * maxHeight / targetHeight)
    targetHeight = maxHeight
  }

  // 创建缩放的 PixelMap
  const pixelMap = await source.createPixelMap({
    desiredSize: { width: targetWidth, height: targetHeight },
    desiredPixelFormat: image.ImagePixelFormat.RGBA_8888,
  })

  // 保存
  const packer = image.createImagePacker()
  const packedImage = await packer.packing(pixelMap, {
    format: 'image/jpeg',
    quality: 80
  })
  const file = fs.openSync(outputPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
  file.writeSync(packedImage.data.buffer)
  fs.closeSync(file)

  source.release()
  packer.release()
}

五、图像旋转与方向修正

5.1 EXIF 方向处理

拍摄的图片可能包含 EXIF 方向信息,需要修正:

async function correctOrientation(filePath: string): Promise<void> {
  const source = image.createImageSource(filePath)

  // 读取图像属性
  const info = await source.getImageInfo(0)
  const orientation = (info as Record<string, Object>)['exifOrientation'] as number ?? 0

  // 根据方向旋转
  let rotation = 0
  switch (orientation) {
    case 3: rotation = 180; break
    case 6: rotation = 90; break
    case 8: rotation = 270; break
  }

  if (rotation > 0) {
    // 如需旋转,重新生成 PixelMap 并保存覆盖原文件
    // ...
  }
  source.release()
}

六、AnalyzingPage 中的图片使用

6.1 页面入口

// AnalyzingPage.ets
aboutToAppear() {
  this.startAnalysis()
  // ...
}

private async startAnalysis() {
  const imagePath = AppStorage.get<string>('capture_image_path') ?? ''
  const source = AppStorage.get<string>('capture_source') ?? 'camera'

  // 模拟 5 层分析进度
  for (let i = 0; i < 5; i++) {
    this.currentStep = i
    await new Promise<void>(resolve => setTimeout(() => resolve(), 800))
  }

  // 生成模拟推理结果
  const result = this.generateMockResult(source, archiveId)

  // 写入 DB
  await HandwritingDao.create({ /* ... */ })
  await ReportDao.create({ /* ... */ })

  // 跳转到报告详情页
  this.getUIContext().getRouter().replaceUrl({
    url: 'pages/ReportDetailPage',
    params: { imagePath, reportId }
  })
}

6.2 图片路径传递方式对比

方式 项目使用 优点 缺点
文件路径 previewPath 简单直接,可跨页面传递 文件可能被清理
AppStorage ✅ 全局存储 跨页面共享,不依赖路由参数 需要手动清理
路由参数 ✅ pushUrl params 与导航绑定 仅适用于相邻页面

七、图片处理的常见问题

问题 原因 解决方案
图片方向不对 EXIF 方向未处理 correctOrientation() 修正
图片太大导致 OOM 原始分辨率过高 createPixelMap 时指定 desiredSize
JPEG 压缩质量过低 设置不当 quality: 80-85 平衡质量与文件大小
处理耗时导致 ANR 同步操作 全部使用 async/await 异步 API

八、图像处理 API 汇总

API 模块 用途 是否异步
image.createImageSource(path) @kit.ImageKit 创建图片源
source.createPixelMap(options) @kit.ImageKit 解码为 PixelMap
image.createImagePacker() @kit.ImageKit 编码为文件格式
packer.packing(pixelMap, options) @kit.ImageKit 压缩编码
fs.openSync(path, mode) @kit.CoreFileKit 打开文件 ❌(无同步版本)
file.writeSync(data) @kit.CoreFileKit 写入数据

总结

本文解析了鸿蒙应用中图像预处理到 AI 分析的完整链路:

  1. 图片传递:通过文件路径 + AppStorage + 路由参数 3 种方式跨页面传递
  2. PixelMap 解码image.createImageSource()createPixelMap() 加载图片
  3. 裁剪与缩放cropRegion 裁剪区域,desiredSize 控制目标尺寸
  4. 方向修正:读取 EXIF orientation,90/180/270 度旋转
  5. 输出编码ImagePacker.packing() 输出为 JPEG 文件
  6. 分析链路:图片路径 → AnalyzingPage → 模拟推理 → 写入 DB → 报告详情

下一篇文章将介绍 相册读取与图片选择——PhotoViewPicker 的完整使用。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


参考资源:


七、图像处理性能优化

7.1 异步处理避免 UI 阻塞

图像解码和压缩是 CPU 密集型操作,必须在异步上下文中执行:

// ✅ 正确:在 async 函数中执行,不阻塞 UI
private async processImage(imagePath: string): Promise<string> {
  // 1. 解码图片(耗时操作)
  const imageSource = image.createImageSource(imagePath)
  const pixelMap = await imageSource.createPixelMap({
    desiredSize: { width: 1024, height: 1024 },
    desiredSamplingQuality: image.SamplingQuality.MEDIUM
  })

  // 2. 压缩编码(耗时操作)
  const packer = image.createImagePacker()
  const packOptions: image.PackingOption = { format: 'image/jpeg', quality: 80 }
  const arrayBuffer = await packer.packing(pixelMap, packOptions)

  // 3. 写入文件
  const outputPath = `${getContext().cacheDir}/processed_${Date.now()}.jpg`
  const file = fs.openSync(outputPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY)
  fs.writeSync(file.fd, arrayBuffer)
  fs.closeSync(file)

  // 4. 释放 PixelMap 内存
  pixelMap.release()
  return outputPath
}

7.2 PixelMap 内存管理

PixelMap 占用大量内存,使用后必须及时释放:

let pixelMap: image.PixelMap | null = null
try {
  pixelMap = await imageSource.createPixelMap(options)
  // ... 处理图片
} finally {
  if (pixelMap) {
    pixelMap.release()  // 释放 native 内存
    pixelMap = null
  }
}

7.3 图像处理性能指标

操作 耗时参考(4000x3000 图片) 优化建议
解码(原始) 200-500ms 设置 desiredSize 减小尺寸
裁剪 < 50ms PixelMap.crop() 原地裁剪
缩放 50-150ms desiredSize 在解码时同步缩放
JPEG 编码(quality=80) 100-300ms 适当降低 quality 提速

7.4 图像处理工具函数封装

// 项目中封装的图像处理工具
export class ImageUtils {
  // 解码图片为 PixelMap(带缩放)
  static async decodeImage(path: string, maxSize: number = 1024): Promise<image.PixelMap> {
    const source = image.createImageSource(path)
    return source.createPixelMap({ desiredSize: { width: maxSize, height: maxSize } })
  }

  // 压缩 PixelMap 并写入文件
  static async compressToFile(pixelMap: image.PixelMap, outputPath: string, quality: number = 80): Promise<void> {
    const packer = image.createImagePacker()
    const buffer = await packer.packing(pixelMap, { format: 'image/jpeg', quality })
    const file = fs.openSync(outputPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY)
    fs.writeSync(file.fd, buffer)
    fs.closeSync(file)
  }
}

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐