☰
C#调用Halcon读取海康相机并在HSmartWindow实现平移缩放
2026/10/16 6:42:24 网站建设 项目流程

简介:本资源面向C#与机器视觉开发者,聚焦工业相机图像采集与交互显示场景,解决海康相机图像在Halcon窗口控件中实时显示并支持平移缩放的问题。包内共46个文件,以cs源码、dll动态库、config配置、exe可执行程序及pdb调试符号为主,另有sln解决方案、csproj工程文件与resx资源文件,压缩包约22.53MB,可直接在Visual Studio中打开运行。已有927人学习下载,适合具备一定C#基础、希望快速上手Halcon与海康相机集成的中高级开发者。资源完整呈现了从引用HalconDotNet、初始化引擎、打开设备、异步抓图到HWindowControl显示与鼠标事件驱动平移缩放的实现脉络,并附工程目录结构,便于对照调试与二次开发,可有效减少环境配置与接口调用上的试错成本。

1. 从零搭一套 C# + Halcon + 海康相机 + HSmartWindow 的完整链路

很多做机器视觉上位机的朋友都遇到过这个场景:产线用海康工业相机采图,算法用 Halcon 写,界面用 WinForm 或 WPF,最后要把相机图像实时显示在 Halcon 的 HSmartWindow 控件上,还得支持鼠标滚轮缩放、按住拖拽平移。听起来简单,真动手就会发现坑不少——相机 SDK 和 Halcon 的图像格式对不上、HSmartWindow 的缩放平移默认行为和相机刷新打架、多线程下界面卡死。这套 C# 调用 Halcon 读取海康相机并显示在 HSmartWindow 上、实现平移缩放功能的方案,就是把这些环节串成一条能跑通的链路。适合正在做视觉上位机、需要把相机采集和 Halcon 显示整合到一起的开发者,新手能照着搭,熟手能对照看参数边界。

2. 环境准备与相机取流:从 MVS 到 Halcon 图像句柄

2.1 三个组件各自负责什么

先把角色分清楚,后面排错才不会乱。海康工业相机通过 MVS(机器视觉工业相机客户端)或 SDK 提供取流接口,负责把传感器数据变成一帧图像;Halcon 负责图像处理算法和显示控件;C# 负责把两者粘起来,并驱动 HSmartWindow 的交互。

海康 SDK 里常用的取流方式有两种:一种是主动调用MV_CC_GetOneFrameTimeout拉流,另一种是注册回调MV_CC_RegisterImageCallBackEx被动收流。做实时显示,回调方式更顺,但要注意回调线程不是 UI 线程,不能直接操作控件。Halcon 这边,图像在 .NET 里用HObject表示,显示用HSmartWindowControl(WinForm)或HSmartWindowControlWPF(WPF)。相机 SDK 给的是原始字节缓冲区,需要转成 Halcon 能认的HObject。

常见做法是:回调里把原始帧拷进一个线程安全的队列或缓冲区,UI 线程定时取最新帧转HObject并显示。这样既不会丢帧卡界面,也避免在回调里做重活。

2.2 相机初始化与取流的关键参数

下面这段是相机初始化和回调注册的骨架,参数按海康 SDK 的常见命名来。实际项目里我会把相机操作封装成一个类,避免到处散落句柄。

// 相机初始化与回调注册骨架 private IntPtr _cameraHandle = IntPtr.Zero; private MyCamera.MV_CC_DEVICE_INFO _deviceInfo; public bool OpenCamera() { // 枚举设备,取第一个 GigE 或 USB 相机 MyCamera.MV_CC_DEVICE_INFO_LIST deviceList = new MyCamera.MV_CC_DEVICE_INFO_LIST(); int ret = MyCamera.MV_CC_EnumDevices_NET( MyCamera.MV_GIGE_DEVICE | MyCamera.MV_USB_DEVICE, ref deviceList); if (ret != 0 || deviceList.nDeviceNum == 0) return false; _deviceInfo = (MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure( deviceList.pDeviceInfo[0], typeof(MyCamera.MV_CC_DEVICE_INFO)); // 创建句柄并打开 _cameraHandle = MyCamera.MV_CC_CreateHandle_NET(ref _deviceInfo); ret = MyCamera.MV_CC_OpenDevice_NET(_cameraHandle); if (ret != 0) return false; // 设置触发模式为连续采集 MyCamera.MV_CC_SetEnumValue_NET(_cameraHandle, "TriggerMode", 0); // 注册回调,回调里只做拷贝,不做显示 MyCamera.MV_CC_RegisterImageCallBackEx_NET( _cameraHandle, OnFrameCallback, IntPtr.Zero); // 开始取流 return MyCamera.MV_CC_StartGrabbing_NET(_cameraHandle) == 0; }

逻辑说明:MV_CC_EnumDevices_NET枚举设备,MV_CC_CreateHandle_NET创建句柄,MV_CC_OpenDevice_NET打开设备。TriggerMode设为 0 表示连续采集,产线上如果要用硬触发就改成 1 并配置触发源。MV_CC_RegisterImageCallBackEx_NET注册回调,回调函数OnFrameCallback里只把数据拷进缓冲区,不碰 UI。

参数说明:MV_GIGE_DEVICE | MV_USB_DEVICE表示同时枚举网口和 USB 相机,按实际接口类型选。TriggerMode是枚举值,0 连续、1 触发,改错会导致相机不出图。回调注册后必须调用MV_CC_StartGrabbing_NET才开始出流,这一步漏了相机是静默的,不报错但没图。

2.3 原始帧转 Halcon 的 HObject

海康回调给的是pData指针和帧信息,包含宽、高、像素格式。Halcon 的HObject可以用GenImage1或GenImage3从字节数组生成。常见像素格式是 Mono8 和 BayerRG8,前者直接生成单通道,后者要先转 RGB 或让 Halcon 做 debayer。

// 回调里拷贝原始帧到缓冲区 private byte[] _frameBuffer; private int _frameWidth, _frameHeight; private readonly object _frameLock = new object(); private void OnFrameCallback(IntPtr pData, ref MyCamera.MV_FRAME_OUT_INFO_EX pFrameInfo, IntPtr pUser) { int size = (int)(pFrameInfo.nWidth * pFrameInfo.nHeight); lock (_frameLock) { if (_frameBuffer == null || _frameBuffer.Length != size) _frameBuffer = new byte[size]; Marshal.Copy(pData, _frameBuffer, 0, size); _frameWidth = pFrameInfo.nWidth; _frameHeight = pFrameInfo.nHeight; } } // UI 线程定时取帧转 HObject 并显示 private void DisplayLatestFrame() { byte[] localBuf; int w, h; lock (_frameLock) { if (_frameBuffer == null) return; localBuf = (byte[])_frameBuffer.Clone(); w = _frameWidth; h = _frameHeight; } HObject image; HOperatorSet.GenImage1(out image, "byte", w, h, localBuf); hSmartWindowControl1.HalconWindow.DispObj(image); image.Dispose(); }

逻辑说明:回调里用Marshal.Copy把非托管指针数据拷到托管字节数组,加锁保护。UI 线程用定时器(比如 30ms 一次)取最新帧,GenImage1生成单通道图像,DispObj显示。显示完Dispose释放HObject,否则内存会涨。

参数说明:GenImage1的第二个参数"byte"是像素类型,Mono8 用 byte,Mono16 用 uint2。宽高必须和缓冲区长度匹配,否则 Halcon 会报错或显示花屏。定时器间隔决定刷新率,太小会占 UI 线程,太大会感觉卡顿,30 到 50ms 是常见区间。

3. HSmartWindow 的平移缩放:交互逻辑与坐标换算

3.1 HSmartWindow 自带的交互能力

Halcon 的HSmartWindowControl本身支持鼠标滚轮缩放和拖拽平移,但默认行为有个问题:每次DispObj新图,窗口的缩放和平移状态可能被重置,或者新图按原始比例显示,用户刚拖到的位置就跳回去了。做实时显示时,这个体验很糟。

解决思路是:不要每帧都DispObj,而是用SetPart固定显示区域,或者用DispObj后手动恢复缩放状态。更稳的做法是关掉自动适应,自己管理显示区域。

常见做法是:第一次显示时调用SetPart设置一个初始显示范围,之后每帧只更新图像内容,不重置SetPart。但DispObj本身会按图像尺寸调整窗口,所以要么用SetPart覆盖,要么在显示后重新SetPart。

3.2 手动实现平移缩放:鼠标事件与 SetPart

如果自带交互不够用,或者需要和相机刷新配合,可以自己处理鼠标事件。核心是维护一个显示区域(row1, col1, row2, col2),滚轮改缩放系数,拖拽改偏移,然后SetPart。

// 手动管理显示区域,实现平移缩放 private double _row1 = 0, _col1 = 0, _row2 = 0, _col2 = 0; private bool _isDragging = false; private Point _lastMouse; private void InitDisplayPart(int width, int height) { _row1 = 0; _col1 = 0; _row2 = height; _col2 = width; hSmartWindowControl1.HalconWindow.SetPart( (int)_row1, (int)_col1, (int)_row2, (int)_col2); } private void hSmartWindowControl1_MouseWheel(object sender, MouseEventArgs e) { double factor = e.Delta > 0 ? 0.9 : 1.1; // 滚轮上滚放大 double centerRow = (_row1 + _row2) / 2; double centerCol = (_col1 + _col2) / 2; double halfH = (_row2 - _row1) / 2 * factor; double halfW = (_col2 - _col1) / 2 * factor; _row1 = centerRow - halfH; _row2 = centerRow + halfH; _col1 = centerCol - halfW; _col2 = centerCol + halfW; hSmartWindowControl1.HalconWindow.SetPart( (int)_row1, (int)_col1, (int)_row2, (int)_col2); } private void hSmartWindowControl1_MouseDown(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { _isDragging = true; _lastMouse = e.Location; } } private void hSmartWindowControl1_MouseMove(object sender, MouseEventArgs e) { if (!_isDragging) return; double dx = e.X - _lastMouse.X; double dy = e.Y - _lastMouse.Y; double scaleX = (_col2 - _col1) / hSmartWindowControl1.Width; double scaleY = (_row2 - _row1) / hSmartWindowControl1.Height; _col1 -= dx * scaleX; _col2 -= dx * scaleX; _row1 -= dy * scaleY; _row2 -= dy * scaleY; hSmartWindowControl1.HalconWindow.SetPart( (int)_row1, (int)_col1, (int)_row2, (int)_col2); _lastMouse = e.Location; } private void hSmartWindowControl1_MouseUp(object sender, MouseEventArgs e) { _isDragging = false; }

逻辑说明:_row1/_col1/_row2/_col2是 Halcon 图像坐标系下的显示区域。滚轮事件里以当前显示中心为基准,按系数缩放区域,再SetPart。拖拽事件里把鼠标像素位移换算成图像坐标位移,更新区域。scaleX/scaleY是图像坐标和控件像素的比例,保证拖拽跟手。

参数说明:factor0.9 和 1.1 是缩放步长,改小更细腻但滚轮次数多,改大更快但容易过头。SetPart的参数是图像坐标,不是控件坐标,别搞混。拖拽时如果scaleX算错,会出现鼠标移动和图像移动不同步的“飘”感。

3.3 实时刷新与交互的冲突处理

相机实时刷新时,每帧都DispObj会覆盖SetPart的效果,因为DispObj默认会重置显示区域。解决办法有两个:一是显示前先SetPart再DispObj,二是用DispObj后立刻重新SetPart。前者更干净。

// 显示时保持当前缩放平移状态 private void DisplayWithPart(HObject image) { hSmartWindowControl1.HalconWindow.SetPart( (int)_row1, (int)_col1, (int)_row2, (int)_col2); hSmartWindowControl1.HalconWindow.DispObj(image); }

逻辑说明:先SetPart锁定显示区域,再DispObj画图,这样新帧不会改变缩放平移。注意SetPart要在DispObj之前调用,顺序反了会被覆盖。

参数说明:如果图像尺寸变了(比如切换分辨率),需要重新InitDisplayPart,否则SetPart区域可能超出图像范围,显示空白。

4. 避坑与常见问题排查

4.1 相机出流但 HSmartWindow 黑屏

现象:相机回调有数据,GenImage1也不报错,但 HSmartWindow 上就是黑的。

原因:常见是GenImage1的宽高和实际帧不一致,或者像素格式不对。海康某些型号默认输出 BayerRG8,直接按 Mono8 生成会花屏或黑屏。另外DispObj后没有SetPart,显示区域可能落在图像外。

解决:先确认pFrameInfo.enPixelType,Mono8 用GenImage1,BayerRG8 用GenImage3或先转 RGB。宽高从pFrameInfo.nWidth/nHeight取,别硬编码。显示前调SetPart到图像范围内。

4.2 缩放平移后图像跳回原始位置

现象:拖拽或滚轮后,下一帧刷新时图像跳回初始位置。

原因:DispObj默认会重置显示区域,或者定时器里重新调用了InitDisplayPart。

解决:显示时先SetPart再DispObj,并且只在初始化时调一次InitDisplayPart。定时器里不要重置显示区域。

4.3 回调线程操作控件导致崩溃

现象:程序运行一段时间后抛跨线程异常,或者界面卡死。

原因:海康回调在 SDK 线程执行,直接调HSmartWindowControl的方法会跨线程操作控件。

解决:回调里只拷贝数据到缓冲区,UI 线程用定时器取帧显示。所有控件操作都在 UI 线程。

4.4 内存持续增长

现象:运行几小时后内存占用越来越高,最终卡顿或崩溃。

原因:HObject没有Dispose,或者每帧都新建HObject但没释放。

解决:GenImage1生成的HObject用完立刻Dispose。如果频繁创建,考虑复用缓冲区或减少刷新频率。

4.5 拖拽时图像移动方向和鼠标相反

现象:鼠标往右拖,图像往左跑。

原因:SetPart的坐标方向和鼠标位移方向没对齐,scaleX/scaleY符号搞反。

解决:检查_col1 -= dx * scaleX的符号,通常鼠标右移应该让显示区域左移(_col1减小),如果反了就改符号。调试时打印_row1/_col1的值看变化方向。

5. 进阶技巧:用 Halcon 的 HWindow 句柄做双缓冲与性能优化

实时显示做到后面,性能是绕不开的。每帧GenImage1+DispObj在低分辨率下没问题,但 500 万像素、30fps 时,UI 线程会被拖垮。我一般会做两件事:一是用HWindow的SetWindowParam关掉不必要的刷新,二是用双缓冲减少闪烁。

Halcon 的HWindow支持SetWindowParam设置"flush"参数,控制是否立即刷新。实时显示时可以设成"false",批量画完再flush_buffer。另外HSmartWindowControl的HalconWindow可以直接拿到HWindow句柄,用HOperatorSet操作。

// 双缓冲与刷新控制 private HObject _backBuffer; private void DisplayOptimized(HObject image) { // 关掉自动刷新 hSmartWindowControl1.HalconWindow.SetWindowParam("flush", "false"); hSmartWindowControl1.HalconWindow.SetPart( (int)_row1, (int)_col1, (int)_row2, (int)_col2); hSmartWindowControl1.HalconWindow.DispObj(image); // 手动刷新 hSmartWindowControl1.HalconWindow.FlushBuffer(); hSmartWindowControl1.HalconWindow.SetWindowParam("flush", "true"); }

逻辑说明:SetWindowParam("flush", "false")暂停自动刷新,DispObj后调FlushBuffer一次性刷新,减少重绘次数。适合高帧率场景。

参数说明:"flush"是 Halcon 窗口参数,值"true"/"false"。FlushBuffer是HWindow的方法,不是HSmartWindowControl的。如果画面撕裂,可以配合SetWindowParam("double_buffering", "true")。

另一个技巧是限制刷新率。相机 30fps,UI 没必要也 30fps,定时器设 50ms 取最新帧,丢掉的帧不影响显示,但能省一半 CPU。我习惯在定时器里判断帧序号,只有新帧才显示,避免重复画同一帧。

验证方法:用任务管理器看 CPU 和内存,优化前 500 万像素 30fps 可能占 30% 以上 CPU,优化后能降到 10% 左右。内存看HObject是否及时释放,稳定后应该是一条平线。

从那以后我每次做相机 + Halcon 显示,都强制走一遍「回调只拷贝、UI 定时取帧、SetPart 锁区域、HObject 即用即释放」这四步,少一步都可能翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询