引言
在现代工业自动化领域,机器视觉技术已成为质量检测的关键手段。本文将详细介绍如何使用HALCON实现高精度的圆测量,分享我在工业视觉项目中的实践经验。
一、项目背景
在机械零件检测中,圆形特征的尺寸精度直接影响产品质量。传统的人工检测方式存在效率低、一致性差等问题。我们开发的这套视觉系统可以实现:
- 自动检测多个圆形零件
- 实时测量半径尺寸
- 自动判断合格与否
- 结果记录与追溯
二、代码实现
点击助手,打开新的Image Acquisition
点击选择路径,选择文件夹
点击代码生成,插入代码
第二句替换一下,以便于后面获取图像的宽度和高度
获取图像宽高,打开新窗口,将图片显示在新窗口,画圆
完整代码
*测量
list_files ('img', ['files','follow_links'], ImageFiles)
read_image (Image, ImageFiles[0])
*获取图像大小
get_image_size (Image, Width, Height)
dev_open_window (0, 0, Width/4, Height/4, 'black', WindowHandle)
*创建测量模型
create_metrology_model (MetrologyHandle)
*设置测量模型的大小
set_metrology_model_image_size (MetrologyHandle, Width, Height)
dev_display (Image)
*画一个圆,获取圆的半径,后面以这个圆的半径来测量零件是否合格
draw_circle (WindowHandle, Row, Column, Radius)
*对多张图像进行测量
for Index := 0 to |ImageFiles|-1 by 1
read_image (ResultImage, ImageFiles[Index])
*阈值分割,挑选出来配件部分
threshold (ResultImage, ResultRegions, 3, 60)
*连通性
connection (ResultRegions, ResultConnectedRegions)
*特征提取:用面积,和圆两个指标提取 area
select_shape (ResultConnectedRegions, ResultSelectedRegions1, ['area','roundness'], 'and', [250000,0.9], [320000,1])
*获取
area_center (ResultSelectedRegions1, ResultArea, ResultRow, ResultColumn)
*设置输出的颜色
dev_set_color ('green')
dev_set_draw ('margin')
dev_display (ResultImage)
*生成圆:注意,这里借助了上方两个特征提取出来的坐标
gen_circle (ResultCircle, ResultRow, ResultColumn, Radius)
*把圆添加到模型中
add_metrology_object_circle_measure (MetrologyHandle, ResultRow, ResultColumn, Radius,100, 5, 1.5, 30, ['measure_transition','min_score'], ['all',0.4], MetrologyCircleIndice)
*测量并拟合集合形状
apply_metrology_model (ResultImage, MetrologyHandle)
*获取测量模型中的测量轮廓,方便后期显示
get_metrology_object_result_contour (ResultContour, MetrologyHandle,'all', 'all', 1.5)
*在轮廓基础上获得此次测量的结果:第二个参数是每个图象的索引
get_metrology_object_result (MetrologyHandle, MetrologyCircleIndice, 'all', 'result_type', 'radius',CircleRadiusResult)
*显示图像
dev_display (ResultImage)
*显示测量轮廓
dev_display (ResultContour)
*设置文字位置
set_tposition (WindowHandle, 0, 0)
*在指定位置显示信息
write_string (WindowHandle, '圆的半径:'+CircleRadiusResult)
stop ()
endfor
*清除测量模型
clear_metrology_model (MetrologyHandle)
三、导出代码,配置文件
点击文件,导出
四、创建Winfrom窗体应用
找到halcon的文件夹,在dotnet35中找到 halcondotnet.dll 在x64里面找到halcon.dll文件
将这两个文件复制到项目的debug目录下
取消勾选32位
设置完成后将测量.cs拖入项目中
画出winfrom窗体
五、代码写入
引入命名空间
using HalconDotNet;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
这些命名空间包含了应用程序所需的类和方法,如Halcon操作符、Windows窗体控件、文件操作等。
类和变量定义
namespace 摄像头
{
public partial class Form1 : Form
{
HObject Image;
HTuple windowID;
Thread t1;
HObject[] imageArr = new HObject[8];
bool t1_Stop = false;
HObject
和HTuple
是Halcon库中用于处理图像和元组数据的类。Image
用于存储单个图像。windowID
用于标识Halcon窗口。t1
是一个线程,用于执行图像测量任务。imageArr
是一个包含8个图像对象的数组。t1_Stop
是一个布尔变量,用于控制线程的停止状态。
图像加载方法
private void LoadBateImage()
{
for (int i = 0; i < 8; i++)
{
HOperatorSet.ReadImage(out imageArr[i], $"img/{i}.bmp");
}
}
LoadBateImage
方法用于从指定路径加载8张图像,并存储到imageArr
数组中。
显示单个图像
private void LoadImage()
{
HOperatorSet.ReadImage(out Image, "img/0.bmp");
HTuple Width = null, Height = null;
HOperatorSet.GetImageSize(Image, out Width, out Height);
HOperatorSet.SetColor(windowID, "green");
HOperatorSet.SetPart(windowID, 0, 0, Height, Width);
HOperatorSet.DispObj(Image, windowID);
}
LoadImage
方法用于加载并显示第一张图像(img/0.bmp
)。
创建Halcon窗口
private void CreateHalconWindow()
{
HTuple winforms = this.pictureBox1.Handle;
HOperatorSet.SetWindowAttr("background_color", "black");
HOperatorSet.OpenWindow(0, 0, this.pictureBox1.Width, this.pictureBox1.Height, winforms, "", "", out windowID);
}
CreateHalconWindow
方法用于在窗体的pictureBox1
控件中创建一个Halcon窗口,并设置背景颜色为黑色。
按钮点击事件处理
停止测量
private void button2_Click(object sender, EventArgs e)
{
t1_Stop = true;
if (!Directory.Exists(dicPatj))
{
Directory.CreateDirectory(dicPatj);
}
else
{
string filePath2 = Path.Combine(dicPatj, filePath);
File.AppendAllText(filePath2, "停止测量" + Environment.NewLine);
}
}
- 当点击
button2
时,设置t1_Stop
为true
,以停止测量线程,并记录“停止测量”信息到文件。
测量半径
private void button3_Click(object sender, EventArgs e)
{
radiu();
}
private double radiu()
{
HOperatorSet.DrawCircle(windowID, out hv_Row, out hv_Column, out hv_Radius);
double radius = hv_Radius;
MessageBox.Show("半径" + radius);
return radius;
}
- 当点击
button3
时,调用radiu
方法。 radiu
方法用于绘制一个圆并获取其半径,随后通过消息框显示半径值。
开始测量
private void button1_Click(object sender, EventArgs e)
{
string fullFilePath = Path.Combine(dicPatj, filePath);
File.WriteAllText(fullFilePath, string.Empty, Encoding.UTF8);
if (t1.ThreadState == ThreadState.Unstarted)
{
t1.Start();
}
if (t1.ThreadState == ThreadState.Stopped || t1.ThreadState == ThreadState.Aborted)
{
t1 = new Thread(new ThreadStart(PlayThread));
t1.Start();
}
if (!Directory.Exists(dicPatj))
{
Directory.CreateDirectory(dicPatj);
}
else
{
string filePath2 = Path.Combine(dicPatj, filePath);
File.AppendAllText(filePath2, "开始测量" + Environment.NewLine);
}
}
- 当点击
button1
时,清空文件内容并开始测量线程t1
。 - 如果线程已经停止或中止,则重新创建并启动线程。
- 记录“开始测量”信息到文件。
退出应用程序
private void button4_Click(object sender, EventArgs e)
{
this.Close();
if (!Directory.Exists(dicPatj))
{
Directory.CreateDirectory(dicPatj);
}
else
{
string filePath2 = Path.Combine(dicPatj, filePath);
File.AppendAllText(filePath2, "退出" + Environment.NewLine);
}
}
- 当点击
button4
时,关闭应用程序,并记录“退出”信息到文件。
测量线程
private void PlayThread()
{
int i = 0;
t1_Stop = false;
HTuple width = null, height = null;
while (!t1_Stop)
{
try
{
HOperatorSet.DispObj(imageArr[i], windowID);
HOperatorSet.GetImageSize(imageArr[i], out width, out height);
HOperatorSet.SetPart(windowID, 0, 0, height, width);
double CriRad = ImageMeasure(imageArr[i]);
if (dataGridView1.InvokeRequired)
{
dataGridView1.Invoke(new Action(() =>
{
bool isOK = (CriRad > hv_Radius.D - 4 && CriRad < hv_Radius.D + 4);
var a = isOK ? "OK" : "NG";
dataGridView1.Rows.Add(i, CriRad.ToString("F2"), isOK ? "OK" : "NG");
dataGridView1.FirstDisplayedScrollingRowIndex = dataGridView1.Rows.Count - 1;
if (!Directory.Exists(dicPatj))
{
Directory.CreateDirectory(dicPatj);
}
else
{
string filePath2 = Path.Combine(dicPatj, filePath);
File.AppendAllText(filePath2, "编号" + i + " 半径" + CriRad.ToString("F2") + " 结果" + a + Environment.NewLine);
}
}));
}
Thread.Sleep(1000);
i = (i + 1) % 7; // 循环0-6
}
catch (Exception ex)
{
if (!this.IsDisposed)
{
this.Invoke(new Action(() =>
{
MessageBox.Show($"发生错误: {ex.Message}");
}));
}
break;
}
}
}
PlayThread
方法在一个独立的线程中循环显示和测量imageArr
中的图像。- 每次循环中,显示图像、获取图像尺寸并设置显示区域,然后调用
ImageMeasure
方法测量图像中圆的半径。 - 测量结果被添加到
dataGridView1
中,并同时记录到文件中。 - 如果发生异常,显示错误信息并停止线程。
测量图像中的圆的半径
private double ImageMeasure(HObject ho_ResultImage)
{
HObject ho_Image, ho_ResultRegions = null, ho_Connection = null;
HObject ho_SelectedRegions = null, ho_Circle = null, ho_Contour = null;
HTuple hv_ImageFiles = null, hv_Width = null, hv_Height = null, hv_WindowHandle = null, hv_MetrologyHandle = null;
HTuple hv_NewRadius = null, hv_CirCleRadiusTolerance = null;
HTuple hv_Index = null, hv_Area = new HTuple(), hv_Row = new HTuple();
HTuple hv_Column = new HTuple(), hv_MetrologyCircleIndice = new HTuple();
HTuple hv_Parameter = new HTuple();
HOperatorSet.GenEmptyObj(out ho_Image);
HOperatorSet.GenEmptyObj(out ho_ResultRegions);
HOperatorSet.GenEmptyObj(out ho_Connection);
HOperatorSet.GenEmptyObj(out ho_SelectedRegions);
HOperatorSet.GenEmptyObj(out ho_Circle);
HOperatorSet.GenEmptyObj(out ho_Contour);
HOperatorSet.GetImageSize(ho_ResultImage, out hv_Width, out hv_Height);
HOperatorSet.CreateMetrologyModel(out hv_MetrologyHandle);
HOperatorSet.SetMetrologyModelImageSize(hv_MetrologyHandle, hv_Width, hv_Height);
hv_NewRadius = hv_Radius.D;
hv_CirCleRadiusTolerance = 100;
ho_ResultRegions.Dispose();
HOperatorSet.Threshold(ho_ResultImage, out ho_ResultRegions, 2, 58);
ho_Connection.Dispose();
HOperatorSet.Connection(ho_ResultRegions, out ho_Connection);
ho_SelectedRegions.Dispose();
HOperatorSet.SelectShape(ho_Connection, out ho_SelectedRegions, (new HTuple("area")).TupleConcat("roundness"), "and", (new HTuple(249155)).TupleConcat(0.8473), (new HTuple(316723)).TupleConcat(1));
HOperatorSet.AreaCenter(ho_SelectedRegions, out hv_Area, out hv_Row, out hv_Column);
HOperatorSet.SetColor(windowID, "green");
HOperatorSet.SetDraw(windowID, "margin");
HOperatorSet.DispObj(ho_SelectedRegions, windowID);
ho_Circle.Dispose();
HOperatorSet.GenCircle(out ho_Circle, hv_Row, hv_Column, hv_NewRadius);
HOperatorSet.AddMetrologyObjectCircleMeasure(hv_MetrologyHandle, hv_Row, hv_Column, hv_NewRadius, hv_CirCleRadiusTolerance, 5, 1.5, 30, (new HTuple("measure_transition")).TupleConcat("min_score"), (new HTuple("all")).TupleConcat(0.4), out hv_MetrologyCircleIndice);
HOperatorSet.ApplyMetrologyModel(ho_ResultImage, hv_MetrologyHandle);
ho_Contour.Dispose();
HOperatorSet.GetMetrologyObjectResultContour(out ho_Contour, hv_MetrologyHandle, "all", "all", 1.5);
HOperatorSet.GetMetrologyObjectResult(hv_MetrologyHandle, hv_MetrologyCircleIndice, "all", "result_type", "radius", out hv_Parameter);
HOperatorSet.DispObj(ho_ResultImage, windowID);
HOperatorSet.DispObj(ho_Contour, windowID);
HOperatorSet.SetTposition(windowID, 0, 0);
HOperatorSet.WriteString(windowID, "圆的半径:" + hv_Parameter);
HOperatorSet.ClearMetrologyModel(hv_MetrologyHandle);
ho_Image.Dispose();
ho_ResultRegions.Dispose();
ho_Connection.Dispose();
ho_SelectedRegions.Dispose();
ho_Circle.Dispose();
ho_Contour.Dispose();
return hv_Parameter;
}
CopyInsert
ImageMeasure
方法用于测量给定图像中的圆的半径。- 首先对图像进行阈值分割、连接和形状选择,以提取感兴趣的圆区域。
- 然后创建一个测量模型,并将提取的圆区域添加到模型中。
- 应用测量模型,获取测量结果,并显示在Halcon窗口中。
- 最后,清空测量模型中的数据,释放资源并返回测量结果。
六、完整代码
using HalconDotNet;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 摄像头
{
public partial class Form1 : Form
{
HObject Image;
HTuple windowID;
Thread t1;
//优化了Halcon中的数组
HObject[] imageArr = new HObject[8];
bool t1_Stop = false;//线程的状态
private void LoadBateImage()
{
for (int i = 0; i < 8; i++)
{
HOperatorSet.ReadImage(out imageArr[i], $"img/{i}.bmp");
}
}
private void LoadImage()
{
HOperatorSet.ReadImage(out Image, "img/0.bmp");
HTuple Width = null, Height = null;
HOperatorSet.GetImageSize(Image, out Width, out Height);
HOperatorSet.SetColor(windowID, "green");
HOperatorSet.SetPart(windowID, 0, 0, Height, Width);
HOperatorSet.DispObj(Image, windowID);
}
private void CreateHalconWindow()
{
//注意这里没有hWindowControl控件了,变成了pictureBox
HTuple winforms = this.pictureBox1.Handle;
HOperatorSet.SetWindowAttr("background_color", "black");
//这句代码决定了现在所有视图在picture展示,教育windowsID控制picture
HOperatorSet.OpenWindow(0, 0, this.pictureBox1.Width, this.pictureBox1.Height, winforms, "", "", out windowID);
}
private void button2_Click(object sender, EventArgs e)
{
t1_Stop = true;
if (!Directory.Exists(dicPatj))
{
Directory.CreateDirectory(dicPatj);
}
else
{
string filePath2 = Path.Combine(dicPatj, filePath);
File.AppendAllText(filePath2, "停止测量" + Environment.NewLine);
}
}
private void Form1_Load(object sender, EventArgs e)
{
//读图
LoadBateImage();
//加载窗体
CreateHalconWindow();
LoadImage();
t1 = new Thread(new ThreadStart(PlayThread));
}
HTuple hv_ImageFiles = null, hv_Index = null, hv_Area = new HTuple();
HTuple hv_Row = new HTuple(), hv_Column = new HTuple();
HTuple hv_Row2 = new HTuple(), hv_Column2 = new HTuple();
HTuple hv_Radius = new HTuple();
HTuple hv_Parameter = new HTuple();
private void button3_Click(object sender, EventArgs e)
{
radiu();
}
private double radiu()
{
HOperatorSet.DrawCircle(windowID, out hv_Row, out hv_Column, out hv_Radius);
double radius = hv_Radius;
MessageBox.Show("半径"+radius);
return radius;
}
public Form1()
{
InitializeComponent();
}
#region 开始测量
private void button1_Click(object sender, EventArgs e)
{
string fullFilePath = Path.Combine(dicPatj, filePath);
File.WriteAllText(fullFilePath, string.Empty, Encoding.UTF8);
if (t1.ThreadState == ThreadState.Unstarted)
{
t1.Start();
}
//如果线程停止或者线程为空
if (t1.ThreadState == ThreadState.Stopped ||
t1.ThreadState == ThreadState.Aborted)
{
//开启线程进行测量
t1 = new Thread(new ThreadStart(PlayThread));
t1.Start();
}
if (!Directory.Exists(dicPatj))
{
Directory.CreateDirectory(dicPatj);
}
else
{
string filePath2 = Path.Combine(dicPatj, filePath);
File.AppendAllText(filePath2, "开始测量"+ Environment.NewLine);
}
}
string dicPatj = @"测量";
string filePath = @"OKorNG.txt";
private void PlayThread()
{
int i = 0;
t1_Stop = false;
HTuple width = null, height = null;
while (!t1_Stop)
{
try
{
// 显示图像
HOperatorSet.DispObj(imageArr[i], windowID);
// 获取图像尺寸并设置显示区域
HOperatorSet.GetImageSize(imageArr[i], out width, out height);
HOperatorSet.SetPart(windowID, 0, 0, height, width);
// 测量半径
double CriRad = ImageMeasure(imageArr[i]);
// 安全更新UI
if (dataGridView1.InvokeRequired)
{
dataGridView1.Invoke(new Action(() =>
{
// 检查半径是否在允许范围内
bool isOK = (CriRad > hv_Radius.D - 4 && CriRad < hv_Radius.D + 4);
var a = isOK ? "OK" : "NG";
// 添加数据到DataGridView
dataGridView1.Rows.Add(
i, // 序号
CriRad.ToString("F2"), // 半径值(保留2位小数)
isOK ? "OK" : "NG" // 结果
);
// 自动滚动到最后一行
dataGridView1.FirstDisplayedScrollingRowIndex = dataGridView1.Rows.Count - 1;
if (!Directory.Exists(dicPatj))
{
Directory.CreateDirectory(dicPatj);
}
else
{
string filePath2 = Path.Combine(dicPatj, filePath);
File.AppendAllText(filePath2,"编号"+i+" 半径"+ CriRad.ToString("F2")+" 结果"+a + Environment.NewLine);
}
}));
}
Thread.Sleep(1000);
i = (i + 1) % 7; // 循环0-6
}
catch (Exception ex)
{
// 错误处理
if (!this.IsDisposed)
{
this.Invoke(new Action(() =>
{
MessageBox.Show($"发生错误: {ex.Message}");
}));
}
break;
}
}
}
//获取模型测量的结果
private double ImageMeasure(HObject ho_ResultImage)
{
HObject ho_Image, ho_ResultRegions = null, ho_Connection = null;
HObject ho_SelectedRegions = null, ho_Circle = null, ho_Contour = null;
// Local control variables
HTuple hv_ImageFiles = null, hv_Width = null;
HTuple hv_Height = null, hv_WindowHandle = null, hv_MetrologyHandle = null;
HTuple hv_NewRadius = null, hv_CirCleRadiusTolerance = null;
HTuple hv_Index = null, hv_Area = new HTuple(), hv_Row = new HTuple();
HTuple hv_Column = new HTuple(), hv_MetrologyCircleIndice = new HTuple();
HTuple hv_Parameter = new HTuple();
// Initialize local and output iconic variables
HOperatorSet.GenEmptyObj(out ho_Image);
HOperatorSet.GenEmptyObj(out ho_ResultRegions);
HOperatorSet.GenEmptyObj(out ho_Connection);
HOperatorSet.GenEmptyObj(out ho_SelectedRegions);
HOperatorSet.GenEmptyObj(out ho_Circle);
HOperatorSet.GenEmptyObj(out ho_Contour);
HOperatorSet.GetImageSize(ho_ResultImage, out hv_Width, out hv_Height);
HOperatorSet.CreateMetrologyModel(out hv_MetrologyHandle);
//设置测量模型的大小
HOperatorSet.SetMetrologyModelImageSize(hv_MetrologyHandle, hv_Width, hv_Height);
//定义圆的半径:注意,变得是这个值
hv_NewRadius = hv_Radius.D;
//也可以配置其他的参数,例如测量对象的宽度允许范围
hv_CirCleRadiusTolerance = 100;
//阈值分割,挑选出来配件部分
ho_ResultRegions.Dispose();
HOperatorSet.Threshold(ho_ResultImage, out ho_ResultRegions, 2, 58);
ho_Connection.Dispose();
HOperatorSet.Connection(ho_ResultRegions, out ho_Connection);
ho_SelectedRegions.Dispose();
HOperatorSet.SelectShape(ho_Connection, out ho_SelectedRegions, (new HTuple("area")).TupleConcat(
"roundness"), "and", (new HTuple(249155)).TupleConcat(0.8473), (new HTuple(316723)).TupleConcat(
1));
//获取
HOperatorSet.AreaCenter(ho_SelectedRegions, out hv_Area, out hv_Row, out hv_Column);
HOperatorSet.SetColor(windowID, "green");
HOperatorSet.SetDraw(windowID, "margin");
HOperatorSet.DispObj(ho_SelectedRegions, windowID);
//生成圆
ho_Circle.Dispose();
HOperatorSet.GenCircle(out ho_Circle, hv_Row, hv_Column, hv_NewRadius);
//把圆添加到模型中 ['measure_transition','min_score']允许小范围的缩放平移旋转 后面[]是分数
HOperatorSet.AddMetrologyObjectCircleMeasure(hv_MetrologyHandle, hv_Row, hv_Column,
hv_NewRadius, hv_CirCleRadiusTolerance, 5, 1.5, 30, (new HTuple("measure_transition")).TupleConcat(
"min_score"), (new HTuple("all")).TupleConcat(0.4), out hv_MetrologyCircleIndice);
//测量并拟合集合形状
HOperatorSet.ApplyMetrologyModel(ho_ResultImage, hv_MetrologyHandle);
//获取测量模型中的测量轮廓,方便后期显示
ho_Contour.Dispose();
HOperatorSet.GetMetrologyObjectResultContour(out ho_Contour, hv_MetrologyHandle,
"all", "all", 1.5);
//在轮廓基础上获得此次测量的结果:第二个参数是每个图像的索引
HOperatorSet.GetMetrologyObjectResult(hv_MetrologyHandle, hv_MetrologyCircleIndice,
"all", "result_type", "radius", out hv_Parameter);
HOperatorSet.DispObj(ho_ResultImage, windowID);
HOperatorSet.DispObj(ho_Contour, windowID);
HOperatorSet.SetTposition(windowID, 0, 0);
HOperatorSet.WriteString(windowID, "圆的半径:" + hv_Parameter);
//清除测量模型
HOperatorSet.ClearMetrologyModel(hv_MetrologyHandle);
ho_Image.Dispose();
ho_ResultRegions.Dispose();
ho_Connection.Dispose();
ho_SelectedRegions.Dispose();
ho_Circle.Dispose();
ho_Contour.Dispose();
//返回结果
return hv_Parameter;
}
#endregion
private void button4_Click(object sender, EventArgs e)
{
//退出
this.Close();
if (!Directory.Exists(dicPatj))
{
Directory.CreateDirectory(dicPatj);
}
else
{
string filePath2 = Path.Combine(dicPatj, filePath);
File.AppendAllText(filePath2, "退出" + Environment.NewLine);
}
}
}
}
七、效果展示
先点交互测量,点击鼠标左键在图像中画圆,右键结束
获取到半径
点击开始,按照获取到的半径,检测圆是否合格,并将结果显示在左侧
在项目debug目录下可以查看日志