前阵子接到任务要学习并软件实现一个ISP,刚拿到任务时既兴奋又担心。兴奋又有很多知识要了解了,担心ISP太过复杂,自己不能胜任这个任务。不管怎么样,还是坚持尝试了近一个月,捣腾出一个小ISP。
连载:
Image Signal Processing(ISP)-第一章-ISP基础以及Raw的读取显示
Image Signal Processing(ISP)-第二章-Demosaic去马赛克以及BMP软件实现
Image Signal Processing(ISP)-第三章-BCL, WB, Gamma的原理和软件实现
Github:
BoyPao/ImageSignalProcessing-ISP
从这篇文章开始,我将介绍整个ISP最基础的要求,并介绍如何软件实现这些最基础的要求。因为内容实在太多,此篇 Image Signal Processing(ISP)-第一章 将介绍关于ISP的基础概念,以及ISP第一步,Raw图的读取。如果这些知识能够帮到你,希望你要持续关注泡的连载哦,虽然下一篇遥遥无期,哈哈哈哈哈。
ISP是什么? Image Signal Processing(ISP) 的目的是对光学传感器输出的Raw图数据进行信号处理,使之成为符合人眼真实生理感受的信号,并加以输出。
为什么需要ISP呢? 这是个复杂的问题,其根源就是Raw图数据排布的形式。以下给出jpg和Raw的对比效果,这能非常直观地告诉你,Raw图为什么叫做Raw图(未经处理的图)。
jpg和Raw对比,左jpg,右Raw
我们知道所有的光都被能分解为几种基础色光。反之,用一定比例的各种饱和度、色调的基础色光就可以合成几乎所有的光。光学Seneor正是利用了这一原理,设置了红绿蓝pixel(像素单元)来接收光信号。而目前,红绿蓝pixel的排布采用了Bayer(贝尔)排布。这种排布造就了上图显示的传感器Raw图数据直接显示的效果。问题所在:1.图上全是绿色,2有网格现象(我们称之为棋盘格现象)
怎么样实现ISP?那么如何处理Raw图的数据,使之能够显示符合人眼真实感受的图片,这就成为了ISP的重要使命。为实现这一使命,ISP分为三个重要的部分,Bayer域信号处理,RGB域信号处理,YUV域信号处理。
ISP三个域的处理流程Bayer域的信号处理,主要目的是修正传感器物理特性造成的数据偏移,并且将信号进行插值,恢复完整RGB信号。
RGB域的信号处理,主要目的是对色彩进行补偿,恢复人眼真实感受
YUV域的信号处理,主要目的是分离亮度信号和色度信号,分别对两种信号处理,并且进行JPG的压缩编码。
这里,我设计了一个简单的ISP,使之能简单地完成最基础的ISP要求,并用软件实现了此ISP。(实际上,商用ISP比我设计的简单ISP要复杂很多,处理上也涉及很多更好的算法。本文只讨论最为基础的东西,以做简单的分享。了解ISP后对其各模块有兴趣的朋友可以针对地查阅相应模块的论文或资料)
这个简单的ISP flow chart显示在下方,它包含了Bayer域,RGB域,YUV域的具体操作。
我们来看看这些具体的操作有些什么?
Black Level Correction:指黑电平矫正。我们知道8bit的数字信号量化范围是0-255,0代表最黑,255代表最亮。但是sensor由于要上电感光,所以最黑的情况,也是有电压的,所以这时并不是0电平。这一步就是矫正sensor的非零电平。
Shading Correction:指光偏移矫正。由于镜头对不同色光的折射率是不同的,RGB三色光不能完全一致地在Sensor上成像,因此需要这一步补偿矫正。
G Channels Correction:指两个绿色通道的矫正。在Bayer域绿色通道有两个,但是由于Sensor的构造,感光时这两个通道会存在差异。这一步矫正此差异
White Balance:指白平衡。由于光源不同的色温会带来不同的成像,而往往人们需要去除色温带来的差异,因此需要白平衡这一步操作。
Demosaic:指去马赛克操作。实际上这一步实现了Bayer排布方式转为RGB排布方式
Color Correction:色彩矫正。这一步利用标准色卡进行信号的矫正,使信号恢复人眼真实感受
Gamma:伽马矫正,对信号进行非线性矫正,提高图像对比度,并使其符合CRT显示器的非线性显示。虽然现在不使用CRT显示器了,但gamma矫正仍是ISP必须的一部分,因为Sensor端依然和人眼有着差异。
Noise Reduction:降噪操作,由于Gamma,Shading Correction等一些列操作会增强信号,一些噪声也随之被增强,要去除增强的影响,同时还原更干净的成像,需要进行图像降噪。
Edge Enhancement:边缘增强,因为降噪处理会平滑图片,为了让图片更为清晰,在降噪后通常需要增强图片。
这个简单的ISP采用C++和Opencv实现。使用的测试图片为1920x1080大小的一张Raw图,此Raw图的bayer排布为B,Gb,Gr,R,编码为Mipi编码。(这对后面的软件实现尤为重要)。在此后的章节,我将介绍每一个模块如何通过软件实现,并附上一些经验教训。
在了解了ISP的一些基础概念后,要实现ISP,还需要获取Raw的数据,然后才能进行数据的处理。那么如何获取Raw图数据,那就需要好好了解一下Raw的规则了。Raw的规则由Sensor的像素排布决定,而目前这种排布采用的是Bayer排布。
Bayer排布是什么?Raw图所存的是Sensor直接生成的数据。目前的Sensor采用Bayer排布,我们称这样排的排布为Bayer域。
在Bayer域中,以四个邻域像素为单元组成整幅图片,每个单元都有两个绿色感光像素Gr 和Gb,一个红色感光像素R,一个蓝色感光像素B。我们称之为B通道,Gr通道,Gb通道,R通道。
如下图所示,整幅图由4x3个单元组成,每个单元有4个像素点,他的顺序是B,Gb,Gr,R。所以整幅图的大小是8x6=48个像素。
Bayer排布示例
单元内的四个像素排布顺序是不固定的,可能是B,Gb,Gr,R,也可能是R,Gr,Gb,B。这是由Sensor厂商决定的。
为什么要采用Bayer排布?前面我们介绍了,我们可以将任意光分解为几个基础色光,然后用一定比例的基础色光合成任意的光。这里Bayer排布提供了我们一种分解光的规则。此规则把任意的光分解为了蓝绿红三个基础色光。
但为什么一个单元有两个绿色像素呢?原因是因为人眼对于绿色和黄色更加敏感,采用两个通道的绿色可以更好地还原真实图像。这也是为什么Raw看上去是一片绿色的原因。一些厂商甚至采用两个通道的黄色生产了BYbYrR的贝尔传感器,这里就不介绍了。
在了解了什么是Bayer域后,我们就可以谈谈储存Bayer域数据的Raw文件了。
Raw是什么?Raw文件就是只有Bayer域数据的文件,未经任何的压缩。所以Raw文件有三个特点:1. 数据是Bayer数据。2. 没有任何关于数据格式的信息。3. 特别大,都在十几Mb以上。
我们可以用notepad的二进制插件打开一副Raw图,可以看到他的数据内容。
Raw文件的数据
在我第一次读取Raw文件的时候,天真地认为Raw没有任何的格式,于是假定一个10bit Raw图是两个字节组合的16位舍弃6位剩下的数据为一个像素数据。然后尝试以这种形式读出了数据并放在了Opencv Mat类CV_8UC3三通道的蓝色通道中,得出了错误的图像。
错误显示
后来思考错误的原因,定位问题应该是文件格式理解有错误。之后就是漫长的debug。
首先我这个Raw图大小是1920x1080=2073600,并且是10bit编码。这意味着有2073600个像素点,每个像素点有10bit。按照原来的假定,每个像素的10bit由两字节舍去6位保存,则文件将有2073600x2=4147200个字节。
然后我检查了Raw图的字节数。
Raw文件最后几行数据
如图显示,测试Raw图有0x00278cf8+0x8+0x1=0x00278d00字节(十六进制:最后一行数据地址+最后一行数据个数+1(此1因为地址从0开始))。换算成十进制是2592000字节。这与假设的4147200字节,相差甚远。
思考了一阵,我认为是Raw文件格式假定有误,于是上网查阅Raw资料。终于找到问题,10bit Mipi编码的Raw用5个字节存储4个像素10bit数据。5x8=4x10,这样将完全没有编码冗余。
了解Mipi编码后,最终解决了此bug。
10bit Mipi编码Raw的文件规则是,用5个字节存储4个像素数据,这样5x8=4x10,没有任何的编码冗余,完美利用每一位编码来存储数据。数据字节以5个为一组,每一组第五个字节头两位补充到第一字节后面组成10bit,次两位补充到第二字节后面组成第二个pixel数据,以此类推。
Mipi编码规则
到此,我们终于清楚地认识到了Raw图以及Raw图的规则,那么让我们上手获取他的数据吧。
接下来,直接放代码吧
#define WIDTH 1920
#define HEIGHT 1080
#define IMGPATH "C:\\Users\\penghao6\\Desktop\\1MCC_IMG_20181229_001526_1.RAW"
int main() {
//设置显示图片的大小
int Imgsizex, Imgsizey;
int Winsizex, Winsizey;
Winsizex = GetSystemMetrics(SM_CXSCREEN);
Winsizey = GetSystemMetrics(SM_CYSCREEN);
Imgsizey = Winsizey * 2 / 3;
Imgsizex = Imgsizey * WIDTH / HEIGHT;
int i,j;
ifstream OpenFile(IMGPATH);
if (OpenFile.fail()){
cout << "Open RAW failed!" << endl;
}
else {
size_t nsize = WIDTH * HEIGHT;
unsigned char *data = new unsigned char[nsize * 5 / 4];
int *decodedata = new int[nsize];
int *Bdata = new int[nsize];
int *Gdata = new int[nsize];
int *Rdata = new int[nsize];
unsigned char *r = new unsigned char[nsize];
unsigned char *g = new unsigned char[nsize];
unsigned char *b = new unsigned char[nsize];
Mat dst(HEIGHT, WIDTH, CV_8UC3, Scalar(0, 0, 0));
//将读不到的pixel置为黑色
for (i = 0; i < nsize; i++) {
Bdata[i] = 0;
Gdata[i] = 0;
Rdata[i] = 0;
}
OpenFile.read((char *)data, WIDTH * HEIGHT * 5 / 4);
data = reinterpret_cast<unsigned char *>(data);
Mipidecode(data, decodedata);//Mipi decode
ReadChannels(decodedata, Bdata, Gdata, Rdata);//pick up channels
//压缩10bit到8bit用于显示
Compress10to8(Bdata, b);
Compress10to8(Gdata, g);
Compress10to8(Rdata, r);
//dst保存四个通道,其中两个绿色通道通一保存在Gdata和g中
for (i = 0; i < HEIGHT; i++) {
for (j = 0; j < WIDTH; j++) {
dst.data[i*WIDTH * 3 + 3 * j] = (unsigned int)b[i*WIDTH + j];
dst.data[i*WIDTH * 3 + 3 * j + 1] = (unsigned int)g[i*WIDTH + j];
dst.data[i*WIDTH * 3 + 3 * j + 2] = (unsigned int)r[i*WIDTH + j];
}
}
namedWindow(RESNAME, 0);
resizeWindow(RESNAME, Imgsizex, Imgsizey);
imshow(RESNAME, result);
waitKey(0);
OpenFile.close();
}
cin >> i;//保持程序不退
return 0;
}
这里用到的Mipi解码函数是
void Mipidecode(unsigned char* src, int * dst) {
int i;
for (i = 0; i < WIDTH * HEIGHT * 5 / 4; i += 5) {
dst[i*4/5] = ((int)src[i] << 2) + (src[i+4]&0x3);
dst[i * 4 / 5+1] = ((int)src[i+1] << 2) + ((src[i + 4]>>2) & 0x3);
dst[i * 4 / 5+2] = ((int)src[i+2] << 2) + ((src[i + 4] >> 4) & 0x3);
dst[i * 4 / 5+3] = ((int)src[i+3] << 2) + ((src[i + 4] >> 6) & 0x3);
}
cout << " Mipi decode finished " << endl;
}
用到的读BGR通道函数是
void ReadChannels(int* data, int* B, int* G, int* R){
int i, j;
for (i = 0; i < HEIGHT; i ++) {
for (j = 0; j < WIDTH; j ++) {
if(i%2==0 &&j%2==0)
B[i*WIDTH + j] = data[i*WIDTH + j];
if ((i % 2 == 0 && j % 2 == 1) || (i % 2 == 1 && j %2== 0))
G[i*WIDTH + j] = data[i*WIDTH + j];
if (i % 2 == 1 && j % 2 == 1)
R[i*WIDTH + j] = data[i*WIDTH + j];
}
}
cout << " Read RGB channels finished " << endl;
}
用到的10bit压缩为8bit函数是
void Compress10to8(int * src, unsigned char * dst) {
int i, j;
for (i = 0; i < HEIGHT; i++) {
for (j = 0; j < WIDTH; j++) {
//采取直接舍弃最低两位,暴力压缩方式,舍弃精度,这应该放在ISP后期
if ((src[i*WIDTH + j] >> 2) > 255)
dst[i*WIDTH + j] = 255;
else if ((src[i*WIDTH + j] >> 2) < 0)
dst[i*WIDTH + j] = 0;
else
dst[i*WIDTH + j] = (src[i*WIDTH + j] >> 2) & 255;
}
}
}
通过以上代码,我们实现了将Raw图数据读取出来,并加以显示。
读出的Raw图
我们将Raw图放大,可以看到Bayer排布的现象
Bayer排布的棋盘格显现
至此我们成功地读取的Raw图了数据,并把这个数据加以显示。从中我们看到三个严重的现象:1. 棋盘格现象。2. 颜色全是绿的。3.图片太暗了。如何从这样的图片获得符合真实人眼感受的图片呢?泡将在下一章继续分享ISP是如何解决这两个现象的。ISP任重而道远!
敬请期待!