OpenCV计算机视觉开发入门与实践<十一>:矩阵的复制和矩形类Rect

1.复制矩阵

  复制矩阵分为深复制(也叫完全拷贝)和浅复制。深复制就是不但复制矩阵头,还复制矩阵数据区。而浅复制只复制矩阵头,数据区则不复制。

  深拷贝的成员函数有 clone 和 copyTo。浅拷贝常用赋值运算符和拷贝构造函数,它们仅拷贝矩阵信息头。

  值得注意的是,无论是深复制还是浅复制,矩阵头肯定是新申请内存空间并进行复制的。此外,深复制一定有复制(全部或部分矩阵)数据区这个操作,而浅复制则没有这个操作。而且,深复制有开辟新数据区内存和不开辟新数据区内存两种情况,后者在原有的数据区内存中完成复制,比如 copyTo 函数在某些条件下就是如此。

1) 深复制

  Mat 类提供了成员函数 clone 和 copyTo 用来实现完全复制矩阵,其中 clone 是完全的深拷贝,在内存中申请新的空间(包括矩阵头和数据区)。函数 clone 声明如下:

cpp 复制代码
Mat clone();

  该函数为新矩阵申请新的内存空间(包括矩阵头空间和数据区空间),并将原矩阵的全部数据(包括矩阵头和数据区)复制到新矩阵,函数返回新矩阵的 Mat 对象。示例代码如下:

cpp 复制代码
Mat A = Mat::ones(4,5,CV_32F);
Mat B = A.clone(); //clone 是完全的深拷贝,在内存中申请新的空间,B 与A完全独立

  copyTo 也是深拷贝,但是否申请新的内存空间,取决于 dst 矩阵头中的大小信息是否与 src 一致,若一致,则只深拷贝且不申请新的空间,否则先申请空间,再进行拷贝。函数声明如下:

cpp 复制代码
void copyTo (OutputArray m);
void cv::Mat::copyTo(OutputArray m, InputArray mask );

  其中参数 m 表示目标数组。对于带 mask 参数这种使用方式,mask 作为一个掩码矩阵,大小必须和调用者对象的 Mat 矩阵大小相同,类型必须是 CV_8U,通道数可以为一或多通道,但只看单通道,所以用 CV_8U1 即可。

  这里敲一下黑板,mask 的作用:这个函数会检测 mask 中相应位置是否为 0,如果不为 0,就会把输入 Mat 相应位置的值直接复制到输出 OutputArray 的相应位置上,如果 mask 中相应位置为 0,输出 OutputArray 的相应位置就置为 0,mask 的作用相当于一个"与"的操作。

  OutputArray 是 InputArray 的派生类,而 InputArray 是一个接口类,这个接口类可以是 Mat、Mat_、Mat_<T, m, n>、vector、vector<vector>、vector。也就意味着如果看见函数的参数类型是 InputArray 型,那么把上述几种类型作为参数都是可以的。示例代码如下:

cpp 复制代码
Mat A = Mat::ones(4, 5, CV_32F); //A 的大小(通过 size()函数获得)是[5,4],列数(宽度)是 5,行数是 4
Mat C; //此时 C 的大小是[0,0]
A.copyTo(C) //C的大小是[0,0],若大小不合适,则需要申请新的内存空间,并完成拷贝,等同于 clone()
Mat D = A.col(1); //赋值后,D 的大小是[1,4],宽度是 1,高度是 4,相当于列数是 1,行数是 4
A.col(0).copyTo(D) //此处 A.col(0)和 D 一样大,因此不会申请空间,而是直接进行拷贝
 //相当于把A的第1列赋值给第二列。注意别看走眼,别看成 A.copyTo(D)
Mat mat1 = Mat::ones(1, 5, CV_32F); // [1,1,1,1,1]
Mat mat3 = Mat::zeros(1, 5, CV_32F); // [0,0,0,0,0]
// 因为 mat3 和 mat1 大小一样,所以 mat1 未被重新分配内存,通过 mat1 可以改变 mat3 的内容
mat3.copyTo(mat1); 

【例 1】两种方式实现深复制矩阵

cpp 复制代码
#include <iostream>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv; //所有 OpenCV 类都在命名空间 cv 下
using namespace std;
#pragma comment(lib, "opencv_world4140d.lib") //引用引入库
Mat colorReduce_2(Mat img, int div)
{
	Mat image;
	image = img.clone();
	//img.copyTo(image); //效果同 clone 一样
	int nl = image.rows; //得到行数
	int nc = image.cols * image.channels(); //得到实际列数
	for (int j = 0; j < nl; j++)
	{
		uchar* data = image.ptr<uchar>(j);
		for (int i = 0; i < nc; i++)
			data[i] = data[i] - data[i] % div + div / 2;
	}
	return image;
}

int main()
{
	Mat img = imread("1.jpg");
	Mat img2 = colorReduce_2(img, 64);
	imshow("reduce520", img2);
	imshow("520", img);
	waitKey(0);
}

  1.jpg 是工程目录下的一个图片。我们自定义了函数 colorReduce_2,该函数对每个像素值进行减少,由于用了 clone(copyTo 效果也一样),并不会影响原图,因为这两个拷贝函数是完全拷贝(矩阵头和矩阵数据全部拷贝)。

【例 2】实验 mask 中为 0 和不为 0 的情况

cpp 复制代码
#include <iostream>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv; //所有 OpenCV 类都在命名空间 cv 下
using namespace std;
#pragma comment(lib, "opencv_world4140d.lib") //引用引入库
int main()
{
	Mat image, mask;
	Rect r1(10, 10, 60, 100);//生成一个矩形
	Mat img2;
	image = imread("1.jpg");
	mask = Mat::zeros(image.size(), CV_8UC1); //掩码矩阵灰度值为 0,单通道矩阵,全黑的图
	mask(r1).setTo(255);//将掩码矩阵中 r1 范围内的灰度值设定为 255,即白图
	image.copyTo(img2, mask);//将掩码图像与 image 图像进行"与操作",然后赋值给 img2图像
	imshow("origin", image);
	imshow("copy", img2);
	imshow("mask", mask);
	waitKey();
	return 0;
}

【例 3】利用 copyTo 实现图 1 贴在图 2 上

cpp 复制代码
#include <iostream>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv; //所有 OpenCV 类都在命名空间 cv 下
using namespace std;
#pragma comment(lib, "opencv_world4140d.lib") //引用引入库
int main()
{
	Mat Image1 = imread("1.jpg");
	Mat Image2 = imread("2.jpg");
	imshow("1-origin", Image1);
	imshow("2-origin", Image2);
	//在 image1 上创建一个感兴趣的区域
	Mat imagedst = Image1(Rect(0, 0, Image2.cols, Image2.rows));
	Mat mask = imread("2.jpg", 0);//加载图 2,转为灰度图后存于 mask
	imshow("mask", mask); //显示 mask 图
	//进行复制
	Image2.copyTo(imagedst, mask); //mask 必须和调用者 Mat 矩阵(这里是 dstImage)大小相同
	imshow("result", Image1); //显示效果图
	waitKey(0);
	return 0;
}

  在代码中,我们的目标是把 image2 拷贝到 image1 的某块区域内(这里是 imagedst),这个区域大小和 image2 相同。首先在 image1 上创建一个感兴趣的区域,并存于 imagedst 中,然后加载图2,转为灰度图像后存于掩码矩阵 mask 中,注意 imread 的第二个参数,如果取值为 0,就将图像转化为灰度图像。有了 mask 后,就可以开始利用 copyTo 复制了,注意 mask 必须和调用者 Mat矩阵(这里是 Image2)大小相同。

2)浅复制

  浅复制常见的方式有:使用赋值运算符和拷贝构造函数。使用赋值运算符和拷贝构造函数都是拷贝信息头,比如以下程序:

cpp 复制代码
Mat src1 = imread("1.jpg");
Mat src2=src1; //src2 和 src1 共用一个矩阵,所以当改变二者中的任意一个时,另一个会随之改变
Mat dst(src1); //使用拷贝构造函数,只复制矩阵的信息头------典型的浅复制

2.判断矩阵是否有元素

函数 empty 用于判断矩阵是否有元素,声明如下:

cpp 复制代码
bool empty();

如果矩阵没有元素,函数就返回 true。

3.矩阵的 5 种遍历方式

  遍历就是对每个元素数据进行访问。通常有指针数组方式、.ptr 方式、.at 方式、内存连续法和迭代器遍历法,其中.ptr 方式和指针数组方式是快速高效的方式。

1)指针数组方式

  该方式就定义一个指针,指向 Mat 矩阵数据区开头:

cpp 复制代码
uchar* pdata = (uchar*)mymat.data;

  然后开始循环移动指针。移动的时候一般用两个 for 循环,进行行和列的遍历。要注意的是,矩阵的实际列数是像素列数乘以通道数。另外,如果要输出某个元素值,要注意 cout 输出的 uchar是 ASCII 码,即直接通过 cout<< pdataj<<endl 输出的话,将其数据对应的 ASCII 码进行输出,如果需要输出整数,还需要强制转换,比如:

cpp 复制代码
int img_pixel=(int)padata[j]; //像素中数据为 0-255,所以使用 int 类型即可
cout<<padata[j]<<endl;

【例 1】使用指针数组方式遍历矩阵并输出

cpp 复制代码
#include <iostream>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv; //所有 OpenCV 类都在命名空间 cv 下
using namespace std;
#pragma comment(lib, "opencv_world4140d.lib") //引用引入库
int main()
{
	Mat mymat = cv::Mat::ones(cv::Size(3, 2), CV_8UC3);//定义 2 行 3 列 3 通道矩阵,实际矩阵共有 9 列
		uchar * pdata = (uchar*)mymat.data;
	for (int i = 0; i < mymat.rows; i++)
	{
		for (int j = 0; j < mymat.cols * mymat.channels(); j++)
			cout << (int)pdata[j] << " ";
		cout << endl;
	}
}

2).ptr 方式

  该方式也是比较高效的方式。这种方式使用形式如下:

cpp 复制代码
mat.ptr<type>(row)[col]

  对于 Mat 的 ptr 函数,返回的是<>中的模板类型指针,指向的是()中的第 row 行的起点。通常<>中的类型和 Mat 的元素类型应该一致,然后用该指针访问对应 col 列位置的元素。

  ptr 函数访问任意一行像素的首地址,特别方便图像一行一行地横向访问,如果需要一列一列地纵向访问图像,就稍微麻烦一点。但是 ptr 的访问效率比较高,程序也比较安全,有越界判断。

  在 OpenCV 中,Mat 矩阵 data 数据的存储方式和二维数组不一致,二维数组按照行优先的顺序依次存储,而 Mat 中还有一个标记步进距离的变量 Step。使用 Mat.ptr(row) 行指针的方式定位到每一行。

  在 Mat 矩阵中,数据指针 Mat.data 是 uchar 类型的指针,CV_8U 系列可以通过计算指针位置快速地定位矩阵中的任意元素。二维单通道元素可以使用 MAT::at(i,j),i 是行号,j 是列号。但对于多通道的非 uchar 类型矩阵来说,以上方法不适用,此时可以用 Mat::ptr()来获得指向某行元素的指针,再通过行数与通道数计算相应点的指针。比如以下代码:

cpp 复制代码
//单通道
cv::Mat image = cv::Mat(400, 600, CV_8UC1); //定义了一个 Mat 变量 image,宽 400,长 600
uchar * data00 = image.ptr<uchar>(0); //data00 是指向 image 第一行第一个元素的指针
uchar * data10 = image.ptr<uchar>(1); //data10 是指向 image 第二行第一个元素的指针
uchar * data01 = image.ptr<uchar>(0)[1]; //data01 是指向 image 第一行第二个元素的指针
uchar * data = image.ptr<uchar>(3)[42]; //得到第 3 行第 43 个像素的指针
//多通道
cv::Mat image = cv::Mat(400, 600, CV_8UC3); //宽 400,长 600,3通道彩色图片
cv::Vec3b * data000 = image.ptr<cv::Vec3b>(0);
cv::Vec3b * data100 = image.ptr<cv::Vec3b>(1);
cv::Vec3b * data001 = image.ptr<cv::Vec3b>(0)[1];

Vec3b 可以看作是 vector<uchar, 3>,简单而言就是一个 uchar 类型、长度为 3 的向量。

【例 2】使用.ptr 方式遍历矩阵并输出

cpp 复制代码
#include <iostream>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv; //所有 OpenCV 类都在命名空间 cv 下
using namespace std;
#pragma comment(lib, "opencv_world4140d.lib") //引用引入库
int main()
{
	Mat mymat = cv::Mat::ones(cv::Size(3, 2), CV_8UC3);//定义 2 行 3 列 3 通道矩阵,实际矩阵共有 9 列
		for (int i = 0; i < mymat.rows; i++)
		{
			uchar* pdata = mymat.ptr<uchar>(i); //每一行图像的指针
			for (int j = 0; j < mymat.cols * mymat.channels(); j++)
				cout << (int)pdata[j] << " ";
			cout << endl;
		}
}

  值得说明的是,每一行数据元素在内存中是连续存储的,每个三通道像素按顺序存储。但是这种用法不能用在行与行之间,因为图像在 OpenCV 中的存储机制问题,行与行之间可能有空白单元,这些空白单元对图像来说没有意义,只是为了在某些架构上能够更有效率,比如 Intel MMX可以更有效地处理个数是 4 或 8 的倍数的行。

  也就是说行与行之间不一定连续。当然,这里只是说不一定,一般情况下,如果该 Mat 只有 1行,那当然是连续的,如果有多行,那么每行的 end 要与下一行的 begin 连在一起才算连续。一般情况下我们建立的 Mat 都是连续的,但是用 Mat::col()截取建立的 Mat 是不连续的。如果 Mat 连续,我们访问时就可以把其当成一个长行,这样访问更加高效快速。

  我们可以通过Mat的成员函数isContinuous来判断Mat中的像素点在内存中的存储是否连续。

3).at 方式

  Mat 类提供了.at 方式用于取得图像上的点,它是一个模板函数,可以取到任何类型的图像上的点。该函数声明如下:

cpp 复制代码
template<typename _Tp >
_Tp& at ( int row, int col); 

  其中 row 是元素所在的行,col 是元素所在的列,返回元素值。.at 方式取图像中的点的用法如下:

cpp 复制代码
image.at<uchar>(i,j); //取出灰度图像中 i 行 j 列的点
image.at<Vec3b>(i,j)[k]; //取出彩色图像中 i 行 j 列第 k 通道的颜色点

  其中 uchar、Vec3b 都是图像像素值的类型,不要对 Vec3b 这种类型感到害怕,其实在 Core中,它是通过 typedef Vec<T,N>来定义的,N 代表元素的个数,T 代表类型,比如:

cpp 复制代码
image.at<uchar>(10, 200) = 255; //在矩阵(10, 200)位置赋值 255

  下面我们通过一个图像处理中的实际应用来说明它的用法。在实际应用中,我们很多时候需要对图像降色彩,如常见的 RGB24 图像有 256×256×256 种颜色,通过 Reduce Color(降色)将每个通道的像素减少 8 倍至 256/8=32 种,则图像只有 32×32×32 种颜色。假设量化减少的倍数是N,则代码实现时就是简单的 value/N×N,通常我们会再加上 N/2 以得到相邻的 N 的倍数的中间值,最后图像被量化为(256/N)×(256/N)×(256/N)种颜色。

【例 3】使用.at 方式遍历矩阵并降色

cpp 复制代码
#include <iostream>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv; //所有 OpenCV 类都在命名空间 cv 下
using namespace std;
#pragma comment(lib, "opencv_world4140d.lib") //引用引入库
void colorReduce(Mat& image, int div = 64)
{
	for (int i = 0; i < image.rows; i++)
	{
		for (int j = 0; j < image.cols; j++)
		{
			image.at<Vec3b>(i, j)[0] = image.at<Vec3b>(i, j)[0] / div * div +
				div / 2;
			image.at<Vec3b>(i, j)[1] = image.at<Vec3b>(i, j)[1] / div * div +
				div / 2;
			image.at<Vec3b>(i, j)[2] = image.at<Vec3b>(i, j)[2] / div * div +
				div / 2;
		}
	}
}

int main()
{
	Mat A; //仅仅创建了矩阵头
	A = imread("1.jpg", 1);
	imshow("origin", A);
	colorReduce(A);
	imshow("result", A);
	waitKey(0);
}

4)内存连续法

  有些图像行与行之间往往存储是不连续的,但是有些图像可以是连续的,Mat 提供了一个检测图像是否连续的函数 isContinuous()。当图像连续时,我们就可以把图像完全展开,看成是一行。这样访问更加高效。

cpp 复制代码
int main()
{
	Mat mymat = cv::Mat::ones(cv::Size(3, 2), CV_8UC3);//定义 2 行 3 列 3 通道矩阵,实际矩阵共有 9 列
		int nr = mymat.rows;
	int nc = mymat.cols * mymat.channels();
	if (mymat.isContinuous()) //判断矩阵存储是否内存连续
	{
		nr = 1; //如果连续,就可以当成一行
		nc = nc * mymat.rows; //当成一行后的总列数
	}
	for (int i = 0; i < nr; i++)
	{
		uchar* pdata = mymat.ptr<uchar>(i); //每一行图像的指针
		for (int j = 0; j < nc; j++)
			cout << (int)pdata[j] << " ";
		cout << endl;
	}
}

  在代码中,通过函数 isContinuous 来判断矩阵存储是否连续,如果连续,就可以当成一行来处理,从而不必移动行指针。

5)迭代器遍历法

  通过迭代器方式遍历相对简单点,只要设置好迭代器的开始和结束,然后递增,就可以开始遍历了。迭代器主要是通过 MatConstIterator_这个模板类来实现的,定义如下:

cpp 复制代码
template<typename _Tp>
class cv::MatConstIterator_< _Tp >

其继承关系如图所示。

下面我们来看实例,还是减少图像的颜色,然后把结果保存到另一个文件 result.jpg 中。

cpp 复制代码
#include <iostream>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv; //所有 OpenCV 类都在命名空间 cv 下
using namespace std;
#pragma comment(lib, "opencv_world4140d.lib") //引用引入库
void colorReduce(const Mat& image, Mat& outImage, int div = 64)
{
	outImage.create(image.size(), image.type()); //创建同样大小的矩阵,用于保存输出
	// 获得迭代器 
	MatConstIterator_<Vec3b> it_in = image.begin<Vec3b>();
	MatConstIterator_<Vec3b> itend_in = image.end<Vec3b>();
	MatIterator_<Vec3b> it_out = outImage.begin<Vec3b>();
	MatIterator_<Vec3b> itend_out = outImage.end<Vec3b>();
	while (it_in != itend_in)
	{
		(*it_out)[0] = (*it_in)[0] / div * div + div / 2;
		(*it_out)[1] = (*it_in)[1] / div * div + div / 2;
		(*it_out)[2] = (*it_in)[2] / div * div + div / 2;
		it_in++;
		it_out++;
	}
}

int main()
{
	Mat A, B; //仅仅创建了矩阵头
	A = imread("1.jpg", 1);
	imshow("1", A);
	colorReduce(A, B);
	imshow("result", B);
	imwrite("result.jpg", B);
	waitKey(0);
}

4.设置矩阵新值

  成员函数 setTo 可以根据掩码设置矩阵的全部或部分元素为新值,声明如下:

cpp 复制代码
Mat & setTo (InputArray value, InputArray mask=noArray());

  其中参数 value 表示要把矩阵 mask 中元素不为 0 的点全部变为新值;mask 表示与调用者矩阵有相同大小的操作掩码矩阵,setTo 就是根据这个掩码矩阵来决定是否要将调用者矩阵某个相同位置的元素设为 value,比如 mask 矩阵中某个元素不为 0,则调用者矩阵与该元素(mask 矩阵中的元素)相同位置的点(调用者矩阵中的点)就设为 value。

  当默认不添加 mask(掩码矩阵)的时候,表明 mask 是一个与原图尺寸大小一致且元素值(mask 矩阵中的元素)全为非零的矩阵,因此不加mask 的时候会将原矩阵的像素值全部赋为 value,mask 矩阵像素的数据类型必须是 CV_8U 型,可以有一个或多个通道。函数返回 Mat 引用。掩码矩阵 mask 的元素相当于一个标志位,当标志位设置为 1 时,只处理原图像中对应掩码图像中非零的那些像素(就是设置为新值 value),标志位为0 时,处理为 0。

5.得到矩阵的元素总个数

  成员函数 total 返回 Mat 矩阵的元素总个数,比如 30×40 的图像,存在 1200 个像素点,即Mat 矩阵的元素总个数是 1200。函数声明如下:

cpp 复制代码
size_t Mat::total();

6. 矩形类 Rect

  矩形类 Rect 是一个经常会用到的数据结构,用来表示一个矩形区域,在画图场景中经常会碰到。它定义如下:

cpp 复制代码
typedef Rect2i cv::Rect
typedef Rect_<int> cv::Rect2i

  Rect_是一个矩形模板类。类 Rect_有 4 个重要的公有成员变量:x 是矩形左上角横坐标,y 是矩形左上角纵坐标,width 是矩形宽度,height 是矩形高度。这 4 个变量可以围成一个矩形。Rect_有 6 种构造函数形式,常用的有两种:

cpp 复制代码
Rect_ (_Tp _x, _Tp _y, _Tp _width, _Tp _height);
Rect_ (const Rect_ &r);

  其中 x 表示矩形左上角横坐标,y 表示矩形左上角纵坐标,width 表示矩形宽度,height 表示矩形高度。r 表示矩形对象,也就是说用一个现成的矩形对象可以构造另一个新的矩形对象。如果创建一个 Rect 对象 rect(100, 50, 50, 100),那么 rect 常会用到以下几个成员函数:

cpp 复制代码
rect.area(); //返回 rect 的面积 5000
rect.size(); //返回 rect 的尺寸 [50 × 100]
rect.tl(); //返回 rect 左上顶点的坐标 [100, 50]
rect.br(); //返回 rect 右下顶点的坐标 [150, 150]
rect.width(); //返回 rect 的宽度 50
rect.height(); //返回 rect 的高度 100
rect.contains(Point(x, y)); //返回布尔变量,判断 rect 是否包含 Point(x, y)点

还可以求两个矩形的交集和并集:

cpp 复制代码
rect = rect1 & rect2;
rect = rect1 | rect2;

也对矩形进行平移和缩放:

cpp 复制代码
rect = rect + Point(-100, 100); //平移,也就是左上顶点的 x 坐标-100,y 坐标+100
rect = rect + Size(-100, 100); //缩放,左上顶点不变,宽度-100,高度+100

也可以对矩形进行对比,返回布尔变量:

cpp 复制代码
if(rect1 == rect2) ...
if(rect1 != rect2) ...

由于 OpenCV 中没有判断 rect1 是否在 rect2 中的功能,我们可以自己写一个:

cpp 复制代码
bool isInside(Rect rect1, Rect rect2)
{
return (rect1 == (rect1&rect2)); //如果矩形 rect1 在 rect2 中,相与后结果就是
rect1
}

OpenCV 中也没有获取矩形中心点的功能,我们也可以自己写一个:

cpp 复制代码
Point getCenterPoint(Rect rect)
{
Point cpt;
cpt.x = rect.x + cvRound(rect.width/2.0);
cpt.y = rect.y + cvRound(rect.height/2.0);
return cpt;
}

围绕矩形中心的缩放功能也经常会用到,我们也可以自定义实现:

cpp 复制代码
Rect rectCenterScale(Rect rect, Size size)
{
rect = rect + size;
Point pt;
pt.x = cvRound(size.width/2.0);
pt.y = cvRound(size.height/2.0);
return (rect-pt);
}
相关推荐
码匠许师傅4 小时前
【C++ 面试真题】聊聊 C++ 的序列容器
java·c++·面试
W_326004 小时前
Python-OpenCV图像像素与通道:通道拆分合并、深浅拷贝
图像处理·人工智能·python·opencv·机器学习
C++ 老炮儿的技术栈4 小时前
Qt5.9.1 Windows 完整开发环境搭建流程
开发语言·c++·windows·qt·编辑器·代码化
CIO_Alliance4 小时前
AI基础系列(1)| 向量、矩阵、张量在AI中分别扮演什么角色?
大数据·人工智能·线性代数·ai·矩阵·企业cio联盟·企业级ai化转型
键盘会跳舞5 小时前
C++:std::tuple 源码级深度拆解——变参模板、SFINAE与模板元编程核心技巧
开发语言·c++·sfinae·变参模板
bkspiderx5 小时前
从零开始:VS Code搭建C/C++开发环境全指南(Windows/macOS/Linux)
c语言·c++·windows·vs code·c/c++开发环境
东华万里5 小时前
第40篇C++核心基础与工程实践:从底层逻辑到避坑指南
开发语言·c++·面试·大学生专区
雪的季节5 小时前
OPENCV学习(补充1)
人工智能·opencv·学习
luj_17685 小时前
元设计的诱惑与现实
c语言·开发语言·c++·经验分享·算法