前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >libjpeg:实现jpeg内存解压缩塈转换色彩空间/压缩分辨率

libjpeg:实现jpeg内存解压缩塈转换色彩空间/压缩分辨率

作者头像
10km
发布2022-05-07 10:17:15
1.2K0
发布2022-05-07 10:17:15
举报
文章被收录于专栏:10km的专栏10km的专栏

前一篇博客《libjpeg:实现jpeg内存压缩暨error_exit错误异常处理和个性化参数设置》实现了jpeg图像的内存压缩,本文来讨论jpeg图像内存解压缩的过程以及让libjpeg在解压缩时就将图像转灰度或其他色彩空间。

先贴出完整代码,再分段说明。 jpeg_mem.h

代码语言:javascript
复制
/* 图像矩阵基本参数 */
typedef struct _image_matrix_pram{
        int32_t		width;					// 图像宽度
        int32_t		height;					// 图像高度
        uint8_t		channels;				// 通道数
        J_COLOR_SPACE color_space; // 图像数据的色彩空间
        uint8_t		align;	// 内存对齐方式 0为不对齐,>0为以2的n次幂对齐
        std::vector <uint8_t> pixels; // 图像数据
}image_matrix_pram,*image_matrix_pram_ptr;
/* 处理压缩解压缩后内存数据的回调函数 */
using mem_callback_fun=std::function<void(const uint8_t*,unsigned long)>;
/* 定制压缩解压缩参数 */
using jpeg_custom_fun=std::function<void(j_common_ptr)>;
/* jpeg图像处理异常类 */
class jpeg_mem_exception:public std::logic_error{
public:
	// 继承基类构造函数
	using std::logic_error::logic_error;
};
/* 图像解压缩接口类 */
struct jpeg_decompress_interface{
	// 图像缓冲区
	std::vector<JSAMPROW> buffer;
	// 设置自定义输出参数的函数对象
	jpeg_custom_fun custom_output=[](j_common_ptr){};
	// 虚函数用于初始化内存填充解压缩后图像信息数据
	virtual void start_output(const jpeg_decompress_struct&cinfo)=0;
	// 虚函数用于将解压缩后的数据写入图像内存区
	virtual void put_pixel_rows(JDIMENSION num_scanlines)=0;
	virtual ~jpeg_decompress_interface()=default;
};

/* 默认的图像解压缩接口实现 */
struct jpeg_decompress_default:public jpeg_decompress_interface{
	/* 解压缩后的图像基本信息 */
	image_matrix_pram img;
	// 当前处理的目标图像像素行数
	JDIMENSION next_line;
	virtual void start_output(const jpeg_decompress_struct&cinfo){
		// 填充图像基本信息结构
		img.width=cinfo.output_width;
		img.height=cinfo.output_height;
		img.color_space=cinfo.out_color_space;
		img.channels=cinfo.output_components;
		// 分配像素数据存储区
		img.pixels=std::vector<uint8_t>(img.width*img.height*img.channels);
		// buffer只保存一行像素的目标数据指针
		buffer=std::vector<JSAMPROW>(1);
		next_line=0;
		// 初始化buffer指向第一像素存储地址
		buffer[next_line]=img.pixels.data();
	}
	virtual void put_pixel_rows(JDIMENSION num_scanlines){
		// buffer指向下一行要像素存储地址
		buffer[0]=img.pixels.data()+(++next_line)*img.width*img.channels;
	}
	virtual ~jpeg_decompress_default()=default;
};

jpeg_mem.cpp

代码语言:javascript
复制
/* 自定义jpeg图像压缩/解压缩过程中错误退出函数 */
METHODDEF(void) jpeg_mem_error_exit (j_common_ptr cinfo) {
	// 调用 format_message 生成错误信息
	char err_msg[JMSG_LENGTH_MAX];
	(*cinfo->err->format_message) (cinfo,err_msg);
	// 抛出c++异常
	throw jpeg_mem_exception(err_msg);
}
/* 将jpeg格式的内存数据块jpeg_data解压缩 
 * 图像行数据存储的方式都由decompress_instance定义
 * 出错抛出 jpeg_mem_exception 
 */
void load_jpeg_mem(uint8_t *jpeg_data,size_t size,
		 jpeg_decompress_interface &decompress_instance) {
	if(nullptr==jpeg_data||0==size)
		throw jpeg_mem_exception("empty image data");
	// 定义一个压缩对象 
	jpeg_decompress_struct  cinfo;
	//用于错误信息 
	jpeg_error_mgr jerr;
	// 错误输出绑定到压缩对象 
	cinfo.err = jpeg_std_error(&jerr);
	// 设置自定义的错误处理函数 
	jerr.error_exit = jpeg_mem_error_exit;
	// RAII对象在函数结束时释放资源
	gdface::raii buffer_guard([&](){
		jpeg_finish_decompress(&cinfo);
		jpeg_destroy_decompress(&cinfo);
	});
	// 初始化压缩对象
	jpeg_create_decompress(&cinfo);
	jpeg_mem_src(&cinfo, jpeg_data, (unsigned long)size); // 设置内存输出缓冲区
	(void) jpeg_read_header(&cinfo, true);
	decompress_instance.custom_output((j_common_ptr)&cinfo); // 执行自定义参数设置函数
	(void) jpeg_start_decompress(&cinfo);
	// 输出通道数必须是1/3/4
	if (cinfo.output_components != 1 && cinfo.output_components != 3 && cinfo.output_components != 4) {
		throw jpeg_mem_exception(
			"load_jpeg_mem(): Failed to load JPEG data cause by output_components error");
	}
	decompress_instance.start_output(cinfo);
	JDIMENSION num_scanlines;
	JDIMENSION max_lines;
	while (cinfo.output_scanline  < cinfo.output_height) {
		num_scanlines = jpeg_read_scanlines(&cinfo, decompress_instance.buffer.data(),
				(JDIMENSION)decompress_instance.buffer.size());
		max_lines=std::min((cinfo.output_height-cinfo.output_scanline),(JDIMENSION)decompress_instance.buffer.size());
		// 如果取到的行数小于预期的行数,则图像数据不完整抛出异常
        if (num_scanlines<max_lines)
        	throw jpeg_mem_exception("load_jpeg_mem(): Incomplete data");
		decompress_instance.put_pixel_rows(num_scanlines);
	}
}

image_matrix_pram

image_matrix_pram用于描述图像矩阵(非压缩状态)的基本信息 图像像素数据保存在类型为std::vector <uint8_t>的向量对象中。 align为每行像素数据的内存对齐方式,如:为2时,以2的2次幂,就是4字节对齐,默认为0。 color_space为图像的色彩空间,枚举类型J_COLOR_SPACEjpeglib.h中定义,一般RGB图像是JCS_RGB,灰度图像是JCS_GRAYSCALE

代码语言:javascript
复制
/* 图像矩阵基本参数 */
typedef struct _image_matrix_pram{
        int32_t		width;					// 图像宽度
        int32_t		height;					// 图像高度
        uint8_t		channels;				// 通道数
        J_COLOR_SPACE color_space; // 图像数据的色彩空间
        uint8_t		align;	// 内存对齐方式 0为不对齐,>0为以2的n次幂对齐
        std::vector <uint8_t> pixels; // 图像数据
}image_matrix_pram,*image_matrix_pram_ptr;

jpeg_decompress_interface

为适应不同的解压缩需求,定义了jpeg_decompress_interface接口类,调用load_jpeg_mem对图像数据解压时必须提供一个类型为jpeg_decompress_interface的对象做入口参数,该接口主要start_outputput_pixel_rows两个函数,用于图像数据初始化和存储。 buffer对象是行像素解压缩数据的存储缓冲区,保存每行像素数据缓冲区的地址,libjpeg每次最多能解压缩的像素行数由buffer的元素个数决定。 start_output根据传入参数jpeg_decompress_struct中提供的图像基本信息,对图像存储区进行初始化。 put_pixel_rows则对负责将解压缩到缓冲区(buffer)的每行(row)像素存储到图像存储区中。

代码语言:javascript
复制
/* 图像解压缩接口类 */
struct jpeg_decompress_interface{
	// 图像缓冲区
	std::vector<JSAMPROW> buffer;
	// 设置自定义输出参数的函数对象
	jpeg_custom_fun custom_output=[](j_common_ptr){};
	// 虚函数用于初始化内存填充解压缩后图像信息数据
	virtual void start_output(const jpeg_decompress_struct&cinfo)=0;
	// 虚函数用于将解压缩后的数据写入图像内存区
	virtual void put_pixel_rows(JDIMENSION num_scanlines)=0;
	virtual ~jpeg_decompress_interface()=default;
};

jpeg_decompress_default:jpeg_decompress_interface的默认实现

一般情况下,像素的每个通道数据都是连续存储的,所以针对这种常用的图像矩阵存储方式,提供了jpeg_decompress_interface接口的默认实现jpeg_decompress_defaultjpeg_decompress_default每次只提供一行像素的缓冲区指针,由此控制libjpeg每次只解压缩一行数据。 成员对象img保存解压缩后的结果数据,当图像成功解压缩后,img中就存储了解压缩后图像的所有完整信息。 next_line成员指向当前要解压缩的像素行数 start_output中根据jpeg_decompress_struct提供的图像宽/高/通道数计算出图像矩阵需要的存储区并分配相应的内存(img.pixels)。 buffer中只有一个指针类型的元素,指向img.pixels每一行像素的地址。这样jpeglib在解压缩出来的一行数据直接写入了img.pixels 因为buffer指针直接指向了图像存储区(img.pixels)每行像素的对应位置,所以put_pixel_rows不需要有复制数据的动作,只需要将next_line加1,并根据next_linebuffer中的指针指向下一行像素的地址就可以了。

代码语言:javascript
复制
/* 默认的图像解压缩接口实现 */
struct jpeg_decompress_default:public jpeg_decompress_interface{
	/* 解压缩后的图像基本信息 */
	image_matrix_pram img;
	// 当前处理的目标图像像素行数
	JDIMENSION next_line;
	virtual void start_output(const jpeg_decompress_struct&cinfo){
		// 填充图像基本信息结构
		img.width=cinfo.output_width;
		img.height=cinfo.output_height;
		img.color_space=cinfo.out_color_space;
		img.channels=cinfo.output_components;
		// 分配像素数据存储区
		img.pixels=std::vector<uint8_t>(img.width*img.height*img.channels);
		// buffer只保存一行像素的目标数据指针
		buffer=std::vector<JSAMPROW>(1);
		next_line=0;
		// 初始化buffer指向第一像素存储地址
		buffer[next_line]=img.pixels.data();
	}
	virtual void put_pixel_rows(JDIMENSION num_scanlines){
		// buffer指向下一行要像素存储地址
		buffer[0]=img.pixels.data()+(++next_line)*img.width*img.channels;
	}
	virtual ~jpeg_decompress_default()=default;
};

load_jpeg_mem

load_jpeg_mem函数根据decompress_instance参数提供的数据存储方式对长度为sizejpeg图像数据jpeg_data进行解压缩,最后解压缩的结果如何处理由decompress_instance对象定义,load_jpeg_mem函数本身并不关心。

代码语言:javascript
复制
/* 将jpeg格式的内存数据块jpeg_data解压缩 
 * 图像行数据存储的方式都由decompress_instance定义
 * 出错抛出 jpeg_mem_exception 
 */
void load_jpeg_mem(uint8_t *jpeg_data,size_t size,
		 jpeg_decompress_interface &decompress_instance) {
	if(nullptr==jpeg_data||0==size)
		throw jpeg_mem_exception("empty image data");
	// 定义一个压缩对象 
	jpeg_decompress_struct  cinfo;
	//用于错误信息 
	jpeg_error_mgr jerr;
	// 错误输出绑定到压缩对象 
	cinfo.err = jpeg_std_error(&jerr);
	// 设置自定义的错误处理函数 
	jerr.error_exit = jpeg_mem_error_exit;
	// RAII对象在函数结束时释放资源
	gdface::raii buffer_guard([&](){
		jpeg_finish_decompress(&cinfo);
		jpeg_destroy_decompress(&cinfo);
	});
	// 初始化压缩对象
	jpeg_create_decompress(&cinfo);
	jpeg_mem_src(&cinfo, jpeg_data, (unsigned long)size); // 设置内存输出缓冲区
	(void) jpeg_read_header(&cinfo, true);// 读取jpeg格式头获取图像基本信息
	decompress_instance.custom_output((j_common_ptr)&cinfo); // 执行自定义参数设置函数
	(void) jpeg_start_decompress(&cinfo);
	// 输出通道数必须是1/3/4
	if (cinfo.output_components != 1 && cinfo.output_components != 3 && cinfo.output_components != 4) {
		throw jpeg_mem_exception(
			"load_jpeg_mem(): Failed to load JPEG data cause by output_components error");
	}
	decompress_instance.start_output(cinfo);
	JDIMENSION num_scanlines;
	JDIMENSION expectd_lines;
	while (cinfo.output_scanline  < cinfo.output_height) {
		num_scanlines = jpeg_read_scanlines(&cinfo, decompress_instance.buffer.data(),
				(JDIMENSION)decompress_instance.buffer.size());
		expectd_lines=std::min((cinfo.output_height-cinfo.output_scanline),(JDIMENSION)decompress_instance.buffer.size());
		// 如果取到的行数小于预期的行数,则图像数据不完整抛出异常
        if (num_scanlines<expectd_lines)
        	throw jpeg_mem_exception("load_jpeg_mem(): Incomplete data");
		decompress_instance.put_pixel_rows(num_scanlines);
	}
}

在上面的代码中用到了我之前一篇博客(《C++11实现模板化(通用化)RAII机制》)中实现的raii对象,该对象保证,不论在解压缩过程中是否发生异常(exception),用于释放资源的函数jpeg_finish_decompressjpeg_destroy_decompress都会被执行,以避免内存泄露问题。

对图像解码时出现的处理方式参见前一篇博客《libjpeg:实现jpeg内存压缩暨error_exit错误异常处理和个性化参数设置》

example,解压缩时转灰或压缩分辨率

下面代码为调用示例。在图像解压缩时就可以将图像转换为指定的色彩空间,也可以将图像分辨率按比例压缩。见代码中的注释说明

代码语言:javascript
复制
#include <iostream>
#include <fstream>
#include <string>
#include <iostream>
#include "jpeg_mem.h"
using namespace cimg_library;
using namespace std;

int main()
{
	try {
		const char *input_jpg_file = "D:/tmp/sample-1.jpg";
		// 将一个jpeg图像文件读取到内存
		std::ifstream is (input_jpg_file, std::ifstream::binary);
		std::vector<uint8_t> jpeg_data;
		if (is) {
			// get length of file:
			is.seekg(0, is.end);
			// 获取文件长度
			auto length = is.tellg();
			is.seekg(0, is.beg);

			jpeg_data = std::vector<uint8_t>(length);
			// read data as a block:
			is.read((char*) jpeg_data.data(), jpeg_data.size());
			is.close();
		}
		
		jpeg_decompress_default default_decompress_instance;
		default_decompress_instance.custom_output = [](j_common_ptr cinfo) {
			// 下面这行注释打开,就是设置解压缩时直接将图像转为灰度图(也可转为其他色彩空间)
			//((j_decompress_ptr)cinfo)->out_color_space = JCS_GRAYSCALE;
			// 下面这两行注释打开,就是设置解压缩时直接将图像尺寸压缩1/2
			//((j_decompress_ptr)cinfo)->scale_num=1;
			//((j_decompress_ptr)cinfo)->scale_denom=2;
			
		};
		load_jpeg_mem(jpeg_data,default_decompress_instance);
		// 函数调用结束,图像解码后的数据保存在default_decompress_instance.img中
	}catch (exception &e){
		// 异常输出
		cout<<e.what()<<endl;
	}
    return 0;
}

jpeg_decompress_interface接口的差异化实现

对于不同的图像处理对象,图像数据的保存方式可能是不一样的,比如CImg,是将每个通道的数据连续存储的,所以每个像素的每个通道的颜色值并不是连续存储的。前面的jpeg_decompress_default对象就不适合这种存储方式,这时就需要自己实现jpeg_decompress_interface接口,才能正确执行解压缩,就以CImg为例:

代码语言:javascript
复制
	// 该函数为继承CImg的子类的成员函数,为了突出重点, 就不贴出子类的完整代码了
	const CImgWrapper<T>& load_mem_jpeg(uint8_t *jpeg_data,size_t size,jpeg_custom_fun custom=jpeg_custom_default){
		// 实现jpeg_decompress_interface 接口
		struct  jpeg_decompress_cimg:public jpeg_decompress_interface {
			// 行缓冲区
			CImg<typename CImg<T>::ucharT> line_buffer;
			// 颜色通道指针
			T *ptr_r=nullptr , *ptr_g=nullptr , *ptr_b=nullptr , *ptr_a=nullptr;
			CImgWrapper<T>& cimg_obj;
			jpeg_decompress_cimg(CImgWrapper<T>& cimg_obj):cimg_obj(cimg_obj){}
			virtual void start_output(const jpeg_decompress_struct&cinfo) {
				line_buffer=CImg<typename CImg<T>::ucharT>(cinfo.output_width*cinfo.output_components);
				cimg_obj.assign(cinfo.output_width,cinfo.output_height,1,cinfo.output_components);
			    ptr_r  = cimg_obj._data,
			    ptr_g = cimg_obj._data + 1UL*cimg_obj._width*cimg_obj._height,
				ptr_b = cimg_obj._data + 2UL*cimg_obj._width*cimg_obj._height,
			    ptr_a = cimg_obj._data + 3UL*cimg_obj._width*cimg_obj._height;
				buffer=std::vector<JSAMPROW>(1);
				buffer[0] =(JSAMPROW) line_buffer._data;
			}
			virtual void put_pixel_rows(JDIMENSION num_scanlines) {
		        const unsigned char *ptrs = line_buffer._data;
		        switch (cimg_obj._spectrum) {
		        case 1 : {
		        	cimg_forX(cimg_obj,x) *(ptr_r++) = (T)*(ptrs++);
		        } break;
		        case 3 : {
		          cimg_forX(cimg_obj,x) {
		            *(ptr_r++) = (T)*(ptrs++);
		            *(ptr_g++) = (T)*(ptrs++);
		            *(ptr_b++) = (T)*(ptrs++);
		          }
		        } break;
		        case 4 : {
		          cimg_forX(cimg_obj,x) {
		            *(ptr_r++) = (T)*(ptrs++);
		            *(ptr_g++) = (T)*(ptrs++);
		            *(ptr_b++) = (T)*(ptrs++);
		            *(ptr_a++) = (T)*(ptrs++);
		          }
		        } break;
		        }
			}
		}jpeg_decompress_cimg_instance(*this);
		jpeg_decompress_cimg_instance.custom_output=custom;
		// 调用load_jpeg_mem解压缩
		load_jpeg_mem(jpeg_data,size,jpeg_decompress_cimg_instance);
		return *this;
	}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016-01-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • image_matrix_pram
  • jpeg_decompress_interface
  • jpeg_decompress_default:jpeg_decompress_interface的默认实现
  • load_jpeg_mem
  • example,解压缩时转灰或压缩分辨率
  • jpeg_decompress_interface接口的差异化实现
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档