第1章 接触图像
第2章 操作像素
第6章 图像滤波
附录 OpenCV3 介绍及代码导读
<div id="Section1">第1章 接触图像</div>
- OpenCV 库的结构
- 载入、显示及保存图像
OpenCV 库的结构
- sources文件夹下的子文件夹:
- doc 文件夹中包含的是文档 + include 文件夹中是所有头文件
- modules 文件夹中包含所有的源程序
- samples 文件夹中则是许多简短的学习用范例
第 2 页讲了下怎么编译的,对于新版 OpenCV 来说已经没有必要了,解压后的 build 文件夹就是编译好的内容。
第 3 页介绍了各模块的功能,还有推荐的声明方式,为什么要用这种声明方式呢?
第 6 页提到,为了遵循 ANSI C++ 标准,在用 Visual Studio 建立工程时选择 Application Settings 时,没有勾选 Precompiled Header 选项,这是 Visual Studio 的预编译头文件特性,可以加速编译过程。
载入、显示及保存图像
- 声明图像变量
cv::Mat image;
创建宽高都为0的图像,返回值是一个结构体,
- 图像读取、解码以及内存分配
image = cv::imread("img.jpg");
- 检查图像是否被正确读取
if (!image.data) {
// 图像尚未创建……
}
此处的成员变量data事实上是指向已分配的内存块的指针,包括图像数据。当不存在数据时,它被简单设置为0.
- 声明一个需要进行图像显示的窗口,接着指定需要显示的图像:
cv::namedWindow("Original Image"); // 定义窗口
cv::imshow("Original Image", image); // 显示图像
显示图像的这条语句之所以还要出现窗口名称,是为了指定究竟把图像显示到哪个窗口去,因为可能存在多个窗口。
- 图像翻转
cv::Mat result;
cv::flip(image, result, 1); // 1表示水平翻转
// 0表示垂直翻转
// 负数表示既有水平也有垂直翻转
- 等待用户输入
cv::waitKey(0); //括号中填的数字是毫秒数,0为一直等待
如果没有这句话,显示的图像会一闪而过。
- 将图像写到磁盘
cv::imwrite("output.bmp", result);
文件的后缀名决定了图像保存时的编码格式。
- 指定初始尺寸
cv::Mat ima(240, 320, CV_8U, cv::Scalar(100));
CV_8U对应的是单字节的像素图象,字母U意味着无符号的(Unsigned)。对于彩色图像,需要指定3个通道(CV_8UC3)。
当 cv::Mat 对象离开作用域后,分配的内存将自动释放,从而避免内存泄漏的困扰。
另外,cv::Mat 实现了引用计数以及浅拷贝,当图像之间进行赋值时,图像数据并没有发生复制,两个对象都指向同一块内存块。这也可用于参数传值的图像,以及返回值传值的图像。引用计数的作用是当所有引用内存数据的对象都被析构后,才会释放内存块。如果你希望创建的图像拥有原始图像的崭新拷贝,那么可以使用copyTo()方法。
cv::Mat image2, image3;
image2 = result; // 两幅图像拥有同一份数据
result.copyTo(image3); // 创建新的拷贝
如果翻转output图像,并显示image2和image3,可以看到image2页翻转了,而image3没有变。
同理,函数返回其实也是一次浅拷贝过程。
cv::Mat function() {
// 创建图像
cv::Mat ima(240, 320, CV_8U, cv::Scalar(100));
// 并返回它
return ima;
}
// 得到灰度图
cv::Mat gray = function();
在函数function内,ima只是个局部变量,在离开作用域时应当被析构掉,但由于他所关联的引用计数表示内部图像正在被另一个对象gray所引用,因此内存块并不会被释放。
<div id="Section2">第2章 操作像素</div>
- 彩色或灰度图像存取像素值
void salt(cv::Mat &image, int n) {
for (int k = 0; k < n; k++) {
// rand() 是随机数生成函数
int i = rand() % image.cols;
int j = rand() % image.rows;
if (image.channels() == 1) { // 灰度图
image.at<uchar>(j,i) = 255;
} else if (image.channels() == 3) { // 彩色图
image.at<cv::Vec3b>(j,i)[0] = 255;
image.at<cv::Vec3b>(j,i)[1] = 255;
image.at<cv::Vec3b>(j,i)[2] = 255;
}
}
}
-
类 cv::Mat 有若干成员函数可以获取图像的属性。公有成员变量 cols 和 rows 给出了图像的宽和高。成员函数 at(int y, int x) 可以用来存取图像元素。 但是必须在编译期知道图像的数据类型,因为 cv::Mat 可以存放任意数据类型的元素。这也是这个函数用模板函数来实现的原因。所以 at 方法要指定数据类型,而且 at 方法本身不会进行任何数据类型转换。
-
cv::Vec3b,即由三个 unsigned char 组成的向量。
image.at<cv::Vec3b>(j,i)[channel] = value;
索引值 channel 标明了颜色通道号。
类似的,还有二元素向量类 cv::Vec2b 和四元素向量类 cv::Vec4b,s 代表 short,i 代表 int,f 代表 float,d 代表 double。所有这些类型都是使用模板类 cv::Vect<T, N> 定义的,其中 T 代表类型,N 代表向量中的元素个数。
- 有时候使用 cv::Mat 的成员函数会很麻烦,因为返回值的类型必须通过在调用时通过模板参数指定。因此,OpenCV 提供了类 cv::Mat_,它是 cv::Mat 的一个模板子类。在事先知道矩阵类型的情况下,使用 cv::Mat_ 可以带来一些便利。这个类额外定义了一些方法,但是没有任何成员变量,所以此类的指针或者引用可以直接进行相互类型转换。该类重载了操作符 (),允许我们可以通过它直接存取矩阵元素。因此,假设有一个 uchar 类型的矩阵,我们可以这样写:
cv::Mat_<uchar> im2 = image; // im2 指向 image
im2(50, 100) = 0; // 存取第 50 行,100列
由于 cv::Mat_ 的元素类型在创建实例的时候已经声明,操作符 () 在编译期就知道要返回的数据类型。使用操作符 () 得到返回值和使用 cv::Mat 的 at 方法得到的返回值是完全一致的,而且写起来更加简洁。
- 双重循环遍历所有像素值:
void colorReduce(cv::Mat &image, int div = 64