opencv c++ canny 实现 以及与halcon canny的对比

Opencv和C++实现canny边缘检测_opencv边缘增强-CSDN博客

一、canny实现步骤

1、图像必须是单通道的,也就是说必须是灰度图像

2、图像进行高斯滤波,去掉噪点 

3、sobel 算子过程的实现,计算x y方向 、梯度(用不到,但是可以看看xy 两个组合起来的结果)

以及梯度方向(很重要)

4、局部非极大值抑制

5、双阈值连接处理

具体可以分为上面的5个步骤,下面一起边看原理边实现。

二、原理与实现

1、图像灰度化

如果是一张3通道的图像,也就是我们常见的彩色图,那么们就需要将其转换成一个灰度图,其规则如下:

             1.浮点算法:Gray = R*0.3 + G*0.59 + B*0.11
    2.整数方法:Gray = (R*30+G*59+B*11)/100
    3.移位方法:Gray = (R*28+G*151+B*77)>> 8
    4.平均值法:Gray = (R+G+B)/3
    5.仅取绿色:Gray = G
但是通常我们自己实现一般都是拿第一种实现的。

OpenCV转灰度图像特别简单,只需调用函数 cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 即可。

code:

void ConvertRGB2GRAY(const Mat& image, Mat& imageGray)
{
	if (!image.data || image.channels() != 3)
	{
		return;
	}
	// 创建一个单通道的灰度图像
	imageGray = Mat::zeros(image.size(), CV_8UC1);

	//  取出存储图像的数组的指针 
	uchar* pointImage = image.data;
	uchar* pointImageGray = imageGray.data;

	int stepImage = image.step;
	int stepImageGray = imageGray.step;
	for (int i = 0; i < imageGray.rows; i++)
	{
		for (int j = 0; j < imageGray.cols; j++)
		{
			pointImageGray[i * stepImageGray + j] = 0.114 * pointImage[i * stepImage + 3 * j] + 0.587 * pointImage[i * stepImage + 3 * j + 1] + 0.299 * pointImage[i * stepImage + 3 * j + 2];
		}
	}
}

2、高斯滤波

在高斯滤波的时候先要生成一个2元高斯核,然后进行高斯滤波,其作用是去掉噪点,其图像变的平滑起来

二元高斯函数

  随着sigma的增大,整个高斯函数的尖峰逐渐减小,整体也变的更加平缓,则对图像的平滑效果越来越明显。

高斯核

代码里面最后一定要归一化


void  CreateGaussianKernel(int  kernel_size, int sigma, Mat& kernel)
{
	const   double  PI = 3.1415926;
	int  center = kernel_size / 2;
	kernel = Mat(kernel_size, kernel_size,CV_32FC1);

	float  segma_pow = 2 * sigma * sigma;   

	float  sum = 0;

	//  二元高斯函数
	for (size_t i = 0; i < kernel_size; i++)
	{
		for (size_t j= 0; j < kernel_size; j++)
		{
			float  temp = ((i - center) * (i - center) + (j - center) * (j - center) )/ segma_pow;
			kernel.at<float>(i, j) = 1 / (PI * segma_pow) * exp(-temp);
			sum += kernel.at<float>(i, j);
		}
	}


	// 归一化
	for (size_t i = 0; i < kernel_size; i++)
	{
		for (size_t j = 0; j < kernel_size; j++)
		{
			kernel.at<float>(i, j) = kernel.at<float>(i, j)/sum;
		}
	}

}

5*5 的高斯核,那个核数一般是不能超过11 ,超过11 其效果均值一样了

高斯滤波


//******************高斯滤波*************************
//第一个参数imageSource是待滤波原始图像;
//第二个参数imageGaussian是滤波后输出图像;
//第三个参数 kernel 是一个指向含有N个double类型数组;
//第四个参数size是滤波核的尺寸
//*************************************************************
void  GaussianFilter(const Mat& imageSource, Mat& imageGaussian, Mat& kernel, int size)
{
	if (!imageSource.data|| imageSource.channels()!=1)
	{
		return;
	}
	imageGaussian = Mat::zeros(imageSource.size(),CV_8UC1);
	
	float  gaussArray[100];
	// 将 kernel 的方阵 变成一个一维度数组 这样在循环的时候啊就少了一次内循环
	int m = 0;
	for (size_t i = 0; i < kernel.rows; i++)
	{
		for (size_t j = 0; j < kernel.cols; j++)
		{
			gaussArray[m] = kernel.at<float>(i,j);
			m++;
		}
	}

	//滤波
		for (int i = 0; i < imageSource.rows; i++)
		{
			for (int j = 0; j < imageSource.cols; j++)
			{
				int k = 0;
				for (int l = -size / 2; l <= size / 2; l++)
				{
					for (int g = -size / 2; g <= size / 2; g++)
					{
						//以下处理针对滤波后图像边界处理,为超出边界的值赋值为边界值
						int row = i + l;
						int col = j + g;
						row = row < 0 ? 0 : row;
						row = row >= imageSource.rows ? imageSource.rows - 1 : row;
						col = col < 0 ? 0 : col;
						col = col >= imageSource.cols ? imageSource.cols - 1 : col;
						//卷积和
						imageGaussian.at<uchar>(i, j) += gaussArray[k] * imageSource.at<uchar>(row, col);
						k++;
					}
				}
			}
		}


}




void  TestGaussian()
{
	Mat  kernel;
	CreateGaussianKernel(5, 1, kernel);

	 // 打印 高斯核
	for (int i = 0; i < kernel.rows; i++)
	{
		for (int j = 0; j < kernel.cols; j++)
		{
			cout << "    " << kernel.at<float>(i, j);
		}
		cout << endl;
	}

	Mat  src = imread("C:\\Users\\alber\\Desktop\\opencv_images\\529.jpg");
	Mat  dst, imageGaussian;
	ConvertRGB2GRAY(src, dst);
	 imwrite("C:\\Users\\alber\\Desktop\\opencv_images\\1\\1.jpg", dst);
	  GaussianFilter(dst, imageGaussian, kernel, 5);
	 imwrite("C:\\Users\\alber\\Desktop\\GaussianFilter.jpg", imageGaussian);
}


 

3、实现sobel 算子

推导出X Y方向的核 

【精选】Opencv 笔记5 边缘处理-canny、sobel、Laplacian、Prewitt_opencv 边缘处理_Σίσυφος1900的博客-CSDN博客

gradient =||dx||+||dy||

theta= atan(gradY / gradX) * 57.3  注意这里的角度转换


//******************Sobel算子计算X、Y方向梯度 以及  梯度方向角********************
//第一个参数imageSourc原始灰度图像;
//第二个参数imageSobelX是X方向梯度图像;
//第三个参数imageSobelY是Y方向梯度图像;
//第四个参数   theta  是梯度方向角数组指针  下一步很重要 就是要用这个值来计算
//*************************************************************
void  SobelGradDirction(const Mat imageSource, Mat& imageX, Mat& imageY, Mat& gradXY, Mat& theta)
{
	imageX = Mat::zeros(imageSource.size(), CV_32SC1);
	imageY = Mat::zeros(imageSource.size(), CV_32SC1);
	gradXY = Mat::zeros(imageSource.size(), CV_32SC1);
	theta = Mat::zeros(imageSource.size(), CV_32SC1);

	int rows = imageSource.rows;
	int cols = imageSource.cols;

	int stepXY = imageX.step;
	int step = imageSource.step;
	/*
	Mat.step参数指图像的一行实际占用的内存长度,
	因为opencv中的图像会对每行的长度自动补齐(8的倍数),
	编程时尽量使用指针,指针读写像素是速度最快的,使用at函数最慢。
	*/
	uchar* PX = imageX.data;
	uchar* PY = imageY.data;
	uchar* P = imageSource.data;
	uchar* XY = gradXY.data;

	for (int i = 1; i < rows - 1; i++)
	{
		for (int j = 1; j < cols - 1; j++)
		{
			int a00 = P[(i - 1) * step + j - 1];
			int a01 = P[(i - 1) * step + j];
			int a02 = P[(i - 1) * step + j + 1];

			int a10 = P[i * step + j - 1];
			int a11 = P[i * step + j];
			int a12 = P[i * step + j + 1];

			int a20 = P[(i + 1) * step + j - 1];
			int a21 = P[(i + 1) * step + j];
			int a22 = P[(i + 1) * step + j + 1];

			double gradY = double(a02 + 2 * a12 + a22 - a00 - 2 * a10 - a20);
			double gradX = double(a00 + 2 * a01 + a02 - a20 - 2 * a21 - a22);


			imageX.at<int>(i, j) = abs(gradX);
			imageY.at<int>(i, j) = abs(gradY);
			if (gradX == 0)
			{
				gradX = 0.000000000001;
			}
			theta.at<int>(i, j) = atan(gradY / gradX) * 57.3;
			theta.at<int>(i, j) = (theta.at<int>(i, j) + 360) % 360;
			gradXY.at<int>(i, j) = sqrt(gradX * gradX + gradY * gradY);
			//XY[i*stepXY + j*(stepXY / step)] = sqrt(gradX*gradX + gradY*gradY);
		}

	}
	convertScaleAbs(imageX, imageX);
	convertScaleAbs(imageY, imageY);
	convertScaleAbs(gradXY, gradXY);
}

 

 这个不明显,所以我打算换个图像test

4、局部非极大值抑制

这里我们就要用到上面一步在sobel里面计算求得的x y 方向以及梯度方向的那些 东西了。

原理:

拿到当前点的梯度方向[0,360],判断其在那个区域,计算梯度方向(一个方向,两个值)在不同权重下(w=dy/dx)的灰度值t1 t2, 最后判断当前点灰度值current 和t1 t2的大小比较,如果当前值current小于t1 t2中的任何一个那么,当前的点就不会是边缘的候选点,current=0;

下面我们看一下梯度的分布:

[0-45] U[180-225]

 [45-90] U[225-270]

 [90-135] U[270-315]

 [135-180] U[315-360]

code:

/// <summary>
///  局部极大值抑制 ,计算八领域  沿着该点梯度方向,比较前后两个点的幅值大小,若该点大于前后两点,则保留,若该点小于前后两点任意一点,则置为0;
/// </summary>
/// <param name="imageInput"> 输入的图像</param>
/// <param name="imageOutput"></param>
/// <param name="theta"></param>
/// <param name="imageX"> </param>
/// <param name="imageY"></param>
void NonLocalMaxValue(const Mat imageInput, Mat& imageOutput, const Mat& theta, const Mat& imageX, const Mat& imageY)
{
	if (!imageInput.data || imageInput.channels() != 1)
	{
		return;
	}
	imageOutput = imageInput.clone();
	int  rows = imageOutput.rows;
	int  cols = imageOutput.cols;

	int  g00, g01, g02, g10, g11, g12, g20, g21, g22;
	int  g1, g2, g3, g4;
	for (size_t i = 1; i < rows-1; i++)
	{
		for (size_t j = 1; j < cols-1; j++)
		{
			// 第一行
			g00 = imageOutput.at<uchar>(i - 1, j - 1);
			g01 = imageOutput.at<uchar>(i - 1, j);
			g02 = imageOutput.at<uchar>(i - 1, j+1);

			// 第二行
			g10 = imageOutput.at<uchar>(i , j - 1);
			g11 = imageOutput.at<uchar>(i , j);
			g12 = imageOutput.at<uchar>(i, j + 1);

			// 第三行
			g20 = imageOutput.at<uchar>(i+1, j - 1);
			g21 = imageOutput.at<uchar>(i+1, j);
			g22 = imageOutput.at<uchar>(i+1, j + 1);
			// 当前点的梯度方向 
			int  direction = theta.at<int>(i, j);

			g1 = 0; 
			g2 = 0;
			g3 = 0;
			g4 = 0;

			// 保存亚像素点插值得到的灰度值 
			double  t1 = 0;
			double  t2 = 0;


			// 计算权重 
			double  w = fabs((double)imageY.at<uchar>(i,j)  / (double)imageX.at<uchar>(i, j));
			if (w==0)
			{
				w = 0.0000001;
			}
			if (w>1)
			{
				w = 1 / w;
			}

			//  g00     g01   g02
			//  g10     g11   g12
			//  g20     g21   g22
			// ================================
			if ((0 <= direction && direction < 45) || 180 <= direction && direction < 225)
			{
				t1 = g10 * (1 - w) + g20 * (w);
				t2 = g02 * (w)+g12 * (1 - w);
			}

			if ((45 <= direction && direction < 90) || 225 <= direction && direction < 270)
			{
				t1  = g01 * (1 - w) + g02 * (w);
				t2  = g20 * (w)+g21 * (1 - w);
			}

			if ((90 <= direction && direction < 135) || 270 <= direction && direction < 315)
			{
				t1  = g00 * (w)+g01 * (1 - w);
				t2  = g21 * (1 - w) + g22 * (w);
			}

			if ((135 <= direction && direction < 180) || 315 <= direction && direction < 360)
			{
				t1  = g00 * (w)+g10 * (1 - w);
				t2  = g12 * (1 - w) + g22 * (w);
			}
		
			if (imageInput.at<uchar>(i,j)<t1 || imageInput.at<uchar>(i, j) < t2)
			{
				imageOutput.at<uchar>(i, j) = 0;
			}
		}
	}

}

5、 双阈值连接处理

双阈值处理

给定一个高阈值high   一个低阈值low, low*[1.5,2]=high 这个是给定规则

判断条件就是

                      当前current<low  ,那么current=0

                       low<current<hight  current 不处理 

                       current>hight   current=255

/// <summary>
///     双阈值原理:   
///   制定一个低阈值 L  一个 高阈值 H,一般取H为整体图像灰度分布的 7成 并且H为1.5-2L
///  灰度值<L   gray=0, gray>H gray=255;
/// </summary>
/// <param name="imageIn"></param>
/// <param name="low"></param>
/// <param name="hight"></param>
void DoubleThreshold(Mat& imageIn, const double low, const double hight)
{
	if (!imageIn.data || imageIn.channels() != 1)
	{
		return;
	}
	int  rows = imageIn.rows;
	int  cols = imageIn.cols;
	
	double  gray;
	for (size_t i = 0; i < rows ; i++)
	{
		for (size_t j = 0; j < cols ; j++)
		{
			gray = imageIn.at<uchar>(i, j);
			gray = gray > hight ? (255) : (gray < low) ? (0) : gray;
			imageIn.at<uchar>(i, j) = gray;
		}
	}
}

 

 将边缘链接起来

经过上每一步的双阈值处理,我们基本上已经拿到了边缘点的候选点,下一步就是将这些边缘点联合起来,组成一个边缘轮廓

这里我们再次使用双阈值的机制  low  和 hight   和当前点的灰度值current 

规则如下: current  的8邻域的灰度值 M介于【low,hight】中,有,可能是边缘点,这个领域的点M=255 ,并且回退 , 如果领域类没有 说明这个点是一个孤立的点 不做处理,

最后判断图像中所有的点,不是255 就是0 ,生成边缘

void DoubleThresholdLink(Mat& imageInput, double lowTh, double highTh)
{
	if (!imageInput.data || imageInput.channels() != 1)
	{
		return;
	}
	int  rows = imageInput.rows;
	int  cols = imageInput.cols;
	double  gray;

	for (size_t i = 1; i < rows-1; i++)
	{
		for (size_t j = 1; j < cols-1; j++)
		{
			gray = imageInput.at<uchar>(i, j);
			if (gray==255)
			{
				continue;
			}
			bool reback = false;
			 // 寻找8领域中是否有介于low 和hight 的值 
			for (size_t k = -1; k < 2; k++)
			{
				for (size_t  l= -1; l < 2; l++)
				{
					if (k == 0 && l == 0)  //当前点 
					{
						continue;
					}
					double t = imageInput.at<uchar>(i + k, j + l);
					if (t>= lowTh&& t<highTh)
					{
						imageInput.at<uchar>(i + k, j + l) = 255;
						reback = true;
					}
				}
			}
			// 回退 
			if (reback)
			{
				if (i > 1) i--;
				if (j > 2)j -= 2;
			}
		}
	}

	 //  最后调整 
	for (int i = 0; i < rows; i++)
	{
		for (int j = 0; j < cols; j++)
		{
			if (imageInput.at<uchar>(i, j) != 255)
			{
				imageInput.at<uchar>(i, j) = 0;
			}
		}
	}

}

opencv 库结果:

还是用opencv库吧,结果比这个好多了 

三、halcon 效果对比

halcon的效果更好

code

read_image (Grayimage, 'C:/Users/alber/Desktop/opencv_images/1/grayImage.jpg')
edges_sub_pix (Grayimage, Edges, 'canny', 1, 20, 40)


 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/114944.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Cesium:CGCS2000坐标系的xyz坐标转换成WGS84坐标系的经纬高度,再转换到笛卡尔坐标系的xyz坐标

作者:CSDN @ _乐多_ 本文将介绍使用 Vue 、cesium、proj4 框架,实现将CGCS2000坐标系的xyz坐标转换成WGS84坐标系的经纬高度,再将WGS84坐标系的经纬高度转换到笛卡尔坐标系的xyz坐标的代码。并将输入和输出使用 Vue 前端框架展示了出来。代码即插即用。 网页效果如下图所示…

探索 Java 8 中的 Stream 流:构建流的多种方式

人嘛&#xff0c;要懂得避嫌… 开篇引入 Java 8引入了Stream流作为一项新的特性&#xff0c;它是用来处理集合数据的一种函数式编程方式。Stream流提供了一种更简洁、高效和易于理解的方法来操作集合数据&#xff0c;同时也能够实现并行处理&#xff0c;以提高性能。 以下是St…

uni-app 解决钉钉小程序日期组件uni-datetime-picker不兼容ios问题

最近在使用uni-app开发 钉钉小程序 &#xff0c;遇到一个ios的兼容性问题 uni-datetime-picker 组件在模拟器上可以使用&#xff0c;在真机上不生效问题 文章目录 1. 不兼容的写法&#xff0c;uni-datetime-picker 不兼容IOS2. 兼容的写法&#xff0c;使用 dd.datePicker 实现。…

windwos10搭建我的世界服务器,并通过内网穿透实现联机游戏Minecraft

文章目录 1. Java环境搭建2.安装我的世界Minecraft服务3. 启动我的世界服务4.局域网测试连接我的世界服务器5. 安装cpolar内网穿透6. 创建隧道映射内网端口7. 测试公网远程联机8. 配置固定TCP端口地址8.1 保留一个固定tcp地址8.2 配置固定tcp地址 9. 使用固定公网地址远程联机 …

系列四、全局配置文件mybatis-config.xml

一、全局配置文件中的属性 mybatis全局配置中的文件非常多&#xff0c;主要有如下几个&#xff1a; properties&#xff08;属性&#xff09;settings&#xff08;全局配置参数&#xff09;typeAliases&#xff08;类型别名&#xff09;typeHandlers&#xff08;类型处理器&am…

整理10个地推拉新app接单平台,免费一手推广渠道平台干货分享

1. 聚量推客&#xff1a; “聚量推客”汇聚了众多市场上有的和没有的地推网推拉新接单项目&#xff0c;目前比较火热&#xff0c;我们做地推和网推从业者如果长期在这行业去做推广可以使用这个平台&#xff0c;价格高数据也好&#xff0c;大部分拉新项目也都是官签一手资源 一…

自己动手实现一个深度学习算法——三、神经网络的学习

文章目录 1.从数据中学习1&#xff09;数据驱动2&#xff09;训练数据和测试数据 2.损失函数1)均方误差2)交叉熵误差3)mini-batch学习 3.数值微分1&#xff09;概念2&#xff09;数值微分实现 4.梯度1&#xff09;实现2&#xff09;梯度法3&#xff09;梯度法实现4&#xff09;…

建议收藏《2023华为海思实习笔试-数字芯片真题+解析》(附下载)

华为海思一直以来是从业者想要进入的热门公司。但是岗位就那么多&#xff0c;在面试的时候&#xff0c;很多同学因为准备不充分&#xff0c;与岗位失之交臂&#xff0c;无缘进入该公司。今天为大家带来《2023华为海思实习笔试-数字芯片真题解析》题目来源于众多网友对笔试的记录…

FBM232 P0926GW 一个基于PC的Studio应用程序

FBM232 P0926GW 一个基于PC的Studio应用程序 告别自定义编程&#xff0c;向S88 Builder问好。它可以帮助您轻松地将泵、混合器和阀门等单个批处理设备配置为特定的协调任务&#xff0c;如灌装、加热和混合。 S88 Builder是什么&#xff1f;它包括一个基于PC的Studio应用程序&…

ubuntu 分区 方案

ubuntu 分区 方案 自动分区啥样子的&#xff1f; 手动分区 需要怎么操作&#xff1f; 注意点是啥&#xff1f; swap分区 要和 内存大小 差不多 安装ubuntu系统时硬盘分区方案 硬盘分区概述 一块硬盘最多可以分4个主分区&#xff0c;主分区之外的成为扩展分区。硬盘可以没有…

javaEE -13(6000字CSS入门级教程 - 2)

一&#xff1a;Chrome 调试工具 – 查看 CSS 属性 首先打开浏览器&#xff0c;接着有两种方式可以打开 Chrome 调试工具 直接按 F12 键鼠标右键页面 > 检查元素 点开检查即可 标签页含义&#xff1a; elements 查看标签结构console 查看控制台source 查看源码断点调试ne…

开源的网站数据分析统计平台——Matomo

Matomo 文章目录 Matomo前言一、环境准备1. 整体安装流程2.安装PHP 7.3.303.nginx配置4.安装matomo4.1 访问安装页面 http://192.168.10.45:8088/index.php4.2 连接数据库4.3 设置管理员账号4.4 生成js跟踪代码4.5 安装完成4.6 警告修改4.7 刷新页面&#xff0c;就可以看到登陆…

AI智能分析网关高空抛物算法如何实时检测高楼外立面剥落?

高楼外立面剥落是一种十分危险的行为&#xff0c;会造成严重的人身伤害和财产损失。TSINGSEE青犀智能分析网关利用高楼外立面剥落的信息&#xff0c;结合高空抛物算法来进行处理就可很好解决此问题。 1. 数据收集 首先&#xff0c;需要收集关于高楼外立面剥落的数据。这可以通…

Android应用集成RabbitMQ消息处理指南

Android应用集成RabbitMQ消息处理指南 RabbitMQ1、前言2、RabbitMQ简介2.1、什么是RabbitMQ2.2、RabbitMQ的特点2.3、RabbitMQ的工作原理2.4、RabbitMQ中几个重要的概念 3、在Android Studio中集成RabbitMQ3.1、在Manifest中添加权限&#xff1a;3.2、在build.gradle(:app)下添…

数据中心如何散热?

数据中心的散热是一个非常重要的问题&#xff0c;因为数据中心内运行的服务器、存储设备以及网络设备等都会产生大量的热量&#xff0c;如果不能有效地进行散热&#xff0c;将会导致设备故障和性能下降。下面是一些常见的数据中心散热方法&#xff1a; 空调系统&#xff1a;数据…

Attention is all you need 论文阅读

论文链接 Attention is all you need 0. Abstract 主要序列转导模型基于复杂的循环或卷积神经网络&#xff0c;包括编码器和解码器。性能最好的模型还通过注意力机制连接编码器和解码器提出Transformer&#xff0c;它 完全基于注意力机制&#xff0c;完全不需要递归和卷积对两…

【机器学习】五、贝叶斯分类

我想说&#xff1a;“任何事件都是条件概率。”为什么呢&#xff1f;因为我认为&#xff0c;任何事件的发生都不是完全偶然的&#xff0c;它都会以其他事件的发生为基础。换句话说&#xff0c;条件概率就是在其他事件发生的基础上&#xff0c;某事件发生的概率。 条件概率是朴…

rabbitmq的confirm模式获取correlationData为null解决办法

回调函数confirm中的correlationDatanull // 实现confirm回调,发送到和没发送到exchange,都触发 Override public void confirm(CorrelationData correlationData, boolean ack, String cause) {// 参数说明:// correlationData: 相关数据,可以在发送消息时,进行设置该参数// …

我在Vscode学OpenCV 处理图像

既然我们是面向Python的OpenCV&#xff08;OpenCV for Python&#xff09;那我们就必须要熟悉Numpy这个库&#xff0c;尤其是其中的数组的库&#xff0c;Python是没有数组的&#xff0c;唯有借助他库才有所实现想要的目的。 # 老三样库--事先导入 import numpy as np import c…

Python操作CMD大揭秘!轻松玩转命令行控制

导语&#xff1a; 命令行界面&#xff08;Command Line Interface&#xff0c;简称CLI&#xff09;是计算机操作系统中一种基于文本的用户界面&#xff0c;通过输入命令来与计算机进行交互。Python作为一门强大的编程语言&#xff0c;提供了丰富的库和模块&#xff0c;可以方便…