【opencv】示例-grabcut.cpp 使用OpenCV库的GrabCut算法进行图像分割

news2024/12/23 17:49:46

41f08706f303066b2ff6f1dc27fbfc16.png

left mouse button - set rectangle

33bae4d5ad6bf1e6d3d10211a597a22c.png

bd1a7f237d6a0e78aeb8b84c86f86330.png

SHIFT+left mouse button - set GC_FGD pixels

CTRL+left mouse button - set GC_BGD pixels

这段代码是一个使用OpenCV库的GrabCut算法进行图像分割的C++程序。它允许用户通过交互式方式选择图像中的一个区域,并利用GrabCut算法尝试将其分割出来。代码中包含用户操作指南、颜色定义、事件处理以及GrabCut算法的迭代过程,实现了用户通过鼠标交互和按键控制来迭代分割效果。程序逻辑结构清晰,采用面向对象的方式封装了GrabCut分割相关的操作。

// 引入OpenCV库中处理图像编解码的相关头文件
#include "opencv2/imgcodecs.hpp"
// 引入OpenCV库中高级GUI的相关头文件
#include "opencv2/highgui.hpp"
// 引入OpenCV库中图像处理的相关头文件
#include "opencv2/imgproc.hpp"


// 引入输入输出流的库文件
#include <iostream>


// 使用标准命名空间std,免去std::前缀
using namespace std;
// 使用OpenCV命名空间cv,免去cv::前缀
using namespace cv;


// 定义静态辅助函数,用于显示程序帮助信息
static void help(char** argv)
{
    cout << "\nThis program demonstrates GrabCut segmentation -- select an object in a region\n"
            "and then grabcut will attempt to segment it out.\n"
            "Call:\n"
        << argv[0] << " <image_name>\n" // 显示如何调用程序
        "\nSelect a rectangular area around the object you want to segment\n"
        // 以下是热键操作说明
        "\nHot keys: \n"
        "\tESC - quit the program\n" // 按ESC键退出程序
        "\tr - restore the original image\n" // 按'r'键还原到原始图像
        "\tn - next iteration\n" // 按'n'键执行下一迭代步骤
        "\n"
        // 以下是鼠标操作说明
        "\tleft mouse button - set rectangle\n" // 左键点击设置矩形区域
        "\n"
        "\tCTRL+left mouse button - set GC_BGD pixels\n" // 按住CTRL键同时左键点击设置背景像素
        "\tSHIFT+left mouse button - set GC_FGD pixels\n" // 按住SHIFT键同时左键点击设置前景像素
        "\n"
        "\tCTRL+right mouse button - set GC_PR_BGD pixels\n" // 按住CTRL键同时右键点击设置可能的背景像素
        "\tSHIFT+right mouse button - set GC_PR_FGD pixels\n" << endl; // 按住SHIFT键同时右键点击设置可能的前景像素
}


// 定义一些标量常量表示不同的颜色
const Scalar RED = Scalar(0,0,255);
const Scalar PINK = Scalar(230,130,255);
const Scalar BLUE = Scalar(255,0,0);
const Scalar LIGHTBLUE = Scalar(255,255,160);
const Scalar GREEN = Scalar(0,255,0);


// 定义背景键和前景键的标志,用于鼠标操作事件中
const int BGD_KEY = EVENT_FLAG_CTRLKEY;
const int FGD_KEY = EVENT_FLAG_SHIFTKEY;


// 声明静态函数,用于从复合掩模图中提取二值掩模图
static void getBinMask( const Mat& comMask, Mat& binMask )
{
    // 确保comMask非空且类型为CV_8UC1,否则抛出错误
    if( comMask.empty() || comMask.type()!=CV_8UC1 )
        CV_Error( Error::StsBadArg, "comMask is empty or has incorrect type (not CV_8UC1)" );
    // 如果binMask为空或大小与comMask不一致,则重新创建与comMask大小一致的binMask
    if( binMask.empty() || binMask.rows!=comMask.rows || binMask.cols!=comMask.cols )
        binMask.create( comMask.size(), CV_8UC1 );
    // 将comMask中的最低位复制到binMask中
    binMask = comMask & 1;
}


// 声明GrabCut应用类,用于实现GrabCut算法的交互操作
// 定义用于GrabCut算法的应用类
class GCApplication
{
public:
    // 定义选择状态的枚举类型
    enum{ NOT_SET = 0, IN_PROCESS = 1, SET = 2 };
    // 定义圆的半径
    static const int radius = 2;
    // 定义圆的线条厚度,-1表示填充圆
    static const int thickness = -1;


    // 成员函数声明
    void reset();  // 重置函数
    void setImageAndWinName( const Mat& _image, const string& _winName );  // 设置图像和窗口名
    void showImage() const;  // 展示图像
    void mouseClick( int event, int x, int y, int flags, void* param );  // 鼠标点击事件处理函数
    int nextIter();  // 迭代处理函数
    int getIterCount() const { return iterCount; }  // 获取迭代次数的函数


private:
    // 私有成员函数和变量声明
    void setRectInMask();  // 设置矩形到掩模中
    void setLblsInMask( int flags, Point p, bool isPr );  // 设置标签到掩模中的函数


    const string* winName;  // 窗口名称
    const Mat* image;  // 图像引用
    Mat mask;  // 掩模矩阵
    Mat bgdModel, fgdModel;  // 背景和前景模型


    uchar rectState, lblsState, prLblsState;  // 矩形和标签状态变量
    bool isInitialized;  // 是否已经初始化的标志


    Rect rect;  // 矩形区域
    vector<Point> fgdPxls, bgdPxls, prFgdPxls, prBgdPxls;  // 记录前景和背景像素位置的向量
    int iterCount;  // 迭代计数
};


// 成员函数定义
// 重置GCApplication
void GCApplication::reset()
{
    if( !mask.empty() )
        mask.setTo(Scalar::all(GC_BGD));  // 如果掩模非空,设置掩模的所有值为GC_BGD
    // 清除前景、背景像素位置的记录
    bgdPxls.clear(); fgdPxls.clear();
    prBgdPxls.clear();  prFgdPxls.clear();


    // 重置相关的状态标志和迭代计数
    isInitialized = false;
    rectState = NOT_SET;
    lblsState = NOT_SET;
    prLblsState = NOT_SET;
    iterCount = 0;
}


// 设置图像和窗口名
void GCApplication::setImageAndWinName( const Mat& _image, const string& _winName  )
{
    if( _image.empty() || _winName.empty() )
        return;  // 如果图像或窗口名为空,直接返回
    image = &_image;  // 设置图像引用
    winName = &_winName;  // 设置窗口名
    mask.create( image->size(), CV_8UC1);  // 创建与图像相同大小的掩模矩阵
    reset();  // 重置GCApplication
}


// 展示图像
void GCApplication::showImage() const
{
    if( image->empty() || winName->empty() )
        return;  // 如果图像为空或窗口名为空,直接返回


    Mat res;
    Mat binMask;
    image->copyTo( res );  // 复制图像到res
    if( isInitialized ){
        getBinMask( mask, binMask);  // 如果已经初始化,获取二值掩模


        Mat black (binMask.rows, binMask.cols, CV_8UC3, cv::Scalar(0,0,0));
        black.setTo(Scalar::all(255), binMask);  // 设置黑色图像的掩模区域为白色


        addWeighted(black, 0.5, res, 0.5, 0.0, res);  // 将黑色图像与原图混合
    }


    vector<Point>::const_iterator it;
    // 绘制背景和前景像素位置
    for( it = bgdPxls.begin(); it != bgdPxls.end(); ++it )
        circle( res, *it, radius, BLUE, thickness );
    for( it = fgdPxls.begin(); it != fgdPxls.end(); ++it )
        circle( res, *it, radius, RED, thickness );
    for( it = prBgdPxls.begin(); it != prBgdPxls.end(); ++it )
        circle( res, *it, radius, LIGHTBLUE, thickness );
    for( it = prFgdPxls.begin(); it != prFgdPxls.end(); ++it )
        circle( res, *it, radius, PINK, thickness );


    // 绘制矩形框
    if( rectState == IN_PROCESS || rectState == SET )
        rectangle( res, Point( rect.x, rect.y ), Point(rect.x + rect.width, rect.y + rect.height ), GREEN, 2);


    imshow( *winName, res );  // 显示图像
}   


// GCApplication类的成员函数:设置矩形框到掩模中
void GCApplication::setRectInMask()
{
    CV_Assert( !mask.empty() ); // 确认掩模不为空
    mask.setTo( GC_BGD ); // 将掩模的全部像素值设置为背景
    // 修正矩形坐标,确保矩形框不会超出图像范围
    rect.x = max(0, rect.x);
    rect.y = max(0, rect.y);
    rect.width = min(rect.width, image->cols-rect.x);
    rect.height = min(rect.height, image->rows-rect.y);
    // 将矩形框内的像素值设置为可能的前景
    (mask(rect)).setTo( Scalar(GC_PR_FGD) );
}


// GCApplication类的成员函数:根据用户输入,在掩模上设置前景或背景标签
void GCApplication::setLblsInMask( int flags, Point p, bool isPr )
{
    vector<Point> *bpxls, *fpxls; // 指向背景和前景像素点的向量
    uchar bvalue, fvalue; // 背景和前景的像素值
    // 根据是否是概率图(isPr)设置不同的指针和像素值
    if( !isPr )
    {
        bpxls = &bgdPxls;
        fpxls = &fgdPxls;
        bvalue = GC_BGD;
        fvalue = GC_FGD;
    }
    else
    {
        bpxls = &prBgdPxls;
        fpxls = &prFgdPxls;
        bvalue = GC_PR_BGD;
        fvalue = GC_PR_FGD;
    }
    // flags中对应的按键标志位被设置,更新背景像素点向量,并在掩模上画圆
    if( flags & BGD_KEY )
    {
        bpxls->push_back(p);
        circle( mask, p, radius, bvalue, thickness );
    }
    // flags中对应的按键标志位被设置,更新前景像素点向量,并在掩模上画圆
    if( flags & FGD_KEY )
    {
        fpxls->push_back(p);
        circle( mask, p, radius, fvalue, thickness );
    }
}


// GCApplication类的成员函数:处理鼠标点击事件
void GCApplication::mouseClick( int event, int x, int y, int flags, void* )
{
    switch( event )
    {
    // 左键按下事件:设置矩形或者GC_BGD/GC_FGD像素标签
    case EVENT_LBUTTONDOWN: 
        {
            bool isb = (flags & BGD_KEY) != 0,
                 isf = (flags & FGD_KEY) != 0;
            if( rectState == NOT_SET && !isb && !isf )
            {
                rectState = IN_PROCESS; // 开始绘制矩形框
                rect = Rect( x, y, 1, 1 ); // 初始化矩形框大小
            }
            if ( (isb || isf) && rectState == SET )
                lblsState = IN_PROCESS; // 开始设置标签状态
        }
        break;
    // 右键按下事件:设置GC_PR_BGD/GC_PR_FGD像素标签
    case EVENT_RBUTTONDOWN: 
        {
            bool isb = (flags & BGD_KEY) != 0,
                 isf = (flags & FGD_KEY) != 0;
            if ( (isb || isf) && rectState == SET )
                prLblsState = IN_PROCESS; // 开始设置概率标签状态
        }
        break;
    // 左键释放事件:完成矩形绘制或者设置像素标签
    case EVENT_LBUTTONUP:
        if( rectState == IN_PROCESS )
        {
            // 如果起点和终点一样,则不设置矩形框
            if(rect.x == x || rect.y == y){
                rectState = NOT_SET;
            }
            else{
                rect = Rect( Point(rect.x, rect.y), Point(x,y) );
                rectState = SET; // 设置矩形框状态为已设置
                setRectInMask(); // 在掩模上绘制矩形框
                CV_Assert( bgdPxls.empty() && fgdPxls.empty() && prBgdPxls.empty() && prFgdPxls.empty() );
            }
            showImage(); // 显示图像
        }
        if( lblsState == IN_PROCESS )
        {
            setLblsInMask(flags, Point(x,y), false); // 设置像素标签
            lblsState = SET; // 设置标签状态为已设置
            nextIter(); // 进行一次迭代
            showImage(); // 显示图像
        }
        else{
            // 如果矩形框状态已设置,进行一次迭代后显示图像
            if(rectState == SET){
                nextIter();
                showImage();
            }
        }
        break;
    // 右键释放事件:完成概率标签的设置
    case EVENT_RBUTTONUP:
        if( prLblsState == IN_PROCESS )
        {
            setLblsInMask(flags, Point(x,y), true); // 设置概率标签
            prLblsState = SET; // 设置概率标签状态为已设置
        }
        // 如果矩形框状态已设置,进行一次迭代后显示图像
        if(rectState == SET){
            nextIter();
            showImage();
        }
        break;
    // 鼠标移动事件:更新矩形框大小或者继续设置像素标签
    case EVENT_MOUSEMOVE:
        if( rectState == IN_PROCESS )
        {
            rect = Rect( Point(rect.x, rect.y), Point(x,y) );
            CV_Assert( bgdPxls.empty() && fgdPxls.empty() && prBgdPxls.empty() && prFgdPxls.empty() );
            showImage(); // 显示图像
        }
        else if( lblsState == IN_PROCESS )
        {
            setLblsInMask(flags, Point(x,y), false); // 设置像素标签
            showImage(); // 显示图像
        }
        else if( prLblsState == IN_PROCESS )
        {
            setLblsInMask(flags, Point(x,y), true); // 设置概率标签
            showImage(); // 显示图像
        }
        break;
    }
}


// 主函数入口
int main( int argc, char** argv )
{
    // 解析命令行参数
    cv::CommandLineParser parser(argc, argv, "{@input| messi5.jpg |}");
    // 调用help函数显示帮助信息
    help(argv);


    // 获取图片文件名
    string filename = parser.get<string>("@input");
    // 如果文件名为空,输出错误信息并返回1
    if( filename.empty() )
    {
        cout << "\nDurn, empty filename" << endl;
        return 1;
    }
    // 读取图片
    Mat image = imread(samples::findFile(filename), IMREAD_COLOR);
    // 如果读取失败,输出错误信息并返回1
    if( image.empty() )
    {
        cout << "\n Durn, couldn't read image filename " << filename << endl;
        return 1;
    }


    // 创建窗口
    const string winName = "image";
    namedWindow( winName, WINDOW_AUTOSIZE );
    // 设置鼠标回调函数
    setMouseCallback( winName, on_mouse, 0 );


    // 设置gcapp对象的图片和窗口名称
    gcapp.setImageAndWinName( image, winName );
    // 显示图片
    gcapp.showImage();


    // 无限循环,等待用户按键
    for (;;)
    {
        // 等待按键事件
        char c = (char)waitKey(0);
        switch (c)
        {
        case '\x1b': // 按Esc键退出
            cout << "Exiting ..." << endl;
            goto exit_main;
        case 'r': // 按'r'键重置
            cout << endl;
            gcapp.reset(); // 调用重置函数
            gcapp.showImage(); // 显示图像
            break;
        case 'n': // 按'n'键进行下一次迭代
            int iterCount = gcapp.getIterCount(); // 获取当前迭代次数
            cout << "<" << iterCount << "... ";
            int newIterCount = gcapp.nextIter(); // 调用下一个迭代
            if (newIterCount > iterCount)
            {
                gcapp.showImage(); // 显示新的图像
                cout << iterCount << ">" << endl;
            }
            else
                cout << "rect must be determined>" << endl; // 提示需要先确定矩形框
            break;
        }
    }


exit_main:
    // 销毁窗口并退出
    destroyWindow( winName );
    return 0;
}

4396662620bffcce3c85a06b8f6b8519.png

vector<Point>::const_iterator it;
    // 绘制背景和前景像素位置
    for (it = bgdPxls.begin(); it != bgdPxls.end(); ++it)
        circle(res, *it, radius, BLUE, thickness);
    for (it = fgdPxls.begin(); it != fgdPxls.end(); ++it)
        circle(res, *it, radius, RED, thickness);
    for (it = prBgdPxls.begin(); it != prBgdPxls.end(); ++it)
        circle(res, *it, radius, LIGHTBLUE, thickness);
    for (it = prFgdPxls.begin(); it != prFgdPxls.end(); ++it)
        circle(res, *it, radius, PINK, thickness);

f7e7b43a3df3c230bdc740bf725eeb2f.png

de922bc6f830506138ace52857ca324d.png

grabCut(*image, mask, rect, bgdModel, fgdModel, 1);

70dc097b43f56b2b7747b1df365b07ad.png

// 如果标签已经设置或概率标签已经设置,用mask初始化grabCut
  if (lblsState == SET || prLblsState == SET)
      grabCut(*image, mask, rect, bgdModel, fgdModel, 1, GC_INIT_WITH_MASK);
  else
      // 否则,用rect初始化grabCut
      grabCut(*image, mask, rect, bgdModel, fgdModel, 1, GC_INIT_WITH_RECT);

a69d4dac7b64bbaebfd262eea01fcebe.png

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

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

相关文章

【大语言模型】基础:如何处理文章,向量化与BoW

词袋模型&#xff08;BoW&#xff09;是自然语言处理&#xff08;NLP&#xff09;和机器学习中一种简单而广泛使用的文本表示方法。它将文本文档转换为数值特征向量&#xff0c;使得可以对文本数据执行数学和统计操作。词袋模型将文本视为无序的单词集合&#xff08;或“袋”&a…

洛谷题单 -- 图论的简单入门

B3643 图的存储 链接 : 图的存储 - 洛谷 思路 : 这一题要考察图的存储方式 , 一般可以使用邻接矩阵 或 邻接表来存储 图的结点 和1 边的信息 &#xff0c;详情请看代码 : 代码 #include<bits/stdc.h> using namespace std;const int N 1010 ; int n , m ; int …

建造者模式:构造复杂对象的艺术

在面向对象的设计中&#xff0c;建造者模式是一种重要的创建型设计模式&#xff0c;专门用来构建复杂的对象。它主要目的是将对象的构造代码与其表示代码分离&#xff0c;使同样的构建过程可以创建不同的表示。本文将详细介绍建造者模式的定义、实现、应用场景以及优缺点&#…

VBA中如何对工作表进行排序

代码 在VBA中对工作表进行排序的最简单方法是直接使用Move方法来移动工作表。 Sub SortSheetsByNameDescending()Dim sheetsDim sheet As WorksheetDim i As Integer, j As IntegerDim sortedSheets() As Array 获取当前工作簿中的所有工作表Set sheets ThisWorkbook.Sheets…

【深入理解Java IO流0x09】解读Java NIO核心知识(下篇)

1. NIO简介 在开始前&#xff0c;让我们再简单回顾一下NIO。 在传统的 Java I/O 模型&#xff08;BIO&#xff09;中&#xff0c;I/O 操作是以阻塞的方式进行的。也就是说&#xff0c;当一个线程执行一个 I/O 操作时&#xff0c;它会被阻塞直到操作完成。这种阻塞模型在处理多…

【研发效能·创享大会-嗨享技术轰趴】-IDCF五周年专场

一、这是一场创新分享局&#xff01; 来吧&#xff0c;朋友们! 参加一场包含AIGC、BizDevOps、ToB产品管理、B端产品运营、平台工程、研发效能、研发度量、职业画布、DevOps国标解读的研发效能创享大会&#xff0c;会有哪些收益呢&#xff1f; 知识更新与技能提升&#xff1a;…

给现有rabbitmq集群添加rabbitmq节点

现有的&#xff1a;10.2.59.216 rabbit-node1 10.2.59.217 rabbit-node2 新增 10.2.59.199 rabbit-node3 1、分别到官网下载erlang、rabbitmq安装包&#xff0c;我得版本跟现有集群保持一致。 erlang安装包&#xff1a;otp_src_22.0.tar.gz rabbitmq安装包&#xff1…

Linux系统启动过程详解

启动过程是指计算机从开机自检到操作系统完全加载的一系列动作。深入理解启动过程对于有效解决启动问题、提升系统性能以及高效管理系统的启动组件至关重要。例如&#xff0c;可以帮助我们识别和处理在启动过程中可能出现的诸如硬件故障、配置错误等问题。例如帮助我们个性化定…

C语言之九九乘法表||素数||最小公倍数

一、九九乘法表 &#xff08;1&#xff09;思路 1、九九乘法表中存在三个变量&#xff0c;以 x1 ; x2 ; y 为例&#xff08;这里也可以使用两个变量&#xff0c;用x1和x2来表示y&#xff0c;方法一样&#xff09; 2、想好了变量之后&#xff0c;我们要想怎样将他实现呢&#x…

Robotstudio2024中从备份文件恢复和创建工作站的具体方法演示

Robotstudio2024中从备份文件恢复和创建工作站的具体方法演示 如下图所示,打开Robotstudio2024软件,有需要的可以从以下链接获取: ABB机器人编程仿真软件RobotStudio 2024.1-链接baiduyun 点击“新建”—工作站—创建, 如下图所示,点击“ABB模型库”,选择自己使用的机器…

二刷大数据(一)- Hadoop

目录 大数据4V Hadoop概念Hadoop大版本区别HDFS产生背景架构文件块大小写文件流程读数据流程NameNode & SecondNameNodeDataNode工作机制 YARNMapReduce为什么不适合实时核心思想切片与MapTask原理MapTask机制MapReduceApplicationMasterApplicationMaster shuffle机制Redu…

jenkins+sonar配置

安装插件 Sonar Scanner 用于扫描项目 配置sonar scanner jenkins集成sonar 1、sonar生成token 生成完保存好&#xff0c;刷新后无法查看 2、jenkins配置全局凭据 3、jenkins配置系统设置

扭蛋机小程序:线上扭蛋机模式发展空间有多大?

潮玩行业近几年的发展非常快&#xff0c;推动了扭蛋机市场的发展&#xff0c;越来越多的人加入到了扭蛋机赛道中&#xff0c;市场迎来了新的发展期。如今&#xff0c;我国的二次元文化的发展不断成熟&#xff0c;扭蛋机主打的二次元商品迎来了更多的商业机会。 一、互联网扭蛋机…

改变LoRA的初始化方式,北大新方法PiSSA显著提升微调效果

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了免费的人工智能中文站https://ai.weoknow.com 新建了收费的人工智能中文站https://ai.hzytsoft.cn/ 更多资源欢迎关注 随着大模型的参数量日益增长&#xff0c;微调整个模型的开销逐渐变得难以接受。 为此&#x…

RestTemplate—微服务远程调用—案例解析

简介&#xff1a;总结来说&#xff0c;微服务之间的调用方式有多种&#xff0c;选择哪种方式取决于具体的业务需求、技术栈和架构设计。RESTful API和HTTP客户端是常见的选择&#xff0c;而Feign和Ribbon等辅助库可以简化调用过程。RPC和消息队列适用于特定的场景&#xff0c;如…

FPGA - 以太网UDP通信(三)

一&#xff0c;引言 前文链接&#xff1a;FPGA - 以太网UDP通信&#xff08;一&#xff09; FPGA - 以太网UDP通信&#xff08;二&#xff09; 在以上文章中介绍了以太网简介&#xff0c;以太网UDP通信硬件结构&#xff0c;以及PHY芯片RGMII接口-GMII接口转换逻辑&#xff0c…

Node.js从基础到高级运用】二十三、Node.js中自动重启服务器

引言 在Node.js开发过程中&#xff0c;我们经常需要修改代码后重启服务器来应用这些更改。手动重启不仅效率低下&#xff0c;而且会打断开发流程。幸运的是&#xff0c;有一些工具可以帮助我们自动化这个过程。本文将介绍如何使用nodemon来实现Node.js服务器的自动重启。 什么是…

清楚明了的凸松弛最优潮流!基于混合整数二阶锥规划的主动配电网最优潮流研究程序代码!

前言 最优潮流(optimal power flow&#xff0c;OPF)问题&#xff0c;是电力系统中最常见、最基础的一类优化问题。在满足基尔霍夫定律、线路容量约束以及运行安全约束等电力网络物理约束的前提下&#xff0c;OPF问题旨在寻找一个最优的潮流稳态工作点&#xff0c;使得在该工作…

【LAMMPS学习】八、基础知识(2.5)恒压器

8. 基础知识 此部分描述了如何使用 LAMMPS 为用户和开发人员执行各种任务。术语表页面还列出了 MD 术语&#xff0c;以及相应 LAMMPS 手册页的链接。 LAMMPS 源代码分发的 examples 目录中包含的示例输入脚本以及示例脚本页面上突出显示的示例输入脚本还展示了如何设置和运行各…

WebSocket一篇讲清楚

文章目录 WebSocket简介WebSocket与HTTP的区别WebSocket的工作原理WebSocket的应用场景WebSocket的使用WebSocket 属性WebSocket 事件WebSocket 方法 WebSocket的心跳机制WebSocket 的安全性和跨域问题如何处理&#xff1f;有哪些好用的客户端WebSocket第三方库总结 WebSocket简…