Hello!ISP系列文章终于更新了,距离上一篇文章发布已经过去半年多啦!哈哈,虽然这段时间没有写文,但是这个简单ISP的代码还是有更新的哦,有兴趣的朋友可以到Github查看。话不多说,我们接着讲ISP。
Image Signal Processing-第三章-BCL, WB, Gamma的原理和软件实现
- 1. Black Level Correction
- 1.1 Black Level Correction 原理
- 1.2 Black Level Correction 软件实现
- 2. White Balance
- 2.1 White Balance 原理
- 2.2 White Balance 软件实现
- 3. Gamma
- 3.1 Gamma 原理
- 3.2 Gamma 软件实现
往期:
Image Signal Processing(ISP)-第一章-ISP基础以及Raw的读取显示
Image Signal Processing(ISP)-第二章-Demosaic去马赛克以及BMP软件实现
Github:
BoyPao/ImageSignalProcessing-ISP
上文说到我们已经完成了ISP的必要操作,接下来为了让图像能呈现和人眼感官最为接近的画面,就需要对之前的结果进行优化操作了。
让我们回顾一下上期Demosaic处理后的结果以及存在的问题吧:
存在的问题:1. 图片非常暗,2. 图片颜色还是非常绿。
但这两个问题我们需要暂且放一放。因为在这之前,有一个更急迫的问题需要我们解决。只有解决这个问题,我们才能说所有的数据都是最接近真实的。
1. Black Level Correction
这个更迫切的问题是来自于传感器(Sensor)本身的一个硬件问题。
由于在数字图像系统中采用0和1来编码图像数据,因此势必需要用0表示一个数据。比如在常见的8bit量化编码的RGB域数据中,我们使用三个通道的0值表示纯黑色,三个通道的255值来表示纯白色。
理论上在非常低的照度下,也就是黑暗的环境下,Sensor应该输出0。但是实际上,由于Sensor需要上电工作,存在基本的工作电流,因此Sensor在这种黑暗情况下是难以输出0值的。其输出的Raw图的数据其实是一个非常小的电流偏移值。
1.1 Black Level Correction 原理
为方便消除这个偏移,我们假定这个偏移值对于同一幅图片的每一个像素来说都是一个常量。那么,可以将Sensor输出的所有像素值都减去这个常量,使其对黑暗的感光输出值真正变为0,我们把这种操作称为黑电平矫正(Black Level Correction)。而商业领域这个偏移值对0-255的编码范围来说一般是16。
当然,这样做会让白色对应的值不再是255,而是255减去这个偏移值。但这对我们没有影响,因为许多后续的图像处理步骤都是有增益的,并且这个增益不会是负数,因此不必担心255向小偏移。
此外需要注意,这个操作的目的是去除由硬件引入的电流偏移量,因此我们需要把这个步骤放在所有处理的前面,以保证后续所有增益操作的数据溢出部分尽可能地保留下来。
1.2 Black Level Correction 软件实现
以下就是对应的代码实现。需要注意的是当所有值减去偏移值后需要保证结果不为负数,将结果为负的值修正为0。
ISPResult BlackLevelCorrection(void* data, uint32_t argNum, ...)
{
ISPResult result = ISPSuccess;
va_list(va);
__crt_va_start(va, argNum);
int BLValue = static_cast<int>(__crt_va_arg(va, int));
bool enable = static_cast<bool>(__crt_va_arg(va, bool));
__crt_va_end(va);
if (enable == true) {
int i;
int temp = 0;
for (i = 0; i < WIDTH * HEIGHT; i++) {
temp = static_cast<int*>(data)[i] - BLValue;
if (temp < 0)
temp = 0;
static_cast<int*>(data)[i] = temp;
}
cout << __FUNCTION__ << " finished" << endl;
} else {
cout << __FUNCTION__ << " BLC disabled" << endl;
}
return result;
}
由于Raw的数据在本文中是mipi10编码的,有10bit(见上一章),因此常量偏移值应该是16左移两位,也就是16的4倍。
然后和之前的步骤一样,在ISP中调用处理函数。这段代码里,我对函数进行了封装,你可以只关注上方的BLC算法函数,也可以到我的github查看完整代码,以及具体的封装。
ISPNode* node = new ISPNode;
result = node->Init(BLC);
result = node->Process((void*)decodedata, 2, 16 * 4, BLCen);
接下来让我们看一看,BLC操作对图片的影响吧。
这个操作的结果不是很明显。但还是可以看到,图片确实向0偏移了(变暗了)。接下来就可在修正这个电流偏移值后的图片上开始其他的图像处理了。
下面我们来介绍针对图片又黑又绿这两个问题的优化操作。
2. White Balance
首先我们解决图片颜色偏绿的问题。
此时图片偏绿的主要原因是人眼对绿色黄色等中频光谱的感受更为敏感。前文提到Sensor正是依据了这一点在贝尔域采用两个G通道记录数据,使数据包含的信息量更大更有效。虽然在完成demosaic之后,每个像素点的RGB信息比例都是1:1:1,但依然由于人眼对G通道数据更为敏感,我们看到的图片还是偏绿的。当然这一块的结论是在大量的主观实验的基础上获得的。有兴趣的朋友可以查阅这方面的统计信息。
如何解决这个问题呢?很简单,我们需要提高R和B在一个像素中的比重。这种操作被我们称为白平衡操作(White Balance)。
2.1 White Balance 原理
白平衡是我们熟知的,它的目的是对各色彩通道进行不同的增益,去除光源色温的影响,最终让传感器在某种特定光源下对白色物体感光后输出的数据显示白色。
由于我采用的Raw数据是在D65光源大概1500lux照度下获取的,因此只需要对此光源环境进行白平衡操作就可修正这种光源下图片偏绿的问题,由此得到的三个通道gain值也只有一组。需要提及的是,这组gain值只适用于我使用的这个Sensor在D65光环境下的情况。当光源或传感器变化时,gain值是需要进行调整的。
2.2 White Balance 软件实现
以下就是具体的代码实现。
ISPResult WhiteBalance(void* data, uint32_t argNum, ...)
{
ISPResult result = ISPSuccess;
va_list(va);
__crt_va_start(va, argNum);
double bgain = static_cast<double>(__crt_va_arg(va, double));
double ggain = static_cast<double>(__crt_va_arg(va, double));
double rgain = static_cast<double>(__crt_va_arg(va, double));
bool enable = static_cast<bool>(__crt_va_arg(va, bool));
__crt_va_end(va);
int* B = static_cast<int*>(data);
int* G = B + WIDTH * HEIGHT;
int* R = G + WIDTH * HEIGHT;
if (enable == true) {
int i;
for (i = 0; i < WIDTH * HEIGHT; i++)
{
B[i] = B[i] * bgain;
G[i] = G[i] * ggain;
R[i] = R[i] * rgain;
}
cout << __FUNCTION__ << " finished" << endl;
}
return result;
}
因为我们需要去除偏绿的问题,因此这里G通道的gain值为1,R和B通道的gain值则要大于1。需要注意的是,白平衡操作和Gamma操作的顺序不同的话,增益也是不同的。我的ISP默认是先做白平衡,后做Gamma的。以下给出三通道的增益值。
bool is_WB1st_Gamma2nd = true;
const float Rgain = is_WB1st_Gamma2nd ? 1.994 : 1.378;
const float Ggain = is_WB1st_Gamma2nd ? 1.0 : 1.0;
const float Bgain = is_WB1st_Gamma2nd ? 2.372 : 1.493;
然后和之前的步骤一样,在ISP中调用处理函数。
result = node->Init(WB);
result = node->Process((void*)BGRdata, 4, Bgain, Ggain, Rgain, WBen);
让我们来看下白平衡操作后的结果。
可以看到,通过提高R和B通道的增益,我们解决了图片偏绿的问题。
3. Gamma
经过了白平衡操作,我们成功解决了图片偏绿问题,接下来我们一鼓作气解决图片偏暗的问题。同样的,先分析偏暗的原因。
首先,之前ISP进行了BLC操作,让所有像素点的数据向0偏移,这是图片偏暗的其中一个原因,但BLC是去除硬件影响所必要的步骤,因此我们不考虑调整BLC。
此外,还有一个因素,那就是Sensor输出的数据是线性编码的,而人眼对亮度的感受却不是线性的。这是我们看这张图片偏暗的主要原因。换句话说就是若Sensor感应并输出一份由黑到白线性递增的灰阶图时,我们的感观并不认为它由黑到白的变化是均匀的。
3.1 Gamma 原理
如何理解这里说的人眼对光感受的非线性?我们参考一张演示的曲线图。
橙色曲线表示的是光由暗到亮人眼感受到的照度增长规律,这是由主观实验统计而得的。而蓝色曲线则是传感器对同样的光环境变化所输出的数据增长规律。
从中可以得出结论,人眼对光的感知是非线性变化的,并且对同一光强而言,人眼感受值比传感器的输出值要大。因此当我们得到了传感器输出的数据后,需要对数据做处理,以拟合人眼感光的非线性规律,这样才能还原人眼的真实感受。
说了这么多,总结一下就是需要对Sensor输出的数据进行非线性增益。需要说明的是,此ISP中使用的Gamma增益值也只适用于我实验使用的这个传感器在D65光源下的情况。
3.2 Gamma 软件实现
以下就是Gamma的实现
ISPResult GammaCorrection(void* data, uint32_t argNum, ...)
{
ISPResult result = ISPSuccess;
int* B = static_cast<int*>(data);
int* G = B + WIDTH * HEIGHT;
int* R = G + WIDTH * HEIGHT;
va_list(va);
__crt_va_start(va, argNum);
bool enable = static_cast<bool>(__crt_va_arg(va, bool));
__crt_va_end(va);
unsigned int lut[1024];
unsigned int* plut;
plut = lut;
GenerateGammaLookUpTable(plut);
if (enable == true) {
for (int i = 0; i < WIDTH * HEIGHT; i++)
{
B[i] = lut[B[i]];
G[i] = lut[G[i]];
R[i] = lut[R[i]];
}
cout << __FUNCTION__ << " finished" << endl;
}
return result;
}
这里并没有直接做data * gain的操作,取而代之的是look up table(LUT)的操作。因为非线性映射中gain值不是一个常量,为它用非线性拟合来找到一个合适的数学表达式也比较麻烦,所以采用LUT这种直接进行值与值映射的操作,这可以让我们避免在代码中的非线性拟合或是乘积操作,提高处理效率。但这也需要事先准备好LUT具体的映射值。
其中GenerateGammaLookUpTable就生成了一个巨大的数组,储存这些非线性映射值。由于我们的数据到此还没有进行压缩,所以,数据范围是0-1023(10bit)。
void GenerateGammaLookUpTable(unsigned int* lut) {
lut[0] = 0; lut[1] = 4; lut[2] = 9; lut[3] = 13; lut[4] = 18; lut[5] = 21; lut[6] = 24; lut[7] = 27;
lut[8] = 30; lut[9] = 33; lut[10] = 36; lut[11] = 39; lut[12] = 42; lut[13] = 45; lut[14] = 48; lut[15] = 51;
lut[16] = 54; lut[17] = 57; lut[18] = 60; lut[19] = 63; lut[20] = 66; lut[21] = 69; lut[22] = 72; lut[23] = 75;
lut[24] = 78; lut[25] = 81; lut[26] = 85; lut[27] = 88; lut[28] = 92; lut[29] = 95; lut[30] = 99; lut[31] = 102;
......
......
......
lut[1005] = 1016; lut[1006] = 1016; lut[1007] = 1016;
lut[1008] = 1017; lut[1009] = 1017; lut[1010] = 1018; lut[1011] = 1018; lut[1012] = 1019; lut[1013] = 1019; lut[1014] = 1019; lut[1015] = 1019;
lut[1016] = 1020; lut[1017] = 1020; lut[1018] = 1021; lut[1019] = 1021; lut[1020] = 1022; lut[1021] = 1022; lut[1022] = 1022; lut[1023] = 1023;
}
同样的,在ISP中调用Gamma,以实现Gamma处理。
result = node->Init(GAMMA);
result = node->Process((void*)BGRdata, 1, Gammaen);
那么接下来,让我们看看Gamma处理后的结果吧。
至此我们解决了Demosaic后的两大遗留问题,图片偏绿和偏暗。再看看这一章为图像做BLC,WB,Gamma的总体效果吧。
可以看到,图片信息的真实度有了极大的改善。但这远远不及完整ISP处理的结果。我们将此时的结果和商用ISP结果进行对比,看看现阶段,还存在一些什么问题。
可以看到非常明显的有两个问题,1.图片边角的亮度低,2. 图片色彩饱和度很低。
现阶段又给我们的ISP提出了两个迫切的问题,在下一章,我将介绍如何完善我们的ISP以解决这两个新的问题。