Hello!距这个小ISP的软件实现大概过去两年了,一直没有打起精神来将连载写完。但是我们绝不烂尾,绝不!
Image Signal Processing-第四章-LSC, CC的原理和软件实现
连载:
Image Signal Processing(ISP)-第一章-ISP基础以及Raw的读取显示
Image Signal Processing(ISP)-第二章-Demosaic去马赛克以及BMP软件实现
Image Signal Processing(ISP)-第三章-BCL, WB, Gamma的原理和软件实现
Image Signal Processing(ISP)-第四章-LSC, CC的原理和软件实现
Github:
BoyPao/ImageSignalProcessing-ISP
上文我们介绍了BLC,WB,Gamma三种基础的优化操作,并对图像数据进行了这几种基础矫正,图片的效果向人眼真实感受靠近。但明显的是,与商用isp处理的效果有非常大的差距,所以同志们要继续努力呀。
让我们先回顾一下上文结束时的修改效果以及图片仍然存在的问题吧。
此阶段主要问题:1.图片边角的亮度低,2. 图片色彩饱和度很低。
本文将介绍针对这两个问题的解决方法:1. LSC,2.CC。
首先我们先解决亮度不均的问题。
1. Lens Shading Correction (LSC)
我们看到上图的亮度是不均匀的,较亮的部分集中在图像的中央,而周围的部分相比而言要暗很多。这种亮度不均的现象主要是由于镜头对光线的折射造成的。Lens shading译为镜头阴影,其指的就是这种现象。
如上图,光学镜头一般采用镜片组实现对外界光的采集,由于CMOS的尺寸一般较小,镜片组对光路的导向总体上需要是集中的。因此大部分光将打到CMOS的中间区域,而四周的光强会相对小一些。对于手机镜头,安防镜头,无人机镜头等这类小镜头来说,这种显现更为明显。
1.1 LSC原理
Lens Shading Correction(LSC)就是处理 lens shading 这种亮度不均的一种矫正方法。当然了,原理是非常简单的,我们只需将成像的四周进行相应的增益计算,就可以使四周亮度看起来和中央亮度近似了。
上图表示的是存在lens shading现象的镜头对白色卡片或墙面拍照输出图片的最中间的一行以及任意某行的亮度分布。对这两行的中央较亮的像素点,设置增益趋近于1,让他们尽量保持这个像素值,而对于两侧较暗的像素,逐渐增大增益以提高矫正后的像素值。用这样的方法,来解决lens shading。
说到Shading,还有一种叫color shading的现象。指的是由于镜片对不同频率的光折射率不同造成各种颜色的光的lens shading 程度轻重不同的现象。也就是说对同一镜片组来说折射率大的色光,其lens shading更严重一些。为解决这个问题,我们的方法也很简单,就是对各种颜色通道采取不同的增益,来分而治之。
这里需要强调的是,我们对各通道采取不同的增益并不意味着我们是在改变色调,因为事实上各通道的增益都是中间小四周大的,差异值是非常小的数字。而亮度值就是各个通道像素值经过加权平均所得,因此此处对各色彩通道采用略有差异的增益,实质上是希望改变整体亮度,而不是色调的。
1.2 LSC软件实现
以下贴出软件实现。
BZResult BZ_LensShadingCorrection(void* data, LIB_PARAMS* pParams, ISP_CALLBACKS CBs, ...)
{
BZResult result = BZ_SUCCESS;
(void)CBs;
if (!data) {
result = BZ_INVALID_PARAM;
BZLoge("data is null! result:%d", result);
}
if (SUCCESS(result)) {
int32_t width, height;
LIB_BAYER_ORDER bayerOrder = LIB_BAYER_ORDER_NUM;
int32_t i, j;
float* pR = nullptr;
float* pGr = nullptr;
float* pGb = nullptr;
float* pB = nullptr;
float* pLut = nullptr;
width = pParams->info.width;
height = pParams->info.height;
bayerOrder = pParams->info.bayerOrder;
BZLogd("width:%d height:%d type:%d", width, height, bayerOrder);
switch (bayerOrder)
{
case LIB_RGGB:
pR = pParams->LSC_param.GainCh1;
pGr = pParams->LSC_param.GainCh2;
pGb = pParams->LSC_param.GainCh3;
pB = pParams->LSC_param.GainCh4;
break;
case LIB_BGGR:
pB = pParams->LSC_param.GainCh1;
pGb = pParams->LSC_param.GainCh2;
pGr = pParams->LSC_param.GainCh3;
pR = pParams->LSC_param.GainCh4;
break;
case LIB_GRBG:
pGr = pParams->LSC_param.GainCh1;
pR = pParams->LSC_param.GainCh2;
pB = pParams->LSC_param.GainCh3;
pGb = pParams->LSC_param.GainCh4;
break;
case LIB_GBRG:
pGb = pParams->LSC_param.GainCh1;
pB = pParams->LSC_param.GainCh2;
pR = pParams->LSC_param.GainCh3;
pGr = pParams->LSC_param.GainCh4;
break;
default:
result = BZ_INVALID_PARAM;
BZLoge("Unsupported bayer order:%d result:%d", bayerOrder, result);
break;
}
if (SUCCESS(result)) {
for (i = 0; i < height; i++) {
for (j = 0; j < width; j++) {
if (i % 2 == 0 && j % 2 == 0) {
pLut = pB;
}
if (i % 2 == 0 && j % 2 == 1) {
pLut = pGb;
}
if (i % 2 == 1 && j % 2 == 0) {
pLut = pGr;
}
if (i % 2 == 1 && j % 2 == 1) {
pLut = pR;
}
static_cast<uint16_t*>(data)[i * width + j] = static_cast<uint16_t*>(data)[i * width + j] *
LSCInterpolation(width, height,
*(pLut + (LSC_LUT_HEIGHT - 1) * i / height * LSC_LUT_WIDTH + (LSC_LUT_WIDTH - 1) * j / width),
*(pLut + (LSC_LUT_HEIGHT - 1) * i / height * LSC_LUT_WIDTH + (LSC_LUT_WIDTH - 1) * j / width + 1),
*(pLut + ((LSC_LUT_HEIGHT - 1) * i / height + 1) * LSC_LUT_WIDTH + (LSC_LUT_WIDTH - 1) * j / width),
*(pLut + ((LSC_LUT_HEIGHT - 1) * i / height + 1) * LSC_LUT_WIDTH + (LSC_LUT_WIDTH - 1) * j / width + 1),
i,
j);
}
}
}
}
return result;
}
上面的函数写了很多行,但其实前面大部分代码是支持各种Bayer排布的代码,主要是最后调用了LSCInterpolation()来获取每个像素点的增益,再乘上像素点的值来实现矫正。
LSCInterpolation()是一个增益插值函数。为啥需要这个函数呢?前面说到了对不同颜色需要分而治之,那么在bayer域做 lsc 就需要4个 lut (lut是啥上一章介绍过) 来支持 4个颜色通道,这会造成数据量变得庞大。所以采取将图片划分为少量几块,每一块用一个lut值,最后每个像素通过临域块的 lut 值插值得到该像素 lsc增益的方法来降低 lut 的数据量。
用到的LSCInterpolation() 函数如下。
float LSCInterpolation(int32_t WIDTH, int32_t HEIGHT,
float LT, float RT, float LB, float RB,
int32_t row, int32_t col)
{
float TempT, TempB, result;
TempT = LT - (LT - RT) * (col % (WIDTH / (LSC_LUT_WIDTH - 1))) * (LSC_LUT_WIDTH - 1) / WIDTH;
TempB = LB - (LB - RB) * (col % (WIDTH / (LSC_LUT_WIDTH - 1))) * (LSC_LUT_WIDTH - 1) / WIDTH;
result = TempT - (TempT - TempB) * (row % (HEIGHT / (LSC_LUT_HEIGHT - 1))) * (LSC_LUT_HEIGHT - 1) / HEIGHT;
return result;
}
其lsc的部分lut如下。
LSC_PARAM LSCPARM_1920x1080_D65_1000Lux = {
//bGain
{{ 2.934418, 2.6059866, 2.220798, 1.94021428, 1.74472368, 1.600148, 1.50334871, 1.44453084, 1.42655516, 1.45087564, 1.51691735, 1.620021, 1.77389872, 1.984156, 2.26868367, 2.685018, 3.086324 },
{ 2.780221, 2.42652345, 2.055793, 1.81020319, 1.61290979, 1.47168612, 1.37039435, 1.312223, 1.292875, 1.31676388, 1.381954, 1.48782563, 1.63721418, 1.83661532, 2.09502649, 2.48011971, 2.89426875 },
。。。。。。}
//gbGain
{{ 3.20395136, 2.84868073, 2.42183638, 2.12311363, 1.91179454, 1.756917, 1.64773118, 1.58069026, 1.55754256, 1.5834856, 1.645207, 1.74964738, 1.90877008, 2.1196, 2.41837454, 2.8678813, 3.28876615 },
{ 3.04708457, 2.663059, 2.24919081, 1.97792578, 1.76208353, 1.60036659, 1.48812246, 1.417937, 1.39421225, 1.41655445, 1.48318923, 1.59559619, 1.75348139, 1.96583772, 2.233202, 2.655679, 3.10571742 },
。。。。。。}
//grGain
{{ 3.25572634, 2.89292026, 2.4517138, 2.14141822, 1.91608775, 1.75056577, 1.6297735, 1.55698335, 1.53054082, 1.55629158, 1.62307167, 1.74096286, 1.90679991, 2.12313032, 2.435579, 2.887383, 3.319794 },
{ 3.1139915, 2.71121383, 2.28292322, 2.001553, 1.773555, 1.60334086, 1.47839546, 1.40285826, 1.37624037, 1.39933944, 1.46882379, 1.58867931, 1.75515807, 1.97776151, 2.256206, 2.685162, 3.150439 },
。。。。。。}
//rGain
{{ 3.26140237, 2.90829229, 2.44395447, 2.14117551, 1.92580986, 1.76098931, 1.64909816, 1.57704759, 1.554534, 1.58389091, 1.65474868, 1.77599037, 1.95483339, 2.17210364, 2.49666047, 2.99027562, 3.42638421 },
{ 3.10735345, 2.697388, 2.26802278, 1.99615026, 1.77538717, 1.60802543, 1.48951221, 1.41702485, 1.39288378, 1.420929, 1.49541867, 1.61999846, 1.79255354, 2.01902175, 2.304034, 2.75090647, 3.242971 },
。。。。。。}
在ISP中调用上面介绍的函数以实现LSC操作。
1.3 LSC效果
让我们看看矫正后的结果吧。
可以看到四周的亮度被提高了,看起来整幅图片的亮度的分布比较均匀了。
让我们看看与商用isp的对比。
现在看来我们急需要解决饱和度的问题了。那还等什么呢?
2. Color Correction (CC)
要解决饱和度低的问题,就需要对色彩进行矫正,也就是这一章我们要介绍的color correction。
对颜色的矫正需求来源于sensor输出的数据和人眼对实物的感受之间存在的色彩差异。而Color correction其本质就是对数据进行人眼真实感受值映射的一种过程。
但无论我们采用什么样的映射方法,首先要确定的是描述颜色的方法。此前我们说过该 ISP 的 CC 操作位于 RGB域,也就是说当前步骤采用RGB色彩空间来描述颜色。
所以我们的CC就是在RGB域进行的对sensor输出数据向人眼真实感受值的一种映射。
2.1 CC原理
为了实现这种映射过程,我们需要获取到数据和感官真实值之间的具体的映射方式。确定映射方式的一种方法是标定法。即利用标定好的某种颜色的标准值和sensor拍摄该值实物图片所输出值做差,来求得CC对该颜色需要做的映射。
我们常见的color checker 色卡就是标定好的颜色,我们知道色卡上每个色块在RGB域的真实感受值,所以我们可以在某种色温环境下对该色卡进行拍摄,输出一份未作CC的数据,用这个数据和标定值做差来确定为该色温环境下这个sensor需要做的CC映射。
此后在该色温环境下使用该sensor时,对其输出作CC,就可以还原出人眼真实感受值了。
由于在RGB域有三个通道,我们的这种映射关系可以由矩阵来表示,这就是我们常说的Color Correction Metric(CCM)。
2.2 CC软件实现
cc的软件实现采用了CCM进行矩阵计算,参考了imtest官网对CCM的介绍。以下贴出代码。
BZResult BZ_ColorCorrection(void* data, LIB_PARAMS* pParams, ISP_CALLBACKS CBs, ...)
{
BZResult result = BZ_SUCCESS;
(void)CBs;
if (!data) {
result = BZ_INVALID_PARAM;
BZLoge("data is null! result:%d", result);
}
if (SUCCESS(result)) {
int32_t width, height;
float* pCcm;
int32_t i, j;
width = pParams->info.width;
height = pParams->info.height;
pCcm = pParams->CC_param.CCM;
BZLogd("width:%d height:%d", width, height);
for (i = 0; i < CCM_HEIGHT; i++) {
BZLogd("CCM[%d]:%f CCM[%d]:%f CCM[%d]:%f", i * CCM_WIDTH, pCcm[i * CCM_WIDTH],
i * CCM_WIDTH + 1, pCcm[i * CCM_WIDTH + 1],
i * CCM_WIDTH + 2, pCcm[i * CCM_WIDTH + 2]);
}
Mat CCM = Mat::zeros(3, 3, CV_32FC1);
for (i = 0; i < 3; i++) {
for (j = 0; j < 3; j++) {
CCM.at<float>(i, j) = *(pCcm + i * CCM_WIDTH + j);
}
}
uint16_t* B = static_cast<uint16_t*>(data);
uint16_t* G = B + width * height;
uint16_t* R = G + width * height;
Mat outMatric;
Mat inMatric(width * height, 3, CV_32FC1);
for (i = 0; i < height * width; i++) {
inMatric.at<float>(i, 0) = (float)B[i];
inMatric.at<float>(i, 1) = (float)G[i];
inMatric.at<float>(i, 2) = (float)R[i];
}
outMatric = inMatric * CCM;
for (i = 0; i < height * width; i++) {
if (outMatric.at<float>(i, 0) > 1023)
outMatric.at<float>(i, 0) = 1023;
if (outMatric.at<float>(i, 0) < 0)
outMatric.at<float>(i, 0) = 0;
if (outMatric.at<float>(i, 1) > 1023)
outMatric.at<float>(i, 1) = 1023;
if (outMatric.at<float>(i, 1) < 0)
outMatric.at<float>(i, 1) = 0;
if (outMatric.at<float>(i, 2) > 1023)
outMatric.at<float>(i, 2) = 1023;
if (outMatric.at<float>(i, 2) < 0)
outMatric.at<float>(i, 2) = 0;
B[i] = (uint16_t)outMatric.at<float>(i, 0);
G[i] = (uint16_t)outMatric.at<float>(i, 1);
R[i] = (uint16_t)outMatric.at<float>(i, 2);
}
}
return result;
}
这里采用了Opencv的 Mat 类快速地实现矩阵相乘,由于结果可能超出此时10bit 的编码上限,因此需要对超出的数据进行修正,修正为10bit的上限值1023。
这里的CCM为:
CC_PARAM CCPARAM_1920x1080_D65_1000Lux{
{{ 1.819, -0.248, 0.213 },
{ -1.069, 1.322, -1.078 },
{ 0.250, -0.074, 1.865 }}
};
由CCM第一行可以看出,我们对B通道的映射是将其蓝色分量进行强调,红色分量稍微提高,绿色分量稍微减弱,总体上实现了B通道数据在RGB域向B轴正方向的偏移,以此提高了B通道的饱和度。第二行和第三行都实现了类似的偏移,饱和度都得到了提高。需注意,该CCM只适用于在D65光源下我测试使用的这个sensor。
2.3 CC效果
现在让我们看看CC处理前后的效果对比吧。
很明显经过CC,图片的色彩得到了改善。
让我们看一下这一章介绍的LSC以及CC的总体效果吧。
如此看来,是不是已经很接近商用ISP的效果了呢?让我们放大看看细节吧。
可以看出目前我们的ISP已经对raw作出了很大的改善,但我们的输出有很多红绿的色斑,并且对比度不够高,锐度不够强呀。哈哈哈哈,所以下一章,也就是此系列的终章,我会针对这个两个问题做一些改善。
敬请期待!