Image Signal Processing(ISP)-第一章-ISP基础以及Raw的读取显示

前阵子接到任务要学习并软件实现一个ISP,刚拿到任务时既兴奋又担心。兴奋又有很多知识要了解了,担心ISP太过复杂,自己不能胜任这个任务。不管怎么样,还是坚持尝试了近一个月,捣腾出一个小ISP。

Image Signal Processing-第一章-ISP基础以及Raw的读取显示

    • 1. ISP基础概念
    • 2. Raw的读取与显示
      • 2.1 Bayer域
      • 2.2 Raw文件
        • 经验教训(可略过,不影响阅文)
      • 2.3 10bit Mipi编码Raw文件规则
      • 2.4 读取和显示的软件实现

连载
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图的读取。如果这些知识能够帮到你,希望你要持续关注泡的连载哦,虽然下一篇遥遥无期,哈哈哈哈哈。

1. ISP基础概念

Image Signal Processing(ISP)-第一章-ISP基础以及Raw的读取显示_第1张图片 ISP在相机数据流中的位置

ISP是什么? Image Signal Processing(ISP) 的目的是对光学传感器输出的Raw图数据进行信号处理,使之成为符合人眼真实生理感受的信号,并加以输出。

为什么需要ISP呢? 这是个复杂的问题,其根源就是Raw图数据排布的形式。以下给出jpg和Raw的对比效果,这能非常直观地告诉你,Raw图为什么叫做Raw图(未经处理的图)。

Image Signal Processing(ISP)-第一章-ISP基础以及Raw的读取显示_第2张图片

jpg和Raw对比,左jpg,右Raw

我们知道所有的光都被能分解为几种基础色光。反之,用一定比例的各种饱和度、色调的基础色光就可以合成几乎所有的光。光学Seneor正是利用了这一原理,设置了红绿蓝pixel(像素单元)来接收光信号。而目前,红绿蓝pixel的排布采用了Bayer(贝尔)排布。这种排布造就了上图显示的传感器Raw图数据直接显示的效果。问题所在:1.图上全是绿色,2有网格现象(我们称之为棋盘格现象)

怎么样实现ISP?那么如何处理Raw图的数据,使之能够显示符合人眼真实感受的图片,这就成为了ISP的重要使命。为实现这一使命,ISP分为三个重要的部分,Bayer域信号处理,RGB域信号处理,YUV域信号处理。

Image Signal Processing(ISP)-第一章-ISP基础以及Raw的读取显示_第3张图片 ISP三个域的处理流程

Bayer域的信号处理,主要目的是修正传感器物理特性造成的数据偏移,并且将信号进行插值,恢复完整RGB信号。

RGB域的信号处理,主要目的是对色彩进行补偿,恢复人眼真实感受

YUV域的信号处理,主要目的是分离亮度信号和色度信号,分别对两种信号处理,并且进行JPG的压缩编码。

这里,我设计了一个简单的ISP,使之能简单地完成最基础的ISP要求,并用软件实现了此ISP。(实际上,商用ISP比我设计的简单ISP要复杂很多,处理上也涉及很多更好的算法。本文只讨论最为基础的东西,以做简单的分享。了解ISP后对其各模块有兴趣的朋友可以针对地查阅相应模块的论文或资料)

这个简单的ISP flow chart显示在下方,它包含了Bayer域,RGB域,YUV域的具体操作。

Image Signal Processing(ISP)-第一章-ISP基础以及Raw的读取显示_第4张图片

我们来看看这些具体的操作有些什么?

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编码。(这对后面的软件实现尤为重要)。在此后的章节,我将介绍每一个模块如何通过软件实现,并附上一些经验教训。

2. Raw的读取与显示

在了解了ISP的一些基础概念后,要实现ISP,还需要获取Raw的数据,然后才能进行数据的处理。那么如何获取Raw图数据,那就需要好好了解一下Raw的规则了。Raw的规则由Sensor的像素排布决定,而目前这种排布采用的是Bayer排布。

2.1 Bayer域

Bayer排布是什么?Raw图所存的是Sensor直接生成的数据。目前的Sensor采用Bayer排布,我们称这样排的排布为Bayer域。

在Bayer域中,以四个邻域像素为单元组成整幅图片,每个单元都有两个绿色感光像素Gr 和Gb,一个红色感光像素R,一个蓝色感光像素B。我们称之为B通道,Gr通道,Gb通道,R通道。

如下图所示,整幅图由4x3个单元组成,每个单元有4个像素点,他的顺序是B,Gb,Gr,R。所以整幅图的大小是8x6=48个像素。
Image Signal Processing(ISP)-第一章-ISP基础以及Raw的读取显示_第5张图片
Bayer排布示例

单元内的四个像素排布顺序是不固定的,可能是B,Gb,Gr,R,也可能是R,Gr,Gb,B。这是由Sensor厂商决定的。

为什么要采用Bayer排布?前面我们介绍了,我们可以将任意光分解为几个基础色光,然后用一定比例的基础色光合成任意的光。这里Bayer排布提供了我们一种分解光的规则。此规则把任意的光分解为了蓝绿红三个基础色光。

但为什么一个单元有两个绿色像素呢?原因是因为人眼对于绿色和黄色更加敏感,采用两个通道的绿色可以更好地还原真实图像。这也是为什么Raw看上去是一片绿色的原因。一些厂商甚至采用两个通道的黄色生产了BYbYrR的贝尔传感器,这里就不介绍了。

在了解了什么是Bayer域后,我们就可以谈谈储存Bayer域数据的Raw文件了。

2.2 Raw文件

Raw是什么?Raw文件就是只有Bayer域数据的文件,未经任何的压缩。所以Raw文件有三个特点:1. 数据是Bayer数据。2. 没有任何关于数据格式的信息。3. 特别大,都在十几Mb以上。
我们可以用notepad的二进制插件打开一副Raw图,可以看到他的数据内容。

Raw文件的数据

经验教训(可略过,不影响阅文)

在我第一次读取Raw文件的时候,天真地认为Raw没有任何的格式,于是假定一个10bit Raw图是两个字节组合的16位舍弃6位剩下的数据为一个像素数据。然后尝试以这种形式读出了数据并放在了Opencv Mat类CV_8UC3三通道的蓝色通道中,得出了错误的图像。

Image Signal Processing(ISP)-第一章-ISP基础以及Raw的读取显示_第6张图片

错误显示

后来思考错误的原因,定位问题应该是文件格式理解有错误。之后就是漫长的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。

2.3 10bit Mipi编码Raw文件规则

10bit Mipi编码Raw的文件规则是,用5个字节存储4个像素数据,这样5x8=4x10,没有任何的编码冗余,完美利用每一位编码来存储数据。数据字节以5个为一组,每一组第五个字节头两位补充到第一字节后面组成10bit,次两位补充到第二字节后面组成第二个pixel数据,以此类推。

Image Signal Processing(ISP)-第一章-ISP基础以及Raw的读取显示_第7张图片

Mipi编码规则

到此,我们终于清楚地认识到了Raw图以及Raw图的规则,那么让我们上手获取他的数据吧。

2.4 读取和显示的软件实现

接下来,直接放代码吧

#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图数据读取出来,并加以显示。
Image Signal Processing(ISP)-第一章-ISP基础以及Raw的读取显示_第8张图片
读出的Raw图
我们将Raw图放大,可以看到Bayer排布的现象
Image Signal Processing(ISP)-第一章-ISP基础以及Raw的读取显示_第9张图片
Bayer排布的棋盘格显现
至此我们成功地读取的Raw图了数据,并把这个数据加以显示。从中我们看到三个严重的现象:1. 棋盘格现象。2. 颜色全是绿的。3.图片太暗了。如何从这样的图片获得符合真实人眼感受的图片呢?泡将在下一章继续分享ISP是如何解决这两个现象的。ISP任重而道远!

敬请期待

你可能感兴趣的:(ISP,ISP,Raw读取和显示,Image,Singal,Processing,C++,Opencv)