← Back to blog

滤镜与前端应用

一、前言

大家常常会开玩笑说不要看到摄影师的原图,因为往往会做一些后期处理照片。毕竟,原图可能存在着各种各样的瑕疵,比如曝光不准确、色彩不鲜艳、构图不够完美等等。摄影师们通过后期处理,可以对这些问题进行修正和优化,让照片更加出色。比如调整亮度和对比度,让画面更加清晰和有层次感;增强色彩饱和度,使照片更加鲜艳和生动;甚至还可以进行裁剪和旋转,以达到更好的构图效果。

image.png

image.png

PS: 原图因为太大没有贴在这里(📍徐汇滨江)

最简单的操作大家可能都比较熟悉:

image.png

  • 打开一个修图软件,例如醒图
  • 选中图片
  • 熟练的点开一个滤镜处理
  • 导出

即可拥有一个风格化处理的图片

image.png

二、滤镜

其实下载的是一个完整的叫做 LUT (Look-Up-Table) 的东西。

image.png

image.png

3D LUTs 将红色、绿色和蓝色映射到一个三维立方体的三个轴上。颜色值可以相对调整,这允许任何颜色映射到任何其他颜色。

例如: 一个333333 规格的一个 LUT 文件

Color_Domi.cube

image.png

image.png

image.png

333333=35937 个点

eg: https://lut.tgratzer.com/

计算方式

一个图片往往是一个二维数组的数据组成的,处理图片,就是对于每一个像素点的值做一次运算。

// 对图片的 二维数组 进行处理
function applyLUT(buffer,width,height,lutData) {
    const data = new Uint8Array(buffer);
    const output = new Uint8Array(width * height * 4);
    const lutSize = 33;// 假设使用 33x33x33 的 LUT

    for (let i = 0; i < data.length; i += 4) {
// 获取原始 RGB 值
        const r = data[i];
        const g = data[i + 1];
        const b = data[i + 2];
        const a = data[i + 3];

// 计算 LUT 索引
        const lutR = (r / 255) * (lutSize - 1);
        const lutG = (g / 255) * (lutSize - 1);
        const lutB = (b / 255) * (lutSize - 1);

// 获取新的颜色值(通过 LUT)const newColor = lookupColor(lutR, lutG, lutB, lutData, lutSize);

// 写入新的颜色值
        output[i] = newColor.r;
        output[i + 1] = newColor.g;
        output[i + 2] = newColor.b;
        output[i + 3] = a;
    }

    return output.buffer;
}

image.png

  • 某个点是深天空蓝,RGB 色值是(0, 191, 255)

  • .cube 滤镜中的数值范围都是 0-1,因此,我们的 RGB 色值也要转换到这个范围(0, 0.7490196, 1):

  • 我们的 Cube文件是 333333

  • 分别乘以32,得到:(0, 23.9686272, 32)

  • B 是32,也就是蓝色是32,正好是整数,因为33个色块,每一块四块蓝色都是固定的,很显然,蓝色的这个色块是最后一个。

  • G比较麻烦,因为23.9686272是小数,我们不妨先简单点来算取近似整数24,则表示最后一格纵向第24个点是我们的目标颜色。

  • R 是0,因此水平第1个点。

  • 算一下索引:(32 * 32) * 32 + 24 * 32 + 1 = 33537 行

    image.png

  • (0.03102911, 0.78814191, 0.90205824) 乘以 255 之后进行四舍五入之后变成 (8, 201, 230)

image.png

  • 更仔细计算的话:一般在小数的情况下会进行差值计算

    • 三线性插值(Trilinear Interpolation)原理:

    image.png

    // 假设我们有一个颜色点 (r,g,b)
    const r = 128; // 0-255
    const g = 128;
    const b = 128;
    
    // 在 33x33x33 的 LUT 中,需要映射到 0-32 的范围
    const lutSize = 33;
    const lutR = (r / 255) * (lutSize - 1); // 例如: 16.5
    const lutG = (g / 255) * (lutSize - 1);
    const lutB = (b / 255) * (lutSize - 1);
    
    // 这个点会落在 LUT 的 8 个格点之间
    
    // 获取周围 8 个点的坐标
    const r0 = Math.floor(lutR);  // 16
    const g0 = Math.floor(lutG);
    const b0 = Math.floor(lutB);
    
    const r1 = Math.min(r0 + 1, lutSize - 1);  // 17
    const g1 = Math.min(g0 + 1, lutSize - 1);
    const b1 = Math.min(b0 + 1, lutSize - 1);
    
    // 计算权重(小数部分)
    const rw = lutR - r0;  // 0.5
    const gw = lutG - g0;
    const bw = lutB - b0;
    
    function trilinearInterpolation(c000, c001, c010, c011, c100, c101, c110, c111, x, y, z) {
        // 第一步:在 x 方向插值(R 方向)
        const c00 = c000 * (1 - x) + c100 * x;  // 前下左
        const c01 = c001 * (1 - x) + c101 * x;  // 前下右
        const c10 = c010 * (1 - x) + c110 * x;  // 前上左
        const c11 = c011 * (1 - x) + c111 * x;  // 前上右
    
        // 第二步:在 y 方向插值(G 方向)
        const c0 = c00 * (1 - y) + c10 * y;  // 前面
        const c1 = c01 * (1 - y) + c11 * y;  // 后面
    
        // 第三步:在 z 方向插值(B 方向)
        return c0 * (1 - z) + c1 * z;
    }
    
        c110 -------- c111
         /|           /|
        / |          / |
    c010 -------- c011 |
      |   |         |  |
      |  c100 ----- |-- c101
      | /           |  /
      |/            | /
    c000 -------- c001
    
    c000: 前下左点 (r0,g0,b0)
    c001: 前下右点 (r0,g0,b1)
    c010: 前上左点 (r0,g1,b0)
    c011: 前上右点 (r0,g1,b1)
    c100: 后下左点 (r1,g0,b0)
    c101: 后下右点 (r1,g0,b1)
    c110: 后上左点 (r1,g1,b0)
    c111: 后上右点 (r1,g1,b1)
    
  • 这种插值计算确保了:

    • 颜色过渡平滑
    • 没有明显的阶梯效果
    • 保持了 LUT 的精确性
    • 适用于任何大小的 LUT

我们之前常用的修图软件里面会有,百分比的选项,请问这个一般是怎么算出来的?

滤镜强度

0-100%

image.png

当计算出某个颜色对应的 finalColor 后:

// 在应用最终颜色时,使用强度参数进行混合
pixels[i] = Math.round(pixels[i] * (1 - intensity) + finalColor[0] * intensity);
pixels[i + 1] = Math.round(pixels[i + 1] * (1 - intensity) + finalColor[1] * intensity);
pixels[i + 2] = Math.round(pixels[i + 2] * (1 - intensity) + finalColor[2] * intensity);

三、前端渲染

Mac M2 Pro 32G 环境下 480p 视频

主线程计算

帧率稳定在 11 fps

image.png

Worker 计算

帧率稳定在 40 fps

image.png

https://codesandbox.io/p/sandbox/5vjk78

再往上优化性能

之前介绍过 WebGL 和 WebGPU 的一些优化手段我就不重复介绍了。

一些有意思的问题

  • 之前 Photoshop 和 LightRoom 和醒图区别?
  • 为什么30M 的图片,使用同样的滤镜,LR 导出同样格式还是 30M,醒图导出只有 2M