C#联合海康相机与雷赛运动控制卡实现产线ID识别与数据库上传

发布时间:2026/10/30 16:12:16
C#联合海康相机与雷赛运动控制卡实现产线ID识别与数据库上传

简介:这份资源面向具备一定C#基础的工控与机器视觉开发者,聚焦海康工业相机与雷赛运动控制卡的联合应用,解决芯片数字识别后自动上传数据库的完整流程问题。压缩包共205个文件,约22.98MB,以39个cs源码文件为核心,辅以resx、resources等界面资源,dll、lib、h等依赖库,以及config、xml、json等配置文件,另有exe可执行程序、bmp与jfif图像素材和xlsx数据表,覆盖从采集、识别到入库的完整工程结构。目前已有102人学习下载。读者可参考相机取像与运动控制卡协同工作的代码组织方式,理解数字识别结果如何写入数据库,并借助数据库查询模块掌握数据回读与校验思路;工程内保留的日志与缓存文件也有助于排查运行期异常,适合作为课程设计、设备上位机开发或视觉检测项目的源码学习范本。

1. 一条产线追溯链的起点:这套 C# 资源到底在解决什么

产线上扫一个 ID,相机拍照识别,运动控制卡把工件送到位,识别结果写进数据库——这条链路听起来简单,真正落地时每个环节都能卡人。这套 C# 联合海康相机、雷赛运动控制卡识别 ID 上传数据库的资源包,针对的正是这类工位级追溯场景:用 C# 做上位机主控,海康工业相机负责图像采集与 ID 识别,雷赛运动控制卡负责轴运动与 IO 时序,识别结果通过数据库持久化。适合做非标自动化、产线追溯、上位机开发的从业者,尤其是需要把视觉、运动、数据三块拼到一起的人。它不是一个能直接跑起来的成品软件,而是一套可拆解、可复用的工程骨架,价值在于把三个子系统的调用方式、时序配合和数据落库路径摊开给你看。下面按「资源是什么 → 怎么用 → 坑在哪」的顺序拆。

2. 海康相机 SDK 在 C# 里的取流与 ID 识别接入

2.1 为什么选海康 MVS SDK 而不是 DirectShow

海康工业相机(MV 系列)在 C# 上位机里的标准接入方式是官方 MVS SDK,核心是MvCameraControl.Net.dll这套托管封装。热词里有人问「c# directshow uvc 回调里区分多个摄像头」,那是 USB 摄像头走 UVC 协议的路子,工业相机一般不这么用——海康 MV 系列走的是 GigE 或 USB3 Vision,SDK 直接给到设备枚举、取流、参数配置的完整接口,比 DirectShow 稳定得多,也不会有多摄像头回调里分不清设备的问题。选型理由很直接:工业现场要的是确定性,帧率、曝光、触发模式都得能精确控制,DirectShow 那套面向消费级摄像头的抽象在这里不够用。

接入流程分四步:枚举设备、创建句柄、打开设备、开始取流。下面是我一般会写的初始化骨架。

// 海康 MVS SDK 相机初始化与取流 using MvCamCtrl.NET; using System.Runtime.InteropServices; private MyCamera _camera = new MyCamera(); private bool _isGrabbing = false; public bool InitCamera() { // 1. 枚举 GigE 和 USB 设备 MyCamera.MV_CC_DEVICE_INFO_LIST deviceList = new MyCamera.MV_CC_DEVICE_INFO_LIST(); int nRet = MyCamera.MV_CC_EnumDevices_NET( MyCamera.MV_GIGE_DEVICE | MyCamera.MV_USB_DEVICE, ref deviceList); if (nRet != MyCamera.MV_OK || deviceList.nDeviceNum == 0) return false; // 2. 用第一个设备创建句柄 IntPtr pInfo = Marshal.UnsafeAddrOfPinnedArrayElement(deviceList.pDeviceInfo, 0); var devInfo = (MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure( pInfo, typeof(MyCamera.MV_CC_DEVICE_INFO)); nRet = _camera.MV_CC_CreateDevice_NET(ref devInfo); if (nRet != MyCamera.MV_OK) return false; // 3. 打开设备,独占模式 nRet = _camera.MV_CC_OpenDevice_NET( MyCamera.MV_ACCESS_Exclusive, 0); if (nRet != MyCamera.MV_OK) return false; // 4. 注册回调,开始取流 _camera.MV_CC_RegisterImageCallBackEx_NET(OnImageCallback, IntPtr.Zero); nRet = _camera.MV_CC_StartGrabbing_NET(); _isGrabbing = (nRet == MyCamera.MV_OK); return _isGrabbing; }

逻辑说明:MV_CC_EnumDevices_NET把网口和 USB 口上的海康设备都列出来,MV_CC_CreateDevice_NET用设备信息建句柄,MV_CC_OpenDevice_NET的第二个参数是权限模式,独占模式防止别的进程抢相机。回调注册用MV_CC_RegisterImageCallBackEx_NET,图像数据在回调里拿到,不要在回调里做耗时操作,否则丢帧。

参数说明:MV_CC_OpenDevice_NET的MV_ACCESS_Exclusive是独占,MV_ACCESS_Control是只控制不取流,MV_ACCESS_Monitor是监听。产线上一般用独占。取流前如果相机是触发模式,还得配TriggerMode和TriggerSource,否则StartGrabbing之后一帧都不来。

2.2 回调里做 ID 识别与结果提取

图像回调拿到的是原始帧数据,ID 识别通常有两种做法:一是相机自带读码功能(部分海康型号支持),二是把帧转成Bitmap或Mat再走 OpenCvSharp 或第三方读码库。热词里「c# opencvsharp ordercorners」说明有人在做角点排序,那多半是定位类任务,ID 识别更常见的是条码/二维码解码。

private void OnImageCallback(IntPtr pData, ref MyCamera.MV_FRAME_OUT_INFO_EX pFrameInfo, IntPtr pUser) { // 只处理单帧,避免回调堆积 if (pFrameInfo.enPixelType != MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono8) return; int width = pFrameInfo.nWidth; int height = pFrameInfo.nHeight; byte[] buffer = new byte[width * height]; Marshal.Copy(pData, buffer, 0, buffer.Length); // 转成 Bitmap 交给读码器 Bitmap bmp = ByteToBitmap(buffer, width, height); string id = DecodeId(bmp); // 读码逻辑,可用 ZXing 或相机自带 if (!string.IsNullOrEmpty(id)) { // 抛到主线程或队列,别在回调里写数据库 _resultQueue.Enqueue(new IdResult { Id = id, Time = DateTime.Now }); } }

逻辑说明:回调里只做数据拷贝和轻量解码,识别结果丢进队列,由后台线程消费。参数上PixelType_Gvsp_Mono8是黑白 8 位,读码够用;彩色帧数据量翻三倍,回调压力大。nWidth、nHeight从帧信息里取,不要写死。

提示:回调函数运行在 SDK 的内部线程上,在里面直接操作 WinForm 控件会抛跨线程异常,必须用Invoke或队列转出去。

3. 雷赛运动控制卡的轴控与 IO 时序配合

3.1 雷赛卡在 C# 里的调用方式与初始化

雷赛运动控制卡(DMC 系列、SMC 系列等)在 C# 里通过官方提供的LTDMC.dll或对应型号的封装库调用,本质是 P/Invoke 到原生 DLL。热词里「c# modbus tcp 客户端」是另一条路——有些雷赛卡支持 Modbus TCP,但产线上更常见的是直接调 DLL,响应更快、功能更全。选 DLL 的理由:轴参数、IO 状态、插补指令都能直接拿到,不用自己拼 Modbus 寄存器。

初始化顺序:打开卡、设脉冲当量、设加减速、使能轴。下面是我常用的初始化片段。

// 雷赛 DMC 系列控制卡初始化 using csLTDMC; // 雷赛提供的 C# 封装 private ushort _cardNo = 0; public bool InitMotionCard() { // 1. 打开控制卡,0 是卡号 short ret = LTDMC.dmc_board_init(); if (ret <= 0) return false; // 2. 设置 0 轴的脉冲当量:多少脉冲走 1mm // 假设丝杆导程 5mm,电机 10000 脉冲/转 double pulsePerMm = 10000.0 / 5.0; LTDMC.dmc_set_equiv(_cardNo, 0, pulsePerMm); // 3. 设加减速:起跳速度、最大速度、加速时间 LTDMC.dmc_set_profile_unit(_cardNo, 0, 10, // 起始速度 mm/s 200, // 最大速度 mm/s 0.2, // 加速时间 s 0.2); // 减速时间 s // 4. 轴使能 LTDMC.dmc_set_axis_enable(_cardNo, 0); return true; }

逻辑说明:dmc_board_init返回卡数量,小于等于 0 说明没找到卡。dmc_set_equiv的第三个参数是脉冲当量,算错这个轴就走不准,这是最常见的翻车点。dmc_set_profile_unit用单位制设速度曲线,比原始脉冲制直观。dmc_set_axis_enable使能后电机才锁死。

参数说明:脉冲当量 = 电机每转脉冲数 / 丝杆导程。如果电机带减速机,还要乘减速比。起始速度别设 0,否则启动会有冲击;加速时间太短会丢步,太长节拍上不去。

3.2 视觉触发与运动到位的时序编排

这套资源的核心难点不在单个模块,而在时序:工件到位 → 触发相机 → 识别 → 结果写库 → 卡继续走。常见做法是用 IO 触发相机,而不是软件触发,因为软件触发有延迟抖动。热词里「海康相机使用 io 触发模式并输出 ng/ok」说的就是这个场景。

// 运动到位后触发相机,等待识别结果 public bool MoveAndInspect(double targetPos, int timeoutMs) { // 1. 发绝对定位指令 LTDMC.dmc_pmove_unit(_cardNo, 0, targetPos, 1); // 1 = 绝对模式 // 2. 等轴到位,dmc_check_done 返回 0 表示到位 int elapsed = 0; while (LTDMC.dmc_check_done(_cardNo, 0) != 0) { Thread.Sleep(5); elapsed += 5; if (elapsed > timeoutMs) return false; // 超时保护 } // 3. 到位后置 IO 输出,触发相机拍照 LTDMC.dmc_write_outbit(_cardNo, 0, 1); // OUT0 拉高 Thread.Sleep(20); // 触发脉宽 LTDMC.dmc_write_outbit(_cardNo, 0, 0); // 4. 等识别结果,超时算 NG return _resultQueue.TryDequeue(out var result, timeoutMs); }

逻辑说明:dmc_pmove_unit发定位,dmc_check_done轮询到位状态,轮询间隔 5ms 是精度和 CPU 占用的折中。到位后拉 IO 触发相机,脉宽 20ms 保证相机能采到。识别结果从队列取,超时判 NG。

参数说明:dmc_pmove_unit最后一个参数 1 是绝对定位,0 是相对。dmc_write_outbit的第二个参数是输出口编号,接相机触发线的那一路。超时时间按最坏节拍设,一般给识别时间的 1.5 倍。

注意:IO 触发相机的接线要和相机端TriggerSource配置一致,Line0 还是 Line1 接错了,相机永远不触发,这种问题查半天。

4. 识别结果落库:C# 写数据库的选型与批量写入

4.1 数据库选型与连接方式

热词里「c# 数据库」「数据库增删改查」「mysql 数据库连接池」都指向同一个问题:上位机识别结果往哪写、怎么写。产线追溯场景一般选 MySQL 或 SQL Server,轻量场景用 SQLite。选型看三点:并发写入量、是否需要远程查询、现场有没有 IT 运维。单工位每秒几条记录,SQLite 都够;多工位汇总到 MES,就得上 MySQL 或 SQL Server。

C# 里连 MySQL 用MySqlConnector或MySql.Data,连 SQL Server 用System.Data.SqlClient或Microsoft.Data.SqlClient。连接池是默认开的,但要注意连接字符串里的Pooling=true和Max Pool Size,产线长时间跑,连接泄漏会把池占满。

// 用 MySqlConnector 写入识别结果 using MySqlConnector; private const string ConnStr = "Server=127.0.0.1;Database=trace_db;Uid=op;Pwd=***;" + "Pooling=true;Max Pool Size=20;"; public void SaveResult(IdResult result) { using var conn = new MySqlConnection(ConnStr); conn.Open(); string sql = "INSERT INTO id_record (id_code, station, create_time) " + "VALUES (@id, @station, @time)"; using var cmd = new MySqlCommand(sql, conn); cmd.Parameters.AddWithValue("@id", result.Id); cmd.Parameters.AddWithValue("@station", "ST01"); cmd.Parameters.AddWithValue("@time", result.Time); cmd.ExecuteNonQuery(); }

逻辑说明:using确保连接归还池。参数化查询防注入,也避免拼接字符串的转义问题。Max Pool Size=20按并发线程数设,别设太大,数据库端连接数也有限制。

参数说明:Pooling=true默认就是开的,显式写出来便于排查。AddWithValue简单但有类型推断问题,严格场景用Add指定MySqlDbType。

4.2 高频写入下的批量提交与事务

单条INSERT在每秒几十条时就开始吃力,热词里「c# sqlbulkcopy 表变动有影响」提示了批量写入的坑。SQL Server 有SqlBulkCopy,MySQL 可以用MySqlBulkCopy或拼多值INSERT。产线上更稳的做法是攒一批再提交,配合事务。

// 攒批写入,每 50 条或 500ms 提交一次 private List<IdResult> _pending = new List<IdResult>(); private readonly object _lock = new object(); public void EnqueueForBatch(IdResult r) { lock (_lock) { _pending.Add(r); if (_pending.Count >= 50) FlushBatch(); } } private void FlushBatch() { if (_pending.Count == 0) return; using var conn = new MySqlConnection(ConnStr); conn.Open(); using var tx = conn.BeginTransaction(); try { string sql = "INSERT INTO id_record (id_code, station, create_time) VALUES " + string.Join(",", _pending.Select((_, i) => $"(@id{i}, @st{i}, @tm{i})")); using var cmd = new MySqlCommand(sql, conn, tx); for (int i = 0; i < _pending.Count; i++) { cmd.Parameters.AddWithValue($"@id{i}", _pending[i].Id); cmd.Parameters.AddWithValue($"@st{i}", "ST01"); cmd.Parameters.AddWithValue($"@tm{i}", _pending[i].Time); } cmd.ExecuteNonQuery(); tx.Commit(); _pending.Clear(); } catch { tx.Rollback(); throw; } }

逻辑说明:攒批减少网络往返,事务保证一批要么全进要么全不进。lock保护列表,因为入队可能来自多个线程。参数名带下标避免重复。

参数说明:批量大小 50 是经验值,太大单条 SQL 过长,太小没效果。超时时间也要调,默认 30 秒,批量大时可能不够。

提示:SqlBulkCopy在表结构变动(加列、改类型)后会静默失败或错位,用之前先校验目标表结构,这是血泪经验。

5. 避坑与排查:这套链路最容易翻车的五个点

5.1 相机取流正常但一帧都不到

现象:StartGrabbing返回成功,回调不触发。原因:相机处于触发模式但没给触发信号,或者TriggerSource配错。解决:先查TriggerMode是否为On,是的话确认触发源(软触发调MV_CC_TriggerSoftwareExecute_NET,硬触发查接线和 IO 配置)。

5.2 轴走位偏差累积

现象:跑几十个循环后位置越偏越多。原因:脉冲当量算错,或加减速太猛丢步。解决:重新核算脉冲当量(含减速比),把加速时间从 0.1s 放到 0.2s 以上,起始速度别低于 5mm/s。

5.3 数据库连接池耗尽

现象:跑几小时后报「too many connections」。原因:某处MySqlConnection没using,或异常路径没释放。解决:全局搜new MySqlConnection,确保都在using里;连接字符串加Connection Lifetime=300让池定期回收。

5.4 回调里写库导致丢帧

现象:识别率时高时低,帧率不稳。原因:在图像回调里直接写数据库,阻塞了 SDK 取流线程。解决:回调只入队,写库交给独立线程,队列设上限,满了丢最旧或报警。

5.5 多相机时回调分不清设备

现象:两个相机回调数据串了。原因:回调注册时pUser传了IntPtr.Zero,没带设备标识。解决:注册回调时把设备索引或句柄包成GCHandle传给pUser,回调里解出来区分。

6. 进阶:把识别结果和运动轨迹绑成可追溯记录

基础链路跑通后,真正让这套资源有价值的是把「识别结果」和「运动轨迹」绑在一起。产线追溯不只是记一个 ID,还要知道这个 ID 是在哪个位置、哪个时刻、哪个轴状态下识别的。我一般会在id_record表里加三列:axis_pos(识别时的轴坐标)、cycle_no(循环序号)、image_path(原图存档路径)。这样出问题时能回放整个工位状态。

-- 扩展后的追溯表结构 ALTER TABLE id_record ADD COLUMN axis_pos DECIMAL(10,3) COMMENT '识别时轴坐标', ADD COLUMN cycle_no INT COMMENT '循环序号', ADD COLUMN image_path VARCHAR(255) COMMENT '原图路径';

写入时把dmc_get_position_unit拿到的坐标一起带上:

double pos = LTDMC.dmc_get_position_unit(_cardNo, 0); var record = new IdResult { Id = id, Time = DateTime.Now, AxisPos = pos, CycleNo = _cycleCounter++, ImagePath = SaveImage(bmp, id) // 按 ID 命名存图 }; EnqueueForBatch(record);

验证方法:跑 100 个循环,查库看cycle_no是否连续、axis_pos是否和理论位置一致、image_path对应的图能否打开且 ID 可复现。三个都对,链路才算稳。

参数上,axis_pos用DECIMAL(10,3)够到微米级;image_path别存绝对路径,存相对路径,换机器不用改。存图会吃磁盘,按天分目录,定期清理。

从那以后我每次接这类视觉加运动的工位,都强制先把「到位 → 触发 → 识别 → 落库」这条时序在纸上画一遍,标好每个环节的超时和失败分支,再动手写代码。希望帮到你。

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