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

news2024/11/18 16:35:48

扫描图片数据

  • 应用情景
    • 图像数据扫描的难点
    • 颜色空间缩减(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中
else
    I = 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);
    else
        I = 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 matrices
    CV_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 matrices
    CV_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 matrices
    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)
                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.coloradmin.cn/o/1949108.html

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

相关文章

项目一缓存商品

文章目录 概要整体架构流程技术细节小结 概要 因为商品是经常被浏览的,所以数据库的访问量就问大大增加,造成负载过大影响性能,所以我们需要把商品缓存到redis当中,因为redis是存在内存中的,所以效率会比MySQL的快. 整体架构流程 技术细节 我们在缓存时需要保持数据的一致性所…

AHK是让任何软件都支持 Shift + 鼠标滚轮 实现界面水平滚动

目录 基本介绍 详细特点 图解安装 下载失败&#xff1f;缓慢&#xff1f; 创建并运行脚本代码&#x1f603; 新建空 xxx.ahk文件 vscode/记事本等编辑工具打开 复制并粘贴简易脚本 运行 其他问题 问题一&#xff1a;弹出无法执行此脚本 关闭脚本 基本介绍 AutoHot…

zookeeper开启SASL权限认证

目录 一、SASL介绍 二、使用 SASL 进行身份验证 2.1 服务器到服务器的身份验证 2.2 客户端到服务器身份验证 三、验证功能 一、SASL介绍 默认情况下&#xff0c;ZooKeeper 不使用任何形式的身份验证并允许匿名连接。但是&#xff0c;它支持 Java 身份验证与授权服务(JAAS)…

用户需要什么-软件的工程可用性(第一部分)01

Larry L. Constantine 著&#xff0c;Huang Yin 译 “究竟用户的需要是什么&#xff1f;”如果 Fred 作为一个程序员而不是一个心理学家时他可能会提出这 样一个问题。用户们通常需要更多&#xff0c;而开发人员似乎看上去并不能很好的领会并更好的满足他们。对于我们而言&…

WPF使用TouchSocket实现Tcp client

文章目录 前言1、页面展示2、主页面UI代码2、TCP client的UI代码3、Tcp client后台代码实现4、UI与后台代码的关联 前言 在该篇的Demo中&#xff0c;您可以找到以下内容&#xff1a; 1、TouchSocket的使用&#xff1b; 2、CommunityToolkit.Mvvm的使用&#xff1b; 3、AvalonD…

IF=8.5 MIMIC-IV高阶玩法!中国用新指标SHR+机器学习拿一区top,思路太牛了

‍ MIMIC-IV 发文难&#xff1f;那是你还没遇到对的思路&#xff01;如今机器学习数据库挖掘的文章层出不穷&#xff0c;今天介绍的这篇文章是在MIMIC-IV数据库的基础上&#xff0c;用了一个新指标—应激性高血糖比&#xff08;SHR&#xff09;&#xff0c;结合机器学习构建预测…

【iOS】——Block循环引用

循环引用原因 如果在Block中使用附有_ _strong修饰符的对象类型自动变量&#xff0c;那么当Block从栈复制到堆时&#xff0c;该对象为Block所持有&#xff0c;这样容易引起循环引用。 HPPerson *person [[HPPerson alloc] init];person.block ^{NSLog("person.age--- …

Redis使用场景-热点数据缓存

什么是缓存&#xff1f; 为了把一些经常访问的数据放入缓存中已减少对数据库的访问&#xff0c;从而减少数据库的压力&#xff0c;提高程序的性能。【内存中存储】-效率快 缓存的原理 什么样的数据适合放入缓存中&#xff1f; 1.查询频率高且修改频率低 2.数据安全性低 哪些组件…

《python语言程序设计》第6章第7题财务应用程序:计算未来投资,编写函数计算制定年数以给定利率

记住这里增加循环应该是以年为单位。但是添加的数是月为单位 此处需留意其实点不是1&#xff0c;1代表1年&#xff0c;这里月所以其实是12&#xff0c;这里的单位是月&#xff0c;而不是年。 python for i in range(12,monthNum12,12) 如果你把12都换成1呢&#xff1f;&…

本地生活抽佣系统搭建:如何让系统具有竞争优势?

随着本地生活的潜力不断展现&#xff0c;本地生活服务商逐渐成为新兴职业中的一大热门&#xff0c;本地生活抽佣系统搭建的热度也一直保持着飙升的状态。 抖音生活发布的《2023年数据报告》显示&#xff0c;2023年&#xff0c;抖音生活服务平台总交易额增长256%&#xff0c;抖…

监测Nginx访问日志状态码,并做相应动作

文章目录 引言I 监测 Nginx 访问日志情况,并做相应动作1.1 前提准备1.2 访问日志 502 情况,重启 bttomcat9服务1.3 其他案例:访问日志 502 情况,重启 php-fpm 服务II 将Shell 脚本check499.sh包装成systemd服务2.1 创建systemd服务2.2 配置service2.3 开机启动2.4 其他常用…

自监督学习概述(Self-Supervised Learning,SSL)

自监督学习&#xff08;Self-Supervised Learning&#xff0c;SSL&#xff09;是一种机器学习方法&#xff0c;旨在利用未标记数据进行训练。这种方法通过从数据本身生成伪标签&#xff0c;来创建监督信号&#xff0c;使得模型能够学习有效的数据表示。自监督学习在深度学习领域…

HTTP传输下载和P2P传输下载的区别?

HTTP传输下载和P2P&#xff08;Peer-to-Peer&#xff09;传输下载在多个方面存在显著的区别&#xff0c;以下是详细的分析&#xff1a; 1. 工作原理 HTTP传输下载&#xff1a; HTTP&#xff08;Hypertext Transfer Protocol&#xff09;是一种用于在Web上进行数据通信的协议&…

PHP多功能投票系统源码小程序

&#x1f389;决策不再难&#xff01;「多功能投票小程序」一键搞定所有选择困难症✨ &#x1f914;选择困难&#xff1f;「多功能投票小程序」来救场&#xff01; 每次聚会、团队讨论还是日常小决策&#xff0c;是不是总有那么几个瞬间让你陷入“选哪个好呢&#xff1f;”的…

spine to unity-2.利用边缘框实现实时碰撞检测

主要讲spine的边缘框&#xff0c;在unity中&#xff0c;实现实时碰撞检测。其中使用的素材&#xff0c;是我为独立游戏ink制作的动画。独立游戏ink的开发日志&#xff0c;在小红薯持续更新中。spine工具包的安装&#xff0c;下载请参考spine to unity-1spine BoundingBoxFollow…

【MySQL篇】Percona XtraBackup标准化全库完整备份策略(第三篇,总共五篇)

&#x1f4ab;《博主介绍》&#xff1a;✨又是一天没白过&#xff0c;我是奈斯&#xff0c;DBA一名✨ &#x1f4ab;《擅长领域》&#xff1a;✌️擅长Oracle、MySQL、SQLserver、阿里云AnalyticDB for MySQL(分布式数据仓库)、Linux&#xff0c;也在扩展大数据方向的知识面✌️…

STM32H7的LPUART基础和唤醒示例

STM32H7的LPUART基础知识 硬件框图低功耗的高级特性低功耗串口的时钟以及波特率低功耗串口发送时序低功耗串口支持的唤醒方式 LPUART 的全称是 Low power universal synchronous asynchronous receiver transmitter&#xff0c;中文意思是低功耗通用异步收发器&#xff0c;简称…

【C语言】栈的实现(数据结构)

前言&#xff1a; 还是举一个生活中的例子&#xff0c;大家都玩过积木&#xff0c;当我们把积木叠起来的时候&#xff0c;如果要拿到最底部的积木&#xff0c;我们必须从顶端一个一个打出&#xff0c;最后才能拿到底部的积木&#xff0c;也就是后进先出&#xff08;先进后出&a…

Python - 开源库 ReportLab 库合并 CVS 和图像生成 PDF 文档

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/140281680 免责声明&#xff1a;本文来源于个人知识与公开资料&#xff0c;仅用于学术交流&#xff0c;欢迎讨论&#xff0c;不支持转载。 Report…

[Spring] MyBatis操作数据库(基础)

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…