OpenCV 基本操作

创建 cv::Mat

OpenCV 中用数据格式 cv::Mat 存储图片。

有以下几种生成 cv::Mat 的方式:

直接或间接指定图片的行、列维度
cv::Mat myMat(240, 320, CV_8U, cv::Scalar(255)); // 240 行 × 320 列,每个元素的类型为 8 位无符号整型,单通道,元素初值为 255

// 也可以将 Scalar(255) 简写成 255,但是注意 Scalar(0) 不能直接写成 0 
cv::Mat myMat(240, 320, CV_8U, 255); 

// 图片大小也可以用 Size() 的格式送入
cv::Mat myMat(cv::Size(320 240), CV_8U, cv::Scalar(255)); // 以 (width, height) 的形式送入,Size(320, 240) 就是宽320,高 240,对应了 240 行,320 列。

// 也可以通过 size() 函数得到某个图片的大小,然后构造同样大小的图片
cv::Mat myMat(image1.size(), CV_8U, cv::Scalar(255));

OpenCV 有一套自己的数据类型名称,与标准的 C++ 数据类型对应关系如下:

  • CV_8U -> unsigned char (min = 0, max = 255)
  • CV_8S -> char (min = -128, max = 127)
  • CV_16U -> unsigned short (min = 0, max = 65535)
  • CV_16S -> short (min = -32768, max = 32767)
  • CV_32S -> int (min = -2147483648, max = 2147483647)
  • CV_32F -> float
  • CV_64F -> double

另外,在数据类型中可以添加通道数,例如 CV_8UC3 表示 3 通道的 8 位无符号整型。

用 create( ) 函数对原 Mat 重新赋值
image1.create(200, 200, CV_8U);
image1 = 255;

出于性能方面的考虑,如果新的尺寸和类型与原来相同,则不会重新分配内存空间。

Mat 类型数据复制: copyTo( ) 和 clone( )

在复制 Mat 类型的数据时,要注意复制前后变量指向同一数据还是指向完全分离的数据。

cv::Mat image2(image1);    // 方式 1

cv::Mat image3 = image1;   // 方式 2

上述两种方式复制得到变量 image2 和 image3 与 image1 指向相同的数据。这种形式的复制也被称为浅复制 shallow copy,可以节省内存空间,但这种数据关联也可能导致意想不到的错误,修改其中一个,其他的也都会改变。例如,下边这个 Test 类,其中的 method() 函数返回一个 Mat 数据类型变量 img,由于内存共享的问题,如果一个对象修改了 img 数据,那么Test 类和它生成的所有对象也会被修改,这显然违背了面向对象编程中的封装原则。因此,不建议在类中返回 Mat 类型变量。如果需要的话,可以用后边的深复制函数返回变量的一个独立的复制。

class Test{
  cv::Mat img;
  public: 
       Test(): img(240, 320, CV_8U, 100){ }
       cv::Mat method() {return img;}
}

如果要得到完全分离的数据,应该用 copyTo() 或 clone() 这两个深复制函数明确指出:

image1.copyTo(image2);    // 方式 1

cv::Mat image2 = image1.clone();  // 方式 2
Mat 数据转换: convertTo( )
image1.convertTo(image4, 
                 CV_32F, // 数据类型转化成浮点型
                 1/255.0, // 缩放因子 
                 1.0);  // 偏移量。最终结果就是 x/255.0 + 1.0

OpenCV 在显示图像的时候,可以接受整型的数据类型,取值范围 0 ~ 255,也可以接受浮点型的数据类型,取值范围 0.0 ~ 1.0. 因此当把整型数据转成浮点型时,要考虑是否需要数据范围的缩放。例如,下面左侧原图为 CV_8UC3 类型,用 convertTo() 函数转换成浮点型 CV_32FC3,如果数值缩放因子设置为 1,即不缩放,那么转化之后超过 1.0 的数据均被截断,转化后的图像呈现很多白色区域,这显然不是我们想要的效果。


without scale.png

如果 Mat 元素很少且已知,例如输入相机的内参矩阵,就可以在初始化时直接输入

cv::Mat K = (cv::Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
读入图片
  • 原样读取,不特别设定为灰度图或者彩色图像,把第二个参数设定为负数即可,例如:
cv::Mat image = cv::imread(“image.jpg”, -1);
  • 读取为灰度图:
cv::Mat image = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);

// 或者写对应的编号
cv::Mat image = cv::imread("image.jpg", 0);
  • 读取为彩色图:第二个参数设定为正数,或者设定为 cv::IMREAD_COLOR

第二个参数取值总结:
\begin{cases} \text{彩色} ~~~~~~ \text{正数、cv::IMREAD_COLOR}\\ \text{灰度} ~~~~~~ \text{零、cv::IMREAD_GRAYSCALE}\\ \text{原样} ~~~~~~ \text{负数} \end{cases}

waitKey( ) 等待

一般与 imshow() 连用,效果是暂停处理,让图片显示一段时间。如果不加 waitKey(),就不会显示图片。

cv::waitKey( int delay=0)
  • delay <= 0,无限等待
  • delay >0 ,等待 delay 时间,单位为 ms

如果在等待期间有按键输入,则返回值为按键对应的编码;如果等待期间没有按键,则返回 -1.

整行、整列赋值
image.row(2).setTo(cv::Scalar(100));

image.col(2).setTo(cv::Scalar(100));   // 以上两个命令针对灰度图/单通道

image.col(2).setTo(cv::Scalar(0, 20, 34));  //彩色,OpenCV 中颜色通道默认顺序为 B、G、R

// 把 src 矩阵的第 i 行 赋值给 dst 矩阵的第 j 行
src.row(i).copyTo(dst.row(j));
查找最小值、最大值及其位置
cv::minMaxLoc(src, double* minVal, double* maxVal=0, 
              Point* minLoc=0, Point* maxLoc=0, 
              InputArray mask=noArray())
  • src:输入的图像。
  • minVal:最小值,可输入NULL表示不需要。
  • maxVal :最大值,可输入NULL表示不需要。
  • minLoc:最小值的位置,可输入NULL表示不需要,Point类型。
  • maxLoc:最大值的位置,可输入NULL表示不需要,Point类型。
  • mask:选定在哪些区域进行搜索。noArray() 表示没有设置 mask

如果只需要得到最小值和最大值,而不需要位置:

double my_min, my_max;
cv::minMaxLoc(input, &my_min, &my_max); // 只需要送入变量地址即可
像素运算之后截断

当涉及到像素值之间的加、减时,为了得到有意义的结果,一般要进行截断操作。
比如取值在 0~255 的像素,当运算结果 > 255 时,取为 255;当结果 < 0 时,取 0.

result = cv::saturate_cast<uchar>(some expression)

实际上,很多 cv 的函数默认内置了 saturate_cast 操作,比如下边提到的 cv::add 和重载的 + 运算。

重载运算符

比较常用的重载有

  • 加、减
  • 函数 min, max, abs
  • 比较运算符 <, <=, ==, !=, >, >=,返回 8 位的二进制值。
  • 矩阵运算:乘法、求逆 image.inv()、转置 image.t()、行列式 image.determinant()、范数 image.norm()、叉乘 v1.cross(v2)、点乘 v1.dot(v2)。 这些运算都不是 in place 的,即不改变原数据。
  • 组合运算符,如 +=
opencv 中的随机数
cv::RNG rng;
rng.uniform(0,255); // 生成范围内的随机数
两图像叠加
cv::add(image1, image2, result)

//或者加权形式 
cv::addWeighted(image1, weight1, image2, weight2, const, result)

OpenCV 中已经重载了 + 运算符,因此可以直接这样:

result = weight1 * image1 + weight2 * image2 + const
图像通道拆分与合并

将三通道拆分出来,存到 vector 中

std::vector<cv::Mat> planes;

cv::split(image1, planes);

这里用的是 c++ 模板中的 vector 数据类型。如果通道数量事先已知,也可以用数组类型,效率更高一些。

cv::Mat planes[3];

cv::split(image1, planes);

合并回去

cv::Mat result; 

cv::merge(planes, result);
阈值操作
cv::threshold(input,  // 输入的图片
              output,  // 输出的图片
              theshold, // 设定的阈值
              255,   // 超过阈值的像素值取为 255,其余为 0
              cv::THRESH_BINARY_INV); // 但这里用了 INV 反向操作,小于等于阈值的取 255,其余为 0

图像重映射

这里的重映射是指将原图中的元素经过某种位置变换,得到新图。这里只是改变了元素的位置,没有修改元素的值。
通过 cv::remap 函数实现。
首先定义重映射参数矩阵

cv::Mat srcX(image.rows, image.cols, CV_32F);
cv::Mat srcY(image.rows, image.cols, CV_32F);

目标图像在 (i,j) 处的像素来自原图的下列位置

(srcX.at<float>(i,j), srcY.at<float>(i,j))

然后在这两个参数矩阵中定义具体的位置映射关系

for (int i=0; i<image.rows; i++){
     for (int j=0; j<image.cols; j++){
       srcX.at<float>(i,j) = j;  // 来自原图的 j 列
       srcY.at<float>(i,j) = i+5*sin(j/10.0);  // 来自原图的 i+5*sin(j/10) 行,即正弦扭曲
     }
}

最后调用 cv::remap 函数

cv::remap(image, result, srcX, srcY, cv::INTER_LINEAR);
反色

对于灰度图,直接用 255 减去就可以了!

cv::Mat result1_inv = 255 - result1;
获取图像元素的常用方法
  1. cv::Mat类中的 at() 方法:
image.at<uchar>(i,j); 

image.at<cv::Vec3b>(i,j)[0]

上边尖括号中的数据类型就是灰度图和彩图中常用的数据类型:ucharcv::Vec3b

在使用 at() 时有一个需要注意的问题:
at() 函数有多个重载的形式,例如既有 at(int row, int col),也有 at(Point2f()) 。Point2f 格式对应的是 (x,y) 坐标,x 轴向右,y 轴向下,与 (row, col) 正好颠倒。因此在调用时一定要注意参数是 (row, col) 形式还是 Point2f(x,y) 坐标形式。

  1. 每次用 at 必须指明元素的数据类型,比较麻烦。可以用另一种方式:
cv::Mat_<uchar> img(image);

img(50, 10)=200;

即在定义变量时就已经指明了元素类型,后边直接取用就可以了。

数据类型的编号
cv::Mat test = cv::Mat::zeros(3,3,CV_64F);

test.type();  // 返回 6

.type() 函数即可返回 Mat 的数据类型,但是返回的是一个整数,需要查看下表确定到底是什么类型:

C1 C2 C3 C4
CV_8U 0 8 16 24
CV_8S 1 9 17 25
CV_16U 2 10 18 26
CV_16S 3 11 19 27
CV_32S 4 12 20 28
CV_32F 5 13 21 29
CV_64F 6 14 22 30

列代表通道数目,例如灰度图就是 C1, jpg 彩图就是 BGR C3,PNG 彩图除了 BGR 还有一个透明度通道,所以是 C4。

在用.at<> 获取 Mat 中的数据时,需要指明元素的数据类型,比如灰度图是 uchar 而不是 CV_8U,查看下表:

C1 C2 C3 C4 C6
uchar uchar cv::Vec2b cv::Vec3b cv::Vec4b
short short cv::Vec2s cv::Vec3s cv::Vec4s
int int cv::Vec2i cv::Vec3i cv::Vec4i
float float cv::Vec2f cv::Vec3f cv::Vec4f cv::Vec6f
double double cv::Vec2d cv::Vec3d cv::Vec4d cv::Vec6d

这里的 cv::Vec3bb 是 byte 的意思,表示占用一个字节。

颜色空间的转换

cv::cvtColor 转换前后,图像的数据类型是相同的。

  • 彩图到灰度:
if (image.channels()==3){
     cv::cvtColor(image, result, cv::COLOR_BGR2GRAY);
     // 老版本的 opencv 用这个 flag
     // cv::cvtColor(color, gray, CV_BGR2Gray)
}
  • 灰度到彩图:
cv::cvtColor(image, result, cv::COLOR_Gray2BGR) // 三个通道数值相同

cv::cvtColor( )函数可以实现很多颜色空间的转换,除了 BGR、灰度图,还有 HSV, HLS,Lab, Luv, YCrCb 等。

感知均匀的色彩空间

原始的 BGR 颜色表示方法有个缺陷:在颜色空间中的差距并不能很好的反映人视觉感知的差距。
两种在空间中距离很远的颜色可能肉眼看起来很接近;而有些在空间距离很近的颜色,看起来差别却很大。因此,BGR 不是感知均匀的色彩空间。

下边是两种感知均匀的色彩空间:

  • CIE L * a * b *
    L 通道:表示亮度,取值 0~100。在使用 8 位图像时,取值 0~255
    a 通道和 b 通道:表示色度部分,与亮度完全无关,取值 -127~127,对于 8 位图,取值 0~255.
    cv::cvtColor(image, converted, cv::COLOR_BGR2Lab)
    
  • CIE L * u * v *
    对亮度通道采用相同的转换公式,但对色度通道使用不同的方法。
    cv::cvtColor(image, converted, cv::COLOR_BGR2Luv)
    
色调、饱和度、亮度空间

人类在描述颜色时,经常说色调、饱和度、亮度等词语,下边的两种色彩空间就是这样设计的:

  • HSV
    H: 色调 hue
    S: 饱和度 saturation
    V: 明度 value

    cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV)
    

    可以把三通道分割开:

    std::vector<cv::Mat> channels;
    
    cv::split(hsv, channels);
    // channels[0] 表示色调  0~180
    // channels[1] 表示饱和度  0~255
    // channels[2] 表示亮度  0~255
    
  • HLS
    H: 色调 hue
    L: 亮度 lightness
    S: 饱和度 saturation

HSV 和 HLS 有时也统称为 HSB ,最后的 B 表示 brightness 亮度。
可以用圆锥体直观地表示 HSB 色彩空间:

HSB_1.png
  • 角度表示色调 H,本来应该用 0~360 表示色调,但是为了适应 8 位图,采用了 0~180,0 度对应红色。
  • 距中轴线的距离表示饱和度 S
  • 高度表示亮度 B

如果要进行图片的 HSB 分析,尤其是涉及到色调(第一个分量)的时候,由于饱和度(第二个分量)低的区域色调信息不准确,因此往往先对饱和度做一些过滤,只分析那些饱和度比较高的区域的色调。

区域图像提取
cv::Mat imageROI = image(cv::Rect(216, 33, 24, 30));  // 从原始图片 image 中提取了一个小方块

// 或者
cv::Mat imageROI(image2, cv::Rect(0,0,image2.cols,image2.rows/2)); 

随后就可以把提取出来的 imageROI 当成普通图片处理了。

添加几何图形和文字

外加图形和文字的颜色维度取决于原始图像的维度。
若原始为灰度图,则设定外加颜色时,即使写出三维像素值 cv::Scalar(255, 0, 128) 也只看第一维的数值;
若原始为彩图,则设定外加颜色时,只写 0 或者 255 表示只设定了第一个颜色通道的值,例如 255 相当于 cv::Scalar(255, 0, 0)

  • 线段

    cv::line(image, 
             cv::Point(5, 5), 
             cv::Point(210, 200),  // 线段的两个端点
             cv::Scalar(255), // 颜色
             1); // 粗细
    
  • 矩形方框

    cv::Rect rect(110, 45, 35, 45); // 设定一个方框,左上角坐标+宽+长
    
    cv::rectangle(image, rect, cv::Scalar(0,0,255));  // 在 image 图像上添加 rect 方框,红色
      
    //或者直接把矩形放在内部定义
    cv::rectangle(image, cv::Rect(110, 45, 35, 45), 0); 
      
    // 或者以两对点集来表示,分别为左上角坐标和右下角坐标
    cv::rectangle(image, 
                  cv::Point(5,5),  // 左上角坐标
                  cv::Point(210, 200), // 右下角坐标
                  cv::Scalar(255),  // 颜色
                  3); // 线条粗细
    
  • cv::circle(image,   // 原始图像
               cv::Point(155, 110),  // 圆心, 以 cv::Point(x,y) 表示图像中的坐标
               65,   // 半径,必须为 int 类型
               0,   // 颜色
               3);  // 线条宽度,若为负数,则画实心圆
    
  • 加重心
    假设图像中物体轮廓已经找到了,要依据轮廓添加物体的重心位置。
    一般计算重心的时候,首先要计算图像的矩 (moment),再基于各阶矩计算出重心:

    itc = contours.begin();  // 已经有了若干轮廓线
    
    while (itc != contours.end()){
         cv::Moments mom = cv::moments(cv::Mat(*itc++)); // 求每条轮廓的各阶矩
         cv::circle(result, 
                    cv::Point(mom.m10/mom.m00, mom.m01/mom.m00), // 通过这种方式计算重心
                    2,
                    cv::Scalar(0),
                    2
         );
    }
    

    原理:
    对于二元连续函数 f(x,y),它的 (p+q) 阶矩为
    M_{pq} = \int_{-\infty}^{\infty}\int_{-\infty}^{\infty}x^py^qf(x,y)dxdy

    对于离散的图片 I(x,y),对应的 (p+q) 阶矩为
    M_{ij} = \sum_x \sum_y x^i y^i I(x,y)

    因此,图片 I(x,y) 的重心为
    (\bar{x},\bar{y}) = \left( \frac{M_{10}}{M_{00}}, \frac{M_{01}}{M_{00}} \right)

  • 椭圆
    可以通过外包矩形的方式画出椭圆

    std::vector<cv::Point> points;
    
    cv::RotatedRect rr = cv::minAreaRect(points); // 由点集得到最小矩形外包,可以倾斜
    
    cv::ellipse(image, rr, 255, 2); 
    

    也可以直接指定椭圆的形状参数

    cv::ellipse(image, 
                cv::Point(WINDOW_WIDTH / 2, WINDOW_WIDTH / 2),  // 椭圆中心点
                cv::Size(WINDOW_WIDTH / 4, WINDOW_WIDTH / 16), // 两个轴的长度
                angle, // 椭圆倾斜的角度
                0, 360,  // 椭圆包围的起始和终止角度
                Scalar(255, 129, 0),
                thickness);
    
  • 多边形

    std::vector<cv::Point> poly; // 存放多边形的顶点坐标
    
    cv::polylines(image, 
                  poly, 
                  true, // 是否闭合,若闭合,则最后一个点将于第一个点相连
                  0,  // 颜色
                  2);  // 宽度
    
  • 加文字

    cv::putText(image, 
                "This is a dog.",
                cv::Point(40, 200),  // 文本位置,文本的左下角对应的坐标
                cv::FONT_HERSHEY_PLAIN, // 字体类型
                2.0,  // 字体大小
                255,  // 字体颜色,这里相当于  cv::Scalar(255, 0, 0)
                2);  // 字体粗细
    
直线拟合

由点集拟合出一条直线。一般的拟合目标是使所有点到直线的距离之和最小。
在计算距离的函数中,欧几里得距离计算的最快,对应参数 cv::DIST_L2,实际上也就是基于最小二乘算法的拟合。

std::vector<cv::Point> points;  // 假设我们已经获取了节点集合

cv::Vec4f line; // 待拟合的直线

cv::fitLine(points,   // 点集
            line,    // 拟合出的直线
            cv::DIST_L2,  // 采用的距离类型
            0,  // L2 距离不需要这个
            0.01, 
            0.01); // 拟合出的直线参数精度

直线是 cv::Vec4f 的格式,包含四个数,前两个数表示单位向量方向,后两个数表示直线上的一个坐标。
也可以在三维空间中拟合直线,只需要将点集和直线的数据类型相应地修改一下:

std::vector<cv::Point3i> points;

// 或者
std::vector<cv::Point3f> points;

cv::Vec6f line;
改变图片大小 resize
cv::resize(image, 
           result, 
           cv::Size(), // 指定改变之后的大小
           4, 4,  // 指定 x,y 方向(即 width, height)缩放比例。本行与上一行至少要设定一个
           cv::INTER_NEAREST); // 插值方式

图片大小改变后,像素值要重新计算,有以下插值方法:

  • INTER_NEAREST - 最邻近插值,对于放大图像的情况,就是简单的把每个像素的尺寸放大。
  • INTER_LINEAR - 双线性插值,如果最后一个参数不指定,默认使用这种方法。
    所谓双线性,就是先在插入点的两侧线性插入新像素值,再以此为基础,再次线性的插入像素值。整个过程中用到 2x2 = 4 个原始像素。


    interpolation1.png
  • INTER_CUBIC - 4x4像素邻域内的双立方插值
  • INTER_LANCZOS4 - 8x8像素邻域内的Lanczos插值

以上插值算法计算复杂度依次增加,效果也是依次提升的。

扫描图像元素的方法
  • .at 函数
    // 减色运算操作
    for(int j=0; j<nl; j++){  // nl 为行数
       for(int i=0; i<nc; i++){  // nc 为列数
         image.at<cv::Vec3b>(j,i)[0] = image.at<cv::Vec3b>(j,i)[0]/div * div + div/2;
         image.at<cv::Vec3b>(j,i)[1] = image.at<cv::Vec3b>(j,i)[0]/div * div + div/2;
         image.at<cv::Vec3b>(j,i)[2] = image.at<cv::Vec3b>(j,i)[0]/div * div + div/2;
       }
    }
    
  • 用指针
    如果要按顺序依次获取图像像素信息,尽量不要用上述 image.at 方法,而要用指针,指针在移动扫描操作时效率更高。
    int nc = image.cols * image.channels();
    for (int j=0; j<nl; j++){
       uchar *data = image.ptr<uchar>(j); // 每行首元素对应的地址
       for (int i=0; i<nc; i++){
         data[i] = data[i]/div*div + div/2;
       }
    }
    

另外,少层循环 + 每个循环中较多操作 好于 多层循环 + 每个循环中较少操作。
例如要对一个像素执行 N 个不同的操作,就应该在单个循环中执行全部操作,而不是写 N 个循环,每个循环执行一个操作。

  • 用迭代器 (iterator)
    cv::Mat_<cv::Vec3b>::iterator it = image.begin<cv::Vec3b>();
    
    cv::Mat_<cv::Vec3b>::iterator itend = image.end<cv::Vec3b>();
    
    for (;it!= itend;++it){
       (*it)[0] = ...
       (*it)[1] = ...
       (*it)[2] = ...
    }
    
    //或者用 while 循环
    while (it!=itend){
       ...
       ++it;
    }
    
减色算法

默认 BGR 三色都是采用 8 位二进制数,即有 0~255 种选择,三通道互相搭配总共的颜色数目为 256 * 256 * 256。 为了降低分析的复杂度,可以先对原图做减色处理,将每个通道颜色的数量降低为原来的 1/n,例如 n=8,则新的图片总共的颜色数目只有 32 * 32 * 32 。

算法步骤:

  • 假设 N 为减色因子,将图像中每个像素的值除以 N,去掉余数部分
  • 将上述结果乘以 N,得到原始像素值相对于 N 的倍数(相当于向下取整)
  • 再加上 N/2,这样相对于原始像素值的误差就从 -N ~ 0 变成了 -N/2 ~ N/2 ,近似效果更好
  • 减色之后共有 (256/N) * (256/N) * (256/N) 种颜色。
锐化图像

目的是放大图像边缘,使图像看起来更加尖锐
对于每个像素,令本身像素值乘以5,然后减去周围的 4 个像素值,即
sharpended\_pixel = 5*current - left - right - up -down
这种操作实际上就是一种卷积

sharpen_kernel.png

用上述 kernel matrix 依次扫过图像,即完成了上述的锐化操作。

cv::Mat 数据类型作为参数传递

cv::Mat 数据结构包括数据头部和数据块:

  • 数据头部包含了属性信息,例如数据的大小、行列、通道数、元素类型等,可以通过 .cols,.rows, channels() 等方式获取数据头部包含的信息;
  • 数据块存储了所有的像素信息,数据头部中包含了 .data 指针,可以访问数据块。

cv::Mat 数据结构的一个关键特点时,当复制一个 cv::Mat 数据时,仅复制了数据头部,因此复制前后的变量都指向同一个数据块(除非特别指明数据头部和数据块全都复制)。前述内容中提到,copyTo(), clone() 函数才会复制得到全新的函数,而用赋值操作仅仅复制了数据头部,数据块仍然是共享的。

当向函数中传递 cv::Mat 类型数据时,比较常见的参数传递形式包括 cv::Mat, const cv::Mat, const cv::Mat &, cv::Mat &.

  1. func(cv::Mat input) 仅仅复制了 input 对应实参变量的数据头部,对 input 的操作会改变实参变量。但是如果对 input 重新赋值,例如 input = cv::Mat::ones(...),此时对 input 的操作不会影响到外部的实参,因为新的 input 不再指向实参变量的数据块。
  2. func(const cv::Mat input) 也是仅仅复制了 input 对应实参的数据头部,只不过由于 const 的限制,不能在函数内部修改该数据头部。
  3. func(const cv::Mat &input) 以引用的形式传递了实参变量的数据头部,同样由于 const 的限制,不能修改该头部。
  4. func(cv::Mat &) 以引用的形式传递了实参变量的数据头部,并且可以修改该头部。

注意:上述参数传递方式仅仅对数据头部做了限制,而不限制头部指向的数据块,因此情况 2 和 3 中,即使由于 const 限制不能修改头部,仍然可以修改头部指向的数据块: input.data[0] = 5

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容