很多刚开始接触机器视觉的朋友,都卡在同一个地方:HALCON里调算子调得飞起,一到要和C#上位机结合就懵了。我在带新人的时候,见过太多人把几十个HALCON算子写进一个按钮的Click事件里,换张图就崩,开相机就卡,改个参数要重新编译。后来我总结出一套适合初学者的C#与HALCON联合开发思路,核心就是把采集、算法、显示、业务拆开,让每个模块各干各的,再由C#统一调度。这套通用视觉框架我已经在好几个项目里用过,也带着零基础的新人跑通过,今天把完整的构建过程、关键代码和踩坑经验都写出来。
写这篇文章,是想帮那些准备入行机器视觉、或者正在做视觉项目但还没形成体系的人,把C#上位机开发和HALCON视觉算法之间的那层窗户纸捅破。看完你不仅能跑通一个完整的图像采集、ROI绘制、测量显示的工程,还能明白为什么很多老手强调循环采集必须用异步,以及怎么避免UI刷新卡顿这类初学者必经的坑。
1. C#和HALCON的分工逻辑:为什么这套组合对初学者最友好
先说一个反直觉的结论:做机器视觉项目,初学者不要纠结哪个工具更强大,而要纠结哪个组合能让你在最短时间内把流程跑通。C#加HALCON恰恰就是这样一个组合。
1.1 C#负责什么
C#在上位机领域几乎是事实标准。它有成熟的WinForms/WPF界面体系,有完善的跨线程机制,有.NET生态里一堆现成的库可以做JSON配置、串口通信、网络通信、数据库存储。视觉项目最终要落地,一定跑不开界面展示、数据记录、与PLC或MES通讯这些活儿,这些用C#做非常顺手。
更重要的是,C#对新手极其友好。语法清晰,IDE提示完善,遇到问题一搜一大片解决方案。哪怕你是刚学会封装和继承的小白,也能用它写出一套能维护的项目结构。
1.2 HALCON负责什么
HALCON最厉害的地方是算子库极其丰富。模板匹配、边缘提取、Blob分析、测量、标定、深度学习,基本你能想到的视觉算法,它都有现成算子。官方还带大量示例程序和示例图片,HDevelop里F1按键一按,帮助文档详尽到minimal、industrial案例都有。
另一个关键点是,HALCON对工业相机生态支持得极好。不管是GigE Vision、USB3 Vision还是Camera Link,通过Framegrabber接口基本都能连得上。这意味着你不需要去研究海康相机SDK、Basler相机SDK各自的调用方式,一个统一的采集接口就把事情办了。
1.3 这套组合的核心边界
我的经验是:算法判断全部交给HALCON,业务流程和界面交互全部留给C#。图像预处理、找ROI、测宽度、判断OK还是NG,这些用HALCON算子或者HDevelop脚本解决;而相机开关、触发时机、结果展示、数据存到哪、什么时候报警,这些用C#控制。
这样分工之后,你会发现开发效率提升非常明显。调试算法在HDevelop里改参数几秒钟就能看效果,确认之后再把流程映射到C#代码里;而涉及界面和业务的部分,C#的强项又能完全发挥出来。
| 方案 | 优点 | 缺点 | 适合人群 |
|---|---|---|---|
| C# + OpenCV | 免费,学习资料多,算法透明 | 工业算法从零搭太慢,相机适配麻烦 | 想深入算法内核的人 |
| C++ + HALCON | 性能极限,适合极度苛刻的实时场景 | C++上手慢,界面开发效率低 | 已有C++功底、产品固化的团队 |
| C# + HALCON | 开发速度快,算子丰富,界面成熟 | HALCON授权有成本 | 初学者、中小项目、快速交付场景 |
我第一次带新人用这套组合,从搭建环境到跑通一个“读图—画ROI—阈值分割—显示结果”的小软件,只花了两天。这个速度在C++加OpenCV的路线下,对零基础的人几乎不可能。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 搭建开发环境与第一个显示工程:版本搭配和引用的坑
这部分是全流程里最枯燥但最关键的。环境配不好,后面都是空中楼阁。我见过有同事因为DLL版本不对,卡了整整半天查“无法加载DLL”的问题。
2.1 HALCON的安装与授权
先去MVTec官网下载HALCON试用版,目前主流版本是HALCON 23.05、24.05等。安装时注意两点:一是选择64位,因为后续C#工程基本都要跑x64;二是安装路径最好保持默认的C:\Program Files\MVTec\HALCON-版本号,后面找DLL方便。
安装完成后,试用license会有一段有效期,如果用的是有狗(硬件加密狗)的正式授权,记得把license文件放到%HALCONROOT%\license目录。很多新人装完HALCON后HDevelop能打开,但程序一跑就提示license问题,基本就是license文件缺失或者环境变量没生效。
HALCON安装完成后会在系统环境变量中创建几个关键变量:
HALCONROOT:指向安装根目录HALCONARCH:当前架构,一般是x64-win64HALCONIMAGES:示例图片路径
这三个变量很重要,因为后续HDevelop中很多示例程序直接读HALCONIMAGES路径下的图片,C#工程运行时也需要通过它们定位原生DLL。
2.2 Visual Studio工程的创建与DLL引用
打开Visual Studio,新建一个WinForms项目(.NET Framework 4.6.2+ 或 .NET 6/8都行)。先别急着写代码,把项目的目标平台设为x64:右键项目—属性—生成—平台目标—选x64。
然后添加HALCON的.NET接口DLL引用。在解决方案资源管理器里右键引用—添加引用—浏览,找到halcondotnet.dll,它的位置通常是:
text复制C:\Program Files\MVTec\HALCON-23.05\bin\x64-win64\dotnet4\halcondotnet.dll
注意不同版本路径可能略有差异,但规律都是bin\x64-win64\dotnet4。如果你调试时选了x86,就要去找bin\win32下的DLL,两者不能混用。
引用完成后,在窗体设计器里把工具箱中的HWindowControl拖到窗体上。如果你工具箱里没看到这个控件,右键工具箱—选择项—浏览—同样选中halcondotnet.dll,稍等几秒就能在工具箱里看到HWindowControl。这个控件就是HALCON图像显示的窗口容器。
2.3 第一个能显示图像的窗体
环境就绪后,我们来写第一行真正接触HALCON功能的代码——读取一张示例图片并显示在窗体上:
csharp复制using System;
using System.Windows.Forms;
using HalconDotNet;
namespace HalconFirstDemo
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
// 使用HALCON官方示例图片,路径相对于HALCONIMAGES环境变量
HImage image = new HImage("printer_chip/printer_chip_01");
hWindowControl1.HalconWindow.DispObj(image);
}
}
}
这段代码干了什么事?new HImage()从磁盘读取图片到内存,DispObj把HALCON图像对象显示到窗口上。跑起来之后你就能在窗体里看到一张PCB电路板的灰度图。
这里有个小细节:如果运行报“找不到图片文件”,多半是环境变量HALCONIMAGES没配好。可以在代码里写死绝对路径,比如new HImage("C:/Program Files/MVTec/HALCON-23.05/examples/images/printer_chip/printer_chip_01"),这样更直接。
还有一个所有人都可能踩的坑:Visual Studio调试时提示“无法加载DLL halcon.dll”或者“找不到指定模块”。别慌,这是因为你的工程找不到HALCON的原生DLL。解决方法是把bin\x64-win64下的halcon.dll、hdevengine.dll等几个核心DLL复制到你的bin\Debug或bin\Release输出目录,或者直接在这个目录下建一个文件夹后手动添加引用。最简单粗暴的做法是项目属性—生成事件—后期生成事件命令行里写一条copy命令,以后生成时自动拷贝所有HALCON运行库。新手阶段可以直接把bin\x64-win64下的DLL全部复制到程序输出目录。
3. 通用视觉框架的整体骨架:采集、算法、显示、交互四大模块怎么组织
如果你只是自己做个demo,代码怎么乱都无所谓。但要做成一个能反复使用的框架,就必须在动手写代码前想清楚模块划分。这一节我直接给你一套我已经用过的骨架设计。
3.1 先想想为什么要把模块拆开
很多初学者写视觉软件的逻辑是:拍照、处理、显示、保存全塞在按钮的Click事件里。这种写法在调通的瞬间很爽,但你很快会发现三个问题:第一,想换一个相机型号,得改那个按钮的代码;第二,想从“拍照一次”改成“连续拍照”,代码结构要推翻重来;第三,界面上的操作线程和采集线程混在一起,极容易卡死。
框架的目的就是把“会变的东西”和“不变的东西”分开。相机接口会变、算法流程会变、界面布局会变,但这些变化的维度是独立的。不管用哪个相机,对上层来说都是“打开、采集、关闭”;不管处理什么图片,对上层来说都是“输入一张图,输出一个结果”。
3.2 模块划分
我的做法是分四层:
| 层级 | 职责 | 关键设计 |
|---|---|---|
| 采集层 | 连接相机、抓取图像 | 抽象出ICamera接口 |
| 算法层 | ROI、预处理、测量、检测 | 调用HALCON算子或执行HDevelop脚本 |
| 显示与交互层 | 图像显示、ROI绘制、结果叠加 | HWindowControl封装 |
| 应用层 | 界面、配置、日志、通信 | WinForms/WPF、JSON、NLog |
采集层我建议先定义接口,不直接依赖HALCON:
csharp复制public interface ICamera
{
bool Open();
bool Close();
bool Grab(out HObject image);
bool IsOpen { get; }
string CameraName { get; }
}
然后用一个实现类去封装HALCON的采集接口:
csharp复制public class HalconGigECamera : ICamera
{
private HTuple _acqHandle;
public bool IsOpen => _acqHandle != null && _acqHandle.Length > 0;
public bool Open()
{
try
{
// 参数顺序请严格对照HALCON文档说明
HOperatorSet.OpenFramegrabber(
"GigEVision2", 0, 0, 0, 0, 0, 0, "default",
-1, "default", -1, "false", "default", "", 0, -1,
out _acqHandle);
HOperatorSet.GrabImageStart(_acqHandle, -1);
return true;
}
catch (HALCONException ex)
{
Console.WriteLine($"打开相机失败: {ex.Message}");
return false;
}
}
public bool Grab(out HObject image)
{
image = null;
try
{
HOperatorSet.GrabImageAsync(out image, _acqHandle, -1);
return true;
}
catch
{
return false;
}
}
public bool Close()
{
if (_acqHandle != null)
{
HOperatorSet.CloseFramegrabber(_acqHandle);
_acqHandle = null;
}
return true;
}
}
这套接口设计的好处,就是将来如果某个项目非要换SDK,你只需要再写一个Camera类实现ICamera,上层界面代码半点不用动。我实际项目中从HALCON采集切到海康SDK采集,只改了一行new哪个实现类。
算法层我习惯直接封装HALCON算子,而不是把所有处理逻辑散落在窗体代码里。初学者很容易犯的错,是在窗体里直接写几十行HOperatorSet调用。更好的做法是建一个VisionProcessor类,把“输入HObject、输出结果结构体”定义清楚。
csharp复制public class VisionResult
{
public bool IsOK { get; set; }
public double WidthInPixels { get; set; }
public HObject ResultRegion { get; set; }
}
public class VisionProcessor
{
public VisionResult Process(HObject srcImage, VisionParams par)
{
// 这里按项目需要写具体的算法流程
return new VisionResult();
}
}
这里只给出骨架结构,下一步的核心函数拆解会对里面的算法实现做详细说明。
3.3 界面、配置与硬编码问题的解决
应用层是初学者最不重视的一层,但工程化恰恰靠它。要避免把曝光时间、阈值、ROI位置这些参数写死在代码里。我通常用一个JSON配置文件保存刚提到的参数,程序启动时读入,界面提供修改入口,改完保存,重启生效。这样换一张生产线上的不同产品,不用重新编译软件。
日志也是一个容易被忽略的模块。程序跑到客户现场,出问题了,你一眼看不到现场,这时一份日志就是你的眼睛。用NLog或者log4net都可以,无非是记录时间、操作、结果和异常信息。
4. 核心功能代码拆解:ROI绘制、结果显示、图像转换是怎么一步步实现的
有了骨架,我们往里面填肉。这一节我把一个实际的“测量零件宽度”流程完整走一遍,你会看到ROI绘制、图像预处理、阈值分割、结果显示这些操作在C#里到底怎么写。
4.1 读图与显示
先从本地读一张图。假设我们要检测一个金属零件上的两个特征区域之间的距离。第一步是把图像加载并显示:
csharp复制private HObject _srcImage;
private void btnLoadImage_Click(object sender, EventArgs e)
{
// 释放上一张图占用的内存
_srcImage?.Dispose();
_srcImage = new HImage("part/part_01");
hWindowControl1.HalconWindow.DispObj(_srcImage);
}
这段代码有两个细节很关键。第一,用_srcImage?.Dispose()释放上一张图。HALCON的对象在底层占用原生内存,虽然新版本有HSmartPointer自动管理,但显式Dispose是更稳妥的习惯,尤其是在长时间运行的工业软件里。第二,这里用HImage构造读取图片,但很多旧的示例代码用的是HOperatorSet.ReadImage(out HObject, ...),两者都行,后者更常见于老项目,我会在兼容性部分再提。
4.2 ROI绘制:交互式画矩形
视觉项目里,多数情况下不需要处理整张图,而是处理某个ROI区域。HALCON的交互算子能够让你在窗口上直接用鼠标画一个矩形,代码非常简单:
csharp复制private void btnDrawRoi_Click(object sender, EventArgs e)
{
if (_srcImage == null) return;
HWindow window = hWindowControl1.HalconWindow;
// 在窗口上交互式画一个旋转矩形,返回四个点的坐标
HTuple row1, col1, row2, col2;
window.SetColor("red");
window.SetDraw("margin");
HOperatorSet.DrawRectangle1(window, out row1, out col1, out row2, out col2);
// 生成矩形Region,用于后续裁切
HObject roiRegion;
HOperatorSet.GenRectangle1(out roiRegion, row1, col1, row2, col2);
// 重新显示原图,然后叠加ROI
window.ClearWindow();
window.DispObj(_srcImage);
window.SetColor("blue");
window.DispObj(roiRegion);
_roiRegion?.Dispose();
_roiRegion = roiRegion;
}
DrawRectangle1这个交互算子会阻塞当前线程,直到你松开鼠标才返回。所以不要在采集循环里调它,否则画面会卡住。它的设计意图就是供用户交互使用的,点一下按钮然后等用户画完,正好符合我们要的场景。
4.3 用ROI裁剪、高斯滤波、阈值分割
拿到ROI之后,我们用ReduceDomain把原图裁剪到ROI范围内,再做预处理和分割:
csharp复制private void btnProcess_Click(object sender, EventArgs e)
{
if (_srcImage == null || _roiRegion == null) return;
HWindow window = hWindowControl1.HalconWindow;
HObject reduced, filtered, region, connected, selected;
// 1. 裁剪出ROI区域内的图像
HOperatorSet.ReduceDomain(_srcImage, _roiRegion, out reduced);
// 2. 高斯滤波,去噪
HOperatorSet.GaussFilter(reduced, out filtered, 1.5);
// 3. 阈值分割
HOperatorSet.Threshold(filtered, out region, 85, 255);
// 4. 连通域分析
HOperatorSet.Connection(region, out connected);
// 5. 按面积筛选,去掉杂质
HOperatorSet.SelectShape(connected, out selected, "area", "and", 500, 999999);
// 显示结果
window.ClearWindow();
window.DispObj(_srcImage);
window.SetColor("green");
window.SetLineWidth(2);
window.DispObj(selected);
// 释放中间对象
reduced.Dispose();
filtered.Dispose();
region.Dispose();
connected.Dispose();
selected.Dispose();
}
这段代码对应HDevelop里最常见的处理流程:裁剪—滤波—阈值—连通域—筛选。你会注意到我在C#里写的东西和HDevelop里几乎是一一对应的,这就是HALCON和C#联合开发最舒服的地方。在HDevelop里先把流程调通,然后照搬到C#里加个out参数就行。
4.4 测量结果与文字叠加
找到目标区域之后,下一步通常是测量。比如找每个区域的最小外接矩形,然后算出两个矩形中心的距离:
csharp复制private void MeasureDistance(HObject selectedRegions)
{
HWindow window = hWindowControl1.HalconWindow;
// 取面积最大的两个区域
HObject selectedSorted;
HOperatorSet.SelectShape(selectedRegions, out selectedSorted, "area", "and", 500, 999999);
HOperatorSet.SortRegion(selectedSorted, out selectedSorted, "first_point", "true", "row");
// 获取每个区域的最小外接矩形中心坐标
HTuple rows, cols;
HOperatorSet.AreaCenter(selectedSorted, out _, out rows, out cols);
if (rows.Length >= 2)
{
double distance = 0;
HOperatorSet.DistancePp(rows[0].D, cols[0].D, rows[1].D, cols[1].D, out distance);
HOperatorSet.DispText(window, $"Distance = {distance:F2} px", "window", 20, 20, "green", "box", "false");
}
selectedSorted.Dispose();
}
DispText是常用的文字叠加算子,在窗口指定位置显示字符串。和SetTposition加WriteString的传统写法相比,DispText参数更紧凑,推荐使用。这里还把计算出来的像素距离直接显示在了画面左上角,让用户一眼能看到结果。
4.5 HObject和Bitmap互转
虽然HALCON窗口有自己的显示机制,但有时你需要把图像交给别的控件,比如PictureBox,或者传给OpenCV做进一步处理。这时候就涉及HObject和Bitmap的转换。
在较新的HALCON版本中,提供了扩展方法:
csharp复制using HalconDotNet;
using System.Drawing;
using System.Drawing.Imaging;
public Bitmap HObjectToBitmap(HObject image)
{
HImage hImage = new HImage(image);
return hImage.ToBitmap();
}
稍旧一些的版本也可以手动安全转换。核心思路是取到图像每个通道的像素指针,然后复制到Bitmap的BitmapData里。这块代码比较繁琐且容易出错,建议优先用ToBitmap扩展方法。
反过来,把Bitmap转成HObject,可以用BitmapToHImage之类的辅助类:
csharp复制public HImage BitmapToHObject(Bitmap bitmap)
{
Rectangle rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
BitmapData bitmapData = bitmap.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed);
HImage image = new HImage("byte", bitmap.Width, bitmap.Height, bitmapData.Scan0);
bitmap.UnlockBits(bitmapData);
return image;
}
这里要特别注意像素格式。灰度图对应“byte”,RGB图像对应“rgb”或“bgr”。格式对不上,出来的图会乱。
5. 循环采集与UI刷新卡顿:初学者的头号拦路虎
所有做过视觉项目的人,几乎都经历过这个场景:相机打开,连续采集,界面一开始正常,几秒钟后画面开始卡顿,再过一会整个窗体无响应,任务管理器里CPU飙高。这个问题在热搜词里反复出现,今天彻底把它讲透。
5.1 卡顿现象与根因
初学者最常见的写法是这样的:
csharp复制private void btnLoop_Click(object sender, EventArgs e)
{
while (true) // 千万别这么写
{
HObject img;
_camera.Grab(out img);
hWindowControl1.HalconWindow.DispObj(img);
img.Dispose();
}
}
这段代码有两个致命问题。第一,while(true)直接跑在UI线程上,界面消息循环被阻塞,窗体当然会卡死。第二,即便你放到Task里跑,每帧都从相机同步抓取,再同步显示,整个链路没有任何缓冲区,一旦处理时间超过帧间隔,就会掉帧、丢帧。
根因归纳起来有三点:
- UI线程被采集和处理操作阻塞
- 同步抓取要求相机等待处理完成才能采下一帧
- 跨线程访问HWindowControl控件没有正确处理
5.2 正确姿势:异步采集加后台线程
解决方案分成两步。第一步,用GrabImageAsync替代同步的GrabImage。GrabImageStart和GrabImageAsync配合,可以让相机在曝光、传输图像的同时,CPU并行处理上一帧数据,吞吐量会有质的提升。
第二步,把采集循环放到后台线程,通过BeginInvoke把图像封送到UI线程去显示。下面是一个可以实际运行的采集循环:
csharp复制private CancellationTokenSource _cts;
private HTuple _acqHandle;
private void btnStartGrab_Click(object sender, EventArgs e)
{
_cts = new CancellationTokenSource();
Task.Run(() => GrabLoop(_cts.Token));
}
private void GrabLoop(CancellationToken token)
{
try
{
HOperatorSet.OpenFramegrabber(
"GigEVision2", 0, 0, 0, 0, 0, 0, "default",
-1, "default", -1, "false", "default", _cameraName, 0, -1,
out _acqHandle);
HOperatorSet.GrabImageStart(_acqHandle, -1);
while (!token.IsCancellationRequested)
{
HObject img = null;
HOperatorSet.GrabImageAsync(out img, _acqHandle, -1);
// 封送到UI线程显示
HObject captured = img;
this.BeginInvoke(new Action(() =>
{
hWindowControl1.HalconWindow.DispObj(captured);
captured.Dispose();
}));
}
HOperatorSet.CloseFramegrabber(_acqHandle);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void btnStopGrab_Click(object sender, EventArgs e)
{
_cts?.Cancel();
}
个人实测下来,同样的相机,同样的处理流程,只把同步抓取改成异步抓取,帧率就能提升差不多30%到50%。BeginInvoke保证了UI刷新不会阻塞后台线程,界面始终保持流畅。
5.3 内存管理:另一个隐性卡顿源头
有些时候UI不卡,但内存一路飙升,最后程序爆掉。这大概率是HObject没有及时释放。
老版本HALCON里,HObject有原生句柄,必须手动Dispose。而在HALCON 20.11之后,官方推荐了HSmartPointer机制,HImage、HRegion这些智能指针类型会在引用计数归零时自动释放,写起来省心很多。
但我要说的是兼容性经验:如果你在用HOperatorSet的out HObject写法,请务必保证每个分支都会Dispose,否则调试时很难察觉。养成三个习惯:
- 图像用完立即Dispose,不要等垃圾回收
- 中间变量在finally或using块里释放
- 对采集显示这种高频操作,优先用HImage等智能指针类型,降低内存管理负担
6. 从跑通到落地:参数配置、异常捕获和工程习惯
很多人的框架卡在“能跑”和“能交付”之间。这一节讲的不是高大上的架构理论,而是让程序在客户现场稳定运行、出了故障能快速定位的工程习惯。
6.1 参数全部从配置文件读取
我在第3节已经埋了伏笔。曝光时间、增益、ROI坐标、阈值上下限、产品型号这些,绝对不能写死在代码里。用JSON配置文件管理是最快最省事的方案:
json复制{
"CameraName": "CAM-001",
"ExposureTime": 5000,
"Gain": 0,
"ThresholdMin": 85,
"ThresholdMax": 255,
"MinArea": 500,
"CheckDistanceMm": 12.5
}
C#端用System.Text.Json直接反序列化成强类型配置类:
csharp复制public class VisionConfig
{
public string CameraName { get; set; }
public double ExposureTime { get; set; }
public double Gain { get; set; }
public int ThresholdMin { get; set; }
public int ThresholdMax { get; set; }
public int MinArea { get; set; }
public double CheckDistanceMm { get; set; }
}
程序启动时读配置文件,界面修改参数时写回文件。这样C#代码完全不需要因为产品换型而改动。
6.2 全局异常捕获和HALCON异常处理
工业软件最怕的就是莫名崩溃。用户看到的不是异常信息,而是“程序没了”。所以在Main入口加上全局异常捕获是底线:
csharp复制[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
File.AppendAllText("error.log", DateTime.Now + " " + e.ExceptionObject + Environment.NewLine);
Application.ThreadException += (s, e) =>
File.AppendAllText("error.log", DateTime.Now + " " + e.Exception + Environment.NewLine);
Application.Run(new MainForm());
}
HALCON抛出的异常是HALCONException,它一般继承了HOperatorException,错误码对应HALCON的错误代码。在调用OpenFramegrabber或图像处理算子的地方,用try-catch把HALCONException单独截住,日志里能直接看到错误序号和描述,省去现场瞎猜。
6.3 版本升级与老项目改造建议
如果你手头已经有一个老项目,用的是HObject加手动Dispose的写法,不要急着全盘重写。渐进式改造路径是:先把HALCON相关代码全部收敛到一个VisionProcessor类里,外部界面只和这个类交互;然后逐步把HObject换成HImage等智能指针类型;最后引入配置文件和日志系统。这样每一步改动都是小且可验证的,不会突然把整个项目搞崩。
我现在带人的流程也固定下来了:先在HDevelop里用官方示例把算法验证一遍,把关键算子参数记录下来;然后在C#里搭出界面壳子,把相机采集跑通;接着按照本文第3节的框架把模块拆好;最后把算法一个一个映射到C#里。整个过程稳扎稳打,几乎没有一上来就写错方向的情况。
关于分辨率、标定和实际测量单位,我再补充一个很容易被忽略的提醒。你在图像里测出来的距离单位是像素,要换算成毫米,离不开标定。最简单的方式是用一块已知尺寸的标准件,拍一张图,算出像素当量(毫米除以像素数)。但要注意,镜头畸变明显时,画面边缘的像素当量和中心差很多,简单换算只适合取近似值,真正精密的项目必须做HALCON的标定流程。
我自己在办公室里带着新人从零搭这套通用视觉框架,从安装HALCON、写第一个显示程序,到把所有模块跑通,平均三到四天就能让一个之前完全没接触过HALCON的人交出一个类似成品的小工具。这套框架不追求极致性能,但胜在结构清楚、容错率高、扩展方便,希望它也能帮你迈过C#和HALCON之间那道坎。
