【opencv】opencv源码分析(一):imread、cvLoadImage、waitKey、imshow函数

时间:2022-07-22 17:19:05

简介:

本文就opencv中的几个常用函数:imreadcvLoadImagewaitKeyimshow,进行简单的源码分析,并对新、老版本进行比较。

实验平台:

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、图像的读取

cvLoadImageimread

方法中的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,因为IplImagecvMat都是由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源码,但是,好多东西都没看懂【opencv】opencv源码分析(一):imread、cvLoadImage、waitKey、imshow函数
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);
        }
    }
}