Opencv之RANSAC算法用于直线拟合及特征点集匹配详解
Opencv之RANSAC算法用于直线拟合及特征点集匹配详解
-
- 讲述Ransac拟合与最小二乘在曲线拟合上的优缺点
-
- 讲述在进行特征点匹配时,最近邻匹配与Ransac匹配的不同之处
-
- 另外,Ransac也被用于椭圆拟合、变换矩阵求解等
1. 直线拟合
1.1 原理
-
RANSAC(RANdom SAmple Consensus,随机采样一致)算法是从一组含有“外点”(outliers)的数据中正确估计数学模型参数的迭代算法。“外点”一般指的的数据中的噪声,比如说匹配中的误匹配和估计曲线中的离群点。故RANSAC也是一种“外点”检测算法。同时RANSAC是一个非确定性算法,在某种意义上说,它会产生一个在一定概率下合理的结果,其允许使用更多次的迭代来使其概率增加。
-
RANSAC算最早是由Fischler和Bolles在SRI上提出用来解决LDP(Location Determination Problem,位置确定问题)问题的。
-
对于RANSAC算法来说一个基本的假设就是数据是由“内点”和“外点”组成的。“内点”就是组成模型参数的数据,“外点”就是不适合模型的数据。同时RANSAC假设:在给定一组含有少部分“内点”的数据,存在一个程序可以估计出符合“内点”的模型
-
算法主要思想:
- 给定一个数据集S,从中选择建立模型所需的最小样本数(空间直线最少可以由两个点确定,所以最小样本数是2,空间平面可以根据不共线三点确定,所以最小样本数为3,拟一个圆时,最小样本数是3),记选择数据集为S1
使用选择的数据集S1计算得到一个数学模型M1- 用计算的模型M1去测试数据集中剩余的点,如果测试的数据点在误差允许的范围内,则将该数据点判为内点(inlier),否则判为外点(outlier),记所有内点组成的数据集为S1*,S1* 称作 S1的一致性集合
- 比较当前模型和之前推出的最好的模型的“内点”的数量,记录最大“内点”数量时模型参数和“内点”数量
- 重复1-4步,直到迭代结束或者当前模型已经足够好了(“内点数目大于设定的阈值”);每次产生的模型要么因为内点太少而被舍弃,要么因为比现有的模型更好而被选用
- 其过程如下图所示:
取点集中的两点确定一条直线,然后通过设定规则选取筛选内殿,拿最多的内点拟合出来的模型作为最终的可用模型

1.2 迭代次数推导
- 根据上面RANSAC基本原理的介绍,在这算法流程中存在两个重要的参数需要设置,迭代次数(采样次数)和距离阈值。
迭代的次数我们应该选择多大呢?这个值是否可以事先知道应该设为多少呢?还是只能凭经验决定呢? 这个值其实是可以估算出来的。下面来推算一下。

内点的概率t通常是一个先验值。然后P 是我们希望RANSAC得到正确模型的概率。如果事先不知道t 的值,可以使用自适应迭代次数的方法。也就是一开始设定一个无穷大的迭代次数,然后每次更新模型参数估计的时候,用当前的内点比值当成t 来估算出迭代次数。
1.3 与最小二乘区别
- 最小二乘法尽量去适应包括外点在内的所有点。因此,最小二乘法只适合与误差较小的情况。假使需要从一个噪音较大的数据集中提取模型(比方说只有20%的数据时符合模型的)时,最小二乘法就显得力不从心了。

- RANSAC相当于一个概率模型,它通过计算内点出现的概率,找出噪点之外的点集拟合出的 最优模型,通常更能表示系统属性。其相当于迭代使用最小二乘法+抽样测试。
1.4 代码实现
- C++实现:
//====================================================================//
//Program:RANSAC直线拟合,并与最小二乘法结果进行对比
//====================================================================//
#include <iostream>
#include <opencv2/opencv.hpp>//RANSAC 拟合2D 直线
//输入参数:points--输入点集
// iterations--迭代次数
// sigma--数据和模型之间可接受的差值,车道线像素宽带一般为10左右
// (Parameter use to compute the fitting score)
// k_min/k_max--拟合的直线斜率的取值范围.
// 考虑到左右车道线在图像中的斜率位于一定范围内,
// 添加此参数,同时可以避免检测垂线和水平线
//输出参数:line--拟合的直线参数,It is a vector of 4 floats
// (vx, vy, x0, y0) where (vx, vy) is a normalized
// vector collinear to the line and (x0, y0) is some
// point on the line.
//返回值:无
void fitLineRansac(const std::vector<cv::Point2f>& points,cv::Vec4f &line,int iterations = 1000,double sigma = 1.,double k_min = -7.,double k_max = 7.)
{unsigned int n = points.size();if(n<2){return;}cv::RNG rng;double bestScore = -1.;for(int k=0; k<iterations; k++){int i1=0, i2=0;while(i1==i2){i1 = rng(n);i2 = rng(n);}const cv::Point2f& p1 = points[i1];const cv::Point2f& p2 = points[i2];cv::Point2f dp = p2-p1;//直线的方向向量dp *= 1./norm(dp);double score = 0;if(dp.y/dp.x<=k_max && dp.y/dp.x>=k_min ){for(int i=0; i<n; i++){cv::Point2f v = points[i]-p1;double d = v.y*dp.x - v.x*dp.y;//向量a与b叉乘/向量b的摸.||b||=1./norm(dp)//score += exp(-0.5*d*d/(sigma*sigma));//误差定义方式的一种if( fabs(d)<sigma )score += 1;}}if(score > bestScore){line = cv::Vec4f(dp.x, dp.y, p1.x, p1.y);bestScore = score;}}
}int main()
{cv::Mat image(720,1280,CV_8UC3,cv::Scalar(125,125,125));//以车道线参数为(0.7657,-0.6432,534,548)生成一系列点double k = -0.6432/0.7657;double b = 548 - k*534;std::vector<cv::Point2f> points;for (int i = 360; i < 720; i+=10){cv::Point2f point(int((i-b)/k),i);points.emplace_back(point);}//加入直线的随机噪声cv::RNG rng((unsigned)time(NULL));for (int i = 360; i < 720; i+=10){int x = int((i-b)/k);x = rng.uniform(x-10,x+10);int y = i;y = rng.uniform(y-30,y+30);cv::Point2f point(x,y);points.emplace_back(point);}//加入噪声for (int i = 0; i < 720; i+=20){int x = rng.uniform(1,640);int y = rng.uniform(1,360);cv::Point2f point(x,y);points.emplace_back(point);}int n = points.size();for (int j = 0; j < n; ++j){cv::circle(image,points[j],5,cv::Scalar(0,0,0),-1);}//RANSAC 拟合if(1){cv::Vec4f lineParam;fitLineRansac(points,lineParam,1000,10);double k = lineParam[1] / lineParam[0];double b = lineParam[3] - k*lineParam[2];cv::Point p1,p2;p1.y = 720;p1.x = ( p1.y - b) / k;p2.y = 360;p2.x = (p2.y-b) / k;cv::line(image,p1,p2,cv::Scalar(0,255,0),2);}//最小二乘法拟合if(1){cv::Vec4f lineParam;cv::fitLine(points,lineParam,cv::DIST_L2,0,0.01,0.01);double k = lineParam[1] / lineParam[0];double b = lineParam[3] - k*lineParam[2];cv::Point p1,p2;p1.y = 720;p1.x = ( p1.y - b) / k;p2.y = 360;p2.x = (p2.y-b) / k;cv::line(image,p1,p2,cv::Scalar(0,0,255),2);}cv::imshow("image",image);cv::waitKey(0);return 0;
}

- Python 实现:
#!/usr/bin/env python3
#coding=utf-8#============================#
#Program:RANSAC_Line.py
===========#import numpy as np
import random
import mathimport cv2def fitLineRansac(points,iterations=1000,sigma=1.0,k_min=-7,k_max=7):"""RANSAC 拟合2D 直线:param points:输入点集,numpy [points_num,1,2],np.float32:param iterations:迭代次数:param sigma:数据和模型之间可接受的差值,车道线像素宽带一般为10左右(Parameter use to compute the fitting score):param k_min::param k_max:k_min/k_max--拟合的直线斜率的取值范围.考虑到左右车道线在图像中的斜率位于一定范围内,添加此参数,同时可以避免检测垂线和水平线:return:拟合的直线参数,It is a vector of 4 floats(vx, vy, x0, y0) where (vx, vy) is a normalizedvector collinear to the line and (x0, y0) is somepoint on the line."""line = [0,0,0,0]points_num = points.shape[0]if points_num<2:return linebestScore = -1for k in range(iterations):i1,i2 = random.sample(range(points_num), 2)p1 = points[i1][0]p2 = points[i2][0]dp = p1 - p2 #直线的方向向量dp *= 1./np.linalg.norm(dp) # 除以模长,进行归一化score = 0a = dp[1]/dp[0]if a <= k_max and a>=k_min:for i in range(points_num):v = points[i][0] - p1dis = v[1]*dp[0] - v[0]*dp[1]#向量a与b叉乘/向量b的摸.||b||=1./norm(dp)# score += math.exp(-0.5*dis*dis/(sigma*sigma))误差定义方式的一种if math.fabs(dis)<sigma:score += 1if score > bestScore:line = [dp[0],dp[1],p1[0],p1[1]]bestScore = scorereturn lineif __name__ == '__main__':image = np.ones([720,1280,3],dtype=np.ubyte)*125# 以车道线参数为(0.7657, -0.6432, 534, 548)生成一系列点k = -0.6432 / 0.7657b = 548 - k * 534points = []for i in range(360,720,10):point = (int((i-b)/k),i)points.append(point)# 加入直线的随机噪声for i in range(360,720,10):x = int((i-b)/k)x = random.sample(range(x-10,x+10),1)y = iy = random.sample(range(y - 30, y + 30),1)point = (x[0],y[0])points.append(point)# 加入噪声for i in range(0,720,20):x = random.sample(range(1, 640), 1)y = random.sample(range(1, 360), 1)point = (x[0], y[0])points.append(point)for point in points:cv2.circle(image,point,5,(0,0,0),-1)points = np.array(points).astype(np.float32)points = points[:,np.newaxis,:]# RANSAC 拟合if 1:[vx, vy, x, y] = fitLineRansac(points,1000,10)k = float(vy) / float(vx) # 直线斜率b = -k * x + yp1_y = 720p1_x = (p1_y-b) / kp2_y = 360p2_x = (p2_y-b) / kp1 = (int(p1_x),int(p1_y))p2 = (int(p2_x), int(p2_y))cv2.line(image,p1,p2,(0,255,0),2)# 最小二乘法拟合if 1:[vx, vy, x, y] = cv2.fitLine(points, cv2.DIST_L2, 0, 0.1, 0.01)k = float(vy) / float(vx) # 直线斜率b = -k * x + yp1_y = 720p1_x = (p1_y - b) / kp2_y = 360p2_x = (p2_y - b) / kp1 = (int(p1_x), int(p1_y))p2 = (int(p2_x), int(p2_y))cv2.line(image, p1, p2, (0, 0, 255), 2)cv2.imshow('image',image)cv2.waitKey(0)
2. 特征匹配
- 基于特征的图像匹配中会存在误匹配对,因此为提高匹配率,在粗匹配的基础上实现精匹配,可采用下面两种方法:

- 用RANSAC算法来寻找最佳单应性矩阵H,在此先提取SIFT特征点进行最近邻粗匹配,然后采取Ransac进行细匹配,最后再进行变换矩阵求解
- 代码实现如下:
//RANSAC算法
int main()
{Mat img_object = imread("./data/101.png", IMREAD_GRAYSCALE);Mat img_scene = imread("./data/100.png", IMREAD_GRAYSCALE);if (img_object.empty() || img_scene.empty()){cout << "Could not open or find the image!\n" << endl;return -1;}//-- Step 1: Detect the keypoints using SURF Detector, compute the descriptorsint minHessian = 800; // default: 400Ptr<SURF> surf = SURF::create(800);std::vector<KeyPoint> keypoints_object, keypoints_scene;Mat descriptors_object, descriptors_scene;surf->detectAndCompute(img_object, noArray(), keypoints_object, descriptors_object);surf->detectAndCompute(img_scene, noArray(), keypoints_scene, descriptors_scene);//-- Step 2: Matching descriptor vectors with a FLANN based matcher// Since SURF is a floating-point descriptor NORM_L2 is usedPtr<DescriptorMatcher> matcher = DescriptorMatcher::create(DescriptorMatcher::FLANNBASED);std::vector< std::vector<DMatch> > knn_matches;matcher->knnMatch(descriptors_object, descriptors_scene, knn_matches, 2);//-- Filter matches using the Lowe's ratio testconst float ratio_thresh = 0.75f;std::vector<DMatch> good_matches;for (size_t i = 0; i < knn_matches.size(); i++){if (knn_matches[i][0].distance < ratio_thresh * knn_matches[i][1].distance){good_matches.push_back(knn_matches[i][0]);}}//-- Draw matchesMat img_matches;drawMatches(img_object, keypoints_object, img_scene, keypoints_scene, good_matches, img_matches, Scalar::all(-1),Scalar::all(-1), std::vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);//-- Localize the objectstd::vector<Point2f> obj;std::vector<Point2f> scene;for (size_t i = 0; i < good_matches.size(); i++){//-- Get the keypoints from the good matchesobj.push_back(keypoints_object[good_matches[i].queryIdx].pt);scene.push_back(keypoints_scene[good_matches[i].trainIdx].pt);}vector<uchar>inliers;Mat H = findHomography(obj, scene, inliers, RANSAC);//-- Draw matches with RANSACMat img_matches_ransac;std::vector<DMatch> good_matches_ransac;for (size_t i = 0; i < inliers.size(); i++){if (inliers[i]){good_matches_ransac.push_back(good_matches[i]);}}drawMatches(img_object, keypoints_object, img_scene, keypoints_scene, good_matches_ransac, img_matches_ransac, Scalar::all(-1),Scalar::all(-1), std::vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);namedWindow("img_matches", WINDOW_NORMAL);imshow("img_matches", img_matches);imwrite("img_matches.jpg", img_matches);namedWindow("img_matches_ransac", WINDOW_NORMAL);imshow("img_matches_ransac", img_matches_ransac);imwrite("img_matches_ransac.jpg", img_matches_ransac);waitKey();return 0;
}
- 只进行knn匹配与加上Ransac匹配的效果对比图如下:

参考:
1.https://blog.csdn.net/leonardohaig/article/details/104570965?spm=1001.2014.3001.5506
2.https://blog.csdn.net/H19981118/article/details/122014318?spm=1001.2014.3001.5506
相关文章:
Opencv之RANSAC算法用于直线拟合及特征点集匹配详解
Opencv之RANSAC算法用于直线拟合及特征点集匹配详解 讲述Ransac拟合与最小二乘在曲线拟合上的优缺点 讲述在进行特征点匹配时,最近邻匹配与Ransac匹配的不同之处 另外,Ransac也被用于椭圆拟合、变换矩阵求解等 1. 直线拟合 1.1 原理 RANSAC(RANdom …...
Jenkins环境部署与任务构建
一、CI/CD 1、CI/CD 概念: CI/CD 是一种软件开发和交付方法,旨在加速应用程序的开发、测试和部署过程,以提高软件交付的质量和效率。 (1) 持续集成 (CI Continuous Integration): 持续集成是开发团队频繁集成其代码更改的过程。开发者将其…...
ES6 Class和Class继承
1.class的基本语法 class可以理解为是一个语法糖,将js只能通过构造函数创建实例的方法进行了补充 构造函数: function Person ({ name, age18 }) {this.name namethis.age age } new Person({name: 张三}) Class类: class Person {con…...
C++11 packaged_task
std::packaged_task 把一个方法打包成一个task扔到线程中执行,然后通过packaged_task中的furture等待执行结果。 void test_promise() {std::packaged_task <int()> task([]()->int {std::cout << "packaged_task begin \n" << std…...
delete、drop、truncate三兄弟
比较方面/具体命令deletetruncatedrop删除范围逐行删除(记录行)逐页删除(数据页)整张表(数据表结构)所属范畴数据操作语言(DML)数据定义语言(DDL)数据定义语言…...
C/C++运算优先级
文章目录 前言1.运算优先级表2.举例说明:总结 前言 最近复习C基础知识的时候,发现对这部分还是有些模糊。常用的 - ,括号等运算符对于它们的优先级还是比较明确的。但是涉及到移位运算,逻辑运算这种,再结合四则运算…...
apache搭建静态网站,moongoose搭建网站后台,出现的跨域问题解决
文章目录 1,问题描述1.1,当网页和后台是不同服务时会产生跨域问题1.2,跨域问题 2,nginx端口转发解决跨域问题2.1,下载并安装nginx2.1.1,解压后如下所示2.1.2,进入解压目录后,执行配置…...
LiveQing视频点播流媒体RTMP推流服务功能-支持视频点播分屏大屏展示视频轮巡分组播放RMP推流直播大屏展示
LiveQing支持视频点播分屏大屏展示视频轮播分组播放RMP推流直播大屏展示 1、分屏展示2、轮巡播放3、RTMP推流视频直播和点播流媒体服务 1、分屏展示 LiveQing支持将视频点播、鉴权直播,拉转直播视频流,进行分屏播放。 2、轮巡播放 3、RTMP推流视频直播和…...
tf loss构建常用到函数
1、tf.map_fn tf.map_fn是TensorFlow中的一个函数,用于对给定的函数和输入进行逐元素的映射,其定义如下: tf.map_fn(fn,elems,dtypeNone,parallel_iterationsNone,back_propTrue,swap_memoryFalse,infer_shapeTrue,nameNone,fn_output_sign…...
行为型模式-备忘录模式
备忘录模式保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。 主要解决:所谓备忘录模式就是在不破坏…...
Android Studio初学者实例:RecyclerView学习--模仿今日头条--续
新学期开始了,这篇文章收到了很多人的评论有很多地方不懂,所以写下了以下的文章--续篇 首先使用RecyclerView也好还是使用ListView,更或是GridView你都要先构思需要什么 这些东西无一例外通常都是用在列表显示下,那么需要一些&a…...
栈和队列的C++模拟实现
一、栈stack 1.介绍(库里面的文档介绍) 1. stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。 2. stack是作为容器适配器被实现的,容器适配器即是对…...
UE4/5:通过Blender制作BlendShape导入【UE4/5曲线、变形目标,blender形态键】
UE4/5里面,我们经常可以在一些骨骼模型上面看到相关的曲线,如Metahuman里面就是通过这个曲线来改变人物的脸部表情。 而这里笔者将教导如何去制作这种曲线。 这种曲线都是存在于骨骼模型上的,所以我们要么直接制作骨骼模型导入ue࿰…...
微信小程序进阶——后台交互
目录 一、后台准备 1.1 pom.xml 1.2 配置数据源 1.3 整合mybatis 二、前后端交互 2.1 method1 2.2 method2 2.2.1 封装request 2.2.2 头部引用util 2.2.3 编写方法 2.2.4 展示效果 三、WXS的使用 3.1 会议状态 3.1.2 引入wxs 3.1.3 修改代码 3.1.4 展示效果 3…...
二维码智慧门牌管理系统升级解决方案:突破传统,实现质检与抽检的个性化配置
文章目录 前言一、引入“独立质检”二、个性化抽检类别设定三、触发重采要素的功能升级四、升级优势与展望 前言 在数字化时代,智慧门牌管理系统已经成为社会管理的重要工具。为了满足各种复杂需求,系统升级是必然趋势。本次升级主要针对质检和抽检两大…...
《动手学深度学习 Pytorch版》 9.4 双向循环神经网络
之前的序列学习中假设的目标是在给定观测的情况下对下一个输出进行建模,然而也存在需要后文预测前文的情况。 9.4.1 隐马尔可夫模型中的动态规划 数学推导太复杂了,略。 9.4.2 双向模型 双向循环神经网络(bidirectional RNNs)…...
【Axure高保真原型】可视化图表图标
今天和粉丝们免费分享可视化图表图标原型模板,包括柱状图、条形图、环形图、散点图、水波图等常用的可视化图表图标。 【原型效果】 【原型预览】 https://axhub.im/ax9/d402c647c82f9185/#c1 【原型下载】 这个模板可以在 Axure高保真原型哦 小程序里免费下载哦…...
安装mmcv及GPU版本的pytorch及torchvision
一、先装GPU版本的pytorch和torchvision pip install torch1.9.1cu111 torchvision0.10.1cu111 torchaudio0.9.1 -f https://download.pytorch.org/whl/torch_stable.html注意:以上适用cuda11.1版本 如果想离线安装,就看这篇文章 二、安装mmcv 看这篇…...
全国342个城市往返最短通勤时间(铁路)数据
全国342个城市往返最短通勤时间(铁路)数据 1、时间:采集时间是2022年 2、来源:12306 3、数据说明:数据采集12306数据,整理全国342个城市往返最短通勤时间,本数据是铁路包含动车、高铁所有路线…...
AWK语言第二版 第3章.探索性数据分析 3.1泰坦尼克号的沉没
这章也是第一版没有,第二版新增的。 3. 探索性数据分析 上一章给出了一些个人使用的小脚本,通常是特制或专用的。在本章中,我们还会展示Awk在现实中的典型使用场景:使用Awk和其他工具来非正式地探索一些真实的数据,目…...
OpenLayers 可视化之热力图
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 热力图(Heatmap)又叫热点图,是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...
docker详细操作--未完待续
docker介绍 docker官网: Docker:加速容器应用程序开发 harbor官网:Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台,用于将应用程序及其依赖项(如库、运行时环…...
椭圆曲线密码学(ECC)
一、ECC算法概述 椭圆曲线密码学(Elliptic Curve Cryptography)是基于椭圆曲线数学理论的公钥密码系统,由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA,ECC在相同安全强度下密钥更短(256位ECC ≈ 3072位RSA…...
(十)学生端搭建
本次旨在将之前的已完成的部分功能进行拼装到学生端,同时完善学生端的构建。本次工作主要包括: 1.学生端整体界面布局 2.模拟考场与部分个人画像流程的串联 3.整体学生端逻辑 一、学生端 在主界面可以选择自己的用户角色 选择学生则进入学生登录界面…...
Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…...
微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...
【WiFi帧结构】
文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成:MAC头部frame bodyFCS,其中MAC是固定格式的,frame body是可变长度。 MAC头部有frame control,duration,address1,address2,addre…...
R语言AI模型部署方案:精准离线运行详解
R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
页面渲染流程与性能优化
页面渲染流程与性能优化详解(完整版) 一、现代浏览器渲染流程(详细说明) 1. 构建DOM树 浏览器接收到HTML文档后,会逐步解析并构建DOM(Document Object Model)树。具体过程如下: (…...
