BMP (Bitmap) 图像格式是一种无损压缩的位图文件格式,最初由微软公司在Windows操作系统中引入,用于存储图像数据。BMP格式的主要优点是它简单易用,且支持多种颜色深度。这种格式不包含任何压缩算法,这意味着图像的质量不会因为保存而损失,但这也导致了文件大小相对较大。
当前做的项目是采用STM32F103ZET6单片机接上OV7725(带FIFO)摄像头实现图像采集,拍照功能。 OV7725摄像头输出的格式是RGB565像素格式,为了方便将OV7725摄像头返回的图像数据放在SD卡里存储,并且能够在电脑上打开,通过图片查看软件查看。就需要将RGB565像素数据封装成一张图片格式,也就是相当于加一个壳子。这样电脑上的图片查看器就可以正常查看图片了。
目前的图片格式有很多,平时最常见的有JPG、PNG、BMP这些格式。 这里面的JPG是压缩格式,保存的图片可以很小,JPEG使用离散余弦变换(DCT)压缩,这个算法在单片机上实现的要求毕竟高,毕竟单片机的性能摆在这里。 而BMP是不包含任何压缩算法,存储的是原始的像素格式,作为单片机里拍照存储这是首选的图片封装格式了。
整个项目设计完的核心功能是:
通过OV7725摄像头采集一帧RGB565格式的图像,并将其封装成BMP格式后,利用FATFS文件系统存储到SD卡上。项目中,STM32单片机通过SPI协议与SD卡进行通信。由于OV7725摄像头输出的是RGB565格式的数据,而标准BMP文件使用RGB888格式存储像素数据,因此还涉及到了图像格式的转换问题。
要完成这个项目涉及的技术其实也有几个的:
(1)SD卡的驱动编写,SD卡支持SDIO和SPI两种协议。 要说简单那自然首选SPI协议,不过就是速度稍微慢一点。
(2)OV7725摄像头的驱动编写,毕竟要从摄像头里读取数据。分为控制协议和数据总线。
(3)FATFS文件系统的移植,如果在单片机上要以文件的形式管理SD卡,那肯定是需要文件系统了。
(4)BMP图片的格式理解,要将图片保存为BMP图片格式。需要完全理解BMP图片格式的是如何的封装的。
这篇文章最重要的是内容是讲解“ BMP图片如何封装,学习BMP图像格式封装,RGB565与RGB888像素点转换。
BMP 文件的内部格式组成:
(1)文件头 (File Header)
(2)信息头 (Info Header)
(3)颜色表 (Color Table)
(4)像素数据 (Pixel Data)
下面是BMP文件格式的一个详细描述,包括每个字段的名称、长度、含义以及它们在文件中的位置。
字段名称 | 类型 | 长度 (字节) | 描述 |
---|---|---|---|
bfType | 字符串 | 2 | 文件类型的标识,通常为 BM (0x424D) |
bfSize | DWORD | 4 | 整个文件的大小,包括文件头、信息头和像素数据 |
bfReserved1 | WORD | 2 | 保留字段,应设为0 |
bfReserved2 | WORD | 2 | 保留字段,应设为0 |
bfOffBits | DWORD | 4 | 像素数据相对于文件起始位置的偏移量 |
biSize | DWORD | 4 | 信息头的大小,通常为40 (0x28) |
biWidth | LONG | 4 | 图像的宽度(以像素为单位),可以是正数或负数 |
biHeight | LONG | 4 | 图像的高度(以像素为单位),可以是正数或负数 |
biPlanes | WORD | 2 | 平面数,通常为1 |
biBitCount | WORD | 2 | 每个像素的位数,常见的值有1、4、8、16、24或32 |
biCompression | DWORD | 4 | 压缩方法,如果是0,则表示没有压缩 |
biSizeImage | DWORD | 4 | 压缩后的图像大小,如果未压缩,则该值可能为0 |
biXPelsPerMeter | LONG | 4 | 水平方向上的分辨率(每米像素数),通常为0 |
biYPelsPerMeter | LONG | 4 | 垂直方向上的分辨率(每米像素数),通常为0 |
biClrUsed | DWORD | 4 | 调色板中的颜色数目,如果为0,则表示所有可能的颜色都被使用 |
biClrImportant | DWORD | 4 | 重要的颜色数目,如果为0,则表示所有颜色都同样重要 |
Color Table | RGBQUAD | 0 or more | 调色板(仅当位数小于24时存在),每个颜色占用4字节 |
Pixel Data | BYTE[] | 变长 | 像素数据,按从左到右、从下到上的顺序排列,每行可能有填充字节 |
对于24位的BMP文件(即 biBitCount 的值为24),不会存在颜色表,每个像素直接由三个字节(RGB888格式)表示。
RGB565和RGB888都是色彩模型在计算机图形学中的具体实现方式,它们分别代表了不同位深的颜色编码方式。这两种格式主要用于存储图像数据,特别是在显示设备和图像处理软件中。
RGB565 是一种16位的彩色图像格式,其中红色和蓝色各占用5位,绿色占用6位。这是因为人眼对绿色更为敏感,因此给绿色分配更多的位数来提高颜色精度。这种格式通常用于节省存储空间或减少内存带宽的需求,尤其是在早期的移动设备和嵌入式系统中非常常见。
这种格式的总位数为16位,可以表示 (2^{16}) 或者 65,536 种不同的颜色。
RGB888 是一种24位的彩色图像格式,每种颜色(红、绿、蓝)都使用8位来表示。这意味着每种颜色都有256级灰度等级,总共可以表示 (2^{24}) 或者 16,777,216 种不同的颜色。
由于RGB888格式使用更多的位数来表示颜色,所以它能够提供更丰富的色彩细节,这对于高保真度的图像来说是非常重要的。
可以创建一个简单的例子来说明这些格式是如何工作的。假设有一个像素,它的红色、绿色和蓝色分量分别为128(十六进制为0x80)。
下面是一个将 RGB565 数组转换为 RGB888 数组的 C 语言函数:
- #include <stdint.h>
- #include <stdlib.h>
- #include <stdio.h>
-
- // 将 RGB565 转换为 RGB888 的函数
- void RGB565_to_RGB888_array(const uint16_t *rgb565_array, size_t length, uint8_t *rgb888_array) {
- for (size_t i = 0; i < length; i++) {
- uint16_t rgb565 = rgb565_array[i];
-
- // 提取 RGB565 中的颜色分量
- uint8_t red = (rgb565 >> 11) & 0x1F; // 5 bits
- uint8_t green = (rgb565 >> 5) & 0x3F; // 6 bits
- uint8_t blue = rgb565 & 0x1F; // 5 bits
-
- // 将颜色分量扩展到 8 位
- uint8_t r = (red << 3) | (red >> 2); // 5 bits to 8 bits
- uint8_t g = (green << 2) | (green >> 4); // 6 bits to 8 bits
- uint8_t b = (blue << 3) | (blue >> 2); // 5 bits to 8 bits
-
- // 将结果存储到 RGB888 数组
- rgb888_array[i * 3] = r;
- rgb888_array[i * 3 + 1] = g;
- rgb888_array[i * 3 + 2] = b;
- }
- }
-
- int main() {
- // 示例 RGB565 数组
- uint16_t rgb565_array[] = {0x1F3F, 0x07E0, 0xF800};
- size_t length = sizeof(rgb565_array) / sizeof(rgb565_array[0]);
-
- // 分配 RGB888 数组内存
- uint8_t *rgb888_array = (uint8_t *)malloc(length * 3 * sizeof(uint8_t));
- if (rgb888_array == NULL) {
- perror("Unable to allocate memory for RGB888 array");
- return 1;
- }
-
- // 转换 RGB565 数组到 RGB888 数组
- RGB565_to_RGB888_array(rgb565_array, length, rgb888_array);
-
- // 打印 RGB888 结果
- for (size_t i = 0; i < length; i++) {
- printf("RGB888[%zu]: R=%d, G=%d, B=%d\n", i, rgb888_array[i * 3], rgb888_array[i * 3 + 1], rgb888_array[i * 3 + 2]);
- }
-
- // 释放分配的内存
- free(rgb888_array);
-
- return 0;
- }
-
这个函数 RGB565_to_RGB888_array 接收一个 RGB565 数组和数组的长度,并返回一个 RGB888 数组。每个 RGB565 值被转换为三个 8 位的 RGB 分量,并存储在提供的 RGB888 数组中。示例中的 main 函数展示了如何调用这个转换函数并打印结果。
- #ifndef BMP_H
- #define BMP_H
- #include "ff.h"
- #include "string.h"
- #include "sys.h"
- #pragma pack(1) /* 必须在结构体定义之前使用,这是为了让结构体中各成员按1字节对齐 */
-
- /*需要文件信息头:14个字节 */
- typedef struct tagBITMAPFILEHEADER
- {
- unsigned short bfType; //保存图片类似。 'BM'
- unsigned long bfSize; //图片的大小
- unsigned short bfReserved1;
- unsigned short bfReserved2;
- unsigned long bfOffBits; //RGB数据偏移地址
- }BITMAPFILEHEADER;
-
- /* 位图信息头 */
- typedef struct tagBITMAPINFOHEADER { /* bmih */
- unsigned long biSize; //结构体大小
- unsigned long biWidth; //宽度
- unsigned long biHeight; //高度
- unsigned short biPlanes;
- unsigned short biBitCount; //颜色位数
- unsigned long biCompression;
- unsigned long biSizeImage;
- unsigned long biXPelsPerMeter;
- unsigned long biYPelsPerMeter;
- unsigned long biClrUsed;
- unsigned long biClrImportant;
- }BITMAPINFOHEADER;
-
- #define RGB888_RED 0x00ff0000
- #define RGB888_GREEN 0x0000ff00
- #define RGB888_BLUE 0x000000ff
-
- #define RGB565_RED 0xf800
- #define RGB565_GREEN 0x07e0
- #define RGB565_BLUE 0x001f
-
- u8 photograph_BMP(u8 *filename,int Width,int Height);
- void photograph_open(u8 *filename,int Width,int Height);
- void photograph_write(u16 *buff);
- void photograph_close(void);
- #endif
-
- #include "bmp.h"
- unsigned short RGB888ToRGB565(unsigned int n888Color)
- {
- unsigned short n565Color = 0;
-
- // 获取RGB单色,并截取高位
- unsigned char cRed = (n888Color & RGB888_RED) >> 19;
- unsigned char cGreen = (n888Color & RGB888_GREEN) >> 10;
- unsigned char cBlue = (n888Color & RGB888_BLUE) >> 3;
-
- // 连接
- n565Color = (cRed << 11) + (cGreen << 5) + (cBlue << 0);
- return n565Color;
- }
-
- unsigned int RGB565ToRGB888(unsigned short n565Color)
- {
- unsigned int n888Color = 0;
-
- // 获取RGB单色,并填充低位
- unsigned char cRed = (n565Color & RGB565_RED) >> 8;
- unsigned char cGreen = (n565Color & RGB565_GREEN) >> 3;
- unsigned char cBlue = (n565Color & RGB565_BLUE) << 3;
-
- // 连接
- n888Color = (cRed << 16) + (cGreen << 8) + (cBlue << 0);
- return n888Color;
- }
-
-
- //拍摄BMP的图片
- u8 photograph_BMP(u8 *filename,int Width,int Height)
- {
- u32 cnt;
- int x,y;
- u8 res;
- char *p;
- u16 c16; //16位颜色值
- u32 c32; //24位颜色值
-
- BITMAPFILEHEADER BmpHead; //保存图片文件头的信息
- BITMAPINFOHEADER BmpInfo; //图片参数信息
-
- /*1. 创建BMP文件*/
- FIL file;
- res = f_open(&file,(char*)filename, FA_OPEN_ALWAYS | FA_READ | FA_WRITE); //读写加创建
- if(res!=0)return 1;
-
- /*2. 填充图片数据头*/
- memset(&BmpHead,0,sizeof(BITMAPFILEHEADER));
- p=(char*)&BmpHead.bfType; //填充BMP图片的类型
- *p='B';
- *(p+1)='M';
- //BmpHead.bfType=0x4d42;//'B''M' //0x4d42
- BmpHead.bfSize=Width*Height*3+54; //图片的总大小
- BmpHead.bfOffBits=54; //图片数据的偏移量
- res=f_write(&file,&BmpHead,sizeof(BITMAPFILEHEADER),&cnt);//写入图片文件头到文文件
- if(res!=0)return 1;
-
- /*3. 填充图片参数*/
- memset(&BmpInfo,0,sizeof(BITMAPINFOHEADER));
- BmpInfo.biSize=sizeof(BITMAPINFOHEADER); //当前结构体大小
- BmpInfo.biWidth=Width;
- BmpInfo.biHeight=Height;
- BmpInfo.biPlanes=1;
- BmpInfo.biBitCount=24;
- res=f_write(&file,&BmpInfo,sizeof(BITMAPINFOHEADER),&cnt);//写入图片文件头到文文件
- if(res!=0)return 1;
-
- /*4. 读取图像参数进行填充*/
- for(y=Height-1;y>=0;y--) //因为BMP图片特性,所有需要从LCD最后一行开始读
- {
- for(x=0;x<Width;x++)
- {
- //c16=LcdReadPoint(x,y); //LCD读点函数
- c32=RGB565ToRGB888(c16); //将16位的颜色转为32位
- f_write(&file,&c32,3,&cnt); //写入图片数据
- }
- }
-
- /*. 关闭文件*/
- f_close(&file);
- return 0;
- }
-
-
- BITMAPFILEHEADER BmpHead; //保存图片文件头的信息
- BITMAPINFOHEADER BmpInfo; //图片参数信息
- #include <stdio.h>
- FIL BMP_file;
- //拍摄1: 创建文件
- void photograph_open(u8 *filename,int Width,int Height)
- {
- u32 cnt;
- u8 res;
- char *p;
-
- /*1. 创建BMP文件*/
- res = f_open(&BMP_file,(char*)filename, FA_OPEN_ALWAYS | FA_READ | FA_WRITE); //读写加创建
- if(res!=0)
- {
- printf("%s文件打开失败.!\r\n",filename);
- return;
- }
-
- /*2. 填充图片数据头*/
- memset(&BmpHead,0,sizeof(BITMAPFILEHEADER));
- p=(char*)&BmpHead.bfType; //填充BMP图片的类型
- *p='B';
- *(p+1)='M';
- //BmpHead.bfType=0x4d42;//'B''M' //0x4d42
- BmpHead.bfSize=Width*Height*3+54; //图片的总大小
- BmpHead.bfOffBits=54; //图片数据的偏移量
- res=f_write(&BMP_file,&BmpHead,sizeof(BITMAPFILEHEADER),&cnt);//写入图片文件头到文文件
- if(res!=0)
- {
- printf("%s BMP文件头1写入失败.!\r\n",filename);
- return;
- }
- else
- {
- printf("%s BMP文件头1写入成功!.%d字节.\r\n",filename,cnt);
- }
-
- /*3. 填充图片参数*/
- memset(&BmpInfo,0,sizeof(BITMAPINFOHEADER));
- BmpInfo.biSize=sizeof(BITMAPINFOHEADER); //当前结构体大小
- BmpInfo.biWidth=Width;
- BmpInfo.biHeight=Height;
- BmpInfo.biPlanes=1;
- BmpInfo.biBitCount=24;
- res=f_write(&BMP_file,&BmpInfo,sizeof(BITMAPINFOHEADER),&cnt);//写入图片文件头到文文件
- if(res!=0)
- {
- printf("%s BMP文件头2数据写入失败.!\r\n",filename);
- return;
- }
- else
- {
- printf("%s BMP文件头2数据写入成功.!%d字节\r\n",filename,cnt);
- }
- }
-
-
- //拍摄2: 写文件
- void photograph_write(u16 *buff)
- {
- u32 c32; //24位颜色值
- UINT cnt;
- u8 res;
- int x;
- /*4. 读取图像参数进行填充*/
- for(x=0;x<320;x++)
- {
- c32=RGB565ToRGB888(buff[x]); //将16位的颜色转为32位
- res=f_write(&BMP_file,&c32,3,&cnt); //写入图片数据
- if(res!=0)
- {
- printf("图像数据写入失败.%d\r\n",x);
- break;
- }
- }
- }
-
-
- //拍摄3: 关闭文件
- void photograph_close(void)
- {
- /*. 关闭文件*/
- f_close(&BMP_file);
- }
-