简介C#联合OpenCVSharp开发的视觉源码程序面向需要快速搭建视觉识别与测量功能的C#开发者直接提供模板匹配、找线找圆、图像预处理等常用模块的完整实现可用于工业定位、尺寸检测、目标识别等场景。压缩包共737个文件包含138个C#源码文件、182个dll依赖库以及xml配置、png/bmp界面资源、txt说明文档等整体约409MB目录结构清晰既可直接运行也便于按需修改扩展。程序内置图像显示控件和ROI绘制交互界面能实时预览处理效果并手动划定感兴趣区域适合对OpenCVSharp不熟悉的开发者作为入门参考也可作为自动化视觉项目的底层框架。已有175人学习下载是学习C#视觉编程与搭建实际工程的良好素材。1. 从C#上位机到视觉检测为什么OpenCvSharp成了最稳的选择在一套C#上位机视觉程序里最磨人的不是算法本身而是图像显示、ROI交互、预处理链路的框架搭建。C#联合OpenCvSharp开发的视觉源码程序通常把模板匹配、找线找圆、预处理这些核心功能连同图像显示控件和绘制ROI的交互底座打包在一起形成一套能直接跑起来的检测框架。这篇文章就按这个骨架来讲显示控件怎么做、ROI怎么画、预处理参数怎么调、模板匹配和几何测量怎么落地以及调试里翻过的那些车。适合正在做上位机视觉软件、想在一周内把检测框架跑起来的工程师——不是泛泛科普是能照着改的实战路径。2. 图像显示控件与ROI绘制先把交互底座搭起来2.1 用PictureBox还是自绘控件显示层的选型逻辑OpenCvSharp本身没有自带图像显示控件这和Halcon的HWindowControl不一样。最常见的做法是两种直接封装PictureBox或者写一个继承UserControl的自绘控件。我一般选后者原因很现实后续要在图像上叠加绘制ROI、十字光标、匹配框和测量结果如果全靠PictureBox的Paint事件重绘缩放和平移时要不断换算坐标代码会越写越乱。自绘控件的好处是把“图像显示”和“图形叠加”拆成两层底层是一张Bitmap上层是GDI绘制的矢量图层。鼠标事件直接落在控件上操作体验接近市面上的视觉软件。public class ImageViewer : UserControl { private PictureBox _picBox; private Mat _currentMat; public ImageViewer() { _picBox new PictureBox { Dock DockStyle.Fill, SizeMode PictureBoxSizeMode.Zoom }; _picBox.MouseDown OnMouseDown; _picBox.MouseMove OnMouseMove; _picBox.MouseUp OnMouseUp; _picBox.Paint OnPaint; Controls.Add(_picBox); } public void ShowImage(Mat image) { _currentMat?.Dispose(); _currentMat image.Clone(); _picBox.Image BitmapConverter.ToBitmap(_currentMat); _picBox.Invalidate(); } }这段代码的要点在ShowImage里传入的Mat不能直接拿给PictureBox用因为外部可能继续修改这张图浅拷贝会互相影响。Clone()之后控件持有自己的数据外部改图不影响显示。BitmapConverter.ToBitmap是OpenCvSharp.Extensions命名空间里的转换方法会把Mat转成System.Drawing.Bitmap。这里有个容易忽略的坑转换出来的Bitmap默认不会释放如果每帧都调ShowImage转一次内存会被Bitmap排队占满。我习惯在赋值前先_picBox.Image?.Dispose()把上一帧的Bitmap清掉。另外_currentMat作为控件内部持有的Mat副本也要在下次ShowImage时释放这是展示层最基本的内存纪律。2.2 鼠标绘制ROI矩形ROI与旋转矩形ROI的实现绘制ROI是视觉框架里用户接触最多的交互。典型需求是在图上按住鼠标左键拖出一个矩形框松开后这个矩形进入ROI列表后续模板匹配和预处理只在这个区域内做计算。实现上要处理两个问题一是鼠标拖动时实时预览二是控件坐标和图像坐标的换算。private Point _dragStart; private Rectangle _currentRect; private bool _isDrawing; private void OnMouseDown(object sender, MouseEventArgs e) { if (e.Button MouseButtons.Left) { _dragStart e.Location; _isDrawing true; } } private void OnMouseMove(object sender, MouseEventArgs e) { if (!_isDrawing) return; Point start ToImagePoint(_dragStart); Point end ToImagePoint(e.Location); _currentRect new Rectangle( Math.Min(start.X, end.X), Math.Min(start.Y, end.Y), Math.Abs(start.X - end.X), Math.Abs(start.Y - end.Y)); _picBox.Invalidate(); } private void OnMouseUp(object sender, MouseEventArgs e) { if (!_isDrawing) return; _isDrawing false; if (_currentRect.Width 5 _currentRect.Height 5) _roiList.Add(new RoiItem { Rect _currentRect, Name $ROI_{_roiList.Count 1} }); }_currentRect在MouseMove里实时更新然后调用Invalidate()触发重绘Paint事件里把框画出来。判断宽高大于5是为了过滤误点击。这里有个细节_dragStart和e.Location都是控件坐标必须先转成图像坐标再构造Rectangle否则放大显示的时候画出来的框位置和鼠标实际位置对不上。旋转矩形ROI在视觉里也常见尤其模板匹配目标本身带角度时。OpenCvSharp里对应的是RotatedRect参数是中心点、Size和角度。鼠标交互上我习惯用“先拖出中心再拖出边最后拖出角度”的三步法实现代价比一次性拖出旋转框低很多。画旋转矩形时不能直接用Cv2.Rectangle要取rotatedRect.Points()拿四个角点再用Cv2.Polylines画出来。2.3 图像坐标与控件坐标的换算缩放、平移与状态栏联动坐标换算是所有视觉交互程序的命门。PictureBox处于Zoom模式时图像等比缩放四周可能留黑边鼠标位置到图像位置的映射必须知道缩放比例和偏移量。public Point ToImagePoint(Point controlPoint) { Size imgSize _currentMat.Size(); Size boxSize _picBox.ClientSize; double scale Math.Min(boxSize.Width / (double)imgSize.Width, boxSize.Height / (double)imgSize.Height); double offsetX (boxSize.Width - imgSize.Width * scale) / 2.0; double offsetY (boxSize.Height - imgSize.Height * scale) / 2.0; int imgX (int)Math.Round((controlPoint.X - offsetX) / scale); int imgY (int)Math.Round((controlPoint.Y - offsetY) / scale); imgX Math.Clamp(imgX, 0, imgSize.Width - 1); imgY Math.Clamp(imgY, 0, imgSize.Height - 1); return new Point(imgX, imgY); }反向换算——从图像坐标到控件坐标——就是上面公式的逆运算通常在绘制十字光标和测量标注时用。这套换算在放大显示和滚轮缩放时同样要一致否则就会出现“鼠标画框框偏移”“标注画到图外面”之类的问题。坐标换算还有个实际用途状态栏联动。把鼠标移动时的图像坐标实时显示在WinForm状态栏里就是“c# winform如何更新状态栏与进度条”的典型场景。我一般用ToolStripStatusLabel在MouseMove里刷新文本注意不要在这个事件里做图像处理它触发频率太高只更新坐标文本否则UI会卡顿。3. 预处理模块模板匹配和找线找圆都靠它喂数据3.1 常用预处理算子与搭配灰度、滤波、阈值、形态学预处理是视觉里最容易被低估的环节。这里的预处理是图像域的预处理不是文本预处理那种词表清理。OpenCvSharp框架里模版匹配和找线找圆的输入质量直接取决于预处理链路的设置。一条典型的链路是灰度化 → 中值滤波 → 自适应阈值 → 形态学开运算。灰度化把三通道图像降为单通道计算量直接变成三分之一。中值滤波对椒盐噪声和产品表面的细小划痕有奇效比高斯滤波更适合边缘类测量任务。自适应阈值解决光照不均的问题它按局部邻域计算阈值同一个参数在画面明暗差异大的区域也能稳定出二值图。最后开运算把细小的连接断开避免后续找轮廓时把不该连的边缘连在一起。public Mat Preprocess(Mat src) { Mat gray new Mat(); Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); Mat median new Mat(); Cv2.MedianBlur(gray, median, _ksize); Mat binary new Mat(); if (_useAdaptive) { Cv2.AdaptiveThreshold(median, binary, 255, AdaptiveThresholdTypes.MeanC, ThresholdTypes.Binary, _blockSize, _cValue); } else { Cv2.Threshold(median, binary, _thresh, 255, ThresholdTypes.Binary); } Mat morph new Mat(); Mat kernel Cv2.GetStructuringElement(MorphShapes.Rect, new Size(_morphSize, _morphSize)); Cv2.MorphologyEx(binary, morph, MorphTypes.Open, kernel); kernel.Dispose(); gray.Dispose(); median.Dispose(); binary.Dispose(); return morph; }参数上要注意_ksize中值滤波核必须是奇数3到7之间够用再大图像会糊_blockSize自适应阈值的邻域尺寸也必须是奇数常见设置31或51值越大阴影适应性越强但太大会把小目标吞掉_cValue是常数偏移正值会让更多像素变成背景负值则相反。形态学核的大小和形状要看目标形态矩形核适合普通表面十字核更适合定位类任务。预处理最大的坑不是选哪个算子而是参数不可调。如果这些参数写死在代码里现场换一个光照环境就得改代码重编译。我坚持把所有预处理参数放进配置类界面上用NumericUpDown直接绑定这也是这套源码程序里预处理模块存在的实际价值。3.2 可调预处理链路让参数面板和图像刷新联动参数面板的交互逻辑很简单改变任何一个预处理参数立即用当前原图重新走一遍链路并把结果显示在图像控件上。这个“实时预览”的交互比反复改代码重跑高效得多。public class PreprocessOptions { public int KSize { get; set; } 5; public bool UseAdaptive { get; set; } true; public int BlockSize { get; set; } 31; public int CValue { get; set; } 10; public int MorphSize { get; set; } 3; } // 界面事件绑定 private void numericUpDown_ValueChanged(object sender, EventArgs e) { _options.KSize (int)numKSize.Value; _options.BlockSize (int)numBlockSize.Value; _options.CValue (int)numCValue.Value; Mat result _preprocessor.Preprocess(_sourceMat); _viewer.ShowImage(result); }参数类用属性而非公共字段是为了后续做JSON序列化持久化。每个参数改完后重新预处理注意这里要控制触发频率ValueChanged在拖动NumericUpDown时会连续触发如果图像分辨率高预处理一次要几十毫秒连续触发会造成界面卡死。常见做法是加一个防抖计时器事件触发后延迟200毫秒再执行期间不断重置计时器直到用户停下。实时预览时的Mat释放也要注意。ShowImage内部会处理显示用Mat的释放但_sourceMat这类长期持有的原图不要在预览时被意外替换。我习惯在整个预处理链路里用using块或手动Dispose中间Mat避免一张大图反复处理产生几百MB的垃圾对象。3.3 中间结果可视化不弹窗的快速调试法预处理调试最血泪的经验是不要用Cv2.ImShow弹窗。OpenCvSharp的Cv2.ImShow在WinForm上位机里会阻塞消息循环弹窗一多程序直接假死。我习惯做一个调试侧面板把每一步中间结果放进一个TabControl或ComboBox切换显示。private ListMat _debugImages new ListMat(); private Liststring _debugNames new Liststring(); public Mat PreprocessWithDebug(Mat src, PreprocessOptions options) { _debugImages.Clear(); Mat gray new Mat(); Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); _debugImages.Add(gray.Clone()); _debugNames.Add(灰度); Mat median new Mat(); Cv2.MedianBlur(gray, median, options.KSize); _debugImages.Add(median.Clone()); _debugNames.Add(中值滤波); Mat binary new Mat(); Cv2.AdaptiveThreshold(median, binary, 255, AdaptiveThresholdTypes.MeanC, ThresholdTypes.Binary, options.BlockSize, options.CValue); _debugImages.Add(binary.Clone()); _debugNames.Add(自适应阈值); gray.Dispose(); median.Dispose(); binary.Dispose(); return binary; }中间结果用Clone()存副本是因为后续会继续Dispose上游Mat如果不克隆调试列表里存的引用全部失效。调试信息收集本身只在开发阶段开启上线时用条件编译去掉避免每帧多几次图像拷贝。4. 模板匹配与找线找圆从定位到测量的完整实现4.1 模板匹配原理与OpenCvSharp的匹配模式模板匹配的原理是在搜索图上滑动模板计算每个位置的相似度OpenCvSharp提供了Cv2.MatchTemplate支持六种匹配模式。实际视觉项目里我用得最多的是TemplateMatchModes.CCoeffNormed归一化相关系数对亮度的线性变化不敏感现场光照波动时比SqDiffNormed稳得多。需要强调的是OpenCvSharp自带的模板匹配对旋转和缩放没有内置支持。模板旋转30度相似度会断崖式下降。处理旋转的常见做法是预生成一个旋转模板集把模板从-10度到10度按步长旋转每个角度存一张匹配时逐一打分取最高。这个方案简单可靠代价是角度范围越大、步长越细耗时线性增长。private ListMat _templateRotations new ListMat(); private Listdouble _templateAngles new Listdouble(); public void BuildTemplate(Mat source, Rect roi, double angleStart, double angleEnd, double step) { Mat template new Mat(source, roi); for (double angle angleStart; angle angleEnd; angle step) { Mat rotated new Mat(); Mat rotationMatrix Cv2.GetRotationMatrix2D( new Point2f(template.Width / 2f, template.Height / 2f), angle, 1.0); Cv2.WarpAffine(template, rotated, rotationMatrix, template.Size(), InterpolationFlags.Cubic, BorderTypes.Replicate); _templateRotations.Add(rotated); _templateAngles.Add(angle); } }旋转模板的坑在于旋转后模板四角会出现无像素区域用BorderTypes.Replicate把边缘像素向外复制比默认的黑边效果好很多黑边在归一化匹配时会拉低分数。模板匹配要的就是稳定模板图像的预处理方式和搜索图保持一致这条铁律能省掉大量调试时间。4.2 创建模板与运行匹配从ROI到匹配框的完整代码运行匹配的核心代码不长但细节多。先看最简版本public MatchResult Match(Mat searchImage) { double bestScore -1; Point bestPoint Point.Empty; double bestAngle 0; Rect bestRect Rect.Empty; foreach (var rotatedTemplate in _templateRotations) { using (Mat result new Mat()) { Cv2.MatchTemplate(searchImage, rotatedTemplate, result, TemplateMatchModes.CCoeffNormed); Cv2.MinMaxLoc(result, out _, out double maxVal, out _, out Point maxLoc); if (maxVal bestScore) { bestScore maxVal; bestPoint maxLoc; bestAngle _templateAngles[_templateRotations.IndexOf(rotatedTemplate)]; bestRect new Rect(bestPoint.X, bestPoint.Y, rotatedTemplate.Width, rotatedTemplate.Height); } } } if (bestScore _scoreThreshold) return null; return new MatchResult { Score bestScore, Rect bestRect, Angle bestAngle }; }MatchTemplate的输入要求是灰度图或彩色图都可以但模板和搜索图必须同通道、同深度否则会抛异常。MinMaxLoc从结果矩阵里拿到最大值和位置这个位置是模板左上角的坐标不是中心点。如果要的是目标中心记得bestRect new Rect(bestPoint.X template.Width / 2, bestPoint.Y template.Height / 2, ...)。_scoreThreshold是匹配通过的最低分数通常设在0.6到0.85之间。太低会误检太高会漏检。判断阈值是否合适的办法很土但有效把现场几十张不同光照的图跑一遍看分数直方图找一个明显的分数间隙作为阈值。4.3 找线、找圆边缘点集与最小二乘拟合找线和找圆是尺寸测量的基本功思路跟模板匹配不一样——匹配是找“在哪”找线找圆是测“准不准”。最可靠的方式不是直接用霍夫变换而是先提取边缘点集再做最小二乘拟合。public LineResult FindLine(Mat preprocessed, Rect roi) { Mat roiMat new Mat(preprocessed, roi); Mat edges new Mat(); Cv2.Canny(roiMat, edges, _cannyLow, _cannyHigh); Point[][] contours; HierarchyIndex[] hierarchy; Cv2.FindContours(edges, out contours, out hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxNone); ListPoint allPoints new ListPoint(); foreach (var contour in contours) { if (Cv2.ContourArea(contour) _minArea) allPoints.AddRange(contour); } if (allPoints.Count 10) return null; Cv2.FitLine(allPoints.ToArray(), out Vec4f line, DistanceTypes.L2, 0, 0.01, 0.01); return new LineResult { Vx line[0], Vy line[1], X0 line[2], Y0 line[3] }; }Canny的双阈值一般设成经验值低阈值是高阈值的一半到三分之一。ContourArea筛选是为了去掉噪声点和小碎边缘最小面积按像素算。FitLine用的DistanceTypes.L2是最小二乘适合边缘干净的场景如果边缘有离群点换成DistanceTypes.Huber或Tukey更稳代价是收敛迭代次数变多。这里有个找线的关键细节FitLine的结果是直线上的一个点(X0, Y0)和方向向量(Vx, Vy)不是线段端点。绘制时要自己算出图像边界的两个交点很多新手拿到Vec4f直接画发现线跑出图像范围就以为算法有问题。找圆和找线一脉相通先Canny拿边缘再筛选轮廓最后拟合。如果目标是一个完整圆弧用Cv2.MinEnclosingCircle暴力求最小包围圆最简单如果目标只是部分圆弧比如圆孔被遮挡了一半得用Cv2.FitEllipse拟合椭圆然后取长轴当直径或者对点集做代数圆拟合。4.4 测量结果输出距离、角度与直径判定测量输出的常见需求是两条边的距离、两条线的夹角、两个圆心的距离、圆直径是否在公差内。这些几何计算用点线公式就能完成。public double AngleBetweenLines(Vec4f line1, Vec4f line2) { double angle1 Math.Atan2(line1[1], line1[0]); double angle2 Math.Atan2(line2[1], line2[0]); double diff Math.Abs(angle1 - angle2); if (diff Math.PI / 2) diff Math.PI - diff; return diff * 180 / Math.PI; }角度计算要处理方向向量反向的问题。Atan2返回的是-180度到180度两条线夹角的有效范围是0到90度不做归一会算出钝角。距离计算则是标准的点到直线公式把(X0, Y0)和(Vx, Vy)代入即可。测直径时我习惯搭配一个在线标定系数——像素直径乘标定值得到毫米值标定值用一个已知尺寸的标准件在部署现场测一次存进配置文件。每次测量结果跟公差上下限比较输出OK/NG这才是产线上真正需要的输出格式。5. 常见的 5 个视觉程序排查现场从 ROI 偏移到内存泄漏的踩坑记录5.1 ROI 画出来了匹配却在 ROI 外乱跳现象在图像控件上拖了一个矩形ROI理论上模板匹配只在该区域内搜索但匹配框经常跑到ROI外面或者定位结果跟鼠标画的框完全对不上。原因ROI坐标没有做从控件坐标到图像坐标的换算。PictureBox处于Zoom模式时图像居中缩放控件坐标直接当作图像坐标使用ROI边界就会有系统性偏差。更隐蔽的是DPI缩放——WinForm在高分屏下自动缩放如果窗体启用了AutoScaleMode.Dpi鼠标事件的坐标已经是物理像素而图像尺寸是逻辑像素差值会被放大。解决所有鼠标事件先用ToImagePoint换算成图像坐标再进入业务逻辑。ROI列表里存的一律是图像坐标只有绘制时反向换算成控件坐标。DPI问题在程序入口显式调用SetProcessDpiAwareness并关闭窗体的自动缩放或者统一用DpiHelper换算。5.2 模板匹配一帧要200ms产线根本跑不动现象算法在开发电脑上运行流畅部署到现场工控机上单帧匹配耗时200毫秒以上节拍完全跟不上。原因搜索图太大。如果整幅200万像素图像直接做MatchTemplate模板每移动一个像素就计算一次在分辨率高、模板多的情况下耗时直接爆炸。现场工控机CPU性能通常比开发机弱差距会被放大。解决做金字塔粗匹配。先把搜索图和模板都做两层PyramidDown在低分辨率下匹配一次得到大致位置再映射回原分辨率后做局部精匹配。ROI范围也要尽量收敛匹配只在包含目标的局部区域执行不整图跑。还有一个习惯提前看Cv2.UseOptimized()是否返回trueOpenCV在构建时启用了IPP加速如果没有得分低一半。5.3 找线找圆结果随光照抖动现象同一个产品在白天和晚上测出来的直径差了0.3个像素产线灯光稍有变化找线的角度就抖动。原因整条链路的阈值是固定值。固定阈值在光照变化时边缘的像素可能落在阈值两侧导致Canny提取的轮廓位置偏移几个像素。最小二乘拟合对边缘点位置敏感几个像素的偏移足以让测量结果抖动。解决一是把固定阈值改成自适应阈值或Canny的自动阈值计算二是拟合时换用DistanceTypes.Huber对离群边缘点更鲁棒三是在硬件上把光源调成稳定的漫射光减少环境光干扰。软件能兜底一部分光照波动但视觉测量要稳定光照一致性是关键。5.4 灰度图像显示成绿屏或偏色现象预处理后的灰度图传给图像显示控件显示结果整体偏绿或者出现红绿杂色完全不是灰度效果。原因OpenCvSharp的Mat默认是BGR三通道灰度图是单通道。BitmapConverter.ToBitmap按图像的通道数生成Bitmap如果单通道图像没有正确转成8位灰度格式GDI会用错误的像素格式解释数据出现偏色。还有一种情况Mat的深度是16位BitmapConverter没做类型转换直接按8位解析显示必然花屏。解决显示前统一做一次格式规范化深度不是MatType.CV_8UC1就先用Cv2.ConvertScaleAbs转成8位再交给BitmapConverter。另外灰度图显示用PixelFormat.Format8bppIndexed加灰度调色板不要直接拿三通道的Bitmap格式硬套。5.5 连续采图内存涨到系统告警现象程序刚启动时内存200MB连续运行两小时后涨到2GB最后触发OutOfMemory异常。重启程序后恢复正常。原因最常见的三个位置一是BitmapConverter.ToBitmap产生的Bitmap没有Dispose每帧泄漏一个GDI对象二是Mat中间变量用using但没有在循环里释放比如在采集线程里每帧新建一个Mat处理完不Dispose三是事件订阅导致对象无法被GC回收比如相机回调里订阅了UI事件而没有退订。解决给Mat和Bitmap的创建建立纪律谁创建谁释放处理完立刻Dispose。采集线程和UI线程通过独立的帧队列解耦Mat在UI消费完由UI线程释放。可以用GC.GetTotalMemory打点监测看到只增不减的曲线再按上面三个位置逐个检查。内存问题往往不是一处泄漏三处叠加才把你逼到崩溃边缘。6. 把模块串成检测框架多 ROI 并行、参数持久化与级联定位当显示控件、预处理、匹配、测量这些模块都跑通后真正让方案变成产品的是最后这三件事。多ROI并行。一个视觉程序常常要同时检测多个位置多个模板匹配、多条边测量。用Parallel.For并行执行不同的ROI任务是压榨工控机多核CPU最直接的手段。注意OpenCvSharp的Mat不是线程安全对象每个并行任务内部必须有自己的Mat实例不要跨任务共享一个Mat。我踩过这个坑两个并行任务同时读同一张图做预处理其中一个任务Dispose了Mat另一个任务读取时直接抛异常。并行任务里只读共享原图处理结果各存各的不共享中间变量。参数持久化用System.Text.Json。ROI列表、预处理参数、匹配阈值、公差上下限这些都应该是现场可调的配置而不是代码里的常量。把ListRoiItem和PreprocessOptions序列化成JSON文件部署时只拷配置文件不动代码。RoiItem里的Rect直接序列化X、Y、Width、Height反序列化时重建这个习惯让换产线、换产品型号的成本从改代码降到改配置。级联定位是视觉程序里最值得抄的架构。先用模板匹配粗定位目标拿到目标在图像里的粗略位置和角度再把用于精密测量的ROI按这个位置做映射最后在映射后的ROI里做找边找圆。这样做的好处是精密测量ROI始终锁定在目标上不会因为产品在来料时的位置偏移而测偏。级联的每一级都独立保存参数单独验证出问题的时候能快速定位是哪一级失效。这几套代码模块我用过几个项目后固定成了习惯显示控件管交互预处理管数据质量模板匹配管定位找线找圆管测量JSON管配置并行框架管性能。一开始我也在每个项目里重写轮子后来发现模板匹配和找线找圆的调试逻辑高度相似就把这套框架沉淀下来。找边找圆的坑我交过不少学费特别是光照波动那次最后靠自适应阈值加鲁棒拟合才稳住。调试视觉程序耐心比聪明值钱参数要一个变量一个变量地动不要同时改两个否则翻车了你都不知道该怪谁。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?