欢迎光临
我们一直在努力

工业相机图像统一采集架构:一套 C# 代码,无缝切换 Basler / 海康 / 堡盟!

请添加图片描述

工业相机统一采集架构:一套 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

核心组件:

  • ICameraService:统一接口
  • BaslerCameraService 等:具体实现
  • CameraServiceFactory:工厂类(支持 DI)
  • ImageFrame:标准化图像数据结构

  • 三、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)?

  • 实现 FLIRCameraService : ICameraService
  • 在工厂中注册
  • 更新配置即可

  • 七、总结

    统一采集架构 = 接口抽象 + 工厂模式 + 异步安全

    这套设计让你:

    • ✅ 告别 SDK 耦合,业务逻辑专注算法与流程
    • ✅ 一键切换相机品牌,客户变更不再头疼
    • ✅ 轻松扩展新品牌,架构未来-proof

    无论是做 标准视觉平台,还是 定制化检测软件,这套 C# 架构都能让你的代码更清晰、更健壮、更易维护。

    赞(0)
    未经允许不得转载:171主机测评 » 工业相机图像统一采集架构:一套 C# 代码,无缝切换 Basler / 海康 / 堡盟!
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址