机器视觉开发教程——C#如何封装海康工业相机SDK调用OpenCV/YOLO/VisionPro/Halcon算法

目录

引言

机器视觉实际应用场景广泛,连接的相机品牌多样化,本教程旨在演示如何利用C#语言中类的继承与多态的特性实现多种工业相机的SDK调用快速封装,可适用于海康威视/Basler/Cognex/华睿科技/迈德威视等各种品牌。

本教程先演示如何封装海康工业相机。

前期准备

下载并安装海康工业相机客户端MVS

Step1 创建工程

利用Visual Studio创建.NET6.0工程
根据实际情况选择.NET/.NET Framework/WPF

Step2 创建接口

创建相机接口ICamera

2.1定义操作相机实例接口方法

接口 传参 说明
List< string > GetListEnum() / 获取相机SN枚举
bool InitDevice(string SN, object Handle = null) 相机SN && 窗口句柄 初始化相机
bool CloseDevice() / 关闭相机
bool StartBySoftTriggerModel() / 相机开始采集并设置为软触发模式
bool StartByHardTriggerModel(TriggerSource triggerSource = TriggerSource.Line0) 触发源 相机开始采集并设置为硬触发模式
bool SoftTrigger() / 软触发一次

2.2定义设置相机参数接口方法(部分)

接口 传参 说明
bool SetTriggerMode(TriggerMode mode, TriggerSource source = TriggerSource.Line0) 触发模式&&触发源 设置触发模式及触发源
bool SetExpouseTime(double value) 曝光时长 设置曝光时长

Step3 创建基类

这里涉及到类的继承与多态,读者不了解可自行扩展学习 之所以定义相机基类是为了规范该接口适用于其他品牌相机的SDK例如巴斯勒/康耐视/华睿/迈德威视等

3.1定义操作相机实例&&设置相机参数的抽象层

接口 传参 说明
public abstract List< string > GetListEnum() / 获取相机SN枚举
public abstract bool InitDevice(string SN, object Handle = null) 相机SN && 窗口句柄 初始化相机
public abstract bool CloseDevice() / 关闭相机
public abstract bool StartBySoftTriggerModel() / 相机开始采集并设置为软触发模式
public abstract bool StartByHardTriggerModel(TriggerSource triggerSource = TriggerSource.Line0) 触发源 相机开始采集并设置为硬触发模式
public abstract bool SoftTrigger() / 软触发一次
public abstract bool SetTriggerMode(TriggerMode mode, TriggerSource source = TriggerSource.Line0) 触发模式&&触发源 设置触发模式及触发源
public abstract bool SetExpouseTime(double value) 曝光时长 设置曝光时长

3.2定义操作相机实例&&设置相机参数的公用方法

1.获取当前帧图片

所有的相机取图都通过回调的方式获取

通过获取相机回调的CallBackImg来实现

该方法不能用于实时取图存在多线程问题

csharp 复制代码
/// <summary>
/// 获取当前帧图像
/// </summary>
/// <param name="bitmap"></param>
/// <param name="outtime"></param>
/// <returns></returns>
public bool GetImage(out Bitmap bitmap, int outtime = 50)
{
    bitmap = null;
    if (CallBackImg == null)
        return false;
    lock (CallBackImg)
    {
        //bitmap = this.CallBackImg.Clone() as Bitmap;
        bitmap = CallBackImg;
        Bitmap = bitmap;
        //必须清空CallBackImg
        CallBackImg = null;
        return true;
    }
}

在相机的图像回调函数中需要将当前帧取图的结果暂存在CallBackImg变量中,具体解释可见Step5-5.3 回调函数

2.软触发获取图像

软触发取图前将暂存原触发方式在本地便于硬触发工况下取图测试

实际操作为发送一次软触发指令通过获取回调返回的单帧图像

csharp 复制代码
/// <summary>
/// 软触发获取图像
/// </summary>
/// <param name="bitmap"></param>
/// <param name="outtime"></param>
/// <returns></returns>
public bool GetImageWithSoftTrigger(out Bitmap bitmap, int outtime = 3000)
{
    if (!isGrabbing)
        StartGrabbing();

    // 开始时间
    DateTime startTime = DateTime.Now; // 当前时间
    // 设置超时时间
    DateTime lastTime = startTime.AddMilliseconds(outtime);

    // 判断是否超时
    while (lastTime > DateTime.Now)// 设置超时时间为 3 秒
    {
        if (isGrabbing)
            break;
    }

    GetTriggerMode(out TriggerMode triggerMode, out TriggerSource triggerSource);
    SetTriggerMode(TriggerMode.On, TriggerSource.Software);

    bitmap = null;

    // 开始时间
    startTime = DateTime.Now; // 当前时间
    // 设置超时时间
    lastTime = startTime.AddMilliseconds(outtime);

    if (!SoftTrigger()) return false;

    // 判断是否超时
    while (lastTime > DateTime.Now)// 设置超时时间为 3 秒
    {
        GetImage(out bitmap, outtime);
        if (bitmap != null)
            break;
    }

    if (triggerMode == TriggerMode.On)
        SetTriggerMode(triggerMode, triggerSource);
    return (bitmap != null);
}
3.获取和设置相机参数
csharp 复制代码
public void SetCamConfig(CameraConfig config)
{
    SetTriggerMode(config.TriggerMode, config.TriggerSource);
    SetTriggerPolarity(config.TriggerPolarity);
    SetTriggerDelay(config.TriggerDelay);
    SetTriggerFliter(config.TriggerFilter);
    SetExpouseTime(config.ExpouseTime);
    SetGain(config.Gain);
}

public void GetCamConfig(out CameraConfig config)
{
    string SN = this.SN;
    CameraBrand Brand = this.Brand;
    GetTriggerMode(out TriggerMode triggerMode, out TriggerSource triggerSource);
    GetTriggerPolarity(out TriggerPolarity triggerPolarity);
    GetTriggerFliter(out double triggerfilter);
    GetTriggerDelay(out double triggerdelay);
    GetExpouseTime(out double expouseTime);
    GetGain(out double gain);

    config = new CameraConfig()
    {
        CameraSN = SN,
        Brand = Brand,
        TriggerMode = triggerMode,
        TriggerSource = triggerSource,
        TriggerPolarity = triggerPolarity,
        TriggerFilter = triggerfilter,
        TriggerDelay = triggerdelay,
        ExpouseTime = expouseTime,
        Gain = gain
    };
}

Step4 添加海康SDK

4.1在工程文件中创建ref文件夹

拷贝成功后在解决方案资源管理器中ref下会有对于的dll(没有则手动添加)

4.2解决方案添加引用



程序集下有MVSDK即代表添加DLL完成

Step5 实现海康SDK的操作相机实例和设置相机参数接口方法

所有相机厂家的SDK都会有接口文档提供,海康工业相机SDK的接口文档路径如下,下面会介绍如何通过接口文档实现自己的接口方法


5.1 获取相机SN枚举(枚举相机)

参考SDK接口文档的选项1枚举相机,返回相机SN队列List< string >

csharp 复制代码
public override List<string> GetListEnum()
{
    List<string> deviceList = new List<string>();
    foreach (var item in GetListInfoEnum())
    {
        IDeviceInfo deviceInfo = item;
        if (deviceInfo.UserDefinedName != "")
            deviceList.Add(deviceInfo.SerialNumber);
        else
            deviceList.Add(deviceInfo.SerialNumber);
    }
    return deviceList;
}

private List<IDeviceInfo> GetListInfoEnum()
{
    System.GC.Collect();
    deviceInfoList.Clear();
    // ch:创建设备列表 | en:Create Device List
    int nRet = DeviceEnumerator.EnumDevices(enumTLayerType, out deviceInfoList);
    if (nRet != MvError.MV_OK)
    {
        ShowErrorMsg("Enumerate devices fail!", nRet);
        return new List<IDeviceInfo>();
    }
    return deviceInfoList;
}

IDeviceInfo中包含了相机的SerialNumber序列号(SN)

为了方便调用只将相机的SN进行了返回

5.2初始化相机(创建相机实例打开相机)

创建相机实例也就是打开相机并且占用相机资源

csharp 复制代码
public override bool InitDevice(string CamSN, object handle = null)
{
    try
    {
        int nRet;
        if (string.IsNullOrEmpty(CamSN)) return false;
        var infolist = GetListInfoEnum();
        if (infolist.Count < 1) return false;

        IDeviceInfo deviceInfo = infolist[0];
        bool selectSNflag = false;
        foreach (var item in infolist)
        {
            if (item.SerialNumber.Equals(CamSN))
            {
                deviceInfo = item;
                selectSNflag = true;
                break;
            }
        }
        if (!selectSNflag) return false;

        try
        {
            // ch:打开设备 | en:Open device
            device = DeviceFactory.CreateDevice(deviceInfo);
        }
        catch (Exception ex)
        {
            MessageBox.Show("Create Device fail!" + ex.Message);
            return false;
        }


        nRet = device.Open();
        if (nRet != MvError.MV_OK)
        {
            ShowErrorMsg("Open Device fail!", nRet);
            return false;
        }

        // ch:探测网络最佳包大小(只对GigE相机有效) | en:Detection network optimal package size(It only works for the GigE camera)
        if (device is IGigEDevice)
        {
            int packetSize;
            nRet = (device as IGigEDevice).GetOptimalPacketSize(out packetSize);
            if (packetSize > 0)
            {
                nRet = device.Parameters.SetIntValue("GevSCPSPacketSize", packetSize);
                if (nRet != MvError.MV_OK)
                    Console.WriteLine("Warning: Set Packet Size failed {0:x8}", nRet);
                
                else
                    Console.WriteLine("Set PacketSize to {0}", packetSize);
                
            }
            else
                Console.WriteLine("Warning: Get Packet Size failed {0:x8}", nRet);
            
        }

        // ch:设置触发模式为On || en:set trigger mode as On
        // 设置为Off会一直触发回调
        SetTriggerMode(TriggerMode.On, TriggerSource.Software);
        device.Parameters.SetEnumValueByString("AcquisitionMode", "Continuous");

        // ch:注册回调函数 | en:Register image callback
        // 注册回调函数操作应放在打开采集流操作之前
        device.StreamGrabber.FrameGrabedEvent += CallBackEventHandler;

        if (nRet != MvError.MV_OK)
        {
            Console.WriteLine("Set TriggerMode failed:{0:x8}", nRet);
            return false;
        }

        if (!StartGrabbing())
        {
            Console.WriteLine("开始采集失败");
            return false;
        }

        //ch: 设置合适的缓存节点数量 | en: Setting the appropriate number of image nodes
        device.StreamGrabber.SetImageNodeNum(5);

        SN = CamSN;
        Brand = CameraBrand.HikCamera;
        return true;
    }
    catch { return false; }
}

1.初始化相机最主要的就是SN,通过枚举相机匹配到对应相机的IDeviceInfo

2.创建并打开相机后进行设备参数配置和图像采集绑定Event

3.设备参数配置将设置为软触发模式

4.图像采集绑定Event订阅了CallBackEventHandler函数

5.相机开始采集成功确认SN与品牌

5.3 回调函数(图像采集Event)

被事件订阅的回调函数在每次帧结束后被触发

复制代码
/// <summary>
/// 回调函数(不要在内部增加耗时操作)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void CallBackEventHandler(object sender, FrameGrabbedEventArgs e)
{
    try
    {
        int nRet;
        Console.WriteLine("Get Image Buffer: Width[{0}] , Height[{1}] , FrameNum[{2}]"
        , e.FrameOut.Image.Width, e.FrameOut.Image.Height, e.FrameOut.FrameNum);

        IImage inputImage = e.FrameOut.Image;
        IImage outImage = inputImage;
        MvGvspPixelType dstPixelType = MvGvspPixelType.PixelType_Gvsp_Undefined;

        if (IsColorPixelFormat(e.FrameOut.Image.PixelType))
                {
                    dstPixelType = MvGvspPixelType.PixelType_Gvsp_RGB8_Packed;
                }
        else if (IsMonoPixelFormat(e.FrameOut.Image.PixelType))
                {
                    dstPixelType = MvGvspPixelType.PixelType_Gvsp_Mono8;
                }
        else
                {
                    Console.WriteLine("Don't need to convert!");
                }

        if (dstPixelType != MvGvspPixelType.PixelType_Gvsp_Undefined)
                {
                    // ch:像素格式转换 | en:Pixel type convert 
                    nRet = device.PixelTypeConverter.ConvertPixelType(inputImage, out outImage, dstPixelType);
                    if (nRet != MvError.MV_OK)
                    {
                        Console.WriteLine("Image Convert failed:{0:x8}", nRet);
                        return;
                    }
                    Console.WriteLine("Image Convert success!");
                }
        //ch: 释放图像缓存 | en: Release image buffer
        device.StreamGrabber.FreeImageBuffer(e.FrameOut);
        if (outImage.ToBitmap() == null) return;
        CallBackImg = outImage.ToBitmap();
        if (this.ImageGrabbed != null)
            this.ImageGrabbed(this, new CameraEventArgs(SN, CallBackImg));
    }
    catch { }
}

1.BaseCamera基类中定义了事件public EventHandler< CameraEventArgs > ImageGrabbed = delegate { };

2.CameraEventArgs含有取图完成的相机SN与单帧图片等信息可自由扩展

3.回调的图片存储在CallBackImg变量中可用于软触发回调

4.返回的图片统一为Bitmap格式通用性强

Step6 调用封装的DLL实现取图

6.1创建工程并引用DLL

创建TestTCameras工程,引用TCameras工程生成的.dll

6.2设计演示窗口

演示窗口只包含了最基础的操作:

1、(Form1_Load)打开窗体时显示可用相机到combox1中

2、(button1_Click)点击button1连接相机

3、(button2_Click)点击button2软触发取图

4、(Form1_FormClosing)关闭窗口前断开已连接的相机


picturbox的SizeMode属性需要设置为Zoom否则无法显示完整

6.3演示窗口代码

csharp 复制代码
using System.Xml.Linq;
using TCameras;

namespace TestTCameras
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        BaseCamera camera = new HikCamera();

        private void Form1_Load(object sender, EventArgs e)
        {
            List<string> lstSN = camera.GetListEnum();
            comboBox1.Items.Clear();
            foreach (var sn in lstSN)
                comboBox1.Items.Add(sn);
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (camera != null || camera.isGrabbing)
                camera.Dispose();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (camera != null && camera.isGrabbing)
            {
                camera.CloseDevice();
                camera = new HikCamera();
            }
            string sn = comboBox1.Text;
            if (camera.InitDevice(sn, this.Handle))
            {
                camera.GetExpouseTime(out float exp);
                camera.GetGain(out float gain);

                textBox1.Text = exp.ToString();
                textBox2.Text = gain.ToString();

                MessageBox.Show($"[{sn}]连接成功");
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            if (camera == null || !camera.isGrabbing)
            {
                MessageBox.Show($"相机未连接");
                return;
            }

            if (float.TryParse(textBox1.Text, out float exp) && float.TryParse(textBox2.Text, out float gain))
            {
                camera.SetExpouseTime(exp);
                camera.SetGain(gain);

                if (camera.GetImageWithSoftTrigger(out Bitmap bitmap))
                {
                    pictureBox1.Image = bitmap;
                }
                else
                    MessageBox.Show($"获取图像失败");
            }

        }
    }
}

6.4效果展示

相机SDK封装取图

相关推荐
鸿蒙布道师11 分钟前
英伟达开源Llama-Nemotron系列模型:14万H100小时训练细节全解析
深度学习·神经网络·opencv·机器学习·自然语言处理·数据挖掘·llama
rrokoko17 分钟前
模拟太阳系(C#编写的maui跨平台项目源码)
c#·maui跨平台
知舟不叙1 小时前
基于OpenCV的人脸识别:LBPH算法
人工智能·opencv·人脸检测·lbph算法
编程乐趣2 小时前
基于.Net Core开发的GraphQL开源项目
后端·.netcore·graphql
Kookoos2 小时前
基于 PostgreSQL 的 ABP vNext + ShardingCore 分库分表实战
数据库·docker·postgresql·c#·.net
白熊1883 小时前
【计算机视觉】基于Python的相机标定项目Camera-Calibration深度解析
python·数码相机·计算机视觉
白熊1883 小时前
【计算机视觉】OpenCV实战项目:FunnyMirrors:基于OpenCV的实时哈哈镜效果实现技术解析
人工智能·opencv·计算机视觉
田辛 | 田豆芽3 小时前
【ASP.net】在Windows 11上安装IIS并测试C# Web项目的踩坑实录
windows·c#·asp.net
QUST-Learn3D3 小时前
OpenCV提取图像中的暗斑/亮斑
人工智能·opencv·计算机视觉
栀栀栀栀栀栀4 小时前
RDD实现单词计数
开发语言·c#