<a name="_Toc342394350">47.1 图片格式简介 我们常用的图片格式有很多,一般最常用的有三种:JPEG(或JPG)、BMP和GIF。其中JPEG(或JPG)和BMP是静态图片,而GIF则是可以实现动态图片。下面,我们简单介绍一下这三种图片格式。 首先,我们来看看BMP图片格式。BMP(全称Bitmap)是Window操作系统中的标准图像文件格式,文件后缀名为“.bmp”,使用非常广。它采用位映射存储格式,除了图像深度可选以外,不采用其他任何压缩,因此,BMP文件所占用的空间很大,但是没有失真。BMP文件的图像深度可选lbit、4bit、8bit、16bit、24bit及32bit。BMP文件存储数据时,图像的扫描方式是按从左到右、从下到上的顺序。 典型的BMP图像文件由四部分组成: 1, 位图头文件数据结构,它包含BMP图像文件的类型、显示内容等信息; 2, 位图信息数据结构,它包含有BMP图像的宽、高、压缩方法,以及定义颜色等信息 1, 调色板,这个部分是可选的,有些位图需要调色板,有些位图,比如真彩色图(24位的BMP)就不需要调色板; 2, 位图数据,这部分的内容根据BMP位图使用的位数不同而不同,在24位图中直接使用RGB,而其他的小于24位的使用调色板中颜色索引值。 关于BMP的详细介绍,请参考光盘的《BMP图片文件详解.pdf》。接下来我们看看JPEG文件格式。 JPEG是Joint Photographic Experts Group(联合图像专家组)的缩写,文件后辍名为“.jpg”或“.jpeg”,是最常用的图像文件格式,由一个软件开发联合会组织制定,同BMP格式不同,JPEG是一种有损压缩格式,能够将图像压缩在很小的储存空间,图像中重复或不重要的资料会被丢失,因此容易造成图像数据的损伤(BMP不会,但是BMP占用空间大)。尤其是使用过高的压缩比例,将使最终解压缩后恢复的图像质量明显降低,如果追求高品质图像,不宜采用过高压缩比例。但是JPEG压缩技术十分先进,它用有损压缩方式去除冗余的图像数据,在获得极高的压缩率的同时能展现十分丰富生动的图像,换句话说,就是可以用最少的磁盘空间得到较好的图像品质。而且 JPEG是一种很灵活的格式,具有调节图像质量的功能,允许用不同的压缩比例对文件进行压缩,支持多种压缩级别,压缩比率通常在10:1到40:1之间,压缩比越大,品质就越低;相反地,压缩比越小,品质就越好。比如可以把1.37Mb的BMP位图文件压缩至20.3KB。当然也可以在图像质量和文件尺寸之间找到平衡点。JPEG格式压缩的主要是高频信息,对色彩的信息保留较好,适合应用于互联网,可减少图像的传输时间,可以支持24bit真彩色,也普遍应用于需要连续色调的图像。 JPEG/JPG的解码过程可以简单的概述为如下几个部分: 1、从文件头读出文件的相关信息。 JPEG 文件数据分为文件头和图像数据两大部分,其中文件头记录了图像的版本、长宽、采样因子、量化表、哈夫曼表等重要信息。所以解码前必须将文件头信息读出,以备 图像数据解码过程之用。 2、从图像数据流读取一个最小编码单元(MCU) ,并提取出里边的各个颜色分量单元。 3、将颜色分量单元从数据流恢复成矩阵数据。 使用文件头给出的哈夫曼表,对分割出来的颜色分量单元进行解码,把其恢复成8×8 的数据矩阵。 4、8×8 的数据矩阵进一步解码。 此部分解码工作以8×8 的数据矩阵为单位, 其中包括相邻矩阵的直流系数差分解码、使用文件头给出的量化表反量化数据、反Zig- zag 编码、隔行正负纠正、反向离散余弦变换等5 个步骤, 最终输出仍然是一个8×8 的数据矩阵。 5、颜色系统YCrCb 向RGB 转换。 将一个MCU的各个颜色分量单元解码结果整合起来,将图像颜色系统从YCrCb 向RGB 转换。 6、排列整合各个MCU 的解码数据。 不断读取数据流中的MCU 并对其解码,直至读完所有MCU 为止,将各MCU 解码后的数据正确排列成完整的图像。 JPEG的解码本身是比较复杂的,在这里一时半会也说不清,更详细的介绍,请大家参考光盘JPEG解码的相关资料。BMP和JPEG这两种图片格式均不支持动态效果,而GIF则是可以支持动态效果。最后,我们来看看GIF图片格式。 GIF(Graphics Interchange Format)是CompuServe公司开发的图像文件存储格式,1987年开发的GIF文件格式版本号是GIF87a,1989年进行了扩充,扩充后的版本号定义为GIF89a。 GIF图像文件以数据块(block)为单位来存储图像的相关信息。一个GIF文件由表示图形/图像的数据块、数据子块以及显示图形/图像的控制信息块组成,称为GIF数据流(Data Stream)。数据流中的所有控制信息块和数据块都必须在文件头(Header)和文件结束块(Trailer)之间。 GIF文件格式采用了LZW(Lempel-Ziv Walch)压缩算法来存储图像数据,定义了允许用户为图像设置背景的透明(transparency)属性。此外,GIF文件格式可在一个文件中存放多幅彩色图形/图像。如果在GIF文件中存放有多幅图,它们可以像演幻灯片那样显示或者像动画那样演示。 一个GIF文件的结构可分为文件头(File Header)、GIF数据流(GIF Data Stream)和文件终结器(Trailer)三个部分。文件头包含GIF文件署名(Signature)和版本号(Version);GIF数据流由控制标识符、图象块(Image Block)和其他的一些扩展块组成;文件终结器只有一个值为0x3B的字符(';')表示文件结束。 关于GIF的详细介绍,请参考光盘GIF解码相关资料。图片格式简介,我们就介绍到这里。 <a name="_Toc342394352">47.3 软件设计 打开上一章的工程,首先在HARDWARE文件夹所在的文件夹下新建一个PICTURE的文件夹。在该文件夹里面新建bmp.c、bmp.h、jpeg.c、jpeg.h、gif.c、gif.h、piclib.c和piclib.h等8个文件。并将PICTURE文件夹加入头文件包含路径。 其中bmp.c和bmp.h用于实现对bmp文件的解码;jpeg.c和jpeg.h用于实现对jpeg/jpg文件的解码;gif.c和gif.h用于实现对gif文件的解码;这几个代码太长了,所以我们在这里不贴出来,请大家参考光盘本例程的源码,我们打开piclib.c,在里面输入如下代码: #include "piclib.h" #include "lcd.h" _pic_info picinfo; //图片信息 _pic_phy pic_phy; //图片显示物理接口 //lcd.h没有提供划横线函数,需要自己实现 void piclib_draw_hline(u16 x0,u16 y0,u16 len,u16 color) { if((len==0)||(x0>lcddev.width)||(y0>lcddev.height))return; LCD_Fill(x0,y0,x0+len-1,y0,color); } //画图初始化,在画图之前,必须先调用此函数 //指定画点/读点 void piclib_init(void) { pic_phy.read_point=LCD_ReadPoint; //读点函数实现 pic_phy.draw_point=LCD_Fast_DrawPoint; //画点函数实现 pic_phy.fill=LCD_Fill; //填充函数实现 pic_phy.draw_hline=piclib_draw_hline; //画线函数实现 picinfo.ImgWidth=0; //初始化宽度为0 picinfo.ImgHeight=0; //初始化高度为0 picinfo.Div_Fac=0; //初始化缩放系数为0 picinfo.S_Height=0; //初始化设定的高度为0 picinfo.S_Width=0; //初始化设定的宽度为0 picinfo.S_XOFF=0; //初始化x轴的偏移量为0 picinfo.S_YOFF=0; //初始化y轴的偏移量为0 picinfo.staticx=0; //初始化当前显示到的x坐标为0 picinfo.staticy=0; //初始化当前显示到的y坐标为0 } //快速ALPHA BLENDING算法. //src:源颜色 //dst:目标颜色 //alpha:透明程度(0~32) //返回值:混合后的颜色. u16 piclib_alpha_blend(u16 src,u16 dst,u8 alpha) { u32 src2; u32 dst2; src2=((src16)|dst2; } //初始化智能画点 //内部调用 void ai_draw_init(void) { float temp,temp1; temp=(float)picinfo.S_Width/picinfo.ImgWidth; temp1=(float)picinfo.S_Height/picinfo.ImgHeight; if(temp1)temp1=1; //使图片处于所给区域的中间 picinfo.S_XOFF+=(picinfo.S_Width-temp1*picinfo.ImgWidth)/2; picinfo.S_YOFF+=(picinfo.S_Height-temp1*picinfo.ImgHeight)/2; temp1*=8192;//扩大8192倍 picinfo.Div_Fac=temp1; picinfo.staticx=0xffff; picinfo.staticy=0xffff;//放到一个不可能的值上面 } //判断这个像素是否可以显示 //(x,y) :像素原始坐标 //chg :功能变量. //返回值:0,不需要显示.1,需要显示 u8 is_element_ok(u16 x,u16 y,u8 chg) { if(x!=picinfo.staticx||y!=picinfo.staticy) { if(chg==1){picinfo.staticx=x; picinfo.staticy=y;} return 1; }else return 0; } //智能画图 //FileName:要显示的图片文件 BMP/JPG/JPEG/GIF //x,y,width,height:坐标及显示区域尺寸 //acolor :alphablend的颜色(仅对不大于320*240的32位bmp有效!) //abdnum :alphablend的值(0~32有效,其余值表示不使用alphablend, //仅对不大于320*240的32位bmp有效!) //图片在开始和结束的坐标点范围内显示 u8 ai_load_picfile(const u8 *filename,u16 x,u16 y,u16 width,u16 height) { u8 res;//返回值 u8 temp; if((x+width)>lcddev.width)return PIC_WINDOW_ERR; //x坐标超范围了. if((y+height)>lcddev.height)return PIC_WINDOW_ERR; //y坐标超范围了. //得到显示方框大小 if(width==0||height==0)return PIC_WINDOW_ERR; //窗口设定错误 picinfo.S_Height=height; picinfo.S_Width=width; //显示区域无效 if(picinfo.S_Height==0||picinfo.S_Width==0) { picinfo.S_Height=lcddev.height; picinfo.S_Width=lcddev.width; return FALSE; } //显示的开始坐标点 picinfo.S_YOFF=y; picinfo.S_XOFF=x; //文件名传递 temp=f_typetell((u8*)filename); //得到文件的类型 switch(temp) { case T_BMP: res=stdbmp_decode(filename); break; //解码bmp case T_JPG: case T_JPEG: res=jpg_decode(filename); break; //解码JPG/JPEG case T_GIF: res=gif_decode(filename,x,y,width,height); break;//解码gif default: res=PIC_FORMAT_ERR;break; //非图片格式!!! } return res; } 此段代码总共6个函数,其中,piclib_draw_hline函数用于快速画横线,由gif解码程序使用。 piclib_init 函数,该函数用于初始化图片解码的相关信息,其中_pic_phy是我们在piclib.h里面定义的一个结构体,用于管理底层LCD接口函数,这些函数必须由用户在外部实现。_pic_info则是另外一个结构体,用于图片缩放处理。 piclib_alpha_blend函数,该函数用于实现半透明效果,在小格式(分辨率小于240*320)bmp解码的时候,可能被用到。 ai_draw_init函数,该函数用于实现图片在显示区域的居中显示初始化,其实就是根据图片大小选择缩放比例和坐标偏移值。 is_element_ok函数,该函数用于判断一个点是不是应该显示出来,在图片缩放的时候该函数是必须用到的。 ai_load_picfile函数,该函数是整个图片显示的对外接口,外部程序,通过调用该函数,可以实现bmp、jpg/jpeg和gif的显示,该函数根据输入文件的后缀名,判断文件格式,然后交给相应的解码程序(bmp解码/jpeg解码/gif解码),执行解码,完成图片显示。注意,这里我们用到一个f_typetell的函数,来判断文件的后缀名,f_typetell函数在exfuns.c里面实现,具体请参考光盘源码。 保存piclib.c,然后在工程里面新建一个PICTURE的分组,将bmp.c、gif.c、jpeg.c和piclib.c等4个c文件加入到PICTURE分组下。然后打开piclib.h,在该文件输入如下代码: #ifndef __PICLIB_H #define __PICLIB_H #include "sys.h" #include "lcd.h" #include "malloc.h" #include "ff.h" #include "exfuns.h" #include "bmp.h" #include "jpeg.h" #include "gif.h" #define PIC_FORMAT_ERR 0x27 //格式错误 #define PIC_SIZE_ERR 0x28 //图片尺寸错误 #define PIC_WINDOW_ERR 0x29 //窗口设定错误 #define PIC_MEM_ERR 0x11 //内存错误 //图片显示物理层接口 //在移植的时候,必须由用户自己实现这几个函数 typedef struct { u16(*read_point)(u16,u16);//u16 read_point(u16 x,u16 y) 读点函数 void(*draw_point)(u16,u16,u16); //void draw_point(u16 x,u16 y,u16 color) 画点函数 void(*fill)(u16,u16,u16,u16,u16); //void fill(u16 sx,u16 sy,u16 ex,u16 ey,u16 color) 单色填充函数 void(*draw_hline)(u16,u16,u16,u16); //void draw_hline(u16 x0,u16 y0,u16 len,u16 color) 画水平线函数 }_pic_phy; extern _pic_phy pic_phy; //图像信息 typedef struct { u32 ImgWidth; //图像的实际宽度和高度 u32 ImgHeight; u32 Div_Fac; //缩放系数 (扩大了8192倍的) u32 S_Height; //设定的高度和宽度 u32 S_Width; u32 S_XOFF; //x轴和y轴的偏移量 u32 S_YOFF; u32 staticx; //当前显示到的xy坐标 u32 staticy; }_pic_info; extern _pic_info picinfo;//图像信息 void piclib_init(void); //初始化画图 u16 piclib_alpha_blend(u16 src,u16 dst,u8 alpha); //alphablend处理 void ai_draw_init(void); //初始化智能画图 u8 is_element_ok(u16 x,u16 y,u8 chg); //判断像素是否有效 u8 ai_load_picfile(const u8 *filename,u16 x,u16 y,u16 width,u16 height);//智能画图 #endif 这里基本就是我们前面提到的两个结构体的定义以及一些函数的申明,保存piclib.h。最后我们在test.c文件里面修改代码如下: //得到path路径下,目标文件的总个数 //path:路径 //返回值:总有效文件数 u16 pic_get_tnum(u8 *path) { u8 res; u16 rval=0; DIR tdir; //临时目录 FILINFO tfileinfo; //临时文件信息 u8 *fn; res=f_opendir(&tdir,(const TCHAR*)path); //打开目录 tfileinfo.lfsize=_MAX_LFN*2+1; //长文件名最大长度 tfileinfo.lfname=mymalloc(SRAMIN,tfileinfo.lfsize); //为长文件缓存区分配内存 if(res==FR_OK&&tfileinfo.lfname!=NULL) { while(1)//查询总的有效文件数 { res=f_readdir(&tdir,&tfileinfo); //读取目录下的一个文件 if(res!=FR_OK||tfileinfo.fname[0]==0)break; //错误了/到末尾了,退出 fn=(u8*)(*tfileinfo.lfname?tfileinfo.lfname:tfileinfo.fname); res=f_typetell(fn); if((res&0XF0)==0X50) rval++;//图片文件?则有效文件加1 } } return rval; } int main(void) { u8 res; DIR picdir; //图片目录 FILINFO picfileinfo; //文件信息 u8 *fn; //长文件名 u8 *pname; //带路径的文件名 u16 totpicnum; //图片文件总数 u16 curindex; //图片当前索引 u8 key; //键值 u8 pause=0; //暂停标记 u8 t; u16 temp; u16 *picindextbl; //图片索引表 Stm32_Clock_Init(9); //系统时钟设置 delay_init(72); //延时初始化 uart_init(72,9600); //串口1初始化 LCD_Init(); //初始化液晶 LED_Init(); //LED初始化 KEY_Init(); //按键初始化 usmart_dev.init(72) ; //usmart初始化 mem_init(SRAMIN); //初始化内部内存池 exfuns_init(); //为fatfs相关变量申请内存 f_mount(0,fs[0]); //挂载SD卡 f_mount(1,fs[1]); //挂载FLASH. POINT_COLOR=RED; while(font_init()) //检查字库 { LCD_ShowString(60,50,200,16,16,"Font Error!");delay_ms(200); LCD_Fill(60,50,240,66,WHITE);//清除显示 } Show_Str(60,50,200,16,"战舰 STM32开发板",16,0); Show_Str(60,70,200,16,"图片显示程序",16,0); Show_Str(60,90,200,16,"KEY0:NEXT KEY2REV",16,0); Show_Str(60,110,200,16,"正点原子@ALIENTEK",16,0); Show_Str(60,130,200,16,"2012年9月19日",16,0); while(f_opendir(&picdir,"0:/PICTURE"))//打开图片文件夹 { Show_Str(60,150,240,16,"ICTURE文件夹错误!",16,0); delay_ms(200); LCD_Fill(60,150,240,156,WHITE); delay_ms(200);//清除显示 } totpicnum=pic_get_tnum("0:/PICTURE"); //得到总有效文件数 while(totpicnum==NULL)//图片文件为0 { Show_Str(60,150,240,16,"没有图片文件!",16,0); delay_ms(200); LCD_Fill(60,150,240,156,WHITE); delay_ms(200);//清除显示 } printf("图片总数为:%d\r\n",totpicnum); picfileinfo.lfsize=_MAX_LFN*2+1; //长文件名最大长度 picfileinfo.lfname=mymalloc(SRAMIN,picfileinfo.lfsize);//为长文件缓存区分配内存 pname=mymalloc(SRAMIN,picfileinfo.lfsize);//为带路径的文件名分配内存 picindextbl=mymalloc(SRAMIN,2*totpicnum); //申请2*totpicnum个字节的内存,用于存放图片索引 while(picfileinfo.lfname==NULL||pname==NULL||picindextbl==NULL) //内存分配出错 { Show_Str(60,150,240,16,"内存分配失败!",16,0); delay_ms(200); LCD_Fill(60,150,240,146,WHITE); delay_ms(200);//清除显示 } //记录索引 res=f_opendir(&picdir,"0:/PICTURE"); //打开目录 if(res==FR_OK) { curindex=0;//当前索引为0 while(1)//全部查询一遍 { temp=picdir.index; //记录当前index res=f_readdir(&picdir,&picfileinfo); //读取目录下的一个文件 if(res!=FR_OK||picfileinfo.fname[0]==0)break; //错误了/到末尾了,退出 fn=(u8*)(*picfileinfo.lfname?picfileinfo.lfname:picfileinfo.fname); res=f_typetell(fn); if((res&0XF0)==0X50)//取高四位,看看是不是图片文件 { picindextbl[curindex]=temp; curindex++;//记录索引 } } } Show_Str(60,150,240,16,"开始显示...",16,0); delay_ms(1500); piclib_init(); //初始化画图 curindex=0; //从0开始显示 res=f_opendir(&picdir,(const TCHAR*)"0:/PICTURE"); //打开目录 while(res==FR_OK)//打开成功 { dir_sdi(&picdir,picindextbl[curindex]); //改变当前目录索引 res=f_readdir(&picdir,&picfileinfo); //读取目录下的一个文件 if(res!=FR_OK||picfileinfo.fname[0]==0)break; //错误了/到末尾了,退出 fn=(u8*)(*picfileinfo.lfname?picfileinfo.lfname:picfileinfo.fname); strcpy((char*)pname,"0:/PICTURE/"); //复制路径(目录) strcat((char*)pname,(const char*)fn); //将文件名接在后面 LCD_Clear(BLACK); ai_load_picfile(pname,0,0,lcddev.width,lcddev.height);//显示图片 Show_Str(2,2,240,16,pname,16,1); //显示图片名字 t=0; while(1) { key=KEY_Scan(0); //扫描按键 if(t>250)key=KEY_RIGHT; //模拟一次按下右键 if(key==KEY_LEFT) //上一张 { if(curindex)curindex--; else curindex=totpicnum-1; break; }else if(key==KEY_RIGHT)//下一张 { curindex++; if(curindex>=totpicnum)curindex=0;//到末尾的时候,自动从头开始 break; }else if(key==KEY_UP) {pause=!pause; LED1=!pause;} //暂停的时候LED1亮. if(pause==0)t++; delay_ms(10); } res=0; } myfree(SRAMIN,picfileinfo.lfname); //释放内存 myfree(SRAMIN,pname); //释放内存 myfree(SRAMIN,picindextbl); //释放内存 } 此部分除了mian函数,还有一个pic_get_tnum的函数,用来得到path路径下,所有有效文件(图片文件)的个数。在mian函数里面我们通过索引(图片文件在PICTURE文件夹下的编号),来查找上一个/下一个图片文件,这里我们需要用到fatfs自带的一个函数:dir_sdi,来设置当前目录的索引(因为f_readdir只能沿着索引一直往下找,不能往上找),方便定位到任何一个文件。dir_sdi在FATFS下面被定义为static函数,所以我们必须在ff.c里面将该函数的static修饰词去掉,然后在ff.h里面添加该函数的申明,以便main函数使用。 其他部分就比较简单了,至此,整个图片显示实验的软件设计部分就结束了。该程序将实现浏览PICTURE文件夹下的所有图片,并显示其名字,每隔3s左右切换一幅图片。 47.4 下载验证 在代码编译成功之后,我们下载代码到ALIENTEK战舰STM32开发板上,可以看到LCD开始显示图片(假设SD卡及文件都准备好了),如图47.4.1所示: 图47.4.1 图片显示实验显示效果 按KEY0和KEY2可以快速切换到下一张或上一张,WK_UP按键可以暂停自动播放,同时DS1亮,指示处于暂停状态,再按一次WK_UP则继续播放。同时,由于我们的代码支持gif格式的图片显示(注意尺寸不能超过LCD屏幕尺寸),所以可以放一些gif图片到PICTURE文件夹,来看动画了。 另外需要注意,不是所有的jpg格式图片都可以正常显示,只有JFIF格式的jpg文件才能正常显示,对于EXIF格式的jpg文件,则不能直接显示,大家可以将EXIF格式的jpg文件用XP的画图打开,然后再保存一下,就将EXIF格式转为JFIF格式了,这样就可以正常解码显示了。 本章,同样可以通过USMART来测试该实验,将ai_load_picfile函数加入USMART控制(方法前面已经讲了很多次了),就可以通过串口调用该函数,在屏幕上任何区域显示任何你想要显示的图片了! |