【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.rhkb.cn/news/383952.html

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

相关文章

调度子系统在特定时间执行

时序逻辑调度器设计模式允许您安排Simulink子系统在指定时间执行。以下模型说明了这种设计模式。 时序逻辑调度器图表包含以下逻辑&#xff1a; 时序逻辑调度器的关键行为 时序逻辑调度器图表包含两个状态&#xff0c;它们以不同的速率调度函数调用子系统A1、A2和A3的执行&…

【管控业财一体化】

1. 引言 大型集团在现代企业管理中扮演着举足轻重的角色&#xff0c;其管控业财一体化解决方案是实现企业高效运营的关键。随着数字化转型的加速&#xff0c;业财一体化不再局限于财务与业务流程的简单融合&#xff0c;而是向着更深层次的数据驱动、智能化决策和价值创造方向发…

Python小工具——监听某网站的数据变化并进行邮件通知

目录 一、需求描述 二、解析 三、实例代码 一、需求描述 监听自考网2024年广东省6月份的毕业生学历注册进度&#xff0c;这是网址&#xff1a;https://www.chsi.com.cn/xlcx/count_zk.jsp&#xff0c; 如上图所示&#xff0c;我们想知道这个红色的空格啥时候被填满&#xf…

7月26日贪心练习-摆动序列专题

前言 大家好&#xff0c;今天学习用贪心思想解决摆动序列问题&#xff0c;共三题&#xff0c;分享自己的思路&#xff0c;请大家多多支持 算法思想 大家可以先看看这道我们后面会讲的题看看怎么个事&#xff0c;. - 力扣&#xff08;LeetCode&#xff09; 由此题题解说明算…

SwiftSage:参考人脑双系统,结合快思和慢想的智能体,解决复杂任务同时降低成本

SwiftSage&#xff1a;参考人脑双系统&#xff0c;结合快思和慢想的智能体&#xff0c;解决复杂任务同时降低成本 提出背景解法拆解子解法1&#xff1a;SWIFT模块子解法2&#xff1a;SAGE模块模块整合和决策树逻辑链 SwiftSage 工作流程效果 论文&#xff1a;SWIFTSAGE: A Gene…

GMSSL2.x编译鸿蒙静态库和动态库及使用

一、编译环境准备 1.1 开发工具 DevEco-Studio下载。 1.2 SDK下载 下载编译第三方库的SDK有两种方式&#xff0c;第一种方式从官方渠道根据电脑系统选择对应的SDK版本&#xff0c;第二种方式通过DevEco-Studio下载SDK。本文只介绍通过DevEco-Studio下载SDK的方式。 安装SD…

SSD基本架构与工作原理

SSD的核心由一个或多核心的CPU控制器、DRAM缓存以及多个NAND闪存芯片组成。CPU控制器负责管理所有读写操作&#xff0c;并通过DRAM缓存存储映射表等元数据&#xff0c;以加速寻址过程。 NAND闪存则是数据存储的实际介质&#xff0c;其组织结构从大到小依次为通道&#xff08;包…

海山数据库(He3DB)性能优化方案解析

前端优化是一个永恒的话题&#xff0c;每个前端开发者都希望自己的页面能够快速加载&#xff0c;给用户良好的体验。但往往事与愿违。因此&#xff0c;本文从编码优化、构建优化、部署优化三方面入手进行web页面性能优化。 1. 编码优化 1.1. Css优化 1.1.1. 合理使用css选择…

HarmonyOS NEXT零基础入门到实战-第四部分

自定义组件: 概念: 由框架直接提供的称为 系统组件&#xff0c; 由开发者定义的称为 自定义组件。 源代码&#xff1a; Component struct MyCom { build() { Column() { Text(我是一个自定义组件) } } } Component struct MyHeader { build() { Row(…

【React】package.json 文件详解

文章目录 一、package.json 文件的基本结构二、package.json 文件的关键字段1. name 和 version2. description3. main4. scripts5. dependencies 和 devDependencies6. repository7. keywords8. author 和 license9. bugs 和 homepage 三、package.json 文件的高级配置1. 配置…

十、SpringBoot 统⼀功能处理【拦截器、统一数据返回格式、统一异常处理】

十、SpringBoot 统⼀功能处理 1. 拦截器【HandlerInterceptor、WebMvcConfig】1.1 拦截器快速⼊⻔⾃定义拦截器&#xff1a;实现HandlerInterceptor接⼝&#xff0c;并重写其所有⽅法注册配置拦截器&#xff1a;实现WebMvcConfigurer接⼝&#xff0c;并重写addInterceptors⽅法…

压测实操--produce压测方案

作者&#xff1a;九月 环境信息&#xff1a; 操作系统centos7.9&#xff0c;kafka版本为hdp集群中的2.0版本。 Producer相关参数 使用Kafka自带的kafka-producer-perf-test.sh脚本进行压测&#xff0c;该脚本参数为&#xff1a; 在producer涉及到性能的关键因素可能会存在如…

DetectorRS

文章目录 AbstractMethodExperimentAblation StudyMain Results Conclusion未来展望 link code Abstract 本文介绍了一种新的对象检测器——DetectoRS&#xff0c;通过在骨干网络设计中引入递归特征金字塔和可切换的空洞卷积机制&#xff0c;实现了出色的性能提升。在宏观层面…

谷粒商城实战笔记-54-商品服务-API-三级分类-拖拽效果

文章目录 一&#xff0c;54-商品服务-API-三级分类-修改-拖拽效果1&#xff0c;el-tree控件加上允许拖拽的属性2&#xff0c;是否允许拖拽3&#xff0c;完整代码 一&#xff0c;54-商品服务-API-三级分类-修改-拖拽效果 本节的主要内容是给三级分类树形结构加上拖拽功能&#…

四、GD32 MCU 常见外设介绍 (4) EXTI 中断介绍

4.EXTI 中断介绍 EXTI(中断/事件控制器)包含多个相互独立的边沿检测电路并且能够向处理器内核产生中断请求或唤醒事件。 EXTI 有三种触发类型&#xff1a;上升沿触发、下降沿触发和任意沿触发。 EXTI中的每一个边沿检测电路都可以独立配置和屏蔽。 4.1.GD32 EXTI 外设原理简介…

如何使用C#自制一个Windows安装包

原文链接&#xff1a;https://www.cnblogs.com/zhaotianff/p/17387496.html 以前都在用InstallShield制作安装包&#xff0c;基本需求是能满足的&#xff0c;但也有一些缺点&#xff1a; 1、界面不能完全定制 2、不能直接调用代码里的功能 平常使用一些其它软件&#xff0c;…

【基础算法总结】优先级队列

优先级队列 1.最后一块石头的重量2.数据流中的第 K 大元素4.前K个高频单词4.数据流的中位数 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧!&#x1f603;&#x1f603; 1…

FPGA开发——LED流水灯实现先从左往右流水,再从右往左流水

一、概述 我们在设计完一个方向的流水灯的设计时&#xff0c;总是会想实现让流水灯倒着流水回去的设计&#xff0c;这里我也是一样&#xff0c;实现这种设计的方法有很多种&#xff0c;其中就有直接使用case语句将所有可能包含进去编写&#xff0c;这种设计方法是最简单的&…

leetcode日记(51)不同路径Ⅱ

和上一道题&#xff08;无障碍物的最短路径&#xff09;很像&#xff0c;但事实上比上一题多了优化方法 根据上一题改的代码如下&#xff0c;添加了对障碍物的判定&#xff0c;如果有障碍物则将数组值设为0。 class Solution { public:int uniquePathsWithObstacles(vector&l…

Origin制作线性拟合回归图

选中数据&#xff0c;点下方散点图 调整散点颜色 在分析中打开线性拟合回归 添加文本 显示上轴