opencv的点点滴滴

Author Avatar
Magicmanoooo 3月 09, 2019
  • 在其它设备中阅读本文章

Ptr源码分析
Ptr是一个智能指针,用于动态分配objects。这是一个template pointer-wrapping class,用于存储引用计算(reference counter)以及object pointer。它类似于C++中的智能指针,但它更加简易。可以这样使用:Ptr<MyObjectType> ptr

template<typename _Tp> class CV_EXPORTS Ptr
{
public:
    //! empty constructor
    Ptr();
    //! take ownership of the pointer. The associated reference counter is allocated and set to 1
    Ptr(_Tp* _obj);
    //! calls release()
    ~Ptr();
    //! copy constructor. Copies the members and calls addref()
    Ptr(const Ptr& ptr);
    //! copy operator. Calls ptr.addref() and release() before copying the members
    Ptr& operator = (const Ptr& ptr);
    //! increments the reference counter
    void addref();
    //! decrements the reference counter. If it reaches 0, delete_obj() is called
    void release();
    //! deletes the object. Override if needed
    void delete_obj();
    //! returns true iff obj==NULL
    bool empty() const;


    //! helper operators making "Ptr<T> ptr" use very similar to "T* ptr".
    _Tp* operator -> ();
    const _Tp* operator -> () const;

    operator _Tp* ();
    operator const _Tp*() const;

protected:
    _Tp* obj; //< the object pointer.
    int* refcount; //< the associated reference counter
};

所谓的智能指针,其实就是模板参数可以是任意的C++ clas,但考虑到对象在使用完毕的时候,需要进行析构,因此需要特化delete_obj()函数。

Ptr的构造函数:

// obj是指向对象的指针,refcount是引用计数,默认ctor只是构造了一个空对象
// 带参数的ctor则使用了动态分配内存的技术。其中,fastMalloc这个函数使用了pointer alignment技术
template<typename _Tp> inline Ptr<_Tp>::Ptr() : obj(0), refcount(0) {}
template<typename _Tp> inline Ptr<_Tp>::Ptr(_Tp* _obj) : obj(_obj)
{
    if(obj)
    {
        refcount = (int*)fastMalloc(sizeof(*refcount));
        *refcount = 1;
    }
    else
        refcount = 0;
}

 // 对于拷贝ctor,参数对应的对象不需要进行释放,直接进行拷贝即可,所以引用计数加1。
 // 赋值operator则需要先释放掉参数对应的对象,因为将当前对象赋值给另一个对象之后,
 // 当前指针就不再指向任何对象
template<typename _Tp> inline Ptr<_Tp>::Ptr(const Ptr<_Tp>& ptr) {
    obj = ptr.obj;
    refcount = ptr.refcount;
    addref();
}

template<typename _Tp> inline Ptr<_Tp>& Ptr<_Tp>::operator = (const Ptr<_Tp>& ptr) {
    int* _refcount = ptr.refcount;
    if( _refcount )
        CV_XADD(_refcount, 1);
    release();
    obj = ptr.obj;
    refcount = _refcount;
    return *this;
}

fastMalloc()的实现:

void* fastMalloc( size_t size )
{
    // 在此处,内存多开辟了20字节的空间。其中,前4个字节用以存储分配的这块空间的首地址。
    // 在进行释放空间时,可以很快地进行释放。另外的16字节用于调整地址,是地址到达16的倍数
    // 其中,CV_MALLOC_ALIGN的值为16
    uchar* udata = (uchar*)malloc(size + sizeof(void*) + CV_MALLOC_ALIGN);
    if(!udata)
        return OutOfMemoryError(size);
    // alignPtr用于将地址空间的大小向上调整为16的倍数
    // udata+1  ===> 其实是 udata + sizeof(uchar**) ,即4,这4个字节便是用于存储udata这个地址空间的起始地址
    // 要将udata强制转换为uchar**的原因是:因为要访问者4个字节之中的内容,而这个内容中存储的又是首地址,于是就是二级指针 
    uchar** adata = alignPtr((uchar**)udata + 1, CV_MALLOC_ALIGN);
    // adata[-1] ===> -1就是向前移动4个字节,既是udata这个地址空间的起始地址
    adata[-1] = udata;
    return adata;
}

void fastFree(void* ptr)
{
    if(ptr) {
    // 取分配地址的首地址
        uchar* udata = ((uchar**)ptr)[-1];
        CV_DbgAssert(udata < (uchar*)ptr &&
               ((uchar*)ptr - udata) <= (ptrdiff_t)(sizeof(void*)+CV_MALLOC_ALIGN)); 
    // 直接删除首地址即可删除整个分配的空间
        free(udata);
    }
}

alignPtr()的实现:

template<typename _Tp> static inline _Tp* alignPtr(_Tp* ptr, int n=(int)sizeof(_Tp))
{
    return (_Tp*)(((size_t)ptr + n-1) & -n);
}


此处使用了位操作的黑魔法,
这个函数用来将ptr指向的地址对齐到n边界,使得到的地址是n的倍数,由于此处n=CV_MALLOC_ALIGN=16

现假设ptr为十六进制形式,ptr=0xYY YY YY YY,则ptr+n-1的计算为:


ptr的低4位不全为0时,ptr+n-1的结果将导致进位,加1。再与-n&,而-n==0xF0,所以最终返回的地址形式为0x YY YY YY Y0,即为CV_MALLOC_ALIGN==16的倍数。

增加引用计数时,使用了无锁(lock-free)编程技术:

template<typename _Tp> inline void Ptr<_Tp>::addref() { 
if( refcount ) 
    CV_XADD(refcount, 1); 
}

CV_XADD该操作类似于post increment的运算,使用了无锁化编程,其可以用于多线程编程,用户不需要自己维护锁。

接下来是释放内存的操作:

template<typename _Tp> inline void Ptr<_Tp>::release()
{
    if( refcount && CV_XADD(refcount, -1) == 1 )
    {
        delete_obj();
        fastFree(refcount);
    }
    refcount = 0;
    obj = 0;
}

template<typename _Tp> inline void Ptr<_Tp>::delete_obj()
{
    if( obj ) delete obj;
}

template<typename _Tp> inline Ptr<_Tp>::~Ptr() { release(); }

其他操作:

template<typename _Tp> inline _Tp* Ptr<_Tp>::operator -> () { return obj; }
template<typename _Tp> inline const _Tp* Ptr<_Tp>::operator -> () const { return obj; }

template<typename _Tp> inline Ptr<_Tp>::operator _Tp* () { return obj; }
template<typename _Tp> inline Ptr<_Tp>::operator const _Tp*() const { return obj; }

什么是对齐(alignment)?

在现代的计算机中,内存空间都是按照byte进行划分的,从理论上讲,对任何类型的变量的访问,都可以从任何地址开始,但实际情况是:在访问特定变量的时候,经常需要在特定的内存地址进行访问,这就是对齐。

需要对齐的原因:

  1. 某些硬件平台只能在某些地址取某些特定类型的数据,否则抛出硬件异常。
  2. 其余的硬件平台虽然可以在任何地址处,取得任何类型的数据。但如果变量没有对齐的情况下,取这个数据可能会存在效率上的损失。

许多硬件相关的东西在对齐上存在限制。在某些系统中,某种数据类型只能存储在偶数边界的地址处。

例如,在经典的SPARC架构(以及经典的ARM)上,便不能从奇数地址读取一个超过1字节的整型数据。尝试这么做将会立即终止程序,并伴随着总线(bus)错误。而在X86架构上,CPU硬件处理了这个问题,只是这么做将会花费更多时间,而RISC架构通常是不会为用户做这些。

mixChannels()

将输入数组的指定通道复制到输出数组的指定通道

void mixChannels(
const Mat* src,     //输入数组或向量矩阵,所有矩阵的大小和深度必须相同。
size_t nsrcs,          //矩阵的数量
Mat* dst,              //输出数组或矩阵向量,大小和深度必须与src[0]相同
size_t ndsts,        //矩阵的数量
const int* fromTo,//指定被复制通道与要复制到的位置组成的索引对
size_t npairs       //fromTo中索引对的数目
);

例如,将一个4通道BGRA图像分割成一个3通道BGR和一个单独的alpha通道图像:

#include<opencv2/opencv.hpp>
using namespace cv;

int main()
{
    Mat bgra( 500, 500, CV_8UC4, Scalar(255,255,0,255) );
    Mat bgr( bgra.rows, bgra.cols, CV_8UC3 );
    Mat alpha( bgra.rows, bgra.cols, CV_8UC1 );

    Mat out[] = { bgr, alpha };

    int from_to[] = { 0, 2, 1, 1, 2, 0, 3, 3 };
    mixChannels( &bgra, 1, out, 2, from_to, 4 );

    imshow("bgra", bgra);
    imshow("bgr", bgr);
    waitKey(0);
    return 0;
}

其中,索引对from_to[] = { 0, 2, 1, 1, 2, 0, 3, 3 }的含义为:

  • bgra0通道复制到out[]2通道,即bgr0通道
  • bgra1通道复制到out[]1通道,即bgr1通道
  • bgra2通道复制到out[]0通道,即bgr2通道
  • bgra3通道复制到out[]3通道,即alpha通道

因此,原图的青色(255,255,0)通过指定通道复制到输出图像中变成了黄色(0,255,255)

calcHist

void calcHist(
    const Mat* images,         //输入的图像的指针,可以是多幅图像,所有的图像必须有同样的深度(CV_8U or CV_32F)。
    //同时一副图像可以有多个channels。
    int nimages,                  // 输入图像的数量
    const int* channels,      //用来计算直方图的channels的数组。
//比如输入是两副图像,第一副图像有0,1,2共三个channel
//第二幅图像只有一个channel:0,那么输入就一共有4个channels
//如果int channels[3] = {3, 2, 0},那么就表示是使用第二副图像的第一个通道和第一副图像的第2和第0个通道来计算直方图。
    InputArray mask, 
    OutputArray hist,   //计算出来的直方图
    int dims,                   //计算出来的直方图的维数
    const int* histSize, //在每一维上直方图的个数。如果简单地把直方图看作一个一个的竖条的话,就是每一维上竖条的个数
    const float** ranges, 
//用来进行统计的范围。比如
//            float rang1[] = {0, 20};
//           float rang2[] = {30, 40};
//        const float *rangs[] = {rang1, rang2};
//那么就是对0—20和30—40范围的值进行统计。
    bool uniform=true,    // 一个竖条的宽度是否相等
    bool accumulate=false 
)

findContours()

其用于寻找图像中物体的轮廓(一般结合drawContours()函数将找到的轮廓绘制出)。

void cv::findContours   (   
// 输入图像,图像必须为8-bit单通道图像,图像中的非零像素将被视为1,0像素保留其像素值,
//故加载图像后会自动转换为二值图像。同样可以使用cv::compare,cv::inRange,cv::threshold,
//cv::adaptiveThreshold,cv::Canny等函数来创建二值图像。如果第四个参数为cv::RETR_CCOMP或
//cv::RETR_FLOODFILL,输入图像可以是32-bit整型图像(CV_32SC1) 
InputOutputArray    image,
OutputArrayOfArrays     contours,
OutputArray     hier
int     mode,
int     method,
Point   offset = Point() 
 )   
  • 第一个参数:image,单通道(8-bit)图像矩阵,可以是灰度图,但更常用的是二值图像,一般是经过CannyLaplcian等边缘检测算子处理过的二值图像
  • 第二个参数:contours,定义为vector<vector<Point>> contours,是一个双重向量。向量内每个元素保存了一组由连续的Point点构成的点的集合的向量,每一组Point点集就是一个轮廓。有多少轮廓,向量contours就有多少元素。
  • 第三个参数:hierarchy,定义为vector<Vec4i> hierarchy。向量hiararchy内的元素和轮廓向量contours内的元素是一一对应的,向量的容量相同。hierarchy向量内每一个元素的4int型变量—hierarchy[i][0] ~ hierarchy[i][3],分别表示第i个轮廓的后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号。如果当前轮廓没有对应的后一个轮廓、前一个轮廓、父轮廓或内嵌轮廓的话,则hierarchy[i][0] ~ hierarchy[i][3]的相应位被设置为默认值-1
  • 第四个参数:mode,定义轮廓的检索模式:
    • CV_RETR_EXTERNAL:只检测最外围轮廓,包含在外围轮廓内的内围轮廓被忽略
    • CV_RETR_LIST:检测所有的轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关系,彼此之间独立,没有等级关系,这就意味着这个检索模式下不存在父轮廓或内嵌轮廓,所以hierarchy向量内所有元素的第3、第4个分量都会被置为-1
    • CV_RETR_CCOMP:检测所有的轮廓,但所有轮廓只建立两个等级关系,外围为顶层,若外围内的内围轮廓还包含了其他的轮廓信息,则内围内的所有轮廓均归属于顶层
    • CV_RETR_TREE:检测所有轮廓,所有轮廓建立一个等级树结构。外层轮廓包含内层轮廓,内层轮廓还可以继续包含内嵌轮廓。
  • 第五个参数:method,定义轮廓的近似方法:
    • CV_CHAIN_APPROX_NONE:获取每个轮廓的每个像素,相邻的两个点的像素位置差不超过1
    • CV_CHAIN_APPROX_SIMPLE:仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入contours向量内,拐点与拐点之间直线段上的信息点不予保留
    • CV_CHAIN_APPROX_TC89_L1CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain近似算法
  • 第六个参数:offset,所有的轮廓信息相对于原始图像对应点的偏移量,相当于在每一个检测出的轮廓点上加上该偏移量

drawContours

void cv::drawContours   (   
InputOutputArray    image,
InputArrayOfArrays  contours, // 使用findContours检测到的轮廓数据,每个轮廓以点向量的形式存储,Point类型的vector 
int     contourIdx,
const Scalar &  color,    //轮廓的颜色
int     thickness = 1,       //绘制轮廓所用线条粗细度,如果值为负值,则在轮廓内部绘制
int     lineType = LINE_8,//线条类型,默认值为LINE_8
InputArray  hierarchy = noArray(),//可选层次结构信息,可传入vector<Vec4i> hierarchy;
int     maxLevel = INT_MAX,
Point   offset = Point() 
)

createTrackbar()

在显示图像的窗口中快速创建一个滑动控件,用于手动调节阈值。

CV_EXPORTS int createTrackbar(
const string& trackbarname,  //滑动控件的名称
const string& winname,    // 滑动控件所在的图像窗口的名称
int* value,     // 初始化的阈值
int count,//滑动控件的刻度范围
TrackbarCallback onChange = 0,
//TrackbarCallback是回调函数,一般将回调函数定义为:void thresh_callback(int, void*)
void* userdata = 0    
);

其中,TrackbarCallback的定义为:

typedef void (CV_CDECL *TrackbarCallback)(int pos, void* userdata);

approxPolyDP()

它的主要功能是:把一个连续光滑曲线折线化,对图像轮廓点进行多边形拟合

函数原型为:

void approxPolyDP(
InputArray curve,  //一般是由图像的轮廓点组成的点集
OutputArray approxCurve, //表示输出的多边形点集
double epsilon, //要表示输出的精度,就是另个轮廓点之间最大距离数,5,6,7,8...
bool closed //表示输出的多边形是否封闭
)

原理图(对比之前黑点连线,之后蓝色连线):

算法描述如下:
起始曲线是有序的一组点或线,距离维度ε>0该算法递归地划分线

  • 最初,给出了第一点和最后一点之间的所有点,它会自动标记要保存的第一个和最后一个点。
  • 然后,找到距离第一点和最后一点组成的线段的最远的点作为终点; 这一点在距离终点之间的近似线段的曲线上显然最远。 如果该点比线段更接近于ε,那么当前未被标记的任何点将被保存,而没有简化的曲线比ε更差的可以丢弃。

如果离线段最远的点距离近似值大于ε,则必须保留该点。该算法以第一个点和最远点递归地调用自身,然后以最远点和最后一个点(包括最远点被标记为保留)递归调用自身。
当递归完成时,可以生成一个新的输出曲线,其中包括所有且仅标记为保留的点。

伪代码:

function DouglasPeucker(PointList[], epsilon)
    // Find the point with the maximum distance
    dmax = 0
    index = 0
    end = length(PointList)
    for i = 2 to ( end - 1) {
        d = perpendicularDistance(PointList[i], Line(PointList[1], PointList[end])) 
        if ( d > dmax ) {
            index = i
            dmax = d
        }
    }
    // If max distance is greater than epsilon, recursively simplify
    if ( dmax > epsilon ) {
        // Recursive call
        recResults1[] = DouglasPeucker(PointList[1...index], epsilon)
        recResults2[] = DouglasPeucker(PointList[index...end], epsilon)

        // Build the result list
        ResultList[] = {recResults1[1...length(recResults1)-1], recResults2[1...length(recResults2)]}
    } else {
        ResultList[] = {PointList[1], PointList[end]}
    }
    // Return the result
    return ResultList[]
end

boundingRect()

当得到对象轮廓后,可用

  • Rect boundingRect(InputArray points)得到包覆此轮廓的最小正矩形
  • RotatedRect minAreaRect(InputArray points)得到包覆轮廓的最小斜矩形
  • void minEnclosingCircle(InputArray points, Point2f& center, float& radius)得到包覆此轮廓的最小圆形。其中,points为输入的二维点集,center为输出的圆形的中心坐标,radius为输出的最小圆的半径。

这些函数可以填补空隙,或者作进一步的对象辨识。boundingRect()返回正矩形,所以如果对象有倾斜的情形,返回的可能不是想要的结果。

图像通道的分离与合并

在图像处理中,尤其是处理多通道图像时,有时需要对各个通道进行分离,分别处理;有时还需要对分离处理后的各个通道进行合并,重新合并成一个多通道的图像。

cv::split()

void cv::split(
    const cv::Mat& mtx, //输入图像
    vector<Mat>& mv // 输出的多通道序列(n个单通道序列)
);

cv::merge()

void merge(
    const vector<cv::Mat>& mv, // 输入的多通道序列(n个单通道序列)
    cv::OutputArray dst // 输出图像,包含mv
);

图像翻转

void cv::flip(
        cv::InputArray src, // 输入图像
        cv::OutputArray dst, // 输出
        int flipCode = 0 // >0: 沿y-轴翻转, 0: 沿x-轴翻转, <0: x、y轴同时翻转
    );
Mat matRotateClockWise90(Mat src)
{
    if (src.empty())
    {
        qDebug()<<"RorateMat src is empty!";
    }
    // 矩阵转置
    transpose(src, src);
    //0: 沿X轴翻转; >0: 沿Y轴翻转; <0: 沿X轴和Y轴翻转
    flip(src, src, 1);// 翻转模式,flipCode == 0垂直翻转(沿X轴翻转),flipCode>0水平翻转(沿Y轴翻转),flipCode<0水平垂直翻转(先沿X轴翻转,再沿Y轴翻转,等价于旋转180°)
    return src;
}

Mat matRotateClockWise180(Mat src)//顺时针180
{
    if (src.empty())
    {
        qDebug() << "RorateMat src is empty!";
    }

    //0: 沿X轴翻转; >0: 沿Y轴翻转; <0: 沿X轴和Y轴翻转
    flip(src, src, 0);// 翻转模式,flipCode == 0垂直翻转(沿X轴翻转),flipCode>0水平翻转(沿Y轴翻转),flipCode<0水平垂直翻转(先沿X轴翻转,再沿Y轴翻转,等价于旋转180°)
    flip(src, src, 1);
    return src;
    //transpose(src, src);// 矩阵转置
}

Mat matRotateClockWise270(Mat src)//顺时针270
{
    if (src.empty())
    {
        qDebug() << "RorateMat src is empty!";
    }
    // 矩阵转置
    //transpose(src, src);
    //0: 沿X轴翻转; >0: 沿Y轴翻转; <0: 沿X轴和Y轴翻转
    transpose(src, src);// 翻转模式,flipCode == 0垂直翻转(沿X轴翻转),flipCode>0水平翻转(沿Y轴翻转),flipCode<0水平垂直翻转(先沿X轴翻转,再沿Y轴翻转,等价于旋转180°)
    flip(src, src, 0);
    return src;
}

Mat myRotateAntiClockWise90(Mat src)//逆时针90°
{
    if (src.empty())
    {
      qDebug()<<"mat is empty!";
    }
    transpose(src, src);
    flip(src, src, 0);

Mat<<操作

// 创建 3 x 3 双精度恒等矩阵
Mat M = (Mat_ <double> (3,3) <<1,0,0,0,1,0,0,0,1);

使用此方法,首先调用具有适当的参数的 Mat_ 类构造函数,然后只需要把 << 运算符后面的值用逗号分隔,这些值可以是常量、变量、 表达式,等等。注意:所需的额外的圆括号 ((Mat_<double>(3,3)<<1,0,0,0,1,0,0,0,1)) 以免出现编译错误。

CNN 模块

1. 读取 .protxt 文件和 .caffemodel 文件

cv::dnn::Net net = cv::dnn::readNetFromCaffe(modelTxt, modelBin);

2. 检查网络是否读取成功

if (net.empty()) {
    std::cerr << "Can't load network by using the following files: " << std::endl;
    std::cerr << "prototxt:   " << modelTxt << std::endl;
    std::cerr << "caffemodel: " << modelBin << std::endl;
    std::cerr << "bvlc_googlenet.caffemodel can be downloaded here:" << std::endl;
    std::cerr << "http://dl.caffe.berkeleyvision.org/bvlc_googlenet.caffemodel" << std::endl;
    exit(-1);
}

3. blobFromImage


Mat cv::dnn::blobFromImage(    
    InputArray         image,
    double             scalefactor = 1.0,
    const Size &     size = Size(),
    const Scalar &     mean = Scalar(),
    bool             swapRB = false,
    bool             crop = false,
    int             ddepth = CV_32F 
)    

从图像创建一个 4 维 blob。 (可选)从中心调整大小并裁剪 image,减去 mean 值,通过 scalefactor 缩放值,交换蓝色和红色通道。构造 blob,为传入网络做准备,图片不能直接进入网络。

参数:

  • image:input image (with 1-, 3- or 4-channels)
  • scalefactor:multiplier for image values(缩放因子)
  • size:spatial size for output image(输出图像的尺寸)
  • mean:scalar with mean values which are subtracted from channels. Values are intended to be in (mean-R, mean-G, mean-B) order if image has BGR ordering and swapRB is true
  • swapRB:flag which indicates that swap first and last channels in 3-channel image is necessary
  • crop:flag which indicates whether image will be cropped after resize or not
  • ddepth:Depth of output blob. Choose CV_32F or CV_8U

4. setInput

void cv::dnn::Net::setInput    (    
    InputArray                 blob,
    const         String &     name = "",
    double                     scalefactor = 1.0,
    const         Scalar &     mean = Scalar() 
)    

设置网络的新输入值。即将构建的 blob 传入网络 data 层

参数:

  • blob:A new blob. Should have CV_32F or CV_8U depth.
  • name:A name of input layer.
  • scalefactor:An optional normalization scale.
  • mean:An optional mean subtraction values.

5. forward

Mat cv::dnn::Net::forward (    
    const String &     outputName = String()    
)    

进行前向传递,计算名称为 outputName 的图层输出(Runs forward pass to compute output of layer with name outputName,即前向预测)。

返回值:blob for first output of specified layer.