这篇文章教大家写一个醒图(XD
一、前言
大家常常会开玩笑说不要看到摄影师的原图,因为往往会做一些后期处理照片。毕竟,原图可能存在着各种各样的瑕疵,比如曝光不准确、色彩不鲜艳、构图不够完美等等。摄影师们通过后期处理,可以对这些问题进行修正和优化,让照片更加出色。比如调整亮度和对比度,让画面更加清晰和有层次感;增强色彩饱和度,使照片更加鲜艳和生动;甚至还可以进行裁剪和旋转,以达到更好的构图效果。
PS: 原图因为太大没有贴在这里(📍徐汇滨江)
最简单的操作大家可能都比较熟悉:
- 打开一个修图软件,例如醒图
- 选中图片
- 熟练的点开一个滤镜处理
- 导出
即可拥有一个风格化处理的图片
二、滤镜
其实下载的是一个完整的叫做 LUT (Look-Up-Table) 的东西。
为什么不是 32 *32 *32,要是 33 *33 *33 呢?
3D LUTs 将红色、绿色和蓝色映射到一个三维立方体的三个轴上。颜色值可以相对调整,这允许任何颜色映射到任何其他颜色。
例如: 一个33*33*33 规格的一个 LUT 文件
暂时无法在飞书文档外展示此内容
33*33*33=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;
}
- 某个点是深天空蓝,RGB 色值是(0, 191, 255)
- .cube 滤镜中的数值范围都是 0-1,因此,我们的 RGB 色值也要转换到这个范围(0, 0.7490196, 1):
- 我们的 Cube文件是 33*33*33
- 分别乘以32,得到:(0, 23.9686272, 32)
- B 是32,也就是蓝色是32,正好是整数,因为33个色块,每一块四块蓝色都是固定的,很显然,蓝色的这个色块是最后一个。
- G比较麻烦,因为23.9686272是小数,我们不妨先简单点来算取近似整数24,则表示最后一格纵向第24个点是我们的目标颜色。
- R 是0,因此水平第1个点。
- 算一下索引:(32 * 32) * 32 + 24 * 32 + 1 = 33537 行
- (0.03102911, 0.78814191, 0.90205824) 乘以 255 之后进行四舍五入之后变成 (8, 201, 230)
- 更仔细计算的话:一般在小数的情况下会进行差值计算
- 三线性插值(Trilinear Interpolation)原理:
// 假设我们有一个颜色点 (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%
当计算出某个颜色对应的 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
Worker 计算
帧率稳定在 40 fps
https://codesandbox.io/p/sandbox/5vjk78
再往上优化性能
此处callback一下 WebGL初探:从原理到实践解构现代 GUI 框架
之前介绍过 WebGL 和 WebGPU 的一些优化手段我就不重复介绍了。
一些有意思的问题
- 之前 Photoshop 和 LightRoom 和醒图区别?
- 为什么30M 的图片,使用同样的滤镜,LR 导出同样格式还是 30M,醒图导出只有 2M