四.工件位置检测算法
我们希望得到模块右下角第一个插座中的最右侧的插针位置P(m,n)及该插座相对于夹具的角度a。如图31所示。图31 插针坐标与工件的夹角
因为插针有一定的长度,且镜头有变形,直接检测插针前端面的位置偏差较大;且插针前端面的面积小、形状一致性不好,不容易检测。
经分析,插座底部塑料基座的形状特征明显,采用形状匹配函数检测插座底部图像,间接检测插针位置检测,试验证明成功率高。
为提高检测精度和可靠性,采用形状匹配函数检测三个特征图像的位置,并用其中2个位置坐标计算插针坐标P和工件夹角a。这样比仅检测一个特征图像的位置和转角的方法可靠。
1. 偏移量(offset)的设置与计算图32 插针位置的偏移量
如图32所示,点A、C为最右边的2个检测结果的中心坐标。P点是最右侧的插针位置。
1)设置偏移量
已知:a, b, c, d, m, n
求:q, L
由:k1 = (n - b) / (m - a)
k2 = (b - d) / (a - c)
得:q = arctan( (k1 – k2) / (1 + k1 X k2) ) (1)
及:
q、L是相对于插座的尺寸,与工件夹角a无关。
因此,选取好模板、进行一次匹配后,只需手动设置一次插针位置P(m,n);之后,进行插针位置检测时,用A点、C点的坐标及q、L,即可求出P和a。
2)计算偏移量
已知:a, b, c, d, q, L
求:a, m, n
a = arctan((b - d) / (a - c)) (3)
b = q + a
m = a + L X cos(b) (4)
n = b + L X sin(b)
1. C#程序设计
1)将HDevelop的程序My_shape_match导出C#代码如下:
//
// File generated by HDevelop for HALCON/DOTNET (C#) Version 10.0
//
// This file is intended to be used with the HDevelopTemplate or
// HDevelopTemplateWPF projects located under %HALCONEXAMPLES%\c#
using System;
using HalconDotNet;
public partial class HDevelopExport
{
public HTuple hv_ExpDefaultWinHandle;
// Procedures
// External procedures
// Chapter: Matching / Shape-Based
// Short Description: Display the results of Shape-Based Matching.
public void dev_display_shape_matching_results (HTuple hv_ModelID, HTuple hv_Color, HTuple hv_Row, HTuple hv_Column, HTuple hv_Angle, HTuple hv_ScaleR, HTuple hv_ScaleC, HTuple hv_Model)
{
// 该方法的功能是:显示形状匹配的结果。
// 对应于HDevelop中的函数dev_display_shape_matching_results()
// 不使用该方法,程序略。
}
// Chapter: Graphics / Text
// Short Description: This procedure writes a
text message.
public void disp_message (HTuple hv_WindowHandle, HTuple hv_String, HTuple hv_CoordSystem, HTuple hv_Row, HTuple hv_Column, HTuple
hv_Color, HTuple hv_Box)
{
// 该方法的功能是:在窗体中显示文本信息。
// 对应于HDevelop中的函数disp_message()
// 不使用该方法,程序略。
}
// Main procedure
private void action()
{
HSystem sys = new HSystem();
// Local iconic variables
HObject ho_Image=null, ho_Rectangle=null,
ho_ImageReduced=null;
HObject ho_Mask, ho_Cross;
// Local control variables
HTuple hv_Error = null;
HTuple hv_AcqHandle, hv_WindowID=new HTuple();
HTuple hv_Button, hv_R=new HTuple(), hv_C=new HTuple();
HTuple hv_Row1=new HTuple(), hv_Column1=new HTuple(), hv_Row2=new HTuple();
HTuple hv_Column2=new HTuple(), hv_ModelID,
hv_S1, hv_Row;
HTuple hv_Column, hv_Angle, hv_Score, hv_S2, hv_Runtime;
HTuple hv_y0, hv_y1, hv_x0, hv_x1;
// Initialize local and output iconic variables
HOperatorSet.GenEmptyObj(out ho_Image);
HOperatorSet.GenEmptyObj(out ho_Rectangle);
HOperatorSet.GenEmptyObj(out ho_ImageReduced);
HOperatorSet.GenEmptyObj(out ho_Mask);
HOperatorSet.GenEmptyObj(out ho_Cross);
try
{
HOperatorSet.CloseAllFramegrabbers();
//open camera with default settings:
HOperatorSet.OpenFramegrabber("DahengCAM", 1, 1, 0, 0, 0, 0,
"default", -1, "default", -1, "default", "default", "default", -1, -1, out
hv_AcqHandle);
//open a window
//dev_close_window(...);
//dev_open_window(...);
//Define the region fill mode as margin
HOperatorSet.SetDraw(hv_ExpDefaultWinHandle, "margin");
hv_Button = 0;
while ((int)(new HTuple(hv_Button.TupleEqual(0))) != 0)
{
// Grabbing images from a Daheng USB 2.0 camera
ho_Image.Dispose();
HOperatorSet.GrabImage(out ho_Image, hv_AcqHandle);
HOperatorSet.DispObj(ho_Image, hv_ExpDefaultWinHandle);
disp_message(hv_ExpDefaultWinHandle, "Load a old template press LEFT key, Set a new template press middle key", "window", 12, 12, "black", "true");
//draw a rectangle to select region of testing.
HOperatorSet.DispRectangle1(hv_ExpDefaultWinHandle, 320, 250, 630, 750);
// 扫描鼠标按键
{
// 不使用该方法,程序略。
}
}
// 鼠标中键按下,设置新的形状模板
if ((int)(new HTuple(hv_Button.TupleEqual(2))) != 0)
{
disp_message(hv_ExpDefaultWinHandle, "draw rectangle for shape model. ",
"window", 12, 12, "black", "true");
HOperatorSet.DrawRectangle1(hv_ExpDefaultWinHandle, out hv_Row1, out
hv_Column1, out hv_Row2, out hv_Column2);
ho_Rectangle.Dispose();
HOperatorSet.GenRectangle1(out ho_Rectangle, hv_Row1, hv_Column1, hv_Row2,
hv_Column2);
ho_ImageReduced.Dispose();
HOperatorSet.ReduceDomain(ho_Image, ho_Rectangle, out ho_ImageReduced);
HOperatorSet.WriteImage(ho_ImageReduced, "png", 0, "D:/Vision/MySample/MyShapeMatch/Image006.png");
}
// 鼠标左键按下,读取硬盘中已有的形状模板
if ((int)(new HTuple(hv_Button.TupleEqual(1))) != 0)
{
disp_message(hv_ExpDefaultWinHandle, "Load a old template ",
"window", 12, 12, "black", "true");
ho_ImageReduced.Dispose();
HOperatorSet.ReadImage(out ho_ImageReduced, "Image006.png");
}
// 生成一个待检测的小区域,被检测区域的图像名为Mask
ho_Rectangle.Dispose();
HOperatorSet.GenRectangle1(out ho_Rectangle, 320, 250, 630, 750);
ho_Mask.Dispose();
HOperatorSet.ReduceDomain(ho_Image, ho_Rectangle, out ho_Mask);
// 生成形状模板、形状匹配
HOperatorSet.CreateShapeModel(ho_ImageReduced, "auto", (new
HTuple(-45)).TupleRad() , (new HTuple(90)).TupleRad(), "auto", "auto", "use_polarity", "auto", "auto", out hv_ModelID);
HOperatorSet.CountSeconds(out hv_S1);
HOperatorSet.FindShapeModel(ho_Mask, hv_ModelID, (new HTuple(-45)).TupleRad()
, (new HTuple(90)).TupleRad(), 0.5, 3, 0.0, "least_squares", 0, 0.5, out hv_Row,
out hv_Column, out hv_Angle, out hv_Score);
HOperatorSet.CountSeconds(out hv_S2);
hv_Runtime = (hv_S2-hv_S1)*1000;
// 显示匹配结果
HOperatorSet.DispRectangle1(hv_ExpDefaultWinHandle, 320, 250, 630, 750);
dev_display_shape_matching_results(hv_ModelID, "green", hv_Row, hv_Column,
hv_Angle, 1, 1, 0);
ho_Cross.Dispose();
HOperatorSet.GenCrossContourXld(out ho_Cross, hv_Row, hv_Column, 26, (new HTuple(45)).TupleRad() );
HOperatorSet.SetColor(hv_ExpDefaultWinHandle, "red");
HOperatorSet.DispObj(ho_Cross, hv_ExpDefaultWinHandle);
hv_y0 = hv_Row[0];
hv_y1 = hv_Row[1];
hv_x0 = hv_Column[0];
hv_x1 = hv_Column[1];
HOperatorSet.DispLine(hv_ExpDefaultWinHandle, hv_y0, hv_x0, hv_y1,
hv_x1);
disp_message(hv_ExpDefaultWinHandle, ((new HTuple(hv_Score.TupleLength())+" shapes located in ")+(hv_Runtime.TupleString( ".1f")))+" ms ",
"window", 12, 12, "black", "true");
HOperatorSet.ClearShapeModel(hv_ModelID);
HOperatorSet.CloseFramegrabber(hv_AcqHandle);
}
catch (HalconException
HDevExpDefaultException)
{
ho_Image.Dispose();
ho_Rectangle.Dispose();
ho_ImageReduced.Dispose();
ho_Mask.Dispose();
ho_Cross.Dispose();
throw HDevExpDefaultException;
}
ho_Image.Dispose();
ho_Rectangle.Dispose();
ho_ImageReduced.Dispose();
ho_Mask.Dispose();
ho_Cross.Dispose();
}
public void InitHalcon()
{
// Default settings used in HDevelop
HOperatorSet.SetSystem("do_low_error", "false");
}
public void RunHalcon(HTuple Window)
{
hv_ExpDefaultWinHandle = Window;
action();
}
}
1)将导出的C#代码整理编写几个功能独立的方法
如本文第二节“HALCON与C#混合编程的方法”所述,在Program.cs程序中编写类HDevelopExport;然后在其中根据导出的C#代码编写以下几个方法:halcon初始化InitHalcon();摄像头初始化InitCamera(HTuple Window);采集图像、显示图像GrabAndDisplay();设置模板SetTemplate();从文件中读取模板ReadTemplate();模板匹配MatchTemplate();显示偏移点位置DisplayOffset();显示计算角度,划线DisplayLine();关闭相机CloseCamera()。
形状匹配函数的参数设置与本文三.5.节的相同。
Program.cs程序如下所示:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using HalconDotNet;
namespace MatchMyTemplate
{
public partial class HDevelopExport
{
// 定义变量
public HTuple hv_ExpDefaultWinHandle;
HObject ho_Image, ho_Rectangle, ho_ImageReduced;
HObject ho_ModelContours;
HObject ho_Mask;
HTuple hv_AcqHandle;
HTuple hv_S1, hv_S2;
HTuple hv_Width, hv_Height;
HTuple hv_ModelID, hv_Row, hv_Column;
HTuple hv_Row1, hv_Column1, hv_Row2, hv_Column2;
HTuple hv_Angle, hv_Score, hv_Runtime;
HTuple hv_R;
public double[] Px;
public double[] Py;
public int ResultN;
public double usedTime;
public double Cx,Cy;
public double Fx,Fy;
public double Cr = 9;
public void InitHalcon() // 初始化halcon
{
HOperatorSet.SetSystem("do_low_error", "false");
}
public void InitCamera(HTuple Window) // 摄像头初始化
{
hv_ExpDefaultWinHandle = Window;
HOperatorSet.GenEmptyObj(out ho_Image); // 生ho_Image数据区
// Grabbing images from a Daheng USB 2.0 camera
HOperatorSet.CloseAllFramegrabbers();
HOperatorSet.OpenFramegrabber("DahengCAM", 1, 1, 0, 0, 0, 0, "default", -1, "default",-1, "default", "default", "default", -1, -1, out hv_AcqHandle);
//open camera with default settings
}
public void GrabAndDisplay() // 采集图像、显示图像
{
ho_Image.Dispose(); // 清除ho_Image中的数据
HOperatorSet.GrabImage(out ho_Image, hv_AcqHandle); // 采集图像
HOperatorSet.GetImageSize(ho_Image, out hv_Width, out hv_Height); // 获取图片的尺寸
HOperatorSet.SetPart(hv_ExpDefaultWinHandle, 0, 0, hv_Height - 1, hv_Width - 1);
HOperatorSet.DispObj(ho_Image, hv_ExpDefaultWinHandle);
// 显示ho_Image中的图片
HOperatorSet.SetDraw(hv_ExpDefaultWinHandle,"margin"); // 填充模式为只画框
HOperatorSet.SetColor(hv_ExpDefaultWinHandle,"red"); // 画线颜色红
HOperatorSet.DispRectangle1(hv_ExpDefaultWinHandle,320, 250, 630, 750);
}
public void SetTemplate() // 设置模板
{
MessageBox.Show("在红框中按下鼠标左键画方框选模板,按右键结束");
HOperatorSet.DrawRectangle1(hv_ExpDefaultWinHandle,
out hv_Row1, out hv_Column1, out hv_Row2, out hv_Column2);
HOperatorSet.GenRectangle1(out ho_Rectangle, hv_Row1, hv_Column1, hv_Row2,
hv_Column2);
HOperatorSet.ReduceDomain(ho_Image, ho_Rectangle, out ho_ImageReduced);
HOperatorSet.WriteImage(ho_ImageReduced, "png", 0, "D:/Vision/MySample/ShapeMatch/Image005.png");
MessageBox.Show("模板已保存");
}
public void CloseCamera() // 关闭相机
{
HOperatorSet.CloseFramegrabber(hv_AcqHandle);
}
public void ReadTemplate() // 从文件中读取模板
{
HOperatorSet.GenEmptyObj(out ho_ImageReduced);
ho_ImageReduced.Dispose();
HOperatorSet.ReadImage(out ho_ImageReduced, "D:/Vision/MySample/ShapeMatch/Image005.png");
}
public void MatchTemplate() // 模板匹配
{
int i,j;
double temp;
HOperatorSet.GenRectangle1(out ho_Rectangle, 320, 250, 630, 750);
HOperatorSet.ReduceDomain(ho_Image, ho_Rectangle, out ho_Mask);
//Reduce image range
HOperatorSet.CreateShapeModel(ho_ImageReduced, "auto", (new HTuple(-45)).TupleRad() ,(new HTuple(90)).TupleRad(),
"auto", "auto","use_polarity", "auto", "auto",out hv_ModelID);
HOperatorSet.CountSeconds(out hv_S1); // Match start
HOperatorSet.FindShapeModel(ho_Mask,hv_ModelID, (new HTuple(-45)).TupleRad()
,(new HTuple(90)).TupleRad(),0.5, 3, 0.0, " least_squares ", 0,0.5, out hv_Row,
out hv_Column, outhv_Angle, out hv_Score);
HOperatorSet.CountSeconds(out hv_S2); // Match stop
hv_Runtime = (hv_S2 - hv_S1) * 1000;
usedTime = hv_Runtime;
ResultN = new HTuple(hv_Row.TupleLength());
// 获取匹配结果个数
if (ResultN >1)
{
Px = new double[ResultN];
Py = new double[ResultN];
hv_R = new HTuple(); //HTuple变量初始化
for (i = 0; i < ResultN; i++)
{
Py[i] = hv_Row[i]; // 将搜索结果的中心坐标读出
Px[i] = hv_Column[i];
Aa[i] = hv_Angle[i];
hv_R[i] = 8; //设置圆半径
}
for (i = 0; i < ResultN; i++) // 从大到小排序
{
for (j = i + 1; j < ResultN; j++)
{
if (Px[i] < Px[j])
{
temp = Px[i];
Px[i] = Px[j];
Px[j] = temp;
temp = Py[i];
Py[i] = Py[j];
Py[j] = temp;
temp = Aa[i];
Aa[i] = Aa[j];
Aa[j] = temp;
}
}
}
HOperatorSet.SetColor(hv_ExpDefaultWinHandle,"red"); // 显示匹配结果位置
HOperatorSet.DispCircle(hv_ExpDefaultWinHandle,hv_Row, hv_Column, hv_R);
HOperatorSet.ClearShapeModel(hv_ModelID);
Cy = Py[0]+ 10; // offset的初始值
Cx = Px[0]+ 60;
}
else
{
MessageBox.Show("匹配失败!?");
}
}
public void DisplayOffset() // 显示偏移点位置
{
HOperatorSet.SetColor(hv_ExpDefaultWinHandle, "red");
HOperatorSet.DispObj(ho_Image,hv_ExpDefaultWinHandle);
HOperatorSet.DispCircle(hv_ExpDefaultWinHandle,hv_Row, hv_Column, hv_R);
HOperatorSet.DispLine(hv_ExpDefaultWinHandle,hv_Row[0], hv_Column[0], hv_Row[1], hv_Column[1]);
HOperatorSet.SetColor(hv_ExpDefaultWinHandle,"yellow");
HOperatorSet.DispCircle(hv_ExpDefaultWinHandle,Cy, Cx, Cr);
}
public void DisplayLine() //显示计算角度,划线
{
HOperatorSet.SetColor(hv_ExpDefaultWinHandle, "yellow");
HOperatorSet.DispLine(hv_ExpDefaultWinHandle, Cy, Cx, Fy, Fx);
}
}
static class Program
{
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}
2)设计窗体及代码
设计窗体如图33所示。Form1.cs程序代码如下:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace MatchMyTemplate
{
public partial class Form1 : Form
{
HDevelopExport hd = new HDevelopExport();
public double Lam,
theta;
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e) // 打开相机
{
hd.InitCamera(hWindowControl1.HalconWindow); // 摄像头初始化
timer1.Enabled = true; // 开始定时拍照显示
textBox1.Text = "";
textBox1.Refresh();
}
private void timer1_Tick(object sender, EventArgs e) // 定时拍照、显示
{
hd.GrabAndDisplay();
}
private void button3_Click(object sender, EventArgs e) // 关闭相机
{
imer1.Enabled = false;
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e) // 关闭窗体
{
timer1.Enabled = false; // 停止拍照
hd.CloseCamera();
}
private void button2_Click(object sender, EventArgs e) // 图像匹配、计算2点距离
{
int j, m;
double d;
double[] x;
double[] y;
timer1.Enabled = false; // 停止拍照
hd.MatchTemplate(); // 图像匹配
button2.Visible = false;
m = hd.ResultN;
x = new double[m];
y = new double[m];
for (j = 0; j < m; j++) // 获取匹配坐标
{
x[j] = hd.Px[j];
y[j] = hd.Py[j];
}
textBox1.Text = "匹配时间:" + hd.usedTime.ToString("#####.##")
+ " ms" + "\r\n" + "\r\n";
for (j = 0; j < m; j++)
{
if (j < m - 1) // 计算2点距离
{
d = (y[j + 1] - y[j]) *(y[j + 1] - y[j]) + (x[j + 1] - x[j]) * (x[j + 1] - x[j]);
d = Math.Sqrt(d);
d = 0.0485 * d;
textBox1.Text =textBox1.Text + "d(" + Convert.ToString(j + 1) + ")
= " + d.ToString("#####.##")+ "mm" + "\r\n";
}
textBox1.Text = textBox1.Text +"x("+ Convert.ToString(j + 1) + ") = " + Convert.ToString(x[j])+" y(" + Convert.ToString(j+ 1) + ") = " + Convert.ToString(y[j]) + "\r\n"+ "\r\n";
}
}
private void button5_Click(object sender, EventArgs e) // 读模板
{
hd.ReadTemplate();
button2.Visible = true;
}
private void button4_Click(object sender, EventArgs e) // 设置模板
{
timer1.Enabled = false; // 停止拍照
hd.SetTemplate();
}
private void Form1_Load(object sender, EventArgs e) // 载入窗体
{
button2.Visible = false;
}
private void button7_Click(object sender, EventArgs e) // 计算偏移点位置
{
double a, b, c, d, m, n, k1, k2;
a = hd.Px[0];
b = hd.Py[0];
c = hd.Px[1];
d = hd.Py[1];
m = hd.Cx;
n = hd.Cy;
k1 = (b - n) / (a - m);
k2 = (d - b) / (c - a);
Lam = Math.Sqrt((b - n) * (b - n) +(a - m) * (a - m));
theta = Math.Atan((k1 - k2) / (1 + k1* k2));
}
private void button8_Click(object sender, EventArgs e) // 偏移点上移
{
hd.Cy = hd.Cy - 1;
hd.DisplayOffset();
}
private void button9_Click(object sender, EventArgs e) // 偏移点下移
{
hd.Cy = hd.Cy + 1;
hd.DisplayOffset();
}
private void button10_Click(object sender, EventArgs e) // 偏移点左移
{
hd.Cx = hd.Cx - 1;
hd.DisplayOffset();
}
private void button11_Click(object sender, EventArgs e) // 偏移点右移
{
hd.Cx = hd.Cx + 1;
hd.DisplayOffset();
}
private void button6_Click(object sender, EventArgs e) // 计算、显示插针位置和夹角
{
double a, b, c, d, m, n;
double alpha, beta;
a = hd.Px[0];
b = hd.Py[0];
c = hd.Px[1];
d = hd.Py[1];
alpha = Math.Atan((d - b) / (c - a));
beta = theta + alpha ;
m = a + Lam * Math.Cos(beta);
n = b + Lam * Math.Sin(beta);
textBox1.Text = textBox1.Text + " m = " + Convert.ToString(m) + "\r\n" + "
n = " + Convert.ToString(n) + "\r\n" + "alpha = " + Convert.ToString(alpha)
+ "\r\n" + " theta = " + Convert.ToString(theta)+ "\r\n" + " beta = " + Convert.ToString(beta);
hd.Cx = m;
hd.Cy = n;
hd.Fx = m-600*Math.Cos(alpha); // 计算夹角斜线的终点坐标
hd.Fy = n-600*Math.Sin(alpha);
hd.DisplayOffset(); // 显示插针位置
hd.DisplayLine(); // 显示工件夹角
}
}
}
1. 实验结果
如图33所示,首先点击“Camera On”键,打开相机,再点击“Set template”键,设置模板,模板图像如图34所示。图33 Form外观及模板设置 图34 模板图像
如果已经设置好模板,则点击“Load template”键,读取模板图像。然后,点击“Match”键开始匹配。
点击“adjust offset”区域中的四个按键,可以调整偏移点,如图35中的黄色圆点所示。将黄色圆点移动至最右边的插针位置上。点击“Set offset”计算偏移点的参数q、L。参见公式(1)、(2)。图35 插针位置检测结果
之后,开始检查插针位置及夹角。
关闭相机,然后再开启相机。将模块任意摆放,但右下角最右侧的4个插针必须在红色框内,如图33所示。
点击“Load template”键,再点击“Match”键,再点击“Display target”键,检测结果显示如图35所示。
图35中3个红色点是3个匹配结果的中心点。匹配时间为13毫秒左右。
经标定,1个像素为0.0485毫米。2个插针之间的公称距离为5毫米。检测数据表明:位置误差小于1个像素。最右侧插针位置由黄点标注,其坐标是由公式(4)计算而得。实验表明检测精度令人满意。
由于显示图像的窗体坐标y轴方向朝下,匹配结果的转角a方向定义与一般直角坐标相反。所以转角a为正时,计算得出的夹角alpha为负。夹角alpha是由公式(3)计算得出,和匹配函数给出的旋转角a(1)~a(3)有一定的偏差。但图35中的黄色线是根据alpha计算画出,显然,其精度更高。
五.小结
HALCON软件功能十分强大,使用比较简单;但没有中文说明书,读英语文档较费时间。由HALCON程序导出的C#代码,不能直接使用,只能参考。希望本文能对想学习、使用机器视觉位置检测的工程师有些帮助。
六.参考文献
[1] MVTec Software GmbH, HALCON Solution Guide II-B. München,Germany, 2010.
[2] 李卫平,左力. 运动控制系统原理与应用. 武汉:华中科技大学出版社,2013.
[3] 孙国栋,赵大兴.机器视觉检测理论与算法. 北京:科学出版社,2015.
[4] 孙正. 数字图像处理与识别. 北京:机械工业出版社,2016.