libpng 移植到 iMX RT1052 --- 压缩

前面的文章翻译了libpng的手册,机器翻译,很多地方还是很难懂。

要用好一个别人的代码,还是要上手实际操作才行。

根据前面的手册翻译(Libpng源码的使用)可以知道,libpng是一个成熟的开源库,提供了成熟的用户接口,在移植时一般不需要对源码本身进行太多修改。

源码

下载libpng和zlib的源码,解压拷贝到工程目录下,并修改文件夹名字,去掉版本信息,注意zlib文件夹名称不能随意修改,因为libpng中引用了zlib的头文件,需要保证路径正确

打开RT1052例程的keil 工程,设置头文件路径,

进入libpng/scripts目录,将 pnglibconf.h.prebuilt 文件拷贝到 libpng目录下,并修改文件名为  pnglibconf.h 这个文件就是libpng源码的配置文件,

将libpng和zlib的源码添加到工程中,开源的源码用户接口的c 文件一般都在最外层,直接添加.c文件就可以了,类似下图,zlib中有些用不到的源码,可以不添加

引用

如果是linux环境,要使用libpng其实非常简单,只要在自己的代码中引用  png.h 头文件即可,如果用到了zlib的内容,引用zlib.h 即可,开源软件这点做的特别方便

include 这两个头文件后就可以使用libpng和zlib了。

修改

因为是移植到RT1052上,可能需要输出很多调试信息来定位问题,需要对这两个开源库的打印函数做一些修改。

这两个库本来是为linux系统编写的,里边使用了很多 fprintf() 函数来打印调试、错误、警告信息,RT1052不能直接使用。可以通过修改pngdebug.h文件来实现。

将文件中的调试接口

#ifndef png_debug
#  define png_debug(l, m) 
#endif
#ifndef png_debug1
#  define png_debug1(l, m, p1)  
#endif
#ifndef png_debug2
#  define png_debug2(l, m, p1, p2) 
#endif

替换为

#ifndef png_debug
#  define png_debug(l, m) \
      do { \
      int num_tabs=l; \
      char format[256]; \
      snprintf(format,256,"%s%s%s",(num_tabs==1 ? "\t" : \
        (num_tabs==2 ? "\t\t":(num_tabs>2 ? "\t\t\t":""))), \
        m,PNG_STRING_NEWLINE); \
      printf(format); \
      } while (0) /*((void)0)*/
#endif
#ifndef png_debug1
#  define png_debug1(l, m, p1)  \
      do { \
      int num_tabs=l; \
      char format[256]; \
      snprintf(format,256,"%s%s%s",(num_tabs==1 ? "\t" : \
        (num_tabs==2 ? "\t\t":(num_tabs>2 ? "\t\t\t":""))), \
        m,PNG_STRING_NEWLINE); \
      printf(format,p1); \
      } while (0)/*((void)0)*/
#endif
#ifndef png_debug2
#  define png_debug2(l, m, p1, p2)  \
      do { \
      int num_tabs=l; \
      char format[256]; \
      snprintf(format,256,"%s%s%s",(num_tabs==1 ? "\t" : \
        (num_tabs==2 ? "\t\t":(num_tabs>2 ? "\t\t\t":""))), \
        m,PNG_STRING_NEWLINE); \
      printf(format,p1,p2); \
      } while (0)/* ((void)0) */
#endif

libpng本来是规定了3个打印级别,这里为了方便,没有使用  PNG_DEBUG 宏定义,直接修改了png_debug ,png_debug1,png_debug2的内容,其实就是代码里的fprintf()不能用串口输出,改为printf(),用串口输出打印。

zlib库的情况差不多,将  zutil.h 中的调试接口修改一下,方便在RT1052上调试

/* Diagnostic functions */
#ifdef ZLIB_DEBUG
#  include <stdio.h>
   extern int ZLIB_INTERNAL z_verbose;
   extern void ZLIB_INTERNAL z_error OF((char *m));
#  define Assert(cond,msg) {if(!(cond)) z_error(msg);}    // fprintf
#  define Trace(x) {if (z_verbose>=0) fprintf x ;}
#  define Tracev(x) {if (z_verbose>0) fprintf x ;}
#  define Tracevv(x) {if (z_verbose>1) fprintf x ;}
#  define Tracec(c,x) {if (z_verbose>0 && (c)) fprintf x ;}
#  define Tracecv(c,x) {if (z_verbose>1 && (c)) fprintf x ;}
#else
	extern int ZLIB_INTERNAL z_verbose;
#  define Assert(cond,msg)
#  define Trace(x)			{if (z_verbose >= 0) printf x ;} 
#  define Tracev(x)			{if (z_verbose > 0) printf x ;} 
#  define Tracevv(x)		{if (z_verbose > 1) printf x ;}
#  define Tracec(c,x)		{if (z_verbose > 0 && (c)) printf x ;} 
#  define Tracecv(c,x)		{if (z_verbose > 1 && (c)) printf x ;} 
#endif

修改 zutil.c 中的 z_error函数

#ifdef ZLIB_DEBUG
#include <stdlib.h>
#  ifndef verbose
#    define verbose 0
#  endif
int ZLIB_INTERNAL z_verbose = verbose;

void ZLIB_INTERNAL z_error (m)
    char *m;
{
//    fprintf(stderr, "%s\n", m);
	printf( "%s\n", m);
    exit(1);
}
#endif
#include <stdlib.h>
#  ifndef verbose
#    define verbose 0
#  endif
int ZLIB_INTERNAL z_verbose = verbose;

void ZLIB_INTERNAL z_error (m)
    char *m;
{
//    fprintf(stderr, "%s\n", m);
	printf( "%s\n", m);
//    exit(1);
}

在 zconf.h 中添加下面三个宏定义

#define DZ_PREFIX 
#define Z_PREFIX 
#define Z_SOLO 

在 trees.c 中增加 引用一个头文件


#include "deflate.h"
#  include <ctype.h>

剩下的就是使用查找替换功能,把打印函数全部替换就可以了,类似下面这样

 

 

 

设置

移植到RT1052上还有一个头疼的问题要解决:libpng的例子pngtest.c默认使用FILE文件操作 ,使用 png_init_io()函数绑定文件和png结构体指针即可,使用了标准的fopen,fread,fwrite函数,这些函数在RT1052上还无法使用,我没有重定义文件操作函数。幸好libpng提供了另一种读写方式,用户可以自定义png读写函数,可以直接在内存中操作。只需要使用提供png_set_write_fn()和png_set_read_fn() 函数的修改png读写的方法即可。他们的关系如下

因为没有使用标准的文件系统,对读写的控制只能自己来实现,比如我的例程只是把图片写入内存,那我就可以在内存中定义一段存储空间,在定义一个标志,用于表示当前读写的位置。

//定义一个类似文件的结构体 用于在内存中存储处理后的 png文件
typedef struct  memImage
{
	//一个静态存储区 用于存储图像文件
	char png [biHeight*biWidth*3];  //定义足够大空间  png 空间肯定会小于RGB像素空间
	//当前写入的数据位置
	unsigned int png_cnt;
	
} memImage;
//声明一个文件实体,用来存储压缩后的png图片数据
static memImage my_image;

我的write_function函数进行的操作就是将传入的数据写入静态存储区中,write_function函数名称可以任意定,但是参数列表必须按照libpng规定的,我的操作如下:

//自定义写入函数 ,用此函数替换标准文件写入函数
static void PNGCBAPI write_function(png_structp pp, png_bytep data, size_t size)
{
    // png_get_io_ptr()
    memImage * mem = (memImage *) png_get_io_ptr(pp);
    memcpy((mem->png + mem->png_cnt) , data, size);
    mem->png_cnt += size;
}

png_structp pp 是png的结构体指针,里边包含了png文件的所有内容,

png_bytep data 是要写入的数据,具体是什么可以不用关心,libpng在定义的时候自己确定,

size_t size 是要写入的 data数据大小,单位是字节。

看write_function函数内,其实就是将data写入到pp内,我这个函数写的比较简单,可以参照libpng库提供的其他例子(pngstest.c  ,pngimage.c)增加动态内存管理,错误判断等功能,使程序更可靠。

需要注意的是传入的png结构体 pp 包含了好多其他元素,我们只需要把数据写入其数据区即可,libpng提供了png_get_io_ptr(pp) 函数,返回结构体的数据区。

这里也可以看出,libpng对内部封装比较规整,不建议直接去修改源码的其他内容,我们能访问的内容是通过接口函数访问。感兴趣的可以自己深入研究。

以上准备工作基本上就做完了,下面开始进入正式的压缩操作流程。

初始化

首先定义两个png结构的指针

png_structp write_ptr;
png_infop write_info_ptr;

我们对png的操作都是通过这两个结构体实现,这两个结构体包含了png图像的所有信息,因为定义的是两个指针,不能直接使用,需要进行初始化,下面贴出我的初始化函数,其实就是调用了一些列libpng的函数。

//在内存中操作图像文件
// 为libpng 设置用户IO函数 用于在内存中读写文件
void set_png_user_IO(voidp write_io_ptr)
{
	error_parameters.file_name = "test.png";
	//初始化 write_ptr
	write_ptr =
       png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
	if(write_ptr == NULL)
      {
         printf( "write_ptr %s [%d] \r\n" ,__FILE__,__LINE__);
         png_destroy_write_struct(&write_ptr,  NULL);
         return ;
      }
   png_set_error_fn(write_ptr, &error_parameters, pngtest_error,
                    pngtest_warning);
					
	//初始化 write_info_ptr
	write_info_ptr = png_create_info_struct(write_ptr);
	if(write_info_ptr == NULL)
      {
         printf("png_create_info_struct %s [%d] \r\n", __FILE__,__LINE__);
//         fclose(pPNG);
         png_destroy_write_struct(&write_ptr,  &write_info_ptr);
         return ;
      }
    write_end_info_ptr = png_create_info_struct(write_ptr);
					
	if (setjmp(png_jmpbuf(write_ptr)))
	{
		/* If we get here, we had a problem writing the file */
		png_destroy_write_struct(&write_ptr, &write_info_ptr);
		printf("setjmp error %s [%d] \r\n", __FILE__,__LINE__);
		return ;
	}
	// 重要 设置 libpng 读写函数
//	write_io_ptr = png_get_io_ptr(write_ptr); 
	png_set_write_fn(write_ptr, write_io_ptr, write_function,
                    NULL);
//	png_init_io(write_ptr, pPNG);
	//设置压缩等级
   png_set_compression_level(write_ptr,
        Z_BEST_COMPRESSION);

    /* Set other zlib parameters for compressing IDAT */
    png_set_compression_mem_level(write_ptr, 8);
    png_set_compression_strategy(write_ptr,
        Z_DEFAULT_STRATEGY);
    png_set_compression_window_bits(write_ptr, 15);
    png_set_compression_method(write_ptr, 8);
    png_set_compression_buffer_size(write_ptr, 8192); //8192

    /* Set zlib parameters for text compression
     * If you don't call these, the parameters
     * fall back on those defined for IDAT chunks
     */
    png_set_text_compression_mem_level(write_ptr, 8);
    png_set_text_compression_strategy(write_ptr,
        Z_DEFAULT_STRATEGY);
    png_set_text_compression_window_bits(write_ptr, 15);
    png_set_text_compression_method(write_ptr, 8);
	
	//初始化图像 iHDR
	png_set_IHDR(write_ptr, write_info_ptr, biWidth,
                   biHeight, bit_DEPTH, PNG_COLOR_TYPE_RGB,
                   PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
                   PNG_FILTER_TYPE_DEFAULT);
		// update write_info
//      printf( "png_set_bgr \r\n");
      png_set_bgr(write_ptr); // for BMP data   BGR -> RGB
      // auto flush write data when use low level inteface
//      png_set_flush(write_ptr, 50);
      // all set finish
      png_write_info(write_ptr, write_info_ptr);
}

基本流程是这样

这个函数完成了png结构和png信息结构的初始化,并将iHDR信息写入到内存中。关于iHDR的信息请看这篇文章

PNG文件结构分析 ---Png解析(转),里边有png文件结构的说明,我们简单点,png图片只需要包含iHDR和iDATA部分即可。

写入数据

完成初始化以后就是写入真正的图像数据了,libpng提供了很多函数完成写入,这里我选择了内存占用小的一种,使用

png_write_row(write_ptr, (png_bytep ) &row_pointers[row][0]); // one row eatch time

函数每次写入一行图像,还有其他多种方式,

png_write_rows(write_ptr, row_pointers,number_of_rows); //每次写入多行

png_write_image(write_ptr, row_pointers); //一次性写入整幅图像

注意 row_pointers 的数据类型和代表的 意义

png_write_rows / png_write_row / png_write_image 内部自动按照一定规则调用zlib压缩函数,不需要手动干预。zlib压缩的规则可以在前面初始化的时候修改,但是一般就没有必要改动。

下面是写入部分的代码,我是通过UDP接收的原始RGB像素信息,每次传传过来一行像素,写入png图像中,当传输完所有行数据后,调用

png_write_end(write_ptr, write_info_ptr); // write finish

写入png图像的结尾

然后通过串口打印出来16进制字符,方便与png文件结构进行对比。

最后在电脑上又把16进制字符转成二进制文件,后缀改成png,就能用电脑端的图片查看工具查看图片了。你也可以通过其他方式直接传输图像文件。

set_png_user_IO(&my_image); 
while(1)
    {
        bool isCheck = false;
		//接收 UDP数据 此处换成图像数据
		if(row < biHeight )
		{
			int len = get_udp_rxData(row_pointers[row], rowcnt);
			if(len >= rowcnt)
			{
				printf("receive row %d %d byte \r\n",row ,len);
				 tim1 = xTaskGetTickCount();
				png_write_row(write_ptr, (png_bytep ) &row_pointers[row][0]); // one row eatch time
				 tim2 = xTaskGetTickCount();
				printf("png_write_row use %d ms \r\n",tim2-tim1);
				row++;
			}
			if(row == biHeight) 
				cnt= 0;
			
		}
		else if(row == biHeight)
		{
			//接收完成
			printf("receive finish \r\n");
			tim1 = xTaskGetTickCount();
			png_write_end(write_ptr, write_info_ptr); // write finish
			tim2 = xTaskGetTickCount();
			printf("png_write_end use %d ms \r\n",tim2-tim1);
			
//			png_destroy_write_struct(&write_ptr, &write_info_ptr);
			for (int i = 0;i< my_image.png_cnt ;i++)
			{
				printf("%02X ",my_image.png[i]);
				
			}
//			fclose(pPNG);
			memset(&my_image,0,sizeof(my_image));
			cnt++;
			row++;
		}
		else
		{
			
		}
		
		vTaskDelay(pdMS_TO_TICKS(100));
		
    }

经过测试,RT1052 528MHz的主频,32MB SDRAM,压缩一行1920像素(5760 Bytes)的图片,用时大概是1-8ms,时间不等,因为压缩数据的过程是接收一定数据量后才进行的。

这样一幅1920×1200的原始图片,压缩用时大概是3-7秒,没有很严格的测算,感觉还有优化空间。

screen.bmp是源图像,通过UDP发送时只发送了像素信息,BMP的文件信息不发送。receive.png是收到的压缩后图像,因为所用的测试图像有大片单调像素,可以看到图片从6.7MB压缩到83KB,压缩率还是很可观的,当然最后得到的png图片大小是和图像内容相关的,有些画面复杂多变的图像使用png压缩大概只能压缩到1/3或1/2,要更高压缩率可以尝试libjpeg或者libjpeg-turbo,待后续文章介绍。

关于png使用的压缩算法,请查看其他文章。建议稍微了解一下png文件结构,png压缩算法,会对使用libpng有很大帮助。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值