news 2026/10/1 3:18:18

C#上位机通过OPCAutomation连接KEPServerEX 6实现曲线监控

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#上位机通过OPCAutomation连接KEPServerEX 6实现曲线监控

简介:一份完整的C# OPC通信示例工程,演示通过OPC自动化接口连接KEPServerEX 6服务器,并借助Windows窗体与图表控件将实时数据绘制成动态曲线。内容涵盖OPC服务器连接、数据项订阅、定时刷新、异常重连与图表优化等关键环节,适合工业自动化软件开发者、上位机工程师以及刚接触OPC技术的C#学习者参考。压缩包大小约一百零二KB,共三十六个文件,以C#源文件、Visual Studio解决方案与工程配置文件为主,另有编译生成的可执行程序及调试缓存,可直接打开运行或按需修改。目前已有151人学习下载。通过该示例可拿到完整开发环境,理清从OPC连接、数据点订阅到图表控件曲线刷新的实现路径,同时参考其中的异常处理和定时更新逻辑,为搭建自己的工业监控界面提供可复用的代码骨架,并迁移到其他OPC服务器场景。

1. OPC与KEPServerEX 6:这套C#上位机方案能解决什么问题

做上位机开发的人迟早要碰OPC。工厂里的PLC、仪表、变频器,协议五花八门,最后都得通过OPC服务器做一层统一,然后交给监控软件去读。这套基于C#的OPCAutomation连接KEPServerEX 6资源,解决的正是工业现场最典型的需求:在一台装了KEPServerEX 6的工控机上,用C# Windows Forms程序连上OPC服务器,订阅设备数据点,再把实时值以曲线方式画在界面上。工程里已经包含完整的项目结构、OPCAutomation连接逻辑、定时器刷新的数据读取方式和Chart控件曲线绘制代码,拿来改改控件布局、换一换数据点路径,就能直接跑起来,适合刚接触OPC开发的上位机工程师,也适合需要快速出Demo验证设备通信的项目。

2. OPCAutomation连接KEPServerEX 6:从引用配置到握手成功

2.1 为什么选OPCAutomation,以及它和OPC UA的边界

在C#里接OPC服务器,路径不止一条。用OPC UA .NET标准库写得非常优雅,但前提是服务器端也开启UA服务,证书管理、安全策略配置够你折腾一阵。OPCAutomation这个老牌COM接口在很多老项目里依然坚挺,它连接的是OPC DA 2.0规范,走的是Windows DCOM通道,不需要证书交换,引用一个DLL就能new对象,适合快速落地,尤其适合工控机上那些还跑着Windows 7或Windows 10老镜像的环境。

KEPServerEX 6本身同时支持OPC DA和OPC UA。如果用UA,客户端要配置证书,服务器端要导入信任列表,稍微配错一步就连不上。而OPC DA模式下,只要DCOM权限放通,通信链路就通。所以很多产线项目至今仍然用OPC DA方式做本机采集,不是因为老,是因为简单可靠。

选型边界要说清楚:OPCAutomation适合数据点几百个以内的中小规模采集,走的是COM互操作,频繁大流量读取时性能不如原生OPC UA。但如果你的应用就是连一台KEPServerEX,监控几个到几十个Tag,画曲线,那OPCAutomation的简洁性就是最大优势。追求极致性能的场景,才需要考虑换成UA或者OPC基金会提供的.NET SDK。

2.2 环境准备:KEPServerEX 6、Quick Client和DCOM基线

KEPServerEX 6安装没什么特殊门槛,关键是建通道、设备、标签这个三级结构。我一般会新建一个Channel命名Channel1,然后在Channel下挂Device命名SetDevice,再在Device下建Tag命名Tag1、Tag2。Tag的地址映射到PLC的具体寄存器,通信超时、字节序设置错一个,Quick Client里表现出的就是数值不变或者地址错误。

Quick Client是KEPServerEX自带的一个OPC客户端测试工具,验证配置时的价值不可替代。Tag建完之后打开Quick Client连接本机服务器,能把Tag拖进去看到实时值变化,再关闭它。这个动作值得做,是因为C#连接失败时很多人怀疑OPC配置,其实只要Quick Client能读到,OPC服务就没问题,问题就在C#侧的ProgID、DCOM权限或者路径写法。

DCOM基线配置在开发机上一般这样处理:运行dcomcnfg,在组件服务里找到KEPServerEX 6对应的组件,将身份验证级别调整为无,将启动和激活权限、访问权限设为允许Everyone或当前登录用户。同时防火墙入站规则开放135端口以及Kepware相关程序的端口。本机开发调试时,把DCOM权限放开能省掉后续一堆莫名其妙的问题。

2.3 添加OPCAutomation到C#工程

在Visual Studio里打开工程,解决方案资源管理器里右键引用,选择添加引用,在COM选项卡里勾选OPC Automation 2.0。如果机器上装了KEPServerEX 6,这个COM组件一定会被注册,但版本号在列表里可能显示为2.02,选中就好。

添加成功后生成Interop.OPCAutomation.dll,代码里using OPCAutomation即可。这里有个关键细节:项目平台目标建议设为x86。工业现场很多OPC组件是32位注册的,如果项目按AnyCPU跑在64位进程里,运行时会在注册表里找不到32位COM组件,报Class not registered。这个坑我在新电脑上碰到过不止一次,解决方式是在生成配置管理器里把平台改成x86,或者勾选首选32位。

2.4 连接代码、ProgID与首次握手验证

连接代码展开来看,一共就三步:创建OPCServer对象、调用Connect、检查状态。

using OPCAutomation; // 创建OPC服务器对象 OPCServer kepServer = new OPCServer(); try { // Connect(ProgID, 主机名) // ProgID对应KEPServerEX 6在注册表里的COM标识 // 本机用localhost,跨机器填IP或主机名 kepServer.Connect("Kepware.KEPServerEx.V6", "localhost"); // ServerState为1表示运行中,为2表示故障或停止 if (kepServer.ServerState == 1) { Console.WriteLine("KEPServerEX 6已连接,状态正常"); } } catch (Exception ex) { Console.WriteLine("连接异常:" + ex.Message); }

Connect方法第一个参数是ProgID,必须和注册表完全一致。常见写法Kepware.KEPServerEx.V6适用于6.x,如果你装的是KEPServerEX 6.10或者更高版本,ProgID通常不变,但推荐用OpcEnum工具枚举一遍本机可用的OPC服务器列表,把实际返回的ProgID打印出来再写进代码。我一般会在写正式连接逻辑前先跑一个枚举脚本,避免手滑写错ID。

第二个参数是主机名,本机填localhost,跨机器时填IP或NetBIOS名。注意DCOM远程调用要求当前用户对目标机器有合适的访问权限,这个要在开发环境提前验证,比部署到产线后排查省事得多。

连接成功后先别急着读数据,打印一下服务器状态和版本,确认正确的COM对象被创建了,再往下走创建组和数据项。这一步能稳定复现连接成功,就说明引用、ProgID、DCOM三个环节全部打通。

3. 数据订阅与定时刷新:把设备数据点变成实时数据流

3.1 理解OPC Group和Item的层级关系

OPC服务器的对象模型是典型的树状结构:一个OPCServer对象下面挂着OPCGroups集合,每个OPCGroup代表一组数据项,而真正的数据点是OPCItems集合里的OPCItem。Group的概念很接近工业场景里的数据块,同一个设备或同一个机组的所有监控点放一个组里管理最方便。

OPCGroup有几个关键属性,UpdateRate表示数据刷新的最小间隔,单位是毫秒;IsActive控制组是否处于激活状态;IsSubscribed控制是否启用异步通知机制。这三个属性直接影响后面数据读取的策略。

这里要分清同步读取和异步读取。SyncRead是程序主动去读,调用一次取一次,适合定时器驱动;DataChange事件是服务器主动推,数据一变就触发,理论上实时性更好,但回调里的代码要够快,否则容易卡界面线程。我一般会结合定时器做同步读取,逻辑可控、问题定位容易,对新手最友好。

3.2 创建OPCGroup并添加数据项

连接成功后,下一步就是建组、加数据点。

OPCGroups groups = kepServer.OPCGroups; OPCGroup group = groups.Add("MonitorGroup"); // 刷新周期100ms,决定数据变化被发现的频率 group.UpdateRate = 100; group.IsActive = true; group.IsSubscribed = true; OPCItems items = group.OPCItems; // AddItem(完整路径, 客户端句柄) // 完整路径格式:Channel.Device.Tag // 客户端句柄一般传0,也可以用自定义索引 OPCItem item1 = items.AddItem("Channel1.SetDevice.Tag1", 0); OPCItem item2 = items.AddItem("Channel1.SetDevice.Tag2", 0); OPCItem item3 = items.AddItem("Channel1.SetDevice.Tag3", 0);

AddItem的第二个参数是ClientHandle,C#侧自定义的客户端句柄,一般给0就行,也可以拿来自定义索引。这三个Tag添加成功后,OPCItem对象里会带回ServerHandle,后面同步读返回的数组下标就是以ServerHandle为依据的。

这里有一个容易翻车的点:AddItem成功与否不会用返回值告诉你,而是直接抛异常。如果路径里有一个Tag不存在,整个AddItem调用直接失败,前面加的项也会一起回滚。所以Tag路径一定要先在Quick Client里验证一遍,不要靠记忆拼路径。

3.3 定时器驱动的同步读取

Windows Forms里最省心的定时器是System.Windows.Forms.Timer,它的事件运行在UI线程上,可以直接访问控件,不需要额外做线程调度。

System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer(); timer.Interval = 100; // 与UpdateRate保持一致或略大 timer.Tick += Timer_Tick; timer.Start(); private void Timer_Tick(object sender, EventArgs e) { try { // 1表示从设备读,2表示从缓存读 // SyncRead(数据源, 项数量, out 值数组, out 错误数组, out 品质数组, out 时间戳数组) Array values; Array errors; Array qualities; Array timestamps; group.SyncRead(1, 1, out values, out errors, out qualities, out timestamps); // 用ServerHandle做索引取第一个Tag的值 int handle = item1.ServerHandle; float temp = (float)values.GetValue(handle); ShowData(temp); } catch (Exception ex) { // 读取失败时记录日志,不要直接抛到界面 Console.WriteLine("读取异常:" + ex.Message); } }

SyncRead的第一个参数是数据源,传1表示从设备直接读,传2表示从服务器缓存读。一般监控场景用缓存足够,因为KEPServerEX本身就一直从设备同步数据。直接访问设备会把响应时间拉长很多,当数据点较多时还会拖累整个通信链路。

这里重点说一个细节:values数组的下标不是从0开始的。OPC DA规范里,服务器返回的数组下标对应ServerHandle,而ServerHandle不一定从0开始,所以读值的时候要用item的ServerHandle做索引,而不是想当然地GetValue(0)。我见过不少人在这个地方拿到的数据全都不对,还以为是通信问题。

3.4 事件回调模式与数据缓冲队列

除了定时器主动读,OPCGroup还支持DataChange事件。注册方式很简单,给group的DataChange事件挂一个处理函数。

group.DataChange += Group_DataChange; private void Group_DataChange(int TransactionID, int NumItems, ref Array clientHandles, ref Array itemValues, ref Array qualities, ref Array timestamps) { // 此回调运行在COM线程,不能直接访问控件 for (int i = 1; i <= NumItems; i++) { int handle = (int)clientHandles.GetValue(i); float value = (float)itemValues.GetValue(i); // 把数据丢进线程安全队列,由UI定时器消费 dataQueue.Enqueue(new DataPoint(handle, value, (DateTime)timestamps.GetValue(i))); } }

回调模式的数据实时性明显好于定时器轮询,数据一有变化就触发,不用等定时器到点。但回调线程不是UI线程,直接往Chart控件上AddPoint一定会抛跨线程异常。常见做法是用ConcurrentQueue做缓冲,UI定时器每50到100毫秒消费一次队列里的数据,既保住实时性,又绕开线程问题。

dataQueue的容量要控制,工业现场如果数据点很多,回调频率又高,队列不消费就会越积越多。我的处理方式是入队前检查Count,超过5000就清空旧数据只留最新的,反正曲线展示看的就是最近一段时间。

4. Chart控件曲线展示:坐标轴自适应与刷新性能调优

4.1 Chart控件的基本配置

Windows Forms的Chart控件是.NET自带的图表组件,拖到窗体上就能用。要画一条实时曲线,最小配置是ChartArea和Series。

// 配置坐标轴 ChartArea area = chart1.ChartAreas[0]; area.AxisX.Title = "时间"; area.AxisY.Title = "数值"; area.AxisX.MajorGrid.LineColor = Color.Silver; area.AxisY.MajorGrid.LineColor = Color.Silver; // 配置曲线系列 Series series = chart1.Series[0]; series.ChartType = SeriesChartType.Line; series.BorderWidth = 2; series.Color = Color.DodgerBlue; series.XValueType = ChartValueType.DateTime;

X轴用DateTime类型,XValueType设置成DateTime后,AddXY的时候第一个参数传DateTime.Now就能自动按时间轴排列。Y轴默认是double,温度、压力、流量这些数值直接往上放。

ChartArea如果不做设置,Y轴刻度是自动缩放的,但初始范围往往不准。比如数据一直在一个小区间小幅波动,自动缩放会把波动放大得离谱,看起来像心脏骤停的心电图,这种状况要提前做范围限制。

4.2 定时把数据点加到曲线上

从数据队列里消费数据然后画图,是最稳的方式。每个数据点携带handle、value、time,UI定时器里逐个加进Series。

private void uiTimer_Tick(object sender, EventArgs e) { while (dataQueue.Count > 0) { DataPoint dp; if (dataQueue.TryDequeue(out dp)) { Series s = chart1.Series["Series1"]; // X轴传时间,Y轴传值 s.Points.AddXY(dp.Time, dp.Value); // 限制显示点数,只保留最近500个点 if (s.Points.Count > 500) { s.Points.RemoveAt(0); } } } }

两个问题要注意。第一个是数据点累积,曲线如果不设最大点数,跑个把小时内存里就是几万个Point对象,Chart控件渲染会越来越慢,界面开始卡顿。限制点数是最直接的解法,用RemoveAt(0)把最前面的点移除,就能维持一个移动窗口。500个点对分钟级数据或秒级数据都够用。

第二个问题是Chart控件的重绘频率。如果每秒进来20个点,每帧都重绘,CPU占用会很难看。可以只在添加完数据后用chart1.Invalidate()触发重绘,让控件合并在同一个绘制周期里,或者把UI定时器的Interval控制在合理范围,不必每次Tick都等量重绘。

4.3 坐标轴自适应与移动窗口

实时曲线最烦Y轴自动缩放。数据在小幅波动时,如果不做任何设置,Y轴会一直在窄区间里变,曲线看起来像剧烈抖动。

常见做法是做带余量的Y轴自适应,用当前可见窗口的最大值和最小值计算范围。

double max = double.MinValue; double min = double.MaxValue; foreach (DataPoint pt in series.Points) { if (pt.YValues[0] > max) max = pt.YValues[0]; if (pt.YValues[0] < min) min = pt.YValues[0]; } // 留10%余量,避免数据点贴到坐标轴边界 double range = max - min; area.AxisY.Minimum = min - range * 0.1; area.AxisY.Maximum = max + range * 0.1;

设置AxisY的Minimum和Maximum之后,Chart控件会用这个范围渲染,不再自己跳动。但设置范围要谨慎,如果设的是固定上下限,数据一旦超范围曲线就会飞出可视区域。我一般做滑动窗口,每轮刷新时重算当前可见点的范围,数据没变化时范围不动,数据突变时范围跟随。

X轴不需要手动滚动。X轴类型是DateTime,Chart控件会按数据点的顺序自动延伸,配合最大点数限制就能产生滚动效果,这个设计简单且可靠。

4.4 多曲线与图例配置

工业场景经常要同时看温度、压力、流量好几条曲线。实现方式不复杂,多加几个Series,注意每条Series的Name不重复。

chart1.Series.Add(new Series("Temperature")); chart1.Series["Temperature"].ChartType = SeriesChartType.Line; chart1.Series["Temperature"].Color = Color.OrangeRed; chart1.Series.Add(new Series("Pressure")); chart1.Series["Pressure"].ChartType = SeriesChartType.Line; chart1.Series["Pressure"].Color = Color.SteelBlue;

多曲线场景下图例必须配好,否则多条线颜色一样根本分不清。Chart控件自带Legend,运行时确保每个Series的LegendText有内容就行。

还有一个常见需求,不同量纲的数据共享Y轴会导致小数值曲线被大数值压成一条平线。解决方式是给量纲不同的曲线分配独立的AxisY,在ChartArea里手动添加第二条Y轴,然后把对应Series的YAxisType设成Secondary。

5. 连接断开、数据抖动与界面假死:高频坑位排查指南

5.1 DCOM权限与AddItem路径:连接阶段最容易翻车的两个点

第一个典型表现:运行程序时Connect方法抛COMException,提示服务器运行失败,或者弹出DCOM配置错误。原因很直接,OPC DA通信依赖Windows DCOM,KEPServerEX 6作为OPC服务器要允许被本机或远程调用,当前Windows用户没有访问权限时,COM组件无法启动。解决方式是打开dcomcnfg的组件服务,找到KEPServerEX 6对应的组件,把身份验证级别改成无,在启动和激活权限里加入当前用户或Users组。工控机上还要确认防火墙放行了135端口和Kepware相关程序。

第二个典型表现:items.AddItem("Channel1.SetDevice.Tag1", 0)直接报错,但在Quick Client里能读到这个Tag。原因绝大多数是路径分隔符写错。KEPServerEX内部路径用英文句点做分隔符,很多人习惯性写成斜杠。其次是Tag名大小写或空格不对,OPC DA的路径严格区分大小写。解决方式是回到Quick Client右键复制Tag的完整路径,粘贴进代码。从Excel表格复制过来的路径要Trim去空格,还要检查有没有混入不可见字符,这类问题打印出来根本看不出,只能打印路径的字节去排查。

5.2 跨线程访问控件与CPU飙升:运行期最容易踩的两个坑

跨线程访问控件的表现是:使用DataChange回调时,在回调里直接给chart1添加数据点,程序抛InvalidOperationException,提示线程间操作无效。原因是DataChange事件运行在COM回调线程,不是UI线程,Windows Forms控件只能由创建它的线程访问。解决办法是别用CheckForIllegalCrossThreadCalls = false去绕过,那是给自己埋雷。正确做法是把回调数据放进ConcurrentQueue,由UI线程定时器消费。

CPU飙升的表现是:程序正常跑,数据也能画出来,但任务管理器里CPU一直在90%以上,界面明显卡顿。原因通常是定时器刷新频率过高,SyncRead每50毫秒读一次,Chart控件每帧重绘,两个动作叠加把CPU打满。很多新手把UpdateRate设置成10ms,实际上OPC DA服务器的刷新能力根本跟不上,反而产生大量无效通信。解决方式是把UpdateRate控制在100到500毫秒之间,UI定时器也可以用200毫秒的间隔去消费100毫秒更新的数据,界面帧率稳定在5帧左右看起来已经很流畅。

5.3 运行几小时后连接掉线:长期运行的稳定性排查

表现是程序跑了一夜,第二天发现曲线不更新了,OPC连接丢失,重新Connect也可能报错。原因有两个方向:COM对象长时间运行后没有正确释放,引用计数一直占用;另一个是KEPServerEX侧空闲连接被回收,OPCAutomation又没有主动心跳,连接就死掉了。

解决方式分两部分。一是程序退出时按顺序释放资源:先移除Group,再Disconnect,最后把OPCServer对象置null并调用Marshal.ReleaseComObject。二是加心跳检测,定时器每次读取时记录状态,连续失败三次就执行自动重连,重连成功后再重建Group和Item,因为旧的ServerHandle已经失效。这个重连逻辑要独立封装,别散落在Form事件里,不然维护起来很痛苦。

6. 从Demo到交付:三步把重连机制做成生产级

Demo能跑只是第一步,真正在产线上长期运行,重连机制必须单独设计。

我习惯用一个状态机来管理连接生命周期:正常读取状态、连续失败计数、重连等待状态。每次SyncRead抛出异常时failCount加一,连续三次失败触发重连逻辑。重连完成后把failCount清零,同时恢复数据点订阅。

参数建议值说明
失败阈值3次连续读取失败触发重连
重试间隔60秒避免频繁重连打爆日志
最大重试10次超过后停止自动重连,界面弹窗报警
int failCount = 0; private void CheckConnection() { if (kepServer == null || kepServer.ServerState != 1) { failCount++; if (failCount >= 3) { Reconnect(); failCount = 0; } } } private void Reconnect() { try { // 先释放旧连接,再走完整连接流程 Marshal.ReleaseComObject(kepServer); kepServer = new OPCServer(); kepServer.Connect("Kepware.KEPServerEx.V6", "localhost"); // 重建Group和Item,旧句柄已失效 RebuildGroupAndItems(); } catch (Exception ex) { Console.WriteLine("重连失败:" + ex.Message); } }

重连方法里先调用Marshal.ReleaseComObject释放旧COM对象,这一步很多人会漏掉,导致重连十几次后进程里堆满无法回收的COM实例,内存持续上涨。重建Group和Item时,所有ServerHandle都要重新获取,老句柄直接丢掉,别无脑存着复用。

交付前还建议做一轮模拟断线验证:在KEPServerEX里停掉设备通道,观察程序能否在预期时间内自动恢复;恢复通道后再看曲线是否衔接连贯。从那以后我每次写OPC上位机,都会强制走一遍Quick Client路径验证、DCOM权限放通、重连测试这三件事,之前踩过的坑基本都能在这一遍里提前暴露出来。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 3:17:17

企业AI转型四步法:从场景选择到规模化落地的避坑指南

我们部门去年搞了一场AI转型动员会&#xff0c;各部门负责人都到了。讲台上厂商顾问放了一段特别炫酷的演示&#xff0c;大模型在屏幕上秒答问题、自动生成报表&#xff0c;台下几位老总眼里都在放光。三个月后我再去回访&#xff0c;发现那套系统除了在汇报PPT里出现过&#x…

作者头像 李华
网站建设 2026/10/1 3:17:06

帝国CMS处理Word图文混排的完整指南

做网站内容运营的朋友&#xff0c;十有八九都遇到过这种场景&#xff1a;编辑在Word里把图文排版打理得整整齐齐&#xff0c;复制粘贴到帝国CMS&#xff08;EmpireCMS&#xff09;后台编辑器里&#xff0c;结果图片全变成了带本地路径的破图&#xff0c;表格样式挤成一团&#…

作者头像 李华
网站建设 2026/10/1 3:16:41

体育赛事实时数据分析:Kafka架构设计、集群部署与消费端优化实战

1. 体育赛事数据的特殊性&#xff1a;为什么流式架构是绕不开的做体育数据这行之前&#xff0c;我一直觉得Kafka就是个普通的消息管道——往里丢消息&#xff0c;消费者取出来&#xff0c;完事。直到真正接手实时比赛数据分析系统&#xff0c;被体育赛事的流量节奏和数据形态连…

作者头像 李华
网站建设 2026/10/1 3:16:12

肺炎胸片4分类实战:从数据处理到迁移学习与模型评估

简介&#xff1a;面向医学图像分类任务的肺炎胸片四分类数据集&#xff0c;适合深度学习初学者与医疗影像研究人员直接用于模型训练与验证。数据涵盖COVID&#xff08;新型冠状肺炎&#xff09;、Lung_Opacity&#xff08;肺部浑浊&#xff09;、Normal&#xff08;正常&#x…

作者头像 李华
网站建设 2026/10/1 3:15:54

CH32L103 RISC-V工业MCU选型与低功耗设计实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 3:15:39

猪场实拍YOLO数据集:3万+真场景目标检测数据

简介&#xff1a;本资源是面向农业AI与智能养殖领域的猪只目标检测专用数据集&#xff0c;适用于计算机视觉初学者、算法工程师及智慧畜牧项目开发者&#xff0c;解决猪只在复杂监控场景下的精准识别与定位难题。数据集包含2000张真实养猪场监控实拍图像&#xff0c;配套3万余个…

作者头像 李华