计算反向 LUT

Andy Guo原创
  • Color
  • 三十分钟系列
  • Color
  • Color Science
  • 色彩空间
  • 色彩科学
  • LUT
大约 8 分钟

计算反向 LUT

经常会有需求,对于一个LUT 进行求逆。

如果是 Arri 这种文档详细的公司,那么可以得知具体的算法,那么肯定可以得到反向LUT

但是很多情况下,我们只有LUT,在这种情况下,得到“精确”的反向LUT 会非常困难:

  • 摄影机厂家自己提供的转换LUT
  • 某些特定的调色LUT

反向LUT 的求解困难

反向求解LUT 的困难主要存在以下几个方面:

  1. LUT 存在 “多对一” 的映射关系,反向求解无法还原精确的结果
  2. 可能存在LUT 超界的情况。正向LUT 导致超过1,而反向LUT 只能还原一部分
  3. 正向LUTsrc 是等间隔的 lattice,但是映射后由于存在插值的情况,导致dst 会落在lattice 中间。所以反向LUTsrc 并不会直接对应正向LUT 中的结果

第一个、第二个问题,这是无法解决的。

对于第三个问题,我们可能会有很多算法来解决。

我们使用2D 的grid 来说明这个问题:

正向 LUT 的 src

微信图片_20200414203140

正向 LUT 的 dst

微信图片_20200414204940

如何求解反向LUT src 对应的 dst?

微信图片_20200414204945

利用机器学习的思路来解决 lattice 对位

于是,有了一个思路。

我们可以通过不断的穷举 src 中的某个具体颜色c,通过正向LUT 变换后,得到的c'

直到c' 落在对应的lattice 网格上。

此时c' 就是反向的LUTsrc shaperc 就是反向LUT 中的dst 数值

很明显,这里的穷举肯定会有一些技巧。对于一个“好” 的LUT 来说,至少会满足几个特性:

  • 实际光照强度和code value 应该是单调递增的关系(不会出现越亮反而数值越小)
  • 只存在“压缩” 的情况,而不存在完全变成一个数值的情况(多对一)

这就正好利用了梯度下降的思路。

微信图片_20200414204949

代码实现

详情
import colour
import numpy as np
import torch
import torch.autograd as autograd
import torch.nn as nn
import torch.optim as optim
from colour.algebra import table_interpolation_tetrahedral


class Tetrahedral(autograd.Function):
    @staticmethod
    def forward(ctx, input, lut):
        ctx.lut = lut.table
        ctx.input = input
        result = table_interpolation_tetrahedral(
            input.clone().detach(), lut.table)
        return torch.from_numpy(result)

    @staticmethod
    def backward(ctx, grad_output):
        input, lut = ctx.input, ctx.lut
        delta = 1e-5
        f_plus_delta = input.clone().detach()
        f_plus_delta += delta
        f_minus_delta = input.clone().detach()
        gradient = (table_interpolation_tetrahedral(f_plus_delta, lut) -
                    table_interpolation_tetrahedral(f_minus_delta, lut)) / delta
        return (torch.from_numpy(gradient) + torch.randn(gradient.shape)*0.03) * grad_output, None


class LUT:
    def __init__(self, lut_path):
        self.forward_lut = colour.read_LUT(lut_path)
        self.backward_lut = None
        self.backward_lut_step = None

    def invert(self, step=33, error=1e-5, max_iter=10000, clamp=True):
        mesh_x, mesh_y, mesh_z = torch.meshgrid(torch.linspace(0, 1, step),
                                                torch.linspace(0, 1, step),
                                                torch.linspace(0, 1, step))
        lattice = []
        for item in zip(mesh_x.reshape(-1),
                        mesh_y.reshape(-1),
                        mesh_z.reshape(-1)):
            lattice.append([item[2].item(), item[1].item(), item[0].item()])
        lattice = torch.tensor(lattice, dtype=torch.float64)

        X = torch.ones(step**3, 3) * 0.5
        X.requires_grad_()
        interpolate = Tetrahedral.apply
        criterion = nn.MSELoss()
        optimizer = torch.optim.Adam([X], lr=1e-2)

        epoch = 0
        while epoch < max_iter:
            optimizer.zero_grad()
            Y = interpolate(X, self.forward_lut)
            loss = criterion(Y, lattice)
            loss.backward()
            optimizer.step()
            print(f'{epoch} loss = {loss}')
            epoch += 1

        self.backward_lut = X
        if clamp:
            self.backward_lut = torch.clamp(self.backward_lut, 0, 1)
        self.backward_lut_step = step
        return self.backward_lut

    def save_invert_lut(self, cube_path):
        if self.backward_lut is None:
            raise Exception(
                'place run .invert() method before .save_invert_lut()')

        result_string = f'LUT_3D_SIZE {self.backward_lut_step}\n'
        result_string += '\n'.join(
            (f'{p[0]:.06f}\t{p[1]:.06f}\t{p[2]:.06f}' for p in self.backward_lut))
        with open(cube_path, 'w') as f:
            f.write(result_string)


if __name__ == '__main__':
    lut = LUT('/Users/andyguo/Desktop/Slog3-S-Gamut3.Cine_To_s709.cube')
    result = lut.invert(step=33, max_iter=100000)
    print(result)
    lut.save_invert_lut('/Users/andyguo/Desktop/invert_sony_600.cube')

算法效果

sony slog3 sgamut3.cine to s709LUT 为例。

进行反向求解,在解算过程中所有误差的统计信息如下:

err_rate 0.5272003784400479

min: 0.0

max: 0.0156503826299

mean: 8.41535125187e-05

var: 7.76369308248e-08

median: 3.4147489589e-05

从空间的lattice 也可以看出,比较好的抵消了正向LUT 的变化。使得结果在有效范围内回归到标准lattice 平直的状态。

标准lattice正向LUT正向+ 反向抵消
top微信图片_20200414204953微信图片_20200414204957微信图片_20200414205002
front微信图片_20200414205005微信图片_20200414205010微信图片_20200414205014
right微信图片_20200414205017微信图片_20200414205020微信图片_20200414205024
perspective微信图片_20200414205027微信图片_20200414205031微信图片_20200414205034

最后,可以从实际拍摄的素材中来查看人眼实际感觉上的差异,可以认为非常小

原始Slog SGamut3.cine转换为s709
s709+反向LUT差异(放大64倍)
差异统计信息
img

潜在的问题

目前的算法,没有对求解得到的反向结果进行平滑。这就导致了反向LUT 其实存在轻微的抖动(jitter)。

个人理解,这种抖动主要是由于在进行梯度下降的过程中,优化结果在真实解的附近来回跳动,而临近的每一个lattice 都会出现不同的跳动。

img

在图像上直观的反应,就是平滑色彩的过度不是非常平滑。

原始图像(gain = 4)正向+反向LUT 抵消(gian=4)
img

之后,可以考虑对所有的反向求解point,进行laplacian smooth

应该可以优化这个问题。(暂时没有进行测试)

我可以认为如果LUT 中相邻lattice 的色彩数值应该是平滑的,如果出现了抖动,那就可能造成这个问题:

RGB
imgimgimg

参考阅读

https://colour.readthedocs.io/en/develop/_modules/colour/algebra/interpolation.htmlopen in new window

Colour 库 实现的 tetrahedral 3D LUT 插值算法,经过验证和nuke 中的结果完全一致)

相关文件下载

文中的代码和对应 sony invert lut 文件

文件下载

点击购买open in new window,或者扫描二维码

面包多二维码

温馨提示

现在有 Andy 十分钟色彩科学合集售卖open in new window

十分钟色彩科学面包多二维码

购买后加微信muyanru345,拉入Andy 铁粉儿群,可在群里对学习工作中的色彩疑问进行讨论。

上次编辑于:
贡献者: Yanru Mu