HarmonyOS 相机 + 文字识别实现拍照识字 32 ImageReceiver 图像接收器
32 ImageReceiver 图像接收器
引言
上一篇文章中,"拍照识别文字"工程走的是 PhotoOutput 拍照链路:按下快门 → photoAvailable 回调 → 取出 JPEG Buffer → 交给 OCR 识别。这是一条"一拍一张、按需处理"的链路。但很多场景需要的是"连续帧":对准名片自动识别、扫码、实时取词翻译——此时不可能一秒钟按几十次快门,而是要把相机输出的每一帧图像都接住并处理。
接住连续帧的组件就是本文的主角:image.ImageReceiver(图像接收器)。它在"拍照识别文字"工程里同样存在(Camera.ets 的 receiver 字段),本文就沿着这个真实工程讲清楚 ImageReceiver 的创建、绑定、事件与读取,并给出"逐帧识别"的完整示例。

正文知识点
createImageReceiver:创建接收器
let receiver: image.ImageReceiver =
image.createImageReceiver(width, height, format, capacity);
四个参数的含义(源码参考:entry/src/main/ets/common/constants/CommonConstants.ets 中的常量定义):
| 参数 | 工程取值 | 含义 |
| width | IMAGE_RECEIVER_WIDTH = 640 |
接收图像流的宽,决定了每帧图像分辨率 |
| height | IMAGE_RECEIVER_HEIGHT = 480 |
接收图像流的高 |
| format | image.ImageFormat.YUV420_SP(常用) |
图像格式,也常用 JPEG |
| capacity | IMAGE_RECEIVER_CAPACITY = 8 |
缓冲队列容量:最多缓存多少帧 |
工程里 640×480 的选择很讲究:OCR 官方建议输入 720p 以上,但那是针对"拍照大图";对逐帧识别而言,帧率远比单帧分辨率重要,640×480 既能保证文字可读,又能显著压低识别耗时与内存,让"每帧都来得及处理"。capacity = 8 意味着帧产生速度超过消费速度时,最多缓冲 8 帧,再新到的帧会覆盖旧帧——容量越大越不容易丢帧,但内存占用越高。
getReceivingSurfaceId:把 Surface 交给相机
ImageReceiver 本身并不产生图像,它只是提供一块"画布"。相机要往里画帧,需要拿到接收器的 Surface ID 并把它作为输出目标:
let surfaceId: string = await receiver.getReceivingSurfaceId();
工程中封装成了专门的方法(源码参考:entry/src/main/ets/common/utils/Camera.ets):
async getImageReceiverSurfaceId(receiver: image.ImageReceiver): Promise<string | undefined> {
let photoSurfaceId: string | undefined = undefined;
if (receiver !== undefined) {
photoSurfaceId = await receiver.getReceivingSurfaceId();
Logger.info(TAG, `getReceivingSurfaceId success`);
}
return photoSurfaceId;
}
拿到 surfaceId 后,既可以 cameraManager.createPreviewOutput(profile, surfaceId) 把预览画面"重定向"到接收器,也可以作为 PhotoOutput 的拍照目标(createPhotoOutput(profile, surfaceId) 重载)。XComponent 之所以能显示相机预览,也是同一个原理:xcomponentController.getXComponentSurfaceId() 拿到的是 XComponent 的 Surface ID。
imageArrival 事件:帧到了
监听图像到达有两种姿势:
// 姿势一:事件订阅,回调里直接拿 Image(AsyncCallback 形式)
receiver.on('imageArrival', (err: BusinessError, image: image.Image) => {
// 处理这一帧,用完必须 image.release()
});
// 姿势二:事件只做"通知",帧用 readNextImage 主动取
receiver.on('imageArrival', () => {
receiver.readNextImage().then((img: image.Image) => { /* ... */ });
});
官方建议:readNextImage 必须在 imageArrival 回调触发后再调用才能正确取到数据;同时存在一个 readLatestImage(),它永远取缓冲队列里最新的一帧,适合"只关心最新画面"的场景(如实时识别),而 readNextImage 按队列顺序取帧,适合"每一帧都要处理"的场景(如录像)。
帧的处理与释放
ImageReceiver 里取出的 image.Image 与普通图片不同,它是对"流内帧"的引用:
img.getComponent(image.ComponentType.JPEG, (err, component) => {
let buffer = component.byteBuffer; // 这一帧的编码数据
// buffer 交给 OCR / 保存 / 编码……
});
img.release(); // 铁律:每帧用完必须 release,否则队列被占满、不再投递新帧
这和工程 photoAvailable 中 photo.main 的处理如出一辙:取 Component、拿 byteBuffer、用完释放。区别是 photo.main 是一次性照片,而 ImageReceiver 的帧会源源不断到来,每一帧都要释放,漏一帧就少一个缓冲位。
与 PhotoOutput 的职责划分
| 维度 | PhotoOutput(拍照) | ImageReceiver(帧流) |
| 触发方式 | capture() 主动拍照 |
imageArrival 被动收帧 |
| 数据粒度 | 单张照片 | 连续帧 |
| 典型用途 | 高分辨率成片、本文工程的 OCR 拍照 | 实时识别、扫码、预览分析 |
| 释放对象 | photo.main |
每帧 Image.release() |
代码示例
基于工程的相机结构,给出"逐帧 OCR 识别"的完整示例:创建接收器 → 绑定到会话 → 帧到达即识别。
// 源码参考:entry/src/main/ets/common/utils/Camera.ets、CommonConstants.ets
import { camera } from '@kit.CameraKit';
import { image } from '@kit.ImageKit';
import { textRecognition } from '@kit.CoreVisionKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import CommonConstants from '../constants/CommonConstants';
export class FrameOcrHelper {
private receiver: image.ImageReceiver | undefined = undefined;
private previewOutput: camera.PreviewOutput | undefined = undefined;
private session: camera.PhotoSession | undefined = undefined;
private recognizing = false; // 防重入:上一帧识别未完不处理下一帧
// 1. 创建接收器(640×480,YUV 格式,缓冲 8 帧)
private createReceiver(): void {
this.receiver = image.createImageReceiver(
CommonConstants.IMAGE_RECEIVER_WIDTH,
CommonConstants.IMAGE_RECEIVER_HEIGHT,
image.ImageFormat.YUV420_SP,
CommonConstants.IMAGE_RECEIVER_CAPACITY);
}
// 2. 把接收器作为输出加入会话,并注册帧到达监听
async start(manager: camera.CameraManager, device: camera.CameraDevice): Promise<void> {
this.createReceiver();
const surfaceId = await this.receiver.getReceivingSurfaceId(); // 工程同名方法
// 预览画面输出到接收器(而非 XComponent),帧随预览实时流入
const capability = manager.getSupportedOutputCapability(device, camera.SceneMode.NORMAL_PHOTO);
this.previewOutput = manager.createPreviewOutput(capability.previewProfiles[0], surfaceId);
this.session = manager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
this.session.beginConfig();
this.session.addInput(manager.createCameraInput(device));
this.session.addOutput(this.previewOutput);
await this.session.commitConfig();
await this.session.start();
// 3. 帧到达:读取 → 识别 → 释放,形成消费闭环
this.receiver.on('imageArrival', () => {
if (this.recognizing) {
return; // 上一帧还没识别完,丢弃本帧,避免队列积压
}
this.recognizing = true;
this.receiver?.readNextImage().then(async (frame: image.Image) => {
try {
frame.getComponent(image.ComponentType.JPEG, async (err: BusinessError, comp: image.Component) => {
if (err || comp === undefined) {
return;
}
const text = await this.recognizeFrame(comp.byteBuffer); // 复用工程的识别逻辑
hilog.info(0x0000, 'FrameOcr', `frame text: ${text}`);
});
} finally {
frame.release(); // 铁律:每帧必须释放
this.recognizing = false;
}
});
});
}
// 4. 复用工程的识别管线(简化,详见第 34 篇)
private async recognizeFrame(buffer: ArrayBuffer): Promise<string> {
const source = image.createImageSource(buffer);
const pixelMap = await source.createPixelMap();
const visionInfo: textRecognition.VisionInfo = { pixelMap };
const config: textRecognition.TextRecognitionConfiguration = {
isDirectionDetectionSupported: true
};
try {
const result = await textRecognition.recognizeText(visionInfo, config);
return result.value;
} finally {
pixelMap.release();
source.release();
}
}
// 5. 停止:释放接收器(与工程 releaseCamera 一致)
async stop(): Promise<void> {
await this.previewOutput?.release();
await this.session?.release();
if (this.receiver) {
await this.receiver.release(); // 源码参考:Camera.ets releaseCamera
}
}
}
运行效果与注意事项
- 必须配消费端:接收器只是缓冲队列,没有人
readNextImage/readLatestImage消费,队列很快被写满,新帧将无法投递,imageArrival也不再触发; - 每帧必 release:
Image.release()漏调,队列占满后表现是"画面卡住、事件消失",且不报错,极难排查; - 防重入设计:OCR 是耗时操作,一帧没处理完下一帧就到,必须用
recognizing标志做互斥,否则内存与耗时双双失控; - 分辨率与 capacity 是权衡:工程 640×480 + capacity 8 的组合适合逐帧识别;分辨率越高单帧越清晰但耗时越长,容量越大越流畅但内存越高,建议按"单帧处理耗时 < 1/期望帧率"来配置;
- 与拍照链路共存:ImageReceiver 也可以与
PhotoOutput同时挂在会话上——接收器负责实时帧分析,拍照输出负责留存高清原图,互不干扰,这正是很多扫描类 App 的做法。
总结
image.ImageReceiver 是鸿蒙相机输出体系中"面向连续帧"的组件:createImageReceiver 定义画布与缓冲,getReceivingSurfaceId 把 Surface 交给相机,imageArrival 通知帧的到来,readNextImage/readLatestImage 消费帧。工程中它作为 receiver 字段存在、在 releaseCamera 中被释放,常量 640×480×8 定义了逐帧识别的平衡点。与拍照链路的本质区别只有一条:照片是一次性的,帧是持续的,每帧必须释放。理解了这一点,拍照识别与实时识别两条链路就能在同一工程里自由切换。
更多推荐



所有评论(0)