ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

LUT与PIP结合:可复现的调色对比流程实战

LUT与PIP结合:可复现的调色对比流程实战 在视频后期处理里LUTLook-Up Table颜色查找表和 PIPPicture-in-Picture画中画通常被当作两个独立功能。前者负责把颜色从一套规则映射到另一套规则后者负责在画面角落叠加一个子画面。把两者放在同一条工作流里能解决一个很实际的问题调出来的颜色到底有没有变干净。下面用一次代号为 LUT-E 的调色实战记录为例说明如何用 LUT 把灰蒙蒙的素材清理出层次再用 PIP 输出前后对比最后得到一张可以贴到交付文档里的效果图。这是一次典型的“清图胜利”流程重点不是某个参数多高明而是整条链路从素材、配置、脚本到验证都是可复现的。1. 先理解 LUT 与 PIP一个负责改颜色一个负责看对比1.1 LUT 是什么为什么能“清图”LUT 的全称是 Look-Up Table中文叫颜色查找表。通俗地说它是一张输入颜色到输出颜色的映射表给一个 RGB 值它返回一个新的 RGB 值。调色软件拿到这张表后会把图像里的每一个像素按表重新映射一次从而改变画面整体风格。LUT 分为 1D LUT 和 3D LUT。1D LUT 只对 RGB 三个通道分别做独立映射适合做亮度、伽马这类调整。3D LUT 把 R、G、B 三个通道组合起来查表可以表达更复杂的颜色交叉影响比如改变绿色时同时影响黄色和青色。影视后期常见的.cube文件就是 3D LUT 的一种标准格式。下面是一个最小.cube文件的片段TITLE lut_e LUT_3D_SIZE 33 DOMAIN_MIN 0.0 0.0 0.0 DOMAIN_MAX 1.0 1.0 1.0 0.000000 0.000000 0.000000 0.001953 0.001953 0.001953 0.003906 0.003906 0.003906 ...这个文件表示颜色输入范围是 0.0 到 1.0每个维度采样 33 个点形成一个 33x33x33 的三维映射表。实际应用时图像里的任意 RGB 值都会在表中找到对应输出。很多人容易把 LUT 和“滤镜”混为一谈。滤镜通常是预设好的一组视觉风格LUT 则是滤镜背后的颜色映射数据。同一张 LUT可以在 DaVinci Resolve、Premiere Pro、ffmpeg 里反复使用。这也是为什么调色项目里LUT 文件本身要单独保存、单独版本管理。1.2 PIP 为什么能帮调色做判断PIP 是 Picture-in-Picture 的缩写中文叫画中画。最常见的场景是直播时把摄像头画面放到游戏画面角落但在调色工作流里它的作用更大主画面显示处理后的效果角落子画面显示原始未处理画面。这样可以在同一张图里完成“前后对比”不需要来回切换文件也不会因为看图环境变化而误判。PIP 看起来只是多了一个小窗口实际却很考验处理细节两个画面必须使用相同的分辨率缩放逻辑避免比例被压扁子画面位置要避开关键主体输出时还要考虑边框、阴影和透明度否则对比图会显得杂乱。在 LUT-E 项目中PIP 承担的是“验收”角色。LUT 负责把画面从灰蒙蒙变成通透PIP 负责用分钟级的速度给出对比结果。调色完成不等于“看起来更好”而是要在原始画面对比下仍然成立。1.3 LUT-E 这次要解决什么问题这次项目的输入素材是一张带噪声和灰雾感的测试图亮度集中在中间暗部没有沉下去亮部也没有层次。目标是做一套可复现的 LUT 调色流程实现三个效果提高对比度让画面从“灰”变“通透”微调色温去掉偏色适当锐化让边缘更清楚但不产生明显噪点。最终用 PIP 把原图和结果放在同一张图里形成“清图胜利”的验证结果。整条流程不依赖图形界面使用 Python 脚本生成 LUT 文件再用 ffmpeg 合成对比图。这样做的好处是换一张图片、换一批参数只要重跑脚本就能得到同样效果。2. 环境准备先搭好可重复运行的骨架2.1 工具链选型LUT-E 的核心是三个组件Python 负责生成 LUT 和应用 LUTffmpeg 负责生成 PIP 对比图YAML 负责记录参数。这样做的原因很简单脚本可重复执行配置可版本管理输出目录固定不会出现“这次调得顺手下次找不到参数”的问题。组件用途建议版本Python运行 LUT 生成和应用脚本3.9 及以上ffmpeg生成 PIP 对比图后续可扩展视频批处理6.0 及以上numpy处理三维 LUT 网格和图像数组1.24 及以上Pillow图片读写、锐化9.4 及以上PyYAML读取 YAML 配置6.0 及以上如果你的电脑里还没有这些工具建议先使用 Python 虚拟环境避免污染系统目录。ffmpeg 建议直接使用官方网站提供的静态构建版本因为它包含的滤镜组件更完整。2.2 目录结构工程目录不需要复杂但要分层清晰。推荐结构如下lute-demo/ ├── config/ │ └── lut_e.yaml ├── input/ │ └── original.png ├── luts/ │ └── lut_e.cube ├── output/ │ ├── applied.png │ └── pip_preview.png └── scripts/ ├── make_lut.py ├── apply_lut.py └── make_sample.pyconfig 放调色参数luts 放生成的 LUT 文件output 放中间产物和最终对比图scripts 放脚本。原图放在 input 下处理后的图片不覆盖输入文件。这个约定能避免误操作也让批处理脚本更容易扩展。2.3 安装依赖并验证在命令行执行mkdir -p lute-demo/{config,input,luts,output,scripts} cd lute-demo python -m venv venv source venv/bin/activate pip install --upgrade pip pip install numpy pillow pyyaml ffmpeg -versionWindows 下激活虚拟环境的命令是venv\Scripts\activate其余命令一致。如果python命令指向的是 Python 3.9 以上版本安装后可以用一个更直接的检查命令确认python -c import numpy, PIL, yaml; print(deps ok) ffmpeg -filters | grep overlaygrep overlay是为了确认 ffmpeg 编译时包含了 overlay 滤镜。PIP 合成依赖这个滤镜如果版本过旧或编译配置不全后面会报错。2.4 生成一张测试原图为了方便复现可以直接用脚本生成一张带渐变和噪声的图片模拟“灰蒙蒙”的效果。新建scripts/make_sample.py#!/usr/bin/env python3 import numpy as np from PIL import Image def main(): rng np.random.default_rng(42) w, h 1920, 1080 r np.linspace(120, 200, w, dtypenp.uint8) g np.linspace(110, 180, w, dtypenp.uint8) b np.linspace(100, 160, w, dtypenp.uint8) r np.tile(r, (h, 1)) g np.tile(g, (h, 1)) b np.tile(b, (h, 1)) noise rng.normal(0, 8, (h, w, 3)) rgb np.stack([r, g, b], axis2) rgb np.clip(rgb noise, 0, 255).astype(np.uint8) Image.fromarray(rgb).save(input/original.png) print(input/original.png saved) if __name__ __main__: main()运行python scripts/make_sample.py这张图的特点是整体偏灰、层次不强但保留渐变和噪声适合验证 LUT 后的对比度变化。实际项目中这一步替换为真实素材即可。3. 实现一个最小的 LUT-E 流程3.1 用 YAML 记录调色参数配置文件config/lut_e.yaml定义了整个流程的核心参数。为什么用 YAML 而不是直接把参数写死在 Python 脚本里因为调色是一个反复试参数的过程配置和代码分离后改参数不用动到执行逻辑也更容易记录“哪一版参数产生了哪一张图”。project: lut-e image: input/original.png output_dir: output color_space: sRGB lut: name: lut_e size: 33 exposure: 1.06 contrast: 1.16 saturation: 1.08 temperature: -6 post: sharpen: 0.8 pip: enabled: true position: bottom-right margin: 40 scale: 0.35这里先解释几个关键字段lut.size3D LUT 的采样密度常见是 17、33、65。33 是质量和文件大小的中间选择。exposure整体亮度增益。大于 1 变亮小于 1 变暗。contrast对比度系数。大于 1 拉大亮暗差距小于 1 会让画面变灰。saturation饱和度系数。调整颜色鲜艳程度。temperature色温偏移负值偏冷正值偏暖。post.sharpenLUT 应用后的锐化强度数值越大边缘越锐利。pip.scalePIP 子画面相对于主画面的缩放比例。3.2 用 Python 生成 CUBE 格式 LUTscripts/make_lut.py读取 YAML生成一个 3D LUT 文件。代码只做了最简单的亮度、对比度、饱和度和色温调整方便看清每一段之间的关系。#!/usr/bin/env python3 import argparse import yaml import numpy as np def load_config(path): with open(path, r, encodingutf-8) as f: return yaml.safe_load(f) def identity_3d(size33): values np.linspace(0.0, 1.0, size) grid np.stack( np.meshgrid(values, values, values, indexingij), axis-1 ) return grid.reshape(-1, 3) def adjust_3d(lut, cfg): lut_cfg cfg[lut] exposure lut_cfg.get(exposure, 1.0) contrast lut_cfg.get(contrast, 1.0) saturation lut_cfg.get(saturation, 1.0) temperature lut_cfg.get(temperature, 0) out lut * exposure out (out - 0.5) * contrast 0.5 gray out.mean(axis-1, keepdimsTrue) out gray (out - gray) * saturation out[..., 0] temperature / 255.0 return np.clip(out, 0.0, 1.0) def save_cube(lut, path, size33, titlelut_e): with open(path, w, encodingutf-8) as f: f.write(fTITLE {title}\n) f.write(fLUT_3D_SIZE {size}\n) f.write(DOMAIN_MIN 0.0 0.0 0.0\n) f.write(DOMAIN_MAX 1.0 1.0 1.0\n) for r, g, b in lut: f.write(f{r:.6f} {g:.6f} {b:.6f}\n) if __name__ __main__: parser argparse.ArgumentParser(descriptionGenerate CUBE LUT from YAML config) parser.add_argument(--config, requiredTrue) parser.add_argument(--output, requiredTrue) args parser.parse_args() cfg load_config(args.config) lut_size cfg[lut].get(size, 33) lut identity_3d(lut_size) lut adjust_3d(lut, cfg) save_cube(lut, args.output, lut_size, cfg[lut].get(name, lut_e)) print(fsaved {args.output} with {len(lut)} entries)执行python scripts/make_lut.py --config config/lut_e.yaml --output luts/lut_e.cube生成的文件就是标准的 3D LUT可以直接导入 DaVinci Resolve、Premiere Pro 等软件。这里使用“先建单位 LUT再调整”的方式优点是代码逻辑直观每个参数只影响一段处理调试时容易定位问题。要注意这种最简单实现没有做真正的三维插值图像应用时用的是最近邻查找。生产环境建议使用三线性插值或四面体插值否则渐变区域可能出现轻微色块。3.3 把 LUT 应用到图片scripts/apply_lut.py读取.cube文件并将颜色映射到目标图片上。它在 LUT 映射后额外做一步锐化把“清理画面”的观感补全。#!/usr/bin/env python3 import argparse import yaml import numpy as np from PIL import Image, ImageFilter def load_cube(path): size 33 values [] with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if not line or line.startswith(#): continue if line.startswith(LUT_3D_SIZE): size int(line.split()[-1]) elif line.startswith(TITLE) or line.startswith(DOMAIN): continue else: parts line.split() if len(parts) 3: values.append([float(x) for x in parts]) data np.array(values, dtypenp.float32).reshape(size, size, size, 3) return size, data def apply_cube(image_path, cube_path, out_path, sharpen0.0): img Image.open(image_path).convert(RGB) arr np.asarray(img, dtypenp.float32) / 255.0 size, lookup load_cube(cube_path) h, w, _ arr.shape idx (arr * (size - 1)).astype(np.int32) r idx[..., 0] g idx[..., 1] b idx[..., 2] mapped lookup[r, g, b] out (np.clip(mapped, 0.0, 1.0) * 255).astype(np.uint8) result Image.fromarray(out) if sharpen 0: result result.filter( ImageFilter.UnsharpMask(radius2, percentsharpen * 100, threshold3) ) result.save(out_path) return result if __name__ __main__: parser argparse.ArgumentParser(descriptionApply CUBE LUT to an image) parser.add_argument(--config) parser.add_argument(--image, requiredTrue) parser.add_argument(--lut, requiredTrue) parser.add_argument(--output, requiredTrue) args parser.parse_args() sharpen 0.0 if args.config: with open(args.config, encodingutf-8) as f: cfg yaml.safe_load(f) sharpen cfg.get(post, {}).get(sharpen, 0.0) apply_cube(args.image, args.lut, args.output, sharpen) print(applied LUT and saved:, args.output)运行python scripts/apply_lut.py \ --config config/lut_e.yaml \ --image input/original.png \ --lut luts/lut_e.cube \ --output output/applied.png这段代码有几个关键点查找时用(arr * (size - 1))把 0 到 1 的颜色值映射到表的索引区间lookup[r, g, b]直接按三维索引取值得到的是该颜色对应的输出 RGBLUT 处理后立刻 clamp 到 0 到 1再转回 0 到 255避免越界导致偏色锐化放在 LUT 之后因为锐化属于空间处理无法写进逐像素的 LUT 文件。3.4 用 ffmpeg 生成 PIP 对比图生成 LUT 并应用后需要一张对比图验收。使用 ffmpeg 的 overlay 滤镜把原图缩略图放在主画面右下角ffmpeg -y \ -i output/applied.png \ -i input/original.png \ -filter_complex \ [0:v]scale1920:1080:force_original_aspect_ratioincrease,crop1920:1080,setsar1[base]; \ [1:v]scaleiw*0.35:ih*0.35,setsar1[small]; \ [base][small]overlayW-w-40:H-h-40:formatauto[out] \ -map [out] -frames:v 1 output/pip_preview.png这段命令的意思是第一个输入是处理后的图片把它固定到 1920x1080作为主画面第二个输入是原始图片缩放为原来的 0.35 倍作为角落子画面overlay 滤镜把子画面放到主画面的右下角距离边距 40 像素输出一张静态 PNG。如果想要子画面在左下角把overlay的坐标改成40:H-h-40即可。其他位置可以根据需要调整。3.5 用 run.sh 串联整条流程开发阶段可以一条一条执行命令但第二次、第三次调参时最好用一个脚本把三步串起来#!/usr/bin/env bash set -euo pipefail PYTHONvenv/bin/python CONFIGconfig/lut_e.yaml LUT_OUTluts/lut_e.cube IMG_INinput/original.png IMG_LUToutput/applied.png PIP_OUToutput/pip_preview.png python scripts/make_sample.py $PYTHON scripts/make_lut.py --config $CONFIG --output $LUT_OUT $PYTHON scripts/apply_lut.py --config $CONFIG --image $IMG_IN --lut $LUT_OUT --output $IMG_LUT ffmpeg -y \ -i $IMG_LUT \ -i $IMG_IN \ -filter_complex \ [0:v]scale1920:1080:force_original_aspect_ratioincrease,crop1920:1080,setsar1[base]; \ [1:v]scaleiw*0.35:ih*0.35,setsar1[small]; \ [base][small]overlayW-w-40:H-h-40:formatauto[out] \ -map [out] -
返回列表