在安装好OpenCV后,编写第一个OpenCV测试程序如下:
#include
#include
#include
#include
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
#include
#include
#include
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类由两部分构成:
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
#include
#include
#include
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
#include
#include
#include
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方式有查找表(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
#include
#include
#include
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);