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类由两部分构成:
- 矩阵头(matrix header),存储图片矩阵的信息,包括矩阵形状,色彩空间,矩阵的内存地址等.
- 指向矩阵内容的
data
指针.
OpenCV使用指针计数管理内存的申请和释放,在矩阵头中有一个指针int* refcount
,统计使用同一个图片矩阵的cv::Mat
对象个数.
-
cv::Mat
对象的引用赋值和复制构造函数都不会引起图片矩阵的复制:Mat A, C; // 只创建了两个矩阵头 A = imread(argv[1], IMREAD_COLOR); // 读入数组 Mat B(A); // 复制构造函数 C = A; // 引用赋值
在上面的程序中,
A
,B
,C
3个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
对象.
-
使用
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
: 非必需项,表示每个像素点的值.Scalar
是vector
的子类.
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]
-
使用
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;
-
也可以使用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]
-
使用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
对象进行像素值的读写:
-
使用
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();
-
使用迭代器(安全,但不灵活)
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();
-
使用指针(效率最高,但要注意数组越界问题)
使用
cv::Mat::ptr(i)
可以获取指向图像矩阵第i
行第一项的指针,实现对图像矩阵的底层读写.要理解这种遍历方法,要先理解图像矩阵在内存中的存储方式:图像矩阵在内存中是以二维数组的形式存储的,数组的行数与图片的行数相同,列数则等于图片列数×图像深度.
另外图像矩阵还存在是否连续的问题,一般来说,图像矩阵是连续的,但通过ROI裁剪等方式得到的图像矩阵有可能是不连续的,可以使用
cv::Mat::isContinuous()
方法判断图像矩阵是否连续.#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);