前言
由于工作需要,最近在折腾一个工业相机,在提供的 CSDK
中,仅提供了获取信息流的方法,若想将图像保存,需自行实现,由于不想借助 OpenCV
去处理(主要是还没装),而且 OPenCV官方
在逐渐抛弃 C 版本
。于是便有了这篇文章。
准备工作
要想实现图片保存,首先我们应先了解BMP图片是如何存储的。
我的方法很简单粗暴,直接上网下载了一张 *.bmp
图片,找不到 bmp格式
的图片没关系,任意下载一张后,通过图片在线转换工具得到 BMP文件
。
下载完成后,若直接使用记事本打开,会呈现出乱码。这是由于使用了 ASCII编码
显示,那么我们更换一下打开的工具,这里我使用的是 Sublime Text 3
。
由于Sublime Text 3
可以预览 bmp文件
,于是我将后缀改为 txt
再打开。
存储形式
BMP
文件格式,又称为 Bitmap(位图)
或是 DIB
( Device-Independent Device
,设备无关位图),是 Windows系统
中广泛使用的图像文件格式。
由于它可以不作任何变换地保存图像像素域的数据,因此成为我们取得 RAW数据
的重要来源。Windows
的图形用户界面( graphical user interfaces
)也在它的内建图像子系统GDI
中对 BMP
格式提供了支持 1。
位图图像文件由若干大小固定(文件头)和大小可变的结构体按一定的顺序构成 2。
几经演变,这部分版本较多。下图来自维基百科:
位图头文件信息
这部分数据块位于文件开头,用于进行文件的识别。典型的应用程序会首先普通读取这部分数据以确保的确是位图文件并且没有损坏。所有的整数值都以 小端序 存放(即最低有效位前置) 2。
域名 | 大小 | 说明 |
---|---|---|
文件标识 | 2byte | BMP文件 常见"BM"(ASCII) ,即 0x42 、0x4D |
文件大小 | 4byte | 双字形式存储。我这里是8a38 0400 ;即 00048a38(H)= 276618(D) |
保留字 | 4byte | 默认为 0 |
偏移量 | 4byte | 位图数据(像素数组)的地址偏移(byte ),也就是起始地址;记录该项便于随机读取 |
PS:
-
文件标志:
BM
– Windows 3.1x, 95, NT, … etc.BA
– OS/2 struct Bitmap ArrayCI
– OS/2 struct Color IconCP
– OS/2 const Color PointerIC
– OS/2 struct IconPT
– OS/2 Pointer
-
文件大小的计算
F i l e s i z e ( ) ≈ 54 + 4 ⋅ 2 n + w i d t h ⋅ h e i g h t ⋅ n 8 Filesize() {\displaystyle \approx 54+4\cdot 2^{n}+{\frac { {\rm {width}}\cdot {\rm {height}}\cdot n}{8}}} Filesize()≈54+4⋅2n+8width⋅height⋅n
位图信息头
该部分从文件的第 15byte
开始,存储详细的图片信息。
这部分数据块对应了 Windows
和 OS/2
中的内部使用的头结构以及其它一些版本的变体。但所有版本均以一个 DWORD位(32位)
开始,用以说明该数据块的大小,使得应用程序能够根据这个大小来区分该图像实际使用了哪种版本的 DIB头结构
2。
下图为所有不同版本的DIB头:
出于兼容性的考量,大多数应用程序使用较旧版本的 DIB头
保存文件。在不考虑 OS/2
的情况下,当前通用的格式为 BITMAPINFOHEADER
版本 2。
以下是其详细内容:
域名 | 大小 | 说明 |
---|---|---|
图像信息头长度 | 4byte | 信息头长度,一般为 28(H) |
图像宽度 | 4byte | 位图图像宽度,单位:像素 |
图像高度 | 4byte | 位图图像高度,单位:像素 |
图像面数 | 2byte | 恒为 1 |
图像像素位数 | 2byte | 1 表示单色位图 ; 4 表示16色位图 ; 8 表示256色位图 ; 16 表示16位高彩色位图 ; 24 表示24位真彩色位图 ;32 表示32位增强真彩色位图 |
压缩种类 | 4byte | 0 表示不压缩 ; 1 表示8位RLE压缩 ; 2 表示4位RLE压缩 ; 3 表示位域存放压缩 |
位图数据大小 | 4byte | 该值一定为 4的倍数 |
水平分辨率 | 4byte | 单位:像素/米 |
垂直分辨率 | 4byte | 单位:像素/米 |
使用的颜色数 | 4byte | 单位:0 表示默认值,亦可为 2^n |
重要的颜色数 | 4byte | 0 亦代表默认值,当数值等于 “颜色数” 的数值的时候表示所有颜色一样重要 |
PS:
- 压缩种类
- 位图数据大小计算
- 先计算每行所占字节数,再乘以列数
- 代码直接看 最简版本
调色板
调色板是一张映射表,标识颜色索引号与其代表的颜色的对应关系。
当 图像像素位数 <= 8
时,调色板不可省略。为什么呢?
16位位图
的为 真彩色
,最高一位为 0
,然后五位是 红色
,中间五位是 绿色
最低五位是 蓝色
。
而典型的位图文件使用 RGB彩色模型
。在这种模型中,每种颜色都是由不同强度的 红色(R)
、绿色(G)
和 蓝色(B)
组成的,也就是说,每种颜色都可以使用红色、绿色和蓝色的值所定义 2。
那么以 8位位图
为例,最多能表示 256种颜色
;由于每个颜色都有RGB三原色
,也就是要3个字节
表示,这样的话 256
并不能表示所有的颜色。
这就需要一个索引,用一个字节的索引指向 3个字节
表示的 颜色(RGB)
。
如果把这 3个字节
表示为一个 Color类型
;那么调色板就是 Color的数组
,也就是个二维数组 palette[N][4]
,其中 N
是颜色的数量 3。
对于 32位位图
,则可用 RGBA
。
位图数据
当 Height
为正数时,图像倒立,从下到上,从左到右,以行为主序排列。
当 Height
为负数时,从上到下存储。
Windows
默认的扫描的最小单位是 4字节
,如果数据对齐满足这个值的话对于数据的获取速度等都是有很大的增益的。
因此,BMP图像
顺应了这个要求,要求每行的数据的长度必须是 4的倍数
,如果不够需要进行 比特填充
(以 0
填充),这样可以达到按行的快速存取。这样的话,位图数据的大小就不一定是 宽x高x每像素字节数
了,因为每行还可能有 0填充
。这也是 位图数据大小
一定是 4的倍数
的原因。
此外需要注意的是:在 windows
中,颜色顺序是:B G R
。
实现
那么我们如何将 视频流数据
转换为 bmp文件
保存?
首先我们需要创建一个文件,然后按照 BMP
的存储格式写入该文件,最后保存为 *.bmp
即可。
最简版本
以下代码,参考 C语言集锦(一) C代码生成图片:BMP、PNG和JPEG 4,我只是完成了小部分修改,增加了可读性。
/************************* Save 24bit BMP ****************************/
#include <stdio.h>
#include <stdlib.h>
#define w 200
#define h 200
void WriteBMP(char*img,const char* filename)
{
// 计算每行所占字节数
// +3是怕出现不满足4的倍数这种情况
// /4*4的目的是保证结果为4的倍数
int l=(w*3+3)/4*4;
// 位图头文件信息(不包含BMP标志)+信息数据
// 利用或运算巧妙的将图像面数及像素位数合并
int bmi[]= {
l*h+54,0,54,40,w,h,1|((3*8)<<16),0,l*h,0,0,0,0};
//创建/打开文件
FILE *fp = fopen(filename,"wb");
//写入BMP标志
fprintf(fp,"BM");
//写入位图头文件信息+信息数据
fwrite(&bmi,52,1,fp);
//写入位图数据
fwrite(img,1,l*h,fp);
fclose(fp);
}
int main()
{
char img[w*h*3];
//随机位图数据
for(int i=0; i<w*h*3; i++)
img[i]=rand()%256;
WriteBMP(img,"test.bmp");
return 0;
}
结构体实现
下面,我们使用结构体实现,特别需要注意的是:考虑字对齐。
关于字对齐,可查看我之前的文章 C - 字对齐那些事儿
#include <stdio.h>
#include <stdlib.h>
#define w 200
#define h 200
#pragma pack(2)
struct file_head
{
short fhType;
unsigned int fhSize;
unsigned int fhRd;
unsigned int fhOffset;
};
struct bmp_head
{
unsigned int bhSize;
unsigned int bhWidth;
unsigned int bhHight;
short bhPlances;
short bhBitCount;
unsigned int bhCompression;
unsigned int bhSizeImage;
unsigned int bhXPelsPerMeter;
unsigned int bhYPelsPerMeter;
unsigned int bhClrUsed;
unsigned int bhClrImportant;
};
#pragma pack()
#pragma pack(1)
//BGR
struct clrtest
{
unsigned char rgbBlue;
unsigned char rgbGreen;
unsigned char rgbRed;
//unsigned char rgbReserved;
};
#pragma pack()
int main(){
struct file_head file_h;
struct bmp_head bmp_h;
int l = (w*3 +3)/4*4;
//unsigned char img[l*h];
struct clrtest image[w*h];
file_h.fhType = 0x4D42;
file_h.fhSize = 120054;
file_h.fhRd = 0;
file_h.fhOffset = 54;
bmp_h.bhSize = 40;
bmp_h.bhWidth = w;
bmp_h.bhHight = h;
bmp_h.bhPlances = 1;
bmp_h.bhBitCount = 24;
bmp_h.bhCompression = 0;
bmp_h.bhSizeImage = l*h;
bmp_h.bhXPelsPerMeter = 0;
bmp_h.bhYPelsPerMeter = 0;
bmp_h.bhClrUsed = 0;
bmp_h.bhClrImportant = 0;
FILE *fp = fopen("testbmp.bmp","wb");
fwrite(&file_h,14,1,fp);
fwrite(&bmp_h,40,1,fp);
//Generate a Blud picture
for (int i = 0; i < w; i++)
{
for(int j = 0; j < h; j++)
{
image[i*w+j].rgbBlue = 255;
image[i*w+j].rgbGreen = 0;
image[i*w+j].rgbRed = 0;
//image[i*j].rgbReserved = 0;
}
}
fwrite(&image,sizeof(image),1,fp);
printf("file_head: sizeof:%d\r\n",sizeof(file_h));
printf("bmp_head: sizeof:%d\r\n",sizeof(bmp_h));
fclose(fp);
return 0;
}
应用
画圆
if(10000 == (i - 100)*(i - 100) + (j - 100)*(j - 100))
{
image[i*w+j].rgbRed = 255;
image[i*w+j].rgbGreen = 255;
image[i*w+j].rgbBlue = 255;
}
else
{
image[i*w+j].rgbRed = 0;
image[i*w+j].rgbGreen = 0;
image[i*w+j].rgbBlue = 0;
}
效果有点差,让我想起了浮点数的比较 if( float_number == xxx )
改进
if(-1 < (i - 100)*(i - 100) + (j - 100)*(j - 100) - 10000 < 1)
{
image[i*w+j].rgbRed = 255;
image[i*w+j].rgbGreen = 255;
image[i*w+j].rgbBlue = 255;
}
else
{
image[i*w+j].rgbRed = 0;
image[i*w+j].rgbGreen = 0;
image[i*w+j].rgbBlue = 0;
}
正在我准备收拾东西走人的时候,突然听到了…那就画一道彩虹吧。
生成彩虹
for (int i = 0; i < w; i++)
{
for(int j = 0; j < h; j++)
{
if (i < 28)
{
image[i*w+j].rgbRed = 139;
image[i*w+j].rgbGreen = 0;
image[i*w+j].rgbBlue = 255;
continue;
}
else if (i < 56)
{
image[i*w+j].rgbRed = 0;
image[i*w+j].rgbGreen = 0;
image[i*w+j].rgbBlue = 255;
continue;
}
else if (i < 84)
{
image[i*w+j].rgbRed = 0;
image[i*w+j].rgbGreen = 127;
image[i*w+j].rgbBlue = 255;
continue;
}
else if (i < 112)
{
image[i*w+j].rgbRed = 0;
image[i*w+j].rgbGreen = 255;
image[i*w+j].rgbBlue = 0;
continue;
}
else if (i < 140)
{
image[i*w+j].rgbRed = 255;
image[i*w+j].rgbGreen = 255;
image[i*w+j].rgbBlue = 0;
continue;
}
else if (i < 168)
{
image[i*w+j].rgbRed = 255;
image[i*w+j].rgbGreen = 165;
image[i*w+j].rgbBlue = 0;
continue;
}
else
{
image[i*w+j].rgbRed = 255;
image[i*w+j].rgbGreen = 0;
image[i*w+j].rgbBlue = 0;
continue;
}