☰
C#+Halcon+海康相机:HSmartWindowControl实时显示与交互全链路
2026/10/16 6:53:30 网站建设 项目流程

简介:这份资源面向具备一定C#基础的机器视觉开发者,聚焦工业相机图像采集与交互显示场景。内容围绕调用Halcon的.NET接口读取海康相机图像,并在HSmartHWind控件上完成显示、平移与缩放操作,涵盖HObject图像对象、HDevEngine引擎初始化、相机打开与异步抓图、窗口显示属性设置以及鼠标滚轮与移动事件响应等关键环节,可帮助读者打通相机取流到交互查看的完整链路。压缩包共46个文件,约22.53MB,以cs源码、dll动态库、config配置、exe可执行文件及pdb调试符号为主,另含sln解决方案、csproj工程文件与resx资源文件,工程结构完整,便于直接编译运行与二次开发。目前已有927人学习下载,适合工业自动化、质量检测方向需要快速搭建Halcon与海康相机联调环境的开发者参考借鉴。

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

很多做机器视觉的同行都遇到过这种局面:产线现场装的是海康工业相机,算法侧用的是 Halcon,上位机界面又要求用 C# WinForm 或 WPF 来写,结果三套东西各说各话——相机 SDK 出一路图,Halcon 要另一路图,显示控件还挑格式。更麻烦的是,客户还要求图像能像地图一样拖着看、滚轮缩放,最好再带个十字光标和实时坐标。这套需求听起来零散,其实是一条完整的链路:采集 → 转换 → 显示 → 交互。

这篇笔记拆的就是这条链路。核心是用 C# 调用 Halcon 的HSmartWindowControl(WPF 里叫 HSmartWindowControlWPF,WinForm 里叫 HSmartWindowControl),把海康相机的实时帧喂进去,并且把平移、缩放、ROI 交互全部打通。适合已经会写 C# 窗体、装过 Halcon、手上有海康 MVS 或 SDK 的从业者;如果你只会拖控件、没碰过HObject和HTuple,建议先把 Halcon 的 .NET 例程跑一遍再回来。下面所有代码基于 Halcon 的 .NET 程序集(halcondotnet)和海康 MVS 自带的MvCameraControl.Net.dll,版本差异我会在踩坑章节单独说。

2. 环境搭建与相机取流:把海康的 BGR 帧接进 Halcon 的 HObject

2.1 三个组件各自的角色和选型理由

先把职责分清楚,不然后面全是玄学问题。海康相机 SDK 负责枚举设备、打开设备、设置触发/曝光/增益、拿原始帧;Halcon 负责把原始帧转成HObject并做算法;HSmartWindowControl负责把HObject画出来并处理鼠标交互。三者之间只有两个数据接口:相机 → Halcon 的像素格式转换,和Halcon → 控件的显示对象绑定。

为什么不用 Halcon 自带的open_framegrabber直接连海康?因为海康的 GigE/USB 相机在 Halcon 的采集接口里支持并不总是完整,尤其是多相机、触发模式、参数细调这些场景,走厂商 SDK 更可控。常见做法是:采集用厂商 SDK,算法和显示用 Halcon,中间自己做一次内存拷贝转换。代价是多一次拷贝,但换来的是参数完全可控。

选HSmartWindowControl而不是老的HWindowControl,理由很直接:老控件要自己写缩放平移的鼠标事件,HSmartWindowControl内置了HWindow.SetPart的交互封装,滚轮缩放、拖拽平移开箱即用,还支持HWindow.DrawRectangle1这类交互式 ROI。省下来的时间够你调半天曝光。

2.2 引用配置与相机初始化

新建一个 WinForm 或 WPF 项目,目标框架建议 .NET Framework 4.7.2 以上(Halcon 的 .NET 程序集对 .NET Core 的支持要看版本,保守起见用 Framework)。需要引用的程序集:

程序集来源作用
halcondotnet.dllHalcon 安装目录 bin/dotnet35 或 dotnetHObject、HTuple、HSmartWindowControl
MvCameraControl.Net.dll海康 MVS 安装目录 Development/Samples/C#/相机枚举、取流、参数
System.Drawing.NET 自带Bitmap 中转(可选)

相机初始化的标准流程是:枚举 → 创建句柄 → 打开 → 设置连续采集 → 注册回调 → 开始取流。下面这段是核心骨架:

// 相机初始化:枚举 + 打开 + 连续采集 private MyCamera _camera = new MyCamera(); public void InitCamera() { // 1. 枚举 GigE 和 USB 设备 MV_CC_DEVICE_INFO_LIST deviceList = new 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) throw new Exception("未找到海康相机,检查网线/USB和 MVS 是否占用"); // 2. 用第一个设备创建句柄 IntPtr handle = IntPtr.Zero; MyCamera.MV_CC_CreateHandle_NET(ref handle, deviceList.pDeviceInfo[0]); _camera = new MyCamera(handle); // 3. 打开设备,注意 accessMode 用独占 ret = _camera.MV_CC_OpenDevice_NET( MyCamera.MV_ACCESS_Exclusive, 0); if (ret != 0) throw new Exception("打开相机失败,可能被 MVS 客户端占用"); // 4. 设置连续采集模式 _camera.MV_CC_SetEnumValue_NET("TriggerMode", 0); // 0=Off 连续 _camera.MV_CC_SetEnumValue_NET("AcquisitionMode", 2); // 2=Continuous // 5. 注册取流回调 _camera.MV_CC_RegisterImageCallBackEx_NET(OnFrameCallback, IntPtr.Zero); // 6. 开始取流 _camera.MV_CC_StartGrabbing_NET(); }

逻辑说明:MV_CC_EnumDevices_NET的第一个参数是设备类型位掩码,同时传 GigE 和 USB 才能都枚举到。MV_CC_CreateHandle_NET用ref传出句柄,后面所有操作都靠它。MV_ACCESS_Exclusive是独占模式,如果 MVS 客户端正开着相机,这里会直接失败——这是新手最常见的翻车点。RegisterImageCallBackEx_NET注册的回调运行在 SDK 的采集线程上,不要在回调里直接操作 UI 控件,否则会抛跨线程异常。

2.3 回调里把 BGR 帧转成 HObject

海康回调给出来的是pData指针加stFrameInfo,像素格式通常是PixelType_Gvsp_BGR8_Packed。Halcon 的HObject不能直接吃指针,需要先转成HImage。最稳的路径是:指针 → byte[] → HImage(通过 gen_image_interleaved 或 gen_image3)。

private HImage _currentImage; private readonly object _imgLock = new object(); private void OnFrameCallback(IntPtr pData, ref MV_FRAME_OUT_INFO_EX frameInfo, IntPtr pUser) { int width = frameInfo.nWidth; int height = frameInfo.nHeight; int size = width * height * 3; // BGR8 每像素 3 字节 byte[] buffer = new byte[size]; Marshal.Copy(pData, buffer, 0, size); // 用 gen_image_interleaved 把交错 BGR 转成三通道 HImage HImage img = new HImage(); img.GenImageInterleaved( buffer, // 像素数据 "bgr", // 交错顺序,海康默认 BGR width, height, // 宽高 -1, // 行对齐,-1 表示紧凑 "byte", // 像素类型 width, height, // 图像宽高 0, 0, // 起始行列 8, 0); // 位深和保留参数 lock (_imgLock) { _currentImage?.Dispose(); _currentImage = img; } }

参数说明:GenImageInterleaved的第二个参数"bgr"必须和相机输出一致,如果相机设成了 RGB 或 Mono,这里要改成"rgb"或走GenImage1。-1表示不做行对齐填充,海康的帧一般是紧凑的。"byte"对应 8 位。最后两个参数是位深和保留位,固定 8 和 0 即可。这里每次回调都 new 一个 HImage,旧的要Dispose,否则跑几分钟内存就爆了——Halcon 的HObject是非托管资源,GC 管不到。

提示:如果相机是黑白 Mono8,直接img.GenImage1("byte", width, height, pData)更省事,不用走交错转换。

3. 显示与交互:HSmartWindowControl 的绑定、平移缩放和坐标映射

3.1 把 HImage 刷到控件上

HSmartWindowControl在 WinForm 里的类型是HalconDotNet.HSmartWindowControl,它内部封装了一个HWindow,通过HWindowControl.HalconWindow属性访问。刷图就一句HalconWindow.DispObj(hImage),但有几个细节决定成败。

private void RefreshDisplay() { if (_currentImage == null) return; // 跨线程访问控件必须 Invoke if (this.InvokeRequired) { this.BeginInvoke(new Action(RefreshDisplay)); return; } lock (_imgLock) { // 关键:先清空再显示,否则旧图叠加 hSmartWindowControl1.HalconWindow.ClearWindow(); hSmartWindowControl1.HalconWindow.DispObj(_currentImage); } }

逻辑说明:InvokeRequired判断当前线程是不是 UI 线程,回调线程直接调DispObj会崩。ClearWindow必须加,否则每帧叠加,画面会糊成一片。刷图频率建议用定时器(比如 30ms 一次)从_currentImage取最新帧,而不是每来一帧就刷一次——相机 60fps,UI 根本刷不过来,反而卡死。

3.2 平移缩放的三种实现路径

HSmartWindowControl的平移缩放有三种做法,选哪种取决于你要不要自己控制交互逻辑。

第一种是控件自带交互。设置hSmartWindowControl1.HDoubleClickToFitContent = true,双击自适应;HMouseWheel += ...里调HalconWindow.SetPart实现滚轮缩放。控件本身对鼠标拖拽的支持有限,需要自己补。

第二种是手动 SetPart,这也是最可控的方式。核心是维护一个显示区域(row1, col1, row2, col2),鼠标拖动时平移这个矩形,滚轮时以鼠标位置为中心缩放。

private double _row1, _col1, _row2, _col2; private Point _lastMouse; private bool _isPanning; // 鼠标按下:记录起点,进入平移模式 private void hSmartWindow_MouseDown(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { _isPanning = true; _lastMouse = e.Location; } } // 鼠标移动:按像素差平移显示区域 private void hSmartWindow_MouseMove(object sender, MouseEventArgs e) { if (!_isPanning) return; // 把屏幕像素差换算成图像坐标差 double scaleX = (_col2 - _col1) / hSmartWindowControl1.Width; double scaleY = (_row2 - _row1) / hSmartWindowControl1.Height; double dx = (e.X - _lastMouse.X) * scaleX; double dy = (e.Y - _lastMouse.Y) * scaleY; _col1 -= dx; _col2 -= dx; _row1 -= dy; _row2 -= dy; hSmartWindowControl1.HalconWindow.SetPart( (int)_row1, (int)_col1, (int)_row2, (int)_col2); _lastMouse = e.Location; RefreshDisplay(); } // 滚轮:以鼠标位置为中心缩放 private void hSmartWindow_MouseWheel(object sender, MouseEventArgs e) { double factor = e.Delta > 0 ? 0.9 : 1.1; // 上滚放大,下滚缩小 // 鼠标位置对应的图像坐标 double mouseRow = _row1 + (_row2 - _row1) * e.Y / hSmartWindowControl1.Height; double mouseCol = _col1 + (_col2 - _col1) * e.X / hSmartWindowControl1.Width; _row1 = mouseRow - (mouseRow - _row1) * factor; _row2 = mouseRow + (_row2 - mouseRow) * factor; _col1 = mouseCol - (mouseCol - _col1) * factor; _col2 = mouseCol + (_col2 - mouseCol) * factor; hSmartWindowControl1.HalconWindow.SetPart( (int)_row1, (int)_col1, (int)_row2, (int)_col2); RefreshDisplay(); }

参数说明:scaleX/scaleY是屏幕像素到图像像素的换算比例,少了这一步拖动会跟手不一致。缩放时以鼠标位置为锚点,公式是「新边界 = 锚点 ± 旧距离 × 系数」,这样鼠标指着哪个点,缩放时那个点就不动,体验才对。factor取 0.9/1.1 是经验值,太大缩放会跳。

第三种是用 Halcon 的set_part算子,效果和SetPart一样,只是走 HTuple 接口。三种方式不要混用,否则显示区域会打架。

3.3 屏幕坐标反算图像坐标

做 ROI 或者取点的时候,必须把鼠标的屏幕坐标转成图像坐标,否则你画的框和实际位置对不上。

private void GetImageCoord(int screenX, int screenY, out double imgRow, out double imgCol) { // 当前显示区域 HTuple r1, c1, r2, c2; hSmartWindowControl1.HalconWindow.GetPart(out r1, out c1, out r2, out c2); double w = hSmartWindowControl1.Width; double h = hSmartWindowControl1.Height; imgRow = r1.D + (r2.D - r1.D) * screenY / h; imgCol = c1.D + (c2.D - c1.D) * screenX / w; }

逻辑说明:GetPart拿到当前显示区域,屏幕坐标按比例映射回图像坐标。注意HTuple取值用.D转 double,别用.I,否则精度丢失。这个函数在画 ROI、显示十字光标、实时坐标时都要用,建议封装成工具类。

注意:如果控件做了 DPI 缩放(高分屏常见),Width/Height和实际像素会有偏差,需要在GetImageCoord里乘上 DPI 比例,否则坐标会偏。

4. 避坑与排查:五个让链路跑不通的典型问题

4.1 相机打开失败,报「设备被占用」

现象:MV_CC_OpenDevice_NET返回非 0,或者直接抛异常。原因:海康 MVS 客户端、其他采集程序、或者上一次没释放的句柄还占着相机。解决:先关掉 MVS 客户端;在程序退出时确保调用了MV_CC_StopGrabbing_NET和MV_CC_CloseDevice_NET;如果还是不行,把相机断电重启,GigE 相机还要检查是不是被别的网段程序抢了。我一般会在FormClosing里强制走一遍释放流程,别指望 GC。

4.2 回调里刷 UI 直接崩,报跨线程异常

现象:程序跑起来几秒后抛InvalidOperationException,提示控件被非创建线程访问。原因:海康的取流回调在 SDK 线程上执行,直接调DispObj或改控件属性就崩。解决:回调里只做数据转换和存_currentImage,刷图交给 UI 定时器,或者用BeginInvoke切回 UI 线程。别用Invoke(同步),高频回调下会阻塞采集线程导致丢帧。

4.3 图像显示偏色,红蓝对调

现象:画面能出来,但颜色明显不对,红色变蓝。原因:海康默认输出BGR8_Packed,而GenImageInterleaved里写成了"rgb",或者相机被设成了 RGB 输出但代码没跟着改。解决:确认相机的PixelFormat枚举值,BGR 就写"bgr",RGB 就写"rgb"。不确定的话,用 MVS 客户端看一眼当前像素格式,别猜。

4.4 平移缩放后图像模糊或错位

现象:拖动或缩放后,图像边缘出现黑边,或者内容对不上。原因:SetPart的边界算错了,或者_row1/_col1/_row2/_col2没有在初始化时按图像尺寸赋值。解决:初始化时用_currentImage.GetImageSize拿到宽高,把四个边界设成整幅图;缩放时限制最小区域(比如不小于 50×50 像素),否则会缩到 0 导致除零。另外SetPart参数是 int,频繁取整会有累积误差,建议内部用 double 维护,只在调用时转 int。

4.5 跑一段时间内存暴涨

现象:程序运行几分钟后内存占用从几十兆涨到几个 G。原因:每次回调 new 的HImage没释放,Halcon 的HObject是非托管资源。解决:在lock里替换_currentImage时先Dispose旧的;程序退出时把_currentImage、相机句柄全部释放。可以用任务管理器盯一下,正常应该稳定在一个区间,持续上涨就是漏了。

5. 进阶技巧:用 HSmartWindowControl 做实时 ROI 与帧率自适应

链路跑通之后,真正拉开差距的是交互细节。这里分享两个我常用的技巧。

实时 ROI 绘制。HSmartWindowControl支持DrawRectangle1、DrawCircle这类交互算子,但它们会阻塞当前线程直到用户画完。正确姿势是:在按钮点击里调HalconWindow.DrawRectangle1(out row1, out col1, out row2, out col2),画完拿到坐标后立刻DispObj一个矩形上去,再把这组坐标传给 Halcon 做ReduceDomain或CropDomain。注意画 ROI 期间要暂停刷图,否则新帧会把 ROI 覆盖掉——我一般用一个_isDrawing标志位控制定时器。

帧率自适应。相机 60fps、UI 刷 30fps 是常态,但如果你发现拖动时卡顿,说明刷图频率和交互事件抢 UI 线程了。做法是:把刷图定时器间隔设成 33ms(约 30fps),拖动时临时降到 66ms,松手恢复。代码上就是timer.Interval动态改,简单有效。

场景刷图间隔说明
静态观察33ms30fps,够用且省 CPU
拖动/缩放中66ms降帧保交互流畅
高速检测16ms60fps,需要确认 UI 扛得住

还有一个容易忽略的点:HSmartWindowControl的KeepAspectRatio属性。默认是 false,图像会被拉伸填满控件,圆形变椭圆。做测量类应用一定要设成 true,否则你的像素当量换算全是错的。设了之后图像会留黑边,但比例正确。

最后说个血泪经验:Halcon 的HObject和相机 SDK 的 buffer 生命周期是两回事。相机回调里的pData在回调返回后可能就被 SDK 回收了,所以Marshal.Copy必须在回调内完成,不能把指针存下来异步处理。我见过有人为了「优化性能」把指针丢到队列里慢慢转,结果图像随机花屏,查了两天才定位到。从那以后我每次写回调,第一件事就是确认数据已经拷进托管内存,再谈别的优化。希望帮到你。

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

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

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

立即咨询