Image Signal Processing(ISP)-第二章-Demosaic去马赛克以及BMP软件实现

余生颓废 提交于 2020-01-19 02:26:45

Hello!ISP的基础知识分享第二章终于来了!最近精力都投入到了工作上,真是没时间写东西。但一位大佬私信我催更,着实让我感动。即使我的文字只有一个人看,那我也会写下去,而且泡做事怎么会半途而费呢?

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

上一篇文章介绍了ISP的基础以及获取Raw的详细方法。在获取Raw数据后,我们就可以正式开始ISP了。这里,我把ISP分为 必要操作优化操作

必要操作 是指对Raw数据不做处理,尽量无失真地转化为常见的图片格式的数据。
优化操作 是指尽可能分析数据不真实的原因,找到并实施对应的改善处理。

必要操作需要较多关于文件格式以及编码的知识,而优化操作需要较多图像算法的知识。

在本篇连载中,让我先介绍ISP中的必要操作,实现把Raw数据转化为常见图片格式的数据。这不仅可以保证整个ISP的通畅,还可以保存常见图片格式文件,帮助我们方便地分析ISP各模块的效果。

此ISP的必要操作有两个,第一是去马赛克操作Demosaic,第二个是压缩编码操作。Demosaic能转化Bayer域数据成为RGB数据。压缩编码能够将图片数据保存为常见的图片格式文件。

1. Demosaic去马赛克

1.1 Demosaic 原理

前面介绍了,Bayer域的数据是以4个像素为一个单元,由N个单元组成一幅图片。这意味着一个单元有4个像素,但其中两个像素是绿色的GbGr,一个是蓝色的B,一个是红色的R。我们要用红绿蓝合成任意的光,就必须在B像素补充绿色和红色,在R像素补充蓝色和绿色,在GbGr补充红色和蓝色。

那如何去补充这些没有被感光器件输出的颜色呢?

我们采用的方式是估计。由于一幅图片在空间上的信息是有相关性的,而这种相关性体现在数字图像中,就是像素值的线性相关。于是我们可以靠线性估计来估计这些没有的数据。说直白点就是靠邻域已有的数据进行线性插值而获得没有的数据。

Demosaic步骤
上图显示了Demosaic的步骤和具体执行的操作。首先提取贝尔域三个通道的数据。然后对三份数据分别进行线性插值。最后用三个通道的补全数据来组成一幅完整的图片。

双线性插值规则
上图中Bn/Gn/Rn为已有数据,bn/gn/rn为待估计像素点。则双线性插值的规则是:

b1=(B1+B2)/2
b2=(B1+B3)/2
b3=(B1+B2+B3+B4)/4

g1=(G1+G2+G3+G4)/4
g2=(G3+G4+G5+G6)/4

r1=(R1+R2)/2
r2=(R1+R3)/2
r3=(R1+R2+R3+R4)/4

1.2 Demosaic软件实现

用到的读取各个通道的函数,在上一章介绍Raw图获取和显示时已经介绍过了,此处再次贴出。

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;
}

操作Demosaic的函数如下。要说明的是,此处的插值函数没有对边界值进行操作,即第一行,最后一行,第一列,最后一列没有进行插值。若要全图插值,则需要添加边界值逻辑。此处不再添加。

void Demosaic(int *data, int *B, int *G, int *R) {
    FirstPixelInsertProcess(data, B);
    TwoGPixelInsertProcess(data, G);
    LastPixelInsertProcess(data, R);
    cout << " Demosaic finished" << endl;
}

void FirstPixelInsertProcess(int *src, int *dst) {
    int i, j;
    for (i = 0; i < HEIGHT; i++) {
        for (j = 0; j < WIDTH; j++) {
            if (i % 2 == 0 && j % 2 == 1 && j > 0 && j < WIDTH - 1) {
                dst[i * WIDTH + j] = (src[i * WIDTH + j - 1] + 
                                   src[i * WIDTH + j + 1]) / 2;}
            if (i % 2 == 1 && j % 2 == 0 && i > 0 && i < HEIGHT - 1) {
                dst[i * WIDTH + j] = (src[(i - 1) * WIDTH + j] + 
                                    src[(i + 1) * WIDTH + j]) / 2;}
         }
    }
    for (i = 0; i < HEIGHT; i++) {
        for (j = 0; j < WIDTH; j++) {
            if (i % 2 == 1 && j % 2 == 1 && j > 0 &&
            j < WIDTH - 1 && i > 0 && i < HEIGHT - 1) {
                dst[i * WIDTH + j] = (src[(i - 1) * WIDTH + j - 1] + 
                                   src[(i - 1) * WIDTH + j + 1]+ 
                                   src[(i + 1) * WIDTH + j - 1]+
                                   src[(i + 1) * WIDTH + j + 1]) / 4;}                      
        }
    }
    //cout << " First Pixel Insert Process finished " << endl;
}

void TwoGPixelInsertProcess(int *src, int *dst) {
    int i, j;
    for (i = 0; i < HEIGHT; i++) {
        for (j = 0; j < WIDTH; j++) {
            if (i % 2 == 0 && j % 2 == 0 && j > 0 &&
            j < WIDTH - 1 && i > 0 && i < HEIGHT - 1) {
                dst[i * WIDTH + j] = (src[i * WIDTH + j - 1] + 
                                    src[i * WIDTH + j + 1] + 
                                    src[(i - 1) * WIDTH + j] + 
                                    src[(i + 1) * WIDTH + j]) / 4;}
            if (i % 2 == 1 && j % 2 == 1 && j > 0 && 
            j < WIDTH - 1 && i > 0 && i < HEIGHT - 1) {
                dst[i * WIDTH + j] = (src[i * WIDTH + j - 1] + 
                                    src[i * WIDTH + j + 1] + 
                                    src[(i - 1) * WIDTH + j] + 
                                    src[(i + 1) * WIDTH + j]) / 4;}
        }
    }
    //cout << " TWO Green Pixel Insert Process finished " << endl;
}

void LastPixelInsertProcess(int *src, int *dst) {
    int i, j;
    for (i = 0; i < HEIGHT; i++) {
        for (j = 0; j < WIDTH; j++) {
            if (i % 2 == 1 && j % 2 == 0 && j > 0 && j < WIDTH - 1) {
                dst[i * WIDTH + j] = (src[i * WIDTH + j - 1] + 
                                   src[i * WIDTH + j + 1]) / 2;}
            if (i % 2 == 0 && j % 2 == 1 && i > 0 && i < HEIGHT - 1) {
                dst[i * WIDTH + j] = (src[(i - 1)*WIDTH + j] + 
                                   src[(i + 1) * WIDTH + j]) / 2;}
        }
    }
    for (i = 0; i < HEIGHT; i++) {
        for (j = 0; j < WIDTH; j++) {
            if (i % 2 == 0 && j % 2 == 0 && j > 0 && 
            j < WIDTH - 1 && i > 0 && i < HEIGHT - 1) {
                dst[i * WIDTH + j] = (src[(i - 1) * WIDTH + j - 1] +
                                   src[(i - 1) * WIDTH + j + 1] + 
                                   src[(i + 1) * WIDTH + j - 1] + 
                                   src[(i + 1)*WIDTH + j + 1]) / 4;}
         }
     }
     //cout << " Last Pixel Insert Process finished " << endl;
 }

通过在主函数中ReadChannels函数后面调用上面介绍的Demosaic(decodedata, Bdata, Gdata, Rdata);函数,实现去马赛克操作。

1.3 输出结果

Demosaic的输出结果如下:
Demosaic输出
接下来我们对比一下Raw输出和Demosaic后的差异
RawvsDemosaic
我们放大图片仔细观察
在这里插入图片描述

可以看到Bayer域数据已经完全被补充完整,成为了完整的图像数据,棋盘格现象消除了,图片似乎可以呈现红色和蓝色了。

对比商用ISP处理的jpg文件,我们看看此时还存在什么缺点。
在这里插入图片描述
可以看出,棋盘格现象已被解决。遗留问题1. 图片非常暗2. 图片颜色还是非常绿。这两个问题需要到下一篇文章,介绍优化操作时才能解决。

值得注意的是,我们的ISP采用了简单的双线性插值来实现Demosaic。但是实际上,线性插值没有考虑到图像内容突变的情况,也就是图像内容为边缘的情况。这种情况下线性插值会使得边缘变得平滑,最终造成图像边缘模糊的后果。在商用领域,还有一些更为复杂的设计与算法来解决这一问题,这里我也没有太多的了解就不做介绍了。

2. BMP位图(Bitmap)

之前已经介绍了ISP必要操作的Demosaic,现在介绍第二个必要操作,压缩编码。本来想介绍JPEG压缩编码的,但是经过了解,发现JPEG压缩过于复杂了,涉及到离散余弦变换,游程编码,压缩量化,霍夫曼编码(熵编码)。如果整个实现JPEG压缩编码,要动用到大二大三两年多的知识。考虑到时间和复杂程度,最终我选择了非常好实现的BMP图片格式。

2.1 BMP基础

BMP就是计算机中最简单的一个图片格式,图片数据按位储存,我们称之为位图。
位图文件格式如下
BMP文件头格式
我们看看上图蓝色部分BITMAPFILEHEADER结构体的声明
BMPFileHeader
WORD是unsigned short,2个字节。DWORD是unsigned long,4个字节。

bfType 是指文件类型,Windows的BMP类型为BM,其值为0x424D,采用小端存储,应该写为4D42。
bfSize 指整个BMP所占的字节数,即文件大小。
bfReserved1 保留字段,目前需要设置为0
bfReserved2 保留字段,目前需要设置为0
bfOffBits 指图像数据地址相对文件开始地址的偏移量。

我们看看上图绿色部分BITMAPINFOHEADER结构体的声明
在这里插入图片描述
biSize 指BITMAPINFOHEADER结构体所占的字节数,就是图中绿色部分的大小
biWidth 指图像的宽度,一行的像素个数
biHeight 指图像的高度,一列的像素个数
biPlanes 一般设置为1
biBitCount 指位深,可以设置为1,4,8,16,24,32。对于8bitRGB数据,应设置为8x3=24
biCompression 指压缩方式,我们这里设置为BI_RGB,即不压缩
biSizeImage 指图像数据所占位数
biXPelsPerMeter 指X轴分辨率,在位宽为24时,用默认值即可
biYpelsPerMeter 指Y轴分辨率,在位宽为24时,用默认值即可
biClrUsed 指调色板使用数量,0值时代表使用所有调色板
biClrImportant 指重要调色板的索引,0表示所有调色板都重要

在我们的ISP中使用压缩后的8bit记录一个像素点的形式,因此位深为24,不需要用调色板。所以调色板代码段,也就是图中黄色部分就被移除了。

最后我们介绍一下上图粉色部分图像数据的排布规则:
打开BMP文件后,图像的显示将从BMP文件数据区的末尾进行倒序扫描。从右到左,从下至上。因此我们在保存Raw图数据的时候,需要把图像数据进行180度旋转。在我的简单ISP中,先对数据以像素为单位倒置,然后对数据进行镜像操作,实现了180度数据旋转。

2.2 保存BMP的软件实现

软件实现分为三个部分,第一部分将10bit图像数据压缩为8bit,第二部对图像数据做180度旋转,第三部分配置Bitmap的头信息,并且写入data数据。

第一部分,压缩数据,压缩的代码已经在上一章分享了。这个简单ISP采取的是暴力去精度的压缩方式。此处再次贴出。

void Compress10to8(int *src, unsigned char *dst) {
    int i, j;
    for (i = 0; i < HEIGHT; i++) {
        for (j = 0; j < WIDTH; j++) {
            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;
            }
         }
     }
}

第二部分,将图片数据进行180度旋转。

void setBMP(BYTE *data, Mat datasrc) {
    int j = 0;
    int row = 0;
    int col = 0;
    int size;
    BYTE temp;
    size= WIDTH * HEIGHT * datasrc.channels();
    memset(data, 0x00, size);
    if (datasrc.channels() == 3) {
        for (int i = 0; i < WIDTH * HEIGHT; i++) {
            data[i * 3] = datasrc.data[i * 3];
            data[i * 3 + 1] = datasrc.data[i * 3 + 1];
            data[i * 3 + 2] = datasrc.data[i * 3 + 2];
        }

        //矩阵反转
        while (j < 3 * WIDTH * HEIGHT - j) {
            temp = data[3 * WIDTH * HEIGHT - j - 1];
            data[3 * WIDTH * HEIGHT - j - 1] = data[j];
            data[j] = temp;
            j++;
        }

        //图像镜像翻转
        for (row = 0; row < HEIGHT; row++) {
            while (col < 3 * WIDTH - col) {
                temp = data[row * 3 * WIDTH + 3 * WIDTH - col - 1];
                data[3 * row * WIDTH + 3 * WIDTH - col - 1] = 
                       data[3 * row * WIDTH + col];
                data[3 * row*WIDTH + col] = temp;
                col++;
            }
            col = 0;
        }
    }
}

第三部分,配置Bitmap的头信息,并且写入data数据

void saveBMP(BYTE *data, string BMPPath) {
    BITMAPFILEHEADER header;
    BITMAPINFOHEADER headerinfo;
    int size = WIDTH * HEIGHT * 3;
    
    //bitmap文件头
    header.bfType = 0x4D42;
    header.bfReserved1 = 0;
    header.bfReserved2 = 0;
    header.bfSize = sizeof(BITMAPFILEHEADER) + 
                    sizeof(BITMAPINFOHEADER) + size;
    header.bfOffBits = sizeof(BITMAPFILEHEADER) + 
                       sizeof(BITMAPINFOHEADER);

    //bitmap信息头
    headerinfo.biSize = sizeof(BITMAPINFOHEADER);
    headerinfo.biHeight = HEIGHT;
    headerinfo.biWidth = WIDTH;
    headerinfo.biPlanes = 1;
    headerinfo.biBitCount = 24;
    headerinfo.biCompression = 0; 
    headerinfo.biSizeImage = size;
    headerinfo.biClrUsed = 0;
    headerinfo.biClrImportant = 0;
    
    //写Bitmap文件
    cout << " BMPPath: " << BMPPath << endl;
    ofstream out(BMPPath, ios::binary);
    out.write((char*)&header, sizeof(BITMAPFILEHEADER));
    out.write((char*)&headerinfo, sizeof(BITMAPINFOHEADER));
    out.write((char*)data, size);
    out.close();
    cout << " BMP saved "<<endl;
}

在主函数中,完成10bit到8bit的压缩,再完成三通道叠加后,开辟内存
BYTE *BMPdata = new BYTE[WIDTH * HEIGHT * dst.channels()];
以保存Bitmap的图像数据。

调用
setBMP(BMPdata, dst); saveBMP(BMPdata,“C:\Users\Pao\Desktop\output.BMP”);
以实现BMP文件的保存。
BMP插入点

2.3 输出结果

在这里插入图片描述
通过上述代码,我们实现了在桌面上保存一张BMP图片。
至此,我们实现了ISP中的必要操作,打通了从Raw转到RGB并保存为BMP的路径,为后期的优化操作提供了基础支持。ISP任重而道远。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!