copyMakeBorder 将源图像复制到目标图像的中间并在图像周围形成边框。 当src已经在dst中间时,该函数不会复制src本身,而只是构造边框。在执行时函数会尝试使用 ROI 之外的像素来形成边界。若要禁用此功能并始终执行外推,就像src不是 ROI 一样,需要设置borderType | BORDER_ISOLATED。copyMakeBorder 是 OpenCV 中较为简单的一个函数,然而由于其浅拷贝和 ROI 机制的存在,实现也并不简单。

cv::copyMakeBorder

python 将opencv图像拷贝到另一个区域 opencv copymakeborder_scala

cv::copyMakeBorder 在调用时分为4种情况:

src与dst尺寸相等且同源,直接返回;

src与dst尺寸相等且不同源则调用 Mat::copyTo;

边界类型需要插值则调用 copyMakeBorder_8u;

边界为常量则调用 copyMakeConstBorder_8u。

CV_INSTRUMENT_REGION() 属于 OpenCV 性能测试系统的实现工具框架。 首先 CV_Assert 检查输入参数是否合法。CV_OCL_RUN 在第一个参数满足条件时调用 OpenCL 的实现。_InputArray::getMat

CV_INSTRUMENT_REGION();

    CV_Assert( top >= 0 && bottom >= 0 && left >= 0 && right >= 0 && _src.dims() <= 2);

    CV_OCL_RUN(_dst.isUMat(),
               ocl_copyMakeBorder(_src, _dst, top, bottom, left, right, borderType, value))

    Mat src = _src.getMat();
    int type = src.type();

Mat::isSubmatrix 判断矩阵的属性是否为子矩阵。Mat::locateROI 获取整个矩阵大小及子矩阵偏移。 如果源是子矩阵,并且边界格式未设置BORDER_ISOLATED,则将src向外扩。_OutputArray::create 根据输入的类型调用相应的创建函数。后者仅在必要的情况下才开辟内存。 如果源与目的地址不一致或者步长不等则调用 Mat::copyTo。

if( src.isSubmatrix() && (borderType & BORDER_ISOLATED) == 0 )
    {
        Size wholeSize;
        Point ofs;
        src.locateROI(wholeSize, ofs);
        int dtop = std::min(ofs.y, top);
        int dbottom = std::min(wholeSize.height - src.rows - ofs.y, bottom);
        int dleft = std::min(ofs.x, left);
        int dright = std::min(wholeSize.width - src.cols - ofs.x, right);
        src.adjustROI(dtop, dbottom, dleft, dright);
        top -= dtop;
        left -= dleft;
        bottom -= dbottom;
        right -= dright;
    }

    _dst.create( src.rows + top + bottom, src.cols + left + right, type );
    Mat dst = _dst.getMat();

    if(top == 0 && left == 0 && bottom == 0 && right == 0)
    {
        if(src.data != dst.data || src.step != dst.step)
            src.copyTo(dst);
        return;
    }

    borderType &= ~BORDER_ISOLATED;

CV_IPP_RUN_FAST 尝试调用 IPP 的实现。 如果不是常量填充则调用 copyMakeBorder_8u,否则调用 copyMakeConstBorder_8u。scalarToRawData 将标量值赋到原始数据。

CV_IPP_RUN_FAST(ipp_copyMakeBorder(src, dst, top, bottom, left, right, borderType, value))

    if( borderType != BORDER_CONSTANT )
        copyMakeBorder_8u( src.ptr(), src.step, src.size(),
                           dst.ptr(), dst.step, dst.size(),
                           top, left, (int)src.elemSize(), borderType );
    else
    {
        int cn = src.channels(), cn1 = cn;
        AutoBuffer<double> buf(cn);
        if( cn > 4 )
        {
            CV_Assert( value[0] == value[1] && value[0] == value[2] && value[0] == value[3] );
            cn1 = 1;
        }
        scalarToRawData(value, buf.data(), CV_MAKETYPE(src.depth(), cn1), cn);
        copyMakeConstBorder_8u( src.ptr(), src.step, src.size(),
                                dst.ptr(), dst.step, dst.size(),
                                top, left, (int)src.elemSize(), (uchar*)buf.data() );
    }

scalarToRawData

调用 scalarToRawData_ 将标量值赋到原始数据。

CV_INSTRUMENT_REGION();

    const int depth = CV_MAT_DEPTH(type), cn = CV_MAT_CN(type);
    CV_Assert(cn <= 4);
    switch(depth)
    {
    case CV_8U:
        scalarToRawData_<uchar>(s, (uchar*)_buf, cn, unroll_to);
        break;
    case CV_8S:
        scalarToRawData_<schar>(s, (schar*)_buf, cn, unroll_to);
        break;
    case CV_16U:
        scalarToRawData_<ushort>(s, (ushort*)_buf, cn, unroll_to);
        break;
    case CV_16S:
        scalarToRawData_<short>(s, (short*)_buf, cn, unroll_to);
        break;
    case CV_32S:
        scalarToRawData_<int>(s, (int*)_buf, cn, unroll_to);
        break;
    case CV_32F:
        scalarToRawData_<float>(s, (float*)_buf, cn, unroll_to);
        break;
    case CV_64F:
        scalarToRawData_<double>(s, (double*)_buf, cn, unroll_to);
        break;
    case CV_16F:
        scalarToRawData_<float16_t>(s, (float16_t*)_buf, cn, unroll_to);
        break;
    default:
        CV_Error(CV_StsUnsupportedFormat,"");
    }

scalarToRawData_

int i = 0;
    for(; i < cn; i++)
        buf[i] = saturate_cast<T>(s.val[i]);
    for(; i < unroll_to; i++)
        buf[i] = buf[i-cn];

copyMakeBorder_8u

检查数据指针及步长是否满足整型对齐,如果满足则按intMode执行。

const int isz = (int)sizeof(int);
    int i, j, k, elemSize = 1;
    bool intMode = false;

    if( (cn | srcstep | dststep | (size_t)src | (size_t)dst) % isz == 0 )
    {
        cn /= isz;
        elemSize = isz;
        intMode = true;
    }

AutoBuffer 为函数的临时缓冲区。_tab对应一行中边界元素的数量。borderInterpolate 计算插值所用元素索引。

cv::AutoBuffer<int> _tab((dstroi.width - srcroi.width)*cn);
    int* tab = _tab.data();
    int right = dstroi.width - srcroi.width - left;
    int bottom = dstroi.height - srcroi.height - top;

    for( i = 0; i < left; i++ )
    {
        j = cv::borderInterpolate(i - left, srcroi.width, borderType)*cn;
        for( k = 0; k < cn; k++ )
            tab[i*cn + k] = j + k;
    }

    for( i = 0; i < right; i++ )
    {
        j = cv::borderInterpolate(srcroi.width + i, srcroi.width, borderType)*cn;
        for( k = 0; k < cn; k++ )
            tab[(i+left)*cn + k] = j + k;
    }

dstInner定位到目的矩阵中与对应源矩阵原点的位置:

  • 逐行处理,如果地址不相同则拷贝;
  • 逐个拷贝左右边界元素。为什么不整体拷贝两段呢?
srcroi.width *= cn;
    dstroi.width *= cn;
    left *= cn;
    right *= cn;

    uchar* dstInner = dst + dststep*top + left*elemSize;

    for( i = 0; i < srcroi.height; i++, dstInner += dststep, src += srcstep )
    {
        if( dstInner != src )
            memcpy(dstInner, src, srcroi.width*elemSize);

        if( intMode )
        {
            const int* isrc = (int*)src;
            int* idstInner = (int*)dstInner;
            for( j = 0; j < left; j++ )
                idstInner[j - left] = isrc[tab[j]];
            for( j = 0; j < right; j++ )
                idstInner[j + srcroi.width] = isrc[tab[j + left]];
        }
        else
        {
            for( j = 0; j < left; j++ )
                dstInner[j - left] = src[tab[j]];
            for( j = 0; j < right; j++ )
                dstInner[j + srcroi.width] = src[tab[j + left]];
        }
    }

处理顶部和低部的行。

dstroi.width *= elemSize;
    dst += dststep*top;

    for( i = 0; i < top; i++ )
    {
        j = cv::borderInterpolate(i - top, srcroi.height, borderType);
        memcpy(dst + (i - top)*dststep, dst + j*dststep, dstroi.width);
    }

    for( i = 0; i < bottom; i++ )
    {
        j = cv::borderInterpolate(i + srcroi.height, srcroi.height, borderType);
        memcpy(dst + (i + srcroi.height)*dststep, dst + j*dststep, dstroi.width);
    }

borderInterpolate

CV_TRACE_FUNCTION_VERBOSE 为追踪日志函数。p为目的点在源矩阵坐标系上的位置,所以会有负值。len标识源矩阵的行长度。 函数内仅处理 python 将opencv图像拷贝到另一个区域 opencv copymakeborder_OpenCV_02

  • BORDER_REPLICATE仅重复首尾元素即可;
  • BORDER_REFLECT和BORDER_REFLECT_101为镜像;
  • BORDER_WRAP为平铺效果。
CV_TRACE_FUNCTION_VERBOSE();

    CV_DbgAssert(len > 0);

#ifdef CV_STATIC_ANALYSIS
    if(p >= 0 && p < len)
#else
    if( (unsigned)p < (unsigned)len )
#endif
        ;
    else if( borderType == BORDER_REPLICATE )
        p = p < 0 ? 0 : len - 1;
    else if( borderType == BORDER_REFLECT || borderType == BORDER_REFLECT_101 )
    {
        int delta = borderType == BORDER_REFLECT_101;
        if( len == 1 )
            return 0;
        do
        {
            if( p < 0 )
                p = -p - 1 + delta;
            else
                p = len - 1 - (p - len) - delta;
        }
#ifdef CV_STATIC_ANALYSIS
        while(p < 0 || p >= len);
#else
        while( (unsigned)p >= (unsigned)len );
#endif
    }
    else if( borderType == BORDER_WRAP )
    {
        CV_Assert(len > 0);
        if( p < 0 )
            p -= ((p-len+1)/len)*len;
        if( p >= len )
            p %= len;
    }
    else if( borderType == BORDER_CONSTANT )
        p = -1;
    else
        CV_Error( CV_StsBadArg, "Unknown/unsupported border type" );
    return p;

copyMakeConstBorder_8u

按照目的行跨度准备一个_constBuf,以value填充。

int i, j;
    cv::AutoBuffer<uchar> _constBuf(dstroi.width*cn);
    uchar* constBuf = _constBuf.data();
    int right = dstroi.width - srcroi.width - left;
    int bottom = dstroi.height - srcroi.height - top;

    for( i = 0; i < dstroi.width; i++ )
    {
        for( j = 0; j < cn; j++ )
            constBuf[i*cn + j] = value[j];
    }

遍历源兴趣区域的每一行,如果和目的兴趣区域地址不同则复制。 左右填充,最后上下填充。

srcroi.width *= cn;
    dstroi.width *= cn;
    left *= cn;
    right *= cn;

    uchar* dstInner = dst + dststep*top + left;

    for( i = 0; i < srcroi.height; i++, dstInner += dststep, src += srcstep )
    {
        if( dstInner != src )
            memcpy( dstInner, src, srcroi.width );
        memcpy( dstInner - left, constBuf, left );
        memcpy( dstInner + srcroi.width, constBuf, right );
    }

    dst += dststep*top;

    for( i = 0; i < top; i++ )
        memcpy(dst + (i - top)*dststep, constBuf, dstroi.width);

    for( i = 0; i < bottom; i++ )
        memcpy(dst + (i + srcroi.height)*dststep, constBuf, dstroi.width);