C#(上位机)—Halcon(图像处理)—ADS(PLC)—Python(机械手)联合编程

最近参与到公司一个联合项目开发,经过近一个月的开发周期基本已经完成上位机需求,写篇博客以记录开发过程中碰到的一些疑难问题,方便日后查看学习。

一、项目内容

项目主要设计一套系统以完成对产品的缺陷检测、智能分拣、外观切割等操作。
具体流程:1、设计Halcon程序,驱动工业相机,完成烟包的中心点坐标、角度的测量;2、设计C#异步服务器(集成TCP和ADS通信),完成对抓取图片的数据通信及下位PLC通信:将角度坐标信息通过TCP传输客户端,将PLC状态信息通过ADS通信传输至PLC;3、设计Python异步客户端,实现TCP通信,接收服务器数据并驱动机械手操作。

二、设计思路

1、Halcon

目的在于获得烟包的中心点坐标及角度,并完成世界坐标系转换。
一开始思路是利用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)

效果图如下:
C#(上位机)—Halcon(图像处理)—ADS(PLC)—Python(机械手)联合编程_第1张图片
注意,在使用本地模板时同样需要clear_shape_model进行资源回收,否则当C#上位机中多次运行该Halcon方法时会造成内存泄漏,最终造成程序卡死。

2、C#异步服务器(TCP+ADS)

目的在于完成数据处理和数据交互,交互包括图片数据交互(用于客户端机械手)及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数据进行读写的操作同样较为复杂,需要仔细根据技术手册进行设计。

三、Python异步客户端(TCP+机械手)

目的在于完成服务器数据(角度坐标)的传输、机械手驱动和状态信息的捕获以及交互。
主要思路是利用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)

你可能感兴趣的:(C#(上位机)—Halcon(图像处理)—ADS(PLC)—Python(机械手)联合编程)