
做机器视觉项目的人都有过这种体会流程控制、界面交互、运动控制交给LabVIEW图像算法交给HALCON两者都是各自领域里很能打的工具但搁在一块儿总有一道绕不过去的墙——LabVIEW没法直接执行HALCON算子HALCON的HDevelop脚本又不能被LabVIEW工程直接引用。所以就有了这个非常经典的组合需求LabVIEW调用HALCON中间再包一层DLL。这个标题“LabVIEW调用HALCON与DLL显示读取图片源码”说白了就是一个LabVIEW、HALCON、DLL三方协作的最小可行工程LabVIEW负责界面和流程HALCON负责图片读取与预处理DLL把HALCON的能力封装成LabVIEW能调的接口。我做这类图像项目时经常是先把这个最小链路跑通再往里面塞模板匹配、测量、缺陷检测这类算法。这篇文章就围绕这个完整链路展开适合刚接触视觉集成的工程师也适合LabVIEW和HALCON两边都熟但没怎么连过的老手。1. 为什么LabVIEW非要通过DLL去调HALCON1.1 这套组合解决的核心痛点先说实际的痛点。一个产线上的视觉工站通常分三块界面和流程、图像采集与算法、运动与通信。LabVIEW的优势在前两块之外的所有地方尤其做状态机流程、PLC通信、数据库记录和操作界面效率非常高。但机器视觉算法这一侧如果要做到高精度边缘提取、亚像素测量、复杂模板匹配HALCON的算子库成熟度比LabVIEW自带的视觉模块要更深入尤其在工业界被验证得比较充分。问题在于HALCON的推理和开发环境是HDevelop脚本语言是类似C的算子流程别人交付给你的可能是.hdvp脚本也可能是导出的C工程。LabVIEW想要消费这两样东西最直接的办法就是把它做成DLL。DLL是Windows平台上几乎所有语言都能调用的标准动态库形态LabVIEW里的Call Library Function Node中文叫“调用库函数节点”专门干这个事。1.2 项目目标边界读图、显示和后续算法挂载我把这个项目的目标收敛得很明确第一能从文件路径读取一张图片第二能在LabVIEW界面上显示出来第三给HALCON算法留一个能扩展的挂载点。它不是一个完整的视觉项目但它是一切视觉项目的地基。一开始我也有过走偏的想法既然LabVIEW也有视觉开发模块VDM那为什么不直接用VDM的IMAQ Read Image和IMAQ WindDraw完事。但真做起来会发现你手里的算法资产往往是HDevelop里调好的算子序列比如halcon测量、halcon高斯差分、halcon模板匹配这些VDM没有直接对应的函数。与其在LabVIEW里重新实现一套不如把HALCON的核心能力通过DLL暴露出来让LabVIEW的主程序变成一个“壳”算法逻辑全部留在DLL里。这样视觉工程师和上位机工程师的分工边界也清楚HDevelop里改算法不牵连界面。2. 环境准备位数、路径、license这三个坑先填平2.1 LabVIEW和HALCON版本位数的匹配关系这个坑排在第一位因为一旦踩了后面所有步骤全废。LabVIEW现在同时提供32位和64位版本HALCON也同样区分x86-win32和x64-win64的运行库。你的LabVIEW如果装的是32位那就必须加载32位的HALCON DLL装64位就必须加载64位的。两者交叉加载的结果就是一张红叉DLL load失败。我之前接过一个外包项目对方电脑上LabVIEW是32位因为早期装了很多32位仪器驱动HALCON却装的是64位。结果HDevelop里算法跑得飞快一接到LabVIEW就报“找不到指定的模块”。排查到最后才发现是位数不匹配白折腾了两天。建议的做法是先想清楚整个产线最终交付是32位还是64位。如果涉及到老款仪器厂商驱动老老实实装32位LabVIEW和32位HALCON如果纯视觉项目直接上64位性能更好内存也更大。2.2 安装顺序与运行时环境变量配置安装顺序方面我推荐的流程是先把HALCON装好再装LabVIEW最后装Visual Studio如果要自己编译DLL的话。HALCON安装完成后安装程序会自动设置HALCONROOT环境变量并把bin\x64-win64路径写入系统PATH。这个PATH直接影响后面C编译时能否找到halcon.dll也影响LabVIEW加载DLL时能否连带找到HALCON的运行时库。如果安装顺序反了或者中途改过HALCON安装路径大概率会出现下面这种经典情况HDevelop正常但VS编译时提示LNK1104: cannot open file halcon.lib或者LabVIEW加载DLL时提示“DLL中找不到指定过程”。解决办法也不复杂手动设置环境变量即可HALCONROOT D:\Program Files\MVTec\HALCON-23.05 PATH 追加 %HALCONROOT%\bin\x64-win64需要注意HALCON的DLL依赖并不是只有halcon.dll一个还可能有halconcpp.dll、hdevengine.dll这些。其中halconcpp.dll是C接口库你用C封装DLL时必须要它。所以PATH里HALCON的bin目录必须保留千万别因为“为了安全”把它删掉。2.3 license配置的合规思路license这一块HALCON不是免费库它的机器码绑定方式也比较严格。常见的授权机制是安装后会识别当前电脑的硬件指纹需要导入对应的license文件。窗口里“License Manager”会显示当前可用的算子级别和到期时间。我的建议非常明确走官方正式通道申请试用授权或采购正式授权。视觉软件不是用完就扔的东西产线上一旦跑了三年五载中途license出问题、版本升级困难损失远远不是省下的授权费能比的。如果只是个人学习MVTec官网和相关代理一般都有评估版申请入口。评估版限制主要是时间但算子功能基本完整足够你把读图、显示、模板匹配、测量这一整套链路跑通。等接到商业项目再补正式授权即可。2.4 VC运行库缺失导致的“找不到指定的模块”另一个很容易被忽视的是VC运行库。HALCON新版DLL编译时依赖Visual C Runtime如果你的电脑没有安装对应的运行库LabVIEW加载DLL时会报Error 7: The specified module could not be found.或者是展开后有一串英文LoadLibrary(xxx.dll) failed。注意它说的是“指定的模块”但很多时候问题不在你的DLL而在于你的DLL依赖的运行库没装。所以拿到一台新电脑先把Visual C 2015-2022 x64运行库装上一般就能解决。3. 从HDevelop到DLL导出流程与接口设计3.1 为什么方案选择导出C/C DLLHDevelop导出代码时可以选导出C、C或者C#。我选择C理由是HALCON的C接口HalconCpp对象封装更现代HImage、HOperatorSet这些类管理内存方便不容易出现C接口里到处是HTuple*裸指针的情况。而且C项目里可以同时链接halcon.lib和halconcpp.lib封装的代码也更整洁。LabVIEW不关心你的DLL是用C写的还是C写的它只认导出函数的调用约定和参数类型。所以选C没有任何兼容性障碍。3.2 HDevelop脚本的导出步骤在HDevelop里先把想要做的图像流程写成算子序列。比如最简单的读图read_image (Image, C:/test/board.png) get_image_size (Image, Width, Height)调通之后选择文件 - 导出导出语言选C。HDevelop会生成一个包含算子调用序列的.cpp文件。这个文件不能直接作为DLL用它只是一个“函数体片段”你需要把它包进一个真正的DLL工程里。更常用的做法是把HDevelop里调通的代码当作参考直接在Visual Studio的DLL工程里调用HALCON C算子。原因很简单HDevelop导出代码带着很多内部变量和临时变量放到DLL里还要做一遍接口手术不如自己写一层干净的导出函数来得快。3.3 在Visual Studio里封装出可供LabVIEW调用的导出函数创建一个VS动态链接库工程配置附加包含目录和附加库目录把%HALCONROOT%\include和%HALCONROOT%\lib\x64-win64加进去然后在附加依赖项里写上halconcpp.lib和halcon.lib。核心导出代码长这样#include HalconCpp.h using namespace HalconCpp; extern C __declspec(dllexport) int QueryImageInfo(const char* filePath, int* width, int* height, int* channels) { try { HImage image; image.ReadImage(filePath); Hlong w 0, h 0; image.GetImageSize(w, h); *width (int)w; *height (int)h; *channels (int)image.CountChannels(); return 0; } catch (HException ex) { return ex.Code(); } } extern C __declspec(dllexport) int CopyImageToBuffer(const char* filePath, unsigned char* buffer, int width, int height, int channels) { try { HImage image; image.ReadImage(filePath); if (channels 1) { HImage byteImage image.ConvertImageType(byte); unsigned char* ptr byteImage.GetImagePointer1(nullptr); memcpy(buffer, ptr, (size_t)width * height); } else { HImage interleaved; image.InterleaveChannels(interleaved, rgb, match, 0); HImage byteRgb interleaved.ConvertImageType(byte); unsigned char* ptr byteRgb.GetImagePointer1(nullptr); memcpy(buffer, ptr, (size_t)width * height * 3); } return 0; } catch (HException ex) { return ex.Code(); } }注意extern C和__declspec(dllexport)这两个关键字缺一不可。前者保证导出符号在编译后不被C的name mangling改名否则LabVIEW会找不到你写的函数名后者把函数暴露到DLL导出表里。3.4 接口设计让LabVIEW能直接认出的参数形态很多LabVIEW工程师第一次写DLL接口时容易把参数设计成C里的std::string、std::vector或者自定义结构体指针。这在C里很方便但LabVIEW的Call Library Function Node不认识STL对象。所以我在设计接口时遵守一条原则所有跨DLL边界的参数尽量是C基础类型、C字符串指针、固定大小数组指针。const char*对应LabVIEW的字符串指针int*对应数值指针unsigned char*对应U8一维数组指针。上面代码里的CopyImageToBuffer为什么要把width、height、channels作为参数传进来因为这个buffer是LabVIEW侧预先分配好的DLL侧只往里拷数据。DLL侧如果自己去分配内存就得再提供一个释放函数麻烦又容易泄漏。用“LabVIEW分配、DLL填写”的方式内存所有权清晰谁也赖不掉谁。4. LabVIEW侧读取并显示图片的具体实现4.1 Call Library Function Node配置清单在LabVIEW的程序框图上放一个“调用库函数节点”双击配置。关键项如下配置项推荐值说明库路径你的DLL完整路径不要只写文件名用绝对路径函数名QueryImageInfo / CopyImageToBuffer下拉框中能看到调用约定C调用约定和C里默认一致返回类型有符号32位整数对应DLL返回的错误码参数1字符串路径字符串指针细节选“按值传递字符串”吗答案选字符串句柄或指针参考LV帮助参数2宽指针有符号32位整数指针用于带回结果参数3高指针有符号32位整数指针同上缓冲数组参数U8数组指针预先创建数组并传入需要特别说明一点LabVIEW里数组默认是按参数值传递的但Call Library Function Node里有一个选项可以提供“数组数据指针”。配置参数类型时把“类型”设为“数组”“数组格式”选“一维数组”“数组数据类型”选“U8”再勾上“数组指针”。这样LabVIEW会把数组的实际数据内存首地址传给DLLDLL的直接memcpy就是写入这块内存。必须保证数组长度足够这就是先调QueryImageInfo的原因。4.2 实现“读取图片→数组回传”的完整调用流程整个VI的程序框图逻辑分三段输入图片路径调用QueryImageInfo得到width、height、channels。根据宽高和通道数预先创建一维U8数组。灰度图大小是width * height彩色图是width * height * 3。再次调用CopyImageToBuffer把图片数据填进数组。这段流程不能缺的细节是LabVIEW里创建U8数组用“初始化数组”节点。指定数组大小为width * height * channels初始值0。然后把这个数组接到Call Library Function Node的数组参数上。调用完之后数组里就是图像原始数据。我早期的版本没有先查询尺寸而是直接在VI里用一次DLL调用同时返回尺寸和数据。那样做也没问题但要么用变体要么用动态分配LabVIEW侧的配置复杂度会成倍上升。用“两次调用”的笨办法反而最稳。4.3 图像显示从数组到IMAQ显示控件的几种做法拿到图像数组之后显示方案有三条路。方案一纯LabVIEW图片控件快速预览。用“从二进制数据绘制像素图”或者“绘制平面化像素图”函数把数组和一个伪彩色调色板传入图片控件。好处是零额外依赖适合只想快速看到图的人。缺点是性能一般缩放交互不够专业。方案二转成IMAQ图像。在LabVIEW里调IMAQ Create创建一个临时图像然后IMAQ ArrayToImage把灰度数组填进去用IMAQ WindDraw显示。这套方案的显示速度、缩放、ROI工具都齐全适合正式项目。前提是装了视觉开发模块Vision Development Module。方案三DLL直接落盘LabVIEW读图显示。这是我最喜欢用来做原型验证的方式。在DLL里加一个导出函数extern C __declspec(dllexport) int SaveImageAsBmp(const char* srcPath, const char* dstBmpPath) { try { HImage image; image.ReadImage(srcPath); image.WriteImage(bmp, 0, dstBmpPath); return 0; } catch (HException ex) { return ex.Code(); } }LabVIEW调用后直接用“读取图片文件”函数加载dstBmpPath显示。这个方法等于把所有的通道顺序、格式转换问题都丢给HALCON和BMP规范去处理快速出预览图特别省事。但要注意写临时文件有IO耗时不适用于高帧率实时显示。实时场合还是走方案二IMAQ更合适。5. 跑起来之后真正需要盯住的坑5.1 DLL加载失败从“独立加载测法”查起项目跑不起来时绝大多数情况是DLL加载失败。这时候不要直接在界面和逻辑上瞎猜我总结了一个定位方法叫“独立加载测法”。第一步先写一个极简VI里面只有一个Call Library Function Node调用DLL里的一个无参数函数。比如我在DLL里放了个GetDllVersion函数返回整数。这个VI不干别的就跑这个调用。如果这里就报错问题确定在DLL本身或环境如果这里能过再去纠结参数和显示。第二步如果最小VI报错优先检查位数。看任务管理器里当前运行的是32位还是64位LabVIEW再用VS看DLL工程的目标平台两者不一致就是罪魁祸首。第三步检查依赖。你的DLL依赖halcon.dllhalconcpp.dll。用dumpbin /dependents yourdll.dll看一眼依赖列表再看这些依赖文件是否在PATH里能找到。我见过有人把DLL拷到项目目录但halcon.dll还在HALCON安装目录里项目里的DLL依赖的halcon跑丢了那不报错才怪。有一点心得网上有些“dll修复工具”它主要扫的是系统公共DLL和VC运行库对自研DLL的依赖问题基本无用。这种工具叫作“系统DLL修复”更合适你自己的DLL加载失败还是按上面三步走最靠谱。5.2 图片读出来是黑屏、花屏或颜色错乱DLL能加载、程序能跑但显示出来的图片不对这种问题往往比加载失败更让人头疼。黑屏最常见的原因是图像深度不对。HALCON读一张16位灰度图GetImagePointer1取出来的指针内存布局是uint2每个像素占2字节。如果LabVIEW侧把数组当成U8来解析相当于每两个字节被拆成两个像素整个画面要么全黑要么全是噪声。解决办法是在DLL里显式ConvertImageType(byte)先把图像转成U8再拷贝。花屏多数是行宽问题。图像数组不是单纯的一长串像素每一个像素行还会带所谓的“步长”。HALCON内部图像为了性能行宽可能做了对齐而LabVIEW显示时往往按整数宽度连续处理。如果原图宽度不是对齐的标准值就会出现斜线、撕裂感。方案是DLL导出前自己做好规划用CropPart或ChangeFormat把图像标准化后再返回保证数组长度严格等于width*height*channels。颜色错乱一般是RGB和BGR顺序互相换。Windows下很多图像格式默认BGR而HALCON处理时用的是RGB。用InterleaveChannels(..., rgb, ...)输出的交织数据是RGB但如果最终用来显示的函数是BMP风格的读图就会出现红蓝互换。我最后选择临时落盘BMP的显示方案一部分原因就是BMP规范里明明白白定义了像素顺序HALCON写入时自己会处理好不需要我在LV侧再纠色。5.3 多线程与内存句柄问题LabVIEW的Call Library Function Node里有“线程”选项我建议项目初期全部选“在UI线程中运行”。这个选择牺牲一点点并行度但能避免一个最常见的崩溃场景多线程同时调用HALCON函数时HALCON的上下文冲突导致随机崩溃。HALCON现在的版本支持多线程调用但前提是每个线程使用独立的资源不能让多个线程共享同一个HImage句柄或全局变量。LabVIEW里如果从多个While循环同时调用同一个DLL函数相当于并发调用。这时候如果DLL内部使用了全局对象就是一个定时炸弹。我的DLL封装原则是所有HALCON对象都是函数局部的进出函数不保留任何全局句柄。这样“在任意线程中运行”也不会出问题。但如果刚开始做还没把握先用“UI线程”跑通再逐步放开线程选项。内存方面还有一点Call Library Function Node的数组参数必须在调用前就分配合适大小。如果数组长度不够DLL的memcpy会写出边界形成内存踩踏当时不报错到程序别的地方突然崩。判断内存边界这种事C侧可以用_msize做一次防御性检查但最好的办法还是LabVIEW侧严格按宽高通道数分配。6. 工程组织与下一步扩展6.1 一个能稳定交付的目录结构项目到后期DLL、LabVIEW工程、HDevelop脚本、临时图片这些东西放一起容易乱。我推荐一个简单的目录结构VisualProject/ ├── LabVIEW/ │ ├── Main.vi │ └── SubVIs/ ├── HALCON/ │ ├── dev_test.hdev │ └── operators/ ├── DLL/ │ ├── halcon_bridge.vcxproj │ ├── x64/Release/halcon_bridge.dll │ └── dependency/ └── ImageData/ ├── test/ └── output/关键在于把DLL的编译输出目录和LabVIEW工程目录分开但最终部署时把DLL和HALCON运行时一起拷贝到dependency目录确保LabVIEW打包时不会漏掉依赖。LabVIEW里调用DLL时路径建议不要硬编码。用一个配置文件记录DLL绝对路径或者用当前VI所在目录拼接相对路径这样换电脑部署时少改一两个地方省事很多。6.2 从读图显示到测量、缺陷检测、深度学习这个最小工程跑通之后能往上挂的算法就多了。HALCON的测量算子可以配合卡尺工具做边缘对检测高斯差分DoG算子可以做斑点缺陷检测模板匹配方面基于形状的匹配做定位是最经典的做法。我自己在项目里就是这样迭代的先用这篇文章里的DLL框架跑通读图显示然后往DLL里追加新的导出函数每个函数对应一个算法环节比如MeasureEdge、MatchShape、DiffOfGauss。LabVIEW侧不需要改架构只要往同样的Call Library Function Node里换上对应的函数名和参数字段。这样每增加一个算法对于LabVIEW团队来说就是新增一个可调的模块。这里顺便说一句总是有人拿HALCON和OpenCV对比。两者并不是同一个赛道的东西。OpenCV免费开源、上手难度低社区资料丰富HALCON则是商业授权但算子经过工业场景大量验证亚像素精度和标定支持非常扎实尤其是3D视觉和深度学习这块集成度更高。如果公司有预算、项目精度要求高走HALCON是比较稳妥的选择。6.3 我自己的几个经验点写到这里说一下我反复踩了多次之后沉淀下来的几点建议。第一DLL函数返回值统一用错误码。0表示成功非0表示HALCON异常码。这样LabVIEW侧可以用一个错误簇统一处理不用根据函数不同去猜返回值含义。第二日志一定要做。DLL代码里在catch块里写日志记录异常算子的名称和错误码。读图这个环节还好等做到测量和匹配阶段一条日志能帮你省下大量现场调试时间。第三不要试图在一个DLL里塞进所有功能。DLL拆成两个或三个比如一个负责读图和显示一个负责测量算法一个负责通信。功能分层以后哪个DLL出问题就替换哪个不用整个推倒重来。第四HDevelop脚本和DLL的C代码保持同一个版本管理仓库。每次改算法都要能追溯到对应版本的HDevelop脚本否则半年后谁也说不清DLL里的逻辑是哪个版本。做LabVIEW调用HALCOM这类整合项目很多时候并不是算法本身难难的恰恰是这些工程边界上的细节。把这个读图显示的最小闭环吃透后面加任何算法都有了干净的地基再往后做的项目会越走越顺。