勇哥注:
这个系列借着盘点WCF知识点的时机,顺便验证一下勇哥的一个新想法: 打造一个内网的Halcon视觉处理中心节点。
这个中心节点是运行在全网络中算力最强的机器上,其它若干客户端通过网络以事务请求的方式让中心结点进行计算,
最后传回处理结果。
本系列贴子要求您已熟悉下面的技术栈:
wpf(了解), halcon(基础应用), wcf(了解), C#(高级)
(一)基本wcf应用创建过程回顾
基本的Wcf应用分为五步:
步骤一:构建整个解决方案
Contracts
Services
Hosting
Client
步骤二:创建服务契约
WCF包含四种类型的契约:服务契约、数据契约、消息契约和错误契约
WCF广泛采用基于自定义特性(Custom Attribtue)的声明式编程模式,
我们通过在接口上应用System.ServiceModel.ServiceContractAttribute特性将一个接口定义成服务契约
WCF采用的是显式选择(Explicit Opt-in)的策略: 我们须要在相应的操作方法上面显式地应用OperationContractAttribute特性。
步骤三:创建服务
实现了服务契约接口
步骤四:通过自我寄宿的方式寄宿服务
终结点由地址(Address)、绑定(Binding)和契约(Contract)三要素组成
地址(Address)
绑定(Binding)
契约(Contract)
WCF服务寄宿通过一个特殊的对象完成:ServiceHost
松耦合是SOA的一个基本的特征,WCF应用中客户端和服务端的松耦合体现在客户端只须要了解WCF服务基本的描述,
而无须知道具体的实现细节,就可以实现正常的服务调用。
WCF服务的描述通过元数据(Metadata)的形式发布出来。
WCF中元数据的发布通过一个特殊的服务行为ServiceMetadataBehavior实现。
步骤五:创建客户端调用服务
服务被成功寄宿后,服务端便开始了服务调用请求的监听工作。
此外,服务寄宿将服务描述通过元数据的形式发布出来,相应的客户端就可以获取这些元数据创建客户端程序进行服务的消费。
在VS下,当我们添加服务引用的时候,VS在内部帮我们实现元数据的获取,
并借助这些元数据通过代码生成工具(SvcUtil.exe)自动生成用于服务调用的服务代理相关的代码和相应的配置。
附件:
下图是wcf体系架构:
(二)halcon功能的wcf代码测试
下面写测试代码:
(1)Hosting代码
using System; using System.Collections.Generic; using System.Linq; using System.ServiceModel; using System.ServiceModel.Description; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using wcf.Contracts; using wcf.Services; namespace Hosting { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { ServiceHost host; public MainWindow() { InitializeComponent(); host = new ServiceHost(typeof(HalconService)); host.AddServiceEndpoint(typeof(IHalon), new WSHttpBinding(), "http://127.0.0.1:8999/halconService"); if(host.Description.Behaviors.Find<ServiceMetadataBehavior>()==null) { ServiceMetadataBehavior behavior = new ServiceMetadataBehavior(); behavior.HttpGetEnabled = true; behavior.HttpGetUrl = new Uri("http://127.0.0.1:8999/halconService/metadata"); host.Description.Behaviors.Add(behavior); } ButStart.IsEnabled = true; ButStop.IsEnabled = false; } private void Button_Click(object sender, RoutedEventArgs e) { //打开服务 host.Opened += Host_Opened; host.Open(); } private void Host_Opened(object sender, EventArgs e) { Dispatcher.BeginInvoke((Action)delegate { TxtLog.Text = "服务已经启动。。。"; }); ButStart.IsEnabled = false; ButStop.IsEnabled = true; } private void Button_Click_1(object sender, RoutedEventArgs e) { //关闭服务 try { host.Close(); } catch { host.Abort(); } Dispatcher.BeginInvoke((Action)delegate { TxtLog.Text = "服务已经关闭。。。"; }); ButStart.IsEnabled = true; ButStop.IsEnabled = false; } } }
在浏览器地址栏中访问一下元数据地址:http://127.0.0.1:8999/halconService/metadata
如果可以看到下面的xml内容就证明元数据发布成功了。
(2)服务与契约
契约
using HalconDotNet; using System; using System.Collections.Generic; using System.Linq; using System.ServiceModel; using System.Text; using System.Threading.Tasks; namespace wcf.Contracts { [ServiceContract(Name = "HalconService", Namespace = "http://47.98.154.65/")] public interface IHalon { [OperationContract] HObject read_image(string imgName); [OperationContract] Tuple<int,int> get_image_pointer1(HObject img); } }
服务
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using HalconDotNet; using wcf.Contracts; namespace wcf.Services { public class HalconService : IHalon { /// <summary> /// 返回图片width,height /// </summary> /// <param name="img"></param> /// <returns></returns> public Tuple<int, int> get_image_pointer1(HObject img) { HOperatorSet.GetImagePointer1(img, out HTuple get_image_pointer1, out HTuple Type, out HTuple width, out HTuple height); return new Tuple<int, int>(width, height); } public HObject read_image(string imgName) { HOperatorSet.ReadImage(out HObject img, imgName); return img; } } }
(3)客户端
<Window x:Class="Client.MainWindow" xmlns:wf="clr-namespace:System.Windows.Forms.Integration;assembly=WindowsFormsIntegration" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Client" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <UniformGrid Grid.Row="0"> <Button x:Name="ButReadImg" FontSize="30" Click="Button_Click">读图片</Button> </UniformGrid> <TextBlock Grid.Row="1" x:Name="TxtLog"></TextBlock> <wf:WindowsFormsHost x:Name="wfHost" Width="550" Height="350" Grid.Row="1"> </wf:WindowsFormsHost> </Grid> </Window>
using Client.halconfun; using HalconDotNet; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace Client { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { private HWindowControl hwin = new HWindowControl(); private HalconServiceClient halcon=new HalconServiceClient(); public MainWindow() { InitializeComponent(); wfHost.Child = hwin; hwin.Dock = System.Windows.Forms.DockStyle.Fill; } private void Button_Click(object sender, RoutedEventArgs e) { //读图片 hwin.HalconWindow.DispObj(halcon.read_image(@"C:\Users\Public\Documents\MVTec\HALCON-19.11-Progress\examples\images\printer_chip\printer_chip_01.png")); } } }
客户端是使用”添加服务引用“,让vs自动生成一个代理类。
如果想自己写这个代理类也是可以的,就是使用ChannelFactory<T>
遇到的第一个问题
客户端运行后,点击”读图片“,发现在代理类内部出现内部错误。
勇哥try了一下看到了下图所示的错误信息。
原因是HObject对象无法处理。
是WSHttpBinding 无法处理HObject图标对象吗?
实际上即你把通讯协议切换成tcp或者pipe都一样会报错,根本的原因是HObject无法被序列化。
在Windows Communication Foundation (WCF) 中使用第三方库(如Halcon的图像处理库)时, 需要特别注意序列化和传输的问题。 WCF 服务契约(Service Contract)定义了服务操作(方法)的输入和输出参数类型, 这些类型需要能够被序列化为消息,以便在网络上进行传输。 HObject 是Halcon图像处理库中的一个核心数据类型,但它并不是可序列化的。 这意味着你不能直接在WCF服务的操作中使用 HObject 作为参数或返回值, 因为WCF无法将其序列化为XML或JSON等格式进行传输
在WCF中,我们有3种不同的Serializer
1。DataContractSerializer(定义在System.RunTime.Serializtion namespace中)
2。XMLSerializer(定义在System.XML.Serialization namespace)
3。NetDataContractSerializer (定义在System.XML.Serialization namespace) 。
第1种DataContractSerializer是默认的Serializer。
不管是上面哪一种序列化器,都是无法处理HObject的。
为了解决这个问题,有几种策略:
(一)使用字节数组,我们把HObject转为字节数组或者内存流,然后通过wcf传输。
在服务端或者客户端,把字节数组或者内存流转换回HObject。这个过程使用的是Halcon的算子进行转换。
(二)使用自定义序列化,即对HObject对象做自定义的序列化器和反序列化器。这个资料缺少,暂时没办法进行。
(三)使用halcon算子中的远程通讯功能。
(四)是处理过程中不传递HObject对象,而是向服务端发送一段Lua代码,它表示一系列的处理过程(可以理解为处理事务),
处理完成后有两种取得处理结果的方式:
1。服务端再把处理结果按(一)的方式返回HObject对象。
2。服务端把处理结果形成图片,放到指定的共享目录
至于服务器要处理的图片,事先保存在一个服务器可以访问的共享目录中,传递给服务器的只是路径和图片文件名称。
这里勇哥先尝试方法(一),之后再扩展为(四)的方法。
方式(一)的halcon代码大致如下:
先是把HObject变成字节数组的过程
// 假设你有一个HObject图像对象 image HObject image; // 这里应该是你的图像HObject // 导出图像为文件 string filePath = "path_to_your_image.png"; // 指定保存路径和文件名 HTuple hv_FileType = "png"; // 设置文件类型 hv_FileType.Dispose(); // 确保在不再需要时释放HTuple资源 HOperatorSet.WriteImage(image, filePath, hv_FileType); // 读取文件为字节数组 byte[] imageBytes = File.ReadAllBytes(filePath);
然后是把字节数组变成HObject的过程
// 假设你已从WCF服务接收到字节数组imageBytes byte[] imageBytes = ...; // 从WCF服务接收到的字节数组 // 将字节数组写入临时文件 string tempFilePath = Path.GetTempFileName(); // 获取临时文件路径 File.WriteAllBytes(tempFilePath, imageBytes); // 现在你可以使用Halcon的API来读取这个临时文件为一个HObject HObject tempImage; HTuple hv_FileType = new HTuple("png"); // 根据保存的格式设置文件类型 HOperatorSet.ReadImage(out tempImage, tempFilePath); hv_FileType.Dispose(); // 释放HTuple资源
修改后的服务如下:
public class HalconService : IHalon { /// <summary> /// 返回图片width,height /// </summary> /// <param name="img"></param> /// <returns></returns> public Tuple<int, int> get_image_pointer1(byte[] data) { File.WriteAllBytes("temp", data); HOperatorSet.ReadImage(out HObject img, "temp"); HOperatorSet.GetImagePointer1(img, out HTuple get_image_pointer1, out HTuple Type, out HTuple width, out HTuple height); return new Tuple<int, int>(width, height); } public byte[] read_image(string imgName) { //HOperatorSet.ReadImage(out HObject img, imgName); var bytes=File.ReadAllBytes(imgName); return bytes; } }
再次运行客户端,出现下面的错误。
这个错误是说消息的大小不能超过65536字节。勇哥读的这个图片已经超了这个大小。
为了解决这个问题,我们需要在客户端的绑定上指定一些属性。
这个时候我们就不能再使用系统自动生成的那个代理类了。
我们需要手工用ChannelFactory方法来生成代理。
public MainWindow() { InitializeComponent(); ListBox listBox2 = new ListBox(); listBox2.Items.Add("Item 1"); listBox2.Items.Add("Item 2"); listBox2.Items.Add("Item 3"); wfHost.Child = hwin; NetTcpBinding binding = new NetTcpBinding(); binding.MaxBufferSize = int.MaxValue; // 设置最大缓冲区大小为int的最大值 binding.MaxReceivedMessageSize = int.MaxValue; // 设置最大接收消息大小为int的最大值 binding.ReaderQuotas.MaxDepth = int.MaxValue; // 设置其他相关属性... binding.ReaderQuotas.MaxStringContentLength = int.MaxValue; binding.ReaderQuotas.MaxArrayLength = int.MaxValue; binding.ReaderQuotas.MaxBytesPerRead = int.MaxValue; binding.ReaderQuotas.MaxNameTableCharCount = int.MaxValue; channelFactory = new ChannelFactory<IHalon>(binding, "net.tcp://127.0.0.1:9001/halconService"); proxy = channelFactory.CreateChannel(); } private void Button_Click(object sender, RoutedEventArgs e) { //读图片 var img= proxy.read_image(@"C:\Users\Public\Documents\MVTec\HALCON-19.11-Progress\examples\images\printer_chip\printer_chip_01.png"); File.WriteAllBytes("tempClient",img); HOperatorSet.ReadImage(out HObject imgdata, "tempClient"); hwin.HalconWindow.DispObj(imgdata); }
运行结果证明方法(一)是可行的。
我们使用byte[] 代替了HObject对象的序列化消息传递,而后者因为无法序列化而无法通过wcf的消息传递。
另外插一句,如果你想同时运行服务器和客户端,让两者都可以断点调试,是有办法的。
因为vs的启动项目可以指定多个。
补充说明一下演示程序的目的,以及为啥要用wcf。
1。 即然说的是分布是应用,那么客户端肯定会分散在网络的不同位置,不同电脑上。
比如电脑A它上面有运动卡,但是没有装halcon环境,因为它性能太差。于是它可以选择使用网络中的视觉服务进行视觉编程。
有一台笔记本电脑,没装halcon环境,但我想用它开发个视觉算法,于是插上CCD,使用视觉服务进行算法编程。
2。客户端类型可以是C#的winform, 控制台程序,wpf程序这些桌面程序。
还可以是web网页。
3。 视觉服务端会定义一个功能接口,它就是wcf的Contracts(服务契约),客户端按照这个功能接口调用功能即可。
4。视觉服务可以挂接到IIS上去,这样随着系统启动而启动。
5。因为第4点的原因,web客户端也可以挂到IIS上去,这样可以在浏览器里直接调用视觉服务。(非编程的应用)
到底有什么用?
1。 首先是好玩。为了做实验的方便 。因为做实验时,有时候运控和、视觉、AI 的电脑不是同一台电脑。
2。 硬生生创造出一个全栈开发的例子,还是为了好玩~~
3。 没有了,如果同学你感觉有其它的意义,欢迎补充~~~~
一些可能遇到的问题:
(1)进程不具有此命名空间的访问权限
这个问题是因为你的VS项目调试时没以管理员身份进行。
这个比较容易,按勇哥下面这个贴子操作就行:
C#项目修改默认启动权限为管理员权限
本文源代码下载:
开发环境:vs2017+ wpf +halcon19.11
链接:https://pan.baidu.com/s/1jRZnF3CxTWYatShocvSs6g
提取码:reln
--来自百度网盘超级会员V6勇哥的分享

