简介:
本文就opencv中的几个常用函数:imread、cvLoadImage、waitKey、imshow,进行简单的源码分析,并对新、老版本进行比较。
实验平台:
xp + vs2010 + opencv2.4.10
案列:
用Opencv读取并显示图片,一般来说有①、②两种方法,下面就①②进行源码分析。
#include <iostream> #include <opencv2/highgui/highgui.hpp> using namespace cv; using namespace std; int main() { //①老版 IplImage *pic = cvLoadImage("lena.jpg", 1); cvShowImage("load", pic); cvWaitKey(0); //②新版 Mat img = imread("lena.jpg"); imshow("read", img); waitKey(0); return 0; }
源码分析:
1、图像的读取
cvLoadImage、imread
方法①中的cvLoadImage:
cvLoadImage函数原型如下,其中参数filename为待读取的图片名(可含路径),iscolor是读取方式,它是一个枚举参数(默认是读取的是彩色):
CVAPI(IplImage*) cvLoadImage( const char* filename, int iscolor CV_DEFAULT(CV_LOAD_IMAGE_COLOR));枚举如下:
enum { /* 8bit, color or not 8位*/ CV_LOAD_IMAGE_UNCHANGED =-1, /* 8bit, gray 8位灰度*/ CV_LOAD_IMAGE_GRAYSCALE =0, /* ?, color 彩色*/ CV_LOAD_IMAGE_COLOR =1, /* any depth, ? 任意深度*/ CV_LOAD_IMAGE_ANYDEPTH =2, /* ?, any color 任意颜色*/ CV_LOAD_IMAGE_ANYCOLOR =4 };
进一步查看cvLoadImage源码(如下),发现实际上是调用的imread_函数,imread_中有个参数是LOAD_IMAGE,这是因为图片是IplImage的(若是Mat类的,则应是LOAD_MAT,下面会提到)。
CV_IMPL IplImage* cvLoadImage( const char* filename, int iscolor ) { return (IplImage*)cv::imread_(filename, iscolor, cv::LOAD_IMAGE ); }
方法②中的imread:
imread函数原型如下,filename是文件名,flags默认为1,其含义同上iscolor枚举。
CV_EXPORTS_W Mat imread( const string& filename, int flags=1 );进一步查看imread源码,发现实际上调用的还是imread_函数,现在这里的参数就是LOAD_MAT了。
Mat imread( const string& filename, int flags ) { Mat img;//定义一个Mat类,用于装载图片 imread_( filename, flags, LOAD_MAT, &img );//读图像 return img; }
显然,不论是cvLoadImage还是imread,都是调用的imread_函数。那么我们就由此及彼,由表及里,去粗取精,去伪存真的去剖析其源码。
imread_函数源码(在源文件loadsave.cpp中):static void* imread_( const string& filename, int flags, int hdrtype, Mat* mat=0 ) { IplImage* image = 0;//定义一个IplImage结构体 CvMat *matrix = 0;//定义一个CvMat结构体 Mat temp, *data = &temp;//data中保存的是temp的地址,temp是一个Mat类容器 ImageDecoder decoder = findDecoder(filename);//①译码器 if( decoder.empty() ) return 0; decoder->setSource(filename); if( !decoder->readHeader() )//②读取信息头 return 0; CvSize size; size.width = decoder->width(); size.height = decoder->height(); int type = decoder->type(); if( flags != -1 )//③ { if( (flags & CV_LOAD_IMAGE_ANYDEPTH) == 0 ) type = CV_MAKETYPE(CV_8U, CV_MAT_CN(type)); if( (flags & CV_LOAD_IMAGE_COLOR) != 0 || ((flags & CV_LOAD_IMAGE_ANYCOLOR) != 0 && CV_MAT_CN(type) > 1) ) type = CV_MAKETYPE(CV_MAT_DEPTH(type), 3);//彩色 else type = CV_MAKETYPE(CV_MAT_DEPTH(type), 1);//灰度 } if( hdrtype == LOAD_CVMAT || hdrtype == LOAD_MAT )//④ { if( hdrtype == LOAD_CVMAT ) { matrix = cvCreateMat( size.height, size.width, type ); temp = cvarrToMat(matrix);//temp与matrix同址 } else { mat->create( size.height, size.width, type ); data = mat;//data与mat同址 } } else { image = cvCreateImage( size, cvIplDepth(type), CV_MAT_CN(type) ); temp = cvarrToMat(image);//temp与image同址 } if( !decoder->readData( *data ))//⑤ { cvReleaseImage( &image ); cvReleaseMat( &matrix ); if( mat ) mat->release(); return 0; } //根据指针及同址关系,可知matrix、image、mat数据(若存在)与data数据一致 return hdrtype == LOAD_CVMAT ? (void*)matrix : hdrtype == LOAD_IMAGE ? (void*)image : (void*)mat; }
在imread_中,有几个地方值得注意①②③④⑤,下面一一分析:
①findDecoder(),这是一个很重要的重载函数,它的目的是:解析图片信息,并确定应该使用的译码器(.jpg格式使用Jpeg译码器),其内部源码如下:
static ImageCodecInitializer codecs; static ImageDecoder findDecoder( const string& filename ) { size_t i, maxlen = 0; for( i = 0; i < codecs.decoders.size(); i++ ) { size_t len = codecs.decoders[i]->signatureLength(); maxlen = std::max(maxlen, len); } FILE* f= fopen( filename.c_str(), "rb" );//读取二进制文件 if( !f ) return ImageDecoder(); string signature(maxlen, ' '); maxlen = fread( &signature[0], 1, maxlen, f ); fclose(f); signature = signature.substr(0, maxlen); for( i = 0; i < codecs.decoders.size(); i++ ) { if( codecs.decoders[i]->checkSignature(signature) ) return codecs.decoders[i]->newDecoder(); } return ImageDecoder(); }
译码器的定义如下:
struct ImageCodecInitializer { ImageCodecInitializer() { decoders.push_back( new BmpDecoder );//Bmp译码器 encoders.push_back( new BmpEncoder );//Bmp编码器 #ifdef HAVE_JPEG decoders.push_back( new JpegDecoder );//Jpeg encoders.push_back( new JpegEncoder ); #endif decoders.push_back( new SunRasterDecoder ); encoders.push_back( new SunRasterEncoder ); decoders.push_back( new PxMDecoder );PxM encoders.push_back( new PxMEncoder ); #ifdef HAVE_TIFF decoders.push_back( new TiffDecoder );//Tiff #endif encoders.push_back( new TiffEncoder ); #ifdef HAVE_PNG decoders.push_back( new PngDecoder );//Png encoders.push_back( new PngEncoder ); #endif #ifdef HAVE_JASPER decoders.push_back( new Jpeg2KDecoder ); encoders.push_back( new Jpeg2KEncoder ); #endif #ifdef HAVE_OPENEXR decoders.push_back( new ExrDecoder ); encoders.push_back( new ExrEncoder ); #endif // because it is a generic image I/O API, supporting many formats, // it should be last in the list. #ifdef HAVE_IMAGEIO decoders.push_back( new ImageIODecoder ); encoders.push_back( new ImageIOEncoder ); #endif } vector<ImageDecoder> decoders; vector<ImageEncoder> encoders; };
②decoder->readHeader(),它是属于decoder类方法,它的作用是:根据上述译码器类型(即Jpeg译码器),对图片进行解压,并读取图片信息头。其源码如下:
bool JpegDecoder::readHeader()//Jpeg格式译码器 { bool result = false; close(); JpegState* state = new JpegState; m_state = state; state->cinfo.err = jpeg_std_error(&state->jerr.pub); state->jerr.pub.error_exit = error_exit; if( setjmp( state->jerr.setjmp_buffer ) == 0 ) { jpeg_create_decompress( &state->cinfo ); if( !m_buf.empty() ) { jpeg_buffer_src(&state->cinfo, &state->source); state->source.pub.next_input_byte = m_buf.data; state->source.pub.bytes_in_buffer = m_buf.cols*m_buf.rows*m_buf.elemSize(); } else { m_f = fopen( m_filename.c_str(), "rb" ); if( m_f ) jpeg_stdio_src( &state->cinfo, m_f ); } if (state->cinfo.src != 0) { jpeg_read_header( &state->cinfo, TRUE ); m_width = state->cinfo.image_width;//宽 m_height = state->cinfo.image_height;//高 m_type = state->cinfo.num_components > 1 ? CV_8UC3 : CV_8UC1; result = true; } } if( !result ) close(); return result; }③flags是用于判断读取图片的方式。
④hdrtype的值不是LOAD_CVMAT就是LOAD_MAT或者LOAD_IMAGE,因为IplImage、cvMat都是由cvArr派生出来的,所以hdrtype不论是LOAD_CVMAT还是LOAD_IMAGE,最终都会cvarrToMat()转换成为Mat类型。
⑤decoder->readData(),它属于decoder类方法,是用来解析图片数据的,它将解析出的数据存放于传入的参数中,其源码如下。在源码的一些函数中,用到Jpeg解压并涉及到了DCT变换的代码,到了底层是汇编代码,在memcpy.asm中(此处我们只看目的,不究过程):
bool JpegDecoder::readData( Mat& img ) { bool result = false; int step = (int)img.step; bool color = img.channels() > 1; if( m_state && m_width && m_height ) { jpeg_decompress_struct* cinfo = &((JpegState*)m_state)->cinfo; JpegErrorMgr* jerr = &((JpegState*)m_state)->jerr; JSAMPARRAY buffer = 0; if( setjmp( jerr->setjmp_buffer ) == 0 ) { /* check if this is a mjpeg image format */ if ( cinfo->ac_huff_tbl_ptrs[0] == NULL && cinfo->ac_huff_tbl_ptrs[1] == NULL && cinfo->dc_huff_tbl_ptrs[0] == NULL && cinfo->dc_huff_tbl_ptrs[1] == NULL ) { /* yes, this is a mjpeg image format, so load the correct huffman table */ my_jpeg_load_dht( cinfo, my_jpeg_odml_dht, cinfo->ac_huff_tbl_ptrs, cinfo->dc_huff_tbl_ptrs ); } if( color ) { if( cinfo->num_components != 4 ) { cinfo->out_color_space = JCS_RGB; cinfo->out_color_components = 3; } else { cinfo->out_color_space = JCS_CMYK; cinfo->out_color_components = 4; } } else { if( cinfo->num_components != 4 ) { cinfo->out_color_space = JCS_GRAYSCALE; cinfo->out_color_components = 1; } else { cinfo->out_color_space = JCS_CMYK; cinfo->out_color_components = 4; } } jpeg_start_decompress( cinfo ); buffer = (*cinfo->mem->alloc_sarray)((j_common_ptr)cinfo, JPOOL_IMAGE, m_width*4, 1 ); uchar* data = img.data; for( ; m_height--; data += step ) { jpeg_read_scanlines( cinfo, buffer, 1 ); if( color ) { if( cinfo->out_color_components == 3 ) icvCvt_RGB2BGR_8u_C3R( buffer[0], 0, data, 0, cvSize(m_width,1) ); else icvCvt_CMYK2BGR_8u_C4C3R( buffer[0], 0, data, 0, cvSize(m_width,1) ); } else { if( cinfo->out_color_components == 1 ) memcpy( data, buffer[0], m_width ); else icvCvt_CMYK2Gray_8u_C4C1R( buffer[0], 0, data, 0, cvSize(m_width,1) ); } } result = true; jpeg_finish_decompress( cinfo ); } } close(); return result; }imread_经过5个关键步骤,将读取到的图片信息头,以及图片数据以Mat类型返回(若是cvLoadImage则会被强制转换成IplImage类型)。
总结下来,方法①②,读取图像的过程都是:输入filename—>解析图片—>确定译码器—>译码函数进行信息、数据的读取—>存放于Mat容器—>返回。
2、图像的显示:
cvShowImage、imshow
方法①中的cvShowImage:
其函数原型如下,其中参数name是窗体名,image是图片。
CVAPI(void) cvShowImage( const char* name, const CvArr* image );方法②中的imshow:
其寒素原型如下,参数意义同上。
CV_EXPORTS_W void imshow(const string& winname, InputArray mat);进入到imshow源码中,可以看到注释出,调用的依然是cvShowImage函数(高版本的opencv是支持opengl的,所以调试时直接到第一个注释调用cvShowImage)。
void cv::imshow( const string& winname, InputArray _img ) { const Size size = _img.size(); #ifndef HAVE_OPENGL CV_Assert(size.width>0 && size.height>0); { Mat img = _img.getMat(); CvMat c_img = img; cvShowImage(winname.c_str(), &c_img);// } #else const double useGl = getWindowProperty(winname, WND_PROP_OPENGL); CV_Assert(size.width>0 && size.height>0); if (useGl <= 0) { Mat img = _img.getMat(); CvMat c_img = img; cvShowImage(winname.c_str(), &c_img);// } else { const double autoSize = getWindowProperty(winname, WND_PROP_AUTOSIZE); if (autoSize > 0) { resizeWindow(winname, size.width, size.height); } setOpenGlContext(winname); if (_img.kind() == _InputArray::OPENGL_TEXTURE) { cv::ogl::Texture2D& tex = wndTexs[winname]; tex = _img.getOGlTexture2D(); tex.setAutoRelease(false); setOpenGlDrawCallback(winname, glDrawTextureCallback, &tex); } else { cv::ogl::Texture2D& tex = ownWndTexs[winname]; if (_img.kind() == _InputArray::GPU_MAT) { cv::ogl::Buffer& buf = ownWndBufs[winname]; buf.copyFrom(_img); buf.setAutoRelease(false); tex.copyFrom(buf); tex.setAutoRelease(false); } else { tex.copyFrom(_img); } tex.setAutoRelease(false); setOpenGlDrawCallback(winname, glDrawTextureCallback, &tex); } updateWindow(winname); } #endif }显然,接下来应该查看cvShowImage源码,但是,好多东西都没看懂。
CV_IMPL void cvShowImage( const char* name, const CvArr* arr ) { CV_FUNCNAME( "cvShowImage" ); __BEGIN__; CvWindow* window; SIZE size = { 0, 0 }; int channels = 0; void* dst_ptr = 0; const int channels0 = 3; int origin = 0; CvMat stub, dst, *image; bool changed_size = false; // philipg if( !name )//必须要有窗体名字,否则报错! CV_ERROR( CV_StsNullPtr, "NULL name" ); window = icvFindWindowByName(name);//通过名字查找窗体句柄 if(!window)//如果没有找到,则自动创建一个同名窗体,这就是为什么在显示图片之前可以不用cvNamedWindow创建窗体的原因。 { cvNamedWindow(name, CV_WINDOW_AUTOSIZE); window = icvFindWindowByName(name); } if( !window || !arr ) EXIT; // keep silence here. if( CV_IS_IMAGE_HDR( arr )) origin = ((IplImage*)arr)->origin; CV_CALL( image = cvGetMat( arr, &stub )); #ifdef HAVE_OPENGL if (window->useGl) { cv::Mat im(image); cv::imshow(name, im); return; } #endif if (window->image)//窗体图像为空 // if there is something wrong with these system calls, we cannot display image... if (icvGetBitmapData( window, &size, &channels, &dst_ptr )) return; if( size.cx != image->width || size.cy != image->height || channels != channels0 )//如果图片的大小与窗体大小不一致 { changed_size = true;//将更改窗体标志设置为ture uchar buffer[sizeof(BITMAPINFO) + 255*sizeof(RGBQUAD)]; BITMAPINFO* binfo = (BITMAPINFO*)buffer; DeleteObject( SelectObject( window->dc, window->image )); window->image = 0; size.cx = image->width;//更改属性 size.cy = image->height; channels = channels0; FillBitmapInfo( binfo, size.cx, size.cy, channels*8, 1 );//该函数内有设置调色板的信息 window->image = SelectObject( window->dc, CreateDIBSection(window->dc, binfo, DIB_RGB_COLORS, &dst_ptr, 0, 0)); } cvInitMatHeader( &dst, size.cy, size.cx, CV_8UC3,//初始化Mat信息头 dst_ptr, (size.cx * channels + 3) & -4 ); cvConvertImage( image, &dst, origin == 0 ? CV_CVTIMG_FLIP : 0 ); // ony resize window if needed if (changed_size)//若窗体大小改变,则更新窗体 icvUpdateWindowPos(window); InvalidateRect(window->hwnd, 0, 0); // philipg: this is not needed and just slows things down // UpdateWindow(window->hwnd); __END__; }
3、等待函数
cvWaitKey、waitKey(时间单位:ms)
方法①中的cvWaitKey:
cvWaitKey函数原型如下,默认参数为0:
/* wait for key event infinitely (delay<=0) or for "delay" milliseconds */ CVAPI(int) cvWaitKey(int delay CV_DEFAULT(0));
方法②中的waitKey:
waitKey函数原型如下,默认参数为0:
CV_EXPORTS_W int waitKey(int delay = 0);
waitKey()源码如下:
int cv::waitKey(int delay) { return cvWaitKey(delay); }那么将重心转移到cvWaitKey来看,其源码如下,它用到了windows编程中的事件及消息机制,不太懂:
CV_IMPL int cvWaitKey( int delay ) { int time0 = GetTickCount(); for(;;)//死循环 { CvWindow* window; MSG message; int is_processed = 0; if( (delay > 0 && abs((int)(GetTickCount() - time0)) >= delay) || hg_windows == 0 ) return -1; if( delay <= 0 ) GetMessage(&message, 0, 0, 0); else if( PeekMessage(&message, 0, 0, 0, PM_REMOVE) == FALSE ) { Sleep(1);//延时1ms continue; } for( window = hg_windows; window != 0 && is_processed == 0; window = window->next ) { if( window->hwnd == message.hwnd || window->frame == message.hwnd ) { is_processed = 1; switch(message.message) { case WM_DESTROY: case WM_CHAR: DispatchMessage(&message);//推送消息事件 return (int)message.wParam;//返回触发按键的ASIIC码 case WM_SYSKEYDOWN: if( message.wParam == VK_F10 ) { is_processed = 1; return (int)(message.wParam << 16); } break; case WM_KEYDOWN: TranslateMessage(&message);//破译消息 if( (message.wParam >= VK_F1 && message.wParam <= VK_F24) || message.wParam == VK_HOME || message.wParam == VK_END || message.wParam == VK_UP || message.wParam == VK_DOWN || message.wParam == VK_LEFT || message.wParam == VK_RIGHT || message.wParam == VK_INSERT || message.wParam == VK_DELETE || message.wParam == VK_PRIOR || message.wParam == VK_NEXT ) { DispatchMessage(&message); is_processed = 1; return (int)(message.wParam << 16); } default: DispatchMessage(&message); is_processed = 1; break; } } } if( !is_processed ) { TranslateMessage(&message); DispatchMessage(&message); } } }