ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C# OpenCVSharp矩形识别实战:轮廓检测与角点定位

C# OpenCVSharp矩形识别实战:轮廓检测与角点定位 简介C#与OpenCV结合的图像矩形识别工程包适合需要做边缘检测、轮廓提取或几何形状识别的.NET开发者与计算机视觉初学者。资源演示了从图像读取、灰度化、高斯滤波、Canny边缘检测到Hough变换及findContours轮廓筛选的完整流程并包含boundingRect获取边界框和绘制结果的代码实现可直接迁移到工业质检、文档扫描等场景。压缩包共261个文件约76MB以dll依赖库、xml配置文件、cs源码、工程sln及调试缓存等为主另有少量示例图片与说明文档结构基本完整。目前已有1066人学习下载可用于快速理解OpenCV在C#环境下的调用方式以及矩形目标定位的具体参数调试与排错思路。1. C# 上位机为什么要自己写矩形识别先搞清楚需求再动手很多人看到「C# OpenCV 图像矩形识别」第一个反应是OpenCV 不是有现成的 FindContours 吗套一下不就行了真做上位机视觉的人都知道麻烦不在“找轮廓”而在“怎么证明你找到的就是你要的那个矩形”。工件定位、ROI 截取、尺寸测量、纠偏引导这些场景看着都是“找矩形”但光照一变、背景一乱同一个算法出来的结果可能天差地别。这篇笔记就是讲怎么用 C# 调 OpenCVSharp把矩形识别从“能跑”做到“能上线”包括算子怎么选、参数怎么调、角点顺序怎么处理、坐标怎么回传以及我在现场踩过的几个坑。适合正在做上位机视觉、想用开源库替代 Halcon 做快速原型或者被老板要求“三天内给个识别方案”的工程师。2. 选型与工程准备OpenCVSharp 和 Emgu 的取舍以及最小依赖配置2.1 为什么是 OpenCVSharp 而不是 EmguC# 生态里接 OpenCV 主要有两条路Emgu CV 和 OpenCVSharp。很多初学者会选 Emgu因为它封装得更“像 C#”Bitmap 和 Mat 之间转换方便文档看起来也全。但真做矩形识别这个具体任务我更推荐 OpenCVSharp原因有三个。第一OpenCVSharp 的 API 和原生 OpenCV 几乎是 1:1 映射的。这意味着你搜到的大部分 C 代码、Python 代码都能直接翻译过来不需要先理解 Emgu 那层封装又绕了什么。比如Cv2.FindContours、Cv2.ApproxPolyDP、Cv2.MinAreaRect这些函数名和参数顺序和原生版本完全一致。对于从 Python 转过来的工程师这能省掉一半的学习成本。第二OpenCVSharp 里轮廓数据直接用Point[][]表示操作起来非常直观。你要筛选轮廓、算面积、做多边形逼近都是直接对数组操作不需要像 Emgu 那样在ContourPoint和数组之间来回转换。这个差异在写矩形筛选逻辑时会明显感受到代码会短很多。第三OpenCVSharp 的 NuGet 包更新很勤和 OpenCV 版本基本同步。你不需要为了某个新算子去手动编译 OpenCV这在 Windows 上位机开发里太省心了。当然Emgu 也有它的价值。如果你做的是复杂的模式匹配、要跑 GPU 加速或者你的团队对 C# 封装有强依赖那 Emgu 的生态更适合。但对于“图像里找矩形”这种基础几何任务OpenCVSharp 的直白风格是最大优势。提示本方案全部基于 OpenCVSharp 4.x。如果你用的是 OpenCvSharp3部分 API 有差异尤其是FindContours的返回值形式不同下文代码不能直接照抄。2.2 NuGet 安装与 Native 依赖用 OpenCVSharp 最恶心的问题不是写代码而是装好之后一运行就报DllNotFoundException。这个库的 C# 层只是封装真正的算法都在 Native 的 DLL 里。常见的翻车姿势是只装了OpenCvSharp4没装OpenCvSharp4.runtime.win结果编译通过跑起来直接崩。以 Visual Studio 2022 为例项目创建好之后在 NuGet 包管理器里执行下面的命令Install-Package OpenCvSharp4 Install-Package OpenCvSharp4.runtime.win这两个包都要装。第一个是 C# API第二个是 Native 运行时。装完之后项目目录下会多出OpenCvSharpExtern.dll和opencv_videoio_ffmpeg4100.dll之类的文件如果你看到这些说明依赖齐了。然后是目标平台的问题。OpenCVSharp 的 Native 库分 x64 和 x86 两套默认情况下你把项目设为AnyCPU运行时可能加载不到正确的那一套。我一般会直接在项目属性里把平台改成x64因为现在几乎不会有上位机还是 32 位系统了。PropertyGroup PlatformTargetx64/PlatformTarget /PropertyGroup顺手把Copy Local属性确认一下确保 DLL 被复制到输出目录。如果你用 .NET Framework 4.7.2 或 .NET 6/8 都无所谓OpenCVSharp 对这两者都支持。2.3 一个能跑起来的最小环境验证装完包别急着写识别逻辑先做一次最小验证确认图像能读、能显示、能处理。这一步能帮你把“环境问题”和“算法问题”隔离开后面就算排查也不会一头雾水。using OpenCvSharp; class Program { static void Main() { // 读取一张测试图IMREAD_COLOR 表示按 BGR 三通道读入 using var src new Mat(test.jpg, ImreadModes.Color); if (src.Empty()) { Console.WriteLine(图像读取失败检查文件路径); return; } // 转灰度后面所有处理的基础 using var gray new Mat(); Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); // 显示结果按任意键关闭窗口 Cv2.ImShow(gray, gray); Cv2.WaitKey(0); Cv2.DestroyAllWindows(); } }这段代码的逻辑很简单读图转灰度弹窗显示。但确认它能跑通等于排除了路径问题、DLL 问题、平台位数问题。特别是using var这个写法Mat是IDisposable的上位机要长时间跑不释放内存会出大问题后面会专门讲。实际开发里我一般不开ImShow窗口而是直接Cv2.ImWrite(debug.jpg, gray)把中间结果写到磁盘。现场往往没有显示器或者程序跑在工控机上弹窗反而耽误事。3. 核心识别流程从灰度图到矩形角点的完整管线3.1 预处理灰度、滤波与自适应阈值矩形识别的第一步不是找轮廓而是把图像处理成“背景干净、目标清晰”的二值图。很多新手上来就Canny结果边缘里全是噪声后面筛选写得再漂亮也白搭。我的习惯是三步走灰度 → 高斯模糊 → 阈值分割。灰度不用多说彩色信息在矩形识别里几乎没有价值反而会增加计算量。高斯模糊的作用是抑制传感器噪声尤其是现场有环境光干扰时这一步能让边缘更稳定。using var src new Mat(test.jpg, ImreadModes.Color); using var gray new Mat(); Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); // 高斯模糊核大小和标准差要按图像尺寸调 using var blurred new Mat(); Cv2.GaussianBlur(gray, blurred, new Size(5, 5), 0); // 自适应阈值blockSize 必须为奇数 using var bin new Mat(); Cv2.AdaptiveThreshold(blurred, bin, 255, AdaptiveThresholdTypes.MeanC, ThresholdTypes.Binary, 15, 10);阈值分割有两种思路全局阈值和自适应阈值。如果你的图像背景亮度稳定比如工件在背光光源下拍出来黑白分明用Cv2.Threshold配合固定阈值就够了。但现场情况往往是零件表面反光、背景有阴影同一个阈值在图像上半部分能用、下半部分就废了。自适应阈值按局部窗口计算阈值抗明暗不均的能力强很多这也是我在现场更喜欢它的原因。参数上要注意两点。blockSize是局部窗口尺寸必须为奇数一般取 11~31 之间。太小会把纹理误判为边缘太大会失去“自适应”的意义。C是从均值里减去的常数越大得到的二值图越“保守”边缘越少。调试时我一般从 10 起步看看二值图里矩形区域是否能完整呈现。还有一种场景是前景和背景灰度接近阈值切不开。这时候我会试Otsu自动阈值或者直接换思路用 Canny 找边缘再闭运算把断裂的边缘连起来。别死磕一种预处理多试几次才能找到适合现场光源的稳定组合。3.2 轮廓查找与多边形逼近二值图准备好之后就可以找轮廓了。OpenCV 的FindContours会把图像里所有连通的白色区域当成一个个轮廓返回。这里有几个关键参数检索模式、逼近方法和返回的层级信息。// 查找轮廓RETR_EXTERNAL 只取最外层APPROX_SIMPLE 压缩轮廓点 Mat[] contours; HierarchyIndex[] hierarchy; Cv2.FindContours(bin, out contours, out hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple); foreach (var contour in contours) { // 计算轮廓面积过滤掉噪声小点 double area Cv2.ContourArea(contour); if (area 1000) continue; // 多边形逼近epsilon 控制逼近精度 var approx Cv2.ApproxPolyDP(contour, 0.02 * Cv2.ArcLength(contour, true), true); // 矩形应该是 4 个顶点 if (approx.Length 4) { Console.WriteLine($找到矩形面积:{area:F2}); } }检索模式RetrievalModes.External的意思是只返回最外层轮廓不关心轮廓内部嵌套的孔洞。矩形识别场景下这个模式通常够用而且能减少大量无关轮廓。如果你要识别的是“矩形边框内还有图形”的靶标那才需要考虑RetrievalModes.CComp或Tree。逼近方法ApproxPolyDP用的是 Douglas-Peucker 算法简单说就是把一条轮廓上的点减少到最少但形状大致不变。epsilon是逼近精度取轮廓周长的 2% 是一个通用经验值。这个值太大会让矩形变成三角形或五边形太小则逼近不彻底矩形边缘上残留大量冗余点干扰判断。判断“是不是矩形”的逻辑就在approx.Length 4。但这里有个大坑现实中的矩形在图像里常常不是标准四边形而是有透视变形的任意四边形。ApproxPolyDP逼近出来的 4 个点其实已经是四边形了你还需要判断这个四边形的角是否接近直角。我一般会算相邻边的夹角在 80°~100° 之间才认为它是矩形这个后文会展开。3.3 角点排序与最小外接矩形找到 4 个角点只是第一步真正麻烦的是这 4 个点的顺序。FindContours返回的轮廓点顺序是沿着轮廓走的但ApproxPolyDP之后顶点的起始位置是不确定的可能从左上角开始也可能从右下角开始。而上位机做定位时你需要知道哪个点是左上角、哪个是右下角否则后续算旋转角度、做坐标补偿就全乱了。// 把顶点按 x y 排序最小的是左上角最大的是右下角 var sorted approx.OrderBy(p p.X p.Y).ToArray(); Point2f tl sorted[0]; Point2f br sorted[3]; // 剩余两个点按 x 排序小的是右上角大的是左下角 var rest sorted.Skip(1).Take(2).OrderBy(p p.X).ToArray(); Point2f tr rest[0]; Point2f bl rest[1];这个排序逻辑只适用于“矩形在画面里没有严重旋转”的情况。如果工件在传送带上转了个角度比如旋转 30°这种简单排序就会失效。更稳的做法是用Cv2.MinAreaRect求出最小外接矩形它能同时给出中心点、宽高和旋转角。// 用最小外接矩形获取旋转信息和中心点 RotatedRect rrect Cv2.MinAreaRect(contour); Point2f center rrect.Center; float width rrect.Size.Width; float height rrect.Size.Height; float angle rrect.Angle; // 获取外接矩形的四个角点顺序固定 var vertices rrect.Points();MinAreaRect返回的RotatedRect自带四个角点但它的顺序也不是你直觉里的“左上、右上、右下、左下”而是按旋转矩形的特定几何规则排列的。要拿到“规范”的四角顺序需要结合angle做一次旋转排序。网上很多帖子直接拿Points()的结果当四角用坐标对不上就抱怨 OpenCV 有 bug其实是没搞懂它的排列规则。我习惯的做法是先用MinAreaRect拿中心和角度做强鲁棒性的粗定位再回到轮廓上做精确定位。这套组合在传送带上的工件识别里非常稳即使矩形有 30°~40° 的偏转也能保证输出坐标不飘。4. 筛选策略与参数设置面积、长宽比、角度阈值怎么定4.1 为什么轮廓筛选比轮廓查找更重要继续把矩形识别这条链路做完预处理拿到二值图查找拿到轮廓。但现实场景并不会让你舒舒服服地只找到目标这一个轮廓。背景里的工件边缘、传送带上的反光、甚至镜头上的灰尘都会被FindContours捡起来。真正决定识别稳不稳的是筛选逻辑。拿一个典型的视觉定位工位来说相机固定向下拍传送带目标是一个 100×80 的金属铭牌。图像里可能出现的东西包括铭牌本身、铭牌上的螺丝孔、旁边工位的工件边角、光照生成的阴影边缘。如果不筛选FindContours可能返回几十个轮廓其中十个都有 4 个顶点。你必须在代码里把“长得像铭牌”的筛出来。我常用的筛选指标有四个面积、长宽比、轮廓周长与面积的比紧凑度、角点角度。下面分别讲。4.2 筛选指标与参数表面积是最直接、最好用的第一道滤网。在固定相机、固定高度的工位里目标在图像中的像素面积基本是个常量。你不需要精确计算只需要设一个范围区间把明显偏大偏小的噪声干掉。double area Cv2.ContourArea(contour); if (area minArea || area maxArea) continue;minArea和maxArea怎么定我的做法是在调试阶段画一条辅助线手动框出目标在画面中的像素大小然后留 30% 的裕量。比如目标在画面里大约 5000 像素那minArea设 3500maxArea设 6500。这个区间不能太紧否则目标稍微移出光源区域、边缘收缩面积变小就被误杀了。长宽比是第二个常用指标。矩形目标的宽高比是已知的例如铭牌是 100×80在画面里的像素宽高比大约 1.25。你可以用MinAreaRect拿到外接矩形的宽高算比值做筛选。RotatedRect rrect Cv2.MinAreaRect(contour); float aspect rrect.Size.Width / rrect.Size.Height; if (aspect maxAspect || aspect minAspect) continue;这里有个细节MinAreaRect的宽高不是固定的“宽 高”它会根据旋转角度交换宽高的定义。所以要先把宽高归一化也就是让Width始终取较大值再算长宽比否则你会发现同一个矩形转 90° 之后比值就反了。角点角度是更严格的筛选。矩形在透视变形不严重的情况下四个角应该接近直角。你可以遍历逼近得到的 4 个点依次计算相邻两条边的夹角用Math.Abs(angle - 90)的偏差来判断。for (int i 0; i approx.Length; i) { Point2f p1 approx[i]; Point2f p2 approx[(i 1) % approx.Length]; Point2f p3 approx[(i 2) % approx.Length]; // 计算 p1-p2 和 p2-p3 两条边的夹角 double angle GetAngle(p1, p2, p3); if (Math.Abs(angle - 90) maxAngleDeviation) { isRectangle false; break; } }maxAngleDeviation我一般取 10°~15°。现场图像有透视或者镜头畸变时矩形投影出来可能是平行四边形角度偏差会比想象中大别设太死。下面是一个参考的参数起步表不同现场按实际情况调整参数作用参考值调试建议minArea / maxArea面积范围过滤目标像素面积 ±30%目标被遮挡时放宽minAspect / maxAspect长宽比过滤理论比值 ±20%透视严重时放宽maxAngleDeviation角点角度偏差上限10°~15°畸变大时放宽epsilon多边形逼近精度轮廓周长 1%~3%轮廓锯齿多时增大4.3 筛选顺序与短路逻辑筛选条件的顺序有讲究。计算面积只需要ContourArea速度快计算角度需要遍历顶点稍慢一些计算长宽比要调MinAreaRect开销最大。所以筛选顺序应该是面积 → 角点数量 → 长宽比 → 角点角度。foreach (var contour in contours) { // 第一级面积过滤掉大多数噪声 double area Cv2.ContourArea(contour); if (area minArea || area maxArea) continue; // 第二级多边形逼近后必须是四边形 var approx Cv2.ApproxPolyDP(contour, epsilon, true); if (approx.Length ! 4) continue; // 第三级长宽比过滤 RotatedRect rrect Cv2.MinAreaRect(contour); float aspect Math.Max(rrect.Size.Width, rrect.Size.Height) / Math.Min(rrect.Size.Width, rrect.Size.Height); if (aspect maxAspect) continue; // 第四级角点角度过滤 if (!IsRectangleAngles(approx, maxAngleDeviation)) continue; // 全部通过加入候选 candidates.Add(contour); }这样写的意义在于减少不必要的计算。现场一帧图像里几十个轮廓大部分在面积这层就被干掉了不会白耗 CPU。上位机往往同时承担通信和界面刷新视觉线程能省计算一定要省。5. 避坑现场环境里矩形识别的几个典型翻车点5.1 角点排序颠倒导致定位错乱这个坑我踩得最深。第一次做旋转工件识别时直接用MinAreaRect.Points()的返回结果当四个角点把坐标发给 PLC 之后设备夹具直接偏了十几毫米。现场排查半天最后发现是角点顺序的问题——Points()返回的是旋转矩形的固有排列顺序并不保证“第一个点是左上角”。RotatedRect.Points()的规律是返回的四个点从某个角开始逆时针排列但起点取决于矩形的旋转角度。当角度在 0° 附近时可能从左上角开始角度到 30° 时可能从右上角开始。所以不能直接信任下标。解决思路是引入排序函数取四个点的重心作为基准然后按角度把四个点排序成顺时针或逆时针最后再根据业务需求指定哪个是起点。更省事的做法是别用Points()直接用上文说的“xy 排序法”虽然它不适用于大角度旋转但配合MinAreaRect的中心点和角度做一次旋转补偿就足够把角点归正了。我的代码里会先判断rrect.Angle如果绝对值大于 15°就先把图像在数学上做个旋转补偿再做简单排序。5.2 边缘断裂导致矩形被拆成多个轮廓这是比角点顺序更隐蔽的问题。光照不足、反光、或者工件表面有暗纹时FindContours可能把一个矩形拆成两半输出两个三角形轮廓而不是一个四边形。现象就是候选列表里找不到任何 4 顶点的轮廓但图像里明明有一个矩形。原因在于二值化之后矩形的边缘并不连续中间断了一截。解决思路也比较固定在二值图之后做一次形态学闭运算把断裂的缝隙“焊”上。// 闭运算先膨胀再腐蚀把断裂的边缘连接起来 using var kernel Cv2.GetStructuringElement(MorphShapes.Rect, new Size(5, 5)); Cv2.MorphologyEx(bin, bin, MorphTypes.Close, kernel);闭运算的核大小要从 3×3 开始试如果矩形边缘断口很大可以加到 7×7 或 9×9。但要注意核太大会把相邻的矩形融合成一个轮廓或者把矩形内部的孔洞也填上导致面积和轮廓形状失真。所以我一般会保留闭运算前后的两张二值图先用未处理的图去找轮廓找不到再退回闭运算的结果重新找。5.3 单位换算时把 RotatedRect 宽高弄反矩形识别最终要输出物理尺寸或者坐标绕不开“像素到毫米”的换算。这里有个非常容易翻车的地方RotatedRect.Size.Width和.Height并不一定对应你直觉里的“宽”和“高”。OpenCV 中RotatedRect的宽高定义是Angle对应旋转的角度从水平轴开始算Width是与旋转角度对应的那条边Height是与之垂直的另一条边。当矩形旋转 90° 时Width和Height的视觉位置就互换了一次。如果你在标定时假定“宽总是水平方向”那旋转后的数据就会差出一个宽高比。处理办法是归一化。在换算物理尺寸之前约定Width恒为较大值、Height恒为较小值或者在代码里显式判断float width rrect.Size.Width; float height rrect.Size.Height; if (width height) { (width, height) (height, width); }这样不论矩形怎么旋转你拿到的宽高都对应同一个物理方向。标定时的像素/毫米比例系数也能保持一致。5.4 图像坐标系与工件坐标系的镜像问题最后一个坑和图像本身无关而是坐标系方向。图像的像素坐标系原点在左上角Y 轴向下而很多设备坐标系 Y 轴向上或者以工件中心为原点。直接把图像的像素坐标发给 PLC常常会发现 Y 值反了或者原点偏了一截。我在代码里会单独封装一个坐标转换函数把像素坐标转换到设备坐标系// 设备坐标系原点在图像中心X 向右Y 向上 Point2f PixelToDevice(Point2f pixelPt, Point2f imageCenter, float scale) { float dx (pixelPt.X - imageCenter.X) * scale; float dy (imageCenter.Y - pixelPt.Y) * scale; return new Point2f(dx, dy); }这个转换有三个参数要标定imageCenter是相机光轴对应的像素位置scale是像素到毫米的比例还有一个没有写进函数的旋转角——如果相机安装时存在绕光轴旋转的角度偏差需要在转换里额外加一个旋转变换。我在现场用的办法是拍一张标准棋盘格让 PLC 走几个已知点位反算出旋转分量一劳永逸。5.5 内存释放与 Mat 生命周期这个不算图像算法问题但它是上位机视觉最致命的坑Mat对象忘记释放内存涨到爆。上位机通常 7×24 小时运行相机每秒拍几帧每一帧如果产生几十个临时Mat没有释放半小时后内存就会冲到几个 GB然后程序卡死、界面白屏。C# 里虽然有 GC但Mat包着的是非托管内存GC 并不直接管它。OpenCVSharp 的Mat实现了IDisposable最安全的写法是全程使用usingusing (var src new Mat()) using (var gray new Mat()) using (var bin new Mat()) { // 处理逻辑 }如果是在循环体里每帧图像的处理都在循环内部局部变量在圈闭结束后就会被释放。但也有例外——如果你把某个Mat赋给了类成员变量或者ConcurrentQueue里缓存了帧生命周期变长就得在不用时手动Dispose()。我自己吃过一次亏为了调试方便把最近 100 帧图像存在内存列表里结果一晚上没关第二天内存占用 3 个 GB。现在凡是缓存帧一律先压缩成 JPEG 再存或者存Rect和关键坐标而不是整帧图。6. 验证与交付用批量目录和坐标回显确认识别效果6.1 批量目录验证脚本单一图像能识别不代表现场能上线。相机角度、光源亮度、工件批次一变算法可能立刻失效。我在交付前的最后一个步骤是收集一批现场图——至少 50 张覆盖不同角度、不同位置、不同光照——放到同一个目录里用脚本批量跑识别统计成功率和误检率。string dir D:\samples; int total 0, success 0; foreach (var file in Directory.GetFiles(dir, *.jpg)) { using var src new Mat(file, ImreadModes.Color); total; // 调用识别函数返回矩形信息 var result DetectRectangle(src); if (result ! null) { success; // 把识别到的矩形画到图上方便人工复查 Cv2.Rectangle(src, result.BoundingRect, Scalar.Red, 2); Cv2.ImWrite(Path.Combine(dir, out, Path.GetFileName(file)), src); } } Console.WriteLine($成功率: {success}/{total} {(double)success / total:P2});这个脚本的价值不是统计数字本身而是把“我觉得没问题”变成“机器跑过 50 张图确认没问题”。按我的经验批量测试之前十有八九会发现之前没注意的模式——比如某一批工件表面有贴纸导致面积异常或者某个固定角度下反光把边缘切断。没有批量验证就上线出了事故就只能蹲在现场加班排查血泪经验。6.2 输出调试文本与图像双回显除了成功率统计我还习惯在每张图上画出识别到的矩形框和中心点坐标保存到输出目录然后随机抽 5~10 张人工看一眼。人眼是最后的把关画出来的框位置对不对一眼就能看出来。同时在控制台输出关键参数Console.WriteLine($中心:({center.X:F1}, {center.Y:F1}), $角度:{angle:F1}°, 宽:{width:F1}, 高:{height:F1});文本和图像双回显的意义在于图像能告诉你“在哪错了”文本能告诉你“为什么错了”。角度异常、宽高比异常、中心点偏移每种情况对应的问题方向不同双回显能让你快速定位是哪一环的参数出了问题。6.3 关于阈值参数的最后一个习惯聊到最后说一个我的个人习惯所有阈值参数不硬编码在程序里而是放到配置文件或者界面调试框里。原因也很简单——现场的光照每隔几个月就会变光源老化、天空亮度变化、甚至车间里加了台焊机都会影响图像质量。如果阈值写死在代码里现场调试就得改代码重新编译这在上位机这种环境里极其痛苦。我一般用appsettings.json存参数程序启动时读一次调试时改完配置重启就能生效。如果你有精力可以做个简单的参数调试窗体用 TrackBar 实时调节阈值所见即所得。矩形识别这个方向做到能批量验证、参数不写死、坐标转换清晰就足够支撑大部分工位需求了。希望这篇笔记对你有帮助——至少让你少走几个我走过的弯路。本文还有配套的精品资源点击获取
返回列表