【opencv】示例-detect_mser.cpp 使用 MSER 算法来检测图像中的极值区域

7cae2c56dcfebfb0311809f4df040d7a.png

f6e56d9a7ed4a0c755bd860fed3011e5.png

180e4add7fc8fe1a796ff866c58315c0.png

4b506fa8be086aa76831f0ab1373fa7c.png

0cf9ec361c492ae8fa305057a633811b.png

#include <opencv2/core.hpp> // 包含OpenCV核心操作的头文件
#include <opencv2/imgproc.hpp> // 包含图像处理功能的头文件
#include <opencv2/highgui.hpp> // 包含图形用户界面的头文件
#include <opencv2/features2d.hpp> // 包含特征检测相关功能的头文件
#include "opencv2/core/opengl.hpp" // 包含OpenCV的OpenGL支持的头文件


#include <vector> // 包含标准模板库的vector(向量)容器类的头文件
#include <map> // 包含标准模板库的map(映射)容器类的头文件
#include <iostream> // 包含标准输入输出流的头文件
#include <iomanip> // 包含输入输出流的格式化操作的头文件
#include <limits> // 包含标准限制值的头文件
#include <stdint.h> // 包含标准整型定义的头文件
// 判断是否有OpenGL的支持
#ifdef HAVE_OPENGL
#ifdef _WIN32 // 如果是Windows 32位操作系统
#define WIN32_LEAN_AND_MEAN 1
#define NOMINMAX 1
#include <windows.h> // 包含Windows API的头文件
#endif
#if defined(_WIN64) // 如果是Windows 64位操作系统
#include <windows.h> // 包含Windows API的头文件
#endif


#if defined(__APPLE__) // 如果是苹果系统(MacOS)
#include <OpenGL/gl.h> // 包含OpenGL库的头文件
#include <OpenGL/glu.h> // 包含OpenGL工具库的头文件
#else // 其他操作系统
#include <GL/gl.h> // 包含OpenGL库的头文件
#include <GL/glu.h> // 包含OpenGL工具库的头文件
#endif
#endif


using namespace std; // 指定使用标准命名空间
using namespace cv; // 指定使用OpenCV命名空间


// 帮助信息函数,打印程序如何使用的说明
static void help(char** argv)
{
    cout << "\nThis program demonstrates how to use MSER to detect extremal regions\n"
            "Usage:\n"
         << argv[0] << " <image1(without parameter a synthetic image is used as default)>\n"
            "Press esc key when image window is active to change descriptor parameter\n"
            "Press 2, 8, 4, 6, +, -, or 5 keys in openGL windows to change view or use mouse\n";
}


// MSER算法参数结构体定义
struct MSERParams
{
    // MSERParams构造函数,带有默认参数
    MSERParams(int _delta = 5, int _min_area = 60, int _max_area = 14400,
               double _max_variation = 0.25, double _min_diversity = .2,
               int _max_evolution = 200, double _area_threshold = 1.01,
               double _min_margin = 0.003, int _edge_blur_size = 5)
    {
        // 初始化成员变量
        delta = _delta;
        minArea = _min_area;
        maxArea = _max_area;
        maxVariation = _max_variation;
        minDiversity = _min_diversity;
        maxEvolution = _max_evolution;
        areaThreshold = _area_threshold;
        minMargin = _min_margin;
        edgeBlurSize = _edge_blur_size;
        pass2Only = false;
    }


    // 定义MSER参数
    int delta; // Δ值,用于检测灰度变化
    int minArea; // 最小区域面积
    int maxArea; // 最大区域面积
    double maxVariation; // 最大变异系数
    double minDiversity; // 最小多样性
    bool pass2Only; // 仅进行第二阶段


    // 其他MSER参数
    int maxEvolution; // 最大演化次数
    double areaThreshold; // 区域阈值
    double minMargin; // 最小边缘
    int edgeBlurSize; // 边缘模糊尺寸
};


// 输出MSERParams参数的辅助函数,用于生成调用MSER算法的配置信息
static String Legende(const MSERParams &pAct)
{
    ostringstream ss;
    // 拼接字符串以显示提取的参数值
    ss << "Area[" << pAct.minArea << "," << pAct.maxArea << "] ";
    ss << "del. [" << pAct.delta << "] ";
    ss << "var. [" << pAct.maxVariation << "] ";
    ss << "div. [" << (int)pAct.minDiversity << "] ";
    ss << "pas. [" << (int)pAct.pass2Only << "] ";
    ss << "RGb->evo. [" << pAct.maxEvolution << "] ";
    ss << "are. [" << (int)pAct.areaThreshold << "] ";
    ss << "mar. [" << (int)pAct.minMargin << "] ";
    ss << "siz. [" << pAct.edgeBlurSize << "]";
    // 返回拼接好的字符串
    return ss.str();
}


#ifdef HAVE_OPENGL
// 特定于OpenGL的变量定义
const int win_width = 800; // OpenGL窗口宽度
const int win_height = 640; // OpenGL窗口高度
#endif


// 用于交互的全局状态变量
bool rotateEnable = true; // 是否允许旋转
bool keyPressed = false; // 是否有键盘按键按下


// 旋转操作的参数
Vec4f rotAxis(1, 0, 1, 0); // 定义旋转的轴
Vec3f zoom(1, 0, 0); // 定义缩放参数


// 观察者(摄像头)的位置参数
float obsX = 0.f; // X坐标
float obsY = 0.f; // Y坐标
float obsZ = -10.f; // Z坐标(负值表示相对于屏幕向内)
float tx = 0.f; // 平移参数X
float ty = 0.f; // 平移参数Y


// 观察者的观察角度参数
float thetaObs = -1.570f; // 仰角
float phiObs = 1.570f; // 方位角
float rObs = 10.f; // 观察半径(距离)


// 鼠标操作的前一状态参数(用于实现旋转等操作)
int prevX = -1; // 鼠标前一X坐标
int prevY = -1; // 鼠标前一Y坐标
int prevTheta = -1000; // 前一观察仰角
int prevPhi = -1000; // 前一观察方位角


// 图像上画多个嵌套矩形的函数
// img: 目标图像
// p0: 起始点坐标
// width: 包含宽度值的数组
// color: 包含颜色值的数组
// n: 要画的矩形数量
static void addNestedRectangles(Mat &img, Point p0, int* width, int *color, int n) {
    // 循环画出每个矩形
    for (int i = 0; i < n; i++)
    {
        // 画矩形
        rectangle(img, Rect(p0, Size(width[i], width[i])), Scalar(color[i]), 1);
        // 调整下一个矩形的起始点坐标
        p0 += Point((width[i] - width[i + 1]) / 2, (width[i] - width[i + 1]) / 2);
        // 填充矩形内的颜色
        floodFill(img, p0, Scalar(color[i]));
    }
}


// 图像上画多个嵌套圆的函数
// img: 目标图像
// p0: 圆心坐标
// width: 包含直径值的数组
// color: 包含颜色值的数组
// n: 要画的圆的数量
static void addNestedCircles(Mat &img, Point p0, int *width, int *color, int n) {
    // 循环画出每个圆
    for (int i = 0; i < n; i++)
    {
        // 画圆
        circle(img, p0, width[i] / 2, Scalar(color[i]), 1);
        // 填充每个圆的内部
        floodFill(img, p0, Scalar(color[i]));
    }
}


// 创建一个合成图像的函数
static Mat MakeSyntheticImage()
{
    // 定义背景色为0 
    const int fond = 0;


    // 创建一个800x800的灰度图像,初始颜色为背景色
    Mat img(800, 800, CV_8UC1);
    img = Scalar(fond);


    // 定义嵌套图形的大小数组
    int width[] = { 390, 380, 300, 290, 280, 270, 260, 250, 210, 190, 150, 100, 80, 70 };


    // 定义四组颜色数组,每个数组的颜色用于一组图形
    int color1[] = { 80, 180, 160, 140, 120, 100, 90, 110, 170, 150, 140, 100, 220 };
    int color2[] = { 81, 181, 161, 141, 121, 101, 91, 111, 171, 151, 141, 101, 221 };
    int color3[] = { 175, 75, 95, 115, 135, 155, 165, 145, 85, 105, 115, 155, 35 };
    int color4[] = { 173, 73, 93, 113, 133, 153, 163, 143, 83, 103, 113, 153, 33 };


    // 使用addNestedRectangles和addNestedCircles函数在图像上添加图形
    addNestedRectangles(img, Point(10, 10), width, color1, 13);
    addNestedCircles(img, Point(200, 600), width, color2, 13);


    addNestedRectangles(img, Point(410, 10), width, color3, 13);
    addNestedCircles(img, Point(600, 600), width, color4, 13);


    // 设置直方图的参数
    int histSize = 256; // 直方图的大小(256个灰度级)
    float range[] = { 0, 256 }; // 直方图的范围
    const float* histRange[] = { range }; // 指向直方图范围的指针数组
    Mat hist; // 创建Mat对象,用于存储直方图


    // 计算图像的直方图
    calcHist(&img, 1, 0, Mat(), hist, 1, &histSize, histRange, true, false);


    // 输出非零直方图区域的直方图值
    cout << "****************Maximal region************************\n";
    for (int i = 0; i < hist.rows; i++)
    {
        if (hist.at<float>(i, 0) != 0)
        {
            cout << "h" << setw(3) << left << i << "\t=\t" << hist.at<float>(i, 0) << "\n";
        }
    }


    // 返回生成的合成图像
    return img;
}
// 函数结束




// 主函数 - 程序的入口点
int main(int argc, char *argv[])
{
    Mat imgOrig, img; // 创建Mat对象,用于储存原始图像和处理后的图像
    Size blurSize(5, 5); // 创建Size对象,设置图像模糊的大小
    // 创建命令行解析器,用于处理程序输入
    cv::CommandLineParser parser(argc, argv, "{ help h | | }{ @input | | }");
    // 如果请求帮助则输出帮助信息并退出
    if (parser.has("help"))
    {
        help(argv); // 调用帮助信息函数
        return 0; // 程序成功退出
    }


    // 获取输入参数,如果提供了参数则尝试加载图像
    string input = parser.get<string>("@input");
    if (!input.empty())
    {
        imgOrig = imread(samples::findFile(input), IMREAD_GRAYSCALE); // 加载传入的图像文件名称参数,以灰度模式读取图像
        blur(imgOrig, img, blurSize); // 对原始图像应用模糊操作
    }
    else
    {
        imgOrig = MakeSyntheticImage(); // 如果没有输入图像,则创建一张合成的图像
        img = imgOrig; // 将合成图像赋值给处理图像
    }
    
    // 下面的代码略去了注释,它们执行MSER特征检测,并且显示结果图像
    // MSER特征描述子列表
    vector<String> typeDesc;
    // MSER参数列表
    vector<MSERParams> pMSER;
    
    // 调色板,用于随机生成颜色
    vector<Vec3b> palette;
    // 填充调色板,生成65536(2^16)种随机颜色(每种颜色由随机的RGB值组成)
    for (int i = 0; i <= numeric_limits<uint16_t>::max(); i++)
        palette.push_back(Vec3b((uchar)rand(), (uchar)rand(), (uchar)rand()));
    
    // 显示帮助信息
    help(argv);
    
    // 初始化MSER参数结构体
    MSERParams params;
    
    // 设置MSER参数
    params.delta = 10;
    params.minArea = 100;
    params.maxArea = 5000;
    params.maxVariation = 2;
    params.minDiversity = 0;
    params.pass2Only = true;
    // 将设置的MSER描述子和参数添加到相应的列表中
    typeDesc.push_back("MSER");
    pMSER.push_back(params);
    
    // 修改参数pass2Only,再次添加
    params.pass2Only = false;
    typeDesc.push_back("MSER");
    pMSER.push_back(params);
    
    // 修改参数delta,再次添加
    params.delta = 100;
    typeDesc.push_back("MSER");
    pMSER.push_back(params);
    
    // 定义MSER参数的迭代器
    vector<MSERParams>::iterator itMSER = pMSER.begin();
    // 引入Feature2D类指针,用于后续创建MSER对象
    Ptr<Feature2D> b;
    // 用于临时存储特征描述标签的字符串
    String label;
    // 特征描述符迭代器
    vector<String>::iterator itDesc;
    // 创建一个三通道的彩色图像result,用于存放处理结果
    Mat result(img.rows, img.cols, CV_8UC3);
    // 遍历描述符列表进行特征提取
    for (itDesc = typeDesc.begin(); itDesc != typeDesc.end(); ++itDesc)
    {
        // 用于存放关键点的列表
        vector<KeyPoint> keyImg1;
        // 如果当前描述子是"MSER"
        if (*itDesc == "MSER")
        {
            // 如果图像是彩色的
            if (img.type() == CV_8UC3)
            {
                // 创建MSER实例,参数从迭代器指向的MSERParams获取
                b = MSER::create(itMSER->delta, itMSER->minArea, itMSER->maxArea, itMSER->maxVariation, itMSER->minDiversity, itMSER->maxEvolution,
                                 itMSER->areaThreshold, itMSER->minMargin, itMSER->edgeBlurSize);
                // 转换参数为字符串标签
                label = Legende(*itMSER);
                // 往后移动迭代器,指向下一个参数
                ++itMSER;
            }
            else
            {
                // 如果图像不是彩色的,则不需要一些参数
                b = MSER::create(itMSER->delta, itMSER->minArea, itMSER->maxArea, itMSER->maxVariation, itMSER->minDiversity);
                // 使用dynamicCast对MSER对象进行转换,以调用非通用接口传递参数
                b.dynamicCast<MSER>()->setPass2Only(itMSER->pass2Only);
                // 转换参数为字符串标签
                label = Legende(*itMSER);
                // 往后移动迭代器,指向下一个参数
                ++itMSER;
            }
        }
    
        // 如果图像是彩色的,直接复制到结果图像
        if (img.type() == CV_8UC3)
        {
            img.copyTo(result);
        }
        else
        {
            // 如果不是彩色,将灰度图转换为彩色图再复制到结果图像
            vector<Mat> plan;
            plan.push_back(img);
            plan.push_back(img);
            plan.push_back(img);
            merge(plan, result);
        }
        try
        {
            // 待检测区域的列表
            vector<KeyPoint> keyImg;
            // 被检测到的区域位置
            vector<Rect> zone;
            // 被检测到的区域点列表
            vector<vector<Point>> region;
            // 特征描述
            Mat desc;
    
            // 如果成功转换为MSER对象
            if (b.dynamicCast<MSER>().get())
            {
                // 转换为MSER类型指针
                Ptr<MSER> sbd = b.dynamicCast<MSER>();
                // 调用detectRegions方法来检测区域
                sbd->detectRegions(img, region, zone);
                // 初始化MSER像素计数
                int nbPixelInMSER = 0;
                // 遍历每个区域
                for (auto itr = region.begin(); itr != region.end(); ++itr)
                {
                    // 遍历区域中的每个点
                    for (auto itp = itr->begin(); itp != itr->end(); ++itp)
                    {
                        // 将属于该区域的像素点在result中标记为蓝色
                        result.at<Vec3b>(itp->y, itp->x) = Vec3b(128, 0, 0);
                        // MSER像素数增加
                        nbPixelInMSER++;
                    }
                }
                // 输出MSER区域的数量和像素总数
                cout << "Number of MSER region: " << region.size() << "; Number of pixels in all MSER region: " << nbPixelInMSER << "\n";
            }
    
            // 生成窗口名,结合特征类型和参数标签
            const string winName = *itDesc + label;
            // 创建自适应大小的窗口
            namedWindow(winName, WINDOW_AUTOSIZE);
            // 在窗口中显示处理结果和原始图像
            imshow(winName, result);
            imshow("Original", img);
        }
        catch (const Exception& e) // 捕捉异常
        {
            // 输出特征和异常信息
            cout << "Feature: " << *itDesc << "\n";
            cout << e.msg << endl;
        }
    #ifdef HAVE_OPENGL // 如果支持OpenGL
        DrawOpenGLMSER(img, result); // 使用OpenGL绘制MSER结果
    #endif
        waitKey(); // 等待键盘输入
    }
    return 0;
}

该程序演示了如何使用MSER来检测极值区域

当图像窗口处于活动状态时按 esc 键可更改描述符参数

在 openGL 窗口中按 2、8、4、6、+、- 或 5 键来更改视图或使用鼠标

终端输出:

MSER区域数量:3; 所有 MSER 区域的像素数:10420

MSER区域数量:6; 所有 MSER 区域的像素数:20840

MSER区域数量:0; 所有 MSER 区域中的像素数:0

Press esc key when image window is active to change descriptor parameter
Press 2, 8, 4, 6, +, -, or 5 keys in openGL windows to change view or use mouse
Number of MSER region: 3; Number of pixels in all MSER region: 10420
Number of MSER region: 6; Number of pixels in all MSER region: 20840
Number of MSER region: 0; Number of pixels in all MSER region: 0

MSER 算法在视频分析中有什么应用?

4ecbe0f09295f7e801d825c9d95dec3a.png

MSER 算法如何应用于实时视频监控系统中的特定物体或活动模式的检测和追踪?

b33fffed1701ccf8bf5cf99a778dbe26.png

The End

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

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

相关文章

【数据结构】考研真题攻克与重点知识点剖析 - 第 5 篇:树与二叉树

前言 本文基础知识部分来自于b站&#xff1a;分享笔记的好人儿的思维导图与王道考研课程&#xff0c;感谢大佬的开源精神&#xff0c;习题来自老师划的重点以及考研真题。此前我尝试了完全使用Python或是结合大语言模型对考研真题进行数据清洗与可视化分析&#xff0c;本人技术…

C# + OpencvSharp4 错误信息收集

异常1&#xff1a; 初次使用&#xff0c;如下代码报错&#xff0c;OpenCvSharp.OpenCvSharpException:“imread failed.” Mat src Cv2.ImRead("Source.png", ImreadModes.Unchanged); 原因&#xff1a;检查Nuget包与OpencvSharp4库相关安装是否完整&#xff0c;…

虚拟地址的设置与应用全攻略

在计算机网络和操作系统中&#xff0c;虚拟地址是一个核心概念&#xff0c;它允许程序在逻辑上拥有连续的内存空间&#xff0c;而无需关心实际的物理内存布局。虚拟地址的引入极大地简化了程序的开发和内存管理&#xff0c;提高了系统的灵活性和安全性。虎观代理将详细解析虚拟…

Spring Boot中整合JodConverter实现文件在线预览

Spring Boot中整合JodConverter实现文件在线预览 1.安装LibreOffice 24.2 下载地址 LibreOffice 是一款功能强大的办公软件&#xff0c;默认使用开放文档格式 (OpenDocument Format , ODF), 并支持 *.docx, *.xlsx, *.pptx 等其他格式。 它包含了 Writer, Calc, Impress, Dra…

java基于微服务的智慧工地管理云平台SaaS源码 数据大屏端 APP移动端

目录 智慧工地云平台概述 智慧工地功能 &#xff08;项目端&#xff09; 一、劳务管理&#xff1a; 二、施工安全管理&#xff1a; 三、视频监控管理&#xff1a; 四、机械安全管理&#xff1a; 五、施工质量管理&#xff1a; 六、绿色文明施工&#xff1a; 七、施工综…

CSS3如何实现雷达扫描图(动态样式)

动态样式控制雷达扫描和暂停&#xff1a; //html部分&#xff1a; <view class"radar" :style"{--state:animationPlayState}"></view>data部分&#xff1a; animationPlayState: paused, methods: changeStatus(){this.animationPlayState …

Failed to start docker.service: Unit is not loaded properly: Invalid argument.

Failed to start docker.service: Unit is not loaded properly: Invalid argument. 未知原因&#xff1a;docker服务无法正常load 解决方式&#xff1a; 卸载docker&#xff0c; 删除docker.service 重新安装docker Docker是一种相对使用较简单的容器&#xff0c;我们可以通过…

证书生成和获取阿里云备案获取密钥流程

1.在java文件夹下 输入 cmd 打开命令行窗口 2. keytool -genkey -alias 证书名 -keyalg RSA -keysize 2048 -validity 36500 -keystore 证书名.keystore 输入这一行&#xff0c;把证书名三个字 改成 项目的名称&#xff08;例如&#xff1a;D23102802&#xff09; 3. 密码默认填…

【会议】Oracle自动化运维峰会

2023年7月21日&#xff0c;杭州。我组织了Oracle自动化运维峰会&#xff0c;大约有20人左右参加会议。以下是会议主题&#xff1a; Oracle自动化运维能力是Oracle 19c自动化运维体系中非常重要的一环&#xff0c;自动化索引、自动化SQL优化、资源隔离等技术能够非常好的提升运维…

备考ICA----Istio实验17---TCP流量授权

备考ICA----Istio实验17—TCP流量授权 1. 环境准备 1.1 环境部署 kubectl apply -f <(istioctl kube-inject -f istio/samples/tcp-echo/tcp-echo.yaml) -n kim kubectl apply -f <(istioctl kube-inject -f istio/samples/sleep/sleep.yaml) -n kim1.2 测试环境 检测…

使用Python将多张图片转换为动态GIF图像

在本文中&#xff0c;我们将学习如何使用Python编写代码&#xff0c;将多张静态图片转换为一个动态的GIF图像。无论你的图片格式是JPEG&#xff08;.jpg&#xff09;还是PNG&#xff08;.png&#xff09;&#xff0c;我们都将使用Python中的PIL库来实现这一功能。通过本文的学习…

k8s_入门_命令详解

命令详解 kubectl是官方的CLI命令行工具&#xff0c;用于与 apiserver进行通信&#xff0c;将用户在命令行输入的命令&#xff0c;组织并转化为 apiserver能识别的信息&#xff0c;进而实现管理k8s各种资源的一种有效途径 1. 帮助 2. 查看版本信息 3. 查看资源对象等 查看No…

精品PPT-数据治理总体解决方案新版(免费下载)

1、知识星球下载&#xff1a; 如需下载完整PPTX可编辑源文件&#xff0c;请前往星球获取&#xff1a;https://t.zsxq.com/19F4dDDrv 2、免费领取步骤&#xff1a; 【1】关注公众号 方案驿站 【2】私信发送 数据治理新版 【3】获取本方案PDF下载链接&#xff0c;直接下载即可…

【C++】手搓 list 容器

送给大家一句话&#xff1a; 若结局非你所愿&#xff0c;就在尘埃落定前奋力一搏。—— 《夏目友人帐》 手搓 list 容器 1 前言1.1 底层结构1.2 使用场景1.3 功能简介 2 框架搭建2.1 节点类2.2 list 类2.3 迭代器类 3 功能实现3.1 begin() 与 end()3.2 插入操作3.3 删除操作3…

云原生之旅第一课(2站搜索K8s成神之路)

自己动手搭建Kubernetes集群&#xff0c;学习如何自定义CRD&#xff0c;以及使用Kubebuilder快速搭建Operator项目&#xff0c;云原生之旅第一课。从一开始准备录制课程&#xff0c;到如今已经有了500位忠实粉丝&#xff0c;我感到无比欣慰。这门课程完全开源&#xff0c;每一集…

FPN网络

FPN&#xff08;Feature Pyramid Network&#xff09;是一种用于目标检测和语义分割等计算机视觉任务的网络结构。它旨在解决不同尺度下的特征信息不足的问题&#xff0c;提高模型对小目标和远距离目标的检测能力。在目标检测任务中&#xff0c;由于目标的尺度和形状各异&#…

聚观早报 | 百度文心一言上线新功能;腾势Z9GT将发布

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 4月08日消息 百度文心一言上线新功能 腾势Z9GT将发布 华为将举办鸿蒙春季沟通会 苹果与Shutterstock达成协议 O…

ebpf+perfetto实现调度延迟记录与展示

1.背景 需要分析生产环境的调度问题,如线程的调度延迟有多少,在哪些时间点延迟比较明显,影响其调度的主要原因是什么?其次,我们希望可以比较直观的展示调度延迟情况。最好能对接perfetto的UI和后处理,因为perfetto已经用于分析比较多的性能数据,可以和调度数据进行整合.我们…

Dify开源大语言模型(LLM) 应用开发平台如何使用Docker部署与远程访问

文章目录 1. Docker部署Dify2. 本地访问Dify3. Ubuntu安装Cpolar4. 配置公网地址5. 远程访问6. 固定Cpolar公网地址7. 固定地址访问 本文主要介绍如何在Linux Ubuntu系统以Docker的方式快速部署Dify,并结合cpolar内网穿透工具实现公网远程访问本地Dify&#xff01; Dify 是一款…

性能优化 - 你知道开发React项目中,可以做哪些性能优化吗

难度级别:中高级及以上 提问概率:75% 在React项目开发中,面临着比较大的问题就是组件更新以及重复渲染的问题,基于这两点,我们可以在日常开发工作中,可以通过以下几点,来提升React的性能,加快组件更新对比,避免过多的重复渲染问题。 …