尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

双目三维重建全流程:标定、立体匹配到点云体积计算

发布时间:2026/9/24 18:23:14

资讯中心
01
ARTICLE

双目三维重建全流程:标定、立体匹配到点云体积计算

双目三维重建全流程:标定、立体匹配到点云体积计算
简介这是一份面向计算机视觉、机器人导航与工业测量等领域开发者的三维重建实战资源完整覆盖摄像头标定、双目立体校正、视差计算、点云生成与体积估算的关键流程。压缩包共5个文件包含3个C源文件、1个头文件及1份README说明约12KB代码结构精简适合已掌握基础OpenCV用法、希望快速跑通双目视觉Pipeline的初中级工程师参考。当前已有136人学习下载。资源中的标定模块用于求解相机内参、畸变系数及外参立体校正模块负责左右图像极线对齐与视差匹配点云与体积计算部分则基于视差图生成三维坐标并估算目标占据体积。README对编译环境与调用步骤进行了说明便于读者按顺序复现实验省去从零搭建的摸索时间。1. 三维重建资源拆解一套 C 双目代码把标定、校正、点云、体积串成完整链路这份压缩包拆开之后是四个 C 文件加一个 READMEtest.cpp、single_calib.h、single_calib.cpp、stereo_calib_match.cpp。名字起得很直白但内容比名字值钱——它把三维重建里最容易卡住人的四个环节完整串了一遍摄像头标定、双目立体校正、视差计算、点云生成与体积测量。多数教程讲到标定就停了或者贴着原理讲完立体匹配就不管后续这套代码是直接奔着算出体积去的适合手里有双目相机、要做物体测量或机器人抓取的工程师也适合拿来做毕业设计的完整框架。一个反直觉的结论先放在这标定重投影误差做到 0.1 像素重建出来的体积照样可能差 20%问题往往出在立体校正和视差尺度转换这两个看起来不起眼的环节上。2. 摄像头标定single_calib.cpp 的内参求解与重投影误差检查2.1 标定到底在解什么内参、外参与畸变系数的物理含义摄像头标定的输出是三样东西内参矩阵、畸变系数、外参。内参矩阵里最关键的是fx、fy、cx、cy四个数。fx不是焦距的物理长度而是焦距除以单个像素的物理尺寸单位是像素——比如传感器单个像素 3.45μm、镜头焦距 8mm那fx 8000 / 3.45 ≈ 2318像素。cx、cy是主点理想情况下在图像正中心实测偏个几十像素都正常但如果偏离到图像宽度的四分之一以上基本可以判定标定数据有问题。畸变系数这里常用的是k1、k2、p1、p2分别对应径向畸变和切向畸变。径向畸变是镜头曲面造成的桶形/枕形变形切向畸变是镜头与传感器不平行造成的。OpenCV 的calibrateCamera内部用的畸变模型还包含k3有时到k6对小视场镜头k3贡献很小建议在标定时直接用CALIB_FIX_K3把它固定为 0减少参数间的耦合否则内参和畸变系数互相补偿RMS 很漂亮但实际矫正效果是歪的。单目标定这一步还有一个容易被忽略的产物外参。每张标定板照片都会得到一组旋转向量rvec和平移向量tvec表示拍摄瞬间棋盘格在相机坐标系下的位姿。单目标定里外参只服务于这张图但如果后面要做双目左右相机各自的标定误差会直接带进立体外参计算所以单目标定这步的质量直接决定后续所有环节的上限。2.2 single_calib.cpp 核心流程从角点检测到 calibrateCamerasingle_calib.cpp的流程和我在项目里常用的写法一致对每张棋盘格图像做角点检测、亚像素细化然后收集全部图像的内角点坐标和对应的三维物理坐标一次性丢给calibrateCamera。核心代码可以浓缩成这样#include opencv2/opencv.hpp #include single_calib.h using namespace cv; using namespace std; // boardSize 是内角点数量例如棋盘格是 10x7 的格子这里传 (9, 6) vectorPoint3f generateObjectPoints(const Size boardSize, float squareSize) { vectorPoint3f pts; for (int r 0; r boardSize.height; r) for (int c 0; c boardSize.width; c) pts.emplace_back(c * squareSize, r * squareSize, 0.0f); // Z0 平面 return pts; } bool calibrateFromImages(const vectorstring paths, const Size boardSize, float squareSize, Mat K, Mat dist, vectorMat rvecs, vectorMat tvecs) { vectorvectorPoint2f cornerList; vectorvectorPoint3f objectList; Size imageSize; for (const auto p : paths) { Mat gray imread(p, IMREAD_GRAYSCALE); if (gray.empty()) continue; imageSize gray.size(); vectorPoint2f corners; bool found findChessboardCorners(gray, boardSize, corners); if (!found) continue; // 检测不到就跳过这张图不强求 // 亚像素细化角点精度从像素级提升到亚像素级 cornerSubPix(gray, corners, Size(5, 5), Size(-1, -1), TermCriteria(TermCriteria::EPS TermCriteria::MAX_ITER, 30, 0.01)); cornerList.push_back(corners); objectList.push_back(generateObjectPoints(boardSize, squareSize)); } if (cornerList.size() 10) return false; // 少于 10 张有效图标定结果不可信 double rms calibrateCamera(objectList, cornerList, imageSize, K, dist, rvecs, tvecs, CALIB_FIX_K3); // 固定 k3减少参数耦合 printf(RMS %.4f px\n, rms); return rms 0.3; // 我一般以 0.3 像素为经验阈值 }这里的两个关键参数boardSize必须传内角点数而不是格子数。10x7 格的标定板内角点数是 9x6传错的话findChessboardCorners大概率检测失败或者标定出的内参严重失真。squareSize是棋盘格格子的物理边长单位毫米这个值决定外参平移向量的尺度也间接影响双目基线的单位——如果标定板实际是 30mm 格子但你填了 25重建出来的所有三维坐标会整体缩小体积自然跟着错。有效图像数量建议不少于 15 张并且拍摄时要让标定板出现在画面的不同位置、不同倾角尤其是画面边缘和四角。标定板要占画面面积的 20% 以上太小了角点亚像素精度不够。整个流程跑完后打印一张表列出每张图的角点数量和重投影误差这张表比 RMS 更能反映问题出在哪张图。2.3 用逐图重投影误差代替 RMS 判断标定质量RMS 是calibrateCamera返回的全局平均把所有图的角点误差揉在一起算了个均方根。实际踩过坑的都知道这个数字有时候会骗人——某张图误差 0.8 像素其余图误差 0.05平均下来 RMS 可能只有 0.2看着合格但那张坏图如果恰好覆盖了画面左下角区域畸变系数就会被带偏。我一般标定完会把每张图的重投影误差单独算出来逐个检查// 逐图计算重投影误差定位是哪张图标定得差 for (size_t i 0; i cornerList.size(); i) { vectorPoint2f projPts; projectPoints(objectList[i], rvecs[i], tvecs[i], K, dist, projPts); double err 0.0; for (size_t j 0; j projPts.size(); j) err norm(cornerList[i][j] - projPts[j]); // 欧氏距离单位像素 err / cornerList[i].size(); printf(Frame %zu: mean reproj err %.3f px\n, i, err); }projectPoints是把标定时算出的三维点用当前的外参、内参、畸变系数重新投影回图像平面和检测到的角点坐标做差。正常一张清晰的棋盘格图平均误差应该在 0.1 像素以内超过 0.3 像素就要警惕要么这张图有运动模糊要么标定板本身有轻微弯曲要么角点检测时亚像素迭代没收敛。找出这几张坏图从列表里剔除后重新标定比闷头重拍 20 张图更高效。3. 双目立体校正stereo_calib_match.cpp 的极线对齐与 Q 矩阵构建3.1 为什么单目标定完了还不够左右相机之间的 R 和 T 决定深度精度单目标定只解决单个相机把三维点映射到像素的问题。双目系统要恢复深度必须知道左右两个相机之间的相对位姿——旋转矩阵R和平移向量T。T的模长就是基线baseline它直接出现在深度公式的分母上Z f * baseline / d。基线测偏 1mm近距离物体深度就偏出几个毫米到几厘米这不是算法能弥补的所以立体外参标定必须用stereoCalibrate做联合优化而不是把左右两套单目外参拿来硬凑。极线约束是立体匹配的理论基础空间一点在左图上的投影点它的同名点一定在右图的某一条直线上这条线叫极线。如果两个相机完全没有校正极线是倾斜的搜索匹配点要在二维平面上扫又慢又容易误匹配。立体校正的目的就是把左右图像重投影到一对虚拟的平行相机上让同名点落在同一水平线上这样匹配只在水平方向做一维搜索效率和正确率都会好很多。3.2 stereoRectify 的完整调用从 stereoCalibrate 到 remapstereo_calib_match.cpp里这一步值得细看我把它拆成四个连续动作先用stereoCalibrate算双目标定再用stereoRectify计算左右相机的矫正旋转矩阵和新的投影矩阵接着用initUndistortRectifyMap生成映射表最后remap重采样输出校正后的左右图。// 输入左右单目标定得到的 K1/D1, K2/D2 // 输入左右图像中棋盘格角点对应的像素坐标 lCorners[i], rCorners[i] stereoCalibrate(objectList, lCorners, rCorners, K1, D1, K2, D2, imageSize, R, T, E, F, CALIB_FIX_INTRINSIC, // 单目已定只优化双目外参 TermCriteria(TermCriteria::EPS | TermCriteria::MAX_ITER, 100, 1e-6)); // R1/R2: 左右相机矫正旋转P1/P2: 矫正后投影矩阵Q: 重投影矩阵 stereoRectify(K1, D1, K2, D2, imageSize, R, T, R1, R2, P1, P2, Q, CALIB_ZERO_DISPARITY, // 主点行对齐Q 矩阵第3行第4列为0 0); // alpha: 0 裁剪黑边1 保留全视场 // 生成映射表并执行重采样 Mat map11, map12, map21, map22; initUndistortRectifyMap(K1, D1, R1, P1, imageSize, CV_16SC2, map11, map12); initUndistortRectifyMap(K2, D2, R2, P2, imageSize, CV_16SC2, map21, map22); Mat leftRect, rightRect; remap(leftRaw, leftRect, map11, map12, INTER_LINEAR); remap(rightRaw, rightRect, map21, map22, INTER_LINEAR);CALIB_FIX_INTRINSIC的意思是左右相机的内参和畸变系数已经由单目标定确定stereoCalibrate只优化 R、T、E、F。这是最稳妥的做法因为单目和双目的目标函数不同如果放开内参一起优化内参和双目外参会互相拉扯得到的结果未必更好。只有当左右单目标定明显不一致时才考虑去掉这个标志让双目联合优化内参但那样需要重投影误差也重新评估。alpha参数是校正阶段最直观的调节钮取 0 时 OpenCV 会把校正后图像中的无效区域黑边全部裁掉输出图像变小但画面干净取 1 时保留原始视场黑边也保留边缘物体可能因为旋转而超出图像。我一般调试阶段用alpha1因为能看到完整的图像内容方便确认有没有裁掉有效区域定稿后改成alpha0再去跑匹配减少无意义的黑边对匹配的干扰。3.3 校正结果的验证极线是否水平一眼就能看出来代码跑通了不等于校正对了。最简单的验证方法是在校正后的左右图上各画几条水平线去同一行找棋盘格角点或者纹理清晰的点看它们的 y 坐标是否一致。更定量的做法是把左右棋盘格角点的 y 坐标做差统计均值// 对校正后的左右棋盘格角点做极线残差统计 double maxDY 0.0, meanDY 0.0; for (size_t i 0; i lCorners.size(); i) { double dy fabs(lCorners[i].y - rCorners[i].y); meanDY dy; maxDY std::max(maxDY, dy); } meanDY / lCorners.size(); printf(Average row difference %.3f px, max %.3f px\n, meanDY, maxDY);这个meanDY正常应该小于 1 像素如果能压到 0.3 像素以内说明R1、R2校正到位后面的 SGBM 才敢放心地把搜索范围限制在水平方向。如果这个值超过 2 像素且图像没有明显畸变残留优先怀疑输入给stereoRectify的K1/D1、K2/D2与采集尺寸不匹配——比如标定用的是 1280x720实际跑的是 1920x1080映射表全部错位。4. 点云生成与体积计算SGBM 视差图到三维坐标的换算与两种体积算法4.1 视差与深度的换算Q 矩阵把这件靠手算容易错的事打包了立体校正之后同名点的视差d x_left - x_right视差越大物体越近。深度与视差是反比关系公式是Z f * baseline / d。stereoRectify输出的Q矩阵把这一步打包成了齐次坐标变换用reprojectImageTo3D一条 API 就能把视差图变成三维点云。Q 矩阵的典型形式是Q [1 0 0 -cx_l 0 1 0 -cy_l 0 0 0 f 0 0 -1/Tx (cx_l - cx_r) / Tx]其中Tx是立体校正后的基线平移量带符号。CALIB_ZERO_DISPARITY标志会让两个主点行对齐此时cx_l与cx_r近似相等第四行第四列约等于 0。这里要特别注意符号-1/Tx这一项决定了深度的正负如果算出来的 Z 全是负值检查是不是把左右图顺序搞反了。4.2 SGBM 参数表每个数字都对应一种典型问题stereo_calib_match.cpp里用的是StereoSGBMOpenCV 4.x 下的创建方式如下PtrStereoSGBM sgbm StereoSGBM::create( 0, // minDisparity: 最小视差通常为 0 64, // numDisparities: 视差搜索范围必须为 16 的倍数 11, // blockSize: 匹配窗口边长推荐奇数 8 * 11 * 11, // P1: 视差平滑惩罚通常取 8 * blockSize^2 32 * 11 * 11, // P2: 大梯度惩罚通常取 32 * blockSize^2 1, // disp12MaxDiff: 左右一致性检查阈值1~2 有效抑制误匹配 0, // preFilterCap: 预滤波截断值默认 0 表示使用内置默认值 63 10, // uniquenessRatio: 唯一性比率5~15越大误匹配越少 100, // speckleWindowSize: 斑块滤波窗口消除小噪声块 32, // speckleRange: 斑块内视差允许的最大跳变 StereoSGBM::MODE_SGBM); // MODE_HH 更慢但更准MODE_SGBM 平衡速度这几个参数里最容易翻车的是P1、P2和blockSize之间的比例关系。P2会惩罚相邻像素视差不连续太大会把物体边缘的细节磨平太小小噪声多P1太小会在平坦区域产生碎点。调参的正确顺序是先定blockSize用 7 或 11再按8 * blockSize^2和32 * blockSize^2给 P1、P2 一个初始值然后调numDisparities让物体最近的表面和最远背景之间的视差范围都被覆盖——物体离相机越近、基线越长需要的numDisparities越大场景深的时候从 64 加到 128 是常态。uniquenessRatio低于 5 时匹配结果里会出现大量横向条纹状噪声speckleWindowSize在 50~200 之间太大会把真实的小物体表面也当成噪声吃掉。4.3 从视差图到点云注意 CV_16S 的定点数陷阱StereoSGBM::compute输出的视差图是CV_16S类型每个像素值除以 16 才是真实的浮点视差。这一步是最容易被绕进去的地方常见写法是直接用reprojectImageTo3D处理CV_16S的图结果所有三维坐标被放大 16 倍。正确做法是先转成CV_32F再喂给重投影函数Mat disp16, disp32f; sgbm-compute(leftRect, rightRect, disp16); // CV_16S值为真实视差 * 16 Mat disp32f; disp16.convertTo(disp32f, CV_32F, 1.0 / 16.0); // 关键还原真实视差 Mat points3D; // 输出为 CV_32FC3 reprojectImageTo3D(disp32f, points3D, Q, true);handleMissingValues参数传trueOpenCV 会把视差无效的点在三维矩阵里标记为 Z 值极大或极小方便后续过滤。遍历points3D生成点云时我一般同时做两层过滤把 Z 值不在合理范围内的点直接丢弃比如小于 0 或大于 15m再用直通滤波把背景中远距离的杂点去掉。Halcon 里有个算子叫depth_to_point_cloud做的也是同一件事——把深度图或视差图结合相机内参转成 XYZ 点云原理完全一样只是 OpenCV 用 Q 矩阵Halcon 用相机内参加基线手动展开。生成的点云要落盘成文件方便可视化最简单的格式是 PLY 或者 CSV。PLY 可以用 CloudCompare 直接打开写一个最小可用的 ASCII PLY 头也就十几行代码这一步对验证重建结果的价值极高——用眼睛看比对着数字猜强太多。4.4 体积计算凸包快但偏大切片积分稳但慢点云体积计算在资源里是压轴一步做法主要有两种。第一种是对整片点云做凸包然后求凸包体积这个方法在物体表面没有大的凹陷时速度极快但对于碗、盒、瓶子这类带内腔的物体凸包会把凹进去的部分也当成实体体积包进去结果系统性偏大。第二种是切片积分沿某个坐标轴把点云切成薄片每片在截面方向求二维凸包面积乘以片厚再累加。对大多数工业测量场景切片法更贴近真实占据空间的定义double calcVolumeBySlicing(const std::vectorcv::Point3f cloud, float slice 2.0f) // 片厚 2mm可调 { if (cloud.empty()) return 0.0; float zMin 1e9f, zMax -1e9f; for (const auto p : cloud) { zMin std::min(zMin, p.z); zMax std::max(zMax, p.z); } double volume 0.0; for (float z zMin; z zMax; z slice) { std::vectorcv::Point2f pts; pts.reserve(2048); // 取当前切片范围内的点z 在 [z, zslice) 之间 for (const auto p : cloud) { if (p.z z p.z z slice) pts.emplace_back(p.x, p.y); } if (pts.size() 3) continue; // 不足 3 个点忽略该片 std::vectorcv::Point2f hull; cv::convexHull(pts, hull); // 每片做二维凸包 volume cv::contourArea(hull) * slice; // 面积 * 片厚 } return volume; }片厚slice的选择直接影响结果片厚太大截面内的点分布被过度平滑起伏细节消失片厚太小点云密度不够空片增多积分结果波动。我一般先统计点云沿 Z 方向的密度保证每片内有 100 个以上有效点再反推片厚。对一个大致的规则物体这个方法的误差可以控制在 3% 以内前提是点云完整覆盖了物体表面且没有大面积空洞——底部没有扫描到的情况切片法只能算出可见部分的体积这是物理限制不是算法问题。5. 避坑与排查标定板、极线、视差和体积计算的四段翻车记录5.1 角点数量填错标定结果整体漂移现象findChessboardCorners频繁返回false偶尔成功一两次或者标定出来的cx、cy严重偏离图像中心比如 1280x720 的图像主点跑到 (700, 500)。原因把棋盘格的格子数当成了内角点数传给boardSize。10x7 的格子内角点是 9x6这是 cornerSubPix 和 calibrateCamera 的前提错一个数字整套计算就建立在错误假设上。另一个常见的诱因是标定板本身不平整或者打印后贴在硬纸板上弯曲角点沿曲面排布检测倒是能检测到但畸变系数会被拉偏去补偿弯板。解决统一用一个函数生成对象点矩阵内角点数量直接从boardSize计算不要手工填。每张图像检测前打印一下检测到的角点数量和期望值比对。标定板最好用玻璃基底的瓷板或者铝板打印纸贴硬纸板只适合验证流程不适合严肃标定。5.2 极线校正后图像斜带黑边y 方向还有残差现象remap之后的左右图有明显斜向黑边或者左右同名点在 y 方向上差了 2 像素以上SGBM 匹配结果出现整行的错位条纹。原因最常见的是传给stereoRectify的imageSize和实际remap的图像尺寸不一致。标定用的是采集当时的图像分辨率如果后续换了采集尺寸映射表按旧尺寸生成重采样时坐标全部错位。另一个常见原因是alpha参数设为 0 时黑边被裁剪输出图像尺寸变了但映射表还是按原尺寸算的。解决固定一套采集分辨率标定和应用全程用同一个imageSize。调试时把alpha临时设成 1确认黑边区域里没有有效内容后再切回 0。极线残差用 3.3 节的均值统计去量化超过 1 像素就先回去查分辨率和内参不要急着调 SGBM。5.3 视差图一半是黑的物体表面大面积缺数据现象StereoSGBM::compute跑完视差图里物体表面全是黑点只有边缘零星有效值或者近距离物体能算出视差但稍远一点的背景全部无效。原因numDisparities覆盖范围不够。视差范围取决于最近物体和最远物体的深度近处物体视差大如果搜索范围上限不够近处表面就会失效远处物体视差小如果minDisparity设得太大远处就匹配不到。另外P2如果设得太大低纹理区域的视差平滑惩罚过高也会被整体判为无效。解决先把numDisparities拉到 128blockSize降到 5看整体覆盖是否改善。P2先按32 * blockSize^2设如果物体表面变成一块平板的形状说明 P2 过大在区域间做了过度平滑往下调。还缺数据就检查曝光是否过曝——双目匹配对纹理一致性极其敏感左图过亮右图正常匹配直接失败这时候调参没用先调相机曝光和白平衡。5.4 体积算出来明显偏大尤其是有凹槽的物体现象对一个有明显凹陷的瓶子或盒子做体积计算结果比实际体积大 20%~40%而且误差随物体形状变化没有规律。原因用了全局凸包。凸包的定义是最小凸集它天然忽略凹陷任何凹槽、内腔都会被当成实体补上。如果点云里还带了一部分背景点比如墙面或桌面凸包会把背景也包进去体积进一步虚高。解决换成切片积分并对点云先做一次直通滤波把 Z 方向上超出物体范围的背景点全部切掉。切片内的二维凸包仍会略微高于真实截面但对多数规则物体误差已经可以接受。如果物体是空腔结构比如杯子还需要先判断开口朝哪个方向让切片的法向对齐开口方向否则切出来的截面是环形面积会被高估。5.5 点云整体放大或深度值与实测不符现象重建出的物体宽度是对的但深度方向明显拉长或压缩或者所有三维坐标整体放大 16 倍。原因前者通常是Q矩阵和实际视差单位不匹配后者几乎可以肯定是CV_16S的视差图没除以 16 就送进了reprojectImageTo3D。放大 16 倍这个错误非常隐蔽因为物体形状完全不变只有尺度不对单看点云很难发现拿尺子量一下才知道差在哪。解决定点数转浮点这步写成独立函数并加注释不要顺手写在调参代码里。验证深度时找一个已知距离的平面比如离相机 1m 的墙面算重建点云的 Z 均值和实测距离比对偏差超过 2% 就回头查Q矩阵里的Tx单位和标定板的squareSize单位是否一致——这是另一个隐蔽坑棋盘格尺寸填的是英寸还是毫米全看generateObjectPoints里怎么写。6. 进阶玩法用已知尺寸物体验证体积切面积分复核凸包结果整条链路跑通之后最难回答的问题是算出来的体积到底准不准我的习惯是先用一个已知尺寸的规则物体做基准验证再决定要不要相信这套标定和匹配参数。找一个硬纸盒量出长宽高算好理论体积放在相机视场中央拍一段跑完整条管线把重建点云直接拖进 CloudCompare 看形状对不对再用calcVolumeBySlicing算体积。理论值与计算值偏差在 3% 以内才算标定和匹配都通过了。如果体积偏差偏大我会把切片法当作复核工具去对比。给盒子的点云跑一次全局凸包体积OpenCV 里可以对整个点云的 xyz 做convexHull后再用convexHullArea的思路推广到三维再跑一次切片法两者差多少暴露的是物体凹陷程度。凸包比切片大物体就有不可忽略的凹结构切片比理论值小多半是某个表面没有重建出来点云有洞。再进一步还可以在代码里加一个合成点云自检手工生成一个 100x80x50mm 的长方体点云密度按真实点云的分布撒点喂给体积函数看算法本身有没有系统误差。这个自检建议放在每次修改完标定参数之后强制跑一遍因为改一次内参或 SGBM 参数重建尺度就可能变一次靠肉眼在点云里看不出来只有数值对不上时才意识到又动了哪根弦。从那以后我每次调整参数都强制走一遍合成点云体积加上真实盒子体积两个基线数字不对就回头查映射表和视差转换确实省下了大量在匹配参数里打转的时间。这套代码包的完整流程是从单目标定一路推到体积计算照着stereo_calib_match.cpp的顺序读一遍再改自己相机的参数希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。