OpenCV实战(19)——特征描述符

news2024/12/25 12:28:23

OpenCV实战(19)——特征描述符

    • 0. 前言
    • 1. 特征描述符
    • 2. 提升匹配集质量
      • 2.1 交叉检查匹配
      • 2.2 比率测试
      • 2.3 距离阈值
    • 3. 完整代码
    • 小结
    • 系列链接

0. 前言

SURF 和 SIFT 关键点检测算法为每个检测到的特征计算位置、方向和比例,比例因子信息可用于定义每个特征点周围的图像窗口的大小。因此,无论特征所属对象的比例如何,定义的邻域都将包含相同的视觉信息。本节将介绍如何使用特征描述符描述兴趣点的邻域,在图像分析中,该邻域中包含的视觉信息可用于表征每个特征点,以区分不同特征点。特征描述符通常是 N 维向量,以对光照变化和透视变形鲁棒的方式描述特征点。通常,可以使用简单的距离度量来比较描述符,例如欧几里得距离。因此,特征描述符是可以用于特征匹配应用程序的强大工具。

1. 特征描述符

cv::Feature2D 抽象类定义了许多成员函数,用于计算关键点列表的描述符。由于大多数基于特征的方法都包括检测器和描述符组件,因此相关的类包括检测函数(检测兴趣点)和计算函数(计算它们的描述符),例如 cv::xfeatures2d::SURFcv::xfeature2d::SIFT 类。接下来,我们创建 SURF 特征检测器。

(1) 创建 SURF 描述符:

cv::Ptr<cv::Feature2D> ptrFeature2D = cv::xfeatures2d::SURF::create(2000.0);

(2) 检测图像中的关键点:

ptrFeature2D->detect(image1, keypoints1);
ptrFeature2D->detect(image2, keypoints2);

(3) 对于检测到的每个关键点,提取它们的描述符:

cv::Mat descriptors1;
cv::Mat descriptors2;
ptrFeature2D->compute(image1, keypoints1, descriptors1);
ptrFeature2D->compute(image2, keypoints2, descriptors2);

(4) 对于 SIFT,只需创建一个 SIFT 对象。输出结果是一个 cv::Mat 矩阵实例,矩阵的行数等于关键点向量中的元素数量,其中每一行都是一个 N 维描述符向量。对于 SURF 描述符,它的默认维度为 64;而对于 SIFT,默认维度为 128。该向量表征特征点邻域的强度模式,两个特征点越相似,它们的描述符向量也就越接近,这些描述符将用于关键点间的匹配。

(5) 将第一张图像中的每个特征描述符向量与第二张图像中的所有特征描述符进行比较,具有最佳相似度分数的特征点对(即两个描述符向量之间距离最小的特征点对)被保留为该特征的最佳匹配,对第一张图像中的所有特征重复此过程,在 OpenCV 中可以使用 cv::BFMatcher 类实现此过程,而无需使用双循环:

// 构建匹配器
cv::BFMatcher matcher(cv::NORM_L2);
// 匹配两个图像描述子
std::vector<cv::DMatch> matches;
matcher.match(descriptors1, descriptors2, matches);

cv::BFMatcher 类是 cv::DescriptorMatcher 类的子类,其定义了不同匹配策略的通用接口,输出结果为 cv::DMatch 实例向量。
使用当前的 SURFHessian 阈值,在第一张图像可以得到 320 个关键点,第二张图像可以得到 266 个关键点。使用蛮力法可以得到 320 个匹配,使用 cv::drawMatches 类绘制结果如下所示:

匹配结果
可以看出,其中一些匹配正确地将左侧的一个点与其右侧的对应点联系起来。但图中也包含一些错误;这是由于图中人物皮肤纹理具有重复结构,使得一些局部匹配受到干扰。
特征描述符必须对光照、视点和图像噪声的微小变化保持鲁棒性,因此,特征描述符通常基于局部强度差异。 SURF 描述符在关键点局部邻域应用以下核:

SURF 核
第一个核测量水平方向上的局部强度差异(指定为 dx),第二个核测量垂直方向上的差异(指定为 dy)。用于提取描述符向量的邻域大小一般定义为特征尺度因子的 20 倍(即 20σ),然后将该方形区域分成 4x4 个较小的方形子区域。对于每个子区域,使用以上核计算结果如下,每个子区域提取四个描述符值:
[ ∑ d x ∑ d y ∑ ∣ d x ∣ ∑ ∣ d y ∣ ] \left[ \begin{array}{ccc} \sum dx & \sum dy & \sum |dx| & \sum |dy| \\\end{array}\right] [dxdydxdy]
由于有 4x4=16 个子区域,共有 64 个描述符值,为了更加重视邻近像素,计算使用以关键点位置为中心的高斯加权 (σ=3.3)。
dxdy 结果也用于估计特征的方向,这些值是在半径为 的圆形邻域内以 σ 为间隔计算的(核大小为 )。对于给定的方向,将某个角度间隔 (π/3) 内的计算结果相加,并将最长矢量方向定义为主导方向。
SIFT 是一种更丰富的描述符,它使用图像梯度而非强度差异,它还将每个关键点周围的正方形邻域拆分为 4x4 子区域(也可以使用 8x82x2 子区域)。在每个区域内,构建了梯度方向的直方图,梯度方向被离散成 8bin,每个梯度方向 bin 与梯度大小成正比。可以用下图进行说明,其中每个星形箭头表示梯度方向的局部直方图:

梯度方向直方图
16 个包含 8bin 的直方图连接在一起,产生一个 128 维的描述符。对于 SURF,梯度值由以关键点位置为中心的高斯滤波器加权,以使描述符对定义邻域周边梯度方向的突然变化不那么敏感,然后对最终描述符进行归一化以使距离测量更加一致。
使用 SURFSIFT 特征和描述符,可以实现尺度不变匹配。下图展示了不同比例的两个图像的 SURF 匹配结果(图中显示了 50 个最佳匹配):

SURF 匹配结果

2. 提升匹配集质量

任何匹配算法产生的匹配结果总是包含大量不正确的匹配。接下来,我们介绍三种提高匹配集质量的策略。

2.1 交叉检查匹配

验证获得的匹配的一种简单方法是再次重复相同的过程,但第二次我们将第二幅图像的每个关键点与第一幅图像的关键点进行比较。只有当我们在两个方向上获得相同的关键点对(两个关键点互为最佳匹配)时,才认为匹配是有效的。cv::BFMatcher 函数提供了使用此策略的选项,当第二个参数设置为 true 时,函数会强制执行交叉检查匹配:

cv::BFMatcher matcher(cv::NORM_L2, true);

改进后的匹配结果如下图所示(以 SURF 特征匹配为例):

带有交叉验证的匹配结果

2.2 比率测试

我们已经了解了场景对象中的重复元素会产生不可靠的结果,因为这会在匹配视觉上相似的结构时存在歧义,在这种情况下,一个关键点将与多个关键点匹配。由于选择错误匹配的可能性较高,因此在这种情况下最好拒绝此类匹配。
要使用此策略,我们需要找到每个关键点的两个最佳匹配点,这可以通过使用 cv::DescriptorMatcher 类的 knnMatch 方法来完成。由于我们只想保留两个最佳匹配,因此指定 k=2

std::vector<std::vector<cv::DMatch> > matches2;
matcher.knnMatch(descriptors1, descriptors2, matches2, 2);

接下来拒绝所有匹配距离与其次佳匹配相似的匹配。由于 knnMatch 输出结果为 std::vector 类实例,可以通过循环每个关键点匹配执行比率测试(如果两个匹配的距离相等,则比率为 1):

double ratioMax = 0.8;
std::vector<std::vector<cv::DMatch> >::iterator it;
for (it=matches2.begin(); it!=matches2.end(); ++it) {
    if ((*it)[0].distance/(*it)[1].distance < ratioMax) {
        matches.push_back((*it)[0]);
    }
}

可以看到匹配由最初的 320 对减少到 29 对,且其中很大一部分均为正确的匹配:

带有比率测试的匹配结果

2.3 距离阈值

距离阈值策略即拒绝描述符之间的距离过高的匹配项,可以使用 cv::DescriptorMatcher 类的 radiusMatch 方法完成:

float maxDist = 0.2;
matches2.clear();
matcher.radiusMatch(descriptors1, descriptors2, matches2, maxDist);

结果同样是一个 std::vector 实例,因为该方法将保留距离小于指定阈值的所有匹配项。这意味着给定的关键点在另一幅图像中可能有多个匹配点,而另一些关键点可能没有任何与之关联的匹配项。使用此策略,可以将匹配数量减少至 25 对:

带有距离阈值的匹配结果

3. 完整代码

完整代码 matcher.cpp 如下所示:

#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/objdetect/objdetect.hpp>
#include <opencv2/xfeatures2d.hpp>

int main() {
    // 图像匹配
    // 1. 读取图像
    cv::Mat image1 = cv::imread("1.png", cv::IMREAD_GRAYSCALE);
    cv::Mat image2 = cv::imread("2.png", cv::IMREAD_GRAYSCALE);
    // 2. 定义关键点向量
    std::vector<cv::KeyPoint> keypoints1;
    std::vector<cv::KeyPoint> keypoints2;
    // 3. 定义特征检测器
    // SURF
    cv::Ptr<cv::Feature2D> ptrFeature2D = cv::xfeatures2d::SURF::create(2000.0);
    // SIFT
    // cv::Ptr<cv::Feature2D> ptrFeature2D = cv::xfeatures2d::SIFT::create(74);
    // 4. 关键点检测
    ptrFeature2D->detect(image1, keypoints1);
    ptrFeature2D->detect(image2, keypoints2);
    // 绘制特征点
    cv::Mat featureImage;
    cv::drawKeypoints(image1, keypoints1, featureImage, cv::Scalar(255, 255, 255), cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
    cv::namedWindow("SURF");
    cv::imshow("SURF",featureImage);
    std::cout << "Number of SURF keypoints (image 1): " << keypoints1.size() << std::endl; 
    std::cout << "Number of SURF keypoints (image 2): " << keypoints2.size() << std::endl;
    // 提取描述子
    cv::Mat descriptors1;
    cv::Mat descriptors2;
    ptrFeature2D->compute(image1, keypoints1, descriptors1);
    ptrFeature2D->compute(image2, keypoints2, descriptors2);
    // 构建匹配器
    cv::BFMatcher matcher(cv::NORM_L2);
    // 交叉验证
    // cv::BFMatcher matcher(cv::NORM_L2, true);
    // 匹配两个图像描述子
    std::vector<cv::DMatch> matches;
    matcher.match(descriptors1, descriptors2, matches);
    // 绘制匹配
    cv::Mat imageMatches;
    cv::drawMatches(image1, keypoints1,       // 第一张图像及其关键点
                    image2, keypoints2,         // 第二张图像及其关键点
                    matches,                    // 匹配
                    imageMatches,               // 生成结果
                    cv::Scalar(255, 255, 255),  // 线颜色
                    cv::Scalar(255, 255, 255),  // 点颜色
                    std::vector<char>(),        // 掩码
                    cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS | cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
    cv::namedWindow("SURF Matches");
    cv::imshow("SURF Matches",imageMatches);
    std::cout << "Number of matches: " << matches.size() << std::endl;
    // 实施比例测试
    std::vector<std::vector<cv::DMatch> > matches2;
    matcher.knnMatch(descriptors1, descriptors2, matches2, 2);
    matches.clear();
    double ratioMax = 0.8;
    std::vector<std::vector<cv::DMatch> >::iterator it;
    for (it=matches2.begin(); it!=matches2.end(); ++it) {
        if ((*it)[0].distance/(*it)[1].distance < ratioMax) {
            matches.push_back((*it)[0]);
        }
    }
    cv::drawMatches(image1, keypoints1,         // 第一张图像及其关键点
                    image2, keypoints2,         // 第二张图像及其关键点
                    matches,                    // 匹配
                    imageMatches,               // 生成结果
                    cv::Scalar(255, 255, 255),  // 线颜色
                    cv::Scalar(255, 255, 255),  // 点颜色
                    std::vector<char>(),        // 掩码
                    cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS | cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
    std::cout << "Number of matches (after ratio test): " << matches.size() << std::endl; 
    cv::namedWindow("SURF Matches (ratio test at 0.6)");
    cv::imshow("SURF Matches (ratio test at 0.6)", imageMatches);
    // 半径匹配
    float maxDist = 0.2;
    matches2.clear();
    matcher.radiusMatch(descriptors1, descriptors2, matches2, maxDist);
    cv::drawMatches(image1, keypoints1,         // 第一张图像及其关键点
                    image2, keypoints2,         // 第二张图像及其关键点
                    matches2,                    // 匹配
                    imageMatches,               // 生成结果
                    cv::Scalar(255, 255, 255),  // 线颜色
                    cv::Scalar(255, 255, 255),  // 点颜色
                    std::vector<std::vector<char> >(), // 掩码
                    cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS | cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
    int nmatches = 0;
    for (int i=0; i<matches2.size(); i++) {
        nmatches += matches2[i].size();
    }
    std::cout << "Number of matches (with max radius): " << nmatches << std::endl;
    cv::namedWindow("SURF Matches (with max radius)");
    cv::imshow("SURF Matches (with max radius)", imageMatches);
    // 尺度不变测试
    image1 = cv::imread("1.png", cv::IMREAD_GRAYSCALE);
    image2 = cv::imread("2.png", cv::IMREAD_GRAYSCALE);
    std::cout << "Number of SIFT keypoints (image 1): " << keypoints1.size() << std::endl; 
    std::cout << "Number of SIFT keypoints (image 2): " << keypoints2.size() << std::endl;
    // 提取关键点和描述符
    ptrFeature2D = cv::xfeatures2d::SIFT::create();
    ptrFeature2D->detectAndCompute(image1, cv::noArray(), keypoints1, descriptors1);
    ptrFeature2D->detectAndCompute(image2, cv::noArray(), keypoints2, descriptors2);
    // 匹配两张图像描述子
    matcher.match(descriptors1, descriptors2, matches);
    // 提取 50 个最佳匹配
    std::nth_element(matches.begin(),matches.begin()+50,matches.end());
    matches.erase(matches.begin()+50,matches.end());
    cv::drawMatches(image1, keypoints1,       // 第一张图像及其关键点
                    image2, keypoints2,         // 第二张图像及其关键点
                    matches,                    // 匹配
                    imageMatches,               // 生成结果
                    cv::Scalar(255, 255, 255),  // 线颜色
                    cv::Scalar(255, 255, 255),  // 点颜色
                    std::vector<char>(),        // 掩码
                    cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS | cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
    cv::namedWindow("Multi-scale SIFT Matches");
    cv::imshow("Multi-scale SIFT Matches",imageMatches);
    std::cout << "Number of matches: " << matches.size() << std::endl; 
    cv::waitKey();
    return 0;
}

小结

特征描述符是用于特征匹配应用程序的强大工具,本节介绍了如何使用 SURFSIFT 特征描述符匹配图像特征点,并介绍了三种不同方法改进匹配结果:交叉检查匹配、比率测试和距离阈值。

系列链接

OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换
OpenCV实战(8)——直方图详解
OpenCV实战(9)——基于反向投影直方图检测图像内容
OpenCV实战(10)——积分图像详解
OpenCV实战(11)——形态学变换详解
OpenCV实战(12)——图像滤波详解
OpenCV实战(13)——高通滤波器及其应用
OpenCV实战(14)——图像线条提取
OpenCV实战(15)——轮廓检测详解
OpenCV实战(16)——角点检测详解
OpenCV实战(17)——FAST特征点检测
OpenCV实战(18)——特征匹配

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

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

相关文章

RestClient查询文档

文章目录1、RestClient查询文档----快速入门2、查询文档--match、term、range、bool3、查询文档-排序和分页4、高亮1、RestClient查询文档----快速入门 基本步骤 1.先创建SearchRequest对象&#xff0c;调用source方法&#xff08;DSL&#xff09;——>相当于红框内的大jso…

最前端|什么是低代码?与传统开发的区别是什么?

目录一、低代码介绍二、背景趋势三、低代码与传统代码开发&#xff08;一&#xff09;低代码能否替代传统开发低代码页面传统开发页面&#xff08;二&#xff09;相同业务不同方式对比1.低代码开发&#xff08;1&#xff09;优点&#xff08;2&#xff09;缺点2.传统代码开发&a…

实现一个登录功能方案设计2

需求MySQL表实现方案index页面home页面需求 实现一个登录功能 实现的功能 注册(邮箱注册)登录(邮箱密码)重置密码查看操作记录(登录, 注册, 重置密码, 登出. 都算操作)登出在第一版的基础上进行优化:\ 优化点: 存操作信息请求的post使用中间件进行储存操作信息.避免重复代码 因…

【面试】如何设计SaaS产品的数据权限?

文章目录前言数据权限是什么&#xff1f;设计原则整体方案RBAC模型怎么控制数据权限&#xff1f;1. 数据范围权限控制2. 业务对象操作权限控制3. 业务对象字段权限控制总结前言 一套系统的权限可以分为两类&#xff0c;数据权限和功能权限&#xff0c;今天我们从以下几个点&am…

阿里云服务器安装宝塔面板搭建网站全流程(一步步详解)

阿里云服务器安装宝塔面板教程&#xff0c;云服务器吧以阿里云Linux系统云服务器安装宝塔Linux面板为例&#xff0c;先配置云服务器安全组开放宝塔所需端口8888、888、80、443、20和21端口&#xff0c;然后执行安装宝塔面板命令脚本&#xff0c;最后登录宝塔后台安装LNMP&#…

UML 简易使用教程

最近刚好有空&#xff0c;遂决定对应 UML 常用的一些图进行整理&#xff0c;供自己以及需要的人查阅。 UML 分为静态模型与动态模型。静态模型描述一个系统的静态特征&#xff0c;固定的框架结构。包括用例图、类图、对象图、组件图、部署图&#xff1b;动态模型包括时序图、协…

TensorFlow 智能移动项目:11~12

原文&#xff1a;Intelligent mobile projects with TensorFlow 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 本文来自【ApacheCN 深度学习 译文集】&#xff0c;采用译后编辑&#xff08;MTPE&#xff09;流程来尽可能提升效率。 不要担心自己的形象&#xff0c;只…

Flutter 第一个界面

第一个页面 app首页 入口函数 一个Flutter工程的入口函数与Dart命令行工程一样是main&#xff0c;不同的是在Flutter中执行runApp(ArticleApp()) 就能够在手机屏幕上展示这个Widget。 import package:flutter/material.dart; void main() > runApp(new ArticleApp()); Ar…

OpenAI不能访问有什么方法解救呢?试试这方法吧

最近发现国内不挂代理是不能访问到openAI的接口的&#xff0c;为了解决这个问题&#xff0c;我一直在github上需在解决方案&#xff0c;今天终于被我找到一个大神开源了一个解决方案。下面就来看看如何做吧。 整个项目的代码很简单只有几行代码&#xff1a; {"rewrites&q…

几种在Python中List添加、删除元素的方法

嗨害大家好鸭&#xff01;我是爱摸鱼的芝士❤ 一、python中List添加元素的几种方法 List 是 Python 中常用的数据类型&#xff0c; 它一个有序集合&#xff0c; 即其中的元素始终保持着初始时的定义的顺序 &#xff08;除非你对它们进行排序或其他修改操作&#xff09;。 …

进程互斥的实现方式

1.进程互斥的软件实现方法 1.单标志法 算法思想&#xff1a;两个进程在访问完临界区后会把使用临界区的权限转让给另一个进程&#xff0c;也就是说每个进程进入临界区的权限只能被另一个进程赋予 局限性 2.双标志先检查法 算法思想&#xff1a;设置一个布尔数组flag[]&#xff…

python 笔记:PyTrack(将GPS数据和OpenStreetMap数据进行整合)【官网例子解读】

论文笔记&#xff1a;PyTrack: A Map-Matching-Based Python Toolbox for Vehicle Trajectory Reconstruction_UQI-LIUWJ的博客-CSDN博客4 0 包的安装 官网的两种方式我都试过&#xff0c;装是能装成功&#xff0c;但是python import PyTrack包的时候还是显示找不到Pytrack …

Altova MapForce 2023 Crack

Altova MapForce 2023 Crack 数据映射项目中的注释-除了支持对数据映射项目的单个连接进行注释外&#xff0c;现在还可以向源组件和目标组件添加注释&#xff0c;以帮助记录映射的作用和实现方式。 支持XML输出中的standalone“yes”声明-在独立文档声明中&#xff0c;值“yes”…

Chat-GLM 详细部署(GPU显存>=12GB)

建议配置: ( Windows OS 11 部署 )CPU-i7 13700F ~ 13700KF RAM: 16GB DDR4 GPU: RTX3080(12G) 安装 conda: 1. 下载安装 miniconda3 &#xff1a; https://docs.conda.io/en/latest/miniconda.html conda是一个包和环境管理工具&#xff0c;它不仅能管理包&#xff0c;还能隔…

Linux嵌入式学习之Ubuntu入门(四)Makefile

系列文章目录 一、Linux嵌入式学习之Ubuntu入门&#xff08;一&#xff09;基本命令、软件安装及文件结构 二、Linux嵌入式学习之Ubuntu入门&#xff08;二&#xff09;磁盘文件介绍及分区、格式化等 三、Linux嵌入式学习之Ubuntu入门&#xff08;三&#xff09;用户、用户组…

go语言切片做函数参数传递+append()函数扩容

go语言切片函数参数传递append()函数扩容 给你二叉树的根节点 root 和一个整数目标和 targetSum &#xff0c;找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 二叉树递归go代码&#xff1a; var ans [][]int func pathSum(root *TreeNode, targetSum int) ( [][…

Longitudinal Change Detection on Chest X-rays Using Geometric Correlation Maps

文章来源&#xff1a;[MICCAI2019] Keywords&#xff1a;Chest X-ray&#xff1b;Longitudinal analysis&#xff1b;Change detection&#xff1b;Geometric correlation 一、本文提出的问题以及解决方案 在胸部X-ray图像的诊断中&#xff0c;医生会考虑与先前检查相比病变的…

8.网络爬虫—正则表达式RE实战

8.网络爬虫—正则表达式RE实战正则表达式&#xff08;Regular Expression&#xff09;re.Ire.Are.Sre.Mre.Xre.Lre.U美某杰实战写入csv文件&#xff1a;前言&#xff1a; &#x1f3d8;️&#x1f3d8;️个人简介&#xff1a;以山河作礼。 &#x1f396;️&#x1f396;️:Pyth…

MongoDB 聚合管道的文档操作($sort,$skip,$limit,$sample,$unwind)

目前为止&#xff0c;我们已经介绍了一部分聚合管道中的管道参数&#xff1a; $match&#xff1a;文档过滤 $group&#xff1a;文档分组&#xff0c;并介绍了分组中的常用操作&#xff1a;$addToSet&#xff0c;$avg&#xff0c;$sum&#xff0c;$min&#xff0c;$max等。 $add…

COCO数据集相关知识介绍

&#x1f468;‍&#x1f4bb;个人简介&#xff1a; 深度学习图像领域工作者 &#x1f389;总结链接&#xff1a; 链接中主要是个人工作的总结&#xff0c;每个链接都是一些常用demo&#xff0c;代码直接复制运行即可。包括&#xff1a; &am…