⒈目标
掌握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 = "⚠️ 未检测到零件轮廓,请调节参数!";
}
}
}
}
注意:以上代码演示主要看矩形测量,保留圆等的尺寸测量以便后续开发。
⒍测试
如下图以上文目标内图片进行视觉测量的像素,此时测量值等于像素值,之后根据实际像素/尺寸进行换算即可,即本文提供的代码可用于图像尺寸的测量和提取。

