前一篇博客《libjpeg:实现jpeg内存压缩暨error_exit错误异常处理和个性化参数设置》实现了jpeg图像的内存压缩,本文来讨论jpeg图像内存解压缩的过程以及让libjpeg在解压缩时就将图像转灰度或其他色彩空间。
先贴出完整代码,再分段说明。
jpeg_mem.h
/* 图像矩阵基本参数 */
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 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 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 (img.width*img.height*img.channels);
// buffer只保存一行像素的目标数据指针
buffer=std::vector (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
/* 自定义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_scanlinesthrow jpeg_mem_exception("load_jpeg_mem(): Incomplete data");
decompress_instance.put_pixel_rows(num_scanlines);
}
}
image_matrix_pram
用于描述图像矩阵(非压缩状态)的基本信息
图像像素数据保存在类型为std::vector
的向量对象中。
align
为每行像素数据的内存对齐方式,如:为2时,以2的2次幂,就是4字节对齐,默认为0。
color_space
为图像的色彩空间,枚举类型J_COLOR_SPACE
在jpeglib.h
中定义,一般RGB图像是JCS_RGB
,灰度图像是JCS_GRAYSCALE
。
/* 图像矩阵基本参数 */
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 pixels; // 图像数据
}image_matrix_pram,*image_matrix_pram_ptr;
为适应不同的解压缩需求,定义了jpeg_decompress_interface
接口类,调用load_jpeg_mem
对图像数据解压时必须提供一个类型为jpeg_decompress_interface
的对象做入口参数,该接口主要start_output
和put_pixel_rows
两个函数,用于图像数据初始化和存储。
buffer
对象是行像素解压缩数据的存储缓冲区,保存每行像素数据缓冲区的地址,libjpeg每次最多能解压缩的像素行数由buffer
的元素个数决定。
start_output
根据传入参数jpeg_decompress_struct
中提供的图像基本信息,对图像存储区进行初始化。
put_pixel_rows
则对负责将解压缩到缓冲区(buffer)的每行(row)像素存储到图像存储区中。
/* 图像解压缩接口类 */
struct jpeg_decompress_interface{
// 图像缓冲区
std::vector 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_interface
接口的默认实现jpeg_decompress_default
。
jpeg_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_line
将buffer
中的指针指向下一行像素的地址就可以了。
/* 默认的图像解压缩接口实现 */
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 (img.width*img.height*img.channels);
// buffer只保存一行像素的目标数据指针
buffer=std::vector (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
函数根据decompress_instance
参数提供的数据存储方式对长度为size
的jpeg
图像数据jpeg_data
进行解压缩,最后解压缩的结果如何处理由decompress_instance
对象定义,load_jpeg_mem
函数本身并不关心。
/* 将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_scanlinesthrow jpeg_mem_exception("load_jpeg_mem(): Incomplete data");
decompress_instance.put_pixel_rows(num_scanlines);
}
}
在上面的代码中用到了我之前一篇博客(《C++11实现模板化(通用化)RAII机制》)中实现的raii
对象,该对象保证,不论在解压缩过程中是否发生异常(exception),用于释放资源的函数jpeg_finish_decompress
和jpeg_destroy_decompress
都会被执行,以避免内存泄露问题。
对图像解码时出现的处理方式参见前一篇博客《libjpeg:实现jpeg内存压缩暨error_exit错误异常处理和个性化参数设置》。
下面代码为调用示例。在图像解压缩时就可以将图像转换为指定的色彩空间,也可以将图像分辨率按比例压缩。见代码中的注释说明
#include
#include
#include
#include
#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 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 (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<return 0;
}
对于不同的图像处理对象,图像数据的保存方式可能是不一样的,比如CImg,是将每个通道的数据连续存储的,所以每个像素的每个通道的颜色值并不是连续存储的。前面的jpeg_decompress_default
对象就不适合这种存储方式,这时就需要自己实现jpeg_decompress_interface
接口,才能正确执行解压缩,就以CImg为例:
// 该函数为继承CImg的子类的成员函数,为了突出重点, 就不贴出子类的完整代码了
const CImgWrapper& 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::ucharT> line_buffer;
// 颜色通道指针
T *ptr_r=nullptr , *ptr_g=nullptr , *ptr_b=nullptr , *ptr_a=nullptr;
CImgWrapper& cimg_obj;
jpeg_decompress_cimg(CImgWrapper& cimg_obj):cimg_obj(cimg_obj){}
virtual void start_output(const jpeg_decompress_struct&cinfo) {
line_buffer=CImg<typename CImg::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 (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;
}