【OpenCV C++20 学习笔记】扫描图片数据

扫描图片数据

  • 应用情景
    • 图像数据扫描的难点
    • 颜色空间缩减(color space reduction)
    • 查询表
  • 扫描算法
    • 计算查询表
    • 统计运算时长
    • 连续内存
    • 3种扫描方法
      • C风格的扫描方法
      • 迭代器方法
      • 坐标方法
      • LUT方法
  • 算法效率对比
  • 结论

应用情景

图像数据扫描的难点

在上一篇文章《基本图像容器——Mat》中,已经详细描述了OpenCV储存图像数据的形式(图像的每个像素储存为一个矩阵中的数据项,矩阵的每个数据项包括各个颜色通道的值,如RGB3通道包含红、绿、蓝共3个通道的颜色值)。所以,矩阵的值的列数为矩阵的列数乘以颜色通道数,如下图所示,OpenCV默认的BGR格式的数据有3个颜色通道,所以实际有m*3列数值;行数则不变:
矩阵数据示意图

如果我们使用uchar(8bits)类型去储存每个像素的值,那么像素的每个颜色通道可以有256个可能的值( 2 8 = 256 2^8=256 28=256),这样的话如果是3通道的数据,那每个数据项就有( 25 6 3 = 16 , 777 , 216 256^3=16,777,216 2563=16,777,216)种可能的颜色值了。如果矩阵很大,那就会给算法的执行带来很大压力。

颜色空间缩减(color space reduction)

为了减轻扫描图像数据的算法压力,可以将现有的颜色值除以某个值,从而缩小颜色值的值域。例如,将所有0-9的颜色值都用0替代,将所有10-19的颜色值都用10替代,依此类推。用数学公式来表示:
I n e w = ( I o l d 10 ) ∗ 10 I_{new} = ( \frac{I_{old}} {10})*10 Inew=(10Iold)10

  • I n e w I_{new} Inew:缩减之后的颜色值,以下简称缩减值
  • I o l d I_{old} Iold:缩减之前的原始颜色值,以下简称原始值
    推而广之,如果想要应用其他的缩减率,比如2,也就是说,0-1都用0来代替,2-3都用1来代替,那除数就会变成2;将这个除数用 d d d来表示,并称之为缩减因子,则公式会变成:
    I n e w = ( I o l d d ) ∗ d I_{new} = ( \frac{I_{old}} {d})*d Inew=(dIold)d
  • I n e w I_{new} Inew:缩减之后的颜色值,以下简称缩减值
  • I o l d I_{old} Iold:缩减之前的原始颜色值,以下简称原始值
  • d d d缩减因子

注意:uchar类型被整数除了之后,得出的结果依然是uchar类型
但是对每个颜色值都执行上述的除法和乘法运算,仍然会消耗很多算力。然而,由于每个颜色值的值域是有限的,比如uchar类型是[0, 256],所以如果能直接计算出所有可能的缩减结果,并进行赋值运算,会节省很多算力。所以,就产生了“查询表”

查询表

查询表就是储存与原始值一一对应的缩减值的数组(一维或多维)。一旦数据类型确定,查询表的大小就确定不变了。比如,uchar类型的查询表就只有256个缩减值,因为原始值总共就只有256种可能。然后运用这个查询表将每个原始值都替换成查询表中对应的值,就不必对每一个原始值都进行缩减计算了,而只是简单的查询和赋值,这样就能节省算力。而且,原始值越多,节省效果越好。这就是查询表的优势。

扫描算法

该实例将3种算法放在一个main函数中,main函数接收一个参数数组argv[],其中有可以有3个或4个参数:

  • 默认参数:程序名(调试时不需要用户指定)
  • 图片文件路径
  • 缩减因子:即上述公式中的 d d d
  • 图片读取格式:以灰度格式读取,则传递“G”;如不指定该参数,则默认采用BGR格式读取
    静态函数help()详细描述了该方法的使用:
static void help()
{std::cout<< "\n--------------------------------------------------------------------------" << endl<< "这个程序展示如何在OpenCV中扫描图片(cv::Mat)"<< "根据输入图片路径以及缩减因子(大于0的整数)" << endl<< "这个程序分别使用了C风格方法、迭代器方法、坐标方法和LUT方法进行扫描" << endl<< "参数输入:" << endl<< "程序名 <图片路径> <缩减因子> [G]" << endl<< "如果加了参数G,则使用灰度格式读入图片" << endl<< "--------------------------------------------------------------------------" << endl<< endl;
}

在main函数中,首先对输入的参数进行分析和处理:

if (argc < 3)
{//参数数量小于3个,则退出函数并输出提示cout << "Not enough parameters" << endl;return -1;
}Mat I, J;
if (argc == 4 && !strcmp(argv[3], "G"))	//当参数数量为4且,最后一个参数是"G"时I = imread(argv[1], IMREAD_GRAYSCALE);	//以灰度格式读取图片,并储存在Mat对象I中
elseI = imread(argv[1], IMREAD_COLOR);	//否则以BGR格式读取图片,并储存在Mat对象I中if (I.empty())
{//如果读取的数据为空,则退出函数并输出提示cout << "The image" << argv[1] << " could not be loaded." << endl;return -1;
}

计算查询表

接下来,根据传入的参数数组中的第3个参数,即argv[2],计算查询表:

int divideWith { 0 }; //1-4行:将字符串转换成数字,并储存在变量divdeWith中作为缩减因子
stringstream s;
s << argv[2];
s >> divideWith;
if (!s || !divideWith)
{//如果无法接收第3个参数,或者参数为0,则退出函数,并输出提示cout << "Invalid number entered for dividing. " << endl;return -1;
}uchar table[256];	//用一个长度为256的一维数组来储存查询表
for (int i { 0 }; i < 256; ++i)	//计算查询表,计算结果为uchar类型table[i] = static_cast<uchar>(divideWith * (i / divideWith));

在将字符串转换成数字的过程中使用了C++中的字符串流stringstream,字符串流对象s,接收参数字符串argv[2],然后将其传给整数变量divideWith,作为后面进行缩减运算的缩减因子。
计算查询表的for循环块,进行了256次循环,将[0, 255]中的所有整数依次进行了缩减运算,得出256个uchar类型的缩减值,并依次储存在一个uchar类型的数组中。

统计运算时长

这个程序为了比较不同扫描方法的速度,使用了使用了OpenCV中的cv::getTickCount()cv::getTickFrequency()函数进行计时。前者返回某个运行节点的CPU的tick数(一个tick为CPU频率的倒数);后者返回CPU每秒的tick数。如果获取事件起始点和结束点的CPU的tick数,然后用它们的差除以CPU每秒的tick数就能得到事件起始点和结束点之间的时间差,单位为秒。具体代码如下:

double t { static_cast<double>getTickCount() };	//将返回值类型从原本的int型转换为double型
//扫描函数
t = (static_cast<double>getTickCount() - t)/getTickFrequency();	//转换为double型后,除法运算结果中的小数就会被保留
cout << "用时 " << t << " 秒" << endl;

这几行代码放在每个扫描方法的前后,从而能为每个扫描方法计算运行时间,便可比较它们的运行速度

连续内存

虽然储存图像数据的Mat对象可能是个二维甚至多维矩阵,但是在内存中矩阵是被按行分成若干个一维数组储存的。这些一维数组可能被放在一起,形成一个连续的内存空间,也可能被分开储存。OpenCV中的cv::Mat::isContinuous()可以判断矩阵在内存中是否是连续的。被储存在一个连续内存中的矩阵扫描起来会更快。

3种扫描方法

如果没耐心对比每种方法的思路,可以直接结论部分看每种方法的优缺点,然后找到相应方法的章节进行进一步阅读。
但是,除了LUT方法,前三种方法都需要自己编写将替换原始值的语句,该语句中对查询表的索引,思路比较绕,为了避免重复,本文只在第一种方法,即C风格的扫描方法中对此进行了详细解释,如果看不懂可以到该章节进行参考。

C风格的扫描方法

C风格的扫描方法少不了要进行C风格的二维数组的遍历操作,包括行指针、列指针等。

//! [scan-c]
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{//只接收uchar类型的矩阵CV_Assert(I.depth() == CV_8U);	//depth函数返回每个通道的数值的类型,如CV_8U为8比特无符号字符串类型int channels { I.channels() };	//channels函数返回矩阵中的颜色通道数int nRows { I.rows };int nCols { I.cols * channels };	//实际列数为矩阵列数*颜色通道数if (I.isContinuous()){//如果是储存在连续空间,则按一维数组来处理nCols *= nRows;	//一维数组的实际列数为原列数*行数nRows = 1;	//一维数组行数为1}int i, j;uchar* p;	//uchar类型的p指针用来储存扫描结果for (i = 0; i < nRows; ++i){p = I.ptr<uchar>(i);	//ptr模板函数根据行数i返回矩阵第i行的行指针,并在尖括号中指定返回的指针类型for (j = 0; j < nCols; ++j){//原来的p[j]是个列指针,实际为矩阵i行j列的值,即颜色的原始值//因为查询表示按0-255的顺序排列的,//所以,以p[j]为下标访问查询表,正好能访问到原始值对应的缩减值p[j] = table[p[j]];	//将查询表中的缩减值赋值给p指针中对应的位置//由于p是指向Mat对象I中的矩阵的,所以I实际上也被更改了}}return I;	//返回被更改的I
}
//! [scan-c]

注意,该函数传入的第2个参数为uchar类型的常量指针常量,即这个指针指向的对象不能被修改,指针的地址也不能被修改,这样才能保证查询表在函数运行结束之后仍然没变,可以被再次利用。
该函数最核心的部分就是对数组进行遍历操作的for循环语句。这里巧妙地使用列指针,即原始值,作为访问查询表数组的下标,从而找到对应的缩减值。具体思路写在注释里了,读者可以参阅。
当矩阵是被储存在连续的内存空间中的时候,实际上是对一个一维数组进行遍历,i为1,只循环1次。
如果确定矩阵是储存在连续内存空间中的,那么还有另外一种方法可以完成对它的遍历:

uchar* p { I.data };	//data是Mat类的public数据成员,它储存了Mat对象首行的行指针for(unsinged int i { 0 }; i < ncol*nrows; ++i)//行指针自增之后就变成了列指针,即一维数组第i列的地址//再进行解引用操作,就得到了第i列的值*p++ = table[*p];

迭代器方法

使用迭代器比用数组指针更加方便,因为不用考虑行指针、列指针的问题,没有嵌套的for循环,只需要一层for循环。
但是,迭代器只能代替矩阵中的数据项。也就是说,如果是单通道的灰度格式的图片数据,迭代器正好代替矩阵中的每个颜色值;但是,如果是3通道的BGR格式的图片数据,迭代器实际上代替的是一个长度为3的数组,其中包含了3个颜色值(分别为蓝、绿、红值),在这种情况下还必须对迭代器进行进一步的操作。

//! [scan-iterator]
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{CV_Assert(I.depth() == CV_8U);const int channels{ I.channels() };switch (channels){//switch语句实现对两种情况的分别处理case 1:	//单通道的灰度图片数据{MatIterator_<uchar> it, end;	//声明起始和终止迭代器for (it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)//begin和end函数分别返回指向第一个和最后一个数据项的迭代器*it = table[*it];	//对迭代器进行解引用操作可获得对应的数据项break;}case 3:	//3通道的BGR图片数据{MatIterator_<Vec3b> it, end;for (it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it){//还需要对迭代器中的3个值分别进行赋值操作//*it[0]为数据项在第一个颜色通道的值(*it)[0] = table[(*it)[0]];(*it)[1] = table[(*it)[1]];(*it)[2] = table[(*it)[2]];}}}return I;
}
//! [scan-iterator]

如果在3通道的情况,没有对迭代器中的数组进行进一步的操作的话,那改变的只是每个像素的蓝色值。因为OpenCV将RGB转换成了BGR,第一个值是蓝色值。

坐标方法

这种方法其实一般是用来确定需要某个数据项的行数和列数的,也称为随机获取,所以并不建议用来扫描图像数据。
这种方法也需要对单通道和3通道数据进行分情况的处理。
在单通道数据中,该方法运用了数据项的动态地址,并返回数据项的引用,这由函数cv::Mat::at()来实现;
在3通道数据中,该方法运用了Mat_类型达到了同样的目的。/Mat_类型为储存了每个数据项的类型信息的Mat类型;Mat_类型可以用(row, col)方式来访问;这种访问等同于在Mat类型中用at(row, col)来进行访问
具体解释见代码注释:

//! [scan-random]
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{CV_Assert(I.depth() == CV_8U);const int channels{ I.channels() };switch (channels){case 1:{for (int i{ 0 }; i < I.rows; ++i)for (int j{ 0 }; j < I.cols; ++j)//at函数获取矩阵在i行j列的数据项的地址,并返回它的引用I.at<uchar>(i, j) = table[I.at<uchar>(i, j)];break;}case 3:{//Vec3b类型为OpenCV中定义的3字节数据类型,用来储存长度为3的uchar类型数组正好Mat_<Vec3b> _I = I;	//这里不能用C++20的初始化语法,会报错:无法转化参数类型for (int i{ 0 }; i < I.rows; ++i)for (int j{ 0 }; j < I.cols; ++j){_I(i, j)[0] = table[_I(i, j)[0]];_I(i, j)[1] = table[_I(i, j)[1]];_I(i, j)[2] = table[_I(i, j)[2]];}I = _I;break;}}return I;
}
//! [scan-random]

LUT方法

在OpenCV的core模块,有一个专门用来修改图片数据矩阵中的值的方法,cv::LUT()。其具体用法如下:

//! [table-init]
Mat lookUpTable(1, 256, CV_8U); //用Mat构造函数创建uchar类型的1维矩阵
uchar* p = lookUpTable.ptr();	//获取矩阵的首地址
for (int i = 0; i < 256; ++i)p[i] = table[i];	//将之前计算的查询表中的数值复制到矩阵中
//! [table-init]//! [table-use]
LUT(I, lookUpTable, J);
//! [table-use]

cv::LUT()函数使用了3个参数

  • 需要进行修改的原始矩阵,输入矩阵
  • 查询表矩阵
  • 接收修改结果的矩阵,输出矩阵
    该函数没有返回值。只用一条语句,就实现了用查询表矩阵中的缩减值替换输入矩阵中的原始值,然后输出替换结果。

算法效率对比

将3种方法整合在一个cpp文件中,并统计每个方法的运算时长。整合后的代码如下:

#include <opencv2/core.hpp>
#include <opencv2/core/utility.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>import <iostream>;
import <sstream>;using namespace cv;
using namespace std;static void help()
{std::cout<< "\n--------------------------------------------------------------------------" << endl<< "这个程序展示如何在OpenCV中扫描图片(cv::Mat)"<< "根据输入图片路径以及缩减因子(大于0的整数)" << endl<< "这个程序分别使用了C风格方法、迭代器方法、坐标方法和LUT方法进行扫描" << endl<< "参数输入:" << endl<< "程序名 <图片路径> <缩减因子> [G]" << endl<< "如果加了参数G,则使用灰度格式读入图片" << endl<< "--------------------------------------------------------------------------" << endl<< endl;
}Mat& ScanImageAndReduceC(Mat& I, const uchar* table);
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* table);
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* table);int main(int argc, char* argv[])
{help();if (argc < 3){std::cout << "缺少参数" << endl;return -1;}Mat I, J;if (argc == 4 && !strcmp(argv[3], "G"))I = imread(argv[1], IMREAD_GRAYSCALE);elseI = imread(argv[1], IMREAD_COLOR);if (I.empty()){std::cout << "图片" << argv[1] << "打不开。" << endl;return -1;}//! [dividewith]int divideWith{ 0 };stringstream s;s << argv[2];s >> divideWith;if (!s || !divideWith){std::cout << "无效缩减因子。 " << endl;return -1;}uchar table[256];for (int i{ 0 }; i < 256; ++i)table[i] = static_cast<uchar>(divideWith * (i / divideWith));//! [dividewith]const int times{ 100 };double t;t = static_cast<double>(getTickCount());for (int i{ 0 }; i < times; ++i){Mat clone_i{ I.clone() };J = ScanImageAndReduceC(clone_i, table);}t = 1000 * (static_cast<double>(getTickCount()) - t) / getTickFrequency();t /= times;std::cout << "C风格方法每运行"<< times << "次平均耗时: " << t << "毫秒。" << endl;t = static_cast<double>(getTickCount());for (int i{ 0 }; i < times; ++i){Mat clone_i{ I.clone() };J = ScanImageAndReduceIterator(clone_i, table);}t = 1000 * (static_cast<double>(getTickCount()) - t) / getTickFrequency();t /= times;std::cout << "迭代器方法每运行"<< times << "次平均耗时:" << t << "毫秒。" << endl;t = static_cast<double>(getTickCount());for (int i{ 0 }; i < times; ++i){Mat clone_i{ I.clone() };ScanImageAndReduceRandomAccess(clone_i, table);}t = 1000 * (static_cast<double>(getTickCount()) - t) / getTickFrequency();t /= times;std::cout << "坐标方法每运行"<< times << "次平均耗时:" << t << "毫秒。" << endl;//! [table-init]Mat lookUpTable(1, 256, CV_8U);uchar* p{ lookUpTable.ptr() };for (int i{ 0 }; i < 256; ++i)p[i] = table[i];//! [table-init]t = static_cast<double>(getTickCount());for (int i{ 0 }; i < times; ++i)//! [table-use]LUT(I, lookUpTable, J);//! [table-use]t = 1000 * (static_cast<double>(getTickCount()) - t) / getTickFrequency();t /= times;std::cout << "LUT方法每运行 "<< times << "次平均耗时:" << t << "毫秒。" << endl;return 0;
}//! [scan-c]
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{// accept only char type matricesCV_Assert(I.depth() == CV_8U);int channels{ I.channels() };int nRows{ I.rows };int nCols{ I.cols * channels };if (I.isContinuous()){nCols *= nRows;nRows = 1;}int i, j;uchar* p;for (i = 0; i < nRows; ++i){p = I.ptr<uchar>(i);for (j = 0; j < nCols; ++j){p[j] = table[p[j]];}}return I;
}
//! [scan-c]//! [scan-iterator]
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{// accept only char type matricesCV_Assert(I.depth() == CV_8U);const int channels{ I.channels() };switch (channels){case 1:{MatIterator_<uchar> it, end;for (it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)*it = table[*it];break;}case 3:{MatIterator_<Vec3b> it, end;for (it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it){(*it)[0] = table[(*it)[0]];(*it)[1] = table[(*it)[1]];(*it)[2] = table[(*it)[2]];}}}return I;
}
//! [scan-iterator]//! [scan-random]
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{// accept only char type matricesCV_Assert(I.depth() == CV_8U);const int channels{ I.channels() };switch (channels){case 1:{for (int i{ 0 }; i < I.rows; ++i)for (int j{ 0 }; j < I.cols; ++j)I.at<uchar>(i, j) = table[I.at<uchar>(i, j)];break;}case 3:{Mat_<Vec3b> _I = I;for (int i{ 0 }; i < I.rows; ++i)for (int j{ 0 }; j < I.cols; ++j){_I(i, j)[0] = table[_I(i, j)[0]];_I(i, j)[1] = table[_I(i, j)[1]];_I(i, j)[2] = table[_I(i, j)[2]];}I = _I;break;}}return I;
}
//! [scan-random]

要调试程序,需要输入参数的main函数。在VS中,可以在项目属性中提前输入参数,如下图中黑体的th.jpg 10,就是用空格隔开的两个参数。注意第一个参数可以不用输入,默认为项目程序名。
输入main函数参数
我这里使用的th.jpg,是一个1920*1200的图片。这是为了测试处理比较大的图片的时候各种方法的性能。
调试运行结果为:
图片扫描结果

结论

从运行结果可以看到用时最短的是LUT方法,这是因为OpenCV库使用了多线程方法加快了运行速度。但这并不代表LUT方法永远是最好的,其他方法的优缺点如下:

  • 给简单的小图片写扫描程序的时候,用C风格的数组方法更好,因为没必要动用多线程;
  • 迭代器方法更安全,但是相对较慢
  • 坐标的方法需要获取动态引用,是在调试模式中耗时最多的方法,但是在发行模式中可能会比迭代器方法更快;不过肯定没有迭代器方法安全

** 文章较长,不免有遗漏或笔误,欢迎大家指正!**

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OAK-FFC 分体式相机使用入门介绍

概述 OAK FFC 主控板和多种可选配镜头模组非常适合灵活的搭建您的3D人工智能产品原型。由于镜头是分体式的&#xff0c;因此你可以根据需要测量的距离&#xff0c;自定义深度相机安装基线&#xff0c;并根据你的项目要求&#xff08;分辨率、快门类型、FPS、光学元件&#xff…

FPGA教程|零基础到实战的全方位指导

在数字化时代&#xff0c;FPGA&#xff08;现场可编程门阵列&#xff09;作为一种灵活的硬件编程平台&#xff0c;正逐渐成为电子工程师和计算机科学家必备的技能之一。无论你是电子专业的学生&#xff0c;还是职场中的工程师&#xff0c;掌握FPGA开发技术&#xff0c;都将为你…

前端与HTML

前端与HTML 什么是前端&#xff1f; 解决GUI人机交互问题跨终端&#xff08;PC/移动浏览器&#xff09;&#xff08;客户端小程序&#xff09;&#xff08;VR/AR等&#xff09;Web技术栈 前端工程师&#xff1a;利用web技术栈解决多端图形界面用户交互的工程师 前端技术栈 …

CDGA|数据治理:安全如何贯穿数据供给、流通、使用全过程

随着信息技术的飞速发展&#xff0c;数据已经成为企业运营、社会管理和经济发展的核心要素。然而&#xff0c;数据在带来巨大价值的同时&#xff0c;也伴随着诸多安全风险。因此&#xff0c;数据治理的重要性日益凸显&#xff0c;它不仅仅是对数据的简单管理&#xff0c;更是确…

rv1126物体检测 rkmedia、opencv……

整体码流流向&#xff1a; 因此代码也分为这几部分&#xff1a; VI&#xff1a;采集视频 配置视频采集信息 模型推理线程&#xff1a;获取VI码流、载入模型、进行模型推理、保存推理结果 画框线程&#xff1a;获取VI码流、获取推理结果、显示推理结果、输出码流到VENC VENC…

如何使用C#快速创建定时任务

原文链接&#xff1a;https://www.cnblogs.com/zhaotianff/p/17511040.html 使用Windows的计划任务功能可以创建定时任务。 使用schtasks.exe可以对计划任务进行管理&#xff0c;而不需要编写额外代码 这里掌握schtasks /CREATE 的几个核心参数就可以快速创建计划任务 /SC …

论文总结:A Survey on Evaluation of Large Language Models-鲁棒性相关内容

A Survey on Evaluation of Large Language Models 只取了鲁棒性相关的内容 LLMs&#xff1a;《A Survey on Evaluation of Large Language Models大型语言模型评估综述》理解智能本质(具备推理能力)、AI评估的重要性(识别当前算法的局限性设 3.2.1 Robustness鲁棒性&#xf…

Gitlab以及分支管理

一、概述 Git 是一个分布式版本控制系统&#xff0c;用于跟踪文件的变化&#xff0c;尤其是源代码的变化。它由 Linus Torvalds 于 2005 年开发&#xff0c;旨在帮助管理大型软件项目的开发过程。 二、Git 的功能特性 Git 是关注于文件数据整体的变化&#xff0c;直接会将文件…

HTTP模块(二)

HTTP 设置 HTTP 响应报文 HTTP报文常见属性&#xff1a; const http require(http);const server http.createServer((request, response) > {// 设置请求状态码 2xx 4xx 5xxresponse.statusCode 200;// 设置请求描述 了解即可response.statusMessage hello// 指定响…

谷粒商城实战笔记-59-商品服务-API-品牌管理-使用逆向工程的前后端代码

文章目录 一&#xff0c; 使用逆向工程生成的代码二&#xff0c;生成品牌管理菜单三&#xff0c;几个小问题 在本次的技术实践中&#xff0c;我们利用逆向工程的方法成功地为后台管理系统增加了品牌管理功能。这种开发方式不仅能快速地构建起功能模块&#xff0c;还能在一定程度…

uni-app声生命周期

应用的生命周期函数在App.vue页面 onLaunch:当uni-app初始化完成时触发&#xff08;全局触发一次&#xff09; onShow:当uni-app启动&#xff0c;或从后台进入前台时显示 onHide:当uni-app从前台进入后台 onError:当uni-app报错时触发,异常信息为err 页面的生命周期 onLoad…

Linux-安装VMware-01

一、认识linux Linux 是一个开源的类 Unix 操作系统&#xff0c;由林纳斯托瓦兹&#xff08;Linus Torvalds&#xff09;于1991年首次发布。Linux 是许多计算机硬件的底层操作系统&#xff0c;特别是服务器、嵌入式系统和个人电脑。它支持多种架构&#xff0c;包括 x86、x64、A…

算法学习笔记(8.8)-多重背包

目录 Question: 思路解析&#xff1a; 代码示例 多重背包的优化问题&#xff1a; 1.二进制优化 代码示例&#xff1a; 2.单调队列优化(滑动窗口) 代码示例 Question: 4. 多重背包问题 I - AcWing题库https://www.acwing.com/problem/content/description/4/ 多重背包简单来说其…

eqmx上读取数据处理以后添加到数据库中

目录 定义一些静态变量 定时器事件的处理器 订阅数据的执行器 处理json格式数据和将处理好的数据添加到数据库中 要求和最终效果 总结一下 定义一些静态变量 // 在这里都定义成全局的 一般都定义成静态的private static MqttClient mqttClient; // mqtt客户端 private s…

springboo 整合 redis

springBoot 整合 redis starter启动依赖。—包含自动装配类—完成相应的装配功能。 引入依赖 <!--引入了redis整合springboot 的依赖--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis&…

基于opencv[python]的人脸检测

1 图片爬虫 这里的代码转载自&#xff1a;http://t.csdnimg.cn/T4R4F # 获取图片数据 import os.path import fake_useragent import requests from lxml import etree# UA伪装 head {"User-Agent": fake_useragent.UserAgent().random}pic_name 0 def request_pic…

谷粒商城实战笔记-65-商品服务-API-品牌管理-表单校验自定义校验器

文章目录 1&#xff0c;el-form品牌logo图片自定义显示2&#xff0c;重新导入和注册element-ui组件3&#xff0c;修改brand-add-or-update.vue控件的表单校验规则firstLetter 校验规则sort 校验规则 1&#xff0c;el-form品牌logo图片自定义显示 为了在品牌列表中自定义显示品…

Typora 以 Github 作为图床使用 PicGo 上传图片

本文简练快速介绍如标题所述的操作流程 文章目录 1.前言1.1 图床简述1.2 Github图床的优缺点1.2.1 优点1.2.2 缺点 2.下载PicGo3.Github访问加速4.用github创建图床服务器4.1 注册4.2 创建仓库 4.3 生成TOKEN令牌5.设置PicGo6.设置Typora7.完成 1.前言 1.1 图床简述 图床&…

人工智能背后的图灵测试(TuringTest)是什么?

人工智能背后的图灵测试(TuringTest)是什么&#xff1f; 一、什么是图灵测试 图灵测试&#xff08;Turing Test&#xff09;由英国数学家和计算机科学家阿兰图灵&#xff08;Alan Turing&#xff09;在1950年提出&#xff0c;用以判断机器是否具有人类智能。图灵在其论文《计…

3.1、数据结构-线性表

数据结构 数据结构线性结构线性表顺序存储和链式存储区别单链表的插入和删除练习题 栈和队列练习题 串&#xff08;了解&#xff09; 数据结构 数据结构该章节非常重要&#xff0c;上午每年都会考10-12分选择题下午一个大题 什么叫数据结构&#xff1f;我们首先来理解一下什…