最近参与到公司一个联合项目开发,经过近一个月的开发周期基本已经完成上位机需求,写篇博客以记录开发过程中碰到的一些疑难问题,方便日后查看学习。
项目主要设计一套系统以完成对产品的缺陷检测、智能分拣、外观切割等操作。
具体流程:1、设计Halcon程序,驱动工业相机,完成烟包的中心点坐标、角度的测量;2、设计C#异步服务器(集成TCP和ADS通信),完成对抓取图片的数据通信及下位PLC通信:将角度坐标信息通过TCP传输客户端,将PLC状态信息通过ADS通信传输至PLC;3、设计Python异步客户端,实现TCP通信,接收服务器数据并驱动机械手操作。
目的在于获得烟包的中心点坐标及角度,并完成世界坐标系转换。
一开始思路是利用Blobe逼近ROI,再通过orientation_region算子得到角度,后发现对于不同照片,Blobe难以以固定的阈值得到ROI,故此法不可行;第二种思路是利用模板匹配,vector_angle_to_rigid来获得目标的中心点及角度,此法比较稳定,易于扩充需求,但是值得注意的是模板的标准问题,因为最终得到的角度是以模板为标准(0度)旋转得到的。另外考虑到烟包的正反面特性,建议提前做好正面、反面模板保存至本地,使用时再利用read_shape_model进行写入。
*烟包测量数据算法*
gen_empty_obj (Models)
IndexS := []
IndexE := []
ModelIDs := []
read_shape_model ('C:/Users/56234/Desktop/烟包检测/格调娇子/img_model_正面', ModelID0)
read_shape_model ('C:/Users/56234/Desktop/烟包检测/格调娇子/img_model_背面', ModelID1)
ModelIDs[0] := [ModelID0]
ModelIDs[1] := [ModelID1]
for I := 0 to 1 by 1
get_shape_model_contours (ModelContours, ModelIDs[I], 1)
count_obj (ModelContours, NumModel)
count_obj (Models, NumModels)
concat_obj (Models, ModelContours, Models)
IndexS := [IndexS,NumModels + 1]
IndexE := [IndexE,NumModels + NumModel]
endfor
* Image Acquisition 01: Code generated by Image Acquisition 01
ImageFiles := []
ImageFiles[0] := 'C:/Users/56234/MVS/Data/Image_20190814150555121.jpg'
ImageFiles[1] := 'C:/Users/56234/MVS/Data/Image_20190814150606739.jpg'
for Index := 0 to |ImageFiles| - 1 by 1
read_image (Image, ImageFiles[Index])
get_image_size (Image, Width, Height)
dev_close_window ()
dev_open_window (-1, -1, Width, Height, 'black', WindowHandle)
dev_display (Image)
find_shape_models (Image, ModelIDs, rad(0), rad(360), 0.4, 0, 1, 'least_squares', 0, 1, Row, Column, Angle, Score, Model)
Num := |Score|
if (Score > 0)
for J := 0 to Num - 1 by 1
copy_obj (Models, ModelSelected, IndexS[Model[J]], IndexE[Model[J]] - IndexS[Model[J]] + 1)
vector_angle_to_rigid (0, 0, 0, Row[J], Column[J], Angle[J], HomMat2D)
affine_trans_contour_xld (ModelSelected, ModelTrans, HomMat2D)
dev_display (Image)
dev_display (ModelTrans)
disp_message (WindowHandle, 'Angle is'+deg(Angle[J])+'Ordination is'+Row[J]+','+Column[J], 'window', 10, 10, 'black', 'true')
dev_close_window()
endfor
endif
endfor
clear_shape_model (ModelID0)
clear_shape_model (ModelID1)
效果图如下:
注意,在使用本地模板时同样需要clear_shape_model进行资源回收,否则当C#上位机中多次运行该Halcon方法时会造成内存泄漏,最终造成程序卡死。
目的在于完成数据处理和数据交互,交互包括图片数据交互(用于客户端机械手)及PLC数据交互(用于Bechoff PLC)。
一开始使用C++做了一个控制台服务器,但没有界面也没有按钮,给实际操作带来很多不便(例如向客户端发送消息,修改PLC中参数等),于是考虑MFC做界面设计,但MFC局限太大:开发周期长,设计缓慢,过于底层致使调用不便,故采用纯面向对象语言——C#进行设计。
2.1 TCP Socket模块
图片数据交互采用异步TCP,主要原理是利用回调函数callback完成对Receive事件的处理,当Socket有数据时触发回调,回调函数完成数据的打印。
//最基本回调Socket例子
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using HalconDotNet;
namespace TCP_Server
{
internal class xProgram
{
private static readonly byte[] Buffer = new byte[1024];
private static int _count;
private static void Main()
{
WriteLine("Server is ready.", ConsoleColor.Green);
//①创建一个新的Socket,这里我们使用最常用的基于TCP的Stream Socket(流式套接字)
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//②将该socket绑定到主机上面的某个端口
//方法参考:http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socket.bind.aspx
socket.Bind(new IPEndPoint(IPAddress.Any, 3001));
//③启动监听,并且设置一个最大的队列长度
//方法参考:http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socket.listen(v=VS.100).aspx
socket.Listen(3);
//④开始接受客户端连接请求
//方法参考:http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socket.beginaccept.aspx
socket.BeginAccept(new AsyncCallback(ClientAccepted), socket);
Console.ReadLine();
}
// 客户端连接成功
public static void ClientAccepted(IAsyncResult ar)
{
//设置计数器
_count++;
var socket = ar.AsyncState as Socket;
//这就是客户端的Socket实例,我们后续可以将其保存起来
if (socket != null)
{
var client = socket.EndAccept(ar);
//客户端IP地址和端口信息
IPEndPoint clientipe = (IPEndPoint)client.RemoteEndPoint;
WriteLine(clientipe + " is connected,total connects " + _count, ConsoleColor.Yellow);
new HDevelopExport(client);
//接收客户端的消息
client.BeginReceive(Buffer, 0, Buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveMessage), client);
}
//准备接受下一个客户端连接请求
if (socket != null) socket.BeginAccept(new AsyncCallback(ClientAccepted), socket);
}
// 接收客户端的信息
public static void ReceiveMessage(IAsyncResult ar)
{
var socket = ar.AsyncState as Socket;
//客户端IP地址和端口信息
if (socket != null)
{
IPEndPoint clientipe = (IPEndPoint)socket.RemoteEndPoint;
try
{
//方法参考:http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socket.endreceive.aspx
var length = socket.EndReceive(ar);
//读取出来消息内容
var message = Encoding.UTF8.GetString(Buffer, 0, length);
//输出接收信息
WriteLine(clientipe + " :" + message, ConsoleColor.White);
//服务器发送消息
//socket.Send(Encoding.UTF8.GetBytes("Server received data"));
//接收下一个消息
socket.BeginReceive(Buffer, 0, Buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveMessage), socket);
}
catch (Exception)
{
//设置计数器
_count--;
//断开连接
WriteLine(clientipe + " is disconnected,total connects " + (_count), ConsoleColor.Red);
}
}
}
public static void WriteLine(string str, ConsoleColor color)
{
Console.ForegroundColor = color;
Console.WriteLine("[{0:MM-dd HH:mm:ss}] {1}", DateTime.Now, str);
}
}
Halcon算法可导出为.cs文件进行C#集成,其中HDevelopExport()方法即实现:图像处理+数据发送的功能。
过程中遇到很多问题,实际大部分来自于对项目属性的配置不正确,对于C#集成Halcon,网上有许多教程,不再赘述;其中比较严重的是内存问题,Halcon环节已提到,解决办法是通过断点调试定位内存暴涨的代码段,再进行分析处理,后发现是本地模板没有Clear导致资源一直被其占用,GC.Collect不会将其判定为dead object,故内存持续增长,对于内存泄漏问题,在C#中同样需要引起重视。
2.2 ADS通信模块
目的在于对PLC数据读和写,对Python客户端进行数据交互,知晓机械手的状态信息并对其进行控制。这方面知识较为冷门,也遇到很多问题,如:对TwinCAT2软件的System manger配置问题,其中很多涉及到网络IP分配和PLC地址的设置,较为复杂;导入AdsDll库,利用其提供的方法在C#环境下对Beckhoff PLC数据进行读写的操作同样较为复杂,需要仔细根据技术手册进行设计。
目的在于完成服务器数据(角度坐标)的传输、机械手驱动和状态信息的捕获以及交互。
主要思路是利用select异步模型完成数据的接收,原理较为简单,网上也有很多例子,需要注意的是Python近乎苛刻的格式要求,以及在跨平台通信时的编码方式。对于C#来说需要进行utf8格式转换,否则会出现数据乱码的情况。频繁进行TCP通信时,需要考虑到数据包粘包情况,现解决办法是time.sleep(),但这会影响通信效率,并不是最佳解决方案。
机械手控制内容现在还未涉及,后续会继续更新。
import errno, select, socket, time
def format_address(address):
host, port = address
return '%s:%s' % (host or '127.0.0.1', 3001)
if __name__ == '__main__':
address = (str('127.0.0.1'), 3001)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sendbuff = "Hello Server!"
sock.connect(address)
sock.setblocking(0)
sock.send(sendbuff.encode("utf-8"))
while sock :
rlist, _, _ = select.select([sock], [], [])
data = b''
try :
new_data = sock.recv(1024)
except socket.error as e :
if e.args[0] == errno.EWOULDBLOCK :
break
else :
if not new_data :
break
else :
print(new_data.decode("utf-8"))
data += new_data
# 将data数据解包,传给机械手
# 机械手抓取动作
Flag = 1 # 就位标志,0:未就位 1:已就位
str = "mechanical arm is ready: BOOL = %d" % Flag
sock.send(str.encode("utf-8")) # 就位信号
if not data :
sock.close()
print("服务器关闭连接!")
break
time.sleep(0.5)