目录

0x01 OpenCv的基础知识

0x02 操作像素


关于如何安装opencv,如何配置环境,这些网上有太多关于这方面的资料了,在这只是我的一些入门笔记:

  • 编译环境:Visual Studio 2019
  • 编程语言:Cpp

0x01 OpenCv的基础知识

(一)OpenCv库分为多个模块:

  • opencv_core模块包含库的核心功能
  • opencv_imgproc模块包含主要的图像处理函数
  • opencv_highgui模块提供了读写图像和视频的函数以及一些用户交互函数

在网上下下来的opencv,安装后所生成的文件中,就有以上这几个模块对应的hpp文件,在调用的时候注意路径问题即可。

那么如何实现?先让其显示一幅图像,因此需要定义了图像数据结构的核心偷吻加和包含了所有图形接口的函数highgui头文件。之后要定义一个Mat类型的对象,接下来调用读函数,就会从文件中读取一个图像,解码,然后分配内存:

#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>

int main()
{
	cv::Mat image;

	image = cv::imread("./image/circle.jpg");

	//显示分辨率
	std::cout << "This image is " << image.rows << "x" << image.cols << std::endl;

	//错误检查,如果没有分配图像数据,则empty方法将返回true
	if (image.empty())
	{
		printf("Error");
		return -1;
	}
	
	//定义窗口(可选)
	cv::namedWindow("Circle");
	//显示图像
	cv::imshow("Circle",image);
	//保持显示
	cv::waitKey(0);
}

之后就是对图像进行处理,opencv有些转换是就地进行的,即转换过程直接在输入的图像上进行,不创建新的图像,比如说翻转。但是我们要进行二值化处理的时候,习惯性的执行创建新的图像。之后我们可以将其存储起来:

#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>

int main()
{
	cv::Mat image;
	cv::Mat result;

	image = cv::imread("./image/circle.jpg");

	//显示分辨率
	std::cout << "This image is " << image.rows << "x" << image.cols << std::endl;

	//错误检查,如果没有分配图像数据,则empty方法将返回true
	if (image.empty())
	{
		printf("Error");
		return -1;
	}
	//定义窗口(可选)
	cv::namedWindow("Circle");
	//显示图像
	cv::imshow("Circle",image);
	//正数表示水平 0表示垂直 负数表示水平和垂直
	cv::flip(image, result, 1);
	//定义窗口(可选)
	cv::namedWindow("Circle_result");
	//显示图像
	cv::imshow("Circle_result", result);
	//存储图像
	cv::imwrite("./image/circle_result.jpg",result);
	//保持显示,0表示永远的等待按键,键入的正数表示等待的毫秒数
	cv::waitKey(0);
}

(二)实现原理

在opencv的c++API中,所有类和函数都在命名空间cv内定义。访问它们的方法共有两种,第一种是在定义main函数前使用如下声明:

using namespace cv;

第二种方法是根据命名空间规范给所有OpenCV的类和函数加上前缀cv::。

highgui模块中有一批能帮助我们轻松显示图像并对图像进行操作的函数。在使用imread函数装载图像时,可以通过设置选项把它转为灰度图像。

//转为灰度图像
image = cv::imread("./image/circle.jpg",cv::IMREAD_GRAYSCALE)

此时三个通道的值是相同的。如果在读入图像的时候进行色彩转换,可以提高运行速度并减少内存使用,读入时把它转换成三通道色彩图像。

image = cv::imread("./image/circle.jpg",cv::IMREAD_COLOR)

当用imread打开路径指定不完整的图像时,imread会自动采用默认目录。如果从控制台运行程序,默认目录显然就是当前控制台的目录;如果在IDE中运行程序,默认目录通常就是项目文件所在的目录。因此,要确保图像文件在正确的目录下。

当用imshow显示由整数(CV_16U表示16位无符号整数,CV_32S表示32位有符号整数)构成的图像时,图像每个像素的值都会被除以256,以便能够在256级灰度中显示。同样,在显示由浮点数构成的图像时,值得范围会被假设位0.0(显示黑色)~1.0(显示白色)。超出这个范围额值会显示为白色(大于1.0的值)或黑色(小于0.0的值)。

就地处理的方式:

cv::flip(image,image,1);

(三)一些扩展

highgui模块中有大量可用来处理图像的函数,可以使程序对鼠标或键盘事件做出响应,也可以在图像上绘制形状或写入文本。

(1)点击图像

需要定义一个回调函数。对于鼠标事件处理函数,回调函数必须具有这种签名:

void onMouse(int event,int x,int y,int flags,void* param);
  • event:表示除法回调函数的鼠标事件类型。
  • x,y:事件发生时鼠标的位置,用像素坐标表示。
  • flags:鼠标按下了哪个键
  • *param:指向任意对象的指针,作为附加的参数发送给函数。

注册回调函数:

cv::setMouseCallback("image",onMouse,reinterprer_cast<void*>(&image));

回调函数不会被显式地调用,但是会在响应特定事件的时候被调用。为了能被程序识别,回调函数需要具有特定的签目,并且必须注册。

对于回调函数:

回调函数就是一个通过指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

机制如下:

  • 定义一个回调函数
  • 提供函数实现的一方在初始化的时候,将回调函数的函数指针注册给调用者
  • 当特定的事件或条件发生时,调用者使用函数指针调用回调函数对事件进行处理

简而言之,回调函数就是允许用户把需要调用的方法的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。

那么对于setMouseCallback:

  • 第一个参数:回调函数需要注册到窗口的名,即产生事件的窗口。
  • 第二个参数:指定窗口里每次鼠标事件发生的时候,被调用的函数指针,回调函数。
  • 第三个参数:用来传递额外的信息给前面提及到的void* param。

那么我们这么使用即可:

#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>

void onMouse(int event, int x, int y, int flags, void* param)
{
	cv::Mat* im = reinterpret_cast<cv::Mat*>(param);
	switch (event)		//调度事件
	{
		//鼠标左键按下事件
		case cv::EVENT_LBUTTONDOWN:	
			//显示像素值(x,y)
			std::cout << "at (" << x << "." << y << ") value is:" << static_cast<int>(im->at<uchar>(cv::Point(x, y))) << std::endl;
			break;	
		default:
			break;
	}
}

int main()
{
	cv::Mat image;

	//转为灰度图像
	image = cv::imread("./image/circle.jpg",cv::IMREAD_GRAYSCALE);

	//显示分辨率
	std::cout << "This image is " << image.rows << "x" << image.cols << std::endl;

	//错误检查,如果没有分配图像数据,则empty方法将返回true
	if (image.empty())
	{
		printf("Error");
		return -1;
	}
	//定义窗口(可选)
	cv::namedWindow("Circle");
	//显示图像
	cv::imshow("Circle",image);

	cv::waitKey(1);
	cv::setMouseCallback("Circle", onMouse, reinterpret_cast<void*>(&image));
	//保持显示,0表示永远的等待按键,键入的正数表示等待的毫秒数
	cv::waitKey(0);
}

鼠标事件的回调函数可能收到的事件还有cv::EVENT_MOUSEMOVE、cv::EVENT_LBUTTONUP、 cv::EVENT_RBUTTONDOWN 和 cv::EVENT_RBUTTONUP。

(2)绘图

opencv可在图像上绘图,基本的形状绘制函数有circle、ellipse、line和rectangle。使用例子:

cv::circle(
			image,					//目标图像
			cv::Point(309.298),		//中心点坐标
			150,						//半径
			0,						//颜色(这里使用黑色)
			3						//厚度
);

 写字:

cv::putText(
        image,					//目标图像
        "Hello",				//文本
        cv::Point(200.300),		//位置
        cv::FONT_HERSHEY_PLAIN,	//字体类型
        2.0,					//字体大小
        255,					//字体颜色(白色)
        2						//文本厚度
);

(四)cv::Mat

程序中的关键部件,它是程序库中的关键部件,用来操作图像和矩阵(从计算机和数学的角度看,图像其实就是矩阵)。在开发程序时,会经常用到这个数据结构。

cv::Mat有两个必不可少的组成部分:一个是头部,一个是数据块。头部包含了矩阵的所有相关信息(大小、通道数量、数据类型等)。数据块包含了图像中所有像素的值。头部有一个指向数据块的指针,即data属性。cv::Mat 有一个很重要的属性,即只有在明确要求时,内存块才会被复制。实际上,大多数操作仅仅复制了cv::Mat的头部,因此多个对象会指向同一个数据块。这种内存管理模式可以提高应用程序的运行效率,避免内存泄漏,但是我们必须了解它带来的后果。

cv::Mat对象默认大小为0,但是可以指定一个初始大小,例如:

//创建图像
cv::Mat ima(500,500,CV_8U,50);

 我们需要指定每个矩阵元素的类型,这里用 CV_8U 表示每个像素对应 1 字节(灰度图像), 用字母 U 表示无符号;你也可用字母 S 表示有符号。对于彩色图像,你应该用三通道类型 (CV_8UC3),也可以定义 16 位和 32 位的整数(有符号或无符号),例如 CV_16SC3。我们甚至 可以使用 32 位和 64 位的浮点数(例如 CV_32F)。图像(或矩阵)的每个元素都可以包含多个值(例如彩色图像中的三个通道),因此 OpenCV 引入了一个简单的数据结构 cv::Scalar,用于在调用函数时传递像素值。该结构通常包含一个 或三个值。如果要创建一个彩色图像并用红色像素初始化,可用如下代码:

//创建一个红色的图像
//通道次序为BGR
cv::Mat image2(240, 320, CV_8UC3, cv::Scalar(0, 0, 255));

与之类似,初始化灰度图像可这样使用这个数据结构:cv::Scalar(100)。

图像的尺寸信息通常也需要传递给调用函数。前面讲过,我们可以用属性 cols 和 rows 来 获得 cv::Mat 实例的大小。cv::Size 结构包含了矩阵高度和宽度,同样可以提供图像的尺寸 信息。另外,可以用 size()方法得到当前矩阵的大小。当需要指明矩阵的大小时,很多方法都使用这种格式。

// 创建一个未初始化的彩色图像
cv::Mat image2(cv::Size(320,240),CV_8UC3);

 可以随时用 create 方法分配或重新分配图像的数据块。如果图像已被分配,其原来的内容会先被释放。出于对性能的考虑,如果新的尺寸和类型与原来的相同,就不会重新分配内存:

// 重新分配一个新图像
//(仅在大小或类型不同时)
image1.create(200,200,CV_8U);

一旦没有了指向 cv::Mat 对象的引用,分配的内存就会被自动释放。这一点非常方便,因 为它避免了 C++动态内存分配中经常发生的内存泄漏问题

当在两幅图像之间赋值时,图像数据(即像素)并不会被复制,此时两幅图像都指向同一个内存块。这同样适用于图像间的值传递或值返回。由于维护了一个引用计数器,因此只有当图像的所有引用都将释放或赋值给另一幅图像时,内存才会被释放。

对上面图像中的任何一个进行转换都会影响到其他图像。如果要对图像内容做一个深复制, 你可以使用 copyTo 方法,目标图像将会调用 create 方法。另一个生成图像副本的方法是 clone

// 这些图像是原始图像的新副本
image3.copyTo(image2); 
cv::Mat image5= image3.clone();

 如果要改变其数据类型:

image1.convertTo(image2,CV_32F,1/255.0,0.0);

本例中的原始图像被复制进了一幅浮点型图像。这一方法包含两个可选参数:缩放比例偏移量。需要注意的是,这两幅图像的通道数量必须相同。

也可以编写返回一副图像的函数(或类方法):

//创建一副图像
cv::Mat function() {
	//创建图像
	cv::Mat ima(500,500,CV_8U,50);
	return ima;
}

 调用:

// 得到一个灰度图像
cv::Mat gray= function();

运行这条语句后,就可以使用变量gray操作这个由function函数创建的图像,而不需要额外分配内存了。

正如前面解释的,从 cv::Mat 实例到灰度图像实际上只是进行了一次浅复制。 当局部变量 ima 超出作用范围后,ima 会被释放。但是从相关引用计数器可以看出,另一个实例 (即变量 gray)引用了 ima 内部的图像数据,因此 ima 的内存块不会被释放。

0x02 操作像素

图像本质上就是一个由数值组成的矩阵。正因为如此,OpenCV 使用了 cv::Mat 结构来操作图像,对灰度图像(黑白图像)而言, 像素是 8 位无符号数(数据类型为 unsigned char),0 表示黑色,255 表示白色。

(1)访问像素值

要访问矩阵中的每个独立元素,只需要指定它的行号列号即可。返回的对应元素可是单个数值,也可以时多通道图像的数值向量。

cv::Mat 类包含多种方法,可用来访问图像的各种属性:利用公共成员变量 cols 和 rows 可得到图像的列数和行数;利用 cv::Mat 的 at(int y,int x)方法可以访问元素,其中 x 是列号,y 是行号。。在编译时必须明确方法返回值的类型,因为 cv::Mat 可以接受任何类型的元 素,所以程序员需要指定返回值的预期类型。

所以使用如下:

image.at<uchar>(j,i)= 255;

 彩色图像的每个像素对应三个部分:红色通道、绿色通道和蓝色通道,因此包含彩色图像的 cv::Mat 类会返回一个向量,向量中包含三个 8 位的数值。OpenCV 为这样的短向量定义了一种 类型,即 cv::Vec3b。这个向量包含三个无符号字符(unsigned character)类型的数据。因此, 访问彩色像素中元素的方法如下所示:

image.at(j,i)[channel]= value;

channel 索引用来指明三个颜色通道中的一个。OpenCV 存储通道数据的次序是蓝色、绿色 和红色(因此蓝色是通道 0)。你也可以直接使用短向量,方法如下所示:

image.at(j, i) = cv::Vec3b(255, 255, 255);

修改图像的函数在使用图像作为参数时,都采用了值传递的方式。它们在复制图像时仍共享了同一块图像数据。因此在修改图像内容时,图像参数没必要采用引用传递的方式。

也可以使用这么访问:新方法中有一个 operator(),可用来直接访问矩阵的元素。

// 用 Mat 模板操作图像
cv::Mat_<uchar> img(image); 
img(50,100)= 0; // 访问第 50 行、第 100 列处那个值

(2)扫描图像

减色算法:假设 N 是减色因子,将图像中每个像素的值除以 N(这里假 定使用整数除法,不保留余数)。然后将结果乘以 N,得到 N 的倍数,并且刚好不超过原始像素 值。加上 N / 2,就得到相邻的 N 倍数之间的中间值。对所有 8 位通道值重复这个过程,就会得到 (256 / N) × (256 / N) × (256 / N)种可能的颜色值。

将旧图像中的每一个颜色值划分成一个方块,并将该方块的中间值作为新的颜色值。new=old//N×N+N/2

减色函数:

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 的地址
        uchar* data= image.ptr<uchar>(j); 
        for (int i=0; i<nc; i++) { 
        // 处理每个像素 --------------------- 
        data[i]= data[i]/div*div + div/2; 
        // 像素处理结束 ---------------- 
    } // 一行结束
    } 
}

可以用下面的代码片段测试这个函数:

// 读取图像
image= cv::imread("boldt.jpg"); 
// 处理图像
colorReduce(image,64); 
// 显示图像
cv::namedWindow("Image"); 
cv::imshow("Image",image);

一个宽 W 高 H 的图像所需的内存块大小为 W×H×3 uchars。如果行数是某个数字(例如 8)的整数倍,图像处理的性 能可能会提高,因此最好根据内存配置情况将数据对齐。我们会用几个额外的像素来填补行的长度,当然,这些额外的像素既不会显示也不会被保持,因此就是最好根据内存配置的情况将其数据对齐

获得每一行中像素值的个数:


int nc= image.cols * image.channels();


以直接访问图像中一行的 起始地址:


uchar* data= image.ptr<uchar>(j);


即利用指针运算从一列移到下一 列:


*data++= *data/div*div + div2;


也可以使用取模运算:


data[i]= data[i] – data[i]%div + div/2;


还可以使用位运算符。。如果把减色因子限定为 2 的指数,即 div=pow(2,n),那么把 像素值的前 n 位掩码后就能得到最接近的 div 的倍数。可以用简单的位移操作获得掩码。

// 用来截取像素值的掩码
uchar mask= 0xFF<<n; // 如 div=16,则 mask= 0xF0

*data &= mask; // 掩码
*data++ += div>>1; // 加上 div/2 
// 这里的+也可以改用“按位或”运算符

使用位运算的代码效率很高。

前面的处理为就地转换,但是我们这种做法不需要额外的图像来输出结果,可以减少内存的使用。但是有些程序不允许对源图像进行修改,这个时候旧应该在调用函数前备份图像。那么对图像进行深复制的最简单的方法时使用clone()方法。

// 读入图像
image= cv::imread("boldt.jpg"); 
// 复制图像
cv::Mat imageClone= image.clone(); 
// 处理图像副本
// 原始图像保持不变
colorReduce(imageClone); 
// 显示结果图像
cv::namedWindow("Image Result"); 
cv::imshow("Image Result",imageClone);

对于colorReduce:

//输入图像是一个引用的 const,表示这幅图像不会在函数中修改。
void colorReduce(const cv::Mat& image, // 输入图像
	cv::Mat& result, // 输出图像
	int div = 128)
{
	result.create(image.rows, image.cols, image.type());

	int nc = image.cols * image.channels();
	int nl = image.rows; // 行数
	for (int j = 0; j < nl; j++) {
		// 获得第 j 行的输入和输出的地址
		const uchar* data_in = image.ptr<uchar>(j);
		uchar* data_out = result.ptr<uchar>(j);
		for (int i = 0; i < nc; i++) {
			// 处理每个像素 --------------------- 
			data_out[i] = data_in[i] / div * div + div / 2;
			// 像素处理结束 ---------------- 
		} // 一行结束
	}
}

如果输入和输出参数用了同一幅图像,这个函数就与本节前面的版本完全等效。如果输出用了另一幅图像,不管在调用函数前是否已经分配了这幅图像,函数都会正常运行。如果输入输出同一幅图像,那就可以这么使用:


colorReduce(image,image);


这里的关键是先检查输出图像,验证它是否分配了一定大小的数据缓冲区,以及像素类型与 输入图像是否相符——所幸 cv::Mat 的 create 方法中已经包含了这个检查过程。当你用新的大小和像素类型重新分配矩阵时,就要调用 create 方法。如果矩阵已有的大小和类型刚好与指 定的大小和类型相同,这个方法就不会执行任何操作,也不会修改实例,而只是直接返回。

(3)对连续图像的高效扫描

为了提高性能,可以在图像的每行末尾用额外的像素进行填充。有趣的是,在 去掉填充后,图像仍可被看作一个包含 W×H 像素的长一维数组。用 cv::Mat 的 isContinuous 方法可轻松判断图像有没有被填充。如果图像中没有填充像素,它就返回 true。

也可以这么测试矩阵的连续性:

// 检查行的长度(字节数)与“列的个数×单个像素”的字节数是否相等 
image.step == image.cols*image.elemSize();

 那么处理这个连续性有什么用?但是不管是哪种情况,都可以使用isContinuous方法检查矩阵的连续性。在一些特殊的处理算法中, 你可以充分利用图像的连续性,在单个(更长)循环中处理图像。

void colorReduce(cv::Mat image, int div=64) { 
     int nl= image.rows; // 行数
     // 每行的元素总数
     int nc= image.cols * image.channels(); 
     if (image.isContinuous()) { 
         // 没有填充的像素
         nc= nc*nl; 
         nl= 1; // 它现在成了一个一维数组
     } 
     int n= staic_cast<int>(log(static_cast<double>(div))/log(2.0) + 0.5);
     // 用来截取像素值的掩码
     uchar mask= 0xFF<<n; // 如果 div=16, 那么 mask= 0xF0 
     uchar div2 = div >> 1; // div2 = div/2
    // 对于连续图像,这个循环只执行一次
     for (int j=0; j<nl; j++) { 
         uchar* data= image.ptr<uchar>(j); 
         for (int i=0; i<nc; i++) { 
         *data &= mask; 
         *data++ += div2; 
         } // 一行结束
     } 
}

4)用迭代器扫描图像

标准模板库 (Standard Template Library,STL)对每个集合类都定义了对应的迭代器类,OpenCV 也提供了 cv::Mat 的迭代器类,并且与 C++ STL 中的标准迭代器兼容。减色函数可以这么写:

void colorReduce(cv::Mat image, int div=64) { 
     // div 必须是 2 的幂
     int n= staic_cast<int>(log(static_cast<double>(div))/log(2.0) + 0.5);
     // 用来截取像素值的掩码
     uchar mask= 0xFF<<n; // 如果 div=16, mask=0xF0 
     uchar div2 = div >> 1; // div2 = div/2 
     // 迭代器
     cv::Mat_<cv::Vec3b>::iterator it= image.begin<cv::Vec3b>(); 
     cv::Mat_<cv::Vec3b>::iterator itend= image.end<cv::Vec3b>(); 
     // 扫描全部像素
     for ( ; it!= itend; ++it) { 
     (*it)[0]&= mask; 
     (*it)[0]+= div2; 
     (*it)[1]&= mask; 
     (*it)[1]+= div2; 
     (*it)[2]&= mask; 
     (*it)[2]+= div2; 
     } 
}

(5)扫描图像并访问相邻像素

基于拉普拉斯算子的锐化函数。如果从图像中减去拉普拉斯算子部分,图像 的边缘就会放大,因而图像会变得更加尖锐。

可以用以下方法计算锐化的数值:


sharpened_pixel= 5*current-left-right-up-down;  


这里的 left 是与当前像素相邻的左侧像素,up 是上一行的相邻像素,以此类推。在计算的时候,我们需要访问它相邻的像素:

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)); 
}

程序原理如下:

  • 若要访问上一行和下一行的相邻像素,只需定义额外的指针,并与当前行的指针一起递增, 然后就可以在扫描循环内访问上下行的指针了。
  • cv::saturate_cast:计算像素的数学表达式的结果经常超出允许的范围(即小于 0 或大于255)。使用这个 函数可把结果调整到 8 位无符号数的范围内,具体做法是把小于 0 的数值调整为 0,大于 255 的 数值调整为 255——这就是 cv::saturate_cast函数的作用。
  • 由于边框上的像素没有完整的相邻像素,因此不能用前面的方法计算,需要另行处理。这里 简单地把它们设置为 0。有时也可以对这些像素做特殊的计算,但在大多数情况下,花时间处理这些极少数像素是没有意义的。用两个特殊的方法把边框的像素设置为了 0,它 们是 rowcol。只要这个一 维矩阵的元素被修改,原始图像也会被修改。我们用 setTo 方法来实现这个功能,此方法将对 矩阵中的所有元素赋值。result.row(0).setTo(cv::Scalar(0)); 这个语句把结果图像第一行的所有像素设置为 0。

一些扩展阅读:

在对像素领域进行计算时,通常用一个核心矩阵来表示,这个核心矩阵展现了如何将计算相关的像素组合起来,才可以得到预期结果。那么对于公式:

sharpened_pixel= 5*current-left-right-up-down;

其实可以表示为一个矩阵:

opencv常见的面试选择题 opencv讲解_计算机视觉

看到这应该会觉得,这个就是卷积的那个思想,是的,没有猜错。

核心矩阵中的每个单元格表示相关像素的乘法系数,像素应用核心矩阵得到的结果,即这些乘积的累加。核心矩阵的大小就是邻域的尊重版权 2.7 实现简单的图像运算大小(这里是 3×3)。根据锐化滤波器的要求,水平和垂直方向的四个相 邻像素与1 相乘,当前像素与 5 相乘。核心矩阵定义了一个用于图像的滤波器。

滤波是图像处理中的常见操作,opencv专门定义了一个函数:cv::filter2D。要使用这个函数,只需要定义一个内核(以矩阵的形式),调用函数并传入图像和内核,即可返 回滤波后的图像。因此,使用这个函数重新定义锐化函数非常容易:

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); 
}

这种实现方式得到的结果与前面的完全相同(执行效率也相同)。如果处理的是彩色图像, 三个通道可以应用同一个内核。注意,使用大内核的 filter2D 函数是特别有利的,因为这时它使用了更高效的算法。

(6)分割图像通道

这当 然可以通过图像扫描循环实现,但也可以使用 cv::split 函数,将图像的三个通道分别复制到 三个 cv::Mat 实例中。假设我们要把一张雨景图只加到蓝色通道中,可以这样实现:

// 创建三幅图像的向量
std::vector<cv::Mat> planes; 
// 将一个三通道图像分割为三个单通道图像
cv::split(image1,planes); 
// 加到蓝色通道上
planes[0]+= image2; 
// 将三个单通道图像合并为一个三通道图像
cv::merge(planes,result);

 cv::merge 函数执行反向操作,即用三个单通道图像创建一个彩色图像。