
工业相机统一采集架构:一套 C# 代码,无缝切换 Basler / 海康 / 堡盟!
“客户今天用 Basler,明天换海康,难道我要重写整个采集模块?”
“三个 SDK,三种回调方式,代码越改越乱……”
在工业视觉软件开发中,多品牌相机兼容性是绕不开的现实需求。不同厂商 SDK 接口迥异、生命周期管理复杂、异常处理逻辑不一——若直接在业务层调用原生 SDK,系统将迅速陷入“意大利面条式”耦合泥潭。
本文将带你构建一个 真正解耦、可扩展、生产就绪的 C# 统一采集架构,只需:
- ✅ 一行配置 切换相机品牌
- ✅ 同一套业务逻辑 适配三家 SDK
- ✅ 新增品牌 = 实现一个类,无需改动核心流程
🛠️ 技术栈:C# 10 + .NET 6+ + 接口抽象 + 工厂模式 + 依赖注入
📦 支持:Basler pylon .NET / 海康 MVS .NET / 堡盟 GAPI .NET
一、痛点:为什么不能直接调用 SDK?
| Basler.Camera vs MvCamera vs BGAPI2.Device | 类型不兼容,无法统一管理 |
| 回调事件签名不同 | 无法复用图像处理逻辑 |
| 初始化/释放流程各异 | 资源泄漏风险高 |
| 异常类型不统一 | 错误处理代码重复 |
而统一架构能带来:
- 业务层只依赖 ICameraService
- 测试时可用 Mock 相机
- 部署时按配置动态加载
二、整体架构设计
graph LR
A[业务层] —>|依赖| B(ICameraService)
B —> C[BaslerCameraService]
B —> D[HikvisionCameraService]
B —> E[BaumerCameraService]
F[CameraServiceFactory] —>|Create("basler")| C
F —>|Create("hikvision")| D
F —>|Create("baumer")| E
G[appsettings.json] —>|"CameraType": "basler"| F
核心组件:
三、Step 1:定义统一接口与数据模型
// Models/ImageFrame.cs
public record ImageFrame
{
public byte[] Data { get; init; } = Array.Empty<byte>();
public int Width { get; init; }
public int Height { get; init; }
public long TimestampUs { get; init; } // 微秒时间戳
public string CameraId { get; init; } = string.Empty;
}
// Services/ICameraService.cs
public interface ICameraService : IDisposable
{
event Action<ImageFrame>? OnNewFrame;
Task<bool> InitializeAsync(string identifier);
Task StartAcquisitionAsync();
Task StopAcquisitionAsync();
Task<ImageFrame?> GrabFrameAsync(int timeoutMs = 1000);
}
✅ 设计亮点:
- 使用 record 确保不可变性;
- 时间戳统一为微秒,便于多相机同步;
- 异步方法支持现代 C# 并发模型。
四、Step 2:实现各品牌服务(以 Basler 为例)
▶ BaslerCameraService.cs
using Basler.Pylon;
public class BaslerCameraService : ICameraService
{
private Camera? _camera;
private readonly object _lock = new();
public event Action<ImageFrame>? OnNewFrame;
public async Task<bool> InitializeAsync(string serialNumber)
{
await Task.Run(() =>
{
try
{
_camera = new Camera();
var deviceInfo = new CameraFinder().Find(new List<ICameraInfo>
{
new CameraInfo(serialNumber, CameraInfoKey.SerialNumber)
}).FirstOrDefault();
if (deviceInfo != null)
_camera.Attach(deviceInfo);
else
throw new ArgumentException($"Camera {serialNumber} not found");
_camera.Open();
_camera.Parameters[PLCamera.MaxNumBuffer].SetValue(10);
_camera.StreamGrabber.ImageGrabbed += OnImageGrabbed;
}
catch
{
return false;
}
return true;
});
return true;
}
private void OnImageGrabbed(object sender, ImageGrabbedEventArgs e)
{
if (e.GrabResult.GrabSucceeded)
{
var payload = e.GrabResult.PayloadSize;
var data = new byte[payload];
Marshal.Copy(e.GrabResult.Buffer, data, 0, payload);
var frame = new ImageFrame
{
Data = data,
Width = e.GrabResult.Width,
Height = e.GrabResult.Height,
TimestampUs = (long)(e.GrabResult.TimeStamp / 1000),
CameraId = _camera?.CameraInfo[CameraInfoKey.SerialNumber] ?? "unknown"
};
OnNewFrame?.Invoke(frame);
}
e.Dispose(); // 重要:释放 GrabResult
}
public Task StartAcquisitionAsync() => Task.Run(() => _camera?.StreamGrabber.Start());
public Task StopAcquisitionAsync() => Task.Run(() => _camera?.StreamGrabber.Stop());
public void Dispose()
{
StopAcquisitionAsync().Wait();
_camera?.Dispose();
}
public Task<ImageFrame?> GrabFrameAsync(int timeoutMs) =>
throw new NotSupportedException("Use event-based mode for Basler");
}
💡 关键点:
- 在回调中 立即拷贝 e.GrabResult.Buffer;
- 必须调用 e.Dispose() 防止内存泄漏。
▶ HikvisionCameraService.cs(轮询模式更稳定)
using MvCamCtrl.NET;
public class HikvisionCameraService : ICameraService
{
private MyCamera? _camera;
private CancellationTokenSource? _cts;
private Task? _grabTask;
public event Action<ImageFrame>? OnNewFrame;
public async Task<bool> InitializeAsync(string ipOrSn)
{
_camera = new MyCamera();
var nRet = _camera.Create();
if (nRet != MyCamera.MV_OK) return false;
// 设置 IP 或序列号(略)
nRet = _camera.Open();
return nRet == MyCamera.MV_OK;
}
public async Task StartAcquisitionAsync()
{
_cts = new CancellationTokenSource();
_grabTask = Task.Run(async () =>
{
while (!_cts.Token.IsCancellationRequested)
{
IntPtr pData;
MV_FRAME_OUT_INFO_EX stInfo = new();
var nRet = _camera.MV_CC_GetImageBuffer(ref pData, ref stInfo, 100);
if (nRet == MyCamera.MV_OK)
{
var data = new byte[stInfo.nFrameLen];
Marshal.Copy(pData, data, 0, (int)stInfo.nFrameLen);
var frame = new ImageFrame
{
Data = data,
Width = (int)stInfo.nWidth,
Height = (int)stInfo.nHeight,
TimestampUs = (long)stInfo.nTimeStampHigh * 1000 + stInfo.nTimeStampLow / 1000,
CameraId = "hikvision"
};
OnNewFrame?.Invoke(frame);
_camera.MV_CC_FreeImageBuffer(pData); // 必须释放!
}
await Task.Delay(1, _cts.Token);
}
}, _cts.Token);
}
public async Task StopAcquisitionAsync()
{
_cts?.Cancel();
if (_grabTask != null) await _grabTask;
_camera?.MV_CC_StopGrabbing();
}
public void Dispose() => StopAcquisitionAsync().Wait();
}
⚠️ 注意:海应回调机制在高负载下易丢帧,推荐轮询模式。
▶ BaumerCameraService.cs(事件驱动)
using BGAPI2;
public class BaumerCameraService : ICameraService
{
private Device? _device;
private RemoteDevice? _remoteDevice;
public event Action<ImageFrame>? OnNewFrame;
public async Task<bool> InitializeAsync(string id)
{
var systemList = new SystemList();
var system = systemList[0];
system.Open();
var devices = system.Devices;
foreach (var dev in devices)
{
if (dev.SerialNumber == id)
{
_device = dev;
_device.Open();
_remoteDevice = _device.RemoteDevice;
_remoteDevice.Events.OnImageReceived += OnImageReceived;
return true;
}
}
return false;
}
private void OnImageReceived(Image image)
{
if (image.IsIncomplete) return;
var data = new byte[image.BufferSize];
Marshal.Copy(image.BufferPtr, data, 0, (int)image.BufferSize);
var frame = new ImageFrame
{
Data = data,
Width = (int)image.Width,
Height = (int)image.Height,
TimestampUs = (long)(image.Timestamp / 1000),
CameraId = _device?.SerialNumber ?? "baumer"
};
OnNewFrame?.Invoke(frame);
}
public Task StartAcquisitionAsync() =>
Task.Run(() => _remoteDevice?.StartAcquisition());
public Task StopAcquisitionAsync() =>
Task.Run(() => _remoteDevice?.StopAcquisition());
public void Dispose()
{
_remoteDevice?.StopAcquisition();
_device?.Dispose();
}
}
五、Step 3:工厂模式 + 依赖注入集成
// Factories/CameraServiceFactory.cs
public static class CameraServiceFactory
{
public static ICameraService Create(string type)
{
return type.ToLowerInvariant() switch
{
"basler" => new BaslerCameraService(),
"hikvision" => new HikvisionCameraService(),
"baumer" => new BaumerCameraService(),
_ => throw new ArgumentException($"Unsupported camera type: {type}")
};
}
}
// Program.cs (.NET 6+)
var builder = WebApplication.CreateBuilder(args);
// 从配置读取相机类型
var cameraType = builder.Configuration["Camera:Type"] ?? "basler";
builder.Services.AddSingleton<ICameraService>(sp =>
CameraServiceFactory.Create(cameraType));
var app = builder.Build();
// 使用示例
app.MapGet("/start", async (ICameraService camera) =>
{
await camera.InitializeAsync("24012345");
camera.OnNewFrame += frame =>
Console.WriteLine($"Frame: {frame.Width}x{frame.Height}");
await camera.StartAcquisitionAsync();
});
✅ 优势:
- 配置驱动,无需 recompile;
- 支持单元测试(Mock ICameraService);
- 易于集成到 WPF / WinForms / ASP.NET Core。
六、避坑指南:5 个实战经验
Q1:如何处理 SDK 初始化失败?
- 统一抛出 CameraInitializationException,上层统一重试或告警。
Q2:海康相机在 .NET Core 下运行?
- 海康 MVS 仅支持 .NET Framework / .NET 6+ Windows,Linux 不可用。
Q3:如何避免内存泄漏?
- Basler:必须 e.Dispose();
- 海康:必须 FreeImageBuffer;
- Baumer:Image 对象回调后自动释放(但需及时处理)。
Q4:性能有损耗吗?
- 无显著损耗!统一接口仅增加一层虚方法调用(<1% CPU)。
Q5:如何支持新品牌(如 FLIR)?
七、总结
统一采集架构 = 接口抽象 + 工厂模式 + 异步安全
这套设计让你:
- ✅ 告别 SDK 耦合,业务逻辑专注算法与流程
- ✅ 一键切换相机品牌,客户变更不再头疼
- ✅ 轻松扩展新品牌,架构未来-proof
无论是做 标准视觉平台,还是 定制化检测软件,这套 C# 架构都能让你的代码更清晰、更健壮、更易维护。

