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

前一篇博客《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 <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

/* 自定义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

/* 图像矩阵基本参数 */
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)像素存储到图像存储区中。

/* 图像解压缩接口类 */
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_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_linebuffer中的指针指向下一行像素的地址就可以了。

/* 默认的图像解压缩接口实现 */
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函数本身并不关心。

/* 将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错误异常处理和个性化参数设置》。

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

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

#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为例:

    // 该函数为继承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;
    }

你可能感兴趣的:(C++,libjpeg,色彩空间,内存解压缩,修改分辨率)