HarmonyOS开发图像滤镜:模糊、锐化、色彩调整与自定义滤镜算法

HarmonyOS开发中的图像滤镜:模糊、锐化、色彩调整与自定义滤镜算法

核心要点:掌握 PixelMap 像素级操作实现图像滤镜,理解卷积核原理,熟练运用模糊/锐化/色彩调整算法,了解 LUT 滤镜加速方案


一、背景与动机

你打开手机相册,随手给一张照片加了个"复古"滤镜------画面瞬间泛黄、对比度降低、暗角出现。看起来很简单的操作,背后到底发生了什么?

滤镜的本质就是对像素值的数学运算。复古滤镜把 RGB 通道做了偏移和缩放,模糊滤镜把相邻像素做了加权平均,锐化滤镜增强了像素之间的差异。不同的数学公式,造就了千变万化的视觉效果。

在 HarmonyOS 上实现图像滤镜,核心思路就是:读取像素 → 计算 → 写回像素。听起来简单,但魔鬼在细节里------性能优化、边界处理、算法选择,每一个都是坑。今天我们就从最基础的模糊滤镜开始,一步步深入到自定义滤镜算法。


二、核心原理

2.1 滤镜的分类

复制代码
A[图像滤镜] --> B[空间域滤镜]
A --> C[频率域滤镜]
A --> D[LUT滤镜]

B --> B1[模糊滤镜]
B --> B2[锐化滤镜]
B --> B3[边缘检测]
B --> B4[浮雕效果]

B1 --> B1a[均值模糊"]
B1 --> B1b[高斯模糊"]

C --> C1[低通滤波 去噪"]
C --> C2[高通滤波 边缘"]

D --> D1[1D LUT"]
D --> D2[3D LUT"]

A --> E[色彩调整]
E --> E1[亮度/对比度"]
E --> E2[色相/饱和度"]
E --> E3[色调映射"]

2.2 卷积核------空间域滤镜的数学基础

空间域滤镜的核心操作是卷积(Convolution)。简单说就是:用一个小的矩阵(卷积核)在图像上滑动,每个位置计算卷积核与覆盖像素的加权和,作为中心像素的新值。

数学公式:

复制代码
Output(x, y) = Σ Σ Kernel(i, j) × Input(x+i, y+j)

常见的卷积核:

滤镜 卷积核(3×3) 效果
均值模糊 1/9 × [1,1,1, 1,1,1, 1,1,1] 均匀模糊
高斯模糊 1/16 × [1,2,1, 2,4,2, 1,2,1] 中心加权模糊
锐化 [0,-1,0, -1,5,-1, 0,-1,0] 增强边缘
边缘检测 [-1,-1,-1, -1,8,-1, -1,-1,-1] 提取轮廓
浮雕 [-2,-1,0, -1,1,1, 0,1,2] 立体浮雕感

2.3 高斯模糊的数学原理

高斯模糊是最常用的模糊算法,它的卷积核由二维高斯函数生成:

复制代码
G(x, y) = (1 / 2πσ²) × e^(-(x²+y²) / 2σ²)

其中 σ(sigma)控制模糊程度:σ 越大,模糊越强。

关键优化 :二维高斯核可以分解为两个一维高斯核的乘积,这意味着一个 O(n²) 的操作可以拆成两次 O(n) 的操作------这就是分离卷积,性能提升巨大。

复制代码
2D卷积: O(W × H × K²)    // K=核大小
分离卷积: O(W × H × K × 2) // 快了 K/2 倍

2.4 LUT 滤镜------用查表代替计算

LUT(Look-Up Table)是一种"空间换时间"的加速策略。预先计算好所有可能的输入→输出映射,运行时直接查表,不需要实时计算。

  • 1D LUT:对每个颜色通道独立映射,如亮度/对比度调整
  • 3D LUT:RGB 三通道联合映射,可以实现任意色彩变换

3D LUT 的原理:把 RGB 空间划分成 N×N×N 的网格,每个网格节点存储变换后的颜色值。运行时通过三线性插值得到任意输入的输出。


三、代码实战

3.1 模糊滤镜:从均值模糊到高斯模糊

typescript 复制代码
import { image } from '@kit.ImageKit';
import { fileIo as fs } from '@kit.CoreFileKit';

/**
 * 图像模糊滤镜工具类
 */
export class BlurFilter {

  /**
   * 均值模糊(Box Blur)
   * 最简单的模糊算法,每个像素取周围邻域的平均值
   * 优点:实现简单,速度快
   * 缺点:模糊效果不够自然,会有"方块感"
   *
   * @param pixelMap 目标图片
   * @param radius 模糊半径(1=3×3核,2=5×5核)
   */
  static async boxBlur(pixelMap: PixelMap, radius: number): Promise<void> {
    const info = await pixelMap.getImageInfo();
    const width = info.size.width;
    const height = info.size.height;
    const bufferSize = width * height * 4;

    // 读取原始像素数据
    const srcBuffer = new ArrayBuffer(bufferSize);
    await pixelMap.readPixelsToBuffer(srcBuffer);
    const src = new Uint8Array(srcBuffer);

    // 创建输出缓冲区
    const dstBuffer = new ArrayBuffer(bufferSize);
    const dst = new Uint8Array(dstBuffer);

    const kernelSize = radius * 2 + 1;
    const kernelArea = kernelSize * kernelSize;

    // 对每个像素进行均值模糊
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        let sumR = 0, sumG = 0, sumB = 0, sumA = 0;

        // 遍历卷积核覆盖区域
        for (let ky = -radius; ky <= radius; ky++) {
          for (let kx = -radius; kx <= radius; kx++) {
            // 边界处理:clamp模式,超出边界取最近像素
            const px = Math.min(Math.max(x + kx, 0), width - 1);
            const py = Math.min(Math.max(y + ky, 0), height - 1);
            const idx = (py * width + px) * 4;

            sumR += src[idx];
            sumG += src[idx + 1];
            sumB += src[idx + 2];
            sumA += src[idx + 3];
          }
        }

        // 计算平均值
        const outIdx = (y * width + x) * 4;
        dst[outIdx] = Math.round(sumR / kernelArea);
        dst[outIdx + 1] = Math.round(sumG / kernelArea);
        dst[outIdx + 2] = Math.round(sumB / kernelArea);
        dst[outIdx + 3] = Math.round(sumA / kernelArea);
      }
    }

    // 写回像素数据
    await pixelMap.writeBufferToPixels(dstBuffer);
  }

  /**
   * 高斯模糊(分离卷积实现)
   * 将2D卷积分解为水平+垂直两次1D卷积
   * 性能从 O(W×H×K²) 提升到 O(W×H×K×2)
   *
   * @param pixelMap 目标图片
   * @param sigma 高斯标准差,控制模糊强度
   */
  static async gaussianBlur(pixelMap: PixelMap, sigma: number): Promise<void> {
    const info = await pixelMap.getImageInfo();
    const width = info.size.width;
    const height = info.size.height;
    const bufferSize = width * height * 4;

    // 1. 生成1D高斯核
    const radius = Math.ceil(sigma * 3);  // 3σ原则,覆盖99.7%的权重
    const kernel = this.generateGaussianKernel1D(sigma, radius);

    // 2. 读取原始像素
    const srcBuffer = new ArrayBuffer(bufferSize);
    await pixelMap.readPixelsToBuffer(srcBuffer);
    const src = new Uint8Array(srcBuffer);

    // 3. 中间缓冲区(水平卷积结果)
    const midBuffer = new ArrayBuffer(bufferSize);
    const mid = new Uint8Array(midBuffer);

    // 4. 水平方向1D卷积
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        let sumR = 0, sumG = 0, sumB = 0, sumA = 0;

        for (let k = -radius; k <= radius; k++) {
          const px = Math.min(Math.max(x + k, 0), width - 1);
          const idx = (y * width + px) * 4;
          const weight = kernel[k + radius];

          sumR += src[idx] * weight;
          sumG += src[idx + 1] * weight;
          sumB += src[idx + 2] * weight;
          sumA += src[idx + 3] * weight;
        }

        const outIdx = (y * width + x) * 4;
        mid[outIdx] = Math.round(sumR);
        mid[outIdx + 1] = Math.round(sumG);
        mid[outIdx + 2] = Math.round(sumB);
        mid[outIdx + 3] = Math.round(sumA);
      }
    }

    // 5. 垂直方向1D卷积
    const dstBuffer = new ArrayBuffer(bufferSize);
    const dst = new Uint8Array(dstBuffer);

    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        let sumR = 0, sumG = 0, sumB = 0, sumA = 0;

        for (let k = -radius; k <= radius; k++) {
          const py = Math.min(Math.max(y + k, 0), height - 1);
          const idx = (py * width + x) * 4;
          const weight = kernel[k + radius];

          sumR += mid[idx] * weight;
          sumG += mid[idx + 1] * weight;
          sumB += mid[idx + 2] * weight;
          sumA += mid[idx + 3] * weight;
        }

        const outIdx = (y * width + x) * 4;
        dst[outIdx] = Math.min(255, Math.max(0, Math.round(sumR)));
        dst[outIdx + 1] = Math.min(255, Math.max(0, Math.round(sumG)));
        dst[outIdx + 2] = Math.min(255, Math.max(0, Math.round(sumB)));
        dst[outIdx + 3] = Math.min(255, Math.max(0, Math.round(sumA)));
      }
    }

    // 6. 写回像素数据
    await pixelMap.writeBufferToPixels(dstBuffer);
  }

  /**
   * 生成1D高斯核
   */
  private static generateGaussianKernel1D(sigma: number, radius: number): number[] {
    const kernel: number[] = [];
    let sum = 0;

    for (let i = -radius; i <= radius; i++) {
      const value = Math.exp(-(i * i) / (2 * sigma * sigma));
      kernel.push(value);
      sum += value;
    }

    // 归一化:使核权重之和为1
    return kernel.map(v => v / sum);
  }
}

3.2 锐化滤镜与边缘检测

typescript 复制代码
import { image } from '@kit.ImageKit';

/**
 * 锐化与边缘检测滤镜
 */
export class SharpenFilter {

  /**
   * 卷积运算核心方法
   * 通用的3×3卷积实现,可复用于各种滤镜
   *
   * @param pixelMap 目标图片
   * @param kernel 3×3卷积核(长度9的数组,行优先)
   */
  static async convolve3x3(pixelMap: PixelMap, kernel: number[]): Promise<void> {
    const info = await pixelMap.getImageInfo();
    const width = info.size.width;
    const height = info.size.height;
    const bufferSize = width * height * 4;

    // 读取原始像素
    const srcBuffer = new ArrayBuffer(bufferSize);
    await pixelMap.readPixelsToBuffer(srcBuffer);
    const src = new Uint8Array(srcBuffer);

    // 输出缓冲区
    const dstBuffer = new ArrayBuffer(bufferSize);
    const dst = new Uint8Array(dstBuffer);

    // 3×3卷积
    for (let y = 1; y < height - 1; y++) {
      for (let x = 1; x < width - 1; x++) {
        let sumR = 0, sumG = 0, sumB = 0;

        // 卷积核9个位置
        for (let ky = -1; ky <= 1; ky++) {
          for (let kx = -1; kx <= 1; kx++) {
            const idx = ((y + ky) * width + (x + kx)) * 4;
            const kIdx = (ky + 1) * 3 + (kx + 1);

            sumR += src[idx] * kernel[kIdx];
            sumG += src[idx + 1] * kernel[kIdx];
            sumB += src[idx + 2] * kernel[kIdx];
          }
        }

        const outIdx = (y * width + x) * 4;
        dst[outIdx] = Math.min(255, Math.max(0, Math.round(sumR)));
        dst[outIdx + 1] = Math.min(255, Math.max(0, Math.round(sumG)));
        dst[outIdx + 2] = Math.min(255, Math.max(0, Math.round(sumB)));
        dst[outIdx + 3] = src[outIdx + 3];  // Alpha通道不变
      }
    }

    // 处理边界像素:直接复制
    this.copyBorderPixels(src, dst, width, height);

    // 写回
    await pixelMap.writeBufferToPixels(dstBuffer);
  }

  /**
   * 锐化滤镜
   * 锐化 = 原图 + (原图 - 模糊图) × 系数
   * 等价于使用锐化卷积核
   *
   * @param strength 锐化强度 0.0~2.0
   */
  static async sharpen(pixelMap: PixelMap, strength: number = 1.0): Promise<void> {
    // 锐化卷积核: [0, -s, 0, -s, 1+4s, -s, 0, -s, 0]
    const s = strength;
    const kernel = [
      0, -s, 0,
      -s, 1 + 4 * s, -s,
      0, -s, 0
    ];
    await this.convolve3x3(pixelMap, kernel);
  }

  /**
   * USM锐化(Unsharp Mask)
   * 比直接锐化更自然,可控性更强
   *
   * 步骤:
   * 1. 对原图做高斯模糊得到低频图
   * 2. 高频图 = 原图 - 低频图
   * 3. 输出 = 原图 + 高频图 × amount
   */
  static async usmSharpen(
    pixelMap: PixelMap,
    amount: number = 1.5,
    threshold: number = 0
  ): Promise<void> {
    const info = await pixelMap.getImageInfo();
    const width = info.size.width;
    const height = info.size.height;
    const bufferSize = width * height * 4;

    // 读取原始像素
    const srcBuffer = new ArrayBuffer(bufferSize);
    await pixelMap.readPixelsToBuffer(srcBuffer);
    const src = new Uint8Array(srcBuffer);

    // 复制一份用于模糊
    const blurBuffer = new ArrayBuffer(bufferSize);
    const blurView = new Uint8Array(blurBuffer);
    blurView.set(src);  // 复制原始数据

    // 创建临时 PixelMap 进行模糊
    // 注意:这里简化处理,实际应使用 BlurFilter.gaussianBlur
    // 此处用均值模糊近似
    const tempPacker = image.createImagePacker();

    // 直接在 buffer 上做简单的均值模糊(3×3)
    for (let y = 1; y < height - 1; y++) {
      for (let x = 1; x < width - 1; x++) {
        let sumR = 0, sumG = 0, sumB = 0;
        for (let ky = -1; ky <= 1; ky++) {
          for (let kx = -1; kx <= 1; kx++) {
            const idx = ((y + ky) * width + (x + kx)) * 4;
            sumR += src[idx];
            sumG += src[idx + 1];
            sumB += src[idx + 2];
          }
        }
        const outIdx = (y * width + x) * 4;
        blurView[outIdx] = Math.round(sumR / 9);
        blurView[outIdx + 1] = Math.round(sumG / 9);
        blurView[outIdx + 2] = Math.round(sumB / 9);
        blurView[outIdx + 3] = src[outIdx + 3];
      }
    }

    // USM: 输出 = 原图 + (原图 - 模糊) × amount
    const dstBuffer = new ArrayBuffer(bufferSize);
    const dst = new Uint8Array(dstBuffer);

    for (let i = 0; i < src.length; i += 4) {
      for (let c = 0; c < 3; c++) {  // R, G, B
        const diff = src[i + c] - blurView[i + c];

        // 阈值过滤:差值小于阈值的不锐化,避免放大噪点
        if (Math.abs(diff) < threshold) {
          dst[i + c] = src[i + c];
        } else {
          dst[i + c] = Math.min(255, Math.max(0,
            Math.round(src[i + c] + diff * amount)
          ));
        }
      }
      dst[i + 3] = src[i + 3];  // Alpha不变
    }

    await pixelMap.writeBufferToPixels(dstBuffer);
  }

  /**
   * 边缘检测(Laplacian算子)
   * 提取图像中的边缘轮廓
   */
  static async edgeDetect(pixelMap: PixelMap): Promise<void> {
    const kernel = [
      -1, -1, -1,
      -1,  8, -1,
      -1, -1, -1
    ];
    await this.convolve3x3(pixelMap, kernel);
  }

  /**
   * 浮雕效果
   * 让图片看起来像浮雕
   */
  static async emboss(pixelMap: PixelMap): Promise<void> {
    const kernel = [
      -2, -1, 0,
      -1,  1, 1,
       0,  1, 2
    ];
    await this.convolve3x3(pixelMap, kernel);
  }

  /**
   * 复制边界像素
   */
  private static copyBorderPixels(
    src: Uint8Array, dst: Uint8Array,
    width: number, height: number
  ): void {
    // 顶边和底边
    for (let x = 0; x < width; x++) {
      const topIdx = x * 4;
      const bottomIdx = ((height - 1) * width + x) * 4;
      dst[topIdx] = src[topIdx];
      dst[topIdx + 1] = src[topIdx + 1];
      dst[topIdx + 2] = src[topIdx + 2];
      dst[topIdx + 3] = src[topIdx + 3];
      dst[bottomIdx] = src[bottomIdx];
      dst[bottomIdx + 1] = src[bottomIdx + 1];
      dst[bottomIdx + 2] = src[bottomIdx + 2];
      dst[bottomIdx + 3] = src[bottomIdx + 3];
    }
    // 左边和右边
    for (let y = 0; y < height; y++) {
      const leftIdx = (y * width) * 4;
      const rightIdx = (y * width + width - 1) * 4;
      dst[leftIdx] = src[leftIdx];
      dst[leftIdx + 1] = src[leftIdx + 1];
      dst[leftIdx + 2] = src[leftIdx + 2];
      dst[leftIdx + 3] = src[leftIdx + 3];
      dst[rightIdx] = src[rightIdx];
      dst[rightIdx + 1] = src[rightIdx + 1];
      dst[rightIdx + 2] = src[rightIdx + 2];
      dst[rightIdx + 3] = src[rightIdx + 3];
    }
  }
}

3.3 色彩调整:亮度、对比度、饱和度、色相

typescript 复制代码
import { image } from '@kit.ImageKit';

/**
 * 色彩调整滤镜
 * 所有方法都基于 readPixelsToBuffer/writeBufferToPixels 批量操作
 */
export class ColorAdjustFilter {

  /**
   * 亮度调整
   * value > 0 变亮,value < 0 变暗
   * 范围: -255 ~ 255
   */
  static async brightness(pixelMap: PixelMap, value: number): Promise<void> {
    await this.forEachPixel(pixelMap, (r, g, b, a) => {
      return [
        Math.min(255, Math.max(0, r + value)),
        Math.min(255, Math.max(0, g + value)),
        Math.min(255, Math.max(0, b + value)),
        a
      ];
    });
  }

  /**
   * 对比度调整
   * factor > 1 增加对比度,factor < 1 降低对比度
   * factor = 0 全灰,factor = 1 原图
   */
  static async contrast(pixelMap: PixelMap, factor: number): Promise<void> {
    const intercept = 128 * (1 - factor);  // 以128为中心点
    await this.forEachPixel(pixelMap, (r, g, b, a) => {
      return [
        Math.min(255, Math.max(0, Math.round(r * factor + intercept))),
        Math.min(255, Math.max(0, Math.round(g * factor + intercept))),
        Math.min(255, Math.max(0, Math.round(b * factor + intercept))),
        a
      ];
    });
  }

  /**
   * 饱和度调整
   * saturation > 1 增加饱和度,saturation < 1 降低饱和度
   * saturation = 0 变为灰度图
   */
  static async saturation(pixelMap: PixelMap, saturation: number): Promise<void> {
    await this.forEachPixel(pixelMap, (r, g, b, a) => {
      // 先转灰度
      const gray = 0.299 * r + 0.587 * g + 0.114 * b;
      // 在灰度和原色之间插值
      return [
        Math.min(255, Math.max(0, Math.round(gray + (r - gray) * saturation))),
        Math.min(255, Math.max(0, Math.round(gray + (g - gray) * saturation))),
        Math.min(255, Math.max(0, Math.round(gray + (b - gray) * saturation))),
        a
      ];
    });
  }

  /**
   * 色相旋转
   * angle 旋转角度(度),0~360
   * 将RGB转HSL,旋转H,再转回RGB
   */
  static async hueRotate(pixelMap: PixelMap, angle: number): Promise<void> {
    await this.forEachPixel(pixelMap, (r, g, b, a) => {
      // RGB → HSL
      const [h, s, l] = this.rgbToHsl(r, g, b);
      // 旋转色相
      const newH = (h + angle / 360) % 1.0;
      // HSL → RGB
      const [newR, newG, newB] = this.hslToRgb(newH < 0 ? newH + 1 : newH, s, l);
      return [newR, newG, newB, a];
    });
  }

  /**
   * 色温调整
   * value > 0 暖色调(偏黄),value < 0 冷色调(偏蓝)
   */
  static async temperature(pixelMap: PixelMap, value: number): Promise<void> {
    await this.forEachPixel(pixelMap, (r, g, b, a) => {
      return [
        Math.min(255, Math.max(0, r + value)),       // 红色通道增减
        Math.min(255, Math.max(0, g)),                 // 绿色不变
        Math.min(255, Math.max(0, b - value)),         // 蓝色通道反向
        a
      ];
    });
  }

  /**
   * 暗角效果(Vignette)
   * 图片边缘变暗,中心保持亮度
   */
  static async vignette(pixelMap: PixelMap, strength: number = 0.5): Promise<void> {
    const info = await pixelMap.getImageInfo();
    const width = info.size.width;
    const height = info.size.height;
    const centerX = width / 2;
    const centerY = height / 2;
    const maxDist = Math.sqrt(centerX * centerX + centerY * centerY);

    await this.forEachPixelWithPosition(pixelMap, (r, g, b, a, x, y) => {
      const dx = x - centerX;
      const dy = y - centerY;
      const dist = Math.sqrt(dx * dx + dy * dy) / maxDist;

      // 距离越远,暗化越强
      const factor = 1 - Math.pow(dist, 2) * strength;
      return [
        Math.round(r * factor),
        Math.round(g * factor),
        Math.round(b * factor),
        a
      ];
    });
  }

  // ==================== 工具方法 ====================

  /**
   * 遍历每个像素并应用变换函数
   */
  private static async forEachPixel(
    pixelMap: PixelMap,
    transform: (r: number, g: number, b: number, a: number) => number[]
  ): Promise<void> {
    const info = await pixelMap.getImageInfo();
    const bufferSize = info.size.width * info.size.height * 4;

    const srcBuffer = new ArrayBuffer(bufferSize);
    await pixelMap.readPixelsToBuffer(srcBuffer);
    const src = new Uint8Array(srcBuffer);

    const dstBuffer = new ArrayBuffer(bufferSize);
    const dst = new Uint8Array(dstBuffer);

    for (let i = 0; i < src.length; i += 4) {
      const [r, g, b, a] = transform(src[i], src[i + 1], src[i + 2], src[i + 3]);
      dst[i] = r;
      dst[i + 1] = g;
      dst[i + 2] = b;
      dst[i + 3] = a;
    }

    await pixelMap.writeBufferToPixels(dstBuffer);
  }

  /**
   * 带位置信息的像素遍历
   */
  private static async forEachPixelWithPosition(
    pixelMap: PixelMap,
    transform: (r: number, g: number, b: number, a: number, x: number, y: number) => number[]
  ): Promise<void> {
    const info = await pixelMap.getImageInfo();
    const width = info.size.width;
    const bufferSize = width * info.size.height * 4;

    const srcBuffer = new ArrayBuffer(bufferSize);
    await pixelMap.readPixelsToBuffer(srcBuffer);
    const src = new Uint8Array(srcBuffer);

    const dstBuffer = new ArrayBuffer(bufferSize);
    const dst = new Uint8Array(dstBuffer);

    for (let y = 0; y < info.size.height; y++) {
      for (let x = 0; x < width; x++) {
        const i = (y * width + x) * 4;
        const [r, g, b, a] = transform(src[i], src[i + 1], src[i + 2], src[i + 3], x, y);
        dst[i] = r;
        dst[i + 1] = g;
        dst[i + 2] = b;
        dst[i + 3] = a;
      }
    }

    await pixelMap.writeBufferToPixels(dstBuffer);
  }

  /**
   * RGB → HSL 转换
   */
  private static rgbToHsl(r: number, g: number, b: number): number[] {
    const rn = r / 255, gn = g / 255, bn = b / 255;
    const max = Math.max(rn, gn, bn);
    const min = Math.min(rn, gn, bn);
    const l = (max + min) / 2;

    if (max === min) return [0, 0, l];  // 灰色

    const d = max - min;
    const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);

    let h = 0;
    if (max === rn) h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6;
    else if (max === gn) h = ((bn - rn) / d + 2) / 6;
    else h = ((rn - gn) / d + 4) / 6;

    return [h, s, l];
  }

  /**
   * HSL → RGB 转换
   */
  private static hslToRgb(h: number, s: number, l: number): number[] {
    if (s === 0) {
      const v = Math.round(l * 255);
      return [v, v, v];  // 灰色
    }

    const hue2rgb = (p: number, q: number, t: number): number => {
      if (t < 0) t += 1;
      if (t > 1) t -= 1;
      if (t < 1 / 6) return p + (q - p) * 6 * t;
      if (t < 1 / 2) return q;
      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
      return p;
    };

    const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    const p = 2 * l - q;

    return [
      Math.round(hue2rgb(p, q, h + 1 / 3) * 255),
      Math.round(hue2rgb(p, q, h) * 255),
      Math.round(hue2rgb(p, q, h - 1 / 3) * 255)
    ];
  }
}

3.4 LUT 滤镜与预设风格

typescript 复制代码
import { image } from '@kit.ImageKit';

/**
 * LUT滤镜实现
 * 通过预计算的颜色映射表快速应用滤镜效果
 */
export class LutFilter {

  /**
   * 1D LUT 滤镜
   * 对每个颜色通道独立应用映射函数
   * 适合亮度/对比度/色调曲线等单通道调整
   *
   * @param pixelMap 目标图片
   * @param lutR 红色通道映射表(长度256)
   * @param lutG 绿色通道映射表(长度256)
   * @param lutB 蓝色通道映射表(长度256)
   */
  static async apply1DLut(
    pixelMap: PixelMap,
    lutR: number[],
    lutG: number[],
    lutB: number[]
  ): Promise<void> {
    const info = await pixelMap.getImageInfo();
    const bufferSize = info.size.width * info.size.height * 4;

    const srcBuffer = new ArrayBuffer(bufferSize);
    await pixelMap.readPixelsToBuffer(srcBuffer);
    const src = new Uint8Array(srcBuffer);

    const dstBuffer = new ArrayBuffer(bufferSize);
    const dst = new Uint8Array(dstBuffer);

    // 直接查表,O(1) 每像素
    for (let i = 0; i < src.length; i += 4) {
      dst[i] = lutR[src[i]];
      dst[i + 1] = lutG[src[i + 1]];
      dst[i + 2] = lutB[src[i + 2]];
      dst[i + 3] = src[i + 3];
    }

    await pixelMap.writeBufferToPixels(dstBuffer);
  }

  /**
   * 生成色调曲线 LUT
   * 模拟 Photoshop 的曲线调整
   */
  static generateCurveLut(curvePoints: number[]): number[] {
    const lut = new Array(256);

    // 线性插值生成平滑曲线
    for (let i = 0; i < 256; i++) {
      const t = i / 255;
      // 找到插值区间
      const idx = t * (curvePoints.length - 1);
      const low = Math.floor(idx);
      const high = Math.min(low + 1, curvePoints.length - 1);
      const frac = idx - low;

      const value = curvePoints[low] * (1 - frac) + curvePoints[high] * frac;
      lut[i] = Math.min(255, Math.max(0, Math.round(value * 255)));
    }

    return lut;
  }

  // ==================== 预设滤镜风格 ====================

  /**
   * 复古滤镜(Vintage)
   * 降低饱和度 + 偏暖色调 + 轻微褪色
   */
  static async vintage(pixelMap: PixelMap): Promise<void> {
    // 生成复古色调的1D LUT
    const lutR: number[] = [];
    const lutG: number[] = [];
    const lutB: number[] = [];

    for (let i = 0; i < 256; i++) {
      const t = i / 255;
      // 红色通道:稍微提亮
      lutR[i] = Math.min(255, Math.round(i * 1.1 + 10));
      // 绿色通道:轻微压暗
      lutG[i] = Math.min(255, Math.round(i * 0.9 + 5));
      // 蓝色通道:明显压暗,偏暖
      lutB[i] = Math.min(255, Math.round(i * 0.7));
    }

    await this.apply1DLut(pixelMap, lutR, lutG, lutB);
  }

  /**
   * 冷色调滤镜(Cool Tone)
   * 偏蓝色调,降低饱和度
   */
  static async coolTone(pixelMap: PixelMap): Promise<void> {
    const lutR: number[] = [];
    const lutG: number[] = [];
    const lutB: number[] = [];

    for (let i = 0; i < 256; i++) {
      lutR[i] = Math.min(255, Math.round(i * 0.85));
      lutG[i] = Math.min(255, Math.round(i * 0.95));
      lutB[i] = Math.min(255, Math.round(i * 1.1 + 10));
    }

    await this.apply1DLut(pixelMap, lutR, lutG, lutB);
  }

  /**
   * 黑金滤镜
   * 高对比度 + 金色高光 + 深黑阴影
   */
  static async blackGold(pixelMap: PixelMap): Promise<void> {
    const lutR: number[] = [];
    const lutG: number[] = [];
    const lutB: number[] = [];

    for (let i = 0; i < 256; i++) {
      const t = i / 255;
      if (t > 0.5) {
        // 高光区域:偏金色
        const gold = (t - 0.5) * 2;
        lutR[i] = Math.min(255, Math.round(i * (1 + gold * 0.3)));
        lutG[i] = Math.min(255, Math.round(i * (1 + gold * 0.1)));
        lutB[i] = Math.min(255, Math.round(i * (1 - gold * 0.3)));
      } else {
        // 阴影区域:压暗偏黑
        lutR[i] = Math.round(i * 0.7);
        lutG[i] = Math.round(i * 0.6);
        lutB[i] = Math.round(i * 0.5);
      }
    }

    await this.apply1DLut(pixelMap, lutR, lutG, lutB);
  }

  /**
   * 赛博朋克滤镜
   * 高饱和度 + 青色/品红偏色 + 高对比度
   */
  static async cyberpunk(pixelMap: PixelMap): Promise<void> {
    const lutR: number[] = [];
    const lutG: number[] = [];
    const lutB: number[] = [];

    for (let i = 0; i < 256; i++) {
      const t = i / 255;
      // 品红偏色:红蓝通道增强
      lutR[i] = Math.min(255, Math.round(i * 1.2 + 15));
      lutG[i] = Math.min(255, Math.round(i * 0.8));
      lutB[i] = Math.min(255, Math.round(i * 1.3 + 20));
    }

    await this.apply1DLut(pixelMap, lutR, lutG, lutB);
  }
}

四、踩坑与注意事项

4.1 逐像素操作的性能瓶颈

在 HarmonyOS 上,readPixelsToBuffer() / writeBufferToPixels() 是批量操作,性能远优于 getImagePixel() / setImagePixel() 逐像素操作。实测数据:

方法 1000×1000图片耗时
getImagePixel/setImagePixel 逐像素 5-10秒
readPixelsToBuffer/writeBufferToPixels 批量 50-200毫秒

结论:永远优先使用批量操作。

4.2 卷积的边界处理

3×3 卷积核在图片边缘会"越界",需要特殊处理。常见策略:

策略 说明 效果
Zero Padding 越界位置填0 边缘变暗
Replicate 越界位置复制最近像素 边缘正常(推荐)
Mirror 越界位置镜像翻转 边缘自然
Skip 不处理边缘像素 边缘保持原样

本文代码使用 Replicate 策略(Math.min(Math.max(...))),这是最常用的方式。

4.3 高斯模糊的 sigma 选择

sigma 值的选择直接影响模糊效果:

sigma 模糊程度 适用场景
1-2 轻微模糊 降噪、磨皮
3-5 中等模糊 背景虚化
10+ 强烈模糊 毛玻璃效果

注意:sigma 越大,卷积核越大,计算量越大。大 sigma 场景建议使用多次小 sigma 模糊叠加来近似:

typescript 复制代码
// 一次 sigma=10 的模糊 ≈ 三次 sigma=5 的模糊叠加
// 但三次 sigma=5 的计算量远小于一次 sigma=10

4.4 LUT 的精度问题

1D LUT 只有 256 个条目,对于简单的色调调整够用了。但如果需要 RGB 三通道联合映射(比如把某个特定的绿色变成另一个特定的蓝色),1D LUT 就不够了------需要 3D LUT。

3D LUT 的体积很大(33×33×33 ≈ 35000 个条目),但查表速度依然很快。HarmonyOS 暂无原生 3D LUT API,需要自行实现三线性插值。

4.5 滤镜叠加的顺序

多个滤镜叠加时,顺序很重要。比如"先模糊再锐化"和"先锐化再模糊"结果完全不同:

typescript 复制代码
// 方案A:先模糊再锐化 → 模糊被锐化抵消,接近原图
await BlurFilter.gaussianBlur(pixelMap, 2);
await SharpenFilter.sharpen(pixelMap, 1.0);

// 方案B:先锐化再模糊 → 锐化的边缘被模糊掉,接近模糊图
await SharpenFilter.sharpen(pixelMap, 1.0);
await BlurFilter.gaussianBlur(pixelMap, 2);

4.6 内存峰值控制

每个滤镜操作都需要额外的 ArrayBuffer(源数据 + 目标数据),对于大图来说内存开销不小。一张 4000×3000 的图片,每个缓冲区占 48MB,两个就 96MB。

优化建议

  1. 尽量复用 ArrayBuffer,避免频繁分配
  2. 滤镜链中,如果中间结果不需要保留,可以就地覆盖
  3. 大图场景考虑分块处理

五、HarmonyOS 6 适配

5.1 EffectKit 滤镜增强

HarmonyOS 6 的 EffectKit 新增了更多内置滤镜,可以直接对 PixelMap 应用:

typescript 复制代码
import { EffectKit } from '@kit.ImageKit';

// API 14+ 内置滤镜
const effectFilter = EffectKit.createFilter(pixelMap);

// 高斯模糊(原生实现,性能远优于手动卷积)
await effectFilter.blur(5);  // radius=5

// 亮度调整
await effectFilter.brightness(1.2);

// 饱和度调整
await effectFilter.saturate(1.5);

5.2 GPU 加速滤镜

HarmonyOS 6 支持将滤镜计算卸载到 GPU,大幅提升性能:

typescript 复制代码
// API 14+ GPU加速滤镜
const gpuFilter = EffectKit.createGpuFilter(pixelMap);
await gpuFilter.blur(10);  // GPU加速的高斯模糊

5.3 版本差异速查

特性 API 12 API 13 API 14
readPixelsToBuffer
writeBufferToPixels
EffectKit 基础滤镜 部分
EffectKit 高斯模糊 部分
GPU 加速滤镜
3D LUT 原生支持 部分

5.4 迁移建议

  1. 优先使用 EffectKit:API 14+ 提供的原生滤镜性能远优于手动实现,能用就用
  2. 自定义滤镜仍需手动实现:EffectKit 不支持的自定义效果,仍需使用 readPixelsToBuffer 方案
  3. GPU 加速注意线程安全:GPU 滤镜操作是异步的,同一 PixelMap 不能同时执行多个 GPU 操作

六、总结

渲染错误: Mermaid 渲染失败: Parse error on line 32: ...els 分离卷积 O(K²)→O(2K) LUT查表 O ----------------------^ Expecting 'SPACELINE', 'NL', 'EOF', got 'NODE_ID'

关键知识点回顾

知识点 要点
卷积核 空间域滤镜的数学基础,3×3核最常用
高斯模糊 分离卷积优化,sigma控制模糊强度
锐化 USM锐化比直接锐化更自然,threshold过滤噪点
色彩调整 RGB↔HSL转换是核心,批量操作保证性能
LUT 用查表代替计算,1D LUT适合单通道,3D LUT适合联合映射
性能 readPixelsToBuffer远快于逐像素操作,分离卷积优化模糊
边界处理 Replicate策略最常用,避免边缘变暗
HarmonyOS 6 EffectKit原生滤镜、GPU加速

图像滤镜是图像处理中最"好玩"的部分------改几个参数就能让照片风格大变。但好玩归好玩,性能和精度是永远绕不开的话题。下一篇我们聚焦图像裁剪缩放,看看如何实现智能裁剪、等比缩放和批量处理。