【ISP】CCM变换和标定

1.CCM的简单介绍

1.1人眼的特性

人眼视网膜的微观结构如下图所示。

从图中可以看到,人眼视网膜上有三种锥细胞(cone)用于感受蓝、绿、红三种频率的色光,一种杆细胞(rod)只在低照度条件下感应亮度信息,不能分辨颜色和细节。

已知三种锥细胞的总个数大约600~800万,分布比例大约是1:16:32,杆细胞的总数大约是1200万。人眼的三种锥细胞对不同波长的电磁波的响应是有明显区别的,研究发现,人眼的红、绿、蓝三种锥细胞光谱响应曲线的峰值比例是0.54:0.575:0.053,如下图所示。

如从图中曲线可以看到,人眼对波长为555纳米的绿色光吸收率可达20%,对波长为650纳米的橙色光的吸收率在4%左右,对波长为450纳米的蓝色光的吸收率最高只有2%左右,大约是绿色光最高吸收率的1/10。

因此,对于辐射强度相同而颜色(波长)不同的光,人眼的亮度感觉是不同的:波长为555纳米的黄绿光感觉最亮,波长为450纳米的蓝色光感觉最暗。另外还发现,当光线微弱时,人眼最敏感的波长会移动至507nm。

人眼对不同波长的光有不同的色调感觉,严格地讲,只有波长为572nm的黄光、503nm的绿光和478nm的蓝光,其色调不随光强而变化,其它波长的色光都随光强的改变略有变化。在可见光谱中,从紫到红分布着各种不同的颜色,人眼能分辨出色调差别的最小波长变化称为色调分辨阈.其数值随波长而改变.人眼对480~640nm区间色光的色调分辨力较高,其中,对500nm (青绿色)和600nm(橙黄色〉两个波长来说,只要波长变化约1nm,便可分辨出色调的变化。而从655nm的红色到可见光谱长波末端,以及从430nm的紫色到可见光谱短波末端,人眼几乎感觉不到色调的差别。当饱和度减小时,人眼的色调分辨力将下降;当亮度太大或太小时,色调分辨力也会下降。

1.2CCM颜色校正

人眼在可见光波段的频谱响应度和半导体传感器的频谱响应以及显示器的激励响应都存在较大的差别,这些差别会对摄像机的色彩还原造成较大的影响。举例来说,从下图所示的光谱响应曲线中可以了解到,一个典型的硅材料sensor在500nm处对蓝、绿光的响应几乎是相等的,但是人眼的蓝色锥细胞对500nm的蓝绿光响应却几乎为零。假设sensor按照自己的特性忠实地记录下它对500nm波长的响应值为b,根据CIE的标准,显示器会在b值的驱动下发出波长为435.8nm的蓝光,使人眼感知到明亮的蓝光。这个过程在人眼看来,就是成像系统在绿色中凭空增加了很多蓝色的成分,降低了绿色的饱和度,造成了颜色失真。

下图是因为人眼和cmos都是针对不同光谱之间的响应程度,是在前端响应端。

有些人会使用LMS那张人眼的图,我认为不合适,因为LMS是使用模拟的三种刺激光,是属于显示器端的发光情况,不属于全光谱的响应情况。(这段有待商榷,我后续有新的理解再来补充)

不仅人眼的响应与sensor不同,不同厂家制造的sensor响应也是不同的。下图显示了三个camera在相同参数下拍摄到的颜色效果。

此外,摄像机光路上一般还存在镜头、滤波片等光学元件,镜头的镀膜、滤光片的频率响应等参数也会对色彩还原造成影响,这些因素综合作用的结果,就是人眼在显示器上看到的RGB颜色与真实世界中感知到的物体颜色存在偏差, 尤其是色饱和度受到较大影响,因此必须对摄像机记录的颜色进行校正以还原人眼的感知效果。

颜色校正,英文color correction,是在RGB空间中完成的处理任务,主流的做法是用一个3x3 的矩阵将一个输入像素值(R, G, B)线性地映射为一个新的像素值(R', G', B'),通过审慎地选择矩阵参数使映射后的颜色更符合人的认知习惯。

Color Correction Matrix (CCM)
The Color Correction Matrix block corrects the image color variations coming from many different causes that can include spectral characteristics of the optics (lens, filters), lighting source variations, characteristics of the color filters of the sensor and many others. The CCM block contains 3x3 programmable coefficient matrix multipliers with offset compensation that can be used in color correction operations such as adjusting white balance, color cast, brightness, or contrast in an image.

为什么ISP模块中要有这个模块呢,上面列的一堆原因,最重要的因素是我们肉眼的对光谱的RGB响应曲线和sensor的响应曲线是不同的(但请注意sensor的QE也不能与人眼相差太远,不然CCM是救不会来的)。

CCM一般是3x3矩阵形式,也有3x4形式的,3x4形式主要是给rgb各自加一个offset

上面的人眼rgb响应和sensor rgb响应曲线都是非线性的,所以指望通过一个CCM矩阵就得到匹配度很好的映射关系是不现实的。现实中,往往会标定很多个CCM,ISP在运行的时候根据照度,光源等等因素,选择两个最近的CCM插值得到最终的CCM。

CCM公式的物理意义是从一种颜色种减除另外两种颜色的成分,以增加该颜色的饱和度,使变换的结果接近人的视觉感受,或者更符合人的主观审美。由于输入颜色可以有上千万种组合,而CCM参数却只有9个,所以CCM实际上只能优先保证几个最重要的颜色在人看来是“正确”的,而不可能面面俱到地保证所有颜色在所有条件下都是最优的。下图显示了变换值(虚线)与理想值(实线)之间的差异。

CCM公式的一个基本约束就是不能破坏白平衡,即对于任何R=G=B的输入,必须保证输出满足R'=G'=B'。正式由于这个原因,颜色校正操作只能放在白平衡调整之后执行。

CCM模块在apply awb gain后面,因此3x3个值存在约束条件:

保证灰点也就是r=g=b的点,经过CCM以后仍然r=g=b;

2.标定CCM方法

  • 用camera拍一张某个色温下的24色卡raw文件:

注意shading影响,拍这个色卡占整个sensor中间一小部分就可以

总共24个patch,用前18个patch即可

  • raw文件预处理

主要包括减blc,根据第4行的patch,获取awb gain值,乘上去;这样就拿到了这个色温下24个patch的rgb值;

  • 理想rgb值

这是色卡厂家提供的24个patch的标准rgb空间下的理论值;

拿到这个值以后,需要进行反gamma处理,因为厂家提供的是srgb的值,是带了2.2gamma的,ISP的CCM模块一般是在gamma前面,因此要对理论值进行反gamma处理;

现在准备工作完成了,接下来是算法。

3.标定算法

已知100个raw rgb值,已知对应的理论rgb值;求一个3x3线性变换矩阵;这个矩阵要使得映射后的rgb值尽可能的接近理论值;

借鉴深度学习的梯度下降方法,可以快速得到CCM;并且可以自定义100个patch的重要程度,使得某些patch的误差非常小。

抱歉手头没有24色卡的raw,没有真实数据;只能用随机数据来模拟了;

首先我们随机生成18个随机rgb值,定义一个矩阵,画出它的映射:

import matplotlib.pyplot as plt
import torch

ccm = torch.tensor([[1655, -442, -189], [-248, 1466, -194], [-48, -770, 1842]], dtype=torch.float32)
rgb_data = torch.randint(0, 255, (3, 100))
rgb_data = rgb_data.float()

rgb_target = ccm.mm(rgb_data)/1024.0

fig1 = plt.figure(1)
ax1 = fig1.add_subplot(111, projection='3d')
x2 = rgb_data[0]
y2 = rgb_data[1]
z2 = rgb_data[2]

ax1.scatter(x2, y2, z2, marker='*', c='b', label='origin RGB')

ax1.set_xlim(-80, 360)
ax1.set_ylim(-80, 360)
ax1.set_zlim(-80, 360)
ax1.set_xlabel('R')
ax1.set_ylabel('G')
ax1.set_zlabel('B')

x3 = rgb_target[0]
y3 = rgb_target[1]
z3 = rgb_target[2]

ax1.scatter(x3, y3, z3, marker='o', c='c', label='target rgb')

for i in range(len(x3)):
    ax1.plot([x2[i], x3[i]], [y2[i], y3[i]], [z2[i], z3[i]], 'k-.')
ax1.legend()

plt.show()

映射关系如下:

现在给原始rgb加一些noise,根据上面得到的CCM后的rgb值,来推算CCM,看是否和预定义的CCM接近:

error_manual = torch.randn((3, 100)) * 16
rgb_target_error = rgb_target + error_manual

定义损失和梯度函数,测量rgb值得差异,采用L2距离;

完整代码如下:

这里可以将RGB转成LAB值,通过LAB计算梯度会更加精准。 

from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import torch

ccm = torch.tensor([[1655, -442, -189], [-248, 1466, -194], [-48, -770, 1842]], dtype=torch.float32)
rgb_data = torch.randint(0, 255, (3, 100))
rgb_data = rgb_data.float()

error_manual = torch.randn((3, 100)) * 16
rgb_target = ccm.mm(rgb_data)/1024.0
rgb_target_error = rgb_target + error_manual
ccm_calc1 = torch.tensor([0.0], dtype=torch.float32, requires_grad=True)
ccm_calc2 = torch.tensor([0.0], dtype=torch.float32, requires_grad=True)
ccm_calc3 = torch.tensor([0.0], dtype=torch.float32, requires_grad=True)
ccm_calc5 = torch.tensor([0.0], dtype=torch.float32, requires_grad=True)
ccm_calc6 = torch.tensor([0.0], dtype=torch.float32, requires_grad=True)
ccm_calc7 = torch.tensor([0.0], dtype=torch.float32, requires_grad=True)

def squared_loss(rgb_tmp, rgb_ideal):
    return torch.sum((rgb_tmp-rgb_ideal)**2)

def sgd(params, lr, batch_size):
    for param in params:
        param.data -= lr * param.grad/batch_size;

def net(ccm_calc1, ccm_calc2, ccm_calc3, ccm_calc5, ccm_calc6, ccm_calc7, rgb_data):
    rgb_tmp = torch.zeros_like(rgb_data)
    rgb_tmp[0, :] = ((1024.0 - ccm_calc1 - ccm_calc2) * rgb_data[0, :] + ccm_calc1 * rgb_data[1, :] + ccm_calc2 * rgb_data[2, :]) / 1024.0
    rgb_tmp[1, :] = (ccm_calc3 * rgb_data[0, :] + (1024.0 - ccm_calc3 - ccm_calc5) * rgb_data[1, :] + ccm_calc5 * rgb_data[2, :]) / 1024.0
    rgb_tmp[2, :] = (ccm_calc6 * rgb_data[0, :] + ccm_calc7 * rgb_data[1, :] + (1024.0 - ccm_calc6 - ccm_calc7) * rgb_data[2, :]) / 1024.0
    return rgb_tmp

lr = 3
num_epochs = 100
for epoch in range(num_epochs):
    l = squared_loss(net(ccm_calc1, ccm_calc2, ccm_calc3, ccm_calc5, ccm_calc6, ccm_calc7, rgb_data), rgb_target_error)
    l.backward()
    sgd([ccm_calc1, ccm_calc2, ccm_calc3, ccm_calc5, ccm_calc6, ccm_calc7], lr, 100)
    ccm_calc1.grad.data.zero_()
    ccm_calc2.grad.data.zero_()
    ccm_calc3.grad.data.zero_()
    ccm_calc5.grad.data.zero_()
    ccm_calc6.grad.data.zero_()
    ccm_calc7.grad.data.zero_()
    print('epoch %d, loss %f'%(epoch, l))

res = torch.tensor([[1024.0 - ccm_calc1 - ccm_calc2, ccm_calc1, ccm_calc2],
                    [ccm_calc3, 1024.0-ccm_calc3-ccm_calc5, ccm_calc5],
                    [ccm_calc6, ccm_calc7, 1024.0-ccm_calc6-ccm_calc7]], dtype=torch.float32)
print(res);

rgb_apply_ccm = res.mm(rgb_data)/1024.0

fig1 = plt.figure(1)
ax1 = fig1.add_subplot(111, projection='3d')
fig2 = plt.figure(2)
ax2 = fig2.add_subplot(111, projection='3d')

x2 = rgb_data[0]
y2 = rgb_data[1]
z2 = rgb_data[2]

ax1.scatter(x2, y2, z2, marker='*', c='b', label='origin RGB')

ax1.set_xlim(-80, 360)
ax1.set_ylim(-80, 360)
ax1.set_zlim(-80, 360)
ax1.set_xlabel('R')
ax1.set_ylabel('G')
ax1.set_zlabel('B')

x3 = rgb_target[0]
y3 = rgb_target[1]
z3 = rgb_target[2]

ax1.scatter(x3, y3, z3, marker='o', c='c', label='target rgb')

for i in range(len(x3)):
    ax1.plot([x2[i], x3[i]], [y2[i], y3[i]], [z2[i], z3[i]], 'k-.')
ax1.legend()

ax2.set_xlim(-80, 360)
ax2.set_ylim(-80, 360)
ax2.set_zlim(-80, 360)
ax2.set_xlabel('R')
ax2.set_ylabel('G')
ax2.set_zlabel('B')
ax2.scatter(x3, y3, z3, marker='o', c='c', label='target rgb')

x4 = rgb_apply_ccm[0]
y4 = rgb_apply_ccm[1]
z4 = rgb_apply_ccm[2]
ax2.scatter(x4, y4, z4, marker='^', c='b', label='apply ccm rgb')
ax2.legend()

plt.show()

运行结果对比:

可视化看一下映射后的点与理论点的距离:

很接近了!!!

参考

https://zhuanlan.zhihu.com/p/108626480

https://zhuanlan.zhihu.com/p/98831426

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值