想给照片加滤镜?用 effectKit 三行代码搞定

你有没有想过,手机相册里那些"复古"、“黑白”、"模糊"滤镜,底层到底是怎么实现的?

其实原理很简单:拿到图片的每一个像素,按照某种数学规则重新算一遍颜色值,就完事了。比如"灰度滤镜"就是把 RGB 三个通道的值加权平均,变成一个灰度值;"模糊滤镜"就是把每个像素和它周围的像素混合一下。

听起来好像要写很多代码?在 HarmonyOS 里,不用。effectKit 这个模块把这些图像效果都封装好了,你只需要:创建滤镜 → 添加效果 → 导出图片,三步。

今天我们就来做一个照片编辑 APP 的滤镜功能,看看怎么用 effectKit 给图片加模糊、调亮度、变灰度、做反转。

先认识一下 effectKit

effectKit 属于 ArkGraphics2D 这个 Kit,专门用来做离线图像处理。什么叫离线?就是你有一张图片(比如用户从相册选的),你想给它加个效果,处理完再展示出来。它不是实时渲染的那种——如果你要给屏幕上的组件加实时模糊,那是 uiEffect 干的事,不一样。

effectKit 提供了三个核心角色:

  • Filter:效果类,你可以往上面"挂"各种效果,比如模糊、灰度、亮度,它会把这些效果串成一条链,最后一次性应用到图片上。
  • ColorPicker:智能取色器,能从一张图片里提取主色调。这个我们下一篇再聊。
  • Color:颜色类,就是 RGBA 四个值,用来保存取色结果。

今天我们重点讲 Filter。

下面是 effectKit 滤镜处理的整体流程:

导入 effectKit 模块

获取图片的 PixelMap

createEffect 创建滤镜头节点

添加滤镜效果

需要叠加更多效果?

getEffectPixelMap 导出处理后图片

展示或保存结果

第一步:导入模块

import { effectKit } from "@kit.ArkGraphics2D";

一行搞定。不过后面我们还需要 ImageKit 来创建 PixelMap,以及 AbilityKit 来读取图片文件,所以一般会这样写:

import { image } from '@kit.ImageKit';
import { effectKit } from '@kit.ArkGraphics2D';
import { common } from '@kit.AbilityKit';

第二步:拿到一张图片的 PixelMap

在 HarmonyOS 里,图片不是直接用文件路径的,而是要用 PixelMap——你可以把它理解成"图片在内存里的表示"。不管用户给你的是一张 PNG、JPEG,还是一个 ArrayBuffer(比如从网络下载的),你都得先把它转成 PixelMap。

怎么转?用 image.createImageSource 创建一个图片源,再用 createPixelMap() 解码:

// 假设你已经有了图片的 ArrayBuffer 数据
let imageSource = image.createImageSource(Image);
let pixelMap = await imageSource.createPixelMap();

如果你的图片放在项目的 rawfile 文件夹里(比如 rawfile/image.png),读取方式是这样的:

async getFileBuffer(): Promise<ArrayBuffer | undefined> {
  try {
    const context: Context = this.getUIContext().getHostContext() as common.UIAbilityContext;
    const fileData: Uint8Array = await context.resourceManager.getRawFileContent('image.png');
    const buffer: ArrayBuffer = fileData.buffer.slice(0);
    return buffer;
  } catch (err) {
    return undefined;
  }
}

这里 getRawFileContent 返回的是 Uint8Array,我们要的是 ArrayBuffer,所以用 .buffer.slice(0) 转一下。为什么要 slice(0)?因为 Uint8Array.buffer 可能包含多余的数据(偏移量),slice(0) 会拷贝一份干净的 ArrayBuffer 出来。

第三步:创建 Filter 实例

有了 PixelMap,就可以创建 Filter 了:

let headFilter = effectKit.createEffect(pixelMap);

这行代码做了什么?它传入一张图片,返回一个 Filter 对象。这个 Filter 是链表的头节点——你可以把它想象成一根绳子的头,后面你可以往绳子上一个一个挂效果。

为什么要用链表?因为滤镜是可以叠加的。你可以先加模糊,再加灰度,再加反转,这些效果会按顺序依次应用。链表结构天然支持这种"串起来"的操作。

第四步:添加效果

模糊效果

headFilter.blur(5);

blur 方法接收一个 radius 参数,就是模糊半径,单位是像素。值越大,模糊效果越明显。打个比方,radius=5 就像隔着一层薄纱看照片,radius=50 就像隔着一层毛玻璃。

从 API version 14 开始,blur 还多了一个重载,可以指定平铺模式:

headFilter.blur(30, effectKit.TileMode.DECAL);

TileMode 控制的是图片边缘的模糊效果怎么处理。目前只支持 DECAL,意思是"只在图片原始边界内渲染",不会让边缘的模糊溢出到外面去。

亮度调节

headFilter.brightness(0.5);

bright 参数取值范围是 [0, 1]。0 表示不改变,1 表示达到预设的最大亮度。你可能会问:为什么不是 0-100 或者 0-255?因为这里用的是归一化值,方便和其他效果统一处理。

如果你觉得图片太暗了,传个 0.5-0.8 的值;如果想做"曝光过度"的特效,直接传 1。

灰度效果

headFilter.grayscale();

灰度效果不需要参数,它会把彩色图片变成黑白的。原理是把 RGB 三个通道的值按照人眼感知的权重(大约是 R:0.2126, G:0.7152, B:0.0722)加权平均。你会发现绿色通道的权重最大,因为人眼对绿色最敏感。

颜色反转

headFilter.invert();

也不需要参数。它会把每个像素的颜色值反转——黑色变白色,红色变青色,就像照片的"底片"效果。实现方式就是用 255 减去原来的值。

自定义颜色矩阵(高级玩法)

如果你觉得上面那些效果太"固定"了,想自己定义颜色变换规则,可以用 setColorMatrix

let colorMatrix: Array<number> = [
  0.2126, 0.7152, 0.0722, 0, 0,
  0.2126, 0.7152, 0.0722, 0, 0,
  0.2126, 0.7152, 0.0722, 0, 0,
  0, 0, 0, 1, 0
];
headFilter.setColorMatrix(colorMatrix);

这是一个 5x4 的矩阵。什么意思呢?它定义了"输出颜色 = 输入颜色 × 矩阵"的计算规则。

矩阵的每一行对应一个输出通道:第一行是输出的 R,第二行是 G,第三行是 B,第四行是 A。每一列对应一个输入通道:前四列分别是输入的 R、G、B、A,第五列是常量偏移。

上面这个矩阵其实就是灰度效果的数学表达——每一行都是同样的权重 0.2126, 0.7152, 0.0722,意思是输出的 R、G、B 都等于输入的灰度值。

你可以用这个矩阵做出各种神奇的效果。比如想让图片偏暖色调(增强红色),可以调大第一行的第一个值;想做旧照片效果,可以调整矩阵让整体偏黄。

矩阵元素取值范围是 [0, 1],0 表示该颜色通道不参与计算,1 表示保持原始权重。

第五步:效果叠加

滤镜链的叠加处理是按顺序依次执行的,下面是效果链的处理模型:

原始图片 PixelMap

第一个效果: brightness 调亮

第二个效果: blur 模糊

第三个效果: grayscale 灰度

getEffectPixelMap 触发实际处理

输出处理后的新 PixelMap

原图不受影响

前面说过,Filter 是链表结构,所以你可以连续调用多个效果方法,它们会串在一起:

let headFilter = effectKit.createEffect(pixelMap);
if (headFilter != null) {
  headFilter.brightness(0.3);  // 先调亮
  headFilter.blur(10);          // 再模糊
  headFilter.grayscale();       // 最后变灰度
}

效果的顺序很重要。先调亮再模糊,和先模糊再调亮,结果是不一样的。你可以把它想象成 PS 里的图层——从上往下依次执行。

第六步:导出处理后的图片

效果加完了,怎么拿到处理后的图片?用 getEffectPixelMap

headFilter.getEffectPixelMap().then(imageData => {
  // imageData 就是处理后的 PixelMap
  // 你可以把它赋值给 Image 组件显示出来
})

这个方法会按照你添加的效果顺序,依次对图片进行处理,最后返回一个新的 PixelMap。注意,原图不会被修改,它返回的是一张新的图片。

从 API version 20 开始,getEffectPixelMap 还支持指定渲染模式:

headFilter.getEffectPixelMap(false);  // false = GPU渲染

默认是 CPU 渲染(true),GPU 渲染在处理大图片时会更快。但目前 GPU 渲染还在实验阶段,生产环境建议还是用 CPU。

把它们组合起来:完整的滤镜页面

好,现在我们把所有步骤串起来,做一个完整的 ArkUI 页面。用户打开页面,看到一张加了模糊效果的图片:

import { image } from '@kit.ImageKit';
import { effectKit } from '@kit.ArkGraphics2D';
import { common } from '@kit.AbilityKit';

// 图片处理函数:传入图片数据,返回加了模糊效果的 PixelMap
function ImageBlur(Image: ArrayBuffer): Promise<image.PixelMap> {
  return new Promise(async (resolve, reject) => {
    let imageSource = image.createImageSource(Image);
    await imageSource.createPixelMap().then(async (pixelMap: image.PixelMap) => {
      let radius = 5;
      let headFilter = effectKit.createEffect(pixelMap);
      if (headFilter != null) {
        // 对图片添加模糊效果
        headFilter.blur(radius);
      }
      // 按照添加的效果标识对图片进行处理并且返回处理好的图片数据
      headFilter.getEffectPixelMap().then(imageData => {
        resolve(imageData);
      })
    })
  })
}

@Entry
@Component
struct Index {
  @State imagePixelMap: image.PixelMap | null = null;
  private imageBuffer: ArrayBuffer | undefined = undefined;

  // 读取 rawfile 文件夹下的图片文件
  async getFileBuffer(): Promise<ArrayBuffer | undefined> {
    try {
      const context: Context = this.getUIContext().getHostContext() as common.UIAbilityContext;
      const fileData: Uint8Array = await context.resourceManager.getRawFileContent('image.png');
      const buffer: ArrayBuffer = fileData.buffer.slice(0);
      return buffer;
    } catch (err) {
      return undefined;
    }
  }

  async aboutToAppear(): Promise<void> {
    this.imageBuffer = await this.getFileBuffer();
    if (this.imageBuffer == undefined) {
      return;
    }
    // 图片处理为异步操作,用 await 等待处理完成
    this.imagePixelMap = await ImageBlur(this.imageBuffer);
  }

  build() {
    Column() {
      Image(this.imagePixelMap)
        .width(304)
        .height(305)
    }
    .height('100%')
    .width('100%')
  }
}

我们来拆解一下这段代码:

  1. ImageBlur 函数:接收图片的 ArrayBuffer,创建 PixelMap,加模糊效果,返回处理后的 PixelMap。整个过程是异步的,所以用 Promise 包起来。

  2. getFileBuffer 方法:从项目的 rawfile 文件夹读取图片。resourceManager.getRawFileContent 返回的是 Uint8Array,我们用 .buffer.slice(0) 转成 ArrayBuffer

  3. aboutToAppear 生命周期:页面创建时,先读取图片,再处理图片。因为图片处理是异步的,所以用 await 等它完成,然后把结果赋给 imagePixelMap

  4. build 方法:用 Image 组件显示处理后的图片。当 imagePixelMap 变化时,UI 会自动刷新。

如果你把 blur 换成 brightness(0.5),就是调亮效果;换成 grayscale(),就是黑白效果;换成 invert(),就是底片效果。你可以自己试试不同的组合。

几个容易踩的坑

1. headFilter 可能是 null

effectKit.createEffect(pixelMap) 在失败时会返回 null。所以一定要先判断 if (headFilter != null) 再调用后续方法,不然会崩。

2. 效果是"标记"而不是"执行"

调用 blur(5) 并不会立刻处理图片,它只是在链表上标记了"这里要模糊"。真正的处理发生在 getEffectPixelMap() 的时候。所以你可以放心地连续添加多个效果,不用担心性能问题。

3. 原图不会变

getEffectPixelMap() 返回的是新图片,原图的 PixelMap 不受影响。如果你想同时展示原图和效果图,直接用两个 Image 组件分别绑定就行。

4. 这是离线处理,不是实时滤镜

effectKit 适合"用户选了一张图 → 加效果 → 展示/保存"这种场景。如果你要做相机实时预览的滤镜,需要用别的方案(比如 uiEffect 或者自定义渲染管线)。

小结

effectKit 的 Filter 用起来就三步:

  1. createEffect(pixelMap) — 创建滤镜头节点
  2. .blur() / .brightness() / .grayscale() / .invert() / .setColorMatrix() — 添加效果
  3. getEffectPixelMap() — 导出处理后的图片

效果可以叠加,顺序可以自定义,还能用颜色矩阵玩出各种花样。对于一个照片编辑 APP 来说,这些基础滤镜功能已经够用了。

下一篇我们聊聊 ColorPicker——怎么从一张照片里"智能"提取主色调,用来做主题色自动适配之类的功能。

Logo

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

更多推荐