简介:这份资源面向机器视觉与嵌入式视觉方向的C#开发者,提供海康、大华、迈德威视、巴斯勒四大主流相机SDK的封装功能块,均为作者自行封装,可直接集成到图像采集与处理项目中,省去逐家研读官方API的时间。压缩包共185个文件,约250.51MB,以70个dll动态库、34个cs源码文件为主,辅以exe示例程序、sln与csproj工程文件、config配置、resx资源及md说明文档,覆盖四个品牌的Demo工程与封装类库,目录按相机厂商分模块组织,便于对照查阅与二次开发。目前已有267人学习下载。读者可从中获得各品牌相机的调用封装函数、类库与示例代码,快速实现曝光、白平衡、分辨率控制及图像抓取、传输、处理等功能,并借助C#面向对象特性降低调试成本,适合需要快速搭建多品牌相机采集系统的中高级开发者参考使用。
1. 工业相机 SDK 封装:为什么我宁愿自己写一层 C# 中间件
产线上四台相机同时跑,海康取像、大华读码、迈德威视做定位、巴斯勒拍高反光件,上位机用 C# 写。如果每个品牌都直接调原生 SDK,代码里会散落四套句柄管理、四套回调线程、四套像素格式转换,换一个型号就要动主流程。这个资源包解决的就是这件事:把海康、大华、迈德威视、巴斯勒四家主流相机的 SDK 各自封装成统一风格的 C# 功能块,开箱即用,接口对齐,上层业务代码只认一套调用约定。
它适合做 C# 上位机、机器视觉检测、非标自动化的工程师,尤其是需要多品牌混用、又不想被某一家 SDK 绑死的场景。你拿到的是封装层,不是相机驱动本身,原生 SDK 和运行时仍要按各品牌要求装好。下面按「怎么接进来、怎么调、坑在哪」拆开讲。
2. 四家 SDK 的封装结构:统一接口怎么对齐
2.1 为什么要在原生 SDK 上再包一层
原生 SDK 的差异不在功能,而在调用模型。海康 MVS 走的是句柄加回调,大华 SDK 很多接口是同步阻塞加事件,迈德威视偏 C 风格导出函数,巴斯勒 pylon 是面向对象的 .NET 库。直接混用,主流程里全是if (brand == ...)的分支,线程模型也对不齐——有的回调在 SDK 内部线程触发,有的要你自己开线程轮询。
封装层的核心目标有三个:把「打开设备、配置参数、触发取像、拿图、释放」抽象成统一方法;把各家的回调统一转成 C# 事件或Task;把像素格式统一成一种上层能直接处理的格式,常见做法是统一转成byte[]加宽高和步长,或者直接给Bitmap。我一般会定义一个ICameraDevice接口,四家各写一个实现类,工厂按枚举创建。
public interface ICameraDevice : IDisposable { bool Open(string serialOrIndex); // 按序列号或索引打开 void SetExposure(double microseconds); // 曝光,单位微秒 void SetGain(double db); // 增益,单位 dB void StartGrabbing(); // 开始取流 event Action<CameraFrame> FrameArrived; // 统一帧事件 void StopGrabbing(); } public class CameraFrame { public byte[] Data; // 统一后的像素数据 public int Width; public int Height; public int Stride; // 行字节数,处理对齐用 public long Timestamp; }接口里SetExposure用微秒、SetGain用 dB,是为了对齐四家的量纲。海康和大华曝光多以微秒计,巴斯勒 pylon 的ExposureTime也是微秒,迈德威视部分型号用毫秒,封装里做一次换算。FrameArrived统一成Action<CameraFrame>,上层不用关心是回调还是轮询,只订阅事件即可。Stride必须保留,因为相机输出常有行对齐填充,直接按Width * 通道数读会错位,这是后面避坑章要展开的点。
2.2 工厂与配置:按品牌创建实例
四家实现类不直接 new,走一个工厂,品牌和参数从配置读。这样换相机只改配置,不动业务代码。配置我一般用 JSON,字段包括品牌、序列号、曝光、增益、触发模式。
public enum CameraBrand { Hikvision, Dahua, MindVision, Basler } public static class CameraFactory { public static ICameraDevice Create(CameraBrand brand) { switch (brand) { case CameraBrand.Hikvision: return new HikCamera(); case CameraBrand.Dahua: return new DahuaCamera(); case CameraBrand.MindVision:return new MvCamera(); case CameraBrand.Basler: return new BaslerCamera(); default: throw new NotSupportedException(brand.ToString()); } } }工厂本身很简单,价值在于把「品牌到实现类」的映射收在一处。实际项目里我会再加一层配置解析,把 JSON 里的字符串转成CameraBrand枚举,再调Create。注意每个实现类的构造函数里不要做重活,打开设备放到Open,这样上层可以先建对象、统一初始化,再按产线节拍逐个开。
2.3 各品牌实现类的关键差异
海康实现类里,初始化要调MV_CC_Initialize,创建句柄用MV_CC_CreateHandle,取流回调注册MV_CC_RegisterImageCallBackEx。回调里拿到的pData要立刻拷贝出来,不能留指针,因为 SDK 回调返回后缓冲区可能被复用。大华这边,打开设备后要CLIENT_StartRealPlay或对应取流接口,事件回调里做同样的拷贝。迈德威视的导出函数偏 C 风格,注意CameraGetImageBuffer拿到的缓冲区要调CameraReleaseImageBuffer释放,漏掉会很快耗尽缓冲。巴斯勒 pylon 用InstantCamera加ImageEventHandler,GrabResult转byte[]时注意PixelType可能是BGR8也可能是Mono8,要分支处理。
四家都要处理「打开失败」的返回码。海康和大华返回 int 错误码,迈德威视返回枚举,巴斯勒抛异常。封装里统一转成自定义异常或返回 bool 加日志,别让上层去认四套错误码。
3. 从零接入:环境准备与第一个取像程序
3.1 原生 SDK 与运行时的安装顺序
封装层依赖原生 SDK,装错顺序会找不到 DLL。常见做法是先装各品牌官方客户端或 SDK 运行包,确认官方 demo 能出图,再引封装。海康装 MVS,大华装对应 SDK 运行库,迈德威视装其驱动和 SDK,巴斯勒装 pylon。装完把各家的托管 DLL 和原生 DLL 路径理清:托管 DLL 用「添加引用」,原生 DLL 要么放输出目录,要么设好 PATH。
提示:四家 SDK 的位数必须和你的 C# 工程一致。x64 工程配 x64 原生 DLL,混用会报
BadImageFormatException,这个错九成是位数不匹配。
3.2 引用封装与配置相机
把封装工程或编译好的 DLL 引入你的上位机项目,然后按配置创建实例。下面是一段最小可跑的取像流程,打开、设参数、订阅、取一帧、释放。
var cam = CameraFactory.Create(CameraBrand.Hikvision); cam.FrameArrived += frame => { // frame.Data 已是统一格式,可直接送算法 Console.WriteLine($"收到帧 {frame.Width}x{frame.Height} stride={frame.Stride}"); }; if (!cam.Open("DA1234567")) // 传序列号,或传索引 { Console.WriteLine("打开失败,检查序列号和占用"); return; } cam.SetExposure(5000); // 5000 微秒 cam.SetGain(2.0); // 2 dB cam.StartGrabbing(); Thread.Sleep(2000); // 演示用,实际由业务控制 cam.StopGrabbing(); cam.Dispose();Open传序列号比传索引稳,索引会随枚举顺序变。SetExposure和SetGain要在StartGrabbing之前调,部分品牌取流中改参数会失败或需要停流。FrameArrived里不要做耗时算法,拷贝数据后丢给处理线程,否则会堵住 SDK 回调线程,表现为掉帧或卡死。
3.3 参数怎么设:曝光、增益、触发模式
曝光和增益是最常调的两个。曝光决定亮度和运动模糊,增益决定亮度但引入噪声。产线节拍快、物体有运动,优先加曝光到不糊,再补少量增益。触发模式分连续和软触发/硬触发,检测工位一般用硬触发,由光电传感器给信号。
| 参数 | 典型范围 | 调整方向 | 注意 |
|---|---|---|---|
| 曝光 | 100~20000 微秒 | 先加曝光再补增益 | 过长会运动模糊 |
| 增益 | 0~15 dB | 最后补,越小噪声越低 | 过高噪点明显 |
| 触发 | 连续/软触发/硬触发 | 工位用硬触发 | 触发源要接对引脚 |
| 像素格式 | Mono8/BGR8 | 按算法选 | 转换有开销 |
触发模式在封装里通常做成SetTriggerMode方法,硬触发还要设触发源和去抖。海康和大华的触发源枚举不同,封装里统一成「软触发/硬触发」两个值,内部映射。
4. 多相机并发与线程模型:别让回调拖垮主流程
4.1 回调线程与业务线程的隔离
四家 SDK 的回调线程归属不同,有的在 SDK 内部线程池,有的在你调用的线程。共同点是:回调里做重活会阻塞取流。正确做法是回调里只做数据拷贝,把CameraFrame塞进BlockingCollection或Channel,业务线程消费。
private BlockingCollection<CameraFrame> _queue = new(10); // 回调里只入队 cam.FrameArrived += frame => _queue.TryAdd(frame); // 业务线程消费 Task.Run(() => { foreach (var frame in _queue.GetConsumingEnumerable()) { // 这里做算法,慢一点没关系 Process(frame); } });队列容量设有限值,满了就丢帧并计数,比无限堆积到内存爆掉好。丢帧计数要暴露出来,产线调试时一眼能看出是处理慢还是相机慢。
4.2 四台相机同时跑的同步问题
多相机工位常要求「四台都取到同一时刻的图」再合成判断。硬触发可以给四台同一触发信号,软触发则要发完信号后等四台都到帧。封装里可以加一个MultiCameraSync辅助类,用CountdownEvent或Task.WhenAll等齐。
var tcs = new TaskCompletionSource<CameraFrame>[4]; for (int i = 0; i < 4; i++) { int idx = i; tcs[idx] = new TaskCompletionSource<CameraFrame>(); cams[idx].FrameArrived += f => tcs[idx].TrySetResult(f); } // 触发后等齐 await Task.WhenAll(tcs.Select(t => t.Task));注意TrySetResult只认第一次,避免重复帧覆盖。等齐后要检查每台是否超时,超时的那台单独报错,不要让整个工位卡死。
4.3 资源释放与异常退出
相机句柄不释放,下次打开会报占用。Dispose里要按「停流、注销回调、销毁句柄、反初始化」顺序来,顺序错会崩。海康要先MV_CC_StopGrabbing再MV_CC_DestroyHandle,大华要先停实时流再登出。异常退出时用try/finally保证释放,别指望进程退出自动回收,SDK 的原生资源不一定跟着回收。
5. 避坑与排查:封装层最常见的五类翻车
5.1 打开失败但官方 demo 正常
现象:封装里Open返回 false,官方客户端能出图。原因多半是序列号传错、相机被官方客户端占用、或位数不匹配。解决:先关掉官方客户端,确认序列号,检查工程和 DLL 位数。海康和大华同一台相机被两个进程打开会失败,这是常见占用问题。
5.2 图像花屏或错位
现象:出图有斜纹、颜色错乱。原因几乎都是Stride没按行对齐读,或像素格式判断错。解决:用Stride而不是Width * 通道数来定位每行起点,转换前确认PixelType。巴斯勒的GrabResult要按PixelType分支,Mono8 当 BGR8 处理必花。
5.3 回调里抛异常导致取流中断
现象:跑一会儿就不出图,日志里有未捕获异常。原因是在FrameArrived里做了可能抛异常的操作,异常冒泡到 SDK 回调线程,SDK 直接停流。解决:回调里整体包try/catch,异常只记日志,绝不外抛。
5.4 多相机时某台掉线
现象:四台跑一段时间,某台不再出帧。原因可能是带宽不足、USB 供电不稳、或该台回调线程被堵。解决:先看丢帧计数和队列长度,确认是处理慢还是链路问题;USB 相机换独立供电口,网口相机检查 MTU 和交换机带宽。
5.5 换型号后参数越界
现象:换一个型号,设曝光报错或图像全黑。原因:不同型号曝光/增益范围不同,封装里写死了上限。解决:打开后先读参数范围,再按范围裁剪设定值,别硬编码。海康和大华都提供范围查询接口,封装里应暴露出来。
6. 进阶:把封装层做成可验证、可扩展的中间件
封装写到能跑只是及格,真正省心的是让它可验证、可扩展。我一般会加一个「自检」方法,打开相机后读一遍关键参数范围、试设一次曝光增益、取一帧校验宽高和Stride,全过才算设备就绪。这样换型号、换工位时,问题在自检阶段就暴露,不会等到产线跑起来才发现。
public bool SelfTest(ICameraDevice cam) { if (!cam.Open(_serial)) return false; cam.SetExposure(1000); cam.SetGain(0); CameraFrame f = null; cam.FrameArrived += frame => f ??= frame; cam.StartGrabbing(); var ok = SpinWait.SpinUntil(() => f != null, 3000); cam.StopGrabbing(); if (!ok || f.Width <= 0 || f.Stride < f.Width) return false; return true; }SpinWait.SpinUntil等首帧,超时 3 秒判失败。校验Stride >= Width能挡住大部分格式错。自检通过再进主流程,失败就报具体哪一步,比一句「打开失败」有用得多。
扩展方向上,新增品牌只要实现ICameraDevice并在工厂注册,不动上层。参数配置建议外置成 JSON,不同工位不同文件,换线只换配置。日志把每台的打开、设参、丢帧、异常都打上序列号,多相机排查时能直接定位到哪台。
注意:封装层不要试图抹平所有差异。有些品牌特有功能,比如海康的某些智能模式、巴斯勒的某些高级触发,硬塞进统一接口会变形。我的做法是统一接口只覆盖 80% 通用能力,特有功能通过
AsHik()、AsBasler()这类方法暴露原生对象,需要的人自己下去调。
从那以后我每次接新相机,都强制先跑一遍自检再进业务代码,宁可多花十分钟,也不在产线上对着黑屏猜。希望帮到你。
本文还有配套的精品资源,点击获取