OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点

这篇具有很好参考价值的文章主要介绍了OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

若该文为原创文章,转载请注明原文出处
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/136535848
各位读者,知识无穷而人力有穷,要么改需求,要么找专业人士,要么自己研究
红胖子(红模仿)的博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬结合等等)持续更新中…(点击传送门)

OpenCV开发专栏(点击传送门)

上一篇:《OpenCV开发笔记(七十五):相机标定矫正中使用remap重映射进行畸变矫正》
下一篇:持续补充中…


前言

  知道图像畸变矫映射的原理之后,那么如何得到相机的内参是矫正的第一步,内参决定了内参矩阵(中心点、焦距等),用内参矩阵才能计算出投影矩阵,从而将原本畸变的图像矫正为平面投影图像。
  本篇描述了相机成形的原理,并绘制出识别的角点。


Demo

  OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点,OpenCV图像处理2,Qt开发,opencv,相机小孔成像,棋盘识别,相机标定棋盘

  OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点,OpenCV图像处理2,Qt开发,opencv,相机小孔成像,棋盘识别,相机标定棋盘

  OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点,OpenCV图像处理2,Qt开发,opencv,相机小孔成像,棋盘识别,相机标定棋盘


相机成形的原理

小孔成像原理

  OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点,OpenCV图像处理2,Qt开发,opencv,相机小孔成像,棋盘识别,相机标定棋盘

  得到矩阵计算原理:
  OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点,OpenCV图像处理2,Qt开发,opencv,相机小孔成像,棋盘识别,相机标定棋盘

  得到计算过程:
  OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点,OpenCV图像处理2,Qt开发,opencv,相机小孔成像,棋盘识别,相机标定棋盘


相机的畸变

  相机的畸变是指相机镜头对物体所成的像相对于物体本身而言的失真程度,它是光学透镜的固有特性。畸变产生的原因主要是透镜的边缘部分和中心部分的放大倍率不一样。
畸变分为以下几类:

  • 径向畸变
  • 切向畸变
  • 薄棱镜畸变
      通常情况下,径向畸变的影响要远远大于其他畸变。畸变是不可消除的,但在实际的应用中,可以通过一些软件来进行畸变的补偿,如OpenCV、MATLAB等。

径向畸变

  主要由透镜不同部位放大倍率不同造成,它又分为枕形畸变和桶形畸变两种。枕形畸变,也称为鞍形形变,视野中边缘区域的放大率远大于光轴中心区域的放大率,常用在远摄镜头中。桶形畸变则与枕形畸变相反,视野中光轴中心区域的放大率远大于边缘区域的放大率,常出现在广角镜头和鱼眼镜头中
  OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点,OpenCV图像处理2,Qt开发,opencv,相机小孔成像,棋盘识别,相机标定棋盘

切向畸变

  主要由透镜安装与成像平面不平行造成,类似于透视原理,如近大远小、圆变椭圆等。
  OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点,OpenCV图像处理2,Qt开发,opencv,相机小孔成像,棋盘识别,相机标定棋盘

薄棱镜畸变

  由透镜设计缺陷和加工安装误差造成,又称为线性畸变。其影响较小,一般忽略不计。


棋牌识别步骤

步骤一:标定采集的数据图像

  采集一张棋盘图片,要确认他是可以被识别的。
  OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点,OpenCV图像处理2,Qt开发,opencv,相机小孔成像,棋盘识别,相机标定棋盘

  读取图像,这里由于图片较大,我们重设大小为原来宽高的1/2:

    // 使用图片
    std::string srcFilePath = "D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/chessboard.png";
//    std::string srcFilePath = "D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/24.jpg";
    cv::Mat srcMat = cv::imread(srcFilePath);
    int chessboardColCornerCount = 6;
    int chessboardRowCornerCount = 9;
    // 步骤一:读取文件
//    cv::imshow("1", srcMat);
//    cv::waitKey(0);
    // 步骤二:缩放,太大了缩放下(可省略)
    cv::resize(srcMat, srcMat, cv::Size(srcMat.cols / 2, srcMat.rows / 2));
    cv::Mat srcMat2 = srcMat.clone();
    cv::Mat srcMat3 = srcMat.clone();
//    cv::imshow("2", srcMat);
//    cv::waitKey(0);

步骤二:图像处理,提取角点,并绘制出来

  先灰度化,然后输入预制的纵向横向角数量,使用棋盘角点函数提取角点

    // 步骤三:灰度化
    cv::Mat grayMat;
    cv::cvtColor(srcMat, grayMat, cv::COLOR_BGR2GRAY);
    cv::imshow("3", grayMat);
//    cv::waitKey(0);
    // 步骤四:检测角点
    std::vector<cv::Point2f> vectorPoint2fCorners;
    bool patternWasFound = false;
    patternWasFound = cv::findChessboardCorners(grayMat,
                                                cv::Size(chessboardColCornerCount, chessboardRowCornerCount),
                                                vectorPoint2fCorners,
                                                cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_FAST_CHECK | cv::CALIB_CB_NORMALIZE_IMAGE);
    /*
    enum { CALIB_CB_ADAPTIVE_THRESH = 1,    // 使用自适应阈值将图像转化成二值图像
           CALIB_CB_NORMALIZE_IMAGE = 2,    // 归一化图像灰度系数(用直方图均衡化或者自适应阈值)
           CALIB_CB_FILTER_QUADS    = 4,    // 在轮廓提取阶段,使用附加条件排除错误的假设
           CALIB_CB_FAST_CHECK      = 8     // 快速检测
         };
    */
    cvui::printf(srcMat, 0, 0, 1.0, 0xFF0000, "found = %s", patternWasFound ? "true" : "false");
    cvui::printf(srcMat, 0, 24, 1.0, 0xFF0000, "count = %d", vectorPoint2fCorners.size());
    qDebug() << __FILE__ << __LINE__ << vectorPoint2fCorners.size();
    // 步骤五:绘制棋盘点
    cv::drawChessboardCorners(srcMat2,
                              cv::Size(chessboardColCornerCount, chessboardRowCornerCount),
                              vectorPoint2fCorners,
                              patternWasFound);

步骤三:进行亚像素角点计算,进一步提取图片准确性

// 步骤六:进一步提取亚像素角点
    cv::TermCriteria criteria(CV_TERMCRIT_EPS | CV_TERMCRIT_ITER,   // 类型
                              30,                                   // 参数二: 最大次数
                              0.001);                               // 参数三:迭代终止阈值
    /*
    #define CV_TERMCRIT_ITER    1                   // 终止条件为: 达到最大迭代次数终止
    #define CV_TERMCRIT_NUMBER  CV_TERMCRIT_ITER    //
    #define CV_TERMCRIT_EPS     2                   // 终止条件为: 迭代到阈值终止
    */
    qDebug() << __FILE__ << __LINE__ << vectorPoint2fCorners.size();
    cv::cornerSubPix(grayMat,
                     vectorPoint2fCorners,
                     cv::Size(11, 11),
                     cv::Size(-1, -1),
                     criteria);

函数原型

findChessboardCorners:识别预制棋盘角点数量的棋盘

  OpenCV 中用于检测图像中棋盘角点的函数。

bool cv::findChessboardCorners(InputArray image,
                           Size patternSize,
                           OutputArray corners,
                           int flags=CALIB_CB_ADAPTIVE_THRESH+CALIB_CB_NORMALIZE_IMAGE)

  参数解释:

  • image:输入的图像,通常是一个灰度图像,因为角点检测在灰度空间中进行更为准确。
  • patternSize:棋盘的内角点数量,例如一个 8x6 的棋盘会有 48 个内角点,所以 patternSize 会是 Size(8, 6)。
  • corners:检测到的角点输出数组。
  • flags:不同的标志,用于指定角点检测的不同方法。可以是以下的一个或多个标志的组合:
    CALIB_CB_ADAPTIVE_THRESH:使用自适应阈值将图像转换为二值图像,而不是使用固定的全局阈值。
    CALIB_CB_NORMALIZE_IMAGE:在寻找角点之前,先对图像进行归一化,以提高鲁棒性。
    CALIB_CB_FAST_CHECK:仅检查角点候选者中的少量点,用于快速检测,但可能不如标准方法准确。

  函数返回值是一个布尔值,如果找到足够的角点以形成一个棋盘模式,则返回 true;否则返回 false。
  findChessboardCorners 函数通常用于相机标定,通过检测棋盘角点来确定图像与真实世界之间的对应关系。一旦角点被检测到,就可以使用这些点来估计相机的内参(如焦距、主点)和外参(如旋转和平移矩阵)。

drawChessboardCorners:绘制棋盘角点

  OpenCV中的一个函数,用于在检测到的棋盘角点周围绘制方框。这对于相机标定、图像对齐等应用非常有用。

void cv::drawChessboardCorners(InputOutputArray image,
                            Size patternSize,
                            InputArray corners,
                            bool patternWasFound)

  参数解释:

  • image:输入的图像,通常是一个彩色图像,函数会在这个图像上绘制角点。
  • patternSize:棋盘的内角点数量,例如一个 8x6 的棋盘会有 48 个内角点,所以 patternSize 会是 Size(8, 6)。
  • corners:检测到的角点,通常是通过 findChessboardCorners 函数得到的。
  • patternWasFound:一个布尔值,表示是否找到了足够的角点来形成一个棋盘模式。如果为 true,则函数会在角点周围绘制彩色的方框;如果为 false,则只会绘制白色的方框。
    这个函数通常与 findChessboardCorners 结合使用,以检测图像中的棋盘角点,并在检测到的角点周围绘制方框。这对于视觉校准和相机标定等任务非常有用。

TermCriteria:迭代终止模板类

  TermCriteria是OpenCV中用于指定迭代算法终止条件的模板类。它取代了之前的CvTermCriteria,并且在许多OpenCV算法中作为迭代求解的结构被使用。

struct TermCriteria {  
    enum { COUNT=1, MAX_ITER=COUNT, EPS=2 };  
    TermCriteria();  
    TermCriteria(int type, int maxCount, double epsilon);  
    TermCriteria(const CvTermCriteria& criteria);  
};

  构造时需要三个参数:

  • 类型(type):它决定了迭代终止的条件。类型可以是CV_TERMCRIT_ITER、CV_TERMCRIT_EPS或CV_TERMCRIT_ITER+CV_TERMCRIT_EPS。在C++中,这些宏对应的版本分别为TermCriteria::COUNT、TermCriteria::EPS。
    CV_TERMCRIT_ITER或TermCriteria::COUNT:表示迭代终止条件为达到最大迭代次数;
    CV_TERMCRIT_EPS或TermCriteria::EPS:表示迭代到特定的阈值就终止;
    CV_TERMCRIT_ITER+CV_TERMCRIT_EPS:则表示两者都作为迭代终止条件。
  • 迭代的最大次数(maxCount):这是算法可以执行的最大迭代次数。
  • 特定的阈值(epsilon):当满足这个精确度时,迭代算法会停止。

cornerSubPix:亚像素角点提取

  OpenCV中用于精确化角点位置,其函数原型如下:

void cv::cornerSubPix(InputArray image,
                   InputOutputArray corners,
                   Size winSize,
                   Size zeroZone,
                   TermCriteria criteria);

  参数解释:

  • image:输入图像的像素矩阵,最好是8位灰度图像,这样检测效率会更高。
  • corners:初始的角点坐标向量,同时作为亚像素坐标位置的输出,因此需要是浮点型数据。
  • winSize:搜索窗口的大小,它表示的是搜索窗口的一半尺寸。
  • zeroZone:死区的一半尺寸,死区是搜索窗口内不对中央位置做求和运算的区域。这是为了避免自相关矩阵出现某些可能的奇异性。
  • criteria:角点搜索的停止条件,通常包括迭代次数、角点位置变化量或角点误差变化量等。

  cornerSubPix函数用于在初步提取的角点信息上进一步提取亚像素信息,从而提高相机标定的精度。在相机标定、目标跟踪和三维重建等应用中,精确的角点位置是非常重要的,因此cornerSubPix函数在这些领域有广泛的应用。


Demo源码

void OpenCVManager::testFindChessboardCorners()
{
#define FindChessboardCornersUseCamera 1
#if !FindChessboardCornersUseCamera
    // 使用图片
    std::string srcFilePath = "D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/chessboard.png";
//    std::string srcFilePath = "D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/24.jpg";
    cv::Mat srcMat = cv::imread(srcFilePath);
#else
    // 使用摄像头
    cv::VideoCapture capture;
    // 插入USB摄像头默认为0
    if(!capture.open(0))
    {
        qDebug() << __FILE__ << __LINE__  << "Failed to open camera: 0";
    }else{
        qDebug() << __FILE__ << __LINE__  << "Succeed to open camera: 0";
    }
    while(true)
    {
        cv::Mat srcMat;
        capture >> srcMat;
#endif
    int chessboardColCornerCount = 6;
    int chessboardRowCornerCount = 9;
    // 步骤一:读取文件
//    cv::imshow("1", srcMat);
//    cv::waitKey(0);
    // 步骤二:缩放,太大了缩放下(可省略)
    cv::resize(srcMat, srcMat, cv::Size(srcMat.cols / 2, srcMat.rows / 2));
    cv::Mat srcMat2 = srcMat.clone();
    cv::Mat srcMat3 = srcMat.clone();
//    cv::imshow("2", srcMat);
//    cv::waitKey(0);
    // 步骤三:灰度化
    cv::Mat grayMat;
    cv::cvtColor(srcMat, grayMat, cv::COLOR_BGR2GRAY);
    cv::imshow("3", grayMat);
//    cv::waitKey(0);
    // 步骤四:检测角点
    std::vector<cv::Point2f> vectorPoint2fCorners;
    bool patternWasFound = false;
    patternWasFound = cv::findChessboardCorners(grayMat,
                                                cv::Size(chessboardColCornerCount, chessboardRowCornerCount),
                                                vectorPoint2fCorners,
                                                cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_FAST_CHECK | cv::CALIB_CB_NORMALIZE_IMAGE);
    /*
    enum { CALIB_CB_ADAPTIVE_THRESH = 1,    // 使用自适应阈值将图像转化成二值图像
           CALIB_CB_NORMALIZE_IMAGE = 2,    // 归一化图像灰度系数(用直方图均衡化或者自适应阈值)
           CALIB_CB_FILTER_QUADS    = 4,    // 在轮廓提取阶段,使用附加条件排除错误的假设
           CALIB_CB_FAST_CHECK      = 8     // 快速检测
         };
    */
    cvui::printf(srcMat, 0, 0, 1.0, 0xFF0000, "found = %s", patternWasFound ? "true" : "false");
    cvui::printf(srcMat, 0, 24, 1.0, 0xFF0000, "count = %d", vectorPoint2fCorners.size());
    qDebug() << __FILE__ << __LINE__ << vectorPoint2fCorners.size();
    // 步骤五:绘制棋盘点
    cv::drawChessboardCorners(srcMat2,
                              cv::Size(chessboardColCornerCount, chessboardRowCornerCount),
                              vectorPoint2fCorners,
                              patternWasFound);
#if FindChessboardCornersUseCamera
    cv::imshow("0", srcMat);
    cv::imshow("4", srcMat2);
    if(!patternWasFound)
    {
        cv::imshow("5", srcMat3);
        cv::waitKey(1);
        continue;
    }
#endif
    // 步骤六:进一步提取亚像素角点
    cv::TermCriteria criteria(CV_TERMCRIT_EPS | CV_TERMCRIT_ITER,   // 类型
                              30,                                   // 参数二: 最大次数
                              0.001);                               // 参数三:迭代终止阈值
    /*
    #define CV_TERMCRIT_ITER    1                   // 终止条件为: 达到最大迭代次数终止
    #define CV_TERMCRIT_NUMBER  CV_TERMCRIT_ITER    //
    #define CV_TERMCRIT_EPS     2                   // 终止条件为: 迭代到阈值终止
    */
    qDebug() << __FILE__ << __LINE__ << vectorPoint2fCorners.size();
    cv::cornerSubPix(grayMat,
                     vectorPoint2fCorners,
                     cv::Size(11, 11),
                     cv::Size(-1, -1),
                     criteria);
    // 步骤七:绘制棋盘点
    cv::drawChessboardCorners(srcMat3,
                              cv::Size(chessboardColCornerCount, chessboardRowCornerCount),
                              vectorPoint2fCorners,
                              patternWasFound);
    cv::imshow("5", srcMat3);
//    cv::waitKey(0);

#if FindChessboardCornersUseCamera
    cv::waitKey(1);
    }
//    cv::imshow(_windowTitle.toStdString(), dstMat);
#else

    cv::waitKey(0);
#endif

}

对应工程模板v1.67.0

  OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点,OpenCV图像处理2,Qt开发,opencv,相机小孔成像,棋盘识别,相机标定棋盘


入坑

入坑一:无法检测出角点

问题

  检测角点失败
  OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点,OpenCV图像处理2,Qt开发,opencv,相机小孔成像,棋盘识别,相机标定棋盘

原因

  输入棋牌横向竖向角点的数量入函数,而不是输入行数和列数。

解决

  输入正确的横向纵向角点数量即可。
  OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点,OpenCV图像处理2,Qt开发,opencv,相机小孔成像,棋盘识别,相机标定棋盘

入坑二:检测亚像素角点崩溃

问题

  检测亚像素角点函数崩溃
  OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点,OpenCV图像处理2,Qt开发,opencv,相机小孔成像,棋盘识别,相机标定棋盘

原因

  输入要是灰度mat

解决

  将灰度图输入即可。


上一篇:《OpenCV开发笔记(七十五):相机标定矫正中使用remap重映射进行畸变矫正》
下一篇:持续补充中…


若该文为原创文章,转载请注明原文出处
本文章博客地址:https://blog.csdn.net/qq21497936/article/details/136535848文章来源地址https://www.toymoban.com/news/detail-837589.html

到了这里,关于OpenCV开发笔记(七十六):相机标定(一):识别棋盘并绘制角点的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 相机棋盘格标定板制作A4打印(无需代码)

    自己在网上搜了半天python脚本代码生成棋盘格标定板,虽然生成了,但是精度上也有些误差,霍霍了一上午,钻牛角尖了属于是。后面得知有一个免费生成标定板的网站,我的反应:?????都快2024年了,不会还有人用代码生成标定板然后去打印出来吧? 网址如下:Came

    2024年01月19日
    浏览(29)
  • c#-OpenCvSharp-棋盘格相机标定与图像矫正(源码demo)

    目录 前言 核心函数: Cv2.FindChessboardCorners:检测角点 Cv2.DrawChessboardCorners:绘制角点 Cv2.CalibrateCamera:相机标定  结果: demo: 前言 相机标定是指确定相机内参和畸变参数的过程,而图像矫正则是对图像进行去畸变操作,以提高图像质量和准确性。 相机标定是的目标是确定相

    2024年04月11日
    浏览(57)
  • 棋盘格测距-单目相机(OpenCV/C++)

    1’ 通过cv::findChessboardCorners寻找棋盘格角点 2‘ 用cv::solvePnP计算旋转向量rvec和平移向量tvec 3’ 通过公式计算相机到棋盘格的距离   已完成单目相机标定的情况下: (可以参考  虽然是我很久之前写的python的,但实现是没啥问题) 需要以下内容: 1、已知相机的内参矩阵 c

    2024年02月08日
    浏览(33)
  • 知识笔记(七十六)———链式语句中limit用法

    limit方法也是模型类的连贯操作方法之一,主要用于指定查询和操作的数量,特别在分页查询的时候使用较多。ThinkPHP的limit方法可以兼容所有的数据库驱动类的。 例如获取满足要求的10个用户,如下调用即可: limit方法也可以用于写操作,例如更新满足要求的3条数据: 用于

    2024年02月03日
    浏览(29)
  • Android OpenCV(七十六): OpenCV 4.9.0 更新,终于有一点Android平台的调整了

    😂 😂 太感动 ing,OpenCV 4.9.0 版本更新,官方 ChangeLog 里终于出现久违的 Android 平台变更。 具体内容如下: Android New Android Archive Package (AAR) distrubuted with Maven Central Finally removed API for OpenCV manager. OpenCVLoader.initLocal() should be used to initialize OpenCV. New Android samples: QR code detector, vide

    2024年01月23日
    浏览(27)
  • 相机标定-机器视觉基础(理论推导、Halcon和OpenCV相机标定)

             相机标定是获得目标工件精准坐标信息的基础。首先,必须进行相机内参标定,构建一个模型消除图像畸变;其次,需要对相机和机器人的映射关系进行手眼标定,构建一个模型将图像坐标系上的点映射到世界坐标系。主要分为背景知识、相机内外参模型推导、

    2023年04月21日
    浏览(37)
  • Android开发—基于OpenCV实现相机实时图像识别跟踪

    xmlns:opencv=“http://schemas.android.com/apk/res-auto” xmlns:tools=“http://schemas.android.com/tools” android:id=“@+id/activity_img_recognition” android:layout_width=“match_parent” android:layout_height=“match_parent” tools:context=“com.sueed.imagerecognition.CameraActivity” org.opencv.android.JavaCameraView android:id=“@+id/jcv”

    2024年04月17日
    浏览(24)
  • 【相机标定】opencv python 标定相机内参时不计算 k3 畸变参数

    畸变参数 k3 通常用于描述径向畸变的更高阶效应,即在需要高精度的应用中可以用到,一般的应用中 k1, k2 足矣。 常见的应用中, orbslam3 中是否传入 k3 是可选的,而 kalibr 标定中则只需要传入 k1, k2 。但计算 k3 时的 k1, k2 不等于不计算 k3 时的 k1, k2 ,因此需要学会两种场景下

    2024年02月09日
    浏览(26)
  • 用OpenCV进行相机标定(张正友标定,有代码)

    理论部分可以参考其他博客或者视觉slam十四讲 相机标定主要是为了获得相机的内参矩阵K和畸变参数 内参矩阵K 畸变系数:径向畸变(k1,k2,k3), 切向畸变(p1,p2) 径向畸变公式 切向畸变公式 张正友标定方法能够提供一个比较好的初始解,用于后序的最优化. 这里用棋盘格进行标定,如

    2024年02月07日
    浏览(30)
  • OpenCV中的相机标定

          之前在https://blog.csdn.net/fengbingchun/article/details/130039337 中介绍了相机的内参和外参,这里通过OpenCV中的接口实现对内参和外参的求解。       估计相机参数的过程称为相机标定(camera calibration)。相机标定是使用已知的真实世界模式(例如棋盘)来估计相机镜头和传感器的外

    2023年04月19日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包