本文分析了Opencv中jpeg的编码流程,希望能够在加速jpeg编码效率上获得一些启发
从Java层开始,Opencv 2.4.13中imencode函数封装在了Highgui类中,而3.0.0以后,Highgui类被取缔,相关编解码操作放在了ImgCodecs这个类里面估计是为了Java和C++保持统一风格吧。这里直接调用即可
Highgui.imencode(String ext, Mat img, MatOfByte buf)
Highgui.imencode(String ext, Mat img, MatOfByte buf, MatOfInt params)
以上可以通过MatOfInt可指定编码时相关参数
开始看到这个时,感觉很奇怪,为啥要用MatOfInt来指定参数呢???不用着急,我们接着往下看
以上两个函数分别对应的native方法如下
// C++: bool imencode(String ext, Mat img, vector_uchar& buf, vector_int params = std::vector<int>())
private static native boolean imencode_0(String ext, long img_nativeObj, long buf_mat_nativeObj, long params_mat_nativeObj);
private static native boolean imencode_1(String ext, long img_nativeObj, long buf_mat_nativeObj);
不难发现,Opencv还是遵循着以往的JNI风格,所有对象都只传递对象起始地址,并且对象可以通过该地址恢复对象所有信息,即存在以该地址为参数的构造函数
我们接着往下,看看JNI层的代码,这里以3.1.0为例,代码位于OPENCV_HOME/release/modules/java/imgcodecs.cpp中
JNIEXPORT jboolean JNICALL Java_org_opencv_imgcodecs_Imgcodecs_imencode_10 (JNIEnv*, jclass, jstring, jlong, jlong, jlong);
JNIEXPORT jboolean JNICALL Java_org_opencv_imgcodecs_Imgcodecs_imencode_10
(JNIEnv* env, jclass , jstring ext, jlong img_nativeObj, jlong buf_mat_nativeObj, jlong params_mat_nativeObj)
{
static const char method_name[] = "imgcodecs::imencode_10()";
try {
LOGD("%s", method_name);
std::vector<uchar> buf;
Mat& buf_mat = *((Mat*)buf_mat_nativeObj);
std::vector<int> params;
Mat& params_mat = *((Mat*)params_mat_nativeObj);
Mat_to_vector_int( params_mat, params );
const char* utf_ext = env->GetStringUTFChars(ext, 0); String n_ext( utf_ext ? utf_ext : "" ); env->ReleaseStringUTFChars(ext, utf_ext);
Mat& img = *((Mat*)img_nativeObj);
bool _retval_ = cv::imencode( n_ext, img, buf, params );
vector_uchar_to_Mat( buf, buf_mat );
return _retval_;
} catch(const std::exception &e) {
throwJavaException(env, &e, method_name);
} catch (...) {
throwJavaException(env, 0, method_name);
}
return 0;
}
JNIEXPORT jboolean JNICALL Java_org_opencv_imgcodecs_Imgcodecs_imencode_11 (JNIEnv*, jclass, jstring, jlong, jlong);
JNIEXPORT jboolean JNICALL Java_org_opencv_imgcodecs_Imgcodecs_imencode_11
(JNIEnv* env, jclass , jstring ext, jlong img_nativeObj, jlong buf_mat_nativeObj)
{
static const char method_name[] = "imgcodecs::imencode_11()";
try {
LOGD("%s", method_name);
std::vector<uchar> buf;
Mat& buf_mat = *((Mat*)buf_mat_nativeObj);
const char* utf_ext = env->GetStringUTFChars(ext, 0); String n_ext( utf_ext ? utf_ext : "" ); env->ReleaseStringUTFChars(ext, utf_ext);
Mat& img = *((Mat*)img_nativeObj);
<span style="color:#FF0000;">bool _retval_ = cv::imencode( n_ext, img, buf );</span>
vector_uchar_to_Mat( buf, buf_mat );
return _retval_;
} catch(const std::exception &e) {
throwJavaException(env, &e, method_name);
} catch (...) {
throwJavaException(env, 0, method_name);
}
return 0;
}
以带参编译的方法为例,也就是imencode10(),首先从地址分别构造出原Java中的MatOfByte和ParamsMat两个Mat的数据结构,分别用于存储encode完的数据和相应传入的编码参数,接下来param_mat转换成了一个int的vector,我们跟进看下发现
void Mat_to_vector_int(Mat& mat, std::vector<int>& v_int)
{
v_int.clear();
CHECK_MAT(mat.type()==CV_32SC1 && mat.cols==1);
v_int = (std::vector<int>) mat;
}
转换过程相当简单。。。其实这个原来Java中MatOfInt的类,存储的其实就是一系列的32为signed int的值而已。。。这些data是可以直接进行类型转换的
在Java层我们仅需要这样构建params参数即可
MatOfInt params = new MatOfInt(Imgcodecs.CV_IMWRITE_JPEG_QUALITY, 90);
对应的构造方法原型为
public MatOfInt(int...a)
我们接着回到正题,拿到编译参数以后,真正的编码调用的是cv::imencode,这个函数的实现是在imgcodes/src下面的loadsave.cpp中
bool imencode( const String& ext, InputArray _image,
std::vector<uchar>& buf, const std::vector<int>& params )
{
Mat image = _image.getMat();
int channels = image.channels();
CV_Assert( channels == 1 || channels == 3 || channels == 4 );
ImageEncoder encoder = findEncoder( ext );
if( !encoder )
CV_Error( CV_StsError, "could not find encoder for the specified extension" );
if( !encoder->isFormatSupported(image.depth()) )
{
CV_Assert( encoder->isFormatSupported(CV_8U) );
Mat temp;
image.convertTo(temp, CV_8U);
image = temp;
}
bool code;
if( encoder->setDestination(buf) )
{
code = encoder->write(image, params);
encoder->throwOnEror();
CV_Assert( code );
}
else
{
String filename = tempfile();
code = encoder->setDestination(filename);
CV_Assert( code );
code = encoder->write(image, params);
encoder->throwOnEror();
CV_Assert( code );
FILE* f = fopen( filename.c_str(), "rb" );
CV_Assert(f != 0);
fseek( f, 0, SEEK_END );
long pos = ftell(f);
buf.resize((size_t)pos);
fseek( f, 0, SEEK_SET );
buf.resize(fread( &buf[0], 1, buf.size(), f ));
fclose(f);
remove(filename.c_str());
}
return code;
}
整个imencode的流程如下,先检查图像的通道数,然后检查文件类型判断是否支持,每个像素点的单通道是否是8bit,接下来setDescription的逻辑也非常简单
bool BaseImageEncoder::setDestination( std::vector<uchar>& buf )
{
if( !m_buf_supported )
return false;
m_buf = &buf;
m_buf->clear();
m_filename = String();
return true;
}
如上,就是清空一下缓存区,接下来就进入了对应encoder的write函数,对应JpegEncoder的源码如下
bool JpegEncoder::write( const Mat& img, const std::vector<int>& params )
{
m_last_error.clear();
struct fileWrapper
{
FILE* f;
fileWrapper() : f(0) {}
~fileWrapper() { if(f) fclose(f); }
};
volatile bool result = false;
fileWrapper fw;
int width = img.cols, height = img.rows;
std::vector<uchar> out_buf(1 << 12);
AutoBuffer<uchar> _buffer;
uchar* buffer;
struct jpeg_compress_struct cinfo;
JpegErrorMgr jerr;
JpegDestination dest;
jpeg_create_compress(&cinfo);
cinfo.err = jpeg_std_error(&jerr.pub);
jerr.pub.error_exit = error_exit;
if( !m_buf )
{
fw.f = fopen( m_filename.c_str(), "wb" );
if( !fw.f )
goto _exit_;
jpeg_stdio_dest( &cinfo, fw.f );
}
else
{
dest.dst = m_buf;
dest.buf = &out_buf;
jpeg_buffer_dest( &cinfo, &dest );
dest.pub.next_output_byte = &out_buf[0];
dest.pub.free_in_buffer = out_buf.size();
}
if( setjmp( jerr.setjmp_buffer ) == 0 )
{
cinfo.image_width = width;
cinfo.image_height = height;
int _channels = img.channels();
int channels = _channels > 1 ? 3 : 1;
cinfo.input_components = channels;
cinfo.in_color_space = channels > 1 ? JCS_RGB : JCS_GRAYSCALE;
int quality = 95;
int progressive = 0;
int optimize = 0;
int rst_interval = 0;
int luma_quality = -1;
int chroma_quality = -1;
for( size_t i = 0; i < params.size(); i += 2 )
{
if(