ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Qt自研卡尺控件:实现VisionPro式视觉测量交互

Qt自研卡尺控件:实现VisionPro式视觉测量交互 简介本资源是一套基于Qt框架实现的VisionPro风格工业视觉卡尺控件库面向机器视觉开发工程师、自动化检测系统开发者及Qt中级以上学习者解决在自研检测软件中快速集成专业级测量交互工具的需求。控件通过重写QGraphicsItem实现完整支持矩形卡尺拖动、缩放、旋转、斜切、直线卡尺拉伸、多矩形设置、缩放与斜切及圆形卡尺圆弧拖拉、补全圆、多矩形配置、缩放具备高复用性与工程适配性。压缩包共12个文件含5个核心功能实现cpp文件、4个对应头文件如mygraphicCaliperitem.h/.cpp、mygraphicShapeitem.h/.cpp、2个UI界面文件mainwindow.ui、formcaliperpara.ui及1个Qt项目配置文件.pro总大小仅30KB轻量紧凑且结构清晰。已有2159人学习下载提供可直接编译运行的完整工程包含参数配置窗体、图形项管理模块与主窗口集成示例便于快速理解卡尺交互逻辑与Qt图形视图框架深度应用。 做视觉测量的上位机开发绕不开卡尺工具。康耐视VisionPro里的Caliper工具用起来确实方便但当你需要在自研的Qt程序里实现同样的测量交互——拖一个矩形ROI、拖动卡尺线、实时看边缘点反馈——就会意识到这套东西不是简单调个OpenCV函数能替代的。我花了两周时间在Qt环境里把矩形、直线、圆形三种卡尺控件完整做了一遍整体架构、核心算法和踩过的坑都在这篇文章里。这篇文章适合两类人一类是正在做视觉检测上位机、想把VisionPro的交互体验移植到自研软件里的工程师另一类是刚接触机器视觉测量、想理解卡尺工具底层原理的Qt开发者。读完之后你不仅能写出可运行的卡尺控件还能明白每一个参数背后的物理意义遇到问题知道从哪儿下手排查。1. 卡尺工具的核心原理与方案选型1.1 VisionPro的Caliper原理拆解VisionPro的CogCaliperTool表面上是拖一个ROI然后输出边缘点坐标但它内部的处理管线非常清晰拆开来看一共四步第一步ROI几何转探针。ROI不是一个完整的测量区域而是一组有方向的采样线段VisionPro里叫probe。每条探针线就是一个一维的灰度采样窗口窗口有宽度、有搜索方向、有搜索长度。第二步灰度剖面提取。沿探针方向把像素灰度值取出来得到一个一维数组。由于单像素采样噪声大实际取的是探针宽度范围内的灰度平均值相当于先做了一次均值滤波。第三步边缘检测。对这个灰度数组做一阶差分找到梯度绝对值最大的位置。梯度方向配合用户设置的极性由暗到亮或由亮到暗决定最终接受的边缘点。超过阈值才算有效边缘。第四步后处理。所有有效边缘点连起来做直线拟合、圆拟合、距离计算输出最终测量值。这个流程的可贵之处在于解耦ROI只负责生成探针检测算法只负责在探针上找点。如果理解了这一点自己实现时就不会把几何逻辑和图像处理逻辑揉在一起。1.2 三种卡尺ROI的几何模型矩形、直线、圆形三种卡尺本质区别只在探针的生成方式后面的边缘检测完全共用同一套代码。ROI类型几何参数探针生成方式搜索方向典型用途矩形卡尺中心点、矩形宽高、旋转角度沿矩形长边等间距分布垂直于长边方向测宽度、找直线边缘直线卡尺起点、终点、直线带宽沿中心线等间距分布垂直于中心线截面测量、边缘位置定位圆形卡尺圆心、半径、起始角、结束角、环宽沿圆周等角度分布径向圆心到圆周方向测直径、找圆心矩形卡尺和直线卡尺看起来很像但使用场景不同矩形卡尺适合在图像中固定一个矩形搜索区域搜索长边两侧的边缘常用于测量物体宽度直线卡尺适合沿一条已知直线方向做剖面扫描比如测量PCB板边缘的高度差或者定位焊点边界。圆形卡尺稍微特殊一点探针线沿圆周均匀排布搜索方向是径向。每条探针都能找到一个边缘点这些点分布在圆周上拟合后就能得到准确的圆心坐标和半径。测量圆形工件外径、内径、孔位偏移都是这个工具的典型应用。1.3 为什么选择自研而非直接调OpenCV有的朋友可能会问OpenCV里有Canny边缘检测有fitLine、fitEllipse拟合函数为什么不直接拼一下我的体会是算法层面的确可以拼但卡尺控件不只是一堆算法函数它还包括交互、渲染、参数反馈三块。OpenCV不负责画可拖拽的ROI框不负责在图像上实时叠加卡尺线和边缘点更不提供鼠标拖拽的命中判定逻辑。VisionPro的价值恰恰在前端的交互体验和工具化封装不只是背后的测量算法。另外一个现实问题是授权。在商业项目里使用康耐视的CogToolBlock需要购买运行时授权而且整个工具链绑定Cognex的架构。如果上位机只是标准Qt应用要嵌入VisionPro的控件得装额外组件在客户现场部署时很麻烦。自研卡尺控件算法全部掌握在自己手里调试时能看到每一步中间数据扩展新功能也方便。当然自研不是从零造轮子。边缘检测的数学原理、直线拟合的最小二乘方法都是经典知识我只需要把它们用Qt的C代码组织起来再配合QGraphicsView做交互层就能得到一个足够用的控件。1.4 整体架构与处理流程整个控件的架构分成四层图像显示层用QGraphicsView和QGraphicsScene承载图像负责缩放和平移这部分Qt已经封装得很好。交互层是一组自定义的QGraphicsItem负责绘制ROI和卡尺线处理鼠标拖拽事件。算法层是纯C类不依赖任何UI库输入图像和参数输出结果。这一层方便单元测试也可以移植到控制台程序里跑批处理。数据层用结构体定义参数和结果作为各层之间传递数据的标准接口。一次完整的测量流程是这样的用户在图像上拖动ROI鼠标释放后触发测量信号算法层读取当前图像、ROI参数和卡尺参数执行探针生成、边缘检测、拟合计算把结果写回数据层最后交互层刷新卡尺线、边缘点标记和测量值文字。2. 工程结构设计与核心类实现2.1 Qt工程的组织方式我建议的工程结构caliper_demo/ ├── CMakeLists.txt ├── src/ │ ├── core/ # 与UI无关的算法层 │ │ ├── CaliperTypes.h │ │ ├── CaliperEngine.h/cpp │ │ ├── ROIBase.h/cpp │ │ ├── RectROI.h/cpp │ │ ├── LineROI.h/cpp │ │ └── CircleROI.h/cpp │ ├── ui/ # 交互层 │ │ ├── ImageView.h/cpp │ │ ├── ROIGraphicsItem.h/cpp │ │ └── ResultGraphicsItem.h/cpp │ └── main.cppcore目录下所有文件不引用QtGui里的控件类最多用QImage、QPointF这些基本数据类型。这样做的好处是算法层可以脱离界面单独测试QImage可以直接从QImage::bits()拿到像素指针不用关心QGraphicsView怎么显示。编译配置上我用CMake组织工程Qt5或Qt6都兼容。只需要Core和Gui两个模块Widgets用来承载QMainWindow。如果以后要扩展成QML界面core部分也能直接复用。2.2 数据模型参数与结果结构体参数结构体是用户和卡尺工具之间的交互接口每个字段都要有明确的物理意义。// CaliperTypes.h enum class EdgePolarity { Any, // 任意方向取梯度绝对值最大 DarkToLight, // 由暗到亮灰度上升沿 LightToDark // 由亮到暗灰度下降沿 }; struct CaliperParams { int sampleCount 20; // 探针数量 int projectionWidth 5; // 投影宽度像素做平滑用 int searchRange 15; // 搜索范围半长像素 double edgeThreshold 25.0; // 梯度阈值 EdgePolarity polarity EdgePolarity::Any; bool useSubPixel true; // 是否亚像素定位 }; struct CaliperPoint { QPointF pos; // 亚像素边缘点坐标 double score; // 梯度强度 bool valid; // 是否通过阈值筛选 }; struct CaliperResult { QListCaliperPoint points; // 全部有效边缘点 QLineF fittedLine; // 直线拟合结果 QPointF circleCenter; // 圆拟合圆心 double circleRadius 0.0; // 圆拟合半径 bool lineValid false; bool circleValid false; double measuredDistance 0.0; // 距离测量值 };在设计时我特意把“用户参数”和“测量结果”分成两个独立结构体这样参数面板和结果显示面板可以分别绑定数据源互不干扰。CaliperParams里的projectionWidth和searchRange是视觉测量中最关键的两个参数后面算法章节会详细讲它们的作用。2.3 ROI基类与派生类的接口设计ROI类的设计是整个控件扩展性的关键。我定义了一个抽象基类派生类只需要实现几何逻辑。// ROIBase.h class ROIBase { public: virtual ~ROIBase() default; // 由ROI几何生成一组探针线 virtual QListQLineF generateProbes(const CaliperParams params) const 0; // 命中测试鼠标点是否在ROI内部 virtual bool contains(const QPointF pos) const 0; // 获取ROI对应的图形区域用于绘制 virtual QPainterPath shape() const 0; // 设置/获取ROI的几何参数 virtual void setGeometry(const QVariantMap geo) 0; virtual QVariantMap geometry() const 0; // 平移、旋转、缩放 virtual void translate(const QPointF offset) 0; virtual void rotate(qreal angle) 0; virtual void scale(qreal factor) 0; };generateProbes是连接UI和算法的桥梁。测量引擎拿到一组QLineF每条线代表一条探针线的起点和终点定义了搜索区间线的方向就是搜索方向。矩形ROI生成探针时先算长边方向轴从矩形一端沿轴等距移动每个位置生成一条垂直于轴的探针线。// RectROI.cpp 核心探针生成逻辑 QListQLineF RectROI::generateProbes(const CaliperParams params) const { QListQLineF probes; QPointF axis(cos(m_angle), sin(m_angle)); QPointF normal(-axis.y(), axis.x()); double scanLength m_height; // 搜索方向的总长度 double halfLength scanLength / 2.0; double step m_width / (params.sampleCount 1); QPointF start m_center - axis * (m_width / 2.0); for (int i 1; i params.sampleCount; i) { QPointF center start axis * (step * i); QPointF p1 center - normal * halfLength; QPointF p2 center normal * halfLength; probes.append(QLineF(p1, p2)); } return probes; }这里要注意一个细节探针数量等于sampleCount步长是矩形宽度除以sampleCount1这样探针不会贴到矩形边缘避免边缘像素干扰。2.4 测量引擎的流程编排测量引擎是算法层的核心它把各环节串起来class CaliperEngine { public: void setImage(const QImage image); CaliperResult execute(const ROIBase *roi, const CaliperParams params); private: std::vectordouble extractProfile(const QImage img, const QLineF probe, const CaliperParams params); std::vectordouble computeGradient(const std::vectordouble profile); CaliperPoint findEdge(const std::vectordouble profile, const std::vectordouble gradient, const CaliperParams params); QImage m_image; };execute方法先调用roi-generateProbes得到所有探针然后对每条探针执行extractProfile、findEdge收集所有有效边缘点最后根据卡尺类型决定是否做直线或圆拟合以及距离计算。这个流程看起来简单但有一个值得强调的设计execute不应在UI线程执行。当我处理一张12兆像素的工业相机图像20条探针、每条探针搜索范围30像素时计算量不大但如果后面扩展成几百条探针或者大尺寸ROIUI卡顿就会很明显。所以我把CaliperEngine设计成可复用的纯计算类UI层用QtConcurrent::run或QThreadPool调用它完成后通过信号槽更新界面。3. 边缘检测与测量算法实现细节3.1 探针线的生成与参数映射生成探针线时几个关键参数的映射关系需要理清。第一步探针线的长度由searchRange决定。searchRange是“搜索范围半长”意思是探针中心到两端各延伸searchRange个像素所以探针总长是2*searchRange。这个值太小会漏掉边缘太大会增加计算量并引入附近无关边缘的干扰。第二步探针的密度由sampleCount决定。sampleCount越多采到的边缘点越多拟合更稳但计算量也线性增加。实际项目中矩形卡尺的sampleCount我通常设置在10到30之间圆形卡尺根据圆周长调整保证相邻探针的夹角在5度左右。第三步探针的方向必须精确垂直于被测边缘。矩形卡尺中探针方向就是矩形短边方向圆形卡尺中探针方向严格径向。如果方向偏了灰度剖面会变平缓梯度峰值变小容易导致漏检。这一点在带角度的矩形ROI上尤其要小心生成探针时务必使用单位向量。3.2 灰度剖面提取与平滑探针线定义了搜索区域接下来要在这个区域内提取灰度数组。由于探针方向不一定是水平或垂直的边缘点落在像素网格中间所以必须做双线性插值。double bilinearSampleGray(const QImage img, double x, double y) { if (x 0 || y 0 || x img.width() || y img.height()) return 0.0; int x0 (int)x, y0 (int)y; int x1 qMin(x0 1, img.width() - 1); int y1 qMin(y0 1, img.height() - 1); double fx x - x0, fy y - y0; double v00 qGray(img.pixel(x0, y0)); double v10 qGray(img.pixel(x1, y0)); double v01 qGray(img.pixel(x0, y1)); double v11 qGray(img.pixel(x1, y1)); return (1-fx)*(1-fy)*v00 fx*(1-fy)*v10 (1-fx)*fy*v01 fx*fy*v11; }灰度剖面提取时projectionWidth的作用就体现了。每条探针不是单像素线而是有宽度的采样带。宽度方向垂直于搜索方向取带内所有像素灰度的平均值等效于在空间上做均值滤波能显著抑制传感器噪声。实际代码里我在每条探针的每个位置沿探针方向的法线方向取projectionWidth个点的平均值。5像素的投影宽度在大多数场景下效果都不错宽度再大边缘会被抹得太平亚像素精度反而下降。3.3 一阶导数边缘检测与亚像素定位拿到灰度剖面数组后计算一阶差分std::vectordouble computeGradient(const std::vectordouble profile) { std::vectordouble grad(profile.size() - 1); for (size_t i 0; i 1 profile.size(); i) { grad[i] profile[i 1] - profile[i]; } return grad; }梯度数组里每个位置代表剖面中相邻两个像素之间的灰度变化速率。边缘点就是梯度绝对值的局部最大值位置。极性过滤在这时候起作用如果设置DarkToLight只找梯度大于阈值且为正的局部最大值LightToDark则相反Any模式取绝对值的局部最大值。亚像素定位是卡尺工具精度的重要来源。像素级边缘只在整数位置但真实边缘可能落在两个像素之间所以需要插值。我常用抛物线拟合法取梯度最大的位置及其左右各一点用这三个点拟合一条抛物线抛物线顶点就是亚像素边缘位置。double refineSubPixel(double gPrev, double gCur, double gNext) { double denom gPrev - 2.0 * gCur gNext; if (qAbs(denom) 1e-9) return 0.0; return 0.5 * (gPrev - gNext) / denom; }返回值是相对于gCur位置的偏移量范围通常在-0.5到0.5像素之间。加上整数位置就是最终亚像素边缘坐标。实测下来这种插值方式在理想边缘上可以做到0.1像素左右的重复精度足够满足大部分尺寸测量需求。3.4 直线拟合与圆拟合获取所有有效边缘点后根据卡尺类型做拟合。直线拟合我用主成分分析法比最小二乘ykxb更稳定因为可以处理垂直线方向。QLineF fitLinePCA(const QListCaliperPoint pts) { double n pts.size(); double sx 0, sy 0; for (const auto p : pts) { sx p.pos.x(); sy p.pos.y(); } double mx sx / n, my sy / n; double sxx 0, sxy 0, syy 0; for (const auto p : pts) { double dx p.pos.x() - mx, dy p.pos.y() - my; sxx dx * dx; sxy dx * dy; syy dy * dy; } double theta 0.5 * std::atan2(2.0 * sxy, sxx - syy); QPointF dir(std::cos(theta), std::sin(theta)); return QLineF(mx, my, mx dir.x(), my dir.y()); }圆的拟合我用Kasa法它是代数圆拟合中最简单的一种最小化代数距离平方QPointF fitCircleKasa(const QListCaliperPoint pts, double outRadius) { int n pts.size(); if (n 3) return QPointF(); double sx 0, sy 0, sxx 0, syy 0, sxy 0; double sx3 0, sy3 0, sxy2 0, sx2y 0; for (const auto p : pts) { double x p.pos.x(), y p.pos.y(); double x2 x*x, y2 y*y; sx x; sy y; sxx x2; syy y2; sxy x*y; sx3 x2*x; sy3 y2*y; sxy2 x*y2; sx2y x2*y; } double A n*sxx - sx*sx; double B n*sxy - sx*sy; double C n*syy - sy*sy; double D 0.5 * (n*sx3 - sx*sxx n*sxy2 - sx*syy); double E 0.5 * (n*sy3 - sy*syy n*sx2y - sy*sxx); double det A*C - B*B; if (qAbs(det) 1e-12) return QPointF(); double cx (D*C - B*E) / det; double cy (A*E - B*D) / det; outRadius 0.0; for (const auto p : pts) { double dx p.pos.x() - cx, dy p.pos.y() - cy; outRadius std::sqrt(dx*dx dy*dy); } outRadius / n; return QPointF(cx, cy); }Kasa法对圆弧不完整的情况比较敏感如果被测圆弧小于90度拟合结果会偏向圆弧内侧。更精确的做法是用几何拟合迭代优化但工业现场多数场景是完整圆或大半圆Kasa法配合RANSAC筛选离群点已经够用。4. Qt交互控件与图形渲染实现4.1 QGraphicsView的图层与场景组织交互控件建立在QGraphicsView框架上。用三个图层管理不同内容最底层是QGraphicsPixmapItem显示图像。中间是ROI交互层放矩形、直线、圆形卡尺的图形项。最上层是测量结果层显示卡尺探针线、边缘点标记、测量文字。分层的好处是刷新互不影响。拖动ROI时只移动中间层的item计算结果刷新时只需要清空和重建顶层的结果item。如果混在同一层图像缩放或平移时所有item都会重绘效率低。缩放显示是个容易忽略的细节。QGraphicsView默认的setRenderHint(QPainter::SmoothPixmapTransform)会让图像在缩小时看起来模糊测量时最好关闭这个选项保持像素级显示。我习惯自定义PicZoomView重写wheelEvent以鼠标位置为锚点缩放以左键拖动为平移。4.2 自定义ROI图形项的绘制ROI图形项继承QGraphicsItempaint函数负责绘制整个卡尺的视觉反馈。void ROIGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option); Q_UNUSED(widget); painter-save(); painter-setRenderHint(QPainter::Antialiasing, true); // 绘制ROI区域 QPen pen(roiColor); pen.setWidth(2); painter-setPen(pen); painter-setBrush(QBrush(QColor(roiColor.red(), roiColor.green(), roiColor.blue(), 30))); painter-drawPath(roi-shape()); // 绘制探针线 QPen probePen(QColor(0, 255, 0), 1); painter-setPen(probePen); const auto probes roi-generateProbes(params); for (const auto line : probes) { painter-drawLine(line); } painter-restore(); }探针线的颜色我用亮绿色ROI框用亮蓝色或橙色。需要保证在暗色背景和亮色背景图像上都清晰可辨。边缘点标记画在结果层用红色十字叉表示方便用户直观看到测量点。4.3 鼠标交互拖拽、缩放与旋转交互细节决定工具是否好用。我的实现里ROI图形项支持三种操作整体拖动、调整宽度/高度、旋转。整体拖动最简单mouseMoveEvent里把鼠标偏移量映射到场景坐标调用roi-translate。调整尺寸需要判断鼠标命中的位置。我在ROI的四个角和四条边中点放置句柄点鼠标按下时计算命中最近句柄进入对应的调整模式。句柄大小不能只看像素要考虑视图缩放比例视图放大后句柄跟着变大否则用户很难点中。旋转处理最容易出错。旋转时以ROI中心点为旋转中心让当前鼠标位置与中心点的连线角度作为ROI的新角度。注意处理角度环绕问题比如从-170度转到170度如果直接相减偏转量是-340度用户感觉是反向转。我用的方法是把角度差限制在-180到180之间。下面是鼠标旋转的核心逻辑void ROIGraphicsItem::rotateTo(const QPointF pos, const QPointF center) { double newAngle std::atan2(pos.y() - center.y(), pos.x() - center.x()); double delta newAngle - currentAngle; while (delta M_PI) delta - 2*M_PI; while (delta -M_PI) delta 2*M_PI; roi-rotate(delta * 180.0 / M_PI); currentAngle delta; }4.4 测量结果叠加显示与刷新策略每次ROI变化或参数改变交互层发出触发测量信号Qt的connect把信号连到measure函数。measure函数在后台线程执行算法完成后通过信号把结果传回UI线程。结果层的内容有三类卡尺探针线中的有效边缘点、拟合出的直线或圆、测量数值文字。探针线中无效边缘点可以用灰色小圆点标记有效边缘点用红色十字叉标记。直线和圆用实线绘制颜色用黄色或白色与边缘点区分开。测量数值用QGraphicsSimpleTextItem显示在ROI附近内容可以是宽度、直径或两个边缘之间的距离。刷新策略方面我在执行测量前给算法层传入当前的ROI参数快照避免后台计算过程中用户拖动ROI导致数据竞争。测量结果也不需要每帧刷新只有ROI或参数变化时才触发实测300毫秒一次的刷新频率足够流畅。5. 常见问题与调试经验5.1 边缘方向和极性配置问题我第一个踩的坑是边缘极性设置错误导致测量值完全不对。在VisionPro里极性表示边缘灰度的变化方向DarkToLight表示边缘从暗过渡到亮LightToDark相反。如果打光条件让目标的边缘灰度和预期相反极性设反就会把整条边漏掉。排查方法是打开梯度曲线调试模式把每条探针的灰度剖面和梯度曲线显示出来。看到曲线后立刻能判断边缘是上升沿还是下降沿梯度峰值是否超过阈值。这个调试功能我用QPainter在一张独立的小图像上绘制开发时特别有用。还有一种情况是图像本身对比度差梯度峰值只有10到20无论怎么调极性都检测不到。这时候先处理图像质量问题比如调整光源、增大曝光而不是勉强压低阈值。5.2 阈值、密度、搜索范围的参数调优参数调优没有万能公式但有基本规律参数设置偏小的影响设置偏大的影响推荐经验值edgeThreshold噪点被当作边缘真实边缘被过滤15-40视对比度而定searchRange边缘超出搜索范围引入相邻干扰边缘边缘到中心距离的1.2倍sampleCount边缘点太少拟合不稳计算量增大直线10-30圆形圆周每5度一个projectionWidth噪声抑制不足边缘被平滑掉3-7像素调试时我习惯把edgeThreshold从高往低调先能看到边缘再看有没有噪点边缘出现。如果清晰边缘的梯度峰值是80阈值设在30左右比较合理留出足够余量。5.3 图像坐标与视图坐标转换用户交互拿到的是视图坐标算法测量需要图像坐标这两者必须清晰区分。QGraphicsView有mapToScene和mapFromScene两个函数完成坐标转换但要注意场景坐标和图像坐标在图片不位移、不缩放时相等一旦view执行了fitInView或用户放缩、平移两者就不一样了。我的处理方式是图像加载后作为场景坐标系的基准把QPixmapItem放在场景坐标(0,0)点不做多余偏移。ROI项存的一律是场景坐标测量时直接从ROI类里读坐标给算法层结果绘制也在场景坐标系完成。鼠标事件拿到的视图坐标先用mapToScene转到场景坐标再参与计算。比较容易出错的是QGraphicsItem里的pos()和scenePos()混用。如果一个item是另一个item的子项pos()是相对父项的坐标。我在设计ROI图形项时统一用scenePos()绘制时也把 painter 的坐标系通过 translate(pos()) 对齐到 item 局部坐标减少混乱。5.4 性能优化与多线程测量当图像分辨率很高比如500万像素以上且ROI内探针数量较多时算法执行时间会明显增加。性能调优我发现两个重点第一避免使用QImage::pixel()取像素这个方法每次调用都有类型检查和函数调用开销。直接用bits()指针加bytesPerLine偏移访问能快3到5倍。注意设置图像格式为QImage::Format_Grayscale8如果原始图是彩色加载时先转换一次。const uchar* linePtr img.constScanLine(y); uchar gray linePtr[x];第二双线性插值会重复计算很多边界条件判断。如果探针方向大多是水平或垂直可以写成两段式先判断是否完全落在图像内再取像素。超出边界的探针直接标记无效不用插值。多线程方面CaliperEngine不持有任何UI对象可以在QThreadPool里安全执行。我用QtConcurrent::run把测量任务丢到线程池主线程通过QFutureWatcher接收完成信号。注意测量结果的QList需要在新线程里构造不能在UI线程和后台线程同时修改。最后再分享一个我在工程里验证很有用的技巧卡尺工具的参数面板和调试信息面板一定要做成独立Docker窗口可以随时打开和关闭。调试时通过参数面板快速调整阈值、密度、搜索范围图像上立刻反馈边缘点的变化这种交互闭环能大幅减少调参时间。做完这套控件后我的常规套路是先固定光源再粗调阈值到边缘稳定可见然后微调投影宽度找到重复精度最好的点最后用标准量块做校正。这个过程里探针线的实时渲染帮了大忙。本文还有配套的精品资源点击获取
返回列表