C#与工业相机集成:海康/大华SDK的异步调用与图像预处理

内容分享3周前发布
0 0 0

在电子制造的AOI(自动光学检测)环节,一台工业相机每秒需采集30帧图像并完成缺陷检测,任何延迟或漏检都可能导致不良品流入下工序。C#凭借快速开发能力与Windows环境深度适配,成为工业相机集成的主流选择——但直接调用海康/大华SDK时,常面临“回调冲突”“内存泄漏”“多相机并发卡顿”等问题。

本文以海康MV-CA050-10GM(500万像素千兆网相机)和大华DH-IPC-HF8231F(200万像素面阵相机)为原型,详解C#集成工业相机的全流程:从SDK初始化到异步回调,从图像格式转换到OpenCV缺陷检测,最后实现4台相机并发采集的线程池优化,附可直接复用的
CameraController
封装类。

一、工业相机SDK集成的核心挑战:从“能采集”到“稳采集”

工业相机与消费级相机的核心差异,在于“实时性”与“可靠性”——需在高帧率(30fps+)下稳定输出图像,且支持连续7×24小时运行。C#集成SDK时需突破三大难点:

挑战 工业场景影响 C#技术应对
回调函数跨线程 图像数据在非UI线程,直接更新UI会崩溃
Invoke
跨线程委托,
ConcurrentQueue
缓存
非托管内存泄漏 连续采集1小时内存暴涨1GB,导致程序崩溃
Marshal.FreeHGlobal
释放非托管内存
多相机资源竞争 4台相机同时采集时帧率骤降(从30→10fps) 线程池隔离,限制单相机最大线程数

二、海康/大华SDK初始化:枚举→连接→回调注册的标准化流程

海康与大华SDK的接口设计思路一致(均基于C语言封装),初始化流程可抽象为“枚举设备→创建句柄→配置参数→注册回调→启动采集”五步。以下以海康SDK为例详解,大华仅需替换对应函数名(如
MV_CC_EnumDevices_NET

DH_CC_EnumDevices
)。

2.1 海康SDK初始化核心步骤(C# P/Invoke封装)

海康SDK提供
MvCameraControl.dll
,需通过C#的
DllImport
导入非托管函数,关键结构体与函数如下:


// 海康SDK核心结构体(设备信息)
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct MV_CC_DEVICE_INFO_NET
{
    public byte[] chManufacturerName; // 厂商名称
    public byte[] chModelName; // 相机型号
    public byte[] chSerialNumber; // 序列号(唯一标识)
    public byte[] chIpAddress; // IP地址
    // 省略其他字段...
}

// 海康SDK函数导入(仅列核心)
public static class HikVisionSDK
{
    // 枚举网络设备
    [DllImport("MvCameraControl.dll")]
    public static extern int MV_CC_EnumDevices_NET(
        uint nTLayerType, // 传输层类型:0=GigE
        ref IntPtr pstDevList, // 设备列表
        IntPtr pUser); // 用户数据

    // 创建相机句柄
    [DllImport("MvCameraControl.dll")]
    public static extern int MV_CC_CreateHandle_NET(
        ref IntPtr hCamera, // 输出句柄
        IntPtr pstDevInfo); // 设备信息

    // 打开相机
    [DllImport("MvCameraControl.dll")]
    public static extern int MV_CC_OpenDevice_NET(IntPtr hCamera);

    // 注册图像回调函数(关键:异步接收图像)
    [DllImport("MvCameraControl.dll")]
    public static extern int MV_CC_RegisterImageCallBackEx_NET(
        IntPtr hCamera,
        ImageCallBackEx pfnCallBack, // 回调函数委托
        IntPtr pUser); // 用户数据(可传递相机ID)

    // 启动采集
    [DllImport("MvCameraControl.dll")]
    public static extern int MV_CC_StartGrabbing_NET(IntPtr hCamera);

    // 关闭相机
    [DllImport("MvCameraControl.dll")]
    public static extern int MV_CC_CloseDevice_NET(IntPtr hCamera);

    // 释放句柄
    [DllImport("MvCameraControl.dll")]
    public static extern int MV_CC_DestroyHandle_NET(IntPtr hCamera);
}

// 图像回调委托(海康要求的签名)
public delegate void ImageCallBackEx(
    IntPtr pData, // 图像数据指针
    ref MV_FRAME_OUT_INFO_EX pFrameInfo, // 图像信息(宽高、格式)
    IntPtr pUser); // 用户数据(传递相机ID)

2.2 CameraController:封装初始化与异常处理


/// <summary>
/// 工业相机控制器(封装海康SDK,支持初始化、采集、释放)
/// </summary>
public class CameraController : IDisposable
{
    private IntPtr _cameraHandle = IntPtr.Zero; // 相机句柄
    private string _cameraId; // 相机唯一标识(序列号)
    private bool _isGrabbing; // 是否正在采集
    private readonly object _lock = new object(); // 线程安全锁

    // 事件:图像采集完成(供外部订阅,如UI显示、算法处理)
    public event Action<byte[], MV_FRAME_OUT_INFO_EX, string> ImageCaptured;

    /// <summary>
    /// 初始化相机(枚举→创建句柄→打开→配置)
    /// </summary>
    /// <param name="cameraSerial">相机序列号(若为null则连接第一个设备)</param>
    public bool Init(string cameraSerial = null)
    {
        lock (_lock)
        {
            // 1. 枚举网络设备
            IntPtr devListPtr = IntPtr.Zero;
            int enumResult = HikVisionSDK.MV_CC_EnumDevices_NET(0, ref devListPtr, IntPtr.Zero);
            if (enumResult != 0)
            {
                LogError("枚举设备失败", enumResult);
                return false;
            }

            // 2. 解析设备列表,找到目标相机
            var devInfo = GetDeviceInfo(devListPtr, cameraSerial);
            if (devInfo == null)
            {
                LogError("未找到目标相机", -1);
                return false;
            }
            _cameraId = Encoding.ASCII.GetString(devInfo.Value.chSerialNumber).Trim('');

            // 3. 创建句柄
            int createResult = HikVisionSDK.MV_CC_CreateHandle_NET(ref _cameraHandle, devInfo.Value.pDevInfo);
            if (createResult != 0 || _cameraHandle == IntPtr.Zero)
            {
                LogError("创建句柄失败", createResult);
                return false;
            }

            // 4. 打开相机(工业场景:重试3次,应对临时连接失败)
            int openResult = 0;
            for (int i = 0; i < 3; i++)
            {
                openResult = HikVisionSDK.MV_CC_OpenDevice_NET(_cameraHandle);
                if (openResult == 0) break;
                Thread.Sleep(500);
            }
            if (openResult != 0)
            {
                LogError("打开相机失败", openResult);
                DestroyHandle();
                return false;
            }

            // 5. 配置参数(如设置分辨率1280×720,帧率30fps)
            if (!SetCameraParam())
            {
                LogError("配置相机参数失败", -1);
                CloseDevice();
                DestroyHandle();
                return false;
            }

            // 6. 注册图像回调(传递相机ID作为用户数据,区分多相机)
            GCHandle userData = GCHandle.Alloc(_cameraId); // 托管对象钉住,避免GC回收
            int callbackResult = HikVisionSDK.MV_CC_RegisterImageCallBackEx_NET(
                _cameraHandle, 
                OnImageReceived, 
                GCHandle.ToIntPtr(userData));
            if (callbackResult != 0)
            {
                LogError("注册回调失败", callbackResult);
                userData.Free();
                CloseDevice();
                DestroyHandle();
                return false;
            }

            return true;
        }
    }

    /// <summary>
    /// 图像回调函数(非UI线程,需处理非托管内存)
    /// </summary>
    private void OnImageReceived(IntPtr pData, ref MV_FRAME_OUT_INFO_EX frameInfo, IntPtr pUser)
    {
        try
        {
            // 1. 解析用户数据(相机ID)
            var userData = GCHandle.FromIntPtr(pUser);
            string cameraId = userData.Target as string;

            // 2. 复制图像数据(非托管→托管,避免原指针被释放)
            byte[] imageData = new byte[frameInfo.nFrameLen];
            Marshal.Copy(pData, imageData, 0, imageData.Length);

            // 3. 触发事件(外部处理:UI显示、算法检测)
            ImageCaptured?.Invoke(imageData, frameInfo, cameraId);
        }
        catch (Exception ex)
        {
            LogError($"回调处理失败:{ex.Message}", -1);
        }
    }

    /// <summary>
    /// 启动采集
    /// </summary>
    public bool StartGrabbing()
    {
        if (_isGrabbing || _cameraHandle == IntPtr.Zero) return false;
        int result = HikVisionSDK.MV_CC_StartGrabbing_NET(_cameraHandle);
        _isGrabbing = result == 0;
        return _isGrabbing;
    }

    // 省略:配置参数(SetCameraParam)、日志记录(LogError)、资源释放(Dispose)等方法...

    /// <summary>
    /// 释放资源(工业场景:必须确保句柄与内存释放,避免占用)
    /// </summary>
    public void Dispose()
    {
        if (_isGrabbing)
        {
            HikVisionSDK.MV_CC_StopGrabbing_NET(_cameraHandle);
            _isGrabbing = false;
        }
        CloseDevice();
        DestroyHandle();
    }
}

2.3 初始化关键注意事项

设备枚举容错:工业环境中相机可能临时离线,需在枚举失败后等待1秒重试(最多3次);句柄管理
_cameraHandle
必须在关闭设备后释放(
MV_CC_DestroyHandle_NET
),否则下次无法创建;回调用户数据:通过
GCHandle
传递托管对象(如相机ID)时,需在相机释放时调用
Free()
,避免内存泄漏;大华SDK差异:大华回调函数参数顺序略有不同(
pUser
在前),需调整委托定义,但整体流程一致。

三、图像格式转换与预处理:从相机原始数据到算法输入

工业相机输出的原始图像格式多为Mono8(单通道灰度)BayerRG8(拜耳格式),需转换为C#可显示的
Bitmap
或OpenCV的
Mat
,同时进行降噪、增强等预处理。

3.1 格式转换:Mono8→Bitmap(实时显示)

Mono8格式(每个像素1字节,0-255表示灰度)是最常用的工业图像格式,转换为
Bitmap
需注意:

图像数据存储顺序为“行优先”,需按宽度×高度计算像素位置;
Bitmap

PixelFormat
需设为
Format8bppIndexed
(8位灰度),并创建灰度调色板。


/// <summary>
/// Mono8图像转Bitmap(用于UI显示)
/// </summary>
public static Bitmap Mono8ToBitmap(byte[] mono8Data, int width, int height)
{
    // 1. 创建8位灰度Bitmap
    var bmp = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
    // 2. 设置灰度调色板(0=黑,255=白)
    var palette = bmp.Palette;
    for (int i = 0; i < 256; i++)
        palette.Entries[i] = Color.FromArgb(i, i, i);
    bmp.Palette = palette;

    // 3. 锁定 bitmap 内存,复制数据(避免逐像素设置,提升性能)
    Rectangle rect = new Rectangle(0, 0, width, height);
    BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.WriteOnly, bmp.PixelFormat);
    IntPtr ptr = bmpData.Scan0;
    int bytes = bmpData.Stride * height;
    // 注意:bmpData.Stride可能大于width(内存对齐),需按行复制
    for (int i = 0; i < height; i++)
    {
        Marshal.Copy(
            mono8Data, i * width, 
            ptr + i * bmpData.Stride, 
            width);
    }
    bmp.UnlockBits(bmpData);
    return bmp;
}

3.2 OpenCvSharp预处理与缺陷检测

OpenCvSharp是OpenCV的C#封装,适合工业图像的实时处理(如缺陷检测、尺寸测量)。以“电子元件引脚划痕检测”为例:


/// <summary>
/// 基于OpenCV的引脚划痕检测
/// </summary>
public class DefectDetector
{
    /// <summary>
    /// 检测图像中的划痕(返回缺陷区域)
    /// </summary>
    public List<Rect> DetectScratches(byte[] mono8Data, int width, int height)
    {
        // 1. Mono8数据转OpenCV Mat
        using var mat = new Mat(height, width, MatType.CV_8UC1, mono8Data);
        // 2. 高斯模糊降噪(工业图像常含传感器噪声)
        using var blurred = new Mat();
        Cv2.GaussianBlur(mat, blurred, new Size(3, 3), 0);
        // 3. Canny边缘检测(突出划痕边缘)
        using var edges = new Mat();
        Cv2.Canny(blurred, edges, 50, 150); // 阈值根据场景调整
        // 4. 形态学操作(膨胀+腐蚀,连接断裂边缘)
        using var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3));
        using var morphed = new Mat();
        Cv2.MorphologyEx(edges, morphed, MorphTypes.Close, kernel);
        // 5. 查找轮廓,筛选划痕(面积>50像素,长度>20像素)
        var contours = new List<MatOfPoint>();
        Cv2.FindContours(morphed, contours, new Mat(), RetrievalModes.External, ContourApproximationModes.ApproxSimple);
        
        var defects = new List<Rect>();
        foreach (var contour in contours)
        {
            var rect = Cv2.BoundingRect(contour);
            double area = Cv2.ContourArea(contour);
            double length = Cv2.ArcLength(contour, closed: true);
            if (area > 50 && length > 20)
                defects.Add(rect);
            contour.Dispose();
        }
        return defects;
    }
}

3.3 性能优化:图像数据零拷贝

高帧率(如30fps)下,图像数据转换的耗时可能成为瓶颈,需采用“零拷贝”策略:

直接使用相机SDK的图像指针创建
Mat
(避免
Marshal.Copy
):


// 从非托管指针创建Mat(零拷贝,需确保指针在Mat生命周期内有效)
using var mat = new Mat(height, width, MatType.CV_8UC1, pData); 

限制预处理频率:若检测逻辑耗时100ms,可设置“每3帧处理1次”,平衡实时性与准确性。

四、多相机并发采集:线程池隔离与资源控制

在PCB检测等场景中,常需4台以上相机同时采集(如正面、反面、4个侧面),若共用线程池会导致资源竞争,帧率骤降。需通过“线程池隔离+任务队列”实现并发控制。

4.1 线程池管理:为每个相机分配独立线程池

C#的
ThreadPool
是全局共享的,可通过
TaskScheduler
为每个相机创建独立的任务调度器,限制最大线程数(如每台相机最多2个线程):


/// <summary>
/// 相机专用任务调度器(限制最大线程数)
/// </summary>
public class CameraTaskScheduler : TaskScheduler
{
    private readonly int _maxThreads; // 最大线程数
    private readonly Queue<Task> _taskQueue = new Queue<Task>();
    private readonly object _queueLock = new object();
    private int _runningThreads = 0;

    public CameraTaskScheduler(int maxThreads)
    {
        _maxThreads = maxThreads;
    }

    protected override IEnumerable<Task> GetScheduledTasks()
    {
        lock (_queueLock)
            return _taskQueue.ToArray();
    }

    protected override void QueueTask(Task task)
    {
        lock (_queueLock)
        {
            _taskQueue.Enqueue(task);
            if (_runningThreads < _maxThreads)
            {
                _runningThreads++;
                ThreadPool.UnsafeQueueUserWorkItem(ProcessTasks, null);
            }
        }
    }

    private void ProcessTasks(object state)
    {
        while (true)
        {
            Task task;
            lock (_queueLock)
            {
                if (_taskQueue.Count == 0)
                {
                    _runningThreads--;
                    break;
                }
                task = _taskQueue.Dequeue();
            }
            TryExecuteTask(task);
        }
    }

    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        return TryExecuteTask(task);
    }
}

4.2 多相机控制器:并发采集与任务分发


/// <summary>
/// 多相机管理器(协调4台相机并发采集)
/// </summary>
public class MultiCameraManager
{
    private readonly Dictionary<string, CameraController> _cameras = new Dictionary<string, CameraController>();
    private readonly Dictionary<string, CameraTaskScheduler> _schedulers = new Dictionary<string, CameraTaskScheduler>();
    private readonly DefectDetector _defectDetector = new DefectDetector();

    /// <summary>
    /// 初始化多相机(每台相机分配独立线程池)
    /// </summary>
    public void InitCameras(List<string> cameraSerials)
    {
        foreach (var serial in cameraSerials)
        {
            // 1. 创建相机控制器
            var camera = new CameraController();
            if (!camera.Init(serial))
                continue; // 初始化失败则跳过
            _cameras[serial] = camera;

            // 2. 创建专用任务调度器(每台相机最大2个线程)
            var scheduler = new CameraTaskScheduler(2);
            _schedulers[serial] = scheduler;

            // 3. 订阅图像事件,在专用线程池处理
            camera.ImageCaptured += (data, info, id) =>
            {
                // 提交图像处理任务到专用线程池
                Task.Factory.StartNew(() =>
                {
                    try
                    {
                        // 转换图像并检测缺陷
                        var defects = _defectDetector.DetectScratches(data, info.nWidth, info.nHeight);
                        // 推送结果(如UI显示、PLC控制)
                        OnDefectDetected(id, defects);
                    }
                    catch (Exception ex)
                    {
                        LogError($"相机{id}处理失败:{ex.Message}");
                    }
                }, CancellationToken.None, TaskCreationOptions.None, scheduler);
            };

            // 4. 启动采集
            camera.StartGrabbing();
        }
    }

    // 省略:缺陷通知事件(OnDefectDetected)、资源释放等方法...
}

4.3 并发采集注意事项

带宽控制:千兆网相机单台最大带宽约100MB/s,4台需确保网卡支持(建议用万兆网卡);线程数设置:线程数=CPU核心数/相机数(如8核CPU,4台相机→每台2个线程),避免过多线程导致上下文切换;数据隔离:每台相机的图像数据、处理结果需通过
cameraId
严格隔离,避免混淆(如检测结果对应错误的相机)。

五、实战效果:电子元件AOI检测系统

某电子元件AOI系统采用上述方案,集成4台海康MV-CA050-10GM相机,实现:

采集性能:单相机30fps稳定输出,4台并发时总帧率120fps(无丢帧);处理延迟:从图像采集到缺陷检测完成<50ms(满足产线节拍);可靠性:连续72小时运行,内存稳定在±5%波动,无泄漏;缺陷检测率:通过Canny边缘检测+形态学处理,划痕检测准确率>99.5%。

六、总结:工业相机集成的“三字诀”——稳、快、准

C#与工业相机集成的核心,是在“稳定性”“实时性”“准确性”之间找到平衡:

:通过句柄管理、内存释放、异常重试,确保长期运行无崩溃;:利用异步回调、零拷贝转换、线程池隔离,满足高帧率需求;:结合OpenCV预处理与算法优化,提升缺陷检测精度。

未来,可进一步集成GPU加速(如OpenCVSharp.Cuda)处理更高分辨率图像(2000万像素以上),或结合深度学习(如ONNX Runtime部署YOLO模型)检测复杂缺陷。但无论技术如何升级,“工业级可靠性”始终是第一准则——毕竟,AOI系统漏检的一个缺陷,可能导致整批产品报废。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...