Hello!ISP的基础知识分享第二章终于来了!最近精力都投入到了工作上,真是没时间写东西。但一位大佬私信我催更,着实让我感动。即使我的文字只有一个人看,那我也会写下去,而且泡做事怎么会半途而费呢?
Image Signal Processing-第二章-Demosaic去马赛克以及BMP软件实现
连载:
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
上一篇文章介绍了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的步骤和具体执行的操作。首先提取贝尔域三个通道的数据。然后对三份数据分别进行线性插值。最后用三个通道的补全数据来组成一幅完整的图片。
上图中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的输出结果如下:
接下来我们对比一下Raw输出和Demosaic后的差异
我们放大图片仔细观察
可以看到Bayer域数据已经完全被补充完整,成为了完整的图像数据,棋盘格现象消除了,图片似乎可以呈现红色和蓝色了。
对比商用ISP处理的jpg文件,我们看看此时还存在什么缺点。
可以看出,棋盘格现象已被解决。遗留问题:1. 图片非常暗,2. 图片颜色还是非常绿。这两个问题需要到下一篇文章,介绍优化操作时才能解决。
值得注意的是,我们的ISP采用了简单的双线性插值来实现Demosaic。但是实际上,线性插值没有考虑到图像内容突变的情况,也就是图像内容为边缘的情况。这种情况下线性插值会使得边缘变得平滑,最终造成图像边缘模糊的后果。在商用领域,还有一些更为复杂的设计与算法来解决这一问题,这里我也没有太多的了解就不做介绍了。
2. BMP位图(Bitmap)
之前已经介绍了ISP必要操作的Demosaic,现在介绍第二个必要操作,压缩编码。本来想介绍JPEG压缩编码的,但是经过了解,发现JPEG压缩过于复杂了,涉及到离散余弦变换,游程编码,压缩量化,霍夫曼编码(熵编码)。如果整个实现JPEG压缩编码,要动用到大二大三两年多的知识。考虑到时间和复杂程度,最终我选择了非常好实现的BMP图片格式。
2.1 BMP基础
BMP就是计算机中最简单的一个图片格式,图片数据按位储存,我们称之为位图。
位图文件格式如下
我们看看上图蓝色部分BITMAPFILEHEADER结构体的声明
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文件的保存。
2.3 输出结果
通过上述代码,我们实现了在桌面上保存一张BMP图片。
至此,我们实现了ISP中的必要操作,打通了从Raw转到RGB并保存为BMP的路径,为后期的优化操作提供了基础支持。ISP任重而道远。