简介:面向计算机视觉、机器人导航与增强现实领域开发者的一份三维重建实战项目,完整演示了从摄像头标定开始,计算内参、外参、畸变系数,到双目立体校正与视差计算,再到点云生成和体积测量的全过程。压缩包仅12KB,共5个文件,其中核心为3个C++源文件与1个头文件,辅助1份Markdown说明文档,代码结构清晰,适合作为算法原型参考或课程设计素材。项目分别给出单目标定与双目标定立体校正的可运行示例,将视差计算、点云映射与体积估算等关键环节串成完整链路,并配有说明文档帮助理解各模块调用关系。读者可替换自己的双目图像数据,直观验证标定与重建效果,也可以结合表面重建、网格化处理等后续算法进一步扩展功能。该资源已有136人学习,对希望掌握三维重建工程化落地要点的工程师具有切实的借鉴价值。
1. 三维重建:从摄像头标定到点云体积计算的完整落地路径
三维重建在工业测量、机器人导航和AR/VR里几乎是绕不开的环节,但很多人把开源库一拉,跑起来却发现点云是花的、体积是错的。问题往往不在重建算法本身,而在最前面的摄像头标定和立体校正。这套资源正好把整条链路串起来:single_calib.cpp负责单目标定,stereo_calib_match.cpp做双目校正与匹配,test.cpp完成点云生成和体积计算。适合刚接手双目视觉项目的工程师,也适合想搞清楚内参外参到底怎么影响最终测量的从业者。下面我把源码结构、关键参数和实际踩坑都拆开讲,照着走一遍,能省下不少调试时间。
2. 摄像头标定:内参外参与畸变模型,single_calib 的用法解析
2.1 标定到底在解哪几个数
摄像头标定求解的是三个层面的参数:内参、畸变系数、外参。内参是焦距fx、fy和主点cx、cy,它们定义了三维点从相机坐标系投影到像素平面的方式。畸变系数包含径向畸变k1、k2、k3和切向畸变p1、p2,用来修正镜头制造和安装带来的图像变形。外参则是相机相对于世界坐标系的旋转和平移,也就是每一张标定图像对应的R和T。
在单目标定里,我们通常假设标定板所在的平面是世界坐标系的Z=0平面,棋盘格角点的三维坐标就是(csize, rsize, 0),其中size是棋盘格边长。相机看到这些角点后,通过最小化重投影误差,同时解出内参、畸变和每组图像的外参。OpenCV的calibrateCamera返回的rms值就是这个重投影误差的均方根,经验上小于0.5像素算合格,大于1像素就得重新检查数据。
这套项目里的single_calib.cpp就是干这件事的。它先把每一帧的棋盘格角点提出来,再喂给calibrateCamera,最后把内参矩阵和畸变系数写进文件。值得提醒的是,fx和fy的单位不是毫米,而是像素,它等于物理焦距除以像元尺寸。所以不要用“焦距35mm”这类镜头参数直接替换,必须以标定结果为准。
2.2 角点提取与亚像素细化
棋盘格角点提取是整个标定流程的第一环,角点位置偏一个像素,标定结果就会明显波动。下面这段是single_calib.cpp里最核心的提取逻辑:
// single_calib.cpp 角点检测与亚像素细化 #include "single_calib.h" #include <opencv2/opencv.hpp> bool SingleCalib::detectCorners(const cv::Mat& image, cv::Size pattern_size, std::vector<cv::Point2f>& corners) { cv::Mat gray; cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY); // 第一步:粗检测,自适应阈值+归一化应对光照不均 bool found = cv::findChessboardCorners(gray, pattern_size, corners, cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE); if (!found) return false; // 第二步:亚像素细化,把角点精度从像素推进到亚像素 cv::TermCriteria criteria( cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.01); cv::cornerSubPix(gray, corners, cv::Size(11, 11), cv::Size(-1, -1), criteria); return true; }逻辑说明:findChessboardCorners做的是粗定位,它根据棋盘格黑白交替的梯度特征找到角点的近似位置。CALIB_CB_ADAPTIVE_THRESH让每个局部区域单独计算阈值,适合光照不均匀的现场;CALIB_CB_NORMALIZE_IMAGE先把图像归一化再处理,也能减少高光影响。cornerSubPix则是在粗位置附近用灰度梯度迭代收敛到亚像素坐标,窗口大小11x11是常用值,窗口越小越容易受噪声影响,窗口越大越平滑但可能丢失细节。
参数说明:pattern_size传入的是内角点数量,不是棋盘格的格子数量。一张10x7的棋盘格,内角点数是9x6。很多人第一次标定失败,就是错把格子数填了进去。criteria里的30是最大迭代次数,0.01是亚像素精度阈值。迭代30次对普通摄像头足够,如果角点在高分辨率下飘得厉害,可以放宽到50次。
2.3 标定板规格与采集姿势
标定板是标定质量的另一半。推荐用玻璃基底的陶瓷棋盘格或高精度打印板,棋盘格尺寸要均匀,边缘不能有毛刺。我一般用A3纸打印的棋盘格,贴在硬质平板上,只要不折皱,精度对于常规双目测量也够用。关键是square_size这个参数一定要量准,它是后续点云尺度的基准。
采集姿势对标定结果的影响,比很多人以为的更大。我见过有人拿十张几乎同一个角度的棋盘格图像去标定,rms还算小,但一上立体校正就原形毕露——外参里的旋转分量约束不够。这里有一个我经常用的采集清单:
| 采集要点 | 推荐做法 | 原因 | | 图像数量 | 15到20张 | 太少解不准,太多浪费时间 | | 角度变化 | 左右倾斜、上下俯仰各拍几张 | 让旋转矩阵有约束 | | 距离变化 | 近、中、远各拍一组 | 让尺度信息更充分 | | 棋盘格占比 | 占画面1/3到1/2 | 太小角点分辨率不足 | | 光照 | 均匀打光,避免强反光 | 反光会让角点提取失败 |
采集时把棋盘格放在画面边缘和角落的位置也很关键,因为镜头畸变在边缘最明显,只有让角点覆盖到画面边缘,畸变系数才能被正确约束。如果所有图像里棋盘格都在画面中央,标定出的k1、k2会偏小,畸变校正等于没做。
最后看标定结果时,除了rms,还要把每张图的角点重投影误差打出来。OpenCV的calibrateCamera不会直接给每个点的误差,可以用projectPoints把每个外参对应的角点重投影回图像,与原始提取位置做差。误差最大的那几张图通常就是拍摄姿势最差的,直接删掉重新标定,比硬留着强。
提示:single_calib 标定完的内参和畸变系数,要单独存成文件。后面 stereo_calib_match 和 test 都会读取这份结果,三个文件共用一组参数,不要在每段代码里各标一遍。
3. 双目立体校正:极线约束与 stereo_calib_match 的匹配流程
3.1 双目标定:左右相机相对位姿怎么来
单目标定知道了每个相机自己怎么把三维点投成像素,但左右两台相机之间是什么关系,还得用双目标定来求。双目标定在单目标定的基础上,同时采集左右相机的棋盘格图像,解出右相机相对于左相机的旋转矩阵R和平移向量T。这一步的意义是:有了R和T,才能把左右图像校正到同一个理想的平行双目模型上。
stereo_calib_match.cpp里会先跑单目标定拿到左右各自的内参,然后调用stereoCalibrate做联合优化。实际工程里,双目标定比单目标定更容易翻车,因为左右图像必须保证同步采集。常见的做法是先用代码里那份single_calib分别对左右相机做单目标定,把内参固定住,再只优化R、T和畸变,这样参数少、收敛快,数据质量差一点也还能救回来。
左右相机的基线长度T的x分量,在测量时会直接影响深度精度。基线越长,同样视差误差带来的深度误差越小;但基线太长,左右视野重叠区域会变小,近处物体容易出画。工业上常见的是50mm到120mm的基线,具体要看被测目标的距离范围。stereo_calib_match.cpp里没有对这个值做特殊处理,它是从标定结果里直接读出来的,换硬件时只要重新标定即可。
双目标定完成后,建议把R和T打印出来看一眼。平移向量T的x分量应该和实际基线长度接近,如果算出来是几十米,说明标定过程有严重问题,多半是左右图像没有同步,或者标定板在左右视角里的位置对不上。这种错误在后面的点云里会表现为深度整体漂移,很难靠调参救回来。
3.2 立体校正:让匹配从二维降成一维
校正前,同一个三维点在左右图像里的投影点,搜索范围是整幅图;校正的目标是让左右图像的极线对齐到同一行,这样搜索范围从二维降到一维。stereoRectify输出的R1、R2是重投影旋转矩阵,P1、P2是新的投影矩阵,Q是重投影矩阵,后面计算点云时会用到。
// stereo_calib_match.cpp 立体校正与重映射 cv::Mat R1, R2, P1, P2, Q; cv::Rect left_roi, right_roi; // 输入双目标定得到的 R、T 和左右内参 cv::stereoRectify(K_left, dist_left, K_right, dist_right, image_size, R, T, R1, R2, P1, P2, Q, cv::CALIB_ZERO_DISPARITY, 0, image_size, &left_roi, &right_roi); // 生成校正映射表 cv::Mat map_left_x, map_left_y, map_right_x, map_right_y; cv::initUndistortRectifyMap(K_left, dist_left, R1, P1, image_size, CV_32FC1, map_left_x, map_left_y); cv::initUndistortRectifyMap(K_right, dist_right, R2, P2, image_size, CV_32FC1, map_right_x, map_right_y); // 重映射,得到极线对齐的校正后图像 cv::remap(img_left, rect_left, map_left_x, map_left_y, cv::INTER_LINEAR); cv::remap(img_right, rect_right, map_right_x, map_right_y, cv::INTER_LINEAR);逻辑说明:stereoRectify把左右相机的光轴校正到平行状态,并且让两个成像平面共面,这样同一三维点的左右投影点才会落在同一行。CALIB_ZERO_DISPARITY表示只校正、不调整视差范围,适合一般测量场景。倒数第三个参数alpha控制校正后图像的裁剪方式,0表示裁剪掉无内容的黑色区域,1表示保留全部像素但边缘会出现拉伸。
参数说明:initUndistortRectifyMap把畸变校正和立体校正合并成一张查找表,map_x和map_y分别记录每个目标像素对应原图的坐标。CV_32FC1表示映射表用单精度浮点存储,是remap的标准输入格式。remap里的INTER_LINEAR是双线性插值,缩放变形不严重时默认用它;如果校正后图像有锯齿,可以换成INTER_CUBIC,但耗时更高。
校正质量怎么验证?把左右校正图并排显示,用鼠标点右图上的一行,看左图同一行是否出现对应的特征点。或者直接画等间距水平线,比较左右图上的棋盘格边缘是否落在同一行。这一步手工看一眼,比任何指标都直观。
3.3 SGBM视差计算与参数调优
校正完成后,stereo_calib_match.cpp进入视差计算环节。OpenCV里常用的方法是StereoSGBM,它对光照差异和纹理稀疏区域比传统BM更稳一些。下面是一组我在类似项目里调过的基础参数:
// stereo_calib_match.cpp SGBM 视差计算 cv::Ptr<cv::StereoSGBM> sgbm = cv::StereoSGBM::create( 0, // minDisparity 80, // numDisparities 11); // blockSize sgbm->setP1(8 * 3 * 11 * 11); sgbm->setP2(32 * 3 * 11 * 11); sgbm->setUniquenessRatio(10); sgbm->setSpeckleWindowSize(100); sgbm->setSpeckleRange(32); sgbm->setDisp12MaxDiff(1); cv::Mat disparity; sgbm->compute(rect_left, rect_right, disparity); // 视差图除以16得到真实视差值 disparity.convertTo(disparity, CV_32F, 1.0 / 16.0);逻辑说明:StereoSGBM是半全局匹配算法,它把匹配代价聚合到多个方向,再用平滑约束减少误匹配。minDisparity设0表示只搜索左图比右图靠左的视差,通常够用;numDisparities必须是16的倍数,它决定了能计算的最近距离范围。blockSize是匹配窗口边长,窗口越大视差越平滑,但边缘细节会丢失。
参数说明:P1和P2是视差平滑惩罚项,P2通常是P1的4到8倍,数值越大越倾向生成平滑的视差图。这里按OpenCV官方建议,用8*3*blockSize^2和32*3*blockSize^2打底再乘通道数3。uniquenessRatio=10表示最佳匹配的代价要比次佳匹配低10%才被接受,太小会引入噪声,太大则容易产生空洞。speckleWindowSize和speckleRange用来过滤视差图中的孤立小连通域,100和32是比较稳的组合。
视差图转成浮点后,每个像素值就是纯视差,单位是像素。根据Z=f*baseline/disparity,视差越大深度越小。这里的f在双目校正后已经隐含在Q矩阵里,后面直接用Q重投影就不需要手动代公式了。
视差图的质量检查不能只看整体,要看具体区域。通常我会在视差图上框选几个已知距离的目标点,用Z=f*baseline/disparity手算一遍,和真实距离做个对比。如果误差在3%以内,说明视差尺度正确,可以继续流向点云;如果偏差超过5%,就得回头查双目参数了。
注意:numDisparities 直接影响可测的最近距离。如果目标物体离相机太近,视差超过搜索范围,计算出的深度是错的。先根据最近工作距离估算最大视差,再把这个值向上取整到16的倍数。
4. 点云生成与体积计算:test.cpp 的数据链路
4.1 从视差图到三维点云
视差图只是中间产品,真正参与体积计算的是三维点云。test.cpp里用reprojectImageTo3D把视差图配合Q矩阵直接转成点云,一步到位。Q矩阵在stereoRectify时已经由代码保存,它包含了焦距、主点和基线信息,不需要再手动拼。
// test.cpp 视差图重投影为三维点云 cv::Mat point_cloud; cv::reprojectImageTo3D(disparity_f, point_cloud, Q, true); // 提取有效点,过滤无效深度 std::vector<cv::Point3f> valid_points; for (int v = 0; v < point_cloud.rows; ++v) { for (int u = 0; u < point_cloud.cols; ++u) { cv::Vec3f p = point_cloud.at<cv::Vec3f>(v, u); if (std::isfinite(p[2]) && p[2] > 0 && p[2] < max_depth) { valid_points.push_back(cv::Point3f(p[0], p[1], p[2])); } } }逻辑说明:reprojectImageTo3D的第四个参数handleMissingValues设为true时,会把无效深度位置替换成一个大值而不是直接写0,方便后面统一过滤。得到的point_cloud是CV_32FC3的三通道矩阵,每个像素对应一个三维点,坐标单位取决于标定时使用的棋盘格边长单位。
参数说明:这里过滤条件p[2] > 0是为了剔除相机平面后方的无效点,p[2] < max_depth则是根据工作距离设一个深度上限,把远处的噪声点切掉。max_depth的具体值应该和场景匹配,测桌面物体时取500mm就够,测房间场景时可以放到3000mm。这一步不做,后面体积计算会被背景点带偏。
还需要注意,reprojectImageTo3D出来的点云是原始采样密度,每个像素一个点。如果直接拿去做体积计算,点数可能超过百万,内存和耗时都扛不住。test.cpp里通常会在这一步之后做一次降采样,常见做法是用voxel grid把空间划分成小立方体,每个立方体保留重心点,既降密度又保持几何形状。
4.2 点云滤波与目标分割
SGBM生成的视差图必然带飞点,尤其在高光表面、弱纹理区域和物体边缘。这些飞点在点云里表现为孤立的小簇,不滤波直接算体积,轻则体积偏大,重则计算失败。test.cpp里可以加一个半径滤波来清理,思路很简单:每个点找相邻半径内的近邻数,少于阈值的判定为飞点剔除。
半径滤波的半径要和点云密度匹配。点云是视差图转的,密度跟相机分辨率、距离直接相关。一般先对有效点做个统计,看平均点间距,然后让半径取平均间距的1.5到2倍。近邻数阈值取10到30,点数稀的地方可以放宽。这个参数不是固定值,每次换相机距离都要重新检查,否则容易把真实表面点误删。
目标分割是另一个容易忽略的环节。如果场景里除了被测物体还有背景,直接把全部点拿来算体积,背景点会全部计入。最简单的方法是先标定一个工作台平面,用平面拟合把台面点删除,再用连通域把目标点云单独拎出来。没有平面先验时,也可以用深度阈值把目标限制在一个区间内,但前提是目标与背景在深度维度上分得开。
4.3 体积计算的两种思路
test.cpp里体积计算有两种常见实现:体素积分和凸包近似。体素积分是把目标点云所在的三维空间划分成固定边长的小立方体,统计被点云占据的立方体数量,再乘以单个立方体的体积。实现逻辑不复杂,关键是体素边长要根据点云密度选,太小会出现空洞,太大会把空隙也计入体积。
// test.cpp 体素法体积计算 double voxel_size = 1.0; // 体素边长,单位mm std::set<int> voxel_keys; for (size_t i = 0; i < points.size(); ++i) { int vx = (int)std::floor(points[i].x / voxel_size); int vy = (int)std::floor(points[i].y / voxel_size); int vz = (int)std::floor(points[i].z / voxel_size); // 用整数编码做哈希,避免set存复杂结构 int key = vx * 1000000 + vy * 1000 + vz; voxel_keys.insert(key); } double volume = voxel_keys.size() * voxel_size * voxel_size * voxel_size;逻辑说明:这里把三维坐标除以体素边长后取整,同一个体素内不管有多少个点都只记一次,最后体素个数乘以体素体积就是整体体积。这个方法的误差来自边界:被目标切了一半的体素也被算成完整体素,体素越小误差越小。1mm的体素对50mm量级的目标,误差通常在百分之几。哈希编码用vx*1000000 + vy*1000 + vz,前提是坐标范围不要超过1000个体素,目标大或者体素小的时候要换成更稳妥的哈希方式。
凸包近似则是把点云外轮廓包起来,用三维凸包计算体积。对于立方体、圆柱、球这类凸形物体,凸包非常准;但对有凹陷、孔洞的物体,凸包会把凹陷填平,体积明显偏大。做工业零件测量时,先判断目标形状再选方法:轴类、方块用凸包,异形件用体素。
尺度标定是最后一道关。标定时square_size填25mm,点云坐标就是毫米;填成25cm,所有尺寸会放大10倍,体积放大1000倍。这个错误在体积结果上特别隐蔽,因为形状完全不变,只是数值不对。排查方式很简单:用游标卡尺量一下点云里两个对应点的距离,跟实际长度对一下,一个点就能暴露单位问题。
提示:test.cpp 跑完可以顺手把点云导出成 ply 或 pcd 文件,用 CloudCompare 打开复核。三维重建做到这一步,光看数值远不如直接把点云转起来看形状可靠。
5. 避坑指南:从标定到体积计算最常见的五个坑
5.1 角点漏检:不是标定板的问题,是输入的姿势问题
现象:findChessboardCorners经常返回false,或者其中某几帧检测失败,导致标定程序直接退出。
原因:最常见的是pattern_size填错,把棋盘格数量当成了内角点数量。其次是棋盘格反光、光照不均、图像分辨率太低,角点在缩小后梯度不明显。还可能是标定板离镜头太近,角点超出畸变严重的区域,边缘角点被裁掉。
解决:先把pattern_size改成内角点数,10x7的格子填cv::Size(9,6)。再检查每帧图像里棋盘格是否完整,不完整直接丢弃。最后加CALIB_CB_ADAPTIVE_THRESH和CALIB_CB_NORMALIZE_IMAGE,这两个开关能救回不少光照不理想的图。我一般只保留角点检测成功且重投影误差小于0.5px的帧。
5.2 校正后图像出现大块黑色区域或明显拉伸
现象:stereoRectify和remap完成后,图像四个角出现黑色三角区,或者图像比例看起来不对,直线边缘变成弧线。
原因:alpha参数设置不当。alpha=0时OpenCV会裁剪掉所有没有像素内容的区域,只保留最大有效内接矩形,但视野会变小;alpha=1时保留全部原始像素,校正后的边缘必然有黑色区域。另一个常见原因是畸变系数标定不准,k1偏大时边缘拉伸特别明显。
解决:对体积测量场景,我推荐alpha设0,牺牲一点视野,换来干净的图像区域,避免黑色区域干扰视差计算。畸变系数不准的问题要从标定数据源头解决,重拍包含画面边缘角点的标定图,而不是在remap参数上硬调。
5.3 视差图满是飞点,点云表面像长刺
现象:SGBM跑出来的视差图上有大量随机孤立点,点云表面凹凸不平,像刺猬一样。
原因:匹配窗口太小导致误匹配,或者numDisparities设得过大,让算法在无纹理区域乱找对应点。左右图亮度差异大、曝光不一致也会让代价计算被亮度主导。P1、P2太小意味着平滑约束弱,算法会容忍很多不连续的视差跳跃。
解决:先把blockSize从11往上加到15或21,看飞点是否明显减少。再把uniquenessRatio调到15以上,压制非唯一匹配。最后检查左右相机曝光,关闭自动曝光和自动白平衡,固定增益。改完参数一定要重新看视差图的错误率,不要只看点云好不好看。
5.4 体积计算结果差一个量级
现象:对一个边长50mm的立方体,算出来的体积接近125000mm³才正常,结果跑出来125cm³甚至更离谱的数字,单位乱套或者数量级不对。
原因:最常见的是square_size填错单位,把毫米填成了厘米,几何尺寸放大10倍,体积放大1000倍。其次是Q矩阵用的内参不是标定结果,而是默认初始化值,导致深度计算整体偏移。还有一种隐蔽情况,是把点云坐标当成像素值处理,忘了视差图除以16。
解决:先用游标卡尺量点云里两个已知点的距离,验证尺度。比如立方体一条边在点云里量出来是500mm,说明square_size被填成了250mm。单位纠正后,再用一个已知体积的标准块跑全流程,验证最终体积误差。这个验证步骤每次换环境都要做,不是标定过一次就一劳永逸。
5.5 左右相机画面亮度不一致导致匹配失败
现象:左右图里同一个物体亮度明显不同,SGBM在亮暗区域产生大面积误匹配,视差图出现块状错误。
原因:双目相机如果没做硬件同步,自动曝光会各自独立调整,左右图的增益、快门都不一样。即使打了固定增益,镜头差异和传感器响应差异也会造成亮度不一致。
解决:优先把左右相机设为同样的手动曝光参数,包括增益、曝光时间、白平衡。如果硬件不支持同步,可以在软件层面对左右图做直方图匹配,让两幅图的灰度分布尽量一致。这个操作虽然不能完全消除差异,但能明显减少SGBM在边缘区域的误匹配。还有一个小技巧是跑SGBM之前先做一次高斯模糊,压制高频噪声带来的假匹配。
6. 进阶验证:用标准块做闭环,把体积误差压到3%以内
到这里,全流程已经能跑通,但能跑通不等于测得准。我建议你拿到这个项目后的第一件事,不是拿真实工件开测,而是做一个标准块验证:找一个边长50mm的金属立方体,或者一个直径已知的圆柱,按完整流程走一遍,得到它的点云和体积,再跟真实体积对比。这个闭环验证是判断整条标定链路有没有问题的最快方式。
标准块的几何形状要跟实际被测物接近。测规则工件用立方体,测轴类零件用圆柱,测异形件就找一个泡棉块手工切成类似轮廓。验证时的相机距离、光照强度也要跟正式测量保持一致,因为SGBM的参数和点云密度都跟距离有关。跑完这一步,你可以记录三组数据:标定误差rms、视差图的误匹配率、最终体积误差。后面每次调整环境,都拿这三组数据跟基线对比,变化超过阈值就回去查标定。
如果体积误差超过3%,优先检查的不是体积计算代码,而是标定和视差这两个上游环节。我自己遇到过一个例子:体积误差稳定在8%,怎么调体积算法都降不下来,后来发现是标定板表面有点卷曲,导致外参整体偏移,重投影误差虽然只有0.7px,但深度误差放大到了毫米级别。换了一块玻璃基底的标定板,误差立刻回到2%以内。硬件层面的精度瓶颈,用软件参数很难完全弥补。
点云导出这一步我建议也养成习惯。test.cpp跑完把结果存成ply,用CloudCompare打开,先用网格化视图看表面是否连续,再用它的体积测量工具复核一遍。CloudCompare的体积工具基于2.5D网格,对凸形物体能快速给一个参考值,跟test.cpp的体素结果互相印证。两个方法算出同一个量级,说明流程稳定,可以放心出数据。
这套资源里那份README已经把完整流程串好了,single_calib.cpp、stereo_calib_match.cpp、test.cpp三个文件正好对应标定、校正、点云体积计算三段链路。从那以后,我每次换相机、换镜头、挪工作台,都强制走一遍标准块验证流程,确认体积偏差在3%以内才开始正式采集。这个习惯帮我挡掉了不少返工,也让我对这套资源的每一段代码都有底。希望帮到你。
本文还有配套的精品资源,点击获取