基于C++与OpenCV的物体尺寸测量系统:从原理到工业级实现

基于C++与OpenCV的物体尺寸测量系统:从原理到工业级实现
1. 项目概述与核心价值最近在整理一些工业视觉和智能硬件相关的项目资料翻到了一个几年前用C和OpenCV做的物体尺寸测量系统。这个项目虽然技术栈不算新潮但它的核心思路和实现细节对于想入门计算机视觉实际应用或者需要解决具体测量问题的朋友来说依然非常有参考价值。简单来说这个项目就是通过摄像头拍一张照片然后自动识别出照片里的物体并计算出它的实际长宽尺寸单位可以是毫米、厘米等。你可能会问现在不是有很多现成的视觉库甚至手机APP都能测尺寸吗为什么还要自己用C和OpenCV从头搞这里面的区别就在于“可控性”和“精度”。现成的工具往往是个黑盒你很难知道它测量的原理当环境光线变化、物体颜色与背景对比度不高、或者需要测量非规则物体时这些通用工具的精度就会大打折扣甚至完全失效。而自己动手实现你可以精确控制每一个环节从图像预处理去噪到边缘检测算子的选择再到轮廓拟合的算法最后到像素与实际尺寸的换算逻辑。整个过程透明、可调试、可优化。这对于嵌入式设备集成、工业质检、自动化分拣等对稳定性和精度有要求的场景是必不可少的。这个项目的核心远不止调用几个OpenCV的API那么简单。它涉及到图像处理的全链路如何从原始图像中稳定地提取出目标物体的轮廓如何区分物体轮廓和图像噪声如何确定一个已知尺寸的“参照物”来建立像素与世界单位的比例关系当摄像头不是正对物体拍摄存在透视畸变时又该如何校正这些都是我们在实现过程中必须啃下来的硬骨头。接下来我就把这个项目的完整实现思路、关键代码、踩过的坑以及优化技巧毫无保留地分享出来。无论你是正在学习OpenCV的学生还是需要解决实际测量问题的工程师相信都能从中找到有用的东西。2. 系统整体设计与核心思路拆解2.1 测量原理从像素到真实世界的桥梁整个系统的基石是“透视投影”和“比例换算”。我们摄像头捕捉到的是一幅二维图像图像中物体的尺寸是以像素为单位的。而我们要得到的是真实三维世界中的尺寸单位是毫米或厘米。连接这两者的是一个比例系数通常称为“像素当量”Pixels Per Metric, PPM即每个像素代表多少实际长度。获取这个比例系数最经典的方法就是引入一个“参照物”。这个参照物需要满足两个条件第一它的真实物理尺寸是已知且精确的第二它在拍摄的图像中必须清晰可见并且与待测物体处于同一平面。这是整个测量能够成立的前提。如果参照物和待测物不在一个平面上由于透视原理它们的缩放比例会不同测量结果将严重失真。常见的参照物是一枚硬币、一张标准尺寸的卡片如信用卡或者在工业场景中直接在载物台上刻一个已知尺寸的标定块。在图像中我们通过图像处理算法检测出这个参照物的轮廓并计算其轮廓的宽度或高度像素值。用参照物的真实物理尺寸除以它的像素尺寸就得到了在当前拍摄姿态和距离下的PPM。注意这里有一个关键细节我们通常使用参照物的“宽度”来计算PPM。因为在实际拍摄中由于镜头畸变或物体摆放轻微倾斜物体的高度和宽度对应的PPM可能会有微小差异。选择一个维度并保持一致能减少误差。通常选择轮廓的外接矩形宽度。2.2 技术选型为什么是C和OpenCVOpenCV它是计算机视觉领域事实上的标准库提供了从图像I/O、矩阵操作、到高级特征检测等几乎全套工具。其函数经过高度优化在x86和ARM平台上都有良好的性能。对于尺寸测量这种涉及大量矩阵和像素级运算的任务OpenCV的Mat类和相关算法效率远高于手动实现。C选择C主要是出于性能和集成度的考虑。在工业边缘计算设备或对实时性要求高的场景如流水线检测C能提供确定性的内存管理和极高的执行效率。此外整个项目逻辑图像采集、处理、计算、输出可以编译成一个独立的可执行文件或库易于嵌入到更大的C系统中无需依赖复杂的Python解释器环境或一堆第三方包。整个系统的处理流程可以概括为以下五个步骤这是一个标准的、鲁棒的视觉测量管线图像采集与输入从摄像头、视频流或图片文件读取图像。图像预处理对原始图像进行降噪、灰度化、二值化等操作突出目标物体与背景的差异为后续检测做准备。轮廓检测与筛选从二值图像中找出所有轮廓然后根据面积、形状、位置等特征从中筛选出参照物轮廓和待测物轮廓。尺寸计算与换算计算筛选出的轮廓的最小外接矩形或旋转矩形得到像素尺寸。利用参照物的像素尺寸和真实尺寸计算PPM再将待测物的像素尺寸乘以PPM得到真实尺寸。结果可视化与输出将检测到的轮廓、外接矩形、尺寸标注等信息绘制到原图上并输出测量结果。3. 核心模块实现与代码详解3.1 环境搭建与项目配置工欲善其事必先利其器。一个清爽的编译环境能避免很多莫名其妙的错误。3.1.1 OpenCV的安装与配置在Windows上我推荐使用vcpkg进行包管理它能自动处理OpenCV复杂的依赖。首先安装vcpkg然后执行vcpkg install opencv4[contrib,nonfree]:x64-windows这条命令会下载并编译包含contrib模块的OpenCV 4。编译时间可能较长但一劳永逸。在Ubuntu等Linux系统上则简单很多sudo apt update sudo apt install libopencv-dev3.1.2 CMake项目配置现代C项目用CMake管理是主流。下面是一个最精简的CMakeLists.txt示例cmake_minimum_required(VERSION 3.10) project(ObjectSizeMeasurement) set(CMAKE_CXX_STANDARD 11) # 查找OpenCV包REQUIRED表示必须找到 find_package(OpenCV REQUIRED) # 包含头文件目录 include_directories(${OpenCV_INCLUDE_DIRS}) # 添加可执行文件将main.cpp编译为measurement add_executable(measurement main.cpp) # 链接OpenCV库 target_link_libraries(measurement ${OpenCV_LIBS})将你的代码放在main.cpp中在项目根目录下执行mkdir build cd build cmake .. make就能生成可执行文件measurement。这种方式的跨平台性最好。3.2 图像预处理为轮廓检测铺平道路原始图像通常包含噪声、光照不均、背景复杂等问题直接进行轮廓检测效果会很差。预处理的目标是得到一个干净的、物体与背景对比鲜明的二值图像。#include opencv2/opencv.hpp #include iostream #include vector using namespace cv; using namespace std; Mat preprocessImage(const Mat src) { Mat gray, blurred, edged; // 1. 转换为灰度图减少计算量很多图像处理操作在单通道上执行 cvtColor(src, gray, COLOR_BGR2GRAY); // 2. 高斯模糊消除高频噪声如传感器噪点、纹理细节。内核大小(5,5)是常用起点。 // 内核越大越模糊但可能丢失边缘。sigmaX0表示根据内核大小自动计算。 GaussianBlur(gray, blurred, Size(5, 5), 0); // 3. 边缘检测这里使用Canny算子。它是经典的梯度-based边缘检测器。 // 参数(30, 150)是低阈值和高阈值。梯度值低于30的认为是非边缘高于150的认为是强边缘。 // 介于两者之间的如果连接到强边缘则认为是边缘否则丢弃。这两个参数需要根据图像调整。 Canny(blurred, edged, 30, 150); // 4. 形态学操作闭合小孔洞连接断开的边缘。 // 这里使用“闭运算”先膨胀后腐蚀能填充轮廓内的小黑洞。 Mat kernel getStructuringElement(MORPH_RECT, Size(5, 5)); morphologyEx(edged, edged, MORPH_CLOSE, kernel); // 5. 膨胀让边缘更粗确保轮廓是闭合的。这对于后续findContours很重要。 dilate(edged, edged, kernel, Point(-1, -1), 2); return edged; // 返回处理好的二值边缘图像 }实操心得Canny算子的两个阈值是最关键的调参点。一个实用的技巧是先设高阈值为中值如用cv::mean计算图像灰度均值低阈值设为高阈值的0.4~0.5倍。也可以考虑用自适应阈值算法但Canny在大多数情况下已经足够好。morphologyEx的核大小也需要根据图像中物体的大小调整太小可能无法闭合缝隙太大则可能使不同物体的边缘粘连。3.3 轮廓发现、筛选与排序从二值图像中我们可以找到很多轮廓但其中大部分是噪声或无用的区域。我们需要从中精准地找到参照物和待测物。vectorvectorPoint findAndFilterContours(const Mat edged, const Mat original, double minArea 500.0, int maxContours 10) { vectorvectorPoint contours; vectorVec4i hierarchy; // 查找轮廓。RETR_EXTERNAL只检测最外层轮廓忽略嵌套轮廓如硬币上的图案。 // CHAIN_APPROX_SIMPLE压缩水平、垂直和对角线段只保留端点节省内存。 findContours(edged.clone(), contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); // 按轮廓面积从大到小排序 sort(contours.begin(), contours.end(), [](const vectorPoint c1, const vectorPoint c2) { return contourArea(c1) contourArea(c2); }); vectorvectorPoint filteredContours; for (size_t i 0; i contours.size() filteredContours.size() maxContours; i) { double area contourArea(contours[i]); if (area minArea) { continue; // 面积太小的认为是噪声 } // 计算轮廓周长用于后续判断形状 double peri arcLength(contours[i], true); // 多边形逼近用更少的点来近似轮廓。第二个参数是精度通常取周长的百分比。 vectorPoint approx; approxPolyDP(contours[i], approx, 0.02 * peri, true); // 筛选形状我们期望参照物是矩形4个顶点或圆形顶点数较多面积与周长比符合圆特征 // 这里以矩形参照物为例 if (approx.size() 4) { filteredContours.push_back(contours[i]); } // 如果你想支持圆形参照物如硬币可以添加如下判断 // else { // double circularity (4 * CV_PI * area) / (peri * peri); // if (circularity 0.8) { // 接近1表示越圆 // filteredContours.push_back(contours[i]); // } // } } // 在原图上绘制筛选后的轮廓用于调试 Mat contourImage original.clone(); drawContours(contourImage, filteredContours, -1, Scalar(0, 255, 0), 2); imshow(Filtered Contours, contourImage); return filteredContours; }踩坑记录findContours函数会修改输入的图像。所以通常传入edged.clone()或edged.copy()避免原始数据被破坏。另外RETR_EXTERNAL和RETR_LIST的选择很重要。如果参照物是平放在桌面上的一张卡片用EXTERNAL没问题。但如果参照物有孔洞或者待测物体内部有复杂结构可能需要用RETR_LIST获取所有轮廓再根据层级关系分析。3.4 参照物识别与像素当量计算假设我们的参照物是一张标准的信用卡宽85.6mm高54.0mm并且它在图像中是最大的矩形轮廓。// 假设 filteredContours 已经按面积从大到小排序且第一个是参照物 bool calculatePixelsPerMetric(const vectorPoint refContour, float knownWidth, // 参照物已知宽度单位毫米 float pixelsPerMetric) { // 计算轮廓的最小外接旋转矩形 RotatedRect box minAreaRect(refContour); // 获取旋转矩形的四个顶点和尺寸。size.width 和 size.height 是像素值。 // 注意RotatedRect的width和height不总是对应实际的长边和短边需要判断。 Point2f vertices[4]; box.points(vertices); float pixelWidth box.size.width; float pixelHeight box.size.height; // 确保我们总是用较长的边作为“宽度”来计算比例以增加鲁棒性 // 因为参照物可能旋转minAreaRect返回的width不一定对应物理宽度。 // 一个更稳健的做法是用已知的宽高比来匹配。 float aspectRatio knownWidth / 54.0; // 已知高度54mm float contourAspectRatio pixelWidth / pixelHeight; // 判断哪个像素维度对应物理宽度 if (abs(contourAspectRatio - aspectRatio) abs((1.0/contourAspectRatio) - aspectRatio)) { // pixelWidth 更可能对应物理宽度 pixelsPerMetric pixelWidth / knownWidth; } else { // pixelHeight 更可能对应物理宽度 pixelsPerMetric pixelHeight / knownWidth; } cout [INFO] 参照物像素宽度: pixelWidth endl; cout [INFO] 参照物像素高度: pixelHeight endl; cout [INFO] 计算出的像素当量 (PPM): pixelsPerMetric pixels per mm endl; return true; }这里引入了一个关键技巧利用参照物的宽高比来判断哪个像素维度对应真实的宽度。因为minAreaRect返回的矩形是能包围轮廓的最小矩形它的width和height是矩形的两边但不一定与物体本身的宽高方向对齐。通过比较轮廓像素宽高比与真实宽高比的接近程度可以更准确地匹配从而提高PPM的计算精度。3.5 待测物体尺寸测量与结果绘制有了PPM测量任何在同一平面上的物体就变得简单了。void measureAndDrawObjects(Mat image, const vectorvectorPoint objectContours, float pixelsPerMetric, const Scalar color Scalar(0, 0, 255)) { for (const auto contour : objectContours) { // 为每个物体计算最小外接矩形可以是旋转矩形 RotatedRect objBox minAreaRect(contour); Point2f objVertices[4]; objBox.points(objVertices); // 计算像素尺寸取矩形的长和宽 float objPixelWidth objBox.size.width; float objPixelHeight objBox.size.height; // 转换为真实尺寸毫米 float objWidthMm objPixelWidth / pixelsPerMetric; float objHeightMm objPixelHeight / pixelsPerMetric; // 在图像上绘制旋转矩形 for (int i 0; i 4; i) { line(image, objVertices[i], objVertices[(i1)%4], color, 2); } // 计算文本显示位置矩形中心偏上 Point2f center objBox.center; int textY static_castint(center.y) - 10; if (textY 20) textY static_castint(center.y) 20; // 如果太靠上就显示在下面 // 构建尺寸文本 string widthText format(W: %.1fmm, objWidthMm); string heightText format(H: %.1fmm, objHeightMm); // 绘制文本分两行显示 putText(image, widthText, Point(center.x - 40, textY), FONT_HERSHEY_SIMPLEX, 0.5, color, 2); putText(image, heightText, Point(center.x - 40, textY 20), FONT_HERSHEY_SIMPLEX, 0.5, color, 2); // 在控制台输出 cout [MEASURE] 物体尺寸: objWidthMm mm x objHeightMm mm endl; } }3.6 主程序流程整合将以上所有模块串联起来形成一个完整的工作流。int main(int argc, char** argv) { // 1. 读取图像 Mat image imread(test_image.jpg); // 替换为你的图片路径 if (image.empty()) { cerr 错误无法加载图像 endl; return -1; } // 为了处理速度可以按比例缩放图像。保持宽高比。 float scale 800.0 / image.cols; // 将宽度缩放到800像素 if (scale 1.0) { resize(image, image, Size(), scale, scale); } Mat original image.clone(); // 保留一份原图副本用于显示 // 2. 图像预处理 Mat edged preprocessImage(image); imshow(Edged Image, edged); // 3. 查找并筛选轮廓 vectorvectorPoint allContours findAndFilterContours(edged, original, 500.0, 5); if (allContours.size() 2) { // 至少需要一个参照物和一个待测物 cerr 错误未检测到足够数量的轮廓。 endl; waitKey(0); return -1; } // 4. 假设第一个轮廓面积最大是参照物 vectorPoint referenceContour allContours[0]; float knownWidth 85.6; // 信用卡宽度单位毫米 float pixelsPerMetric; if (!calculatePixelsPerMetric(referenceContour, knownWidth, pixelsPerMetric)) { cerr 错误参照物识别或PPM计算失败。 endl; waitKey(0); return -1; } // 5. 测量其他物体从第二个轮廓开始 vectorvectorPoint objectContours(allContours.begin() 1, allContours.end()); measureAndDrawObjects(original, objectContours, pixelsPerMetric); // 6. 在参照物上也标注一下方便核对 RotatedRect refBox minAreaRect(referenceContour); Point2f refVerts[4]; refBox.points(refVerts); for (int i 0; i 4; i) { line(original, refVerts[i], refVerts[(i1)%4], Scalar(255, 0, 0), 2); // 用蓝色画参照物 } putText(original, Reference, refBox.center, FONT_HERSHEY_SIMPLEX, 0.6, Scalar(255, 0, 0), 2); // 7. 显示最终结果 imshow(Measurement Result, original); // 可选保存结果图像 // imwrite(result.jpg, original); waitKey(0); return 0; }4. 精度提升关键与高级技巧基础版本能跑通但要想获得稳定、高精度的测量结果还需要在以下几个地方下功夫。4.1 相机标定与畸变校正上面的方法假设相机是理想的小孔成像模型且物体与像平面平行。但实际相机镜头存在畸变尤其是广角镜头主要有径向畸变图像边缘弯曲和切向畸变。这会直接影响像素坐标的准确性。解决方案是进行相机标定。OpenCV提供了完整的标定流程打印一张棋盘格标定板。从不同角度拍摄10-20张标定板图片。使用cv::findChessboardCorners检测角点。使用cv::calibrateCamera计算相机内参矩阵、畸变系数等。在测量前使用cv::undistort函数对图像进行畸变校正。经过校正的图像其像素坐标更符合线性投影关系能显著提升远处或图像边缘物体的测量精度。这是工业级应用必不可少的步骤。4.2 透视变换与鸟瞰图转换即使校正了畸变如果摄像头不是正对着物体平面拍摄而是有一个倾斜角度那么图像中的物体会发生透视变形近大远小。此时用同一个PPM去测量图像中不同位置的物体或者测量一个非矩形的四边形物体误差会很大。解决方案是进行透视变换Perspective Transformation。如果我们能获取物体所在平面的四个角点例如一个矩形的托盘或工作区域就可以通过cv::getPerspectiveTransform计算一个变换矩阵然后使用cv::warpPerspective将图像变换为“鸟瞰图”。在鸟瞰图中物体的像素尺寸与实际物理尺寸的比例是恒定的测量精度最高。// 假设我们已知参照物是一个矩形并且我们检测到了它的四个角点pts_src像素坐标 // 我们也知道这个矩形在真实世界中的长宽例如 85.6mm x 54.0mm可以定义目标坐标pts_dst vectorPoint2f pts_src { ... }; // 从图像中检测到的参照物四个顶点 vectorPoint2f pts_dst; float ppm 10.0; // 假设我们期望在鸟瞰图中1像素0.1mm则ppm10 pixels/mm pts_dst.push_back(Point2f(0, 0)); pts_dst.push_back(Point2f(85.6 * ppm, 0)); pts_dst.push_back(Point2f(85.6 * ppm, 54.0 * ppm)); pts_dst.push_back(Point2f(0, 54.0 * ppm)); Mat M getPerspectiveTransform(pts_src, pts_dst); Mat warped; warpPerspective(original, warped, M, Size(85.6*ppm, 54.0*ppm)); // 现在在warped图像中参照物被“拉直”并缩放到正确比例。 // 此时该图像中任何物体的像素尺寸除以ppm就能得到真实尺寸且整个平面测量一致。这个技巧在文档扫描、仓储包裹尺寸测量等场景非常有用。4.3 参照物的自动识别与匹配之前的代码假设第一个轮廓就是参照物。在实际应用中我们需要更鲁棒的方法来自动识别参照物。有几种思路形状与颜色编码使用特定形状如阿鲁科标记、AprilTag或颜色的参照物。可以先通过颜色阈值分割cv::inRange提取特定颜色的区域再检测形状。特征匹配如果参照物有丰富的纹理可以使用特征检测器如SIFT, SURF, ORB提取特征并与模板进行匹配cv::BFMatcher或cv::FlannBasedMatcher。二维码/条形码直接在参照物上打印二维码其中编码其真实尺寸信息。使用cv::QRCodeDetector检测并解码一举两得地获得参照物位置和尺寸信息。这是最专业和可靠的做法。4.4 光照变化与背景干扰的应对光照不均或复杂背景是视觉项目的永恒挑战。自适应阈值用cv::adaptiveThreshold替代全局阈值它对局部光照变化更鲁棒。背景减除如果相机固定可以拍摄一张背景图无物体然后用当前帧减去背景得到前景物体。OpenCV有cv::createBackgroundSubtractorMOG2等高级算法。使用特定光源在工业环境中这是最有效的方法。使用环形光源、同轴光源或背光源可以创造高对比度、阴影少的成像环境。5. 常见问题排查与性能优化5.1 轮廓检测不到或检测错误问题现象可能原因排查与解决方法完全检测不到轮廓1. 预处理后的二值图像全黑或全白。2.Canny阈值设置过高。3. 物体与背景对比度太低。1. 使用imshow逐步检查gray,blurred,edged图像看哪一步出了问题。2. 动态调整Canny阈值或尝试使用cv::threshold手动二值化。3. 尝试彩色空间转换如在HSV空间根据颜色分割。轮廓不闭合断断续续1. 边缘太细或梯度不够强。2. 形态学操作的核太小。1. 降低Canny的低阈值或先做一次膨胀操作再边缘检测。2. 增大dilate或morphologyEx的核大小。多个物体轮廓粘连在一起1. 物体靠得太近。2. 预处理中的膨胀操作过度。1. 尝试在边缘检测后先进行腐蚀操作分离物体再进行轮廓查找。2. 使用分水岭算法进行分割。检测到大量小噪声轮廓1. 图像噪声多。2. 面积过滤阈值minArea设置太小。1. 增加高斯模糊的核大小。2. 使用中值滤波cv::medianBlur去除椒盐噪声。3. 适当提高minArea值。5.2 尺寸测量结果不稳定或误差大问题现象可能原因排查与解决方法同一物体多次测量结果波动1. 轮廓拟合不稳定边缘有毛刺。2. 参照物识别有轻微偏差。1. 增加approxPolyDP的精度参数或对轮廓点进行平滑滤波。2. 对参照物轮廓进行多次测量取平均。3. 使用cv::fitEllipse拟合圆或椭圆可能比矩形更稳定。测量值系统性偏大或偏小1. 参照物真实尺寸输入错误。2. 参照物与待测物不在同一平面。3. 相机镜头畸变未校正。1. 反复核对参照物尺寸。2. 确保拍摄时参照物与待测物紧贴同一平面。3. 进行相机标定和畸变校正。物体在图像边缘处测量误差大镜头畸变在边缘最明显。必须进行相机标定并使用undistort校正整个图像。长条形物体测量不准minAreaRect对于长宽比极大的矩形其角度和中心点计算可能不稳定。考虑使用cv::fitLine拟合中心线来测量长度或者确保物体摆放角度不要接近45度。5.3 性能优化技巧当需要处理视频流或高分辨率图像时性能成为关键。降低分辨率如主程序所示在不显著影响测量精度的前提下先将图像缩放到一个固定尺寸如800px宽能极大减少后续计算量。设定ROI如果物体总是出现在图像的特定区域使用cv::Rect定义感兴趣区域只处理这一小块图像。使用灰度图尽早转换为灰度图所有预处理和轮廓检测都在单通道上进行。选择更快的轮廓模式CHAIN_APPROX_SIMPLE比CHAIN_APPROX_NONE占用内存少处理更快。并行处理如果有多核CPU可以使用OpenCV的并行框架如cv::parallel_for_或TBB库来并行处理多个检测区域。6. 项目扩展与工程化思考这个基础项目可以朝多个方向扩展以适应更复杂的实际需求。6.1 多物体与动态测量当前的循环可以处理多个物体。要测量视频中的物体只需将主循环放入while中从cv::VideoCapture读取每一帧。注意在动态场景中可能需要引入跟踪算法如KCF, CSRT来稳定物体ID避免帧间跳变。6.2 与硬件集成触发拍照通过GPIO或串口接收传感器信号如光电开关触发相机拍照实现与流水线节拍同步。结果输出将测量结果通过串口、网络TCP/UDP或IO口发送给PLC或机械臂实现自动分拣或剔除。6.3 图形用户界面使用Qt、ImGui或OpenCV自带的HighGUI简单控件可以制作一个方便操作员使用的界面用于设置参数如Canny阈值、minArea、选择参照物、查看实时画面和结果日志。6.4 数据记录与统计分析引入数据库如SQLite或日志文件记录每一张图片的测量结果、时间戳、图像路径。这对于生产质量追溯和统计过程控制至关重要。可以计算CPK等统计指标监控测量系统的稳定性和能力。最后我想强调的是视觉测量是一个“系统工程”代码实现只是其中一环。至少一半的精度和稳定性来自于光、机、电的配合稳定的光源、合适的镜头焦距、牢固的机械结构、干净的拍摄环境这些因素的重要性不亚于算法本身。在动手写代码之前花时间把这些硬件条件调试好往往能事半功倍。这个项目给我最深的体会就是把问题简化、约束条件明确化是成功实现一个鲁棒视觉系统的关键。与其追求一个能应对所有复杂情况的万能算法不如先设计一个能让问题变简单的拍摄场景。