TJpgDec简介
TJpgDec是一个为小型嵌入式系统高度优化的创建JPEG图像的解码软件。它工作时占用的内存非常低,以便它可以集成到微控芯片,如AVR, 8051, PIC, Z80, Cortex-M0等。不依赖于具体的硬件平台,使用ANSI-C编写,易于使用的操作模式,完全可重入的体系结构,非常小的内存占用(独立于图像尺寸的3KB SRAM。3.5-8.5KB的代码空间)。输出格式可以是缩放比例为1/1、1/2、1/4或1/8可选,像素格式可以是RGB888或RGB565。
TJpgDec组件是一个免费开放的软件,可用于教育、科研和商业开发。用户可以在包含TJpgDec的个人项目和商业产品中改写和重新发布该组件。
TJpgDec组件在用户的应用中仅需要调用2个API。
jd_prepare() - 准备解码JPEG图像
jd_prepare
分析JPEG数据并创建一个解码对象用于随后的解码过程。
jd_prepare
函数是JPEG解码过程的第一阶段。它接收用户传入的数据流(需要通过传入的回调函数操作数据流),分析JPEG图像和创建解码参数表。函数成功后,会话准备好在jd_decomp
函数解码JPEG图像。应用程序可以参考JPEG解码对象中存储的尺寸大小。这个信息将用于在后续的解码阶段配置输出设备和参数。
JRESULT jd_prepare (
JDEC* jdec, /* Pointer to blank decompression object */
UINT(*infunc)(JDEC*,BYTE*,UINT), /* Pointer to input function */
void* work, /* Pointer to work area */
UINT sz_work, /* Size of the work area */
void* device /* Device identifier for the session */
);
输入参数:
jdec
- 输出,一个空的JDEC结构体对象,初始化一个解码对象。需要用户先创建JPEC对象的内存,然后本函数在执行过程中会向其中填充有效信息,这个解码对象也将用于后续的解码操作。infunc
- 输入,指定一个为解码算法提供输入数据的回调函数。实际上,这个方法也是关联具体平台的,不仅仅是提供一个读数的方法,更确切是提供一个具体的数据流。这个infunc函数也是需要在具体平台适配过程中需要用户自行实现的。具体写法可以看原作者给的应用说明,也可以参见本文中的基于具体微控制器平台的实现。work
- 输入,指定一个内存块,给解码器内部运行算法的工作空间。sz_work
- 输入,指定work
参数指定工作空间的 size,以字节为单位。TJpgDec至多需要3092字节的工作区域,这依赖于JPEG图像的内置参数表。通常情况下是3092字节工作区域。device
- 输入,指定用户自定义的会话设备标识。它保存在解码对象jdec
的字段device
中。它可以用于I/O函数去识别当前会话。当I/O device固定在project或者不需要这个功能,设置为NULL并忽略它。device参数可以作为回调函数中访问绑定硬件的传参。
返回值:
JDR_OK
- 函数执行成功,且编码对象是有效的。JDR_INP
- 一个错误发生在input函数,由于硬件错误或者流终止。JDR_MEM1
- 工作空间不足解码这个JPEG图像。JDR_MEM2
- 工作空间不足解码这个JPEG图像。可能更小。JDR_PAR
- 参数错误。传入工作空间的指针为NULL。JDR_FMT1
- 数据格式错误。JPEG数据损坏。JDR_FMT2
- 格式正确,但不支持。也许是一个灰度图像。JDR_FMT3
- 不支持JPEG标准,也许是一个后续版本的JPEG图像。
jd_decomp() - 执行解码JPEG图像
jd_decomp()
函数解码JPEG图像并输出RGB
数据。
jd_decomp()
是JPEG解码过程的第二阶段。它进一步执行解码JPEG图像的过程,并通过用户定义的输出函数输出数据,但同时也继续使用在jd_prepare()
传入的输入数据流的函数。在它之后,解码对象将不在有效。
在解码时指定的比例因子,它将JPEG图像按1/2、1/4或1/8比例缩放尺寸。例如,当解码一个1024x768大小JPEG图像在1/4比例,它将输出256x192大小。相比不缩放,1/2和1/4的缩放由于求均值,解码速度略有下降。但是1/8缩放相比不缩放是2-3倍的速度输出,因为每个块IDCT和求均值可以跳过,这一特点适合创建缩略图。
JRESULT jd_decomp (
JDEC* jdec, /* Pointer to valid decompressor object */
UINT(*outfunc)(JDEC*,void*,JRECT*), /* Pointer to output function */
BYTE scale /* Scaling factor */
);
输入参数:
jdec
- 输入,指定有效的解码对象。其实就是之前在jd_prepare()函数中准备好的JDEC对象。outfunc
- 输入,指定用户定义的JPEG解码过程输出数据流的回调函数。jd_decomp()
调用这个函数去输出解码JPEG图像的RGB形式数据流。具体写法可以看原作者给的应用说明,也可以参见本文中的基于具体微控制器平台的实现。scale
- 输入,指定输出缩放值N。输出图像的缩小比例为1/2^N
(N = 0 to 3)。当不使用缩放功能时(JD_USE_SCALE == 0),可以指定为0。
返回值
JDR_OK
- 函数执行成功。JDR_INTR
- 解码过程在输出函数中断。JDR_INP
- 一个错误发生在input函数,由于硬件错误或者流终止。JDR_PAR
- 参数错误。给定的缩放值无效。JDR_FMT1
- 数据格式错误。JPEG数据损坏。
tjpgdcnf.h - 配置文件
早期版本的TjpgDec源码中并没有tjpgdcnf.h
文件。我试着找了一下tjpgdec项目的changelog,没有直接找到关于版本更新的详情内容,但是遍历了到目前为止tjpgd所有发布版本的软件包,确定了tjpgdcnf.h首次出现的版本是2021年5月8日发布的R0.02版本。在这个文件中,更细化地提取了一些对TJpgDec软件配置和裁剪的一些选项,在当前最新的R0.03版本中源码如下(原作者非常贴心地注释了可用的选项和对应的解释):
/*----------------------------------------------*/
/* TJpgDec System Configurations R0.03 */
/*----------------------------------------------*/
#define JD_SZBUF 512
/* 指定在输入数据流中每次读取的字节数,512、1024、2048等等均可。*/
#define JD_FORMAT 0
/* Specifies output pixel format.
/ 0: RGB888 (24-bit/pix)
/ 1: RGB565 (16-bit/pix)
/ 2: Grayscale (8-bit/pix)
*/
#define JD_USE_SCALE 1
/* Switches output descaling feature.
/ 0: Disable
/ 1: Enable
*/
#define JD_TBLCLIP 1
/* Use table conversion for saturation arithmetic. A bit faster, but increases 1 KB of code size.
/ 0: Disable
/ 1: Enable
*/
#define JD_FASTDECODE 0
/* Optimization level
/ 0: Basic optimization. Suitable for 8/16-bit MCUs.
/ 1: + 32-bit barrel shifter. Suitable for 32-bit MCUs.
/ 2: + Table conversion for huffman decoding (wants 6 < < HUFF_BIT bytes of RAM)
*/
针对使用ARM Cortex-MC1处理器内核和FSMC驱动的16位并口屏幕的MM32F5微控制器,这里需要改写:
#define JD_FORMAT 1 /* 1: RGB565 (16-bit/pix) */
#define JD_FASTDECODE 1 /* 1: + 32-bit barrel shifter. Suitable for 32-bit MCUs. */
注意,这里似乎还藏了一个彩蛋。同时支持RGB888、RGB565和灰度图像,这意味着TJpgDec软件内部的源码内部包含了原始RGB888转RGB565和灰度图像的算式,这个在纯输出的GUI应用开发中讲会用到,届时可以直接复制验证过的代码片段,而不用我们再自行编写调试啦。
关于TJpgDec的软件许可证
这是一份包含在源代码TJpgDec中的许可声明。
/*----------------------------------------------------------------------------/
/ TJpgDec - Tiny JPEG Decompressor R0.xx (C)ChaN, 20xx
/-----------------------------------------------------------------------------/
/ The TJpgDec is a generic JPEG decompressor module for tiny embedded systems.
/ This is a free software that opened for education, research and commercial
/ developments under license policy of following terms.
/
/ Copyright (C) 20xx, ChaN, all right reserved.
/
/ * The TJpgDec module is a free software and there is NO WARRANTY.
/ * No restriction on use. You can use, modify and redistribute it for
/ personal, non-profit or commercial products UNDER YOUR RESPONSIBILITY.
/ * Redistributions of source code must retain the above copyright notice.
/
/----------------------------------------------------------------------------*/
TJpgDec许可是BSD风格的,但存在一些差异。因为TJpgDec是嵌入式项目,对以二进制形式的分发,如嵌入式代码,hex文件或二进制库,未指定以增加其可用性。分发的文档不强制包含关于tjpgdec及其授权文件。TJpgDec是基于GNU GPL兼容的项目。当有任何修改下重新分发,许可证也可以改为GNU GPL或BSD许可证。
应用接口解析
TJpgDec组件的移植过程仅需要实现两个函数,输入数据和输出数据。但从严格意义上讲,这两个函数都只是以约定的方式将数据流传入到TJpgDec组件,或者从TJpgDec组件中传出到指定存储空间,因为完全是内存到内存的操作,不涉及到任何与具体硬件平台相关绑定关系,所以也算不上移植。甚至在原作者的用户使用文档中,也是以应用笔记(TJpgDec Application Note)作为文档的名字,而不是移植指南。
原作者在《TJpgDec Module Application Note》中讲述了在应用中使用TJpgDec组件的操作步骤。建议用户先试着构建和运行原作者提供的示例程序。
解码会话分为两个阶段。第一阶段是分析JPEG图像,第二阶段是解码。
- 初始化输入流。(例如打开一个文件)
- 分配JPEG解码对象和工作区域。
- 调用
jd_prepare()
指定输入数据流,并执行分析和准备压缩的JPEG图像。 - 使用解码对象中的图像信息初始化输出设备。
- 调用
jd_decomp()
指定输出数据流,解码JPEG图像并输出。
图x 在应用中使用TJpgDec组件的调用关系图
这是原作者提供的一段参考源码,描述了如何使用TJpgDec模块。
/*------------------------------------------------*/
/* TJpgDec Quick Evaluation Program for PCs */
/*------------------------------------------------*/
#include < stdio.h >
#include < string.h >
#include "tjpgd.h"
/* 用户定义设备标识 */
typedefstruct {
FILE *fp; /* 用于输入函数的文件指针 */
BYTE *fbuf; /* 用于输出函数的帧缓冲区的指针 */
UINT wfbuf; /* 帧缓冲区的图像宽度[像素] */
} IODEV;
/*------------------------------*/
/* 用户定义input funciton */
/*------------------------------*/
UINT in_func (JDEC* jd, BYTE* buff, UINT nbyte)
{
IODEV *dev = (IODEV*)jd- >device; /* Device identifier for the session (5th argument of jd_prepare function) */
if (buff) {
/* 从输入流读取一字节 */
return (UINT)fread(buff, 1, nbyte, dev- >fp);
} else {
/* 从输入流移除一字节 */
return fseek(dev- >fp, nbyte, SEEK_CUR) ? 0 : nbyte;
}
}
/*------------------------------*/
/* 用户定义output funciton */
/*------------------------------*/
UINT out_func (JDEC* jd, void* bitmap, JRECT* rect)
{
IODEV *dev = (IODEV*)jd- >device;
BYTE *src, *dst;
UINT y, bws, bwd;
/* 输出进度 */
if (rect- >left == 0) {
printf("r%lu%%", (rect- >top < < jd- >scale) * 100UL / jd- >height);
}
/* 拷贝解码的RGB矩形范围到帧缓冲区(假设RGB888配置) */
src = (BYTE*)bitmap;
dst = dev- >fbuf + 3 * (rect- >top * dev- >wfbuf + rect- >left); /* 目标矩形的左上 */
bws = 3 * (rect- >right - rect- >left + 1); /* 源矩形的宽度[字节] */
bwd = 3 * dev- >wfbuf; /* 帧缓冲区宽度[字节] */
for (y = rect- >top; y <= rect- >bottom; y++) {
memcpy(dst, src, bws); /* 拷贝一行 */
src += bws; dst += bwd; /* 定位下一行 */
}
return1; /* 继续解码 */
}
/*------------------------------*/
/* 主程序 */
/*------------------------------*/
int main (int argc, char* argv[])
{
void *work; /* 指向解码工作区域 */
JDEC jdec; /* 解码对象 */
JRESULT res; /* TJpgDec API的返回值 */
IODEV devid; /* 用户定义设备标识 */
/* 打开一个JPEG文件 */
if (argc < 2) return-1;
devid.fp = fopen(argv[1], "rb");
if (!devid.fp) return-1;
/* 分配一个用于TJpgDec的工作区域 */
work = malloc(3500);
/* 准备解码 */
res = jd_prepare(&jdec, in_func, work, 3500, &devid);
if (res == JDR_OK) {
/* 准备好解码。图像信息有效 */
printf("Image dimensions: %u by %u. %u bytes used.n", jdec.width, jdec.height, 3100 - jdec.sz_pool);
devid.fbuf = malloc(3 * jdec.width * jdec.height); /* 输出图像的帧缓冲区(假设RGB888配置) */
devid.wfbuf = jdec.width;
res = jd_decomp(&jdec, out_func, 0); /* 开始1/1缩放解码 */
if (res == JDR_OK) {
/* 解码成功。你在这里已经解码图像到帧缓冲区 */
printf("rOK n");
} else {
printf("Failed to decompress: rc=%dn", res);
}
free(devid.fbuf); /* 释放帧缓冲区 */
} else {
printf("Failed to prepare: rc=%dn", res);
}
free(work); /* 释放工作区域 */
fclose(devid.fp); /* 关闭JPEG文件 */
return res;
}
这里详细说明两个回调函数的写法。
in_func() - 输入数据流回调函数
用户需要在输入数据流的回调函数in_func()
中读取JPEG数据存入传参指针buff
中。在jd_prepare()函数中传入数据流到TJpgDec模块。
UINT in_func (
JDEC* jdec, /* Pointer to the decompression object */
BYTE* buff, /* Pointer to buffer to store the read data */
UINT ndata /* Number of bytes to read */
);
输入参数:
jdec
- 输入,当前服务的jdec对象的handler。通过这个handler可以访问到当前服务的jdec对象的所有资源。buff
- 输出,一块内存区,从介质中读取指定数量的字节数据后,存放到这块内存区,交给调用者。但如果buff
的值为NULL
,就表示跳过ndata
参数指定数量的数据。ndata
- 输入,调用者希望从输入数据流中读到的字节数量,即buff
的大小。或者当buff
的值为NULL
时,此处参数指定为需要在输入数据流中跳过读取的字节数量。
实际上,这里还有一个隐形的参数,即出现在tjpgdcnf.h文件中的JD_SZBUF
,它约定的应该是每次从输入流读取的最大字节数量,即ndata
的最大值。
返回值:
- 返回实际读取或移除的字节数。若返回0,
jd_prepare()
和jd_decomp()
函数将终止并返回JDR_INP
。
out_func() - 输出数据流回调函数
用户可指定解码出来的像素输出到具体的存储区,这个存储区可以是一块内存,或者映射到显存的地址空间。用户需要在out_func()
函数内部,在rect
参数执行的矩形区域中填充bitmap
参数指定的像素信息。
UINT out_func (
JDEC* jdec, /* Pointer to the decompression object */
void* bitmap, /* RGB bitmap to be output */
JRECT* rect /* Rectangular region to output */
);
输入参数:
jdec
- 输入,当前服务的jdec对象的handler。通过这个handler可以访问到当前服务的jdec对象的所有资源。bitmap
- 像素数据流。是按照tjpgdcnf.h
文件中JD_FORMAT
选项指定的格式组织,可以是3字节表示的一个像素的RGB888格式,也可以是2字节表示一个像素的RGB565格式等。第一个像素是rect指定矩形区域的左上角,从左到右,从上到下,最后一个像素是右下角的位置。rect
- 输入,执行显示像素区域的矩形。JRECT
类型的结构体中,有Left
、Right
、Top
、Bottom
四个字段,指示当前解码输出的矩形区域。实际上,这个矩形的大小从1x1到16x16不等,取决于图像的裁剪、缩放和采样因子(JPEG信息)。
返回值:
- 通常返回1,以便TJpgDec继续解码过程。当它返回0,
jd_decomp
函数终止并返回JDR_INTR
。
输入数据流可能时常发生变化,因为要读取不同的jpg图像文件。输出数据流也是在应用中根据需要变化的,只不过因为在大多数微控制器应用中,显示设备通常只有一个,所以统一输出到这个显示设备对应的显存中。有一些对于输出体验有要求的场景,需要输出到特定的存储空间,为了进行二次渲染,或者多缓冲区的应用,此时就需要在应用根据程序执行的情况动态切换输出了区域了。
关于工作区和帧缓冲区
在应用程序中调用jd_prepare()函数时,需要为TJpgDec指定一块工作区,作为TJpgDec在内部运行解码算法的临时空间。TJpgDec至少需要3100字节用于JPEG图像,这取决于解码JPEG图像使用怎样的参数。3100字节是在默认输入缓存(JD_SZBUF == 512)下的最大内存需求,并随JD_SZBUF
和JD_FASTDECODE
的配置值变化。JD_SZBUF
定义每次从输入流中读取多少字节。TJpgDec
对齐每个读请求缓冲区大小,512, 1024, 2048... 字节是从存储设备读取的理想大小。
在样例代码中,原作者使用了动态分配的内存作为工作区,这对于拥有海量存储资源和完善内存管理机制的PC环境是合适的。但在资源受限的嵌入式系统平台上,使用静态内存会是更稳妥的选择。
另外,样例代码中使用了帧缓冲区(devid结构体变量中的fbuf指定内存区,wfbuf指定宽度),在应用程序和out_func回调函数之间传递像素数据,但实际看起来有点莫名其妙。此处了解到以矩形方式传送像素矩阵的模式之后,用户也可以自行简化代码。
在MM32F5微控制器上应用
在MM32F5微控制器上适配TJpgDec时,我使用了FatFs文件系统作为JPEG图像文件的来源,使用静态内存作为工作区,简化了对“帧缓冲区”的使用,并使用LCD模块作为输出设备。最终实现在微控制器系统中启用JPEG图像解码器的功能。
在包含TJpgDec组件的plus-f5270_image_fatfs_tjpgdec_basic_mdk
工程中,我将打开图像文件并解码的过程封装成bool app_fs_display_jpg_file(char * filepath)
函数,如此,在主循环中遍历到文件系统中的jpg文件后,可以直接使用其文件路径打开文件并显示像素信息到LCD屏上。这个函数中,就使用到了静态内存分配的工具区域app_tjpgdec_work_buff
。从代码中可以看出,代码的内容被简化了不少。
#define APP_TJPGDEC_WORK_BUFF_SIZE 3500
uint8_t app_tjpgdec_work_buff[APP_TJPGDEC_WORK_BUFF_SIZE];
/* display a jpg file with its full filepath. */
bool app_fs_display_jpg_file(char * filepath)
{
JRESULT res; /* Result code of TJpgDec API */
JDEC jdec; /* Decompression object */
//void *work; /* Pointer to the work area */
//size_t sz_work = 3500; /* Size of work area */
IODEV devid; /* Session identifier */
FRESULT fres;
/* Initialize input stream */
devid.fp = &app_fs_file;
fres = f_open(devid.fp, filepath, FA_READ);
if (fres != FR_OK)
{
return-1;
}
/* Prepare to decompress */
//work = (void*)malloc(sz_work);
//res = jd_prepare(&jdec, in_func, work, sz_work, &devid);
res = jd_prepare(&jdec, in_func, app_tjpgdec_work_buff, APP_TJPGDEC_WORK_BUFF_SIZE, &devid);
if (res == JDR_OK)
{
/* It is ready to dcompress and image info is available here */
//printf("Image size is %u x %u.n%u bytes of work ares is used.n", jdec.width, jdec.height, sz_work - jdec.sz_pool);
/* Initialize output device */
//devid.fbuf = (uint8_t*)malloc(N_BPP * jdec.width * jdec.height); /* Create frame buffer for output image */
//devid.wfbuf = jdec.width;
res = jd_decomp(&jdec, out_func, 0); /* Start to decompress with 1/1 scaling */
if (res == JDR_OK)
{
/* Decompression succeeded. You have the decompressed image in the frame buffer here. */
printf("rDecompression succeeded.n");
} else {
printf("jd_decomp() failed (rc=%d)n", res);
}
//free(devid.fbuf); /* Discard frame buffer */
}
else
{
printf("jd_prepare() failed (rc=%d)n", res);
}
//free(work); /* Discard work area */
f_close(devid.fp); /* Close the JPEG file */
returntrue;
}
同时,在实现输入输出数据流的回调函数时,也有一些考究。
在实现输入数据流的回调函数的过程中,由于使用了FatFs文件系统,其中很多类POSIX的接口同样例代码的行为并不完全一致,需要做一些转接的工作。特别FatFs文件系统中的f_lseek()
函数是从文件开始计算偏移,而不是像通用的f_seek()
函数从当前位置算偏移,因此需要使用f_tell()
函数做一个适配。
size_t in_func ( /* Returns number of bytes read (zero on error) */
JDEC* jd, /* Decompression object */
uint8_t* buff, /* Pointer to the read buffer (null to remove data) */
size_t nbyte /* Number of bytes to read/remove */
)
{
IODEV *dev = (IODEV*)jd- >device; /* Session identifier (5th argument of jd_prepare function) */
UINT br;
if (buff) /* Read data from imput stream */
{
//return fread(dev- >fp, buff, 1, nbyte, dev- >fp);
return (FR_OK == f_read(dev- >fp, buff, nbyte, &br)) ? br : 0;
}
else/* Remove data from input stream */
{
//return f_seek(dev- >fp, nbyte, SEEK_CUR) ? 0 : nbyte;
return (FR_OK == f_lseek(dev- >fp, f_tell(dev- >fp) + nbyte)) ? nbyte : 0;
}
}
在实现输出数据流的回调函数的过程中,可以直接对接到开发板上LCD屏的显存中,直接显示像素。
int out_func ( /* Returns 1 to continue, 0 to abort */
JDEC* jd, /* Decompression object */
void* bitmap, /* Bitmap data to be output */
JRECT* rect /* Rectangular region of output image */
)
{
/* Progress indicator */
if (rect- >left == 0)
{
printf("r%lu%%", (rect- >top < < jd- >scale) * 100UL / jd- >height);
}
/* 在LCD屏幕上显示图像信息. */
LCD_FillWindow(rect- >left, rect- >top, rect- >right, rect- >bottom, (uint16_t *)bitmap);
return1; /* Continue to decompress */
}
在Keil工程中编译可执行文件,下载到plus-f5270开发板上,实物演示如图x所示。
图x 在plus-f5270开发板上运行TJpgDec
Code Size (-0):
==============================================================================
Total RO Size (Code + RO Data) 45096 ( 44.04kB)
Total RW Size (RW Data + ZI Data) 13032 ( 12.73kB)
Total ROM Size (Code + RO Data + RW Data) 45104 ( 44.05kB)
==============================================================================
其中,SRAM占用量是比较少的,总共13032字节,其中系统栈占用4KB,系统堆占用4KB,工作空间占用3500字节,还带了个FatFs文件系统,这已经算是非常经济的用量了。
一点思考
从实际演示效果来看,移植的样例工程能够完成JPEG解码功能,验证了TJpgDec组件能够正常工作,这是非常不错的。但由于从SD卡读数、解码、刷屏这些个步骤都是一小段一小段执行的,因此实际显示图像刷屏的速度不是很快(估计受读取SD卡过程的影响较大)。一种改进的策略,是将解码得到的图像片段搜集到一块大内存中,然后集中刷屏,这样视觉效果会好很多。解码完之后纯刷屏的速度很快,解码之前虽然有等待,但当前屏幕上还在放映之前解码的图片,参观者不会感到无趣。
plus-f5270开发板上的MM32F5270微控制器仅有128KB的SRAM,不够存放一整张图(480x320),但是可以存放1/4个屏幕的图片数据(75KB),可以考虑将屏幕分成四块小屏来用,每次刷一块。
-
微控制器
+关注
关注
48文章
7497浏览量
151096 -
解码器
+关注
关注
9文章
1132浏览量
40687 -
RGB
+关注
关注
4文章
798浏览量
58395 -
回调函数
+关注
关注
0文章
87浏览量
11543 -
SRAM存储器
+关注
关注
0文章
88浏览量
13281
发布评论请先 登录
相关推荐
评论