C++OpenCV(2):图像处理基础概念与操作

🔆 文章首发于我的个人博客:欢迎大佬们来逛逛

🔆 OpenCV项目地址及源代码:点击这里

文章目录

图形读取与显示

通过 imread 我们可以读取一个图片

其形式如下:

cpp 复制代码
void imshow(const String& winname, InputArray mat);

而我们需要传递一个InputArray类型的参数,实际上就是 cv::Mat

如果创建cv::Mat呢?

可以通过它的很多的构造函数

先来了解一下图片的颜色通道

  • 颜色通道
    • RGB 图像有4 个默认通道:红色、绿色和蓝色各有一个通道,以及一个用于编辑图像复合通道(主通道)
  • 彩色深度
    • 8位色,每个像素所能显示的彩色数为2的8次方,即256种颜色。
    • 16位增强色,16位彩色,每个像素所能显示的彩色数为2的16次方,即65536种颜色。
    • 24位真彩色,每个像素所能显示的彩色数为24位,即2的24次方,约1680万种颜色。
    • 32位真彩色,即在24位真彩色图像的基础上再增加一个表示图像透明度信息的Alpha通道。
      • Alpha通道:一张图片的透明和半透明度

我们使用如下的形式来描述它的通道类型:

cpp 复制代码
CV_<bit_depth>(S|U|F)C<number_of_channels>

其中:

  • bit_depth :位数,就是我们上面讲的彩色深度
  • S|U|F:即 signed ,unsigned int,float 来存储
  • number_of_channels:通道,有单通道,双通道,三通道,和四通道等等

加载图片

Mat类型的结构如下:

cpp 复制代码
class  Mat
{
 
public:
 	/*
 		flag: 
 		1.数字签名 
 		2.维度
 		3.通道数
 		4.连续性
 	*/
	int flags;					
	int dims; 					//数据维数
	int rows,cols; 				//数据行列
	uchar *data;				//存储的数据		
    const uchar* datastart;		//数据开始
    const uchar* dataend;		//数据结束
    const uchar* datalimit;		//数据边界
	//其他成员  
 	//.....
 	//其他方法
 	//.....
 public: 		//构造方式
    // 默认构造函数 Mat A;
    Mat ()
    // 常用构造函数 Mat A(10,10,CV_8UC3);
    Mat (int rows, int cols, int type)
    //Mat A(300, 400, CV_8UC3,Scalar(255,255,255));
    Mat (int ndims, const int *sizes, int type, const Scalar &s)
    Mat (Size size, int type)
    Mat (int rows, int cols, int type, const Scalar &s)
    Mat (Size size, int type, const Scalar &s)
    Mat (int ndims, const int *sizes, int type)
    Mat (const Mat &m)
    Mat (int rows, int cols, int type, void *data, size_t step=AUTO_STEP)
    Mat (Size size, int type, void *data, size_t step=AUTO_STEP)
    Mat (int ndims, const int *sizes, int type, void *data, const size_t *steps=0)
    Mat (const Mat &m, const Range &rowRange, const Range &colRange=Range::all())
    //Mat D (A, Rect(10, 10, 100, 100) );
    Mat (const Mat &m, const Rect &roi)
    Mat (const Mat &m, const Range *ranges)
    
};

我们使用如下的几种方式来加载图片:

  • CV_8UC1:单通道
  • Scalar:使用BGR形式
  • clone/copyTo:从另一个Mat拷贝
cpp 复制代码
void testShow() {
	cv::Mat	m1(200, 200, CV_8UC1);
	cv::imshow("1", m1);
	//std::cout << m << '\n';
	cv::Mat m2(200, 200, CV_8UC3, cv::Scalar(255, 0, 255));
	cv::imshow("2", m2);

	cv::Mat m3 = m2.clone();
	cv::imshow("3", m3);

	cv::Mat m4;
	m3.copyTo(m4);	
	cv::imshow("4", m4);

	//imread
	cv::Mat m5 = cv::imread("images/mm.png", cv::ImreadModes::IMREAD_GRAYSCALE);
	cv::imshow("5", m5);
}

显示图片

我们使用函数:imshow

我们已经直到了它的第二个参数是一个类型,用来表示以何种形式显示图片:**cv::ImreadModes**

cpp 复制代码
Mat imread( const String& filename, int flags = IMREAD_COLOR );
/****************************************************************
*				filename: 文件路径
*				flags   : 显示方式
*****************************************************************/
enum ImreadModes {
       IMREAD_UNCHANGED            = -1,	//按原样返回加载的图像(带有alpha通道,否则会被裁剪)
       IMREAD_GRAYSCALE            = 0, 	//单通道灰度图像
       IMREAD_COLOR                = 1, 	//3通道BGR彩色图像 
       IMREAD_ANYDEPTH             = 2, 	//16位/32位图像,其他则转换为8位
       IMREAD_ANYCOLOR             = 4, 	//图像以任何可能的颜色格式读取
       IMREAD_LOAD_GDAL            = 8, 	//gdal驱动程序加载映像
       IMREAD_REDUCED_GRAYSCALE_2  = 16,	//单通道灰度图像,并将图像大小减小1/2 
       IMREAD_REDUCED_COLOR_2      = 17,	//3通道BGR彩色图像,使图像大小减小1/2
       IMREAD_REDUCED_GRAYSCALE_4  = 32,	//单通道灰度图像,并将图像尺寸减小1/4
       IMREAD_REDUCED_COLOR_4      = 33,	//3通道BGR彩色图像,使图像大小减小1/4
       IMREAD_REDUCED_GRAYSCALE_8  = 64,	//单通道灰度图像,并将图像尺寸减小1/8
       IMREAD_REDUCED_COLOR_8      = 65,	//3通道BGR彩色图像,使图像大小减小1/8
       IMREAD_IGNORE_ORIENTATION   = 128	//不要根据EXIF的方向标志旋转图像
};

打印图片信息

我们可以打印Mat的信息,因为它实际上就是一个矩阵,我们可以采用多种形式来格式化输出

  • C语言形式
  • numpy形式
  • python形式

....

cpp 复制代码
void testPrint() {
	cv::Mat m(10, 10, CV_8UC1);
	std::cout << "Mat: \n"<<m << '\n';
	//格式化
	std::cout << "C: \n" << cv::format(m, cv::Formatter::FMT_C);
	std::cout << "numpy: \n" << cv::format(m, cv::Formatter::FMT_NUMPY);
}

保存图片

使用函数: imwrite

第一个参数为保存的图片的路径,第二个参数为保存的图片。

其中路径我们可以使用 cv::String 来传递,就是个字符串。

cpp 复制代码
void testSaveFile(cv::String filename) {
	auto m = cv::imread("./images/dog.png" ,cv::ImreadModes::IMREAD_GRAYSCALE);	
	cv::imshow("dog", m);
	cv::imwrite(filename, m);
}

色彩模型转换

什么是色彩模型?

颜色模型指的是某个三维颜色空间中的一个可见光子集,它包含某个色彩域的所有色彩。一般而言,任何一个色彩域都只是可见光的子集,任何一个颜色模型都无法包含所有的可见光(通俗一点讲就是表示颜色的一种方式

RGB颜色模型

在计算机体系中,最常见的色彩模型就是**RGB颜色**模型。

它具有三维坐标的模型形式:


  • RGB16 :每个像素用16个比特位表示,占2个字节
    • RGB565:RGB分量分别使用5位、6位、5位:
  • RGB555:RGB分量分别使用5位、5位、5位:

对于RGB555,如何获取各个5位上的值,即分别获取 R,G,B所代表的值?

通过位运算即可获取。

cpp 复制代码
   /*
   RGB 颜色模型
   */
   struct XColor {
   	unsigned int color : 15;
   };
   void getRGB555() {
   	XColor col{};
   	col.color = 0b110000010111100;
   	std::cout << "R: " << (col.color >> 10) << '\n';
   	std::cout << "G: " << ((col.color & 0x3F0) >> 5) << '\n';
   	std::cout << "B: " << (col.color & 0x1F) << '\n';
   }
  • RGB24 格式:每个像素用24比特位表示,占3个字节,在内存中RGB各排列顺序为:BGR:
  • RGB32 格式:每个像素用32比特位表示,占4个字节,R,G,B分量分别用8个bit表示,存储顺序为B,G,R,最后8个字节保留

    • ARGB32:本质就是带alpha通道的RGB24,与RGB32的区别在与,保留的8个bit用来表示透明,也就是alpha的值
    • 对于**RGB32**,如何获取各个8位上的值,即分别获取 R,G,B,Alpha所代表的值?

      • 通过位运算即可获取。
      cpp 复制代码
      void getRGB32() {
      	int color = 0x0F0A2B0C;
      	std::cout << "B: " << (color >> 24) << '\n';
      	std::cout << "G: " << ((color & 0x00FF0000) >> 16) << '\n';
      	std::cout << "R: " << ((color & 0x0000FF00) >> 8) << '\n';
      	std::cout << "Alpha: " << (color & 0x000000FF) << '\n';
      }

HSV颜色模型

HSV (Hue, Saturation,Value)也被称为六角锥体模型 ,即色调,饱和度,明度

将RGB转换为HSV模型:

  • **cvtColor**函数:将图像从一种颜色模型转换为另一个。
  • 起到关键作用的是第三个参数:cv::ColorConversionCodes 是一个枚举,表示了你想要从谁转换到谁,这里我们让BGR形式转换为HSV形式。
cpp 复制代码
cv::Mat res;
cv::cvtColor(m1, res, cv::ColorConversionCodes::COLOR_BGR2HSV);
cv::imshow("hsv", res);

HLS模型

HLS模型分别是色调,亮度,饱和度

上图可以看出,固定一个颜色(H),那么随着饱和度(S,Chroma)的增加,颜色越来越深。

cpp 复制代码
cv::cvtColor(m1, res, cv::ColorConversionCodes::COLOR_BGR2HLS);
cv::imshow("hls", res);

LAB模型

Lab颜色模型由三个要素组成,一个要素是**亮度**(L),a 和b是两个颜色通道

  • **a**包括的颜色是从深绿色(低亮度值)到灰色(中亮度值)再到亮粉红色(高亮度值)
  • **b**是从亮蓝色(低亮度值)到灰色(中亮度值)再到黄色(高亮度值)。
  • 因此,这种颜色混合后将产生具有明亮效果的色彩。
cpp 复制代码
cv::cvtColor(m1, res, cv::ColorConversionCodes::COLOR_BGR2Lab);
cv::imshow("Lab", res);

还有很多的颜色模型都可以在 cvtColor这个函数中找到。


图像像素读写操作

openCV基本类型介绍:

  • 基本类型:
cpp 复制代码
typedef unsigned uint;
typedef signed char schar;
typedef unsigned char uchar;
typedef unsigned short ushort;
  • 封装类型:Vec类似于std::vector,只不过可以指定其大小,并且命名规则为 cv:: Vec大小+ 类型
cpp 复制代码
typedef Vec<uchar, 2> Vec2b;
typedef Vec<uchar, 3> Vec3b;
typedef Vec<uchar, 4> Vec4b;

typedef Vec<short, 2> Vec2s;
typedef Vec<short, 3> Vec3s;
typedef Vec<short, 4> Vec4s;

typedef Vec<ushort, 2> Vec2w;
typedef Vec<ushort, 3> Vec3w;
typedef Vec<ushort, 4> Vec4w;

typedef Vec<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
typedef Vec<int, 6> Vec6i;
typedef Vec<int, 8> Vec8i;

图像是由像素掉构成的,因此我们可以获取图像的每一个像素,这个像素是由(i,j)确定的,即在图片宽度与高度的范围下,每一个行的每一列都可以是一个像素。

我们使用 Mat 的 at 函数来获取某个位置的像素值。

注意像素具有channels的区别。

  • 如果是单通道: 则直接获取 uchar 表示一个像素(一字节)。
  • 如果是三通道: 则需要获取Vec3b 表示三个像素,可以通过[0],[1],[2]来分别操作。

以下操作对图片转换为负片 ,使用 255 - 当前像素值

cpp 复制代码
#include <iostream>
#include <opencv2/opencv.hpp>

class ImgPixel {
public:
	ImgPixel(std::string filename):m(cv::imread(filename)){}
	void show(std::string title) {
		cv::imshow(title, m);
	}
	void Visit_By_Array() {
		auto dims = m.channels(); //获取通道数
		for (int i = 0; i < m.rows; i++) {
			for (int j = 0; j < m.cols; j++) {
				if (dims == 1) {
					//如果是一通道,则直接操作
					uchar rets = m.at<uchar>(i, j);
					rets = 255 - rets;
				}
				if (dims == 3) {
					cv::Vec3b rets = m.at<cv::Vec3b>(i, j);
#if 0
					m.at<cv::Vec3b>(i, j)[0] = 255 - rets[0]; //转换为负片
					m.at<cv::Vec3b>(i, j)[1] = 255 - rets[1];
					m.at<cv::Vec3b>(i, j)[2] = 255 - rets[2];
#else 
					//安全类型转换: 超过255的赋值为 255 小于0的赋值为 0
					m.at<cv::Vec3b>(i, j)[0] = cv::saturate_cast<uchar>(rets[0] + 100);
					m.at<cv::Vec3b>(i, j)[1] = cv::saturate_cast<uchar>(rets[1] + 100);
					m.at<cv::Vec3b>(i, j)[2] = cv::saturate_cast<uchar>(rets[2] + 100);
#endif
				}
			}
		}
	}
private:
	cv::Mat m;
};
int main(){
	ImgPixel img("dog.png");
	img.show("origin");
	img.Visit_By_Array();
	img.show("convert1");

	cv::waitKey(0);
	return 0;
}

安全类型转换:saturate_cast<T> 是openCV的一种安全转换函数,当我们对像素执行加减乘除的时候,有可能会超出 [ 0 , 255 ] [0,255] [0,255]

的范围,因此使用此函数来保证不会越界。

实际上这个函数就是:

  • 超过255:转为255
  • 小于0:转为0
cpp 复制代码
template<> inline uchar saturate_cast<uchar>(int v)          { return (uchar)((unsigned)v <= UCHAR_MAX ? v : v > 0 ? UCHAR_MAX : 0); }

像素算数运算

图像可以进行像素之间的算数运算,跟我们上节的对像素的简单减法是一样的:

我们有专门的函数:

  • add:像素加
  • subtract:像素减
  • multiply:像素乘
  • divide:像素除
  • addWeighted :对图片执行: α ⋅ i m g 1 + β ⋅ i m g 2 = r e s \alpha\cdot img_1 + \beta\cdot img_2 = res α⋅img1+β⋅img2=res 的操作,其中的 α \alpha α和 β \beta β就是透明度

除了基本的运算,还有二进制运算

  • and:像素按位与
  • or:按位或
  • **not:**按位取反
  • xor:按位异或

测试如下:

需要两个原图片执行下面的这些操作,我使用的图片是:(注意尺寸要一样)

然后自行执行下面代码。

cpp 复制代码
#include <iostream>
#include <opencv2/opencv.hpp>

class ImgOperation {
public:
	ImgOperation() :m1(cv::imread("dog.png")), m2(cv::imread("text.jpg")) {}
	void testAdd(std::string title="add") {
		cv::add(m1, m2, res);
		cv::imshow(title, res);
		cv::waitKey();
	}
	void testSub(std::string title = "subtract") {
		cv::subtract(m1, m2, res);
		cv::imshow(title, res);
		cv::waitKey();
	}
	void testMul(std::string title = "multiply") {
		cv::multiply(m1, m2, res);
		cv::imshow(title, res);
		cv::waitKey();
	}
	void testDivide(std::string title = "divide") {
		cv::divide(m1, m2, res);
		cv::imshow(title, res);
		cv::waitKey();
	}
	void testAddWeighted(std::string title="AddWeighted") {
		cv::addWeighted(m1, 0.1, m2, 0.9, 0, res);
		cv::imshow(title, res);
		cv::waitKey();
	}
	//BitWise
	void testBitWise() {
		cv::Mat _and, _or, _xor, _not;
		cv::bitwise_and(m1, m2, _and);
		cv::bitwise_or(m1, m2, _or);
		cv::bitwise_xor(m1, m2, _xor);
		cv::bitwise_not(m1, _not);

		cv::imshow("and", _and);
		cv::imshow("or", _or);
		cv::imshow("xor", _xor);
		cv::imshow("not", _not);
	}
private:
	cv::Mat m1;
	cv::Mat m2;
	cv::Mat res;
};

int main()
{
	ImgOperation opt;
	//opt.testAdd();
	//opt.testSub();
	//opt.testMul();
	//opt.testDivide();
	//opt.testAddWeighted();	
	opt.testBitWise();

	cv::waitKey();
	return 0;
}

图形文字绘制

绘制线

使用**line**函数,原型如下:

cpp 复制代码
void line(InputOutputArray img, Point pt1, Point pt2, const Scalar& color,int thickness = 1, int lineType = LINE_8, int shift = 0);
//线的样式
enum LineTypes {
    FILLED  = -1,
    LINE_4  = 4, //!< 4-connected line
    LINE_8  = 8, //!< 8-connected line
    LINE_AA = 16 //!< antialiased line
};
/*******************************************************************
*			img: 			绘制在那个图像上
*			pt1:			起点
*			pt2:			终点
*			color:			颜色
*			thickness:		厚度(宽度) 
*			lineType:		线的样式
*					FILLED: 线填充的
*					LINE_4:	4邻接连接线
*					LINE_8: 8邻接连接线
*					LINE_AA:反锯齿连接线(高斯滤波)
*			shift: 			坐标点小数点位数(可忽略不写)
*********************************************************************/

在执行 imshow的之前,在图片上画一条线:

cpp 复制代码
void testDrawLine() {
		//绘制线
		cv::line(m, cv::Point(0, 0), cv::Point(300, 300), cv::Scalar(0,255,0), 3, cv::LineTypes::LINE_AA);
	}

绘制圆

使用**circle函数来绘制或者填充圆**

cpp 复制代码
void circle(InputOutputArray img, Point center, int radius,const Scalar& color, int thickness = 1,int lineType = LINE_8, int shift = 0);
//线的样式
enum LineTypes {
    FILLED  = -1,
    LINE_4  = 4, //!< 4-connected line
    LINE_8  = 8, //!< 8-connected line
    LINE_AA = 16 //!< antialiased line
};
/*******************************************************************
*			img: 			绘制在那个图像上
*			center:			圆心坐标
*			radius:			半径
*			color:			颜色
*			thickness:		厚度(宽度)
*								-1: 	填充圆
*								其他值:  空心
*			lineType:		 线的样式
*					FILLED: 线填充的
*					LINE_4:	4邻接连接线
*					LINE_8: 8邻接连接线
*					LINE_AA:反锯齿连接线(高斯滤波)
*			shift: 			坐标点小数点位数(可忽略不写)
*********************************************************************/

效果如下:

  • 填充效果需要把thickness置为-1。
cpp 复制代码
void testDrawCircle() {
		cv::circle(m, cv::Point(150, 150), 50, cv::Scalar(0, 255, 0),-1); //填充圆
		cv::circle(m, cv::Point(150, 150), 52, cv::Scalar(0, 0, 255), 2, 
							cv::LineTypes::LINE_AA); //空心圆
	}

绘制矩形

使用rectangle函数来绘制矩形。

cpp 复制代码
void rectangle(InputOutputArray img, Rect rec,const Scalar& color, int thickness = 1,int lineType = LINE_8, int shift = 0);
/*******************************************************************
*			img: 			绘制在那个图像上
*			rec:			矩形大小  Rect(x,y,w,h);  
*					x,y:	起始坐标
*					w,h:	宽度和高度
*			color:			颜色
*			thickness:		厚度(宽度)
*								-1: 	填充矩形
*								其他值:  空心矩形
*			lineType:		线的样式
*					FILLED: 线填充的
*					LINE_4:	4邻接连接线
*					LINE_8: 8邻接连接线
*					LINE_AA:反锯齿连接线(高斯滤波)
*			shift: 			坐标点小数点位数(可忽略不写)
*********************************************************************/

效果如下:

cpp 复制代码
void testDrawRectangle() {
		cv::rectangle(m, cv::Rect(0, 0, 50, 50), cv::Scalar(100, 45, 20),-1);
	}

绘制椭圆

使用ellipse来绘制椭圆:

cpp 复制代码
void ellipse(InputOutputArray img, Point center, Size axes,double angle, double startAngle, double endAngle,
const Scalar& color, int thickness = 1,int lineType = LINE_8, int shift = 0);
/*******************************************************************
*			img: 			绘制在那个图像上
*			center:			椭圆圆心
*			axes:			矩形内置椭圆
*			angle:			倾斜角
*			startAngle:		扩展的弧度 0
*			endAngle:		 扩展的弧度  360
*			color:			颜色
*			thickness:		线宽度
*								-1: 	填充矩形
*								其他值:  空心矩形
*			lineType:		线的样式
*					FILLED: 线填充的
*					LINE_4:	4邻接连接线
*					LINE_8: 8邻接连接线
*					LINE_AA:反锯齿连接线(高斯滤波)
*			shift: 			坐标点小数点位数(可忽略不写)
*********************************************************************/

效果如下:

cpp 复制代码
void testDrawEllipse() {
		cv::ellipse(m, cv::Point(100, 100),cv::Size(20,80),
						180,0,360, cv::Scalar(54, 54, 43));
		cv::ellipse(m, cv::Point(100, 100), cv::Size(20, 80), 
						90, 0, 360, cv::Scalar(54, 54, 43),-1);
	}

绘制文字

使用**putText来绘制文字,注意不支持中文!!!!!!!!!!!!!**

cpp 复制代码
void putText( InputOutputArray img, const String& text, Point org,int fontFace, double fontScale, Scalar color,int thickness = 1, int lineType = LINE_8,bool bottomLeftOrigin = false );
/*******************************************************************
*			img: 			绘制在那个图像上
*			text:			绘制文字
*			org:			文本框左下角
*			fontFace:		字体
*			fontScale:		缩放
*			color:			颜色
*			thickness		线宽度
*			lineType:		线的样式
*					FILLED: 线填充的
*					LINE_4:	4邻接连接线
*					LINE_8: 8邻接连接线
*					LINE_AA:反锯齿连接线(高斯滤波)
*			bottomLeftOrigin: 		起点位置
*								true:  左上角  反转倒立显示
*								false: 左下角  正常显示
*********************************************************************/
//opencv 不识别汉字
//fontFace: 字体
enum HersheyFonts {
    FONT_HERSHEY_SIMPLEX        = 0, //!< normal size sans-serif font   //灯芯体
    FONT_HERSHEY_PLAIN          = 1, //!< small size sans-serif font
    FONT_HERSHEY_DUPLEX         = 2, //!< normal size sans-serif font (more complex than FONT_HERSHEY_SIMPLEX)
    FONT_HERSHEY_COMPLEX        = 3, //!< normal size serif font
    FONT_HERSHEY_TRIPLEX        = 4, //!< normal size serif font (more complex than FONT_HERSHEY_COMPLEX)
    FONT_HERSHEY_COMPLEX_SMALL  = 5, //!< smaller version of FONT_HERSHEY_COMPLEX
    FONT_HERSHEY_SCRIPT_SIMPLEX = 6, //!< hand-writing style font
    FONT_HERSHEY_SCRIPT_COMPLEX = 7, //!< more complex variant of FONT_HERSHEY_SCRIPT_SIMPLEX
    FONT_ITALIC                 = 16 //!< flag for italic font
};

测试如下:

cpp 复制代码
void testDrawText(const std::string& name) {
		cv::putText(m,
			name,
			cv::Point(10, 200),
			cv::HersheyFonts::FONT_HERSHEY_COMPLEX,
			1.0,
			cv::Scalar(50,50,255),
			2,
			cv::LineTypes::LINE_AA,
			false); //翻转
	}

其他绘制函数

绘制多边形线:

  • polylines

绘制填充多边形:

  • fillPoly

文本参考

百度百科_全球领先的中文百科全书

相关推荐
奋斗的小花生2 小时前
c++ 多态性
开发语言·c++
闲晨3 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
UestcXiye4 小时前
《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
c++·计算机网络·ip·tcp
霁月风5 小时前
设计模式——适配器模式
c++·适配器模式
jrrz08286 小时前
LeetCode 热题100(七)【链表】(1)
数据结构·c++·算法·leetcode·链表
咖啡里的茶i6 小时前
Vehicle友元Date多态Sedan和Truck
c++
海绵波波1076 小时前
Webserver(4.9)本地套接字的通信
c++
@小博的博客6 小时前
C++初阶学习第十弹——深入讲解vector的迭代器失效
数据结构·c++·学习
老艾的AI世界6 小时前
新一代AI换脸更自然,DeepLiveCam下载介绍(可直播)
图像处理·人工智能·深度学习·神经网络·目标检测·机器学习·ai换脸·视频换脸·直播换脸·图片换脸
爱吃喵的鲤鱼7 小时前
linux进程的状态之环境变量
linux·运维·服务器·开发语言·c++