VINS-MONO代码解读----vins_estimator(鲁棒初始化部分)

0. 前言

整个初始化部分的pipeline如下所示,参照之前的博客,接下来根据代码一步步讲解。
在这里插入图片描述

1. 旋转约束标定旋转外参Rbc

上回讲了processImageaddFeatureCheckParallax完成了对KF的筛选,我们知道了2nd是否为KF,接下来是初始化和后端求解部分。对于初始化,先标定Ric外参:

//不知道关于外参的任何info,需要标定
if(ESTIMATE_EXTRINSIC == 2)
{
    ROS_INFO("calibrating extrinsic param, rotation movement is needed");
    if (frame_count != 0)
    {
        // 找相邻两帧(bk, bk+1)之间的tracking上的点,构建一个pair,所有pair是一个vector,即corres(pondents),
        // 要求it.start_frame <= frame_count_l && it.endFrame() >= frame_count_r
        vector<pair<Vector3d, Vector3d>> corres = f_manager.getCorresponding(frame_count - 1, frame_count);
        Matrix3d calib_ric;
        //旋转约束+SVD分解求取Ric旋转外参
        if (initial_ex_rotation.CalibrationExRotation(corres, pre_integrations[frame_count]->delta_q, calib_ric))
        {
            ROS_WARN("initial extrinsic rotation calib success");
            ROS_WARN_STREAM("initial extrinsic rotation: " << endl << calib_ric);
            ric[0] = calib_ric;
            RIC[0] = calib_ric;
            ESTIMATE_EXTRINSIC = 1;
        }
    }
}

CalibrationExRotation是主要函数,用于标定旋转外参Ric,理论依据是旋转约束(下面会讲)。

下面依次讲解各部分代码。

1.1 八点法求取relative pose

重点函数solveRelativeR
其中八点法求取Rt,correspondents不小于9时,1.求取E,2.反解并test出4组Rt,3.使用三角化出的深度来判断正确的那组Rt,这里 求E(看14讲),由E反解出4组Rt(看之前SLAM博客),三角化(看VIO博客),带点进Rt判断正深度来确定正确的Rt(看代码注释) 基本上都是调用的cv库函数,原理之前的SLAM和VIO课程中都有学过,相应回顾即可。

Matrix3d InitialEXRotation::solveRelativeR(const vector<pair<Vector3d, Vector3d>> &corres)
{
    //大于9个点才进行求解,否则直接返回Identity
    if (corres.size() >= 9)
    {
        vector<cv::Point2f> ll, rr;
        for (int i = 0; i < int(corres.size()); i++)
        {
            //将找到的这两帧间的所有corr点收集起来,用于后面的 对极约束求本质矩阵E 和 三角化求深度
            ll.push_back(cv::Point2f(corres[i].first(0), corres[i].first(1)));
            rr.push_back(cv::Point2f(corres[i].second(0), corres[i].second(1)));
        }
        cv::Mat E = cv::findFundamentalMat(ll, rr);//对极约束求本质矩阵E=t^R
        cv::Mat_<double> R1, R2, t1, t2;
        decomposeE(E, R1, R2, t1, t2);

        //这应该是什么判断
        if (determinant(R1) + 1.0 < 1e-09)
        {
            E = -E;
            decomposeE(E, R1, R2, t1, t2);
        }
        //用四个解分别进行Triangulation,带入points,求深度为正的比例,取比例最大的作为正确的Rt解
        double ratio1 = max(testTriangulation(ll, rr, R1, t1), testTriangulation(ll, rr, R1, t2));
        double ratio2 = max(testTriangulation(ll, rr, R2, t1), testTriangulation(ll, rr, R2, t2));
        cv::Mat_<double> ans_R_cv = ratio1 > ratio2 ? R1 : R2;

        Matrix3d ans_R_eigen;
        for (int i = 0; i < 3; i++)
            for (int j = 0; j < 3; j++)
                ans_R_eigen(j, i) = ans_R_cv(i, j);
        return ans_R_eigen;
    }
    return Matrix3d::Identity();
}

三角化以及反解E

//将一个点
double InitialEXRotation::testTriangulation(const vector<cv::Point2f> &l,
                                          const vector<cv::Point2f> &r,
                                          cv::Mat_<double> R, cv::Mat_<double> t)
{
    cv::Mat pointcloud;
    //因为triangulatePoints函数要传入分别两帧的pose,所以通常假设第一帧的外参pose是Identity,这样由relative Rt即可得第二帧的pose,即P1 = relative Rt
    cv::Matx34f P = cv::Matx34f(1, 0, 0, 0,
                                0, 1, 0, 0,
                                0, 0, 1, 0);
    cv::Matx34f P1 = cv::Matx34f(R(0, 0), R(0, 1), R(0, 2), t(0),
                                 R(1, 0), R(1, 1), R(1, 2), t(1),
                                 R(2, 0), R(2, 1), R(2, 2), t(2));
    //pointcloud是三角化的结果,是(4*m)维,其中m是传入的correspondents的对数,pointcloud每一列都是相应的三角化之后的为归一化的齐次坐标
    cv::triangulatePoints(P, P1, l, r, pointcloud);
    int front_count = 0;
    for (int i = 0; i < pointcloud.cols; i++)
    {
        //用于归一化,使最后一维为1,这样前三维就是world系下的3D点了,乘以相应的pose就能转化为每个camera系下的3D点(非归一化平面,
        // 如果再 /p_3d_r(2) 则可转化为归一化平面下的3D点,但是要使用深度是否>0来筛选出正确的Rt的解,所以就不归一化,见《14讲》P169)
        double normal_factor = pointcloud.col(i).at<float>(3);

        cv::Mat_<double> p_3d_l = cv::Mat(P) * (pointcloud.col(i) / normal_factor);
        cv::Mat_<double> p_3d_r = cv::Mat(P1) * (pointcloud.col(i) / normal_factor);
        if (p_3d_l(2) > 0 && p_3d_r(2) > 0)
            front_count++;//统计正深度点的个数
    }
    ROS_DEBUG("MotionEstimator: %f", 1.0 * front_count / pointcloud.cols);
    return 1.0 * front_count / pointcloud.cols;//统计所有pointcloud中正深度点的比率(不是很明白,不应该都是正的吗?直接取ratio=1的不行吗?)
}

//从E中分解出Rt
void InitialEXRotation::decomposeE(cv::Mat E,
                                 cv::Mat_<double> &R1, cv::Mat_<double> &R2,
                                 cv::Mat_<double> &t1, cv::Mat_<double> &t2)
{
    cv::SVD svd(E, cv::SVD::MODIFY_A);
    //绕z顺时针90°
    cv::Matx33d W(0, -1, 0,
                  1, 0, 0,
                  0, 0, 1);
    //绕z逆时针90°
    cv::Matx33d Wt(0, 1, 0,
                   -1, 0, 0,
                   0, 0, 1);
    R1 = svd.u * cv::Mat(W) * svd.vt;
    R2 = svd.u * cv::Mat(Wt) * svd.vt;
    t1 = svd.u.col(2);//3*3取最后一个是待求量(TODO:为什么t2可以直接取负号?)
    t2 = -svd.u.col(2);
}

1.2 旋转约束和旋转residual的构建

在这里插入图片描述
两个路径求取的 q b k c k + 1 q_{b_kc_{k+1}} qbkck+1理想状态下相等,详见VIO Ch7博客2.2节
q b k c k + 1 = q b k b k + 1 ∗ q b c = q b c ∗ q c k c k + 1 \begin{align} q_{b_kc_{k+1}} = q_{b_kb_{k+1}} * q_{bc} = q_{bc} * q_{c_kc_{k+1}} \end{align} qbkck+1=qbkbk+1qbc=qbcqckck+1
但是实际情况,二者之间存在误差,即为residual,移项(左移右)即得residual:
r e s i d u a l = q b c − 1 ∗ q b k b k + 1 − 1 ∗ q b c ∗ q c k c k + 1 = q c b ∗ q b k + 1 b k ∗ q c b − 1 ∗ q c k c k + 1 \begin{align} residual &=q_{bc}^{-1}*q_{b_kb_{k+1}}^{-1} * q_{bc} * q_{c_kc_{k+1}} \tag{1.1} \\ &=q_{cb}*q_{b_{k+1}b_k} * q_{cb}^{-1} * q_{c_kc_{k+1}}\tag{1.2} \\ \end{align} residual=qbc1qbkbk+11qbcqckck+1=qcbqbk+1bkqcb1qckck+1(1.1)(1.2)
上式(1.1)和(1.2)两种形式分别对应旋转外参 q b c q_{bc} qbc q c b q_{cb} qcb,跟后面左右乘的定义方式有关,代码中使用的(1.2)形式的定义,左乘L定义为 相机旋转四元数的左乘矩阵,右乘R定义为 IMU旋转四元数的右乘矩阵,下面会讲。

对应到CalibrationExRotation代码中:
Rc q c k c k + 1 q_{c_kc_{k+1}} qckck+1,由SfM获得,从第 k + 1 k+1 k+1帧到第 k k k帧的rotation
Rimudelta_q q b k + 1 b k q_{b_{k+1}b_k} qbk+1bk:由IMU预积分获得,从第 k k k帧积分到第 k + 1 k+1 k+1
所以式(1.2)中剩下的左边部分 q c b ∗ q b k + 1 b k ∗ q c b − 1 q_{cb}*q_{b_{k+1}b_k} * q_{cb}^{-1} qcbqbk+1bkqcb1对应代码中的Rc_g

1.3 左乘右乘的构建

由于使用了式(1.2)形式来构建左右乘,所以式(1.2)整理为
( [ q c k c k + 1 ] L − [ q b k + 1 b k ] R ) q b c = 0 \begin{align} ([\bm q_{c_kc_{k+1}}]_{\bm L} - [q_{b_{k+1}b_k}]_{\bm R})\bm{q}_{bc} = 0 \end{align} ([qckck+1]L[qbk+1bk]R)qbc=0

崔华坤PDF中的
在这里插入图片描述


文献中的
在这里插入图片描述

二者形式上有差别,但是本质上应该是相同的,崔华坤PDF中的应该是根据代码推出来的,我们暂且使用这一套,对应到代码中:

//L为相机旋转四元数的左乘矩阵,记四元数为(w,x,y,z)
//实际构建的是qc_b
//构建结果为
//w -z  y  x
//z  w -x  y
//-y x  w  z
//-z -y -z  w
double w = Quaterniond(Rc[i]).w();
Vector3d q = Quaterniond(Rc[i]).vec();
L.block<3, 3>(0, 0) = w * Matrix3d::Identity() + Utility::skewSymmetric(q);//构建A左上角3*3
L.block<3, 1>(0, 3) = q;
L.block<1, 3>(3, 0) = -q.transpose();
L(3, 3) = w;

//R为IMU旋转四元数的右乘矩阵,同样记四元数为(w,x,y,z)
//构建结果为
//w  z  -y  x
//-z  w  x  y
//y  -x  w  z
//-x -y -z  w
Quaterniond R_ij(Rimu[i]);
w = R_ij.w();
q = R_ij.vec();
R.block<3, 3>(0, 0) = w * Matrix3d::Identity() - Utility::skewSymmetric(q);
R.block<3, 1>(0, 3) = q;
R.block<1, 3>(3, 0) = -q.transpose();
R(3, 3) = w;

1.4 系数矩阵A的构建

如上构建出来的四元数左右乘矩阵均为(4*4)维,每组可构建一个式(2)所示的方程,多组correspondents构成多个方程,组成超定方程组,于是就需要构建超定方程组的系数矩阵 A A A,维度为(frame_count * 4, 4),如下所示:
在这里插入图片描述

使用 SVD分解求解超定方程组,取最后一个特征值对应的特征向量即为我们所求的 q c b \bm q_{cb} qcb

1.5 鲁棒核函数

为了使方程具有更好的数值稳定性,在构建系数矩阵 A A A时根据每项的residual为相应的(4*4)块添加了权重 ω \omega ω,权重由Huber损失函数计算而得:
在这里插入图片描述
对应代码:

Quaterniond r1(Rc[i]);
Quaterniond r2(Rc_g[i]);

// 这里的angular_distance是角度误差,5是Huber核函数中的threshold,
// angularDistance: returns the angle (in radian) between two rotations返回两个旋转之间的相对旋转(弧度表示),再换算为角度
// 旋转矩阵R和轴角theta之间的关系:tr(R)=1+2cos(theta),所以theta=arccos( (tr(R)-1)/2 ),就是angularDistance的返回值
double angular_distance = 180 / M_PI * r1.angularDistance(r2);
ROS_DEBUG(
    "%d %f", i, angular_distance);

//Huber鲁棒核函数
double huber = angular_distance > 5.0 ? 5.0 / angular_distance : 1.0;

其中angularDistance函数应该对应着上图的式(9),输出rad,代码中又转化为angle。式(8)中的threshold设为5。如此,方程的构建部分就全部完成。

1.6 q c b q_{cb} qcb 的求解

使用SVD分解求解超定方程组,根据之前Triangulation的经验(博客第3.2节),需要对特征值 σ 4 / σ 3 \sigma_4/\sigma_3 σ4/σ3的比值进行判断,但是此处发现并没有满足远小于的条件,后面再探究吧。

关于特征值的判断条件
在这里插入图片描述
至此已完成旋转外参标定的所有理论代码部分的对应,附上该部分完整代码注释。

// 旋转约束标定Ric,两条路径求取的旋转应相同,所以移项构建方程,并根据误差项r的鲁棒核函数值w(r)对每一项的系数进行加权,将大于threshold的部分的权值设的较小,
// 最终使用SVD分解求取特征值最小的特征向量
// 详见第7章博客:https://blog.csdn.net/qq_37746927/article/details/133782580#t4
// 旋转约束:qbk_ck+1=qbk_bk+1*qbc=qbc*qck_ck+1
bool InitialEXRotation::CalibrationExRotation(vector<pair<Vector3d, Vector3d>> corres, Quaterniond delta_q_imu, Matrix3d &calib_ric_result)
{
    frame_count++;
    //correspondents对数不小于9时求出Rt: 1.求取E,2.反解并test出4组Rt,3.使用三角化出的深度来判断正确的那组Rt
    //Rc即Rck_ck+1
    Rc.push_back(solveRelativeR(corres));
    //Rimu即delta_q即qbk_bk+1即Rbk+1_bk
    Rimu.push_back(delta_q_imu.toRotationMatrix());
    //旋转约束:qbk_ck+1 = qbk_bk+1 * qbc = qbc * qck_ck+1,移项(左移右)即得residual=qbc^(-1)*qbk_bk+1^(-1) * qbc * qck_ck+1,
    //其中Rc=qck_ck+1,剩下的项就是qbc^(-1) * qbk_bk+1^(-1) * qbc = qcb * qbk_bk+1^(-1) * qcb^(-1),记为Rc_g
    //上式左边是rbc版本构建A时L为IMU,右边是rcb版本,构建A时L为camera,这里采用了后者rcb,L为camera
    Rc_g.push_back(ric.inverse() * delta_q_imu * ric);//Rc_g * Rc就是整个左移右的residual

    //构建A矩阵
    Eigen::MatrixXd A(frame_count * 4, 4);//这个维度是什么意思?从第[1]帧开始
    A.setZero();
    int sum_ok = 0;
    for (int i = 1; i <= frame_count; i++)
    {
        Quaterniond r1(Rc[i]);
        Quaterniond r2(Rc_g[i]);

        // 这里的angular_distance是角度误差,5是Huber核函数中的threshold,
        // angularDistance: returns the angle (in radian) between two rotations返回两个旋转之间的相对旋转(弧度表示),再换算为角度
        // 旋转矩阵R和轴角theta之间的关系:tr(R)=1+2cos(theta),所以theta=arccos( (tr(R)-1)/2 ),就是angularDistance的返回值
        double angular_distance = 180 / M_PI * r1.angularDistance(r2);
        ROS_DEBUG(
            "%d %f", i, angular_distance);

        //Huber鲁棒核函数
        double huber = angular_distance > 5.0 ? 5.0 / angular_distance : 1.0;
        ++sum_ok;
        Matrix4d L, R;

        //L为相机旋转四元数的左乘矩阵,记四元数为(w,x,y,z)
        //实际构建的是qc_b
        //构建结果为
        //w -z  y  x
        //z  w -x  y
        //-y x  w  z
        //-z -y -z  w
        double w = Quaterniond(Rc[i]).w();
        Vector3d q = Quaterniond(Rc[i]).vec();
        L.block<3, 3>(0, 0) = w * Matrix3d::Identity() + Utility::skewSymmetric(q);//构建A左上角3*3
        L.block<3, 1>(0, 3) = q;
        L.block<1, 3>(3, 0) = -q.transpose();
        L(3, 3) = w;

        //R为IMU旋转四元数的右乘矩阵,同样记四元数为(w,x,y,z)
        //构建结果为
        //w  z  -y  x
        //-z  w  x  y
        //y  -x  w  z
        //-x -y -z  w
        Quaterniond R_ij(Rimu[i]);
        w = R_ij.w();
        q = R_ij.vec();
        R.block<3, 3>(0, 0) = w * Matrix3d::Identity() - Utility::skewSymmetric(q);
        R.block<3, 1>(0, 3) = q;
        R.block<1, 3>(3, 0) = -q.transpose();
        R(3, 3) = w;

        A.block<4, 4>((i - 1) * 4, 0) = huber * (L - R);
    }

    JacobiSVD<MatrixXd> svd(A, ComputeFullU | ComputeFullV);
    Matrix<double, 4, 1> x = svd.matrixV().col(3);
    Quaterniond estimated_R(x);
    ric = estimated_R.toRotationMatrix().inverse();//这里可以看到求得ric实际上是rci=rcb
    //cout << svd.singularValues().transpose() << endl;
    //cout << ric << endl;
    Vector3d ric_cov;
    ric_cov = svd.singularValues().tail<3>();
    ROS_DEBUG_STREAM("svd.singularValues():" << svd.singularValues().transpose() << "  ric_cov: " << ric_cov.transpose() << "  ric_cov(1): " << ric_cov(1));
    //这个取ric_cov相当于取所有特征值里面的第三个,要求它大于某个阈值,最后一个理论上来说应该是特别小的,
    //根据SVD有效性判断方法,σ4 << σ3才算解有效,σ4/σ3比值的阈值一般取1e-2~1e-4算远小于,
    //但是实际Debug发现SVD解出来的一半也不满足这个条件,所以ESTIMATE_EXTRINSIC直接config为0,不标了?
    if (frame_count >= WINDOW_SIZE && ric_cov(1) > 0.25)
    {
        calib_ric_result = ric;
        return true;
    }
    else
        return false;
}

2. initialStructure()

执行视觉惯性联合初始化,包含3部分:

  1. visual SfM,(BA)
  2. 对所有frame的PnP (BA)
  3. visual和IMU的align(估计gyro bias,scale,重力细化RefineGravity) (BA)
    3步都有BA

2.1 visual SfM

2.1 旋转约束标定gyro bias

2.2 速度、重力和尺度初始化 LinearAlignment()

2.3 重力细化 RefineGravity()

5. Reference

1.

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

redis的数据类型的操作增删改查

redis的数据类型的操作增删改查 redis的高可用&#xff1a; 在集群当中有一个非常重要的指标&#xff0c;提供正常服务的时间的百分比&#xff08;365天&#xff09;99.9% redis的高可用的含义要更加宽泛&#xff0c;正常服务是指标之一数据容量扩展&#xff0c;数据的安全性…

开源的文本编辑器Notepad++ 8.6.0版本在Windows系统上的下载与安装配置

目录 前言一、Notepad 安装二、使用配置总结 前言 Notepad 是一款简单而强大的文本编辑工具&#xff0c;通常用于快速创建和编辑文本文件。以下是 Notepad 工具的详细介绍。注&#xff1a;文末附有下载链接&#xff01; 主要特点&#xff1a; ——简洁易用&#xff1a; Note…

matlab画双坐标图的样式

matlab画双坐标图的样式 %% clc,clear,close all; t0:0.1:9*pi; figure; [AX,Ha,Hb]plotyy(t,sin(t),t,exp(t)); % 绘图并创建句柄 % ----------------- 设置刻度 set(AX(1),yTick,[-1.250:0.25:1.25]) % 设置左边Y轴的刻度 set(AX(2),yTick,[0:50:350]) …

vue005——vue组件入门(非单文件组件和单文件组件)

一、非单文件组件 1.1、单文件组件的使用 1.1.1、局部注册 1、第一步&#xff1a;创建school组件 2、第二步&#xff1a;注册组件&#xff08;局部注册&#xff09; 3、第三步&#xff1a;使用组件&#xff08;编写组件标签&#xff09; <!DOCTYPE html> <html>…

【MySQL】数据库基础操作

&#x1f451;专栏内容&#xff1a;MySQL⛪个人主页&#xff1a;子夜的星的主页&#x1f495;座右铭&#xff1a;前路未远&#xff0c;步履不停 目录 一、数据库操作1、创建数据库2、查看所有数据库3、选定指定数据库4、删除数据库 二、数据表操作1、创建数据表2、查看所有表3、…

debian 设置系统默认以命令行方式启动,关闭x windows

debian 设置系统默认以命令行方式启动&#xff0c;关闭x windows 2021-01-02 tech linux 设置 grub启动设置在/etc/default/grub中&#xff0c;打开 default grub 配置: $ sudo vim /etc/default/grub修改以下配置&#xff1a; 更新grub&#xff0c;设置多用户启动: …

为何百兆静态库能打进数兆的可执行文件?

第三方库是工程开发必不可少的部分&#xff0c;而第三方库可以是.a和.framework的静态库&#xff0c;也可以是.framework的动态库&#xff0c;其中静态库是最常用的方式。 静态库往往比较大&#xff0c;可在打包到可执行文件之后&#xff0c;对安装包大小的增加远远小于静态库本…

Linux 常见命令篇

history 获取执行的指令记录 语法格式: history [参数] 常用参数&#xff1a; -a 写入命令记录 -c 清空命令记录 -d 删除指定序号的命令记录 -n 读取命令记录 -r 读取命令记录到缓冲区 -s 将指定的命令添加到缓冲区 -w 将缓冲区信息写入到历史文件 history#获取最近的三条…

Python是个什么鬼?朋友靠它拿了5个offer

闺蜜乐乐&#xff0c;外院科班出身&#xff0c;手持专八和CATTI证书&#xff0c;没想到找工作时却碰了钉子… 半夜12点&#xff0c;乐乐跟我开启了吐槽模式&#xff1a; 拿到offer的都是小公司的翻译活儿&#xff0c;只能糊个口。稍微好点的平台要求可就多了&#xff0c;不仅语…

java计算下一个整10分钟时间点

最近工作上遇到需要固定在整10分钟一个周期调度某个任务&#xff0c;所以需要这样一个功能&#xff0c;记录下 package org.example;import com.google.gson.Gson; import org.apache.commons.lang3.time.DateUtils;import java.io.InputStream; import java.util.Calendar; i…

Hive内置表生成函数

Hive内置UDTF 1、UDF、UDAF、UDTF简介2、Hive内置UDTF 1、UDF、UDAF、UDTF简介 在Hive中&#xff0c;所有的运算符和用户定义函数&#xff0c;包括用户定义的和内置的&#xff0c;统称为UDF&#xff08;User-Defined Functions&#xff09;。如下图所示&#xff1a; UDF官方文档…

Springboot学生疫情管理系统-计算机毕设 附源码 25567

Springboot学生疫情管理系统的设计与实现 摘 要 随着互联网趋势的到来&#xff0c;各行各业都在考虑利用互联网将自己推广出去&#xff0c;最好方式就是建立自己的互联网系统&#xff0c;并对其进行维护和管理。在现实运用中&#xff0c;应用软件的工作规则和开发步骤&#xf…

Dubbo配置注册中心设置application的name使用驼峰命名法可能存在的隐藏启动异常问题

原创/朱季谦 首先&#xff0c;先提一个建议&#xff0c;在SpringBootDubbo项目中&#xff0c;Dubbo配置注册中心设置的application命名name的值&#xff0c;最好使用xxx-xxx-xxx这样格式的&#xff0c;避免随便使用驼峰命名。因为使用驼峰命名法&#xff0c;在Spring的IOC容器…

数据结构总复习

文章目录 线性表动态分配的顺序存储结构链式存储 线性表 动态分配的顺序存储结构 通过分析代码&#xff0c;我们发现&#xff0c;要注意什么&#xff1a; 要分清你的下标Insert 函数是可以用来没有元素的时候&#xff0c;增加元素的Init(或者Create )函数一般只用来分配空间…

Python中如何选择Web开发框架?

Python开发中Web框架可谓是百花齐放&#xff0c;各式各样的web框架层出不穷&#xff0c;那么对于需要进行Python开发的我们来说&#xff0c;如何选择web框架也就变成了一门学问了。本篇文章主要是介绍目前一些比较有特点受欢迎的Web框架&#xff0c;我们可以根据各个Web框架的特…

在线定制印刷系统源码/定制云印刷/个性印刷在线DIY定制商城系统/全站DIV+CSS 布局+手机、PC端

源码简介&#xff1a; 在线定制印刷系统源码/定制云印刷&#xff0c;它是个性印刷在线DIY定制商城系统&#xff0c;而且全站采用DIVCSS 布局&#xff0c;可以手机、PC端实时互通。 支持多种产品定制&#xff0c;包括但不限于水杯、雨伞、U盘、T恤、衬衫和四件套。独创的制作间…

Jenkins与Docker的自动化CI/CD流水线实践

Pipeline 有诸多优点&#xff0c;例如&#xff1a; 项目发布可视化&#xff0c;明确阶段&#xff0c;方便处理问题 一个Jenkins File文件管理整个项目生命周期 Jenkins File可以放到项目代码中版本管理 Jenkins管理界面 操作实例&#xff1a;Pipeline的简单使用 这里是比较…

电源控制系统架构(PCSA)之系统分区电压域

目录 4.1 电压域 4.1.1 系统逻辑 4.1.2 Always-On逻辑 4.1.3 处理器Clusters 4.1.4 图形处理器 4.1.5 其他功能 4.1.6 SoC分区示例 本章描述基于Arm组件的SoC划分为电压域和电源域。 所描述的选择并不详尽&#xff0c;只是可能性的一个子集。目的是描述基于Arm组件的SoC…

MySQL-04-InnoDB存储引擎锁和加锁分析

Latch一般称为闩锁&#xff08;轻量级锁&#xff09;&#xff0c;因为其要求锁定的时间必须非常短。在InnoDB存储引擎中&#xff0c;latch又分为mutex&#xff08;互斥量&#xff09;和rwlock&#xff08;读写锁&#xff09;。 Lock的对象是事务&#xff0c;用来锁定的是…

实验题【网关设置+VRRP+静态路由+OSPF】(H3C模拟器)

嘿&#xff0c;这里是目录&#xff01; ⭐ H3C模拟器资源链接1. 实验示意图2. 要求和考核目标3. 当前配置3.1 PC1、PC2、PC3、PC4和PC5配置3.2 SW配置3.2.1 SW2配置3.2.2 SW3配置3.2.3 SW4配置3.2.4 SW1配置 3.2. R配置3.2.1 R1配置3.2.2 R2配置 ⭐ H3C模拟器资源链接 H3C网络…