最近在系统地学习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;
}
在对像素邻域进行计算时, 通常用一个核心矩阵来表示。 这个核心矩
阵展现了为得到预期结果, 如何将计算相关的像素组合起来。 针对本
节使用的锐化滤波器, 核心矩阵可以是这样的:
鉴于滤波是图像处理中常见的操作,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);
操作的结果是一个新图像,如下图所示: