一、BMP编码简介
1、什么是BMP?
BMP(全称Bitmap)是Windows操作系统中的标准图像文件格式,文件后缀名为“.bmp”。它采用位映射存储格式,除了图像深度可选外,一般不采用其他任何压缩,因此BMP文件所占用的空间很大,但是没有失真。BMP文件的图像深度可选1bit、4bit、8bit、16bit、24bit、32bit。BMP文件存储数据时,图像的扫描方式是按照从左到右、从下到上的顺序。
2、典型的BMP图像文件由四部分组成:
1)位图头文件数据结构,它包含BMP图像文件的类型、显示内容等信息;
2)位图信息数据结构,它包含有BMP图像的宽、高、压缩方法,以及定义颜色等信息;
3)调色板,这部分可选,有些位图需要调色板,有些位图不需要调色板(比如24位的BMP真彩色图就不需要调色板);
4)位图数据,这部分的内容根据BMP位图使用的位数不同而不同,在24位图中直接使用RGB,而其它的小于24位的使用调色板中颜色索引值;
3、BMP组成:
1)BMP文件头(14字节):BMP文件头数据结构含有BMP文件的类型、文件大小和位图起始位置等信息。BMP文件头结构体定义如下:
//BMP文件头
typedef __packed struct
{
u16 bfType; //文件标志,只对‘B’‘M’,用来识别BMP位图类型
u32 bfSize; //文件大小,占四个字节
u16 bfReserved1;//保留
u16 bfReserved2;//保留
u32 bfOffBits; //从文件开始到位图数据(bitmap data)开始之间的偏移量
}BITMAPFILEHADER;
2)位图信息头(40字节):BMP位图信息头数据用于说明位图的尺寸等信息。BMP位图信息头结构体定义如下:
3)颜色表:颜色表用于说明位图中的颜色,它有若干个表项,每一个表项是一个RGBQUAD类型的结构,定义一种颜色,如下图:
typedef __packed struct
{
u8 rgbBlue; //指定蓝色强度
u8 rgbGreen; //指定绿色强度
u8 rgbRed; //指定红色强度
u8 rgbReserved; //保留,设置0
}RGBQUAD;
RGBQUAD结构数据的个数由biBitCount来确定:当biBitCount=1、4、8时,分别有2、16、256个表项;当biBitCount大于8时,没有颜色选项。
BMP文件头、位图信息头和颜色表组成位图信息,BITMAPINFO结构定义如下:
typedef __packed struct
{
BITMAPFILEHADER bmfHeader;
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[1];
}BITMAPINFO;
4)位图数据:记录了位图的每一个像素值,记录顺序是在扫描行内是从左到右,扫描行间是从下到上。位图一个像素值所占字节数:
当biBitCount=1时,8个像素占1个字节;
当biBitCount=4时,2个像素占1个字节;
当biBitCount=8时,1个像素占1个字节;
当biBitCount=16时,1个像素占2个字节;
当biBitCount=24时,1个像素占3个字节;
当biBitCount=32时,1个像素占4个字节;
biBitCount=16,即高彩色(65K色)。
当biCompression=BI_RGB(0),则采用RGB555个数,最高位恒为0;
当biCompression=BI_BITFIELDS(3),则在原来调色板位置用3个DWORD类型的掩码替换,分别代表红、绿、蓝三色的掩码,一般是:0X7C00、0X03E0、0X001F。
5)我们采用16位BMP编码(因为LCD就是16位色的,而且16位BMP编码比24位编码更省空间),故我们需要设置biBitCount的值为16,这样得到新的位图信息(BITMAPINFO)结构体:
typedef __packed struct
{
BITMAPFILEHEADER bmfHeader;
BITMAPINFOHEADER bmfHeader;
u32 RGB_MASK[3]; //调色板用于存放RGB掩码
}BITMAPINFO;
RGB_MASK[3],即颜色掩码,分别代表红、绿、蓝三色的掩码,分别是:0X7C00、0X03E0、0X001F。
4、BMP编码步骤:
1)创健BMP位图信息,初始化各个相关信息
首先,我们要设置BMP图片的分辨率为LCD分辨率、BMP图片的大小(整个BMP文件大小)、BMP的像素位数(16位)和掩码等信息;
2)创建鑫BMP文件,写入BMP位图信息
我们要保存BMP,当然要存放在某个地方(文件),所以需要先创建文件,同时先保存BMP位图信息,之后才开始BMP数据的写入。
3)保存位图数据
只需要从LCD的GRAM里面读取各点的颜色值,依次写入第二步创建的BMP文件即可。注意:保存顺序(即读GRAM顺序)是从左到右、从下到上。
4)关闭文件
使用FATFS,在文件创建之后,必须调用f_close,文件才会真正体现在文件系统里面,否则是不会写入的(要特别注意:写完之后必须调用f_close)。
二、JPEG编码简介
1、什么是JPEG?
JPEG是一个由ISO和IEC两个组织机构联合组成的一个专家组,负责制定静态的数字图像数据压缩码标准,该组开发的算法称为JPEG算法。
JPEG是一个适用范围很广的静态图像压缩标准,即可用于灰度图像又可用于彩色图像。JPEF专家组开发了两种基本的压缩算法,一种是采用以离散余弦变换为基础的有损压缩算法,另一种是采用以预测技术为基础的无损压缩算法。使用有损压缩算法时,在压缩比为25:1的情况下,压缩后还原得到的图像与原始图像相比较,非图像专家难于找出它们之间的区别,因此得到了广泛的应用。
2、JPEG编码步骤
JPEG压缩是有损压缩,它利用了人的视角系统的特性,使用量化和无损压缩编码相结合来去掉视角的冗余信息和数据本事的冗余信息。
JPEG压缩编码分为三个步骤:
1)使用正向离散余弦变换,把空间域表示的图变换成频率域表示的图。
2)使用加权函数对DTC系统进行量化,这个加权函数对于人的视觉系统是最佳的。
3)使用霍夫曼可变字长编码器对量化系数进行编码。
JPEG(直接从摄像头读数据)拍照步骤如下:
1)初始化STM32F4的DCMI接口和OV2640模块
首先,初始化DCMI接口(包括开启DMA和相关中断)和相关IO,然后配置好OV2640输出JPEG数据流。
2)读取OV2640模块的数据
在DCMI接口的驱动下,有序读取OV2640输出的JPEG数据流,我们采用DMA双缓冲来接收JPEG数据流,并将这些数据及时搬运到外部SRAM(通过DMA的传输完成中断来处理)。
3)保存JPEG数据
在采集完一帧JPEG数据之后,利用fatfs,创建一个.jpg文件,然后将存储在外部SRAM的数组(以0XFF,0XD8开头)存储在这个文件里面,最后调用f_close关闭文件,即可实现JPEG拍照保存。
3、JPEG拍照过程
在摄像头实验里面,我们定义了一个很大的数组jpeg_buf(124KB)来存储JPEG图像数据,但是本例程要用到内存管理,其他地方也要用到一些数组,所以无法再定义这么大的数组了。并且这个数组不能使用外部SRAM(实测:DCMI接口使用DMA直接传输JPEG数据到外部SRAM会出现数据丢失,所以DMA接收JPEG数据只能用内部SRAM),所以本例程使用DMA的双缓冲机制来读取,DMA双缓冲读取JPEG数据框图如下图:
DMA接收来自OV2640的JPEG数据流,首先使用M0AR(内存1)来存储,当M0AR满了以后,自动切换到M1AR(内存2),同时程序读取M0AR(内存1)的数据到外部SRAM;当M1AR满了以后,又切回到M0AR,同时程序读取M1AR(内存2)的数据到外部SRAM;依次循环(此时的数据处理是通过DMA传输完成中断实现的,在中断里面处理),知道帧结束,结束一帧数据的采集,读取剩余数据到外部SRAM,完成一次JPEG数据的采集。
这里。M0AR、M1AR所指向的内存,必须是内部内存,不过由于采用了双缓冲机制,我们就不必定义一个很大的数组,一次性接收所有的JPEG数据了,而是可以分批次接收,数组可以定义的比较小。
最后,将存储在外部SRAM的jpeg数据保存为.jpeg/.jpg存放在SD卡,就完成了一次JPEG拍照。
三、关键代码:
1、bmp_encode函数
//BMP编码函数
//将当前LCD屏幕的指定区域截图,存为16位格式的BMP文件 RGB565格式.
//保存为rgb565则需要掩码,需要利用原来的调色板位置增加掩码.这里我们已经增加了掩码.
//保存为rgb555格式则需要颜色转换,耗时间比较久,所以保存为565是最快速的办法.
//filename:存放路径
//x,y:在屏幕上的起始坐标
//mode:模式.0,仅仅创建新文件的方式编码;1,如果之前存在文件,则覆盖之前的文件.如果没有,则创建新的文件.
//返回值:0,成功;其他,错误码.
u8 bmp_encode(u8 *filename,u16 x,u16 y,u16 width,u16 height,u8 mode)
{
FIL* f_bmp; //文件指针
u16 bmpheadsize; //bmp头大小
BITMAPINFO hbmp; //bmp头
u8 res=0;
u16 tx,ty; //图像尺寸
u16 *databuf; //数据缓存区地址
u16 pixcnt; //像素计数器
u16 bi4width; //水平像素字节数
if(width==0||height==0)return PIC_WINDOW_ERR; //区域错误
if((x+width-1)>lcddev.width)return PIC_WINDOW_ERR; //区域错误
if((y+height-1)>lcddev.height)return PIC_WINDOW_ERR; //区域错误
#if BMP_USE_MALLOC == 1 //使用malloc
databuf=(u16*)pic_memalloc(1024); //开辟至少bi4width大小的字节的内存区域 ,对240宽的屏,480个字节就够了.
if(databuf==NULL)return PIC_MEM_ERR; //内存申请失败.
f_bmp=(FIL *)pic_memalloc(sizeof(FIL)); //开辟FIL字节的内存区域
if(f_bmp==NULL) //内存申请失败.
{
pic_memfree(databuf);
return PIC_MEM_ERR;
}
#else
databuf=(u16*)bmpreadbuf;
f_bmp=&f_bfile;
#endif
bmpheadsize=sizeof(hbmp);//得到bmp文件头的大小
mymemset((u8*)&hbmp,0,sizeof(hbmp));//置零空申请到的内存.
hbmp.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);//信息头大小
hbmp.bmiHeader.biWidth=width; //bmp的宽度
hbmp.bmiHeader.biHeight=height; //bmp的高度
hbmp.bmiHeader.biPlanes=1; //恒为1
hbmp.bmiHeader.biBitCount=16; //bmp为16位色bmp
hbmp.bmiHeader.biCompression=BI_BITFIELDS;//每个象素的比特由指定的掩码决定。
hbmp.bmiHeader.biSizeImage=hbmp.bmiHeader.biHeight*hbmp.bmiHeader.biWidth*hbmp.bmiHeader.biBitCount/8;//bmp数据区大小
hbmp.bmfHeader.bfType=((u16)'M'<<8)+'B';//BM格式标志
hbmp.bmfHeader.bfSize=bmpheadsize+hbmp.bmiHeader.biSizeImage;//整个bmp的大小
hbmp.bmfHeader.bfOffBits=bmpheadsize;//到数据区的偏移
hbmp.RGB_MASK[0]=0X00F800; //红色掩码
hbmp.RGB_MASK[1]=0X0007E0; //绿色掩码
hbmp.RGB_MASK[2]=0X00001F; //蓝色掩码
if(mode==1)res=f_open(f_bmp,(const TCHAR*)filename,FA_READ|FA_WRITE);//尝试打开之前的文件
if(mode==0||res==0x04)res=f_open(f_bmp,(const TCHAR*)filename,FA_WRITE|FA_CREATE_NEW);//模式0,或者尝试打开失败,则创建新文件
if((hbmp.bmiHeader.biWidth*2)%4)//水平像素(字节)不为4的倍数
{
bi4width=((hbmp.bmiHeader.biWidth*2)/4+1)*4;//实际要写入的宽度像素,必须为4的倍数.
}else bi4width=hbmp.bmiHeader.biWidth*2; //刚好为4的倍数
if(res==FR_OK)//创建成功
{
res=f_write(f_bmp,(u8*)&hbmp,bmpheadsize,&bw);//写入BMP首部
for(ty=y+height-1;hbmp.bmiHeader.biHeight;ty--)
{
pixcnt=0;
for(tx=x;pixcnt!=(bi4width/2);)
{
if(pixcnt<hbmp.bmiHeader.biWidth)databuf[pixcnt]=LCD_ReadPoint(tx,ty);//读取坐标点的值
else databuf[pixcnt]=0Xffff;//补充白色的像素.
pixcnt++;
tx++;
}
hbmp.bmiHeader.biHeight--;
res=f_write(f_bmp,(u8*)databuf,bi4width,&bw);//写入数据
}
f_close(f_bmp);
}
#if BMP_USE_MALLOC == 1 //使用malloc
pic_memfree(databuf);
pic_memfree(f_bmp);
#endif
return res;
}
2、DCMI_DMA_Init函数
//DCMI DMA配置
//memaddr:存储器地址 将要存储摄像头数据的内存地址(也可以是外设地址)
//DMA_BufferSize:存储器长度 0~65535
//DMA_MemoryDataSize:存储器位宽 @defgroup DMA_memory_data_size :DMA_MemoryDataSize_Byte/DMA_MemoryDataSize_HalfWord/DMA_MemoryDataSize_Word
//DMA_MemoryInc:存储器增长方式 @defgroup DMA_memory_incremented_mode /** @defgroup DMA_memory_incremented_mode : DMA_MemoryInc_Enable/DMA_MemoryInc_Disable
void DCMI_DMA_Init(u32 DMA_Memory0BaseAddr,u32 DMA_Memory1BaseAddr,u16 DMA_BufferSize,u32 DMA_MemoryDataSize,u32 DMA_MemoryInc)
{
DMA_InitTypeDef DMA_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2时钟使能
DMA_DeInit(DMA2_Stream1);//等待DMA2_Stream1
while (DMA_GetCmdStatus(DMA2_Stream1) != DISABLE){}//等待DMA2_Stream1可配置
/* 配置 DMA Stream */
DMA_InitStructure.DMA_Channel = DMA_Channel_1; //通道1 DCMI通道
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&DCMI->DR;//外设地址为:DCMI->DR
DMA_InitStructure.DMA_Memory0BaseAddr = DMA_Memory0BaseAddr;//DMA 存储器0地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;//外设到存储器模式
DMA_InitStructure.DMA_BufferSize = DMA_BufferSize;//数据传输量
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc;//存储器增量模式
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;//外设数据长度:32位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize;//存储器数据长度
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;// 使用循环模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High;//高优先级
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable; //FIFO模式
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;//使用全FIFO
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//外设突发单次传输
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//存储器突发单次传输
DMA_Init(DMA2_Stream1, &DMA_InitStructure);//初始化DMA Stream
if(DMA_Memory1BaseAddr)
{
DMA_DoubleBufferModeCmd(DMA2_Stream1,ENABLE);//双缓冲模式
DMA_MemoryTargetConfig(DMA2_Stream1,DMA_Memory1BaseAddr,DMA_Memory_1);//配置目标地址1
DMA_ITConfig(DMA2_Stream1,DMA_IT_TC,ENABLE);//开启传输完成中断
NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;//抢占优先级0
NVIC_InitStructure.NVIC_IRQChannelSubPriority =0; //子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器、
}
}
3、DMA2_Stream1_IRQHandler函数
void DMA2_Stream1_IRQHandler(void)
{
if(DMA_GetFlagStatus(DMA2_Stream1,DMA_FLAG_TCIF1)==SET)//DMA2_Steam1,传输完成标志
{
DMA_ClearFlag(DMA2_Stream1,DMA_FLAG_TCIF1);//清除传输完成中断
dcmi_rx_callback(); //执行摄像头接收回调函数,读取数据等操作在这里面处理
}
}
4、jpeg_data_process函数
//处理JPEG数据
//当采集完一帧JPEG数据后,调用此函数,切换JPEG BUF.开始下一帧采集.
void jpeg_data_process(void)
{
u16 i;
u16 rlen;//剩余数据长度
u32 *pbuf;
if(ov2640_mode)//只有在JPEG格式下,才需要做处理.
{
if(jpeg_data_ok==0) //jpeg数据还未采集完?
{
DMA_Cmd(DMA2_Stream1,DISABLE); //停止当前传输
while(DMA_GetCmdStatus(DMA2_Stream1) != DISABLE); //等待DMA2_Stream1可配置
rlen=jpeg_dma_bufsize-DMA_GetCurrDataCounter(DMA2_Stream1);//得到剩余数据长度
pbuf=jpeg_data_buf+jpeg_data_len;//偏移到有效数据末尾,继续添加
if(DMA2_Stream1->CR&(1<<19))for(i=0;i<rlen;i++)pbuf[i]=jpeg_buf1[i];//读取buf1里面的剩余数据
else for(i=0;i<rlen;i++)pbuf[i]=jpeg_buf0[i];//读取buf0里面的剩余数据
jpeg_data_len+=rlen; //加上剩余长度
jpeg_data_ok=1; //标记JPEG数据采集完按成,等待其他函数处理
}
if(jpeg_data_ok==2) //上一次的jpeg数据已经被处理了
{ DMA_SetCurrDataCounter(DMA2_Stream1,jpeg_dma_bufsize);//传输长度为jpeg_buf_size*4字节
DMA_Cmd(DMA2_Stream1,ENABLE); //重新传输
jpeg_data_ok=0; //标记数据未采集
jpeg_data_len=0; //数据重新开始
}
}
else
{
LCD_SetCursor(0, 0);
LCD_WriteRAM_Prepare(); //开始写入GRAM
hsync_int = 1;
}
}