libjpeg: realize jpeg memory decompression and conversion of color space / compression resolution

Previous blog libjpeg: realizing jpeg memory compression and error_exit error exception handling and personalized parameter setting The memory compression of jpeg image is realized. This paper discusses the process of jpeg image memory decompression and lets libjpeg convert the image to gray or other color space when decompressing.

First post the complete code, and then explain it in sections. jpeg_mem.h

/* Basic parameters of image matrix */
typedef struct _image_matrix_pram{
        int32_t		width;					// Image width
        int32_t		height;					// Image height
        uint8_t		channels;				// Number of channels
        J_COLOR_SPACE color_space; // Color space of image data
        uint8_t		align;	// Memory alignment 0 is not aligned, > 0 is aligned to the nth power of 2
        std::vector <uint8_t> pixels; // image data
}image_matrix_pram,*image_matrix_pram_ptr;
/* Callback function for processing memory data after compression and decompression */
using mem_callback_fun=std::function<void(const uint8_t*,unsigned long)>;
/* Custom compression and decompression parameters */
using jpeg_custom_fun=std::function<void(j_common_ptr)>;
/* jpeg Image processing exception class */
class jpeg_mem_exception:public std::logic_error{
public:
	// Inherit base class constructor
	using std::logic_error::logic_error;
};
/* Image decompression interface class */
struct jpeg_decompress_interface{
	// Image buffer
	std::vector<JSAMPROW> buffer;
	// Function object to set custom output parameters
	jpeg_custom_fun custom_output=[](j_common_ptr){};
	// Virtual function is used to initialize memory filling and decompressed image information data
	virtual void start_output(const jpeg_decompress_struct&cinfo)=0;
	// The virtual function is used to write the decompressed data into the image memory area
	virtual void put_pixel_rows(JDIMENSION num_scanlines)=0;
	virtual ~jpeg_decompress_interface()=default;
};

/* Implementation of default image decompression interface */
struct jpeg_decompress_default:public jpeg_decompress_interface{
	/* Basic information of decompressed image */
	image_matrix_pram img;
	// Number of pixel lines of the target image currently processed
	JDIMENSION next_line;
	virtual void start_output(const jpeg_decompress_struct&cinfo){
		// Basic information structure of filled image
		img.width=cinfo.output_width;
		img.height=cinfo.output_height;
		img.color_space=cinfo.out_color_space;
		img.channels=cinfo.output_components;
		// Allocate pixel data storage area
		img.pixels=std::vector<uint8_t>(img.width*img.height*img.channels);
		// buffer only saves the target data pointer of one row of pixels
		buffer=std::vector<JSAMPROW>(1);
		next_line=0;
		// The initialization buffer points to the first pixel storage address
		buffer[next_line]=img.pixels.data();
	}
	virtual void put_pixel_rows(JDIMENSION num_scanlines){
		// buffer points to the address of the pixel to be stored in the next row
		buffer[0]=img.pixels.data()+(++next_line)*img.width*img.channels;
	}
	virtual ~jpeg_decompress_default()=default;
};
copy

jpeg_mem.cpp

/* Error exit function during custom jpeg image compression / decompression */
METHODDEF(void) jpeg_mem_error_exit (j_common_ptr cinfo) {
	// Call format_message generates an error message
	char err_msg[JMSG_LENGTH_MAX];
	(*cinfo->err->format_message) (cinfo,err_msg);
	// Throw c + + exception
	throw jpeg_mem_exception(err_msg);
}
/* Convert the memory data block in jpeg format to jpeg_data decompression 
 * The image row data is stored by decompress_instance definition
 * Error throwing 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");
	// Define a compressed object 
	jpeg_decompress_struct  cinfo;
	//For error messages 
	jpeg_error_mgr jerr;
	// The error output is bound to a compressed object 
	cinfo.err = jpeg_std_error(&jerr);
	// Set custom error handling function 
	jerr.error_exit = jpeg_mem_error_exit;
	// The RAII object releases resources at the end of the function
	gdface::raii buffer_guard([&](){
		jpeg_finish_decompress(&cinfo);
		jpeg_destroy_decompress(&cinfo);
	});
	// Initialize compressed object
	jpeg_create_decompress(&cinfo);
	jpeg_mem_src(&cinfo, jpeg_data, (unsigned long)size); // Set memory output buffer
	(void) jpeg_read_header(&cinfo, true);
	decompress_instance.custom_output((j_common_ptr)&cinfo); // Execute custom parameter setting function
	(void) jpeg_start_decompress(&cinfo);
	// The number of output channels must be 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 the number of rows obtained is less than the expected number of rows, the image data is incomplete and an exception is thrown
        if (num_scanlines<max_lines)
        	throw jpeg_mem_exception("load_jpeg_mem(): Incomplete data");
		decompress_instance.put_pixel_rows(num_scanlines);
	}
}
copy

image_matrix_pram

image_matrix_pram is used to describe the basic information of image matrix (uncompressed state) Image pixel data is saved in STD:: vector < uint8_ t> Vector object. align is the memory alignment of each row of pixel data. For example, when it is 2, it is aligned to the power of 2, that is, 4 bytes. The default is 0. color_space is the color space of the image, enumerating type J_COLOR_SPACE is on JPEG lib As defined in H, the general RGB image is JCS_RGB, gray image is JCS_GRAYSCALE.

/* Basic parameters of image matrix */
typedef struct _image_matrix_pram{
        int32_t		width;					// Image width
        int32_t		height;					// Image height
        uint8_t		channels;				// Number of channels
        J_COLOR_SPACE color_space; // Color space of image data
        uint8_t		align;	// Memory alignment 0 is not aligned, > 0 is aligned to the nth power of 2
        std::vector <uint8_t> pixels; // image data
}image_matrix_pram,*image_matrix_pram_ptr;
copy

jpeg_decompress_interface

In order to meet different decompression requirements, JPEG is defined_ decompress_ Interface interface class, calling load_ jpeg_ When MEM decompresses image data, it must provide a type of JPEG_ decompress_ The object of interface is used as the entry parameter, and the interface mainly starts_ Output and output_ pixel_ Rows two functions for image data initialization and storage. The buffer object is the storage buffer of row pixel decompressed data, which stores the address of each row pixel data buffer. The maximum number of pixel rows that libjpeg can decompress each time is determined by the number of buffer elements. start_output according to the incoming parameter JPEG_ decompress_ The basic image information provided in struct initializes the image storage area. put_pixel_rows is responsible for storing the pixels of each row decompressed into the buffer into the image storage area.

/* Image decompression interface class */
struct jpeg_decompress_interface{
	// Image buffer
	std::vector<JSAMPROW> buffer;
	// Function object to set custom output parameters
	jpeg_custom_fun custom_output=[](j_common_ptr){};
	// Virtual function is used to initialize memory filling and decompressed image information data
	virtual void start_output(const jpeg_decompress_struct&cinfo)=0;
	// The virtual function is used to write the decompressed data into the image memory area
	virtual void put_pixel_rows(JDIMENSION num_scanlines)=0;
	virtual ~jpeg_decompress_interface()=default;
};
copy

jpeg_ decompress_ default:jpeg_ decompress_ Default implementation of interface

Generally, each channel data of pixels is stored continuously, so JPEG is provided for this common image matrix storage method_ decompress_ The default implementation of interface is jpeg_decompress_default. jpeg_decompress_default provides a buffer pointer of only one row of pixels at a time, which controls libjpeg to decompress only one row of data at a time. The member object img stores the decompressed result data. When the image is decompressed successfully, IMG stores all the complete information about the compressed image. next_ The line member points to the number of pixel rows currently to be decompressed start_ JPEG in output_ decompress_ The image width / height / channel number provided by struct calculates the storage area required by the image matrix and allocates the corresponding memory (img.pixels). There is only one pointer type element in buffer, pointing to img Pixels the address of each row of pixels. In this way, a row of data extracted by JPEG lib is directly written to img pixels Because the buffer pointer directly points to the corresponding position of each row of pixels in the image storage area (img.pixels), put_pixel_rows doesn't need to copy data, just next_line plus 1, and according to next_line just point the pointer in the buffer to the address of the next row of pixels.

/* Implementation of default image decompression interface */
struct jpeg_decompress_default:public jpeg_decompress_interface{
	/* Basic information of decompressed image */
	image_matrix_pram img;
	// Number of pixel lines of the target image currently processed
	JDIMENSION next_line;
	virtual void start_output(const jpeg_decompress_struct&cinfo){
		// Basic information structure of filled image
		img.width=cinfo.output_width;
		img.height=cinfo.output_height;
		img.color_space=cinfo.out_color_space;
		img.channels=cinfo.output_components;
		// Allocate pixel data storage area
		img.pixels=std::vector<uint8_t>(img.width*img.height*img.channels);
		// buffer only saves the target data pointer of one row of pixels
		buffer=std::vector<JSAMPROW>(1);
		next_line=0;
		// The initialization buffer points to the first pixel storage address
		buffer[next_line]=img.pixels.data();
	}
	virtual void put_pixel_rows(JDIMENSION num_scanlines){
		// buffer points to the address of the pixel to be stored in the next row
		buffer[0]=img.pixels.data()+(++next_line)*img.width*img.channels;
	}
	virtual ~jpeg_decompress_default()=default;
};
copy

load_jpeg_mem

load_jpeg_mem function according to decompress_ The data storage method provided by the instance parameter is JPEG for JPEG image data with length of size_ Data is decompressed. How to deal with the final decompressed result is determined by decompress_instance object definition, load_ jpeg_ The MEM function itself does not care.

/* Convert the memory data block in jpeg format to jpeg_data decompression 
 * The image row data is stored by decompress_instance definition
 * Error throwing 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");
	// Define a compressed object 
	jpeg_decompress_struct  cinfo;
	//For error messages 
	jpeg_error_mgr jerr;
	// The error output is bound to a compressed object 
	cinfo.err = jpeg_std_error(&jerr);
	// Set custom error handling function 
	jerr.error_exit = jpeg_mem_error_exit;
	// The RAII object releases resources at the end of the function
	gdface::raii buffer_guard([&](){
		jpeg_finish_decompress(&cinfo);
		jpeg_destroy_decompress(&cinfo);
	});
	// Initialize compressed object
	jpeg_create_decompress(&cinfo);
	jpeg_mem_src(&cinfo, jpeg_data, (unsigned long)size); // Set memory output buffer
	(void) jpeg_read_header(&cinfo, true);// Read the jpeg format header to obtain the basic information of the image
	decompress_instance.custom_output((j_common_ptr)&cinfo); // Execute custom parameter setting function
	(void) jpeg_start_decompress(&cinfo);
	// The number of output channels must be 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 the number of rows obtained is less than the expected number of rows, the image data is incomplete and an exception is thrown
        if (num_scanlines<expectd_lines)
        	throw jpeg_mem_exception("load_jpeg_mem(): Incomplete data");
		decompress_instance.put_pixel_rows(num_scanlines);
	}
}
copy

In the above code, I used my previous blog( C++11 implementation of templating (generalization) RAII mechanism )The raii object implemented in, which ensures that whether an exception occurs during decompression, the function JPEG used to release resources_ finish_ Decompress and jpeg_destroy_decompress will be executed to avoid memory leakage.

See the previous blog for the processing methods of image decoding libjpeg: realizing jpeg memory compression and error_exit error exception handling and personalized parameter setting.

example: graying out or compression resolution during decompression

The following code is a call example. When decompressing the image, you can convert the image into the specified color space, or compress the image resolution in proportion. See the notes in the code

#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";
		// Read a jpeg image file into memory
		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);
			// Get file length
			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) {
			// The following line of comments opens, which is to set the image to be directly converted to grayscale image (or other color space) during decompression
			//((j_decompress_ptr)cinfo)->out_color_space = JCS_GRAYSCALE;
			// The following two lines of comments are opened to directly compress the image size by 1 / 2 during decompression
			//((j_decompress_ptr)cinfo)->scale_num=1;
			//((j_decompress_ptr)cinfo)->scale_denom=2;
			
		};
		load_jpeg_mem(jpeg_data,default_decompress_instance);
		// After the function call, the decoded image data is saved in default_ decompress_ instance. In img
	}catch (exception &e){
		// Abnormal output
		cout<<e.what()<<endl;
	}
    return 0;
}
copy

jpeg_ decompress_ Differentiated implementation of interface

For different image processing objects, the storage methods of image data may be different. For example, CImg stores the data of each channel continuously, so the color value of each channel of each pixel is not stored continuously. JPEG in front_ decompress_ The default object is not suitable for this storage method, so you need to implement JPEG yourself_ decompress_ Interface interface to perform decompression correctly. Take CImg as an example:

	// This function is a member function that inherits the subclass of CImg. In order to highlight the key points, the complete code of the subclass will not be posted
	const CImgWrapper<T>& load_mem_jpeg(uint8_t *jpeg_data,size_t size,jpeg_custom_fun custom=jpeg_custom_default){
		// Implement jpeg_decompress_interface interface
		struct  jpeg_decompress_cimg:public jpeg_decompress_interface {
			// Row buffer
			CImg<typename CImg<T>::ucharT> line_buffer;
			// Color channel pointer
			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;
		// Call load_jpeg_mem decompression
		load_jpeg_mem(jpeg_data,size,jpeg_decompress_cimg_instance);
		return *this;
	}
copy

Posted by jimmyt1988 on Sat, 07 May 2022 07:11:44 +0300