UEFI开发探索29 – 图像显示(JPEG)

(请保留->发布地址: http://yiiyee.cn/blog/author/luobing/ )

最近大部分时间都在研究怎么调试了。原计划的图像显示系列,落后不少进度。

之前介绍的BMP和PCX格式,都是无损压缩格式。今天想研究下有损压缩的明星—Jpeg格式。

本来是准备了四种格式的研究:BMP、PCX、JPEG和ICO。因为时间原因,ICO就不再继续编程了。如果未来有需要,到时再把它补上吧。

这篇博客是图像显示系列的最后一篇了。

1 图像开源库

JPEG的格式相对比较复杂,涉及到很多数学运算。即便看懂格式以及算法,重新完全自己编写,要在这几天完成,也不是很现实。

最好的做法当然是借助开源库。 最近因为某个案子,我一直在研究CxImage。各种格式都能支持,并且代码都开源了,非常适合学习。

图1 CxImage演示

不过有个问题,Cximage是C++写的,用了各种的Class。如果想移植到UEFI下来,可能要费很多功夫。虽然也可以让UEFI支持C++的方式来减少工作量(《UEFI原理与编程》中介绍过方法),也不是短短几天能搞定的。

最好还是用C语言写的Jpeg库,比较方便移植。

在github上找到了云风的ffjpeg,使用mingw32编译的库,非常合适。作者也是我天朝程序员一枚,在此表示感谢^_^。下载地址为:
https://github.com/rockcarry/ffjpeg

2 JPEG格式简介

网上有很多关于这方面的文章,可以找来看看。也可以直接找它的标准文档《The JPEG Still Picture Compression Standard》仔细读读,我敢保证,会很头疼的。

我们常说的JPEG图像,实际上是JFIF(JPEG File Interchange Format),版本号为1.02。这是由Eric  Hamilton与于1992年9月提出的,目前使用最为广泛。此外还有TIFF JPEG等格式,这种比较复杂。比如专利申请的时候,那些审查意见通知书,都是用tif格式发过来的。日常使用中我只见过专利局使用,其他大部分都是JFIF格式。

JPEG采用大端存储,与平常x86常用的小端存储不同,读取数据的时候要注意这点。

JPEG的压缩过程,大概可以总结为:色彩空间转换->缩减取样->离散余弦变换(DCT)->量化->熵编码。

JPEG的图像并非采用RGB或者CMYK模式,而是采用YCbCr模式。这是因为,人眼对亮度的敏感度要高于对色彩的敏感度。基于此特性,在压缩图像时,将亮度和颜色分开处理。亮度部分不做太多改变,对颜色进行压缩处理,这样就算图像损失了部分细节,也不太容易捕捉到。

RGB与YCbCr模式之间的转换关系如下:

R = Y+1.042(Cr-128)
G = Y-0.3414(Cb-128)-0.71414(Cr-128)
B = Y+1.772(Cb-128)

Y =    0.299R + 0.587G + 0.114 B
Cb = – 0.1687R – 0.3313G + 0.5B + 128
Cr =    0.5R – 0.4187G – 0.0813B + 128

其余步骤的转换,比如DCT等,都有相应的公式。这里不再列出讨论了,移植代码过程中只需要知道有这个过程,了解对应的函数即可。

编程时需要重点关注的是其文件结构。JPEG的每个标记都是由2个字节组成,第一个字节固定为0xFF。每个标记前还可以添加数目不限的0xFF填充字节。下面是常用的八个标记:

1) SOI  0xD8 图像开始
2) APP0  0xE0 JFIF应用资料块
3) APPn  0xE1 – 0xEF 其他的应用资料块(n, 1~15)
4)  DQT  0xDB 量化表
5) SOF0  0xC0 帧开始
6) DHT  0xC4 霍夫曼(Huffman)表
7) SOS  0xDA 扫描线开始
8) EOI  0xD9 图像结束

详细的信息如下:

图2 JPEG文件格式

这么短篇幅的介绍不可能把文件格式说清楚,但足够让我们移植代码了。

3 使用main()入口函数

在ffjpeg的代码中,大量使用了C标准库函数。为了移植,UEFI代码也必须使用C的标准库函数,因此只能使用main()作为入口。

把显示PCX图像的例子稍微修改下就可以了,步骤如下:

1) Luo2.c中,将INTN EFIAPI ShellAppMain(IN UINTN Argc, IN CHAR16 **Argv)改为int main(IN int Argc, IN char **Argv);
2) 在Luo2.inf中[Packages]段添加StdLib/StdLib.dec;在[LibraryClasses]段添加LibC LibStdio;
3) 在Common.h中添加需要的头文件,包括stdio.h、string.h、stdlib.h和wchar.h。

编译会发现提示错误,主要都是因char型字符串与Unicode字符串不一致导致的。 添加库文件<Library/BaseLib.h>,其中提供了将char型字符串转换为Unicode字符串的函数,要不然就自己写一个函数实现吧。

在Option ROM的开发中,只能用UefiMain()作为入口,以上这些库函数都不能使用,需要类似功能时只能自己写。这次是为了简化编程,否则大部分的代码都得重写,比较浪费时间。

对Luo2.c编译错误的地方,使用新的函数转换一下,兼容之前的逻辑即可。这样就把main()作为入口的框架搭建起来了,能比较方便地移植jpeg开源代码。

4 代码移植与演示

代码移植的工作量其实不大,主要将Jpeg解压缩需要的代码移植过来即可。

ffjpeg中提供了演示的代码,将jpeg图像转换为bmp图像。直接跟踪代码,把需要的函数一个个拷贝过来即可。函数内部变量全部都不用动,只是类型为BYTE、WORD和DWORD的变量,都改为UINT8、UINT16、UINT32就可以了。

图3 代码移植

代码移植到之前图像处理的文件Pictures.c中,相关的数据结构定义在Pictures.h中。运行效果如下:

图4 运行效果(显示JPEG图像)

5 调试小技巧

这次其实没有太多代码的编写,主要都是在做移植的工作,测试用的代码不过40来行,移植和编程总共花了我2个小时左右。

虽然完成很快,可惜代码没按照我想象的工作。

剩下的两天时间,我都在调试。正好使用了上一章节的windbg的调试环境,能够很方便的去看中间过程。

UDK的开发环境中,编译选项是打开了优化开关的。所以,在windbg下去看函数内部变量,其实…不一定能看到。虽然可以进行源代码调试,但会发现C语言的代码和汇编代码怎么也对应不起来,有时会让人比较迷惑,还不如直接看汇编代码。

这些锅都应该由编译器的优化来背。 解决方法很简单,在/conf中,修改文件tools_def.txt中相应的编译参数就可以了,把优化相关的都关掉:

图5 关闭编译优化选项

编译之后的APP,从原来的77K增大到了129K。按照上一篇博客的方法搭建调试环境,可以发现所有局部变量都能清晰的观察了:

图6 详细的变量观察

至于困扰我两天的BUG,在经过各种探索之后,我发现:不优化的程序,存储的数据总是会有两个字节的偏差;优化后的程序,内部数据乱得一塌糊涂,连图像大小都错了。一个个函数排查,也没有发现异常的地方。

这很像是数据对齐引起的问题,看下头文件,果然忘记了加限定了。加上语句#pragma pack(1),问题解决了。

这时间浪费得真有点冤。

百度云链接:https://pan.baidu.com/s/1gccSosw8_UAGTI5gZPnLCA
提取码:dx23
文件在 19 Display JPEG 下

2,642 total views, 1 views today

发表评论

电子邮件地址不会被公开。