OpenCV学习笔记01:读取和遍历图像


仅作参考,详细API请参考 OpenCV官方文档.

使用OpenCV读取和保存图片

图片的读取

在安装好OpenCV后,编写第一个OpenCV测试程序如下:

#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>

#include <iostream>

int main() {
    // 读取图片
    cv::Mat img = cv::imread("lena.jpg");
    // 判断是否读取成功
    if (img.empty()) {
        std::cout << "Could not open or find the image" << std::endl;
        return -1;
    }
    // 显示图片
    cv::namedWindow("pic", cv::WINDOW_AUTOSIZE)
    cv::imshow("pic", img);
    cv::waitKey();

    return 0;
}

编译运行程序,可以看到OpenCV读取了图片文件并将其展示出来,证明我们的OpenCV安装成功.

在这里插入图片描述

其中cv::imread(const String& filename, int flags = IMREAD_COLOR)函数用于读取图片,参数列表如下:

  • filename参数表示图片的路径
  • flags参数表示将图片读取到内存的格式,可以是一下三者之一:
    • IMREAD_UNCHANGED(<0)表示以图片的存储格式来读取图片(包含α通道)
    • IMREAD_GRAYSCALE(=0)表示以灰度格式来读取图片(单通道)
    • IMREAD_COLOR(>0)表示以BGR格式读取图片(三通道)

cv::imread()函数返回一个Mat对象,可以调用其isempty()方法判断是否读取成功.

cv::imshow()函数用于展示图片.

图片的变换和保存

下面例子展示使用OpenCV进行色彩空间转换:

#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/opencv.hpp>

#include <iostream>

int main() {
    // 读取图片
    cv::Mat image = cv::imread("lena.jpg", cv::IMREAD_COLOR);
    if (image.empty()) {
        std::cout << "Could not open or find the image" << std::endl;
        return -1;
    }

    // 进行色彩空间转换
    cv::Mat gray_image;
    cv::cvtColor(image, gray_image, cv::COLOR_BGR2GRAY);

    // 保存图片
    cv::imwrite("gray_image.jpg", gray_image);

    // 展示图片
    cv::namedWindow("color imge", cv::WINDOW_AUTOSIZE);
    cv::namedWindow("grayscale image", cv::WINDOW_AUTOSIZE);
    cv::imshow("color imge", image);
    cv::imshow("grayscale image", gray_image);

    cv::waitKey();
    return 0;
}

在这里插入图片描述

cv::cvtColor(InputArray src, OutputArray dst, int code, int dstCn = 0)函数用于进行色彩空间转换,其参数列表如下:

  • src,dst: 原矩阵和目标矩阵.
  • code: 色彩空间转换代码,指示原色彩空间和目标色彩空间,可选值见官方文档.
  • dstCn: 目标图像的通道数,若指定为0则输出通道数由code参数推断.

cv::imread()函数用于保存图片.

cv::Mat基本图像容器

在OpenCV中,图片数据是以cv::Mat类存储的,这是OpenCV得核心类.(在OpenCV1版本中,曾用IplImage结构体来存储图像,现已被废弃).使用cv::Mat类不用手动申请和释放内存,这要归功于cv::Mat类的结构.

cv::Mat类的结构

Mat类由两部分构成:

  1. 矩阵头(matrix header),存储图片矩阵的信息,包括矩阵形状,色彩空间,矩阵的内存地址等.
  2. 指向矩阵内容的data指针.

OpenCV使用指针计数管理内存的申请和释放,在矩阵头中有一个指针int* refcount,统计使用同一个图片矩阵的cv::Mat对象个数.

  • cv::Mat对象的引用赋值复制构造函数都不会引起图片矩阵的复制:

    Mat A, C;                          	// 只创建了两个矩阵头
    A = imread(argv[1], IMREAD_COLOR); 	// 读入数组
    
    Mat B(A);							// 复制构造函数
    C = A;                              // 引用赋值
    

    在上面的程序中,A,B,C3个cv::Mat对象的矩阵头不同,但指向同一个图片矩阵数组,对其中任何一个图片内容的修改会影响到另外两个图片内容.

  • 对图片的裁剪也不会引起矩阵图片的复制.

    Mat D(A, Rect(10, 10, 100, 100)); 		// 使用ROI裁剪
    Mat E = A(Range::all(), Range(1,3)); 	// 指定行列裁剪
    

    D,E两个对象指向的是A图片内容的一部分,仍然与A共享同样的图片矩阵.

  • 可以使用cv::Mat::clone()cv::Mat::copyTo()实现图片矩阵的拷贝,这样拷贝出来的图片内容与原图片矩阵是独立的,修改新图片不会影响原图片.

    Mat F = A.clone();
    Mat G;
    A.copyTo(G);
    

创建cv::Mat对象

在这里插入图片描述

常见有以下几种方式创建cv::Mat对象.

  1. 使用cv::Mat类构造函数

    cv::Mat类有很多构造函数,最常用的为Mat (int rows, int cols, int type, const Scalar &s),参数列表如下:

    • rows,cols: 表示图片尺寸.
    • type: 指定每个像素点的存储类型,为一系列宏定义,格式如下:CV_[每一项的位数][是否有符号][数据类型]C[通道数].例如:8UC3表示3通道,每个通道的每个像素点都由8位uchar表示;CV32FC4表示4通道,每个通道上的每个像素点由32位float表示.
    • s: 非必需项,表示每个像素点的值.Scalarvector的子类.
    Mat M(2,2, CV_8UC3, Scalar(0,0,255));
    cout << "M = " << endl << " " << M << endl << endl;
    

    输出:

    M =
     [  0,   0, 255,   0,   0, 255;
       0,   0, 255,   0,   0, 255]
    
  2. 使用cv::Mat的子类cv::Mat_

    cv::Mat_类使用泛型来替代cv::Mat类构造函数中的type参数来指定像素点的存储类型.这样可以避免一些运行期错误.

    下面程序会产生bug:

    Mat M(2, 2, CV_8UC3, Scalar(0, 0, 255));
    M.at<double>(0, 0) = 1;
    cout << "M = " << endl << " " << M << endl << endl;
    

    输出:

    M =
     [  0,   0,   0,   0,   0,   0;
     240,  63,   0,   0,   0,   0]
    

    可以看到,由于错误的选择了赋值给像素点的数据类型,造成了bug,但是在编译期不会报任何错误.

    下面使用cv::Mat_类,可以看到在编译期会报warning.

    Mat_<Vec3b> M(2, 2, Vec3b(0, 255, 0));
    M.at<double>(0, 0) = 1;
    cout << "M = " << endl << " " << M << endl << endl;
    
  3. 也可以使用MATLAB风格的矩阵定义方式来定义vc::Mat对象.

    Mat E = Mat::eye(4, 4, CV_64F);
    cout << "E = " << endl << " " << E << endl << endl;
    Mat O = Mat::ones(2, 2, CV_32F);
    cout << "O = " << endl << " " << O << endl << endl;
    Mat Z = Mat::zeros(3,3, CV_8UC1);
    cout << "Z = " << endl << " " << Z << endl << endl;
    

    输出:

    E=
     [1, 0, 0, 0;
     0, 1, 0, 0;
     0, 0, 1, 0;
     0, 0, 0, 1]
    
    O =
     [1, 1;
     1, 1]
    
    Z =
     [  0,   0,   0;
       0,   0,   0;
       0,   0,   0]
    
  4. 使用ROI进行图片裁剪

    #include <opencv2/core.hpp>
    #include <opencv2/highgui.hpp>
    #include <opencv2/opencv.hpp>
    
    #include <iostream>
    
    using namespace std;
    using namespace cv;
    
    int main() {
    
        Mat pImg = imread("lena.jpg");
        Rect rect(90, 100, 100, 100); //(offset_x, offset_y)=(180, 200); (width, height)=(200,200);
        Mat roi = Mat(pImg, rect);
        Mat pImgRect = pImg.clone();
        rectangle(pImgRect, rect, Scalar(0, 255, 0), 1);
        imshow("original", pImgRect);
        imshow("roi", roi);
    
        waitKey();
        return 0;
    }
    

    在这里插入图片描述

遍历cv::Mat对象

下面几种方法都可以遍历cv::Mat对象进行像素值的读写:

  1. 使用cv::Mat::at()方法进行随机读写(效率最低,不推荐):

    Mat_<uchar> grayimg(512, 512, (uchar) 0);
    for (int i = 0; i < grayimg.rows; ++i) {
        for (int j = 0; j < grayimg.cols; ++j) {
            grayimg.at<uchar>(i, j) = (uchar) ((i + j) % 255);
        }
    }
    imshow("grayimg", grayimg);
    
    Mat_<Vec3b> colorimg(512, 512, Vec3b(0, 0, 0));
    for (int i = 0; i < colorimg.rows; ++i) {
        for (int j = 0; j < colorimg.cols; ++j) {
            Vec3b pixel;
            pixel[0] = (uchar) (i % 255);   // blue
            pixel[1] = (uchar) (j % 255);   // green
            pixel[2] = 0;       // red
            colorimg.at<Vec3b>(i, j) = pixel;
        }
    }
    imshow("colorimg", colorimg);
    waitKey();
    

    在这里插入图片描述

  2. 使用迭代器(安全,但不灵活)

    Mat_<uchar> grayimg(512, 512, (uchar) 0);
    for (MatIterator_<uchar> grayit = grayimg.begin(); grayit != grayimg.end(); ++grayit) {
        *grayit = (uchar) (rand() % 255);
    }
    imshow("grayimg", grayimg);
    
    Mat_<Vec3b> colorimg(512, 512, Vec3b(0, 0, 0));
    for (MatIterator_<Vec3b> colorit = colorimg.begin(); colorit != colorimg.end(); ++colorit) {
        (*colorit)[0] = (uchar) (rand() + 100 % 255);   // blue
        (*colorit)[1] = (uchar) (rand() + 200 % 255);   // green
        (*colorit)[2] = (uchar) (rand() % 255); 		// red
    }
    imshow("colorimg", colorimg);
    waitKey();
    

    在这里插入图片描述

  3. 使用指针(效率最高,但要注意数组越界问题)

    使用cv::Mat::ptr(i)可以获取指向图像矩阵第i行第一项的指针,实现对图像矩阵的底层读写.要理解这种遍历方法,要先理解图像矩阵在内存中的存储方式:

    图像矩阵在内存中是以二维数组的形式存储的,数组的行数与图片的行数相同,列数则等于图片列数×图像深度.

    在这里插入图片描述

    另外图像矩阵还存在是否连续的问题,一般来说,图像矩阵是连续的,但通过ROI裁剪等方式得到的图像矩阵有可能是不连续的,可以使用cv::Mat::isContinuous()方法判断图像矩阵是否连续.

    http://opencv.jp/cookbook/_images/continuous_discontinuous.png

    #include <opencv2/core.hpp>
    #include <opencv2/highgui.hpp>
    #include <opencv2/opencv.hpp>
    
    #include <iostream>
    
    using namespace std;
    using namespace cv;
    
    // 遍历图片矩阵
    Mat &traversalImage(Mat &img) {
    
        // 获取图像的参数
        int channels = img.channels();      // 通道数
        int nRows = img.rows;               // 行数
        int nCols = img.cols * channels;   	// 列数,考虑到图像矩阵的存储形式,每一行的实际元素数应为列数乘以通道数
    
        // 若图像矩阵是连续的,则只需寻址一次
        if (img.isContinuous()) {
            nCols *= nRows;
            nRows = 1;
        }
    
        // 遍历图像矩阵
        for (int i = 0; i < nRows; ++i) {
            uchar *p = img.ptr<uchar>(i);
            for (int j = 0; j < nCols; ++j) {
                p[j] = (uchar) ((i + j) % 255);
            }
        }
    
        return img;
    }
    
    int main() {
    
        Mat_<uchar> grayimg(512, 512, (uchar) 0);
        traversalImage(grayimg);
    
        Mat_<Vec3b> colorimg(512, 512, Vec3b(0, 0, 0));
        traversalImage(colorimg);
     
        return 0;
    }
    

在这里插入图片描述

例子:对图像进行color space reduction操作

对图片进行color space reduction操作可以降低灰度色阶数,提高运算速度.常见的color space reduction方式有查找表(look up table),计算表达式如下:
I n e w = ( I o l d 100 ) × 100 I_{new} = \left( \frac{I_{old}}{100} \right) \times 100 Inew=(100Iold)×100

下面程序通过遍历实现look up table.

#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/opencv.hpp>

#include <iostream>

using namespace std;
using namespace cv;

int main() {
    // 读取并显示原图片
    Mat img = imread("lena.jpg");
    if (img.empty()) {
        std::cout << "Could not open or find the image" << std::endl;
        return -1;
    }
    imshow("origin_img", img);

    // 遍历原图片进行 color space reduction 并展示
    int channels = img.channels();
    int nRows = img.rows;
    int nCols = img.cols * channels;
    if (img.isContinuous()) {
        nCols *= nRows;
        nRows = 1;
    }
    for (int i = 0; i < nRows; ++i) {
        uchar *p = img.ptr<uchar>(i);
        for (int j = 0; j < nCols; ++j) {
            p[j] = (uchar) (p[j] / 100 * 100);
        }
    }
    imshow("reduced_img", img);

    waitKey();
}

在这里插入图片描述

当然,OpenCV内置了cv::LUT()函数,可以实现同样的效果:

// 构建lookuptable
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.ptr();
for( int i = 0; i < 256; ++i)
    p[i] = (uchar) (i / 100 * 100);
// 进行reduction
LUT(img, lookUpTable, output_img);

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值