⒈目的
基于海康官方提供的MVS案例进行简化至极简状态下的MV-CU系列相机的资源检索、打开、关闭以及PNG格式和JPG格式图片保存,为基于海康MV-CU系列相机的后续视觉开发提供基础。
⒉下载NuGet包
MvCameraControl.Net
推荐安装MvCameraControl.Net包,以便Winform在Debug和Release模式下均能进行模拟和发行测试。
联网状态下,右键解决方案-管理解决方案的NuGet程序包(N)…-浏览(MvCameraControl)-安装该NuGet包:


⒊界面设计

如上图参考海康MVS案例界面,针对MU-CU系列相机在视觉相关应用,截取相机的资源获取、打开、关闭、图像采集(PNG格式、JPG格式)、参数获取和设置等核心功能。
⒋代码编写
⑴命名空间、公共变量及初始化
using MvCameraControl;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinformAreaScanCamera
{
public partial class Form1: Form
{
//设备类型枚举,支持多种相机类型
readonly DeviceTLayerType enumTLayerType = DeviceTLayerType.MvGigEDevice | DeviceTLayerType.MvUsbDevice
| DeviceTLayerType.MvGenTLGigEDevice | DeviceTLayerType.MvGenTLCXPDevice | DeviceTLayerType.MvGenTLCameraLinkDevice | DeviceTLayerType.MvGenTLXoFDevice;
List<IDeviceInfo> deviceInfoList = new List<IDeviceInfo>();//设备信息列表
IDevice device = null;//设置当前操作设备为空
bool isGrabbing = false; //是否正在取图
bool isRecord = false; //是否正在录像
Thread receiveThread = null; //接收图像线程
private IFrameOut frameForSave = null; //获取到的帧信息, 用于保存图像
private readonly object saveImageLock = new object();//图像保存的锁对象
public Form1()
{
InitializeComponent();
SDKSystem.Initialize();//初始化SDK
Control.CheckForIllegalCrossThreadCalls = false;//允许跨线程访问UI控件
}
//内容代码
}
}
⑵错误信息处理
private void ShowErrorMsg(string message, int errorCode)//显示错误信息
{
string errorMsg;
if (errorCode == 0)
{
errorMsg = message;
}
else
{
errorMsg = message + ": Error =" + String.Format("{0:X}", errorCode);
}
switch (errorCode)
{
case MvError.MV_E_HANDLE: errorMsg += " 错误或无效的句柄 "; break;
case MvError.MV_E_SUPPORT: errorMsg += " 不支持的功能 "; break;
case MvError.MV_E_BUFOVER: errorMsg += " 缓存已满 "; break;
case MvError.MV_E_CALLORDER: errorMsg += " 函数调用顺序错误 "; break;
case MvError.MV_E_PARAMETER: errorMsg += " 参数不正确 "; break;
case MvError.MV_E_RESOURCE: errorMsg += " 申请资源失败 "; break;
case MvError.MV_E_NODATA: errorMsg += " 无数据 "; break;
case MvError.MV_E_PRECONDITION: errorMsg += " 前置条件错误或运行环境已改变 "; break;
case MvError.MV_E_VERSION: errorMsg += " 版本不匹配 "; break;
case MvError.MV_E_NOENOUGH_BUF: errorMsg += " 内存不足 "; break;
case MvError.MV_E_UNKNOW: errorMsg += " 未知错误 "; break;
case MvError.MV_E_GC_GENERIC: errorMsg += " 通用错误 "; break;
case MvError.MV_E_GC_ACCESS: errorMsg += " 节点访问条件错误 "; break;
case MvError.MV_E_ACCESS_DENIED: errorMsg += " 无权限访问 "; break;
case MvError.MV_E_BUSY: errorMsg += " 设备忙或网络断开 "; break;
case MvError.MV_E_NETER: errorMsg += " 网络错误 "; break;
}
MessageBox.Show(errorMsg, "提示");
}
⑶索引设备
private void bnEnum_Click(object sender, EventArgs e)//索引列表
{
cbDeviceList.Items.Clear();//清除设备列表
int nRet = DeviceEnumerator.EnumDevices(enumTLayerType, out deviceInfoList);
if (nRet != MvError.MV_OK)
{
ShowErrorMsg("枚举设备失败!", nRet);//枚举设备失败
return;
}
for (int i = 0; i < deviceInfoList.Count; i++) //在窗体列表中显示设备名
{
IDeviceInfo deviceInfo = deviceInfoList[i];
if (deviceInfo.UserDefinedName != "")//优先显示用户自定义名称(如果存在)
{
cbDeviceList.Items.Add(deviceInfo.TLayerType.ToString() + ": " + deviceInfo.UserDefinedName + " (" + deviceInfo.SerialNumber + ")");
}
else//否则显示制造商+型号+序列号
{
cbDeviceList.Items.Add(deviceInfo.TLayerType.ToString() + ": " + deviceInfo.ManufacturerName + " " + deviceInfo.ModelName + " (" + deviceInfo.SerialNumber + ")");
}
}
if (deviceInfoList.Count > 0)//枚举设备数量大于0时,索引第1台设备(选择第一项)
{
cbDeviceList.SelectedIndex = 0;
}
}
⑷打开
private void bnOpen_Click(object sender, EventArgs e)//打开
{
// 1. 设备选择验证
if (deviceInfoList.Count == 0 || cbDeviceList.SelectedIndex == -1)
{
ShowErrorMsg("没有设备,请选择", 0);
return;
}
// 2. 获取选择的设备信息
IDeviceInfo deviceInfo = deviceInfoList[cbDeviceList.SelectedIndex]; //获取选择的设备信息
// 3. 创建设备实例
try
{
//打开设备
device = DeviceFactory.CreateDevice(deviceInfo);
}
catch (Exception ex)
{
MessageBox.Show($"创建设备失败:" + ex.Message);
return;
}
// 4. 打开物理设备
int result = device.Open();
if (result != MvError.MV_OK)
{
ShowErrorMsg("打开设备失败:", result);
return;
}
// 5. GigE设备特殊配置
if (device is IGigEDevice)//判断是否为gige设备
{
IGigEDevice gigEDevice = device as IGigEDevice; //转换为gigE设备
int optionPacketSize;//探测网络最佳包大小(只对GigE相机有效)
result = gigEDevice.GetOptimalPacketSize(out optionPacketSize);
if (result != MvError.MV_OK)
{
ShowErrorMsg($"警告:网络包大小设置失败!", result);
}
else
{
result = device.Parameters.SetIntValue("GevSCPSPacketSize", (long)optionPacketSize);
if (result != MvError.MV_OK)
{
ShowErrorMsg("Warning: Set Packet Size failed!", result);
}
}
}
device.Parameters.SetEnumValueByString("AcquisitionMode", "Continuous");//设置采集连续模式
SetCtrlWhenOpen();//控件操作
bnGetParam_Click(null, null);//获取参数
bnStartGrabControl();//开始采集
}
private void SetCtrlWhenOpen()
{
bnOpen.Enabled = false;
bnClose.Enabled = true;
tbExposure.Enabled = true;
tbGain.Enabled = true;
tbFrameRate.Enabled = true;
cbPixelFormat.Enabled = true;
bnGetParam.Enabled = true;
bnSetParam.Enabled = true;
}
private void bnStartGrabControl()
{
// 1. 启动图像采集线程
try
{
isGrabbing = true; //设置采集标志位置位true
receiveThread = new Thread(ReceiveThreadProcess);// 设为后台线程(防止程序退出时阻塞)
receiveThread.Start();// 启动线程
}
catch (Exception ex)
{
MessageBox.Show($"线程启动失败:" + ex.Message);
throw;// 重新抛出异常(可根据需求改为return)
}
// 2. 开始硬件采集
int result = device.StreamGrabber.StartGrabbing();
if (result != MvError.MV_OK)
{
// 采集失败时的清理工作
isGrabbing = false; // 重置标志位
receiveThread?.Join(500); // 等待线程结束(500ms超时)
ShowErrorMsg($"开始采集失败!", result);
return;
}
SetCtrlWhenStartGrab(); //更新控件操作
}
private void SetCtrlWhenStartGrab()
{
cbPixelFormat.Enabled = false;
bnSaveJpg.Enabled = true;
bnSavePng.Enabled = true;
}
⑸关闭
private void bnClose_Click(object sender, EventArgs e)//关闭
{
bnStopGrabControl(sender, e);//停止控制-传递sender和e参数给控制方法
if (device != null) //关闭设备
{
this.BeginInvoke((MethodInvoker)delegate // 清除显示的图像
{
var oldImg1 = pictureBox1.Image; // 彻底清除图片
pictureBox1.Image = null;
oldImg1?.Dispose();
device.Close();
device.Dispose();
});
}
SetCtrlWhenClose(); //控件操作
}
private void SetCtrlWhenClose()
{
bnOpen.Enabled = true;
bnClose.Enabled = false;
bnSaveJpg.Enabled = false;
bnSavePng.Enabled = false;
tbExposure.Enabled = false;
tbGain.Enabled = false;
tbFrameRate.Enabled = false;
bnGetParam.Enabled = false;
bnSetParam.Enabled = false;
cbPixelFormat.Enabled = false;
}
private void bnStopGrabControl(object sender, EventArgs e)//停止控制
{
// 1. 设置采集标志位为false,注意:这将导致ReceiveThreadProcess循环终止
isGrabbing = false;
// 2. 停止设备采集流,说明:直接调用SDK的StopGrabbing方法,比依赖线程自然退出更可靠
int result = device.StreamGrabber.StopGrabbing();
// 3. 检查停止采集是否成功
if (result != MvError.MV_OK)
{
ShowErrorMsg("停止采集失败!", result);// 显示错误信息(建议后续可改为记录日志)
}
// 4. 更新UI控件状态
// 典型操作包括:
// – 启用"开始采集"按钮
// – 禁用"停止采集"按钮
// – 禁用图像保存按钮
SetCtrlWhenStopGrab();
}
private void SetCtrlWhenStopGrab()
{
cbPixelFormat.Enabled = true;
bnSaveJpg.Enabled = false;
bnSavePng.Enabled = false;
}
⑹线程处理
public void ReceiveThreadProcess()//线程处理
{
int nRet; // 存储SDK调用返回值
while (isGrabbing) // 主采集循环:持续运行直到isGrabbing为false(停止采集)
{
IFrameOut frameOut;//存取获取到的图像帧
nRet = device.StreamGrabber.GetImageBuffer(1000, out frameOut);//获取图像缓冲区,时间为1000ms
if (MvError.MV_OK == nRet)//检查图像获取是否成功
{
if (isRecord)//如果正在录像,输入帧到录像器
{
device.VideoRecorder.InputOneFrame(frameOut.Image);
}
lock (saveImageLock) //保存帧用于后续保存图片,使用lock确保多线程环境下frameForSave的安全访问
{
try
{
if (frameForSave != null)//释放之前帧资源,避免内存泄漏
{
frameForSave.Dispose();
}
frameForSave = frameOut.Clone() as IFrameOut;//克隆当前帧用于后续保存
}
catch (Exception e)
{
MessageBox.Show($"图像帧克隆失败:" + e.Message);//处理帧克隆失败的情况
return;//失败时退出线程
}
}
//条件编译:选择图像渲染方式
#if !GDI_RENDER//使用SDK自带的渲染器显示图像
device.ImageRender.DisplayOneFrame(pictureBox1.Handle, frameOut.Image);
#else//使用GDI绘制图像(备用方案)
try
{
using (Bitmap bitmap = frameOut.Image.ToBitmap())
{
if (graphics == null)
{
graphics = pictureBox1.CreateGraphics();
}
Rectangle srcRect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
Rectangle dstRect = new Rectangle(0, 0, pictureBox1.Width, pictureBox1.Height);
graphics.DrawImage(bitmap, dstRect, srcRect, GraphicsUnit.Pixel);
}
}
catch (Exception e)
{
device.StreamGrabber.FreeImageBuffer(frameOut);
MessageBox.Show(e.Message);
return; }
#endif
device.StreamGrabber.FreeImageBuffer(frameOut); // 释放图像缓冲区(必须调用,防止内存泄漏)
}
}
}
⑺保存PNG/JPG格式图片
private void bnSavePng_Click(object sender, EventArgs e)//保存PNG格式图片
{
int result;// 用于接收SDK操作结果
try
{
// 1. 初始化图片格式参数
ImageFormatInfo imageFormatInfo = new ImageFormatInfo();
imageFormatInfo.FormatType = ImageFormatType.Png;// 设置为PNG格式
// 2. 调用保存图片方法, 说明:SaveImage方法会处理线程安全和文件命名
result = SaveImage(imageFormatInfo);
// 3. 检查保存结果
if (result != MvError.MV_OK)
{
ShowErrorMsg("保存PNG图片失败!", result);// 3.1 保存失败时显示错误详情(含错误码)
return;
}
else
{
ShowErrorMsg("保存PNG图片成功!", 0);// 3.2 保存成功提示
}
}
catch (Exception ex)
{
// 4. 捕获未处理的异常,注意:此处捕获的是非SDK异常(如内存不足、文件访问权限等)
MessageBox.Show("保存PNG图片时发生异常:" + ex.Message);
return;
}
}
private int SaveImage(ImageFormatInfo imageFormatInfo)
{
if (frameForSave == null)// 1. 空值检查,/ 说明:确保有有效的图像帧可供保存
{
throw new Exception("未获取到有效图像帧");
}
// 2. 生成文件名(包含图像属性信息),格式:Image_w{宽度}_h{高度}_fn{帧号}.{格式扩展名}
string imagePath = "Image_w" + frameForSave.Image.Width.ToString() + "_h" + frameForSave.Image.Height.ToString() + "_fn" + frameForSave.FrameNum.ToString() + "." + imageFormatInfo.FormatType.ToString();
lock (saveImageLock)// 3. 线程安全保护,说明:使用锁防止多个保存操作同时访问共享资源
{
// 4. 调用SDK保存图像
// 参数说明:
// imagePath: 保存路径; frameForSave.Image: 图像数据; imageFormatInfo: 图像格式参数; CFAMethod.Equilibrated: 采用平衡算法处理色彩
return device.ImageSaver.SaveImageToFile(imagePath, frameForSave.Image, imageFormatInfo, CFAMethod.Equilibrated);
}
}
private void bnGetParam_Click(object sender, EventArgs e)//保存参数
{
IFloatValue floatValue;// 定义浮点参数接收对象
// 1. 获取曝光时间参数
int result = device.Parameters.GetFloatValue("ExposureTime", out floatValue);
if (result == MvError.MV_OK)
{
tbExposure.Text = floatValue.CurValue.ToString("F1");// 将曝光时间显示到文本框(保留1位小数)
}
// 2. 获取增益参数
result = device.Parameters.GetFloatValue("Gain", out floatValue);
if (result == MvError.MV_OK)
{
tbGain.Text = floatValue.CurValue.ToString("F1");// 将增益值显示到文本框(保留1位小数)
}
// 3. 获取实际帧率参数
result = device.Parameters.GetFloatValue("ResultingFrameRate", out floatValue);
if (result == MvError.MV_OK)
{
tbFrameRate.Text = floatValue.CurValue.ToString("F1");// 将帧率显示到文本框(保留1位小数)
}
// 4. 获取像素格式参数
cbPixelFormat.Items.Clear();//清空下拉列表
IEnumValue enumValue;
result = device.Parameters.GetEnumValue("PixelFormat", out enumValue);
if (result == MvError.MV_OK)
{
foreach (var item in enumValue.SupportEnumEntries)// 遍历所有支持的像素格式
{
cbPixelFormat.Items.Add(item.Symbolic);// 添加像素格式到下拉列表
if (item.Symbolic == enumValue.CurEnumEntry.Symbolic)// 设置当前选中项
{
cbPixelFormat.SelectedIndex = cbPixelFormat.Items.Count – 1;
}
}
}
}
⑻设置相机参数
private void bnSetParam_Click(object sender, EventArgs e)//设置相机参数
{
// 1. 参数有效性验证, 说明:检查用户输入的曝光、增益、帧率是否为有效浮点数
try
{
float.Parse(tbExposure.Text); // 曝光时间
float.Parse(tbGain.Text); // 增益值
float.Parse(tbFrameRate.Text); // 帧率
}
catch
{
ShowErrorMsg("请输入有效的数值!", 0);// 输入格式错误时提示用户
return;
}
//2.设置曝光参数,说明:关闭自动曝光,设置固定曝光时间
device.Parameters.SetEnumValue("ExposureAuto", 0);// 禁用自动曝光
int result = device.Parameters.SetFloatValue("ExposureTime", float.Parse(tbExposure.Text));
if (result != MvError.MV_OK)
{
ShowErrorMsg("设置曝光时间失败!", result);
}
// 3. 设置增益参数, 说明:关闭自动增益,设置固定增益值
device.Parameters.SetEnumValue("GainAuto", 0);// 禁用自动增益
result = device.Parameters.SetFloatValue("Gain", float.Parse(tbGain.Text));
if (result != MvError.MV_OK)
{
ShowErrorMsg("设置增益失败!", result);
}
// 4. 设置帧率参数,说明:启用帧率控制并设置目标帧率
result = device.Parameters.SetBoolValue("AcquisitionFrameRateEnable", true);
if (result != MvError.MV_OK)
{
ShowErrorMsg("启用帧率控制失败!", result);
}
else
{
result = device.Parameters.SetFloatValue("AcquisitionFrameRate", float.Parse(tbFrameRate.Text));
if (result != MvError.MV_OK)
{
ShowErrorMsg("设置帧率失败!", result);
}
}
}
private void cbPixelFormat_SelectionChangeCommitted(object sender, EventArgs e)
{
// 1. 获取当前选择的像素格式并设置到设备参数中,说明:通过字符串形式设置像素格式参数(如"Mono8", "BayerRG8"等)
int result = device.Parameters.SetEnumValueByString("PixelFormat", cbPixelFormat.Text);
// 2. 检查设置是否成功
if (result != MvError.MV_OK)
{
// 3. 如果设置失败,显示错误信息,注意:这里使用的是原始错误码,ShowErrorMsg方法会将其转换为中文描述
ShowErrorMsg("设置像素格式失败!", result);
}
}
⑼资源释放
private void Form1_FormClosing(object sender, FormClosingEventArgs e)//程序关闭事件,释放SDK资源
{
// 1. 程序关闭事件处理,说明:当窗体即将关闭时触发,用于释放所有系统资源
// 2. 释放SDK全局资源,注意:必须在设备关闭前调用,否则可能导致资源泄漏
SDKSystem.Finalize();
if (device != null) //关闭设备
{
this.BeginInvoke((MethodInvoker)delegate // 清除显示的图像
{
var oldImg1 = pictureBox1.Image; // 彻底清除图片
pictureBox1.Image = null;
oldImg1?.Dispose();
device.Close();
device.Dispose();
});
}
}
⒌应用参数
如下图运行测试,点击“检索”,“打开”和“关闭”相机,保存PNG和JPG格式图片,对相机参数进行读取和设置。





