先放书里的源码。
ch5/imageBasics/imageBasics.cpp
#include <iostream>
#include <chrono>
using namespace std;
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
int main(int argc, char **argv) {
// 读取argv[1]指定的图像
cv::Mat image;
image = cv::imread(argv[1]); //cv::imread函数读取指定路径下的图像
// 判断图像文件是否正确读取
if (image.data == nullptr) { //数据不存在,可能是文件不存在
cerr << "文件" << argv[1] << "不存在." << endl;
return 0;
}
// 文件顺利读取, 首先输出一些基本信息
cout << "图像宽为" << image.cols << ",高为" << image.rows << ",通道数为" << image.channels() << endl;
cv::imshow("image", image); // 用cv::imshow显示图像
cv::waitKey(0); // 暂停程序,等待一个按键输入
// 判断image的类型
if (image.type() != CV_8UC1 && image.type() != CV_8UC3) {
// 图像类型不符合要求
cout << "请输入一张彩色图或灰度图." << endl;
return 0;
}
// 遍历图像, 请注意以下遍历方式亦可使用于随机像素访问
// 使用 std::chrono 来给算法计时
chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
for (size_t y = 0; y < image.rows; y++) {
// 用cv::Mat::ptr获得图像的行指针
unsigned char *row_ptr = image.ptr<unsigned char>(y); // row_ptr是第y行的头指针
for (size_t x = 0; x < image.cols; x++) {
// 访问位于 x,y 处的像素
unsigned char *data_ptr = &row_ptr[x * image.channels()]; // data_ptr 指向待访问的像素数据
// 输出该像素的每个通道,如果是灰度图就只有一个通道
for (int c = 0; c != image.channels(); c++) {
unsigned char data = data_ptr[c]; // data为I(x,y)第c个通道的值
}
}
}
chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
chrono::duration<double> time_used = chrono::duration_cast < chrono::duration < double >> (t2 - t1);
cout << "遍历图像用时:" << time_used.count() << " 秒。" << endl;
// 关于 cv::Mat 的拷贝
// 直接赋值并不会拷贝数据
cv::Mat image_another = image;
// 修改 image_another 会导致 image 发生变化
image_another(cv::Rect(0, 0, 100, 100)).setTo(0); // 将左上角100*100的块置零
cv::imshow("image", image);
cv::waitKey(0);
// 使用clone函数来拷贝数据
cv::Mat image_clone = image.clone();
image_clone(cv::Rect(0, 0, 100, 100)).setTo(255);
cv::imshow("image", image);
cv::imshow("image_clone", image_clone);
cv::waitKey(0);
// 对于图像还有很多基本的操作,如剪切,旋转,缩放等,限于篇幅就不一一介绍了,请参看OpenCV官方文档查询每个函数的调用方法.
cv::destroyAllWindows();
return 0;
}
这段代码首先需要完成以下几个功能:
- 使用opencv读取图片、并对其中像素进行修改。
- 将修改后的图片,使用GUI显示出来。
- 并且记录其中操作的耗费时间。
所以可以使用这几个包:
- opencv core.OpenCV的核心包,能够满足几乎所有的基本图像操作要求。
- opencv highgui.OpenCV自带的gui包,虽然OpenCV也可以结合其他比如Qt之类的GUI包使用,但是在测试一些单一功能时,使用highgui会更方便些。
- chrono.常见的计时器包。
那么接下来要拟定算法流程。
1.使用OpenCV读取图像文件。
2.对其中图像像素进行操作,满足修改条件。
3.使用highgui读取图像并输出显示。
//只需要调用这一个函数
//cv::imshow("这是GUI的title",一个Mat类型的图像数据);
cv::imshow("修改前的image",image);
4.给以上模块套上chrono计时器。
//读取当前时间,使用chrono::steady_clock::now()。
chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
//读取程序运行完成后时间。
chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
//注意time_point是一个复杂的数据结构,所以想要得出时间差需要调用chrono包内的函数。
chrono::duration<double> time_used = chrono::duration_cast < chrono::duration < double >> (t2 - t1);
cout << "遍历图像用时:" << time_used.count() << " 秒。" << endl;
首先完成第一部分的功能:
//首先创建一个OpenCV的图像矩阵cv::Mat。
//这个数据结构可以理解为以矩阵的形式存储了图像的像素,并且附注了一些其他信息。不过调用方法和常见的矩阵数据类型有较大差异。
//该数据结构有五种基本属性。图像相关信息flags、维度数dims、列数cols、行数rows、实际数据data。
//以上属性在读取完成后进行输出检测是否符合预期,图像类型flags一般使用成员方法cv::Mat::type()进行检测。
cv::Mat image;
//然后使用imread函数对图片进行读取。
//imread函数是opencv的一个常用图像读取函数,
//输入参数const String & filename为图像路径,支持jpg jpge png webp等几乎一切常见图像类型。
//另一个参数int flags则表示读取图像的形式,有-1、0、1三种取值,从左到右,分别代表以原始图像读取、以灰度图、以彩色图,无特殊需求则不需要变更。
image = cv::imread(argv[1]);
//现在对读取后的图像进行检测。
//首先检测其是否已成功读入。这里使用成员函数cv::Mat::empty进行条件检测。
//如果希望读取一个空图片(那为什么不直接在内存中创建一个呢),则应使用教材中的检测条件 image.data == nullptr
if(!image.empty()){
//返回错误信息
cerr << "文件" << argv[1] << "不存在。" << endl;
}
//接下来检测行列信息、通道数与图像类型。
//Mat::channels()方法是对flags属性的解析,返回通道数。
cout << "rows:" << image.rows << ".cols:" << cols << ".channels:" << image.channels() << endl;
//Mat::type()对flags属性解析后返回int数据,所以需要与宏定义进行对比来检查。8U代表8位、无符号,C1、C3代表通道数1、通道数3。
if (image.type() != CV_8UC1 && image.type() != CV_8UC3) {
// 图像类型不符合要求
cout << "请输入一张彩色图或灰度图." << endl;
return 0;
}
接下来完成程序第二部分的功能,对图像的像素进行操作。
首先实现对指定像素中指定通道的数值读取与操作。
关于使用OpenCV对Mat数据结构的像素操作进行操作,有三种方法。详情可看这里:Mat中像素的获取与赋值
使用cv::Mat::ptr()成员方法,虽然可读性可能有些不便,但是效率和健全性上更有保障。
ptr()方法输入行数y(数据类型由模板指定),返回图像第y行的首地址(数据类型为uchar*)。
要访问Mat中的第y行,第x列像素的第c个通道的值,可以使用以下方法。
//首先创建一个行指针,因为上文检测的图像类型都要求为8U,所以模板指定uchar类型。
uchar * row_ptr = image.ptr<uchar>(y);
//然后再创建一个执行第y行,第x列的像素指针。
//可将row_ptr看做数组,存储了这一行所有像素的信息,访问参数为列数x乘以图像通道数。
//再对访问后的row_ptr[argv]进行取地址,将值赋给data_ptr。
uchar * data_ptr = &row_ptr[x * image.channels()];
//data_ptr此时可看做存储了第y行,第x列像素信息的数组,其长度等于通道数。所以现在访问第c个通道的值。
//前文提到过,允许的两种像素都是8U型,所以读取到的值应为uchar
uchar data = data_ptr[c];
//如果想改变这个通道的值,应该直接对这个数组进行操作,因为我们是使用地址来进行复制,所以可以影响到原本的数据。
data_ptr[c] = 0 ;
在教材中,要求我们将一张图像复制,并对复制品左上角100*100的图像全改成白色。
虽然以上方法也可以实现,但是opencv也提供了方便的函数可供调用。
//首先,使用clone方法对图像数据进行复制。因为Mat数据类型是一个结构体,并且Mat.data是一个指针。
image_clone = image.clone();
//对复制后的图像进行展示
cv::imshow("image_clone",image_clone);
//然后对克隆得到的image_clone进行重载,输入一个Rect类实例来确定范围,重载后得到ROI(感兴趣区域)
//再对这个ROI调用.setTo方法即可仅修改原图像上ROI区域范围的像素。
image_clone(cv::Rect(0,0,100,100)).setTo(255);
//对修改后的图像进行展示
cv::imshow("image_clone",image_clone);
接下来,再将以上的代码部分拼接在一起,便可完成imageBasics程序的要求。
记得先把包都导进来。