一张照片不上传服务器,也能在手机上完成柔焦、灰度和反色。本文把这条链路做成一个可运行的滤镜台:先生成一张离线 PixelMap,再用 EffectKit 产生新的 PixelMap,最后在 Canvas 上切换预览。

先看真机结果

进入首页,点击“实验 25 · FILTERLAB 离线图像处理台”。页面先生成原图,状态条显示“原图已生成,可选择处理效果”。画布中的四个彩色圆和白色斜线就是本次实验的样张。

点击“柔焦”,EffectKit 在本地生成新 PixelMap,画面边缘变柔和,数据卡片显示处理次数。

点击“灰度”和“反色”,两种结果都直接替换画布中的预览,不需要重新创建页面。

点击“暂停预览”,动画进度停住并显示 PAUSED;再次点击即可恢复。退出按钮会释放源图、结果图和动画资源。

准备

  • DevEco Studio,API 26 工程。
  • 已连接并解锁的 HarmonyOS 真机;本次使用 HUAWEI Mate 60 Pro。
  • 页面文件:entry/src/main/ets/features/graphics2d/OfflineImageEffectsPage.ets

本实验使用的是内存中的样张,因此不申请相册或相机权限,适合先验证离线处理链路。接入真实照片时,只需把 source 换成解码得到的 PixelMap。

实施过程

1. 创建离线 PixelMap

先申请一块 RGBA 内存,再用 Canvas 画出可观察的测试图案:

const width: number = 520;
const height: number = 360;
const buffer: ArrayBuffer = new ArrayBuffer(width * height * 4);
this.source = await image.createPixelMap(buffer, {
  editable: true,
  pixelFormat: image.PixelMapFormat.RGBA_8888,
  size: { width, height }
});

const canvas: drawing.Canvas = new drawing.Canvas(this.source);
// 画背景、彩色圆和白色斜线,方便对比滤镜前后差异

FILTER_READY width=520 height=360 offline=true 表示源图已经在本机内存中生成。

2. 根据按钮创建效果

按钮只改变 modeIndex,真正的处理集中在 applyMode()

const filter: effectKit.Filter = effectKit.createEffect(this.source);
if (index === 1) {
  filter.blur(14).brightness(0.08);
} else if (index === 2) {
  filter.grayscale();
} else {
  filter.invert();
}
const output: image.PixelMap = await filter.getEffectPixelMap(true);
this.controller.setOutput(output);

getEffectPixelMap(true) 走 CPU 路径,返回一张新的 PixelMap;原图仍保留,便于复位到“原图”。真机日志中的 success=true cpu=true 是处理完成的依据。

3. 在 RenderNode 中绘制结果

RenderNode 每帧清屏后把当前结果画到画布中心,再绘制边框和进度条:

draw(context: DrawContext): void {
  const width: number = Math.max(1, Math.round(context.sizeInPixel.width));
  const canvas: drawing.Canvas = context.canvas;
  canvas.clear(0xFF081321);
  if (this.output) {
    canvas.drawImage(this.output, (width - 520) / 2, 72);
  }
  this.drawOverlay(canvas, width, context.sizeInPixel.height);
}

Canvas 只负责显示,滤镜计算仍由 EffectKit 完成,两部分职责分开后,切换效果不会影响页面控件。

4. 加入 30 FPS 预览控制

实验页用 Animator 更新进度条,同时保留暂停、继续和复位按钮:

this.animator = this.getUIContext().createAnimator({
  duration: 5000, easing: 'linear', iterations: -1, begin: 0, end: 1
});
this.animator.setExpectedFrameRateRange({ min: 30, max: 30, expected: 30 });
this.animator.onFrame = (value: number): void => {
  if (this.running && !this.released) {
    this.controller.setPhase(value);
  }
};

这里的动画只更新预览装饰,不重复做滤镜计算;点击效果按钮时才调用一次 getEffectPixelMap()

5. 退出时释放 PixelMap

源图和处理结果都属于原生资源,退出页面时统一释放:

this.animator?.cancel();
this.controller.release();
this.source = undefined;
this.animator = undefined;

FilterRenderNode.release() 会先释放输出图,再释放源图,最后销毁 RenderNode。日志中的 FILTER_RELEASE reason=EXIT_BUTTON 与退出截图对应。

遇到的情况与处理

处理结果没有变化

不要只修改按钮文字。必须用 effectKit.createEffect(source) 创建 Filter,并等待 getEffectPixelMap(true) 返回结果,再把返回值交给 RenderNode 的 setOutput()

切换效果后内存持续增长

每次生成的新 PixelMap 都要在下一次替换或退出时释放;原图不能和输出图重复释放。当前实现用引用比较避免了重复释放。

动画卡顿

滤镜生成放在按钮触发的异步函数中,Animator 只负责进度条。这样连续帧不会反复执行灰度、反色等计算。

真机 Hilog

包名、进程号和时间戳已删除,只保留关键字段:

FILTER_LAB_ENTER modes=original,blur,grayscale,invert permissionRequired=false
FILTER_READY width=520 height=360 offline=true
FILTER_MODE mode=1 name=柔焦
FILTER_APPLY mode=1 name=柔焦 success=true cpu=true
FILTER_MODE mode=2 name=灰度
FILTER_APPLY mode=2 name=灰度 success=true cpu=true
FILTER_MODE mode=3 name=反色
FILTER_APPLY mode=3 name=反色 success=true cpu=true
FILTER_PAUSE mode=3
FILTER_RESUME mode=3
FILTER_RELEASE reason=EXIT_BUTTON

验证结果

验证项真机结果
离线源图520 × 360 RGBA PixelMap 生成成功
柔焦blur + brightness 处理成功
灰度grayscale 处理成功
反色invert 处理成功
预览控制暂停、恢复、复位均可操作
资源释放退出日志记录 FILTER_RELEASE

本次文件

entry/src/main/ets/features/graphics2d/OfflineImageEffectsPage.ets
entry/src/main/ets/pages/Index.ets
articles/第二十三篇-ArkGraphics 2D离线图像处理实战/
docs/qa/filterlab-23/frames/
articles/第二十三篇-ArkGraphics 2D离线图像处理实战/真机验证日志.txt

GIF 使用 24 帧连续真机画面制作,规格为 720×610、8 FPS、3 秒;静态图保留完整界面,便于核对每个滤镜按钮和暂停状态。

Logo

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

更多推荐