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。
优化建议:
- 尽量复用 ArrayBuffer,避免频繁分配
- 滤镜链中,如果中间结果不需要保留,可以就地覆盖
- 大图场景考虑分块处理
五、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 迁移建议
- 优先使用 EffectKit:API 14+ 提供的原生滤镜性能远优于手动实现,能用就用
- 自定义滤镜仍需手动实现:EffectKit 不支持的自定义效果,仍需使用 readPixelsToBuffer 方案
- 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加速 |
图像滤镜是图像处理中最"好玩"的部分------改几个参数就能让照片风格大变。但好玩归好玩,性能和精度是永远绕不开的话题。下一篇我们聚焦图像裁剪缩放,看看如何实现智能裁剪、等比缩放和批量处理。