OpenCV计算机视觉开发入门与实践<十六>:图像边框和图像轮廓

1.像素存放类 Scalar

  Scalar 是一个由长度为 4 的数组作为元素构成的类,Scalar 最多可以存储 4 个值,一般用来存放像素值。它由模板类 Scalar_定义而得,代码如下:

cpp 复制代码
template<typename _Tp> class Scalar_ : public Vec<_Tp, 4>
{
public:
 //! various constructors
 Scalar_();
 Scalar_(_Tp v0, _Tp v1, _Tp v2=0, _Tp v3=0); // Scalar 最多可以存储 4 个值
 Scalar_(_Tp v0);
 template<typename _Tp2, int cn>
 Scalar_(const Vec<_Tp2, cn>& v);
 ...
};
typedef Scalar_<double> Scalar;

  类 Scalar 在初始化矩阵时会经常用到,用来对每个通道赋值。比如,存放单通道图像中的像素:cv::Scalar(255),存放三通道图像中的像素:cv::Scalar(255,255,255)。我们先来看一个简单的例子。

cpp 复制代码
#include<iostream>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#pragma comment(lib, "opencv_world4140d.lib") //引用引入库
using namespace cv;
void scalar_demo1()
{
	Mat blue_m(256, 256, CV_8UC3, Scalar(255, 0, 0)); //
	Mat green_m(256, 256, CV_8UC3, Scalar(0, 255, 0));
	Mat red_m(256, 256, CV_8UC3, Scalar(0, 0, 255));
	imshow("Blue", blue_m);
	//waitKey(1000);
	imshow("Green", green_m);
	//waitKey(1000);
	imshow("Red", red_m);
	//waitKey(1000);
}

int scalar_demo2()
{
	cv::Scalar scalar(125);
	cv::Mat mat(2, 3, CV_8UC1, scalar);
	std::cout << mat << std::endl;
	std::cout << std::endl;
	cv::Scalar scalar1(0, 255);
	cv::Mat mat1(4, 4, CV_32FC2, scalar1);
	std::cout << mat1 << std::endl;
	std::cout << std::endl;
	cv::Scalar scalar2(0, 255, 255);
	cv::Mat mat2(4, 4, CV_32FC3, scalar2);
	std::cout << mat2 << std::endl;
	std::cout << std::endl;
	cv::Scalar scalar3(0, 255, 255, 0);
	cv::Mat mat3(4, 4, CV_32FC4, scalar3);
	std::cout << mat3 << std::endl;
	return 0;
}

int main()
{
	scalar_demo1();
	scalar_demo2();
}

  在 scalar_demo1 函数中,imshow 可以显示 Mat 内容。值得注意的是,OpenCV 中对 RGB 图像数据的存储顺序是 BGR,所以 Scalar(255, 0, 0)显示的是蓝色,Scalar(0, 255, 0)显示的是绿色,Scalar(0, 0, 255)显示的是红色。请记住 Scalar 的三个参数的顺序是 B、G、R。

  在 scalar_demo2 函数中,cv::Mat mat(2,3,CV_8UC1,scalar)表示创建单通道,且每个通道的值都为 125,深度为 8,2 行 3 列的图像矩阵,CV_8UC1 表示每个元素的值的类型为 8 位无符号整型,C1 表示通道数为 1,scalar(125)表示对矩阵每个元素都赋值为 125。

  cv::Mat mat1(4,4,CV_32FC2,scalar1)表示创建双通道,且每个通道的值分别为(0,255),深度为 32,4 行 4 列的图像矩阵,CV_32FC2表示每个元素的值的类型为 32 位浮点数,C2 表示通道数为 2,scalar1(0,255)第一个通道中的值都是 0,第二个通道中的值都是 255。

  cv::Mat mat2(4,4,CV_32FC3,scalar2)表示创建三通道,且每个通道的值分别为(0,255,255),深度为 32,4 行 4 列的图像矩阵,CV_32FC3 表示每个元素的值的类型

为 32 位浮点数,C3 表示通道数为 3。scalar2(0,255,255)第一个通道中的值都是 0,第二个通道中的值都是 255,第三个通道中的值都是 255。

  cv::Mat mat3(4,4,CV_32FC4,scalar3)表示创建四通道,且每个通道的值分别为(0,255,255,0),深度为 32,4 行 4 列的图像矩阵,CV_32FC4 表示每个元素的值的类型为 32 位浮点数,C4 表示通道数为 4。scalar3(0,255,255,0)第一个通道中的值都是 0,第二个通道中的值都是 255,第三个通道中的值都是 255,第四个通道中的值都是 0。

2. 为图像添加边框

  在 OpenCV 中,可以使用函数 copyMakeBorder 为图像设置边界。该函数可以为图像定义额外的填充(边框),原始边缘的行或列被复制到额外的边框。该函数声明如下:

cpp 复制代码
void cv::copyMakeBorder ( InputArray src, OutputArray dst, int top, int 
bottom, int left, int right, int borderType, const Scalar & value = Scalar() );

  其中参数 src 表示输入图像,即原图像,填 Mat 类的对象即可;dst 表示输出图像,和原图像有一样的深度,size = Size(src.cols + left +right, src.rows + top + bottom),其中 top、bottom、left、right分别表示在原图像的 4 个方向上扩充多少像素;borderType 表示边界类型,取值如下:

  • BORDER_REPLICATE:复制法,复制最边缘的像素,填充扩充的边界,如图 4-10 所示。 中值滤波就采用这种斱法。

  • BORDER_REFLECT_101:对称法,以最边缘的像素为轴,对称填充,如图 4-11 所示。这是高斯滤波边界处理的默认斱法。

  • BORDER_CONSTANT:常量法,以一个常量像素值(参数为 value)填充扩充的边界,如图所示。这种斱式在仿射变换、透视变换中很常见。

  • BORDER_REFLECT:和对称法原理一致,不过连最边缘的像素也要对称过去。

  • BORDER_WRAP:用另一侧的元素来填充这一侧的扩充边界。

【例】为图像加上边框

cpp 复制代码
#include <opencv2/opencv.hpp>
using namespace cv;
#include<iostream>
using namespace std;
#pragma comment(lib, "opencv_world4140d.lib") //引用引入库
#include <opencv2/highgui/highgui_c.h>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>
#include <stdio.h>
int main(int argc, char** argv)
{
	Mat src, dst;
	int borderType = BORDER_CONSTANT;
	const char* window_name = "copyMakeBorder Demo";
	RNG rng(12345);
	int top, bottom, left, right;
	const char* imageName = argc >= 2 ? argv[1] : "2.jpg";
	// 加载图片
	src = imread(samples::findFile(imageName), IMREAD_COLOR); // Load an image
	// Check if image is loaded fine
	if (src.empty()) 
	{
		printf(" Error opening image\n");
		printf(" Program Arguments: [image_name -- default lena.jpg] \n");
		return -1;
	}
	// Brief how-to for this program
	printf("\n \t copyMakeBorder Demo: \n");
	printf("\t -------------------- \n");
	printf(" ** Press 'c' to set the border to a random constant value \n");
	printf(" ** Press 'r' to set the border to be replicated \n");
	printf(" ** Press 'ESC' to exit the program \n");
	namedWindow(window_name, WINDOW_AUTOSIZE);
	//初始化筛选器的参数
	top = (int)(0.05 * src.rows);
	bottom = top;
	left = (int)(0.05 * src.cols);
	right = left;
	for (;;)
	{
		Scalar value(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0,
			255));
		copyMakeBorder(src, dst, top, bottom, left, right, borderType, value);
		imshow(window_name, dst);
		char c = (char)waitKey(500);
		if (c == 27) break; //esc 退出
		else if (c == 'c') borderType = BORDER_CONSTANT;
		else if (c == 'r') borderType = BORDER_REPLICATE;
	}
	return 0;
}

  在代码中,按 c 键将边界设置为随机常量值,按 r 键设置要复制的边框,按 Esc 键退出程序,注意,这些按键需要对着图像窗口来按才会起作用。在设置好 top、bottom、left 和 right 后,在循环中调用 copyMakeBorder 函数为图像增加边框。

3.在图像中查找轮廓

  在 OpenCV 中通过使用 findContours 函数,简单几个步骤就可以检测出物体的轮廓,很方便。该函数声明如下:

cpp 复制代码
void cv::findContours (InputArray image, OutputArrayOfArrays contours,
OutputArray hierarchy, int mode, int method, Point offset = Point() );

  其中参数 image 表示单通道图像矩阵,可以是灰度图,但更常用的是二值图像,一般是经过Canny、拉普拉斯等边缘检测算子处理过的二值图像;参数 contours 表示轮廓的集合,用于输出轮廓集,该参数是一个向量,并且是一个双重向量,即向量内每个元素保存了一组由连续的 Point 点构成的点的集合的向量,每一组 Point 点集就是一个轮廓,有多少轮廓,向量 contours 就有多少元素;参数 hierarchy 表示可选的输出向量,包含所抽取的图片拓扑信息,有多少条轮廓,hierarchy中就存放多少个元素。

  对于存储在 contours 中的第 i 条轮廓 contoursi,向量容器 hierarchy 中的元素 hierarchyi0、hiearchyi1、hiearchyi2和 hiearchyi3依次存储为和 contoursi在同一层级结构中的上一条轮廓、下一条轮廓、下一层级结构中的子轮廓以及上一层级结构中的父轮廓的索引指数索引。如果第 i 条轮廓 contoursi没有上一条、下一条、父轮廓或子轮廓,则 hiearchyi中的对应的元素将存储为负数。

  参数 mode 表示轮廓提取模式,取值如下:

  • CV_RETR_EXTERNAL:只提取最外层的轮廓,包含在外围轮廓内的内围轮廓被忽略。
  • CV_RETR_LIST:检测所有的轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关系,彼此乊间独立,没有等级关系,这就意味着这个检索模式下不存在父轮廓或内嵌轮 廓,所以 hierarchy 向量内所有元素的第 3、第4 个分量都会被置为-1。
  • CV_RETR_CCOMP:提取所有轮廓,并且将其组织为两层的层级结构:顶层为连通域的
    外围边界,次层为洞的内层边界。如果在内层洞中还有其他的轮廓,那么这个内层洞的轮 廓也将被存储到顶层中。
  • CV_RETR_TREE:提取所有轮廓,并且构建一个嵌套式的轮廓存储。

参数 method 定义轮廓的近似方法,取值如下:

  • CV_CHAIN_APPROX_NONE:保存物体边界上所有连续的轮廓点到 contours 向量内。
  • CV_CHAIN_APPROX_SIMPLE:仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入 contours
    向量内,拐点与拐点乊间直线段上的信息点不予保留。
  • CV_CHAIN_APPROX_TC89_L1:使用 teh-Chinl chain 近似算法。
  • CV_CHAIN_APPROX_TC89_KCOS:使用 teh-Chinl chain 近似算法。

  参数 offset 表示点的偏移量,所有的轮廓信息相对于原始图像对应点的偏移量,相当于在每一个检测出的轮廓点上加上该偏移量,并且 Point 可以是负值。另外,绘制轮廓函数 drawContours 经常和查找轮廓函数 findContours 联合使用,这个好理解,查找出轮廓后,通常需要把轮廓绘制出来。函数 drawContours 声明如下:

cpp 复制代码
void cv::drawContours(InputOutputArray image, InputArrayOfArrays 
contours,int contourIdx,const Scalar & color, int thickness = 1, int lineType 
= LINE_8,InputArray hierarchy = noArray(),int maxLevel = INT_MAX, Point offset 
= Point());

  其中参数 image 表示目标图像;contours 表示输入的轮廓组,每一组轮廓由点 vector 构成;contourIdx 指明画第几个轮廓,如果该参数为负值,就画全部轮廓;color 为轮廓的颜色;thickness为轮廓的线宽,如果为负值或 CV_FILLED,就表示填充轮廓内部;lineType 为线型;hierarchy 为轮廓结构信息。

  maxLevel 表示绘制轮廓的最高级别,这个参数只有 hierarchy 有效的时候才有效,当 maxLevel=0 时,绘制与输入轮廓同一等级的所有轮廓,即输入轮廓和与其相邻的轮廓;当maxLevel=1 时,绘制与输入轮廓同一等级的所有轮廓与其子节点;当 maxLevel=2 时,绘制与输入轮廓同一等级的所有轮廓与其子节点以及子节点的子节点。offset 表示可选轮廓偏移参数。

  下面用效果图对比一下 findContours 函数中各参数取不同值时,向量 contours 和 hierarchy 的内容如何变化,以及有何异同。

【例 】对比 findContours 函数的不同效果

cpp 复制代码
#include <opencv2/opencv.hpp>
using namespace cv;
#include<iostream>
using namespace std;
#pragma comment(lib, "opencv_world4140d.lib") //引用引入库
#include <opencv2/highgui/highgui_c.h>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <stdio.h>

int main(int argc, char* argv[])
{
	int op;
	printf("enter 1--4:");
	scanf_s("%d", &op);
	Mat imageSource = imread("test.jpg", 0);
	imshow("Source Image", imageSource);
	Mat image;
	GaussianBlur(imageSource, image, Size(3, 3), 0);
	Canny(image, image, 100, 250);
	vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;
	if (op == 1)
		findContours(image, contours, hierarchy, RETR_TREE,
			CHAIN_APPROX_SIMPLE, Point());
	else if (op == 2)
		findContours(image, contours, hierarchy, RETR_EXTERNAL,
			CHAIN_APPROX_NONE, Point());
	else if (op == 3)
		findContours(image, contours, hierarchy, RETR_LIST,
			CHAIN_APPROX_SIMPLE, Point());
	else if (op == 4)
		findContours(image, contours, hierarchy, RETR_TREE, CHAIN_APPROX_NONE,
			Point());
	Mat imageContours = Mat::zeros(image.size(), CV_8UC1);
	Mat Contours = Mat::zeros(image.size(), CV_8UC1); //绘制 
	for (int i = 0; i < contours.size(); i++)
	{
		//contours[i]代表的是第 i 个轮廓,contours[i].size()代表的是第 i 个轮廓上所有的像素点数
		for (int j = 0; j < contours[i].size(); j++)
		{
			//绘制出 contours 向量内所有的像素点 
			Point P = Point(contours[i][j].x, contours[i][j].y);
			Contours.at<uchar>(P) = 255;
		}
		//输出 hierarchy 向量内容 
		char ch[256];
		sprintf_s(ch, "%d", i);
		string str = ch;
		cout << "向量 hierarchy 的第" << str << " 个元素内容为:" << endl << hierarchy[i] << endl << endl;
		//绘制轮廓 
		drawContours(imageContours, contours, i, Scalar(255), 1, 8,hierarchy);
	}
	imshow("Contours Image", imageContours); //轮廓 
	imshow("Point of Contours", Contours); //向量 contours 内保存的所有轮廓点集 
	waitKey(0);
	return 0;
}

在代码中,根据用户输入的 1~4 来决定执行哪个 findContours,我们分别设置了 4 种不同参数的 findContours 调用。findContours 调用后轮廓就查找出来了,我们先绘制出 contours 向量内所有的像素点,然后调用 drawContours 绘制出轮廓。通过调整第 4 个参数 mode(轮廓的检索模式)、第 5 个参数 method(轮廓的近似方式)以及不同的偏移量 Point()就可以得到不同的效果。

相关推荐
Tokenge1 小时前
AI前沿日报 | 2026-08-20:AI行业进入“安全、算力、隐私、落地”四线并跑阶段
人工智能·安全
ZYJCSZKJ1 小时前
面向东盟多语种场景的AIGEO与AI数字人融合架构及区域实践
人工智能·架构·#ai数字人·aigeo
Raas1001 小时前
MAI Gateway (魔芋企业级AI网关) 私有化部署全攻略:AI网关怎么部署?附架构图
人工智能·安全·大模型·gateway·ai网关·mai gateway
wuminyu1 小时前
JDK21中FFM api的upcall回调机制解析
java·linux·c语言·jvm·c++
Lucifer三思而后行1 小时前
我准备写 26 篇文章,带 DBA 系统学一遍 AI
数据库·人工智能·dba
JavaPub-rodert1 小时前
Codex 从 0 开始:安装 ChatGPT,并接入 DeepSeek
人工智能·gpt·chatgpt
每日新鲜事1 小时前
北大医院引入WPS Comate智能助手,加速医院管理智能化进程
大数据·人工智能·wps
m4Rk_1 小时前
【论文阅读】Agent 记忆机制(45):ReMemR1——让长期上下文 Agent 可以回溯历史记忆进行非线性推理
论文阅读·人工智能·学习·开源·github
gis分享者1 小时前
LangChain 和 LlamaIndex 有什么区别?各自适合什么场景?
人工智能·ai·langchain·场景·区别·llamaindex