
简介面向工业视觉与自动化领域的开发者这份C#示例程序基于海康工业相机SDK演示了从相机搜索、参数配置到图像采集与保存的完整二次开发流程。压缩包共29个文件包括C#源文件、可执行程序、动态链接库以及项目配置等整体约518KB便于直接打开工程对照学习。示例针对软件触发、硬件触发、单帧与实时采集等关键功能给出可直接运行的代码帮助理解触发时序与图像数据流转工程内还涵盖分辨率、曝光、增益等参数调节以及图像显示、保存和资源释放等操作。通过研读这些源码可快速掌握海康相机SDK的常用接口与调用习惯并借鉴其异步处理、异常捕获等实践写法为实际项目开发提供扎实起点。目前已有3500人浏览学习对想要上手海康相机二次开发的工程师具有较高参考价值。1. 海康工业相机SDK的C#示例程序建议直接抄Demo而不是啃API文档拿到海康工业相机SDK的C#开发示例程序时多数人的第一反应是翻开API手册从枚举设备开始读结果被MV_CC_开头的上百个接口劝退。这份资源恰恰是用来跳过这一步的它是MVS安装包中C#示例Demo的完整打包目录里躺着能直接编译的枚举、取流、存图、回调工程。它的核心价值不是文档是骨架——从创建设备句柄到保存一帧BMP的完整调用链全在几个示例文件里摆着。适合三类人做C#上位机还没摸过工业相机的、被GigE网卡配置折腾到想放弃的、以及需要在项目里快速验证“相机能不能出图”的视觉集成工程师。接下来我就按自己拆这个包的习惯把目录结构、跑通步骤和踩过的坑一次说清。2. 示例程序拆解MVS的C# Demo里藏着一个完整取流链路2.1 压缩包里到底是什么示例程序的文件骨架解压后你会看到一个典型的MVS C#示例目录通常包含多个独立的Visual Studio工程。不同MVS版本目录命名略有差异但核心文件基本固定。我习惯先按功能把文件分成三类封装层、调用层、原生依赖。文件/目录作用我一般怎么用MvCameraControl.csC#侧的P/Invoke封装类声明了所有MV_CC_*接口整个工程的核心不要改动编译时直接编进程序集MvCameraControl.dll原生C接口的动态库所有接口的最终实现在这里拷贝到输出目录x64版本必须匹配Demo目录GrabImage、CallBackGrabImage、EnumDevices等官方示例的入口Program.cs最该抄的部分每个目录对应一种取流场景MvCameraControl.xml接口的XML注释文档代码提示不显示注释时手动翻这个文件注意一点MvCameraControl.cs是“胶水层”它通过DllImport加载MvCameraControl.dll。这意味着你在代码里调用的device.MV_CC_GetImageBuffer最终会落到原生库。所以调试时如果报“找不到Dll”先检查输出目录里有没有对应平台版本的dll再去检查代码逻辑——这是最常见的翻车点。2.2 一条主线看懂MVS的C#接口枚举→打开→抓图→保存→释放所有海康C#示例程序都遵循同一条调用主链顺序不能乱。我把官方Demo的逻辑压缩成下面这段可直接运行的骨架去掉界面交互后只剩本质// 1. 枚举设备 MV_CC_DEVICE_INFO_LIST stDeviceList new MV_CC_DEVICE_INFO_LIST(); int nRet MvCameraControl.MV_CC_EnumDevices( MV_DEVICE_TYPE.MV_GIGE_DEVICE | MV_DEVICE_TYPE.MV_USB_DEVICE, ref stDeviceList); if (nRet ! 0 || stDeviceList.nDeviceNum 0) { Console.WriteLine(枚举失败先检查网卡IP或USB驱动); return; } // 2. 创建句柄并打开第一台设备 MvCameraControl device new MvCameraControl(); nRet device.MV_CC_CreateHandle(ref stDeviceList.pDeviceInfo[0]); nRet device.MV_CC_OpenDevice(MV_ACCESS_MODE.MV_ACCESS_Exclusive, 0); // 3. 开始取流 device.MV_CC_StartGrabbing(); // 4. 主动获取一帧图像超时时间为1000ms MV_FRAME_OUT stFrame new MV_FRAME_OUT(); nRet device.MV_CC_GetImageBuffer(ref stFrame, 1000); if (nRet 0) { // 5. 保存为BMP MV_SAVE_IMAGE_PARAM_EX stSaveParam new MV_SAVE_IMAGE_PARAM_EX(); stSaveParam.hMVHandle device.myDevice; stSaveParam.pData stFrame.pBufData; stSaveParam.nDataLen stFrame.stFrameInfo.nFrameLen; stSaveParam.nWidth stFrame.stFrameInfo.nWidth; stSaveParam.nHeight stFrame.stFrameInfo.nHeight; stSaveParam.enPixelType stFrame.stFrameInfo.enPixelType; stSaveParam.enImageType MV_SAVE_IAMGE_TYPE.MV_Image_Bmp; device.MV_CC_SaveImageToFile(ref stSaveParam); } // 6. 释放 device.MV_CC_StopGrabbing(); device.MV_CC_CloseDevice(); device.MV_CC_DestroyHandle();这段代码的逻辑说清楚就几句话枚举接口MV_CC_EnumDevices的第一个参数是设备类型GigE网口相机和USB3.0相机分别对应MV_GIGE_DEVICE和MV_USB_DEVICE同时传两个值代表两类都搜。MV_CC_CreateHandle传入的是设备信息结构体这里取列表中的第一台设备。MV_CC_GetImageBuffer的第二个参数是超时毫秒数工业现场我一般设1000到3000设太短在触发模式下很容易误报超时。保存图像的关键是MV_SAVE_IMAGE_PARAM_EX里所有字段都要对应上特别是enPixelType必须来自帧信息而非手动指定否则存出来的图片颜色就是乱的。2.3 主动取流和回调取流你的需求决定用哪个官方Demo里通常同时提供GrabImage和CallBackGrabImage两个工程本质区别只有一个谁主动拿数据。主动取流是“调用一次拿一帧”适合需要精确控制节奏的场景比如一次测量触发拍一张、处理完再拍下一张。它的优点是处理完再取下一帧天然不会积压缺点是在等待图像时当前线程被阻塞MV_CC_GetImageBuffer会卡住直到超时或收到帧。回调取流则是“相机把数据推给你”SDK内部有线程持续抓图每来一帧就调用你注册的委托函数。适合流水线上的连续检测场景。官方Demo的回调注册方式如下// 注册回调第二个参数是自定义上下文 device.MV_CC_RegisterImageCallBackEx(ImageCallback, IntPtr.Zero); // 回调实现 private static void ImageCallback(IntPtr pData, ref MV_FRAME_OUT_INFO_EX pFrameInfo, IntPtr pUserContext) { // 只是示例真实项目里这里应该拷走数据或者塞队列 Console.WriteLine($收到一帧 {pFrameInfo.nWidth} x {pFrameInfo.nHeight}); }这里有一条血泪经验回调函数运行在SDK的内部采集线程里绝不要在回调里直接保存图片、做图像算法或弹窗。一旦处理时间超过帧间隔SDK内部缓冲撑满后就开始丢帧表现为帧率上不去但CPU占用不高。常见做法是回调里只拷贝图像数据到共享队列另一条工作线程负责出队处理。对比下来我的选型习惯是单次测量场景用主动取流代码简单、排查容易连续在线检测用回调取流加队列但必须在Demo跑通之后马上加上队列溢出保护。3. 把Demo跑起来环境配置与第一个最小取流工程3.1 环境准备不是装上Visual Studio就能直接编译跑海康C#示例程序有四个前置条件顺序错了会浪费很多时间。首先是MVS完整版不是客户端精简版因为完整版才带Development目录下的示例和头文件。其次是Visual Studio我用2019和2022都编译过示例工程.NET Framework 4.6.1及以上即可。最关键的是平台目标必须设为x64海康的MvCameraControl.dll原生库是64位的如果保持“Any CPU”运行32位进程加载64位原生Dll会直接抛BadImageFormatException。第四点是防火墙GigE相机枚举走UDP广播Windows防火墙默认会拦截安装MVS时如果弹出网络访问提示必须勾选允许。检查这些条件的最快路径是打开MVS自带的客户端软件能预览图像说明驱动、IP、供电都正常这时候再回来跑C#示例问题就只可能出在工程配置层。3.2 把Demo项目复制出来改成自己的工程我不建议直接在官方Demo工程里写业务代码而是复制一份出来改名保留原始的MvCameraControl.cs再添加自己的代码文件。标准做法是# 将官方Demo整个目录复制成自己的工程 cp -r C:/Program Files (x86)/MVS/Development/Samples/C#/Demo ./MyCameraProject # 删除不需要的示例子目录视自己需要 rm -rf MyCameraProject/CallBackGrabImage MyCameraProject/GrabImage复制完成后在自己的工程里做三个修改把目标平台改为x64确认输出目录包含MvCameraControl.dll去掉原Demo里的WinForm界面代码改从Main入口跑控制台逻辑。这样一顿操作之后你得到的是一个干净的最小工程。如果项目提示找不到MvCameraControl命名空间检查.cs文件有没有被包含进编译我遇到过从旧目录拖文件过来时漏加Include的情况。3.3 最小工程踩通枚举、打开、连续采集并保存一帧下面这个控制台程序是我每次验证新相机时必跑的最小例程直接替换默认Program.cs即可using System; using MvCameraControl; class Program { static void Main(string[] args) { // 枚举拿到设备列表 MV_CC_DEVICE_INFO_LIST stDeviceList new MV_CC_DEVICE_INFO_LIST(); int nRet MvCameraControl.MV_CC_EnumDevices( MV_DEVICE_TYPE.MV_GIGE_DEVICE | MV_DEVICE_TYPE.MV_USB_DEVICE, ref stDeviceList); if (nRet ! 0 || stDeviceList.nDeviceNum 0) { Console.WriteLine(没有发现相机请检查连接); return; } // 创建设备句柄 MvCameraControl device new MvCameraControl(); nRet device.MV_CC_CreateHandle(ref stDeviceList.pDeviceInfo[0]); if (nRet ! 0) { Console.WriteLine(创建句柄失败); return; } // 打开设备Exclusive模式会独占设备 nRet device.MV_CC_OpenDevice(MV_ACCESS_MODE.MV_ACCESS_Exclusive, 0); if (nRet ! 0) { Console.WriteLine(打开设备失败); return; } // 开启连续取流默认配置即可出图 nRet device.MV_CC_StartGrabbing(); if (nRet ! 0) { Console.WriteLine(启动取流失败); return; } // 抓一帧超时设为3秒 MV_FRAME_OUT stFrame new MV_FRAME_OUT(); nRet device.MV_CC_GetImageBuffer(ref stFrame, 3000); if (nRet 0) { SaveFrame(device, stFrame, test.bmp); Console.WriteLine(图像已保存); } else { Console.WriteLine(取流超时检查触发模式); } device.MV_CC_StopGrabbing(); device.MV_CC_CloseDevice(); device.MV_CC_DestroyHandle(); } static void SaveFrame(MvCameraControl device, MV_FRAME_OUT stFrame, string fileName) { MV_SAVE_IMAGE_PARAM_EX stSaveParam new MV_SAVE_IMAGE_PARAM_EX(); stSaveParam.hMVHandle device.myDevice; stSaveParam.pData stFrame.pBufData; stSaveParam.nDataLen (uint)stFrame.stFrameInfo.nFrameLen; stSaveParam.nWidth stFrame.stFrameInfo.nWidth; stSaveParam.nHeight stFrame.stFrameInfo.nHeight; stSaveParam.enPixelType stFrame.stFrameInfo.enPixelType; stSaveParam.enImageType MV_SAVE_IAMGE_TYPE.MV_Image_Bmp; device.MV_CC_SaveImageToFile(ref stSaveParam); } }这段代码的关键点在MV_ACCESS_MODE.MV_ACCESS_Exclusive它表示独占访问。如果MVS客户端软件正开着预览独占模式打开会失败报错码类似0x80010100先关掉MVS客户端再跑程序。MV_CC_GetImageBuffer拿到的一帧数据位于SDK内部缓冲区保存完成之前不要去调用下一次取流这是缓存管理的基本规则。参数“3000”是超时毫秒数用它做现场验证能区分“相机完全不出图”和“出图慢”两种故障。4. 参数控制实战曝光、触发、像素格式在哪设4.1 曝光、增益和自动曝光的配合海康相机的参数通过节点名访问这跟工业相机的GenICam标准一脉相承。在C#示例里设置曝光时间和增益的最常见写法是// 先关自动曝光再设置手动曝光时间 device.MV_CC_SetEnumValueByHandle(ExposureAuto, 0); // 0Off device.MV_CC_SetFloatValueByHandle(ExposureTime, 500.0f); // 单位微秒 // 设置增益单位dB device.MV_CC_SetFloatValueByHandle(Gain, 8.0f);这里最容易犯的错误是只设ExposureTime不关ExposureAuto。工业相机出厂默认自动曝光是开启的你设的曝光时间立刻被自动调节覆盖屏幕亮度纹丝不动。参数“500.0f”代表500微秒即0.5毫秒这个量级适合静止或低速场景高速流水线检测需要压到100微秒以下配合补光来保证亮度。我在现场调试的经验是先关自动曝光再用手动曝光试拍拍到目标亮度后基本不动了再调整增益。把曝光设为0.5ms的前提是现场没有频闪光源如果用的是LED频闪灯还会遇到明暗条纹问题那是另一个话题。4.2 触发模式从哪开软触发调试点硬触发接现场示例程序默认是连续采集模式。做定位或测量项目时一般要改成触发模式最常用的形态是软触发和硬触发两种。软触发代码适合先跑通逻辑// 开启触发模式 device.MV_CC_SetEnumValueByHandle(TriggerMode, 1); // 1On device.MV_CC_SetEnumValueByHandle(TriggerSource, 0); // 0Software软件触发 // 每次执行下面这行相机采集一帧 for (int i 0; i 10; i) { device.MV_CC_SetCommandValue(TriggerSoftware); MV_FRAME_OUT stFrame new MV_FRAME_OUT(); int nRet device.MV_CC_GetImageBuffer(ref stFrame, 1000); if (nRet 0) { SaveFrame(device, stFrame, $frame_{i}.bmp); } }TriggerSource设置成0代表触发源是软件指令执行TriggerSoftware后相机曝光采集一帧。实际项目里更常用硬触发把传感器的信号线接到相机Line0输入PLC或光电开关给一个脉冲就采一帧。硬触发时TriggerSource要改成对应的Line编号不同型号相机支持的Line数量不同MVS客户端里能看到当前设备的触发源选项。接线时注意海康的Line输入是光耦隔离的PLC输出要接正负极我见过有人只接一根信号线导致触发不稳定——表现为时有时无地丢帧。4.3 像素格式先想好不然后面全是颜色玄学工业相机默认输出的像素格式往往不是RGB8。海康常见的有Mono8、BayerGB8、BayerRG8、YUV422等。Mono8是灰度Bayer格式本身就不是完整RGB必须先转换再显示或保存否则就是花屏。官方示例的保存接口要求你传入原始像素格式它会自动处理文件头所以前面那个Demo直接存BMP通常没问题。但如果你在回调里拿到Byte[]想做OpenCvSharp处理就必须先转换MV_PIXEL_CONVERT_PARAM stCvtParam new MV_PIXEL_CONVERT_PARAM(); stCvtParam.pSrcData pFrame.pBufData; // 原始数据 stCvtParam.nSrcDataLen (uint)pFrame.stFrameInfo.nFrameLen; stCvtParam.enSrcPixelType pFrame.stFrameInfo.enPixelType; stCvtParam.enDstPixelType PixelTypeEnums.PixelType_Gvsp_RGB8_Packed; // 目标RGB8 stCvtParam.nWidth pFrame.stFrameInfo.nWidth; stCvtParam.nHeight pFrame.stFrameInfo.nHeight; // 调用像素转换接口 device.MV_CC_ConvertPixelType(ref stCvtParam); // 此时stCvtParam.pDstData为RGB8数据转换时注意nSrcDataLen是字节长度不是像素个数。RGB8数据每个像素占3字节转完计算数据量时是宽乘高乘3。如果转出来的图像左右颠倒或者上下颠倒检查是不是相机自身的图像翻转参数被改过和像素转换无关别瞎折腾。5. 避坑记录跑海康C#示例最常见的五个翻车现场5.1 现象DllNotFoundException或BadImageFormatException程序一启动就崩原因MvCameraControl.dll没有出现在应用程序输出目录或者平台目标和dll位数不匹配。32位进程加载64位dll必崩反之也一样。解决先把Visual Studio的解决方案平台改为x64再把“项目属性-生成-输出路径”清空重建最后检查输出目录里是否有MvCameraControl.dll。没有就手动从MVS安装目录的Development\Libraries复制一份复制完右键属性看看是不是被Windows安全中心锁了。这条是最常见也是最好解决的我见过两次都是因为项目里引用了x86的dll还要跑x64。5.2 现象C#程序枚举不到相机但MVS客户端能正常预览原因枚举接口只搜索特定传输层设备要么是SDK版本太旧不支持当前相机协议要么是防火墙拦截了广播包还有一种可能是程序运行在虚拟机里USB相机没有映射进虚拟机。解决先确认枚举代码里设备类型传了MV_GIGE_DEVICE | MV_USB_DEVICE而不是只传一种。然后检查Windows防火墙是否放行了MVS相关程序常见做法是运行开发环境前临时关闭防火墙验证一次。GigE相机还要确认网卡IP和相机IP在同一网段海康相机出厂默认IP是192.168.1.64你的网卡也必须是192.168.1.x才能枚举到。5.3 现象存出来的图偏绿、偏紫或整体发灰看着就是“坏图”原因Bayer格式原始数据被当作RGB直接保存/显示或者保存接口的enPixelType传错了和帧真实格式不一致。解决检查打印出来的stFrameInfo.enPixelType如果是BayerGB8或BayerRG8那存BMP时必须让保存接口自己处理。正确做法是用stFrameInfo.enPixelType赋值给stSaveParam.enPixelType不要手动指定。如果保存前做了像素转换先确认转换后的目标格式是PixelType_Gvsp_RGB8_Packed。这问题我在用OpenCvSharp的Mat赋值时也踩过Mat的通道数必须和像素格式一致否则拿到的是乱码排列的彩色条纹。5.4 现象设置了ExposureTime画面亮度没有变化原因自动曝光没关或者你改完参数后相机处于触发模式下改动没生效在后续帧上。解决先确认代码里执行了MV_CC_SetEnumValueByHandle(ExposureAuto, 0)然后立即读取一次MV_CC_GetFloatValueByHandle(ExposureTime)看看返回值是不是你设的数。是但图像没变说明抓的是触发帧可能缓冲里还有旧帧加一条MV_CC_ClearImageBuffer()清空缓冲再触发。另外曝光最小值和最大值跟帧率有关如果把帧率改成1000fps曝光时间超过帧间隔会被钳制检查当前帧率设置。5.5 现象拔了USB线或断网后程序卡死或直接崩溃原因取流线程还在等数据GetImageBuffer一直阻塞到超时而部分接口在断线状态下内部状态没来得及释放再次开设备时残留句柄导致访问已释放内存。解决现场断连是常态正确做法是在超时返回后主动走一遍StopGrabbing、CloseDevice、DestroyHandle再重新创建句柄和打开设备。我封装了一个重连工具类每次出流错误就自动走完整释放流程等1秒后重新枚举打开。还有一个关键点C#释放时MvCameraControl对象不要重复调用DestroyHandle用try-catch包住释放操作SDK的二次释放行为在不同版本不一致别赌它的实现。6. 进阶改造把示例程序拾掇成自己的持续取流工具跑通官方Demo之后真正干活时你会发现单帧取存不够用。我通常会把示例改造成一个持续取流的后台服务一条采集线程用回调拿图图像数据塞入并发队列另一条工作线程负责出队保存或做算法。这个结构能稳定应对100到200帧每秒的彩色图瓶颈只会在算法处理速度上。public class StreamService { private ConcurrentQueuebyte[] _queue new ConcurrentQueuebyte[](); private MvCameraControl _device; private bool _running; public void Start() { // 枚举和打开设备略 _device.MV_CC_RegisterImageCallBackEx(OnFrame, IntPtr.Zero); _device.MV_CC_StartGrabbing(); _running true; Task.Run(() ProcessLoop()); } private void OnFrame(IntPtr pData, ref MV_FRAME_OUT_INFO_EX info, IntPtr ctx) { byte[] buf new byte[info.nFrameLen]; Marshal.Copy(pData, buf, 0, (int)info.nFrameLen); _queue.Enqueue(buf); // 只拷贝进队列立即返回 } private void ProcessLoop() { while (_running) { if (_queue.TryDequeue(out byte[] frame)) { SaveFrameWithPixel(frame, info); } else { Thread.Sleep(2); // 避免空转 } } } }用这个结构之前先用一个土办法验证相机实际帧率连续采集1200帧记录存图线程的完成时间差算出来平均每帧耗时再和MVS客户端显示的帧率对比。如果差距超过10%说明取流线程在等算法而不是相机出慢了。验证稳定性的标准我一般定为连续跑1小时不溢出队列、不丢帧、存图顺序不乱序。这套结构里最值得保留的习惯是回调函数里只做拷贝和入队出队后再处理业务。我第一次做视觉项目时图省事直接在回调里保存Bitmap界面卡成PPT不说内存还一直涨后来强制规定“回调函数不许碰BMP对象”才算治本。从那以后我每次拿到新相机都会先按这套流程把官方示例跑通、验证帧率、确认释放逻辑再开始写业务代码。这个顺序帮我省掉了至少十次现场返工。希望帮到你。本文还有配套的精品资源点击获取