鸿蒙实战:图像滤镜工坊——ColorFilter 颜色矩阵与动态调节

完整源码:ImageFilterDemo 基于 HarmonyOS 5.0+,实现相册选图、11种滤镜实时预览、强度动态调节。

一、先看效果演示

二、ColorFilter 与 ImageFilter

鸿蒙提供了两种图像滤镜 API:本篇聚焦 ColorFilter

  • ColorFilter:作用于像素颜色,通过 4×5 颜色矩阵或混合模式实现。支持实时预览,GPU 渲染无延迟。典型效果:灰度、复古、反色、美白等。

  • ImageFilter:作用于图像本身,支持高斯模糊、偏移、组合等效果。需要离屏渲染,适合保存时处理。

三、颜色矩阵原理

颜色矩阵是一个 4×5 的数组,控制 RGBA 四个通道的输出:

复制代码
R' = m00×R + m01×G + m02×B + m03×A + m04
G' = m10×R + m11×G + m12×B + m13×A + m14
B' = m20×R + m21×G + m22×B + m23×A + m24
A' = m30×R + m31×G + m32×B + m33×A + m34

矩阵结构说明

  • 4 行:分别对应输出通道 R'、G'、B'、A'
  • 5 列:前 4 列对应输入通道 R、G、B、A 的系数,第 5 列是偏移量

各元素作用

  • 对角线元素(m00,m11,m22,m33):控制对应通道的强度

    • =1:保持原色
    • >1:增强该通道
    • <1:减弱该通道
    • =0:完全移除该通道
    • 负数:反相该通道
  • 非对角线元素:通道混合,将一个通道的值混入另一个通道(例如 m01 是绿色对红色的贡献)

  • 最后一列(m04,m14,m24,m34) :亮度偏移(>0 提亮,<0 压暗)

单位矩阵(原图)

复制代码
[1, 0, 0, 0, 0,
 0, 1, 0, 0, 0,
 0, 0, 1, 0, 0,
 0, 0, 0, 1, 0]

对角线为 1,其余为 0,输出等于输入,保持原图不变。

四、架构设计

4.1 整体架构图

复制代码
┌─────────────────────────────────────────────────────────────┐
│                       UI 层 (Index.ets)                      │
│  选图按钮 │ 图片预览 │ 滤镜列表 │ 强度滑块                    │
└─────────────────────────────┬───────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    状态层 (@State)                           │
│  pixelMap | currentColorFilter | currentFilterId | intensity│
└─────────────────────────────┬───────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                   滤镜管理层 (FilterRegistry)                │
│            Map<string, IFilter> 滤镜注册与获取               │
└─────────────────────────────┬───────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    滤镜实现层                                │
│  DynamicMatrixFilter │ LightingFilter │ LumaFilter          │
│  ColorSpaceFilter                                          │
└─────────────────────────────┬───────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    工具层 (FilterUtils)                      │
└─────────────────────────────────────────────────────────────┘

4.2 目录结构

最初设计每一种滤镜都是固定的,越写越多 colors 文件夹,通过动态矩阵我们可以完成所有的单滤镜效果。最终文件目录如下:

复制代码
ImageFilterDemo/
├── pages/
│   └── Index.ets                    // 主界面
├── filter/
│   ├── core/
│   │   ├── IFilter.ets              // 滤镜接口
│   │   ├── FilterRegistry.ets       // 滤镜注册中心
│   │   └── FilterUtils.ets          // 矩阵工具
│   └── colors/
│       ├── NoneFilter.ets           // 原图
│       ├── DynamicMatrixFilter.ets  // 动态矩阵滤镜
│       ├── LightingFilter.ets       // 光照滤镜
│       ├── LumaFilter.ets           // 亮度透明
│       └── ColorSpaceFilter.ets     // 颜色空间转换
├── constants/
│   └── MatrixConstants.ets          // 颜色矩阵常量
└── utils/
    └── MediaHelper.ets              // 相册选图

五、颜色矩阵常量定义

javascript 复制代码
export class MatrixConstants {
  // 灰度(平均值法)
  // 原理:R' = G' = B' = (R+G+B)/3,三通道取平均值,消除色相
  static readonly GRAY: number[] = [
    0.333, 0.333, 0.333, 0, 0,
    0.333, 0.333, 0.333, 0, 0,
    0.333, 0.333, 0.333, 0, 0,
    0, 0, 0, 1, 0
  ];

  // 复古(棕褐色调)
  // 原理:提高红/橙权重,降低蓝色,增加暖色偏移,模拟老照片
  static readonly SEPIA: number[] = [
    0.38, 0.62, 0.18, 0, 0.04,
    0.32, 0.58, 0.12, 0, 0.02,
    0.26, 0.54, 0.08, 0, 0.00,
    0, 0, 0, 1, 0
  ];

  // 反色
  // 原理:R' = 1-R,G' = 1-G,B' = 1-B,偏移量1保证结果在[0,1]范围
  static readonly INVERT: number[] = [
    -1, 0, 0, 0, 1,
    0, -1, 0, 0, 1,
    0, 0, -1, 0, 1,
    0, 0, 0, 1, 0
  ];

  // 提亮
  // 原理:每个RGB通道增加0.25(约64),整体变亮,色相不变
  static readonly BRIGHTNESS: number[] = [
    1, 0, 0, 0, 0.25,
    0, 1, 0, 0, 0.25,
    0, 0, 1, 0, 0.25,
    0, 0, 0, 1, 0
  ];

  // 美白
  // 原理:增益1.1增强通道强度,偏移0.05提亮,使肤色更白
  static readonly WHITEN: number[] = [
    1.1, 0, 0, 0, 0.05,
    0, 1.1, 0, 0, 0.05,
    0, 0, 1.1, 0, 0.05,
    0, 0, 0, 1, 0
  ];

  // 高对比
  // 原理:增益1.3使亮部更亮,偏移-0.12使暗部更暗,增强对比
  static readonly CONTRAST: number[] = [
    1.3, 0, 0, 0, -0.12,
    0, 1.3, 0, 0, -0.12,
    0, 0, 1.3, 0, -0.12,
    0, 0, 0, 1, 0
  ];
}

六、动态强度调节

强度参数 t ∈ 0,1,在**单位矩阵(原图)目标矩阵(完整效果)**之间线性插值:

复制代码
矩阵_t = 原图 × (1-t) + 目标 × t
  • t=0 → 原图
  • t=1 → 完整滤镜效果
  • t=0.5 → 50% 强度
javascript 复制代码
import { MatrixConstants } from '../../constants/MatrixConstants';

export class FilterUtils {
  /** 单位矩阵(原图) */
  static identity(): number[] {
    return [
      1, 0, 0, 0, 0,
      0, 1, 0, 0, 0,
      0, 0, 1, 0, 0,
      0, 0, 0, 1, 0
    ];
  }

  /** 组合多个滤镜 4x5 矩阵乘法: result = a * b */ 
  static multiplyMatrix4x5(a: number[], b: number[]): number[] {
    const result = new Array<number>(20).fill(0);
    for (let row = 0; row < 4; row++) {
      for (let col = 0; col < 5; col++) {
        let sum = 0;
        for (let i = 0; i < 5; i++) {
          const aVal = a[row * 5 + i];
          const bVal = i < 4 ? b[i * 5 + col] : (col === 4 ? b[i * 5 + col] : 0);
          sum += aVal * bVal;
        }
        result[row * 5 + col] = sum;
      }
    }
    return result;
  }

  /** 动态强度调节矩阵插值:lerp = a * (1 - t) + b * t */
  static lerpMatrix(a: number[], b: number[], t: number): number[] {
    t = Math.min(1, Math.max(0, t));
    const result = new Array<number>(20);
    for (let i = 0; i < 20; i++) {
      result[i] = a[i] * (1 - t) + b[i] * t;
    }
    return result;
  }

  /** 强度可调的灰度矩阵 */
  static createIntensityGray(intensity: number): number[] {
    return FilterUtils.lerpMatrix(FilterUtils.identity(), MatrixConstants.GRAY, intensity);
  }

  /** 强度可调的复古矩阵 */
  static createIntensitySepia(intensity: number): number[] {
    return FilterUtils.lerpMatrix(FilterUtils.identity(), MatrixConstants.SEPIA, intensity);
  }

  /** 强度可调的反色矩阵 */
  static createIntensityInvert(intensity: number): number[] {
    const result = FilterUtils.lerpMatrix(FilterUtils.identity(), MatrixConstants.INVERT, intensity);
    // 反色需要特殊处理偏移量
    for (let i = 4; i < 20; i += 5) {
      result[i] = result[i] * intensity;
    }
    return result;
  }

  /** 强度可调的提亮矩阵 */
  static createIntensityBrightness(intensity: number): number[] {
    const matrix = FilterUtils.identity();
    const offset = 0.25 * intensity;
    matrix[4] = offset;
    matrix[9] = offset;
    matrix[14] = offset;
    return matrix;
  }

  /** 强度可调的高对比矩阵 */
  static createIntensityContrast(intensity: number): number[] {
    const gain = 1 + intensity * 0.3;
    const offset = -0.12 * intensity;
    return [
      gain, 0, 0, 0, offset,
      0, gain, 0, 0, offset,
      0, 0, gain, 0, offset,
      0, 0, 0, 1, 0
    ];
  }

  /** 强度可调的美白矩阵 */
  static createIntensityWhiten(intensity: number): number[] {
    return FilterUtils.lerpMatrix(FilterUtils.identity(), MatrixConstants.WHITEN, intensity);
  }

  /** 验证矩阵是否有效 */
  static validateMatrix(matrix: number[]): boolean {
    return matrix && matrix.length === 20 && matrix.every(v => isFinite(v));
  }
}

七、核心代码实现

7.1 滤镜接口

javascript 复制代码
import { drawing } from '@kit.ArkGraphics2D';

export interface IFilter {
  getId(): string;
  getName(): string;
  getColorFilter(): ColorFilter | undefined;
  getImageFilter(): drawing.ImageFilter | undefined;
  isRealtime(): boolean;
}

7.2 动态矩阵滤镜

javascript 复制代码
import { IFilter } from '../core/IFilter';

export class DynamicMatrixFilter implements IFilter {
  private intensity: number = 1.0;

  constructor(
    private id: string,
    private name: string,
    private matrixGetter: (intensity: number) => number[]
  ) {}

  getId(): string {
    return this.id;
  }

  getName(): string {
    return this.name;
  }

  // 动态滤镜不可以直接返回 matrix,需要返回新的数组对象,否则无法动态调节
  getColorFilter(): ColorFilter | undefined {
    const matrix = this.matrixGetter(this.intensity);
    return [...matrix];
  }

  getImageFilter(): undefined {
    return undefined;
  }

  isRealtime(): boolean {
    return true;
  }

  setIntensity(intensity: number): void {
    this.intensity = Math.min(1, Math.max(0, intensity));
  }

  getIntensity(): number {
    return this.intensity;
  }
}

7.3 原图滤镜

javascript 复制代码
import { drawing } from '@kit.ArkGraphics2D';
import { IFilter } from '../core/IFilter';

export class NoneFilter implements IFilter {
  getId(): string {
    return 'none';
  }

  getName(): string {
    return '原图';
  }

  getColorFilter(): ColorFilter | undefined {
    return undefined;
  }

  getImageFilter(): drawing.ImageFilter | undefined {
    return undefined;
  }

  isRealtime(): boolean {
    return true;
  }
}

7.4 光照滤镜

javascript 复制代码
import { drawing, common2D } from '@kit.ArkGraphics2D';
import { IFilter } from '../core/IFilter';

export class LightingFilter implements IFilter {
  private mulColor: common2D.Color | number;
  private addColor: common2D.Color | number;

  constructor(mulColor: common2D.Color | number, addColor: common2D.Color | number) {
    this.mulColor = mulColor;
    this.addColor = addColor;
  }

  getId(): string {
    return 'lighting';
  }

  getName(): string {
    return '光照';
  }

  getColorFilter(): ColorFilter | undefined {
    return drawing.ColorFilter.createLightingColorFilter(this.mulColor, this.addColor);
  }

  getImageFilter(): drawing.ImageFilter | undefined {
    return undefined;
  }

  isRealtime(): boolean {
    return true;
  }
}

7.5 亮度透明滤镜

javascript 复制代码
import { drawing } from '@kit.ArkGraphics2D';
import { IFilter } from '../core/IFilter';

export class LumaFilter implements IFilter {
  getId(): string {
    return 'luma';
  }

  getName(): string {
    return '亮度透明';
  }

  getColorFilter(): ColorFilter | undefined {
    return drawing.ColorFilter.createLumaColorFilter();
  }

  getImageFilter(): drawing.ImageFilter | undefined {
    return undefined;
  }

  isRealtime(): boolean {
    return true;
  }
}

7.6 颜色空间转换滤镜

javascript 复制代码
import { drawing } from '@kit.ArkGraphics2D';
import { IFilter } from '../core/IFilter';

export enum ColorSpaceType {
  LINEAR_TO_SRGB,
  SRGB_TO_LINEAR
}

export class ColorSpaceFilter implements IFilter {
  private type: ColorSpaceType;

  constructor(type: ColorSpaceType) {
    this.type = type;
  }

  getId(): string {
    return this.type === ColorSpaceType.LINEAR_TO_SRGB ? 'linearToSrgb' : 'srgbToLinear';
  }

  getName(): string {
    return this.type === ColorSpaceType.LINEAR_TO_SRGB ? '线性→SRGB' : 'SRGB→线性';
  }

  getColorFilter(): ColorFilter | undefined {
    if (this.type === ColorSpaceType.LINEAR_TO_SRGB) {
      return drawing.ColorFilter.createLinearToSRGBGamma();
    } else {
      return drawing.ColorFilter.createSRGBGammaToLinear();
    }
  }

  getImageFilter(): drawing.ImageFilter | undefined {
    return undefined;
  }

  isRealtime(): boolean {
    return true;
  }
}

7.7 滤镜注册中心

javascript 复制代码
import { IFilter } from './IFilter';

import { LightingFilter } from '../colors/LightingFilter';
import { LumaFilter } from '../colors/LumaFilter';
import { ColorSpaceFilter, ColorSpaceType } from '../colors/ColorSpaceFilter';
import { NoneFilter } from '../colors/NoneFilter';
import { FilterUtils } from './FilterUtils';
import { DynamicMatrixFilter } from '../colors/DynamicMatrixFilter';

export class FilterRegistry {
  private static filters: Map<string, IFilter> = new Map();

  static initialize() {
    // 原图
    FilterRegistry.register(new NoneFilter());

    FilterRegistry.register(new DynamicMatrixFilter('gray', '灰度', (t) => FilterUtils.createIntensityGray(t)));
    FilterRegistry.register(new DynamicMatrixFilter('sepia', '复古', (t) => FilterUtils.createIntensitySepia(t)));
    FilterRegistry.register(new DynamicMatrixFilter('invert', '反色', (t) => FilterUtils.createIntensityInvert(t)));
    FilterRegistry.register(new DynamicMatrixFilter('brightness', '提亮', (t) => FilterUtils.createIntensityBrightness(t)));
    FilterRegistry.register(new DynamicMatrixFilter('contrast', '高对比', (t) => FilterUtils.createIntensityContrast(t)));
    FilterRegistry.register(new DynamicMatrixFilter('whiten', '美白', (t) => FilterUtils.createIntensityWhiten(t)));

    // 特效滤镜
    FilterRegistry.register(new LightingFilter(0xFFCCCCCC, 0x00222222));
    FilterRegistry.register(new LumaFilter());

    // 颜色空间转换滤镜
    FilterRegistry.register(new ColorSpaceFilter(ColorSpaceType.LINEAR_TO_SRGB));
    FilterRegistry.register(new ColorSpaceFilter(ColorSpaceType.SRGB_TO_LINEAR));

  }
  private static register(filter: IFilter) {
    FilterRegistry.filters.set(filter.getId(), filter);
  }

  static getAll(): IFilter[] {
    if (FilterRegistry.filters.size === 0) {
      FilterRegistry.initialize();
    }
    return Array.from(FilterRegistry.filters.values());
  }

  static get(id: string): IFilter | undefined {
    if (FilterRegistry.filters.size === 0) {
      FilterRegistry.initialize();
    }
    return FilterRegistry.filters.get(id);
  }
}

7.8 相册选图工具

javascript 复制代码
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { image } from '@kit.ImageKit';
import fs from '@ohos.file.fs';

export class MediaHelper {
  // 读取图片
  static async pickImage(): Promise<image.PixelMap | null> {
    const picker = new photoAccessHelper.PhotoViewPicker();
    try {
      const result = await picker.select({
        MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
        maxSelectNumber: 1
      });
      if (result.photoUris.length === 0) {
        return null;
      }
      const uri = result.photoUris[0];
      const file = await fs.open(uri, fs.OpenMode.READ_ONLY);
      const imageSource = image.createImageSource(file.fd);
      const pixelMap = await imageSource.createPixelMap();
      await imageSource.release();
      await fs.close(file);
      return pixelMap;
    } catch (error) {

      return null
    }
  }
}

7.9 主界面

javascript 复制代码
import { IFilter } from '../filter/core/IFilter';
import { MediaHelper } from '../utils/MediaHelper';
import promptAction from '@ohos.promptAction';
import { image } from '@kit.ImageKit';
import { FilterRegistry } from '../filter/core/FilterRegistry';
import { DynamicMatrixFilter } from '../filter/colors/DynamicMatrixFilter';
import { LengthMetrics } from '@kit.ArkUI';


@Entry
@Component
struct Index {
  @State pixelMap: image.PixelMap | null = null;
  @State currentColorFilter: ColorFilter | undefined = undefined;
  @State currentFilterId: string = 'none';
  @State currentFilter: IFilter | undefined = undefined;
  @State currentIntensity: number = 1.0;
  @State filterList: IFilter[] = FilterRegistry.getAll();
  aboutToAppear(): void {
    // 初始化当前滤镜
    this.currentFilter = FilterRegistry.get('none');
    this.currentColorFilter = this.currentFilter?.getColorFilter();
  }

  // 切换滤镜
  private switchFilter(filter: IFilter): void {
    console.info(`[Index] switchFilter: ${filter.getId()}, isDynamic: ${filter instanceof DynamicMatrixFilter}`);

    if (!filter.isRealtime()) {
      promptAction.showToast({ message: '该滤镜仅支持离屏渲染,预览无效果' });
      return;
    }
    this.currentFilterId = filter.getId();
    this.currentFilter = filter;
    this.currentColorFilter = filter.getColorFilter();

    if (filter instanceof DynamicMatrixFilter) {
      this.currentIntensity = filter.getIntensity();
    } else {
      this.currentIntensity = 1.0;
    }
  }

  // 调节强度
  private changeIntensity(value: number): void {
    console.info(`[Index] changeIntensity called, value: ${value}`);
    const filter = this.currentFilter;
    if (filter instanceof DynamicMatrixFilter) {
      filter.setIntensity(value);
      this.currentColorFilter = filter.getColorFilter();
      this.currentIntensity = value;
    }
  }

  build() {
    Column({ space: 16 }) {
      // 图片预览区
      if (this.pixelMap) {
        Image(this.pixelMap)
          .width('100%')
          .layoutWeight(1)
          .objectFit(ImageFit.Contain)
          .colorFilter(this.currentColorFilter)
          .borderRadius(12)
          .margin({ top: 8, bottom: 8 })
          .backgroundColor('#F0F0F0')
      } else {
        Column() {
          Text('暂无图片')
            .fontSize(16)
            .fontColor('#999')
          Text('请点击下方按钮选择图片')
            .fontSize(12)
            .fontColor('#BBB')
            .margin({ top: 8 })
        }
        .width('100%')
        .layoutWeight(1)
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .backgroundColor('#F5F5F5')
        .borderRadius(12)
        .border({ width: 1, color: '#E0E0E0', style: BorderStyle.Dashed })
      }

      // 强度滑块
      if (this.pixelMap && this.currentFilter instanceof DynamicMatrixFilter) {
        Column() {
          Row() {
            Text('强度')
              .fontSize(14)
              .fontColor('#666')
            Blank()
            Text(`${Math.round(this.currentIntensity * 100)}%`)
              .fontSize(14)
              .fontColor('#3B82F6')
          }
          .width('100%')
          .margin({ bottom: 8 })

          Slider({
            value: this.currentIntensity,
            min: 0,
            max: 1,
            step: 0.01
          })
            .width('100%')
            .onChange((value: number) => {
              this.changeIntensity(value);
            })
        }
        .padding({ left: 16, right: 16, top: 12, bottom: 12 })
        .backgroundColor('#F8F9FA')
        .borderRadius(12)
        .margin({ bottom: 8 })
      }

      // 滤镜列表
      Flex({
        space: {
          main: LengthMetrics.vp(10),
          cross: LengthMetrics.vp(10)
        },
        direction: FlexDirection.Row,
        wrap: FlexWrap.Wrap,
        alignContent: FlexAlign.Start,
      }) {
        ForEach(this.filterList, (filter: IFilter) => {
          Text(filter.getName())
            .fontSize(14)
            .padding({ left: 14, right: 14, top: 8, bottom: 8 })
            .borderRadius(24)
            .backgroundColor(this.currentFilterId === filter.getId() ? '#3B82F6' : '#F3F4F6')
            .fontColor(this.currentFilterId === filter.getId() ? '#FFFFFF' : '#374151')
            .opacity(filter.isRealtime() ? 1.0 : 0.7)
            .onClick(() => {
              this.switchFilter(filter);
            })
        }, (filter: IFilter) => filter.getId())
      }
      .padding({ left: 16, right: 16 })
      .width('100%')
      .height(150)

      Row({ space: 12 }) {
        Button('从相册选择图片')
          .layoutWeight(1)
          .height(44)
          .onClick(async () => {
            const p = await MediaHelper.pickImage();
            if (p) {
              this.pixelMap = p;
              // 选图后重新应用当前滤镜
              if (this.currentFilter && this.currentFilter.isRealtime()) {
                this.currentColorFilter = this.currentFilter.getColorFilter();
              }
            } else {
              promptAction.showToast({ message: '未选择图片' });
            }
          })
      }
      .width('100%')
      .padding({ top: 8 })
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor(Color.White)
  }
}

八、总结

本篇完整实现了图片颜色滤镜,涵盖 11 种 ColorFilter 滤镜效果,代码已发布可直接下载运行查看。

核心分享

  • 通过 4×5 颜色矩阵,掌握了对 RGBA 通道值的修改即可改变图片颜色
  • ColorFilter 基于 GPU 渲染,切换滤镜无延迟
  • 矩阵插值技术实现 0-100% 动态强度调节

后续可扩展

  • 自定义矩阵参数,创造更多色彩效果
  • 接入 ImageFilter 实现模糊、偏移等高级滤镜
  • 增加离屏渲染,支持保存带滤镜的图片

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

相关推荐
UnicornDev1 小时前
【Flutter x HarmonyOS 6】设置页面的UI设计
flutter·ui·华为·harmonyos·鸿蒙
大雷神2 小时前
第21篇|侧边导航:平板和 2in1 为什么不照搬手机布局
harmonyos
G_dou_2 小时前
Flutter+OpenHarmony实战:XMB Tracke
flutter·harmonyos·鸿蒙
脑极体10 小时前
点亮星河AI+鸿蒙,一座艺术场馆的日神觉醒
人工智能·华为·harmonyos
●VON10 小时前
鸿蒙Flutter实战:分类管理页BottomSheet CRUD
数据库·flutter·华为·harmonyos·鸿蒙
GitCode官方14 小时前
开源鸿蒙 PC 直播回顾|从环境搭建到真机验证:鸿蒙 PC 命令行迁移全链路。
华为·开源·harmonyos
想你依然心痛14 小时前
HarmonyOS 6(API 23)智能体驱动的沉浸式AR文化遗产数字修复工坊
华为·ar·harmonyos·智能体
largecode18 小时前
座机号码认证如何操作?申请热线实名名片,树立统一官方客服形象
linux·sql·华为·c#·.net·wpf·harmonyos