Winform基于OpenCvSharp的尺寸测量应用

⒈目标

掌握Winform基于OpenCvSharp的尺寸测量应用方法,本文忽略相机图像采集部分,通过笔记本画图创建图形尺寸如下图:

⒉软件

Visual Studio 2022

⒊UI界面设置

如下图设计了如下UI界面,使用控件包括:button、textbox和picturebox。

其中picturebox大小模式设置为AutoSize,此时picturebox尺寸等于图像原始像素尺寸,测量值直接是像素值,不需要进行像素的换算。

⒋ NuGet库安装

OpenCvSharp4

OpenCvSharp4.Extensions

OpenCvSharp4.runtime.win

OpenCvSharp4.Windows

⒌代码编写

⑴图像处理类VisionMeasureHelper

using System;

using System.Collections.Generic;

using System.Drawing;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using OpenCvSharp;

using Point = OpenCvSharp.Point;

using Size = OpenCvSharp.Size;

namespace WinformVisionMeasure

{

/// <summary>

/// 工业零件视觉尺寸测量核心工具类

/// 功能覆盖:图像预处理、轮廓提取、长/宽/直径/间距/周长/面积测量、像素当量换算

/// </summary>

internal class VisionMeasureHelper

{

#region 设置全局配置参数(可动态调节)

/// <summary>

/// 像素当量:核心换算因子(mm/px),标定后赋值

/// 表示1个像素对应的实际物理尺寸(毫米)

/// 例如:0.025f 表示 1像素 = 0.025毫米

/// </summary>

//public static float PixelEquivalent { get; set; } = 0.025f;

public static float PixelEquivalent { get; set; } = 1f;

/// <summary>

/// 自适应二值化参数

/// 用于将灰度图像转换为黑白图像

/// 数值范围:0-255,默认为127

/// </summary>

public static int ThresholdValue { get; set; } = 127;

/// <summary>

/// 形态学操作核大小(去噪)

/// 用于图像的膨胀、腐蚀等操作

/// 数值范围:奇数,如3、5、7等

/// 值越大,去噪效果越强,但可能丢失边缘细节

/// </summary>

public static int MorphKernelSize { get; set; } = 5;

/// <summary>

/// 最小轮廓面积(过滤噪点,单位:像素)

/// 面积小于此值的轮廓将被忽略

/// 用于去除图像中的小噪点,只保留有意义的轮廓

/// </summary>

public static double MinContourArea { get; set; } = 100.0;

#endregion

#region 图像预处理:

/// <summary>

/// 图像预处理:将原始图像转换为可用于轮廓提取的二值图像

/// 处理流程:高斯模糊 → 灰度化 → 自适应二值化 → 形态学操作

/// </summary>

/// <param name="srcMat">原始彩色图像矩阵</param>

/// <returns>处理后的二值化图像矩阵</returns>

public static Mat PreprocessImage(Mat srcMat)

{

if (srcMat.Empty())// 检查输入图像是否有效

return new Mat();

// 使用 using 语句确保 Mat 对象在使用后被正确释放

using (Mat grayMat = new Mat())//灰度图像

using (Mat binaryMat = new Mat())//二值化图像

using (Mat morphMat = new Mat())//形态学处理后的图像

{

Cv2.GaussianBlur(srcMat, srcMat, new Size(5, 5), 0);//高斯模糊,降噪,保护边缘,参数:源图像、目标图像、核大小(5x5)、sigmaX(0表示自动计算)

Cv2.CvtColor(srcMat, grayMat, ColorConversionCodes.BGR2GRAY);//灰度化,将BGR彩色图像转换为灰度图像(单通道)

//自适应二值化:解决光照不均,黑白反转(零件白,背景黑),参数:源、目标、最大值、算法、阈值类型、块大小、常数

Cv2.AdaptiveThreshold(grayMat, binaryMat, 255, AdaptiveThresholdTypes.GaussianC, ThresholdTypes.BinaryInv, 11, 2);

//形态学操作:开运算(去小噪点)+闭运算(填充孔洞),得到光滑轮廓

Mat kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(MorphKernelSize, MorphKernelSize));

Cv2.MorphologyEx(binaryMat, morphMat, MorphTypes.Close, kernel);//闭运算,先膨胀后腐蚀,填充物体内部孔洞

Cv2.MorphologyEx(morphMat, morphMat, MorphTypes.Open, kernel);//开运算,先腐蚀后膨胀,去除小噪点

return morphMat.Clone();//返回副本(避免返回临时对象)

}

}

#endregion

#region 轮廓提取与筛选(只保留零件目标轮廓,过滤无效噪点)

public static List<Point[]> GetTargetContours(Mat binaryMat)

{

Point[][] allContours; // 存储所有找到的轮廓

HierarchyIndex[] hierarchy; // 存储轮廓的层级关系

// 提取所有外部轮廓:RetrievalModes.External:只提取最外层轮廓,ContourApproximationModes.ApproxSimple:压缩水平、垂直和对角线段,只保留端点

Cv2.FindContours(binaryMat, out allContours, out hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple);

// 使用LINQ过滤:只保留面积大于最小阈值的轮廓,过滤掉小的噪点轮廓,只保留目标零件轮廓

return allContours

.Where(contour => Cv2.ContourArea(contour) >= MinContourArea)

.ToList();

}

#endregion

#region 尺寸测量方法

/// <summary>

/// 测量矩形零件:长、宽(mm)

/// 使用外接矩形框计算零件的宽度和高度

/// </summary>

/// <param name="contour">零件轮廓点集</param>

/// <returns>元组:(宽度mm, 高度mm)</returns>

public static (float widthMm, float heightMm) MeasureRectSize(Point[] contour)

{

// 获取轮廓的外接矩形(平行于坐标轴的最小矩形)

Rect rect = Cv2.BoundingRect(contour);

// 像素尺寸转换为实际物理尺寸(毫米)

// 实际尺寸 = 像素尺寸 × 像素当量

//return (rect.Width * PixelEquivalent, rect.Height * PixelEquivalent);

return (rect.Width-5, rect.Height-5);//返回像素值

}

/// <summary>

/// 测量圆形零件:直径(mm)+ 圆心坐标

/// 使用最小外接圆来近似圆形零件

/// </summary>

/// <param name="contour">零件轮廓点集</param>

/// <returns>元组:(直径mm, 圆心坐标)</returns>

public static (float diameterMm, Point2f center) MeasureCircleSize(Point[] contour)

{

// 获取轮廓的最小外接圆

Cv2.MinEnclosingCircle(contour, out Point2f center, out float radiusPx);

// 直径 = 半径 × 2,再转换为毫米

return (radiusPx * 2 * PixelEquivalent, center);

}

/// <summary>

/// 测量轮廓周长(mm)

/// </summary>

/// <param name="contour">零件轮廓点集</param>

/// <returns>周长(毫米)</returns>

public static float MeasureContourLength(Point[] contour)

{

// ArcLength计算轮廓周长,true表示轮廓闭合

// 像素周长转换为实际毫米周长

return (float)(Cv2.ArcLength(contour, true) * PixelEquivalent);

}

/// <summary>

/// 测量轮廓面积(mm²)

/// </summary>

/// <param name="contour">零件轮廓点集</param>

/// <returns>面积(平方毫米)</returns>

public static float MeasureContourArea(Point[] contour)

{

// ContourArea计算轮廓面积

// 像素面积转换为实际面积需要乘以两次像素当量

return (float)(Cv2.ContourArea(contour) * PixelEquivalent);

}

/// <summary>

/// 测量两点间的实际距离(mm)

/// 用于测量零件上任意两点之间的距离

/// </summary>

/// <param name="p1">点1坐标</param>

/// <param name="p2">点2坐标</param>

/// <returns>两点间距离(毫米)</returns>

public static float MeasurePointDistance(Point p1, Point p2)

{

// 计算坐标差值的平方和

double dx = p1.X - p2.X;

double dy = p1.Y - p2.Y;

// 使用勾股定理计算欧几里得距离(像素)

double distancePx = Math.Sqrt(dx * dx + dy * dy);

// 转换为实际物理距离(毫米)

return (float)(distancePx * PixelEquivalent);

}

#endregion

#region 绘制测量结果

/// <summary>

/// 绘制测量结果

/// 在原图上标注轮廓、尺寸数值、中心点等信息

/// </summary>

/// <param name="srcMat">原始图像矩阵(将在此图上绘制)</param>

/// <param name="contours">轮廓列表</param>

public static void DrawMeasureResult(Mat srcMat, List<Point[]> contours)

{

// 将List转换为数组,供DrawContours使用

Point[][] contoursArray = contours.ToArray();

// 绘制所有轮廓:绿色,线宽1

// -1表示绘制所有轮廓

Cv2.DrawContours(srcMat, contoursArray, -1, new Scalar(0, 255, 0), 1);

// 遍历每个轮廓,测量并标注尺寸

foreach (var contour in contours)

{

// 测量矩形尺寸

var (widthMm, heightMm) = MeasureRectSize(contour);

// 获取轮廓的外接矩形(用于定位文字位置)

Rect rect = Cv2.BoundingRect(contour);

// 在图像上标注宽度

// HersheySimples:一种字体,0.6:字体大小,Scalar(0,0,255):红色

// 1:线宽,LineTypes.AntiAlias:抗锯齿(字体更平滑)

Cv2.PutText(srcMat, $"Wide:{widthMm:F2}mm",new Point(rect.X, rect.Y - 30),HersheyFonts.HersheySimplex, 0.6,new Scalar(0, 0, 255), 1, LineTypes.AntiAlias);

// 在图像上标注高度

Cv2.PutText(srcMat, $"Hight:{heightMm:F2}mm",new Point(rect.X, rect.Y - 10), HersheyFonts.HersheySimplex, 0.6,new Scalar(0, 0, 255), 1, LineTypes.AntiAlias);

}

// (可选)测量圆形尺寸并标注的代码已注释,如需启用,取消注释以下代码块即可

// foreach (var contour in contours)

// {

// var (diameterMm, center) = MeasureCircleSize(contour);

// Cv2.Circle(srcMat, (int)center.X, (int)center.Y, 3,new Scalar(255, 0, 0), -1, LineTypes.AntiAlias);

// Cv2.PutText(srcMat, $"直径:{diameterMm:F2}mm", new Point((int)center.X + 10, (int)center.Y), HersheyFonts.HersheySimplex, 0.6, new Scalar(255, 0, 0), 2);

// }

}

#endregion

}

}

⑵创建命名空间和初始化

using OpenCvSharp;

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using System.Windows.Forms;

using System.IO;

using OpenCvSharp.Extensions;

namespace WinformVisionMeasure

{

public partial class btnSetEquivalent : Form

{

// 公差配置(工业必备,按需修改,比如:螺丝直径公差5.0±0.05mm)

private readonly float toleranceMin = 4.95f;//公差上限

private readonly float toleranceMax = 5.05f;//公差下限

public btnSetEquivalent()

{

InitializeComponent();

// 初始化默认参数

// 初始化默认参数

// 像素当量:用于将像素单位转换为实际物理尺寸(如毫米)

// 当前设置:1像素 = 0.025毫米

//VisionMeasureHelper.PixelEquivalent = 0.025f;

VisionMeasureHelper.PixelEquivalent = 1;

// 阈值:图像二值化时使用的灰度阈值

// 当前设置:灰度值127(范围0-255)

// 用于将灰度图像转换为黑白图像,以便进行轮廓检测

VisionMeasureHelper.ThresholdValue = 127;

// 形态学操作核大小:用于图像的膨胀、腐蚀等操作

// 当前设置:5x5的核

// 用于去除噪点或填充文字/物体内部空洞

VisionMeasureHelper.MorphKernelSize = 5;

// 最小轮廓面积:过滤无效轮廓的阈值

// 当前设置:面积小于100像素的轮廓将被忽略

// 用于去除图像中的小噪点,只保留有意义的轮廓

VisionMeasureHelper.MinContourArea = 100;

}

......//主程序代码

}

}

⑶像素当量标定

private void btnSetEquivalent_Click(object sender, EventArgs e)//像素当量标定

{

// 尝试将文本框输入转换为浮点数

// float.TryParse 安全地尝试解析字符串,避免转换异常

if (float.TryParse(txtPixelEquivalent.Text.Trim(), out float equivalent))

{

// 解析成功,将数值保存到测量助手类中

VisionMeasureHelper.PixelEquivalent = equivalent;

// 在界面上显示标定成功的提示信息

// $ 字符串插值语法,将 equivalent 的值嵌入到字符串中

DefectAlarm.Text = $"✅ 像素当量标定成功:{equivalent} mm/px";

}

else

{

// 解析失败(用户输入了非数字内容),显示警告提示

MessageBox.Show(

"请输入有效的像素当量数值!", // 提示内容

"提示", // 对话框标题

MessageBoxButtons.OK, // 只显示"确定"按钮

MessageBoxIcon.Warning // 显示警告图标

);

};

}

其中:txtPixelEquivalent为像素标定按钮右侧的textbox文本。

⑷尺寸测量

private void btnOpenImg_Click(object sender, EventArgs e)

{

using (OpenFileDialog ofd = new OpenFileDialog())

{

ofd.Filter = "图像文件|*.jpg;*.png;*.bmp;*.jpeg|所有文件|*.*";

if (ofd.ShowDialog() == DialogResult.OK)

{

string imgPath = ofd.FileName;

// 读取图片

Mat srcMat = Cv2.ImRead(imgPath);

pbOriginal.SizeMode = PictureBoxSizeMode.AutoSize;//PictureBox 尺寸 = 图像原始像素尺寸,测量值直接就是像素值,无需换算。

pbOriginal.Image = BitmapConverter.ToBitmap(srcMat);

// 开始计时,统计测量耗时

var watch = System.Diagnostics.Stopwatch.StartNew();

// 步骤1:图像预处理

Mat binaryMat = VisionMeasureHelper.PreprocessImage(srcMat);

// 步骤2:提取目标轮廓

var contours = VisionMeasureHelper.GetTargetContours(binaryMat);

// 步骤3:绘制测量结果+标注尺寸

VisionMeasureHelper.DrawMeasureResult(srcMat, contours);

// 结束计时

watch.Stop();

// 显示处理后的图像+测量结果

pbProcessed.SizeMode = PictureBoxSizeMode.AutoSize;//PictureBox 尺寸 = 图像原始像素尺寸,测量值直接就是像素值,无需换算。

pbProcessed.Image = BitmapConverter.ToBitmap(srcMat);

// 计算并显示测量结果

if (contours.Count > 0)

{

var (widthMm, heightMm) = VisionMeasureHelper.MeasureRectSize(contours[0]);//测量长宽

var (diameterMm, _) = VisionMeasureHelper.MeasureCircleSize(contours[0]);//测量圆直径

float lengthMm = VisionMeasureHelper.MeasureContourLength(contours[0]);//测量轮廓周长

float areaMm = VisionMeasureHelper.MeasureContourArea(contours[0]);//测量轮廓面积

// 工业核心:尺寸超差判断

string judge = (diameterMm >= toleranceMin && diameterMm <= toleranceMax) ? "✅ 合格" : "❌ 不合格(超差)";

// 显示测量信息

//lblMeasureResult.Text = $"宽:{widthMm:F2}mm | 高:{heightMm:F2}mm | 直径:{diameterMm:F2}mm | {judge}";

lblMeasureResult.Text = $"宽:{widthMm:F2}mm | 高:{heightMm:F2}mm";//F2表示保留2位小数

lblOtherInfo.Text = $"周长:{lengthMm:F2}mm | 面积:{areaMm:F2}mm² | 耗时:{watch.ElapsedMilliseconds}ms";

// 超差报警

if (judge.Contains("不合格"))

{

DefectAlarm.Text= $"尺寸超差!实际直径:{diameterMm:F2}mm,公差范围:{toleranceMin}~{toleranceMax}mm";

}

}

else

{

lblMeasureResult.Text = "⚠️ 未检测到零件轮廓,请调节参数!";

}

}

}

}

注意:以上代码演示主要看矩形测量,保留圆等的尺寸测量以便后续开发。

⒍测试

如下图以上文目标内图片进行视觉测量的像素,此时测量值等于像素值,之后根据实际像素/尺寸进行换算即可,即本文提供的代码可用于图像尺寸的测量和提取。

相关推荐
科士威传动18 小时前
微型导轨从精密制造到智能集成的跨越
大数据·运维·科技·机器人·自动化·制造
绿算技术19 小时前
宝辰股份董事长莅临绿算技术调研交流
人工智能·科技·算法
北京耐用通信21 小时前
从隔离到互联:工业现场中耐达讯自动化CC-Link IE转Modbus RTU实战指南
人工智能·科技·物联网·自动化·信息与通信
DX_水位流量监测1 天前
德希科技在线 pH 传感器
人工智能·科技·水质监测·水质传感器·水质厂家·供水水质监测·污水监测
瑞和数智1 天前
案例分享 | 瑞和数智助力某农商行打造标签管理平台
大数据·人工智能·科技·金融
科技前瞻观察1 天前
技术自主、量产突围、产业链协同:宇树科技、优艾智合领衔具身智能TOP20领跑全球
大数据·人工智能·科技
智擎软件测评小祺1 天前
科技查新:检测内容与材料全指南
科技·检测·cma·第三方检测·cnas·科技查新报告
nhc0881 天前
贵阳纳海川科技・上门洗车行业解决方案
科技·微信小程序·软件开发·小程序开发
_codemonster1 天前
逻辑判断题的解题技巧
科技
多年小白1 天前
【无标题】
大数据·人工智能·科技·ai·ai编程