最近在系统地学习OpenCV,将学习的过程在此做一个记录,主要以代码+注释的方式
记录学习过程。
1.访问像素值
要访问矩阵中的每个独立元素,只需要指定它的行号和列号。返回的对应元素可以是单个数值,也可
以是多通道图像的数值向量。给图像加入椒盐噪声(salt-and-pepper noise),来说明如何直接访问像素值。顾名思义,椒盐噪声是一个专门的噪声类型,它随机选择一些像素,把它们的颜色替换成白色或黑色。
#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>void salt(cv::Mat image, int n){ int i, j; for (int k = 0; k < n; k++) { // rand()是随机数生成器 //利用cv::Mat中公共成员变量cols和rows得到图像的列数和行数 i = std::rand() % image.cols; j = std::rand() % image.rows; //使用type方法来区分灰度图像和彩色图像。 if (image.type() == CV_8UC1) // 灰度图像 { //利用cv::Mat的at(int y,int x)方法可以访问元素 //at方法被实现成一个模板方法,在调用时必须指定图像元素的类型 image.at<uchar>(j, i) = 255; } else if (image.type() == CV_8UC3) // 彩色图像 { /*彩色图像的每个像素对应三个部分:红色、绿色和蓝色通道。因此包 含彩色图像的cv::Mat类会返回一个向量,向量中包含三个8位的数值。 OpenCV为这样的短向量定义了一种类型,即cv::Vec3b。这个向量包含 三个无符号字符(unsigned character)类型的数据。因此,访问彩色 像素中元素的方法如下:*/ image.at<cv::Vec3b>(j, i)[0] = 255; image.at<cv::Vec3b>(j, i)[1] = 255; image.at<cv::Vec3b>(j, i)[2] = 255; } }}/*修改图像的函数在使用图像作为参数时,都采用了值传递的方式。之所以这样做,是因为它们在复制图像时仍共享了同一块图像数据。因此在需要修改图像内容时,图像参数没必要采用引用传递的方式*/int main(){ // 打开图像 cv::Mat image = cv::imread("boldt.jpg"); // 调用函数以添加噪声 salt(image, 3000); // 显示图像 cv::namedWindow("Image"); cv::imshow("Image", image); cv::waitKey(0); return 0;}运行结果:
2.用指针遍历图像
以减少图像中颜色的数量这个任务为例,来说明遍历图像的过程。
彩色图像由三个通道组成,每个通道对应三原色(红、绿、蓝)之一的强度。由于每个强度值
都是用一个8位的unsigned char表示,所以全部可能的颜色数目为256 × 256 × 256,
大于1600万个。理所当然,为了降低分析的复杂度,降低图像中的颜色数目有时是有用的。
基本的减色算法很简单。假设N是减色因子,将图像中每个像素的每个通道的值除以N
(使用整数除法,不保留余数)。然后将结果乘以N,得到N的倍数,并且刚好不超过原始像素值。
只需加上N/2,就得到相邻的N倍数之间的中间值。对所有8位通道值重复这个过程,就会得到(256/N)× (256/N)×(256/N)种可能的颜色值。
#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>void colorReduce(cv::Mat image, int div = 64){ int nl = image.rows; // 行数 // 每行的元素数量 int nc = image.cols * image.channels(); for (int j = 0; j < nl; j++) { // 取得行j的地址 /*为了简化指针运算的计算过程,cv::Mat类提供ptr函数,可以 直接访问图像中任一行的地址。ptr函数是一个模板函数, 返回第j行的地址:*/ uchar* data = image.ptr<uchar>(j); for (int i = 0; i < nc; i++) { // 处理每个像素 --------------------- data[i] = data[i] / div*div + div / 2; // 像素处理结束 ----------- /*注意在处理语句中, 我们也可以采用另一种等价的做法, 即利用指针 运算从一列移到下一列。 因此可以使用下面的代码:*/ //*data = *data / div*div + div2; data++; } // 一行结束 }}int main(){ // 读取图像 cv::Mat image = cv::imread("boldt.jpg"); // 处理图像 colorReduce(image, 64); // 显示图像 cv::namedWindow("Image"); cv::imshow("Image", image); cv::waitKey(0); return 0;}运行结果:
3.用迭代器遍历图像 在面向对象编程时,我们通常用迭代器对数据集合进行循环遍历。迭代器是一种类,
专门用于遍历集合的每个元素,隐藏了遍历过程的具体细节。标准模板库(STL)对容器类型
都定义了对应的迭代器,OpenCV也提供了cv::Mat的迭代器,并且与C++ STL中的标准迭代器兼容。
依然以减色程序为例。
#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>void colorReduce(cv::Mat &image, int div = 64) { // 在初始位置获得迭代器 /*要得到cv::Mat实例的迭代器,首先要创建一个cv::MatIterator_对象。 跟cv::Mat_类似,这个下划线表示它是一个模板子类。 因为图像迭代器是用来 访问图像元素的,所以必须在编译时就明确返回值的类型。 可以这样定义迭代器:*/ cv::Mat_<cv::Vec3b>::iterator it; /*然后就可以使用常规的迭代器方法begin和end对像素进行循环遍历了。 不同之处在于它们仍然是模板方法。*/ it = image.begin<cv::Vec3b>(); // 获得结束位置 cv::Mat_<cv::Vec3b>::iterator itend = image.end<cv::Vec3b>(); // 循环遍历所有像素 for (; it != itend; ++it) { // 处理每个像素 --------------------- /*注意这里处理的是一个彩色图像, 因此迭代器返回cv::Vec3b实 例。 你可以用取值运算符[]访问每个颜色通道的元素。*/ (*it)[0] = (*it)[0] / div*div + div / 2; (*it)[1] = (*it)[1] / div*div + div / 2; (*it)[2] = (*it)[2] / div*div + div / 2; // 像素处理结束 ---------------- }}int main(){ // 读取图像 cv::Mat image = cv::imread("boldt.jpg"); // 处理图像 colorReduce(image, 64); // 显示图像 cv::namedWindow("Image"); cv::imshow("Image", image); cv::waitKey(0); return 0;}不管扫描的是哪种类型的集合,使用迭代器时总是遵循同样的模式。
首先你要使用合适的专用类创建迭代器对象,在本例中是cv::Mat_<cv::Vec3b>:: iterator,
然后可以用begin方法,在开始位置(本例中为图像的左上角)初始化迭代器。对于cv::Mat实例,
可以使用image.begin<cv::Vec3b>()。
还可以在迭代器上使用数学计算,例如若要从图像的第二行开始,可以用
image.begin<cv::Vec3b>()+image.cols初始化cv::Mat迭代器。
获得集合结束位置的方法也类似,只是改用end方法。但是,用end方法得到的迭代器已经超出了
集合范围,因此必须在结束位置停止迭代过程。结束的迭代器也能使用数学计算,例如,如果你想在最后一行前就结束迭代, 可使用image.end<cv::Vec3b>()-image.cols。初始化迭代器后,建立一个循环遍历所有元素,直到与结束迭代器相等。
典型的while循环就像这样:
while (it!= itend) {// 处理每个像素 ---------------------// 像素处理结束 ---------------------++it;}你可以用运算符++来移动到下一个元素,也可以指定更大的步幅。例如用it+=10,
对每10个像素处理一次。最后,在循环内部使用取值运算符*来访问当前元素,你可以用它来读(例如element= *it;)或写(例如*it= element;)。
运行结果(同2中指针遍历的效果):
4.检查代码运行效率为了衡量函数或代码段的运行时间,OpenCV有一个非常实用的函数,即cv::getTickCount(),
该函数返回从最近一次电脑开机到当前的时钟周期数。因为我们希望得到以秒为单位的代码运行时间,所以要使用另一个方法,即cv::getTickFrequency(),这个方法返回每秒的时钟周期数。
为了获得某个函数(或代码段)的运行时间,通常需使用这样的程序模板:
const int64 start = cv::getTickCount();colorReduce(image); // 调用函数// 经过的时间( 单位: 秒)double duration = (cv::getTickCount()-start)/cv::getTickFrequency();5.遍历图像和邻域操作在图像处理中计算像素值时,经常需要用它的相邻像素的值。
以对图像进行锐化为例,在图像处理领域有一个众所周知的结论:如果从图像中减去拉普拉斯算子部分,图像的边缘就会放大,因而图像会变得更加尖锐。
用以下方法计算锐化的数值:
sharpened_pixel= 5*current-left-right-up-down;
这里不能使用就地处理,使用者必须提供一个输出图像。图像扫描中使用了三个指针,一个表示当前行, 一个表示上面的行,另外一个表示下面的行。另外,在计算每一个像素时都需要访问与它相邻的像素,因此有些像素的值是无法计算的,包括第一行、最后一行、第一列、最后一列的像素。这个循环可以这样写:
#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>void sharpen(const cv::Mat &image, cv::Mat &result) { // 判断是否需要分配图像数据。 如果需要, 就分配 result.create(image.size(), image.type()); int nchannels = image.channels(); // 获得通道数 // 处理所有行( 除了第一行和最后一行) for (int j = 1; j < image.rows - 1; j++) { const uchar* PRevious = image.ptr<const uchar>(j - 1); // 上一行 const uchar* current = image.ptr<const uchar>(j); // 当前行 const uchar* next = image.ptr<const uchar>(j + 1); // 下一行 uchar* output = result.ptr<uchar>(j); // 输出行 for (int i = nchannels; i < (image.cols - 1)*nchannels; i++) { *output++ = cv::saturate_cast<uchar>( 5 * current[i] - current[i - nchannels] - current[i + nchannels] - previous[i] - next[i]); } } // 把未处理的像素设为0 result.row(0).setTo(cv::Scalar(0)); result.row(result.rows - 1).setTo(cv::Scalar(0)); result.col(0).setTo(cv::Scalar(0)); result.col(result.cols - 1).setTo(cv::Scalar(0));}int main(){ // 读取图像 cv::Mat image = cv::imread("boldt.jpg"); cv::Mat result; // 处理图像 sharpen(image, result); // 显示图像 cv::namedWindow("Image"); cv::imshow("Image", result); cv::waitKey(0); return 0;}5+.卷积操作在对像素邻域进行计算时, 通常用一个核心矩阵来表示。 这个核心矩阵展现了为得到预期结果, 如何将计算相关的像素组合起来。 针对本节使用的锐化滤波器, 核心矩阵可以是这样的:
鉴于滤波是图像处理中常见的操作,OpenCV专门为此定义了一个函数, 即cv::filter2D。 要使用这个函数, 只需要定义一个内核( 以矩阵的形式) , 调用函数并传入图像和内核, 即可返回滤波后的图像。 因此, 使用这个函数可以很容易地重新定义锐化函数:
#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>void sharpen2D(const cv::Mat &image, cv::Mat &result) { // 构造内核( 所有入口都初始化为0) cv::Mat kernel(3, 3, CV_32F, cv::Scalar(0)); // 对内核赋值 kernel.at<float>(1, 1) = 5.0; kernel.at<float>(0, 1) = -1.0; kernel.at<float>(2, 1) = -1.0; kernel.at<float>(1, 0) = -1.0; kernel.at<float>(1, 2) = -1.0; // 对图像滤波 cv::filter2D(image, result, image.depth(), kernel);}int main(){ // 读取图像 cv::Mat image = cv::imread("boldt.jpg"); cv::Mat result; // 处理图像 sharpen2D(image, result); // 显示图像 cv::namedWindow("Image"); cv::imshow("Image", result); cv::waitKey(0); return 0;}但是这段代码报错:“filter2D”: 不是“cv”的成员。不知为何。6.实现简单的图像运算
图像就是普通的矩阵,可以进行加、减、乘、除运算,我们使用算法运算符,将第二个图像与输入图像进行组合。下面就是第二个图像:
这里我们把两个图像相加,用于创建特效图或覆盖图像中的信息。 我们可以使用cv::add函数
来实现相加功能。现在我们想得到加权和,因此使用更精确的cv::addWeighted函数:
cv::addWeighted(image1,0.7,image2,0.9,0.,result);操作的结果是一个新图像,如下图所示:
新闻热点
疑难解答