资讯详情

资讯详情

HDevelop仿真程序语法实战:机器视觉算法验证与参数调优指南

1. 为什么还要专门学一门机器视觉领域的“方言”刚接触机器视觉那会儿我以为学会一门通用编程语言就够了图像处理无非就是调库、传参、拿结果。直到第一次接手一个工业检测项目产线上要求对金属零件表面做缺陷识别节拍压到每件两百毫秒以内我用通用语言写的方案跑一遍要八百多毫秒怎么优化都下不来。后来一位做视觉的老工程师丢给我一段脚本让我在HDevelop里跑跑看同样的算法逻辑耗时直接砍到一百五十毫秒。那一刻我才意识到机器视觉这个领域有一套自己的“方言”也就是HDevelop的仿真程序语法它跟通用编程语言是两套思维。这篇内容就是把我这些年用HDevelop写仿真程序的经验系统梳理一遍。所谓仿真程序指的是在HDevelop环境里用它的脚本语法把视觉算法流程先跑通、调好参数、验证效果再决定要不要导出成其他语言去集成。它解决的核心问题是让算法验证和参数调试变得极快不用每次都编译整个工程。适合谁看如果你是刚入行的视觉工程师或者是从通用软件开发转过来做工业视觉的又或者你已经在用HDevelop但只会拖控件、不敢写脚本那这篇内容应该能帮你少走不少弯路。我下面会从整体设计思路讲起然后拆解核心语法细节再给一套完整的实操流程最后把我踩过的坑整理成排查表。全程用从业者之间交流的口吻不堆术语能直接抄的代码我都放出来。2. 仿真程序整体设计与思路拆解2.1 仿真程序到底在仿真什么很多人第一次听到“仿真程序”这个词会懵以为是模拟硬件运行。其实在HDevelop的语境里仿真指的是在不依赖实际相机和产线触发的前提下用本地图像或图像序列把整套视觉算法流程完整跑一遍。它的本质是一个算法验证环境而不是硬件模拟器。为什么需要这样一个环境因为工业视觉项目的开发节奏跟互联网软件完全不同。互联网软件可以灰度发布、可以热更新出了问题回滚就行。但工业视觉算法一旦上线面对的是实实在在的产线误判一个良品可能造成整批产品报废漏判一个缺陷可能让客户产线停摆。所以算法在上线前必须经过大量离线验证而仿真程序就是这个验证过程的主要载体。我通常把仿真程序的工作分成三个阶段。第一阶段是流程搭建用HDevelop的脚本语法把图像采集、预处理、特征提取、判定输出这几个环节串起来。第二阶段是参数寻优针对不同光照条件、不同批次工件反复调整阈值、滤波核大小、形态学结构元素这些参数找到鲁棒性最好的组合。第三阶段是性能评估统计处理耗时、内存占用、误判率漏判率判断这套算法能不能满足产线节拍要求。这三个阶段里仿真程序语法的作用贯穿始终。它不像通用编程语言那样需要严格的类型声明和编译过程而是更接近脚本语言的风格写一行执行一行改一个参数立刻能看到结果。这种即时反馈的特性是它最大的价值。2.2 为什么不用通用语言直接写有人会问我直接用通用语言写视觉算法不行吗当然行但效率差很多。我做过对比同一个缺陷检测算法用通用语言从零写包括图像读取、灰度转换、滤波、阈值分割、连通域分析、特征筛选大概需要两百多行代码调试过程中每次改参数都要重新编译运行一轮下来少说十几秒。而在HDevelop里同样的流程用仿真语法写核心代码不到五十行改完参数按一下运行键结果瞬间刷新。更关键的是HDevelop的算子库是专门为机器视觉优化的。比如形态学操作、亚像素边缘提取、模板匹配这些通用语言里要么没有现成的要么性能差一大截。仿真程序语法直接调用这些底层优化过的算子省去了自己造轮子的时间。还有一个容易被忽略的点仿真程序天然适合做算法原型和最终部署之间的桥梁。在HDevelop里调好的流程可以导出成通用语言代码也可以导出成其他形式的接口。这意味着你可以在仿真环境里把算法逻辑和参数全部确定下来再交给集成工程师去对接产线两边职责清晰不容易扯皮。2.3 仿真程序语法的核心设计哲学HDevelop仿真程序语法的设计哲学我总结成三条。第一条是数据流驱动。整个程序不是按行顺序执行的命令列表而是一张有向无环图。每个算子是一个节点输入输出是边。你写代码的时候变量之间的依赖关系决定了执行顺序而不是你写的先后顺序。这个特性在调试时特别有用你可以单独选中某个中间变量直接看它的图像或数值不用从头跑一遍。第二条是元组为中心。HDevelop里的变量类型很少最核心的就是元组和图像。元组可以装数字、字符串、甚至图像句柄这种灵活性让写代码变得很轻快。比如你要对一组图像做同样的处理不需要写循环直接把图像元组传给算子它会自动逐元素处理。这个特性在批量验证时非常省事。第三条是即时可视化。每个算子执行完结果变量会自动出现在变量窗口里双击就能看图像、看直方图、看轮廓。这种所见即所得的方式让调参从苦差事变成了半自动化的过程。我经常一边拖动滑块改阈值一边盯着图像窗口看分割效果几秒钟就能找到合适的值。理解了这三条再看具体的语法细节就不会觉得零散了。3. 核心语法细节与实操要点3.1 变量、元组与图像的基本操作HDevelop里声明变量不需要指定类型直接赋值就行。比如Width : 640 Height : 480 GrayValue : 128这三行分别创建了一个整数、一个整数、一个整数。看起来跟其他脚本语言差不多但HDevelop的变量有一个特殊之处同一个变量名可以先后持有不同类型的数据。比如你先让它装一个数字再让它装一张图像语法上完全合法。但我不建议这么干因为可读性会变差后面维护的人容易懵。元组是HDevelop里最常用的数据结构。创建元组用方括号Numbers : [1, 2, 3, 4, 5] Names : [front, back, left, right] Mixed : [1, hello, 3.14]元组的索引从0开始跟大多数编程语言一致。取单个元素用Tuple[index]取子元组用Tuple[start:end]。这里有个细节要注意HDevelop的元组索引支持负数Tuple[-1]表示最后一个元素Tuple[-2]表示倒数第二个。这个特性在处理图像序列时特别方便比如你要取最后一张图像做参考直接写Images[-1]就行。图像变量的操作跟元组类似但多了一层“图像对象”的概念。读取图像用read_imageread_image (Image, part_001.png)这行代码把指定路径的图像读入变量Image。如果要批量读取一个文件夹下的所有图像可以用元组通配符list_files (./images, [files,follow_links], ImageFiles) tuple_regexp_select (ImageFiles, [\\.(png|jpg|bmp)$,ignore_case], ImageFiles) for Index : 0 to |ImageFiles| - 1 by 1 read_image (Image, ImageFiles[Index]) * 在这里写处理逻辑 endfor这段代码先用list_files列出目录下所有文件再用tuple_regexp_select筛选出图像格式的文件最后循环读取。注意|ImageFiles|表示元组的长度这个竖线语法是HDevelop特有的跟其他语言用.length或len()不一样。注意list_files的第二个参数是一个控制参数元组files表示只列文件不列目录follow_links表示跟随符号链接。如果你在Linux环境下工作这个参数可能需要调整。3.2 控制流比想象中更灵活HDevelop的控制流语法看起来跟常规语言差不多但有几个细节值得单独说。条件判断用if...elseif...else...endifif (Area 1000) * 面积大于1000的处理 elseif (Area 500) * 面积在500到1000之间的处理 else * 面积小于等于500的处理 endif注意条件表达式必须用括号包起来这是HDevelop的硬性要求不写括号会报语法错误。我刚开始用的时候经常忘后来养成了习惯写if的时候先把括号打上再填内容。循环有两种for循环和while循环。for循环的语法是for Index : 0 to 9 by 1 * 循环体 endforby 1表示步长为1可以改成其他值比如by 2就是隔一个取一个。如果要从大到小循环把to换成to不行得用负数步长for Index : 9 to 0 by -1 * 倒序循环 endforwhile循环更灵活适合不知道循环次数的场景Counter : 0 while (Counter 10) * 循环体 Counter : Counter 1 endwhile这里有个坑HDevelop没有break和continue关键字。如果你想在循环中间跳出得用条件判断把剩余逻辑包起来或者用一个标志变量控制循环条件。我早期写代码时习惯用break转到HDevelop后适应了好一阵。还有一个特殊之处HDevelop的循环可以并行执行。如果你在循环体里调用的算子支持并行化HDevelop会自动利用多核CPU加速。这个特性在批量处理图像时效果很明显我实测过四核机器上处理一百张图像并行比串行快将近三倍。但要注意不是所有算子都支持并行具体得查文档。3.3 算子调用与参数传递的讲究HDevelop的算子调用语法是算子名 (输入参数, 输出参数)。输入参数在前输出参数在后中间用逗号分隔。比如threshold (Image, Region, 128, 255)这行代码对图像Image做阈值分割灰度值在128到255之间的像素被选中结果存入Region。Image是输入Region是输出128和255是控制参数。这里有一个容易混淆的点控制参数和输入图像参数在语法上都是输入参数但它们的角色不同。图像参数是数据流的主体控制参数是调节算子行为的旋钮。HDevelop的文档里会把它们分开标注写代码的时候心里要清楚哪个是哪个。算子的参数顺序是固定的不能调换。比如threshold的第一个参数永远是输入图像第二个永远是输出区域后面才是阈值范围。如果你不确定某个算子的参数顺序把光标放在算子名上按F1会弹出帮助文档里面有详细的参数说明和示例代码。还有一个实用技巧算子可以接受元组作为输入自动逐元素处理。比如你有三张图像存在元组Images里想对每张都做阈值分割不需要写循环threshold (Images, Regions, 128, 255)这行代码执行完Regions是一个包含三个区域的元组分别对应三张图像的分割结果。这个特性在批量验证时能省大量代码。但要注意如果元组里的元素类型不一致比如混了图像和数字算子会报错。所以用这个特性之前确保元组里的元素类型统一。3.4 自定义函数与过程当流程变长代码全堆在主程序里会很难维护。HDevelop支持把一段逻辑封装成自定义函数或过程。自定义函数的定义语法是function my_function (InputParam) * 函数体 return OutputParam endfunction调用的时候跟内置算子一样Result : my_function (Value)过程跟函数的区别在于过程可以有多个输出参数而且不需要返回值。定义语法是procedure my_procedure (InputParam : OutputParam) * 过程体 endprocedure注意输入参数和输出参数之间用冒号分隔这是HDevelop过程定义的特殊语法。我个人的经验是如果一段逻辑只在一个地方用直接写在主程序里就行如果要在多个地方复用或者逻辑比较复杂需要单独调试就封装成函数或过程。封装的好处是可以单独测试输入输出清晰主程序看起来也干净。但封装也有代价。HDevelop的函数调用有一定的开销如果封装的逻辑本身很短比如就一两行算子封装反而会拖慢速度。我做过测试一个只包含threshold的函数调用一千次比直接写一千次threshold慢大概百分之十五。所以封装要适度别为了封装而封装。4. 完整实操流程与核心环节实现4.1 环境准备与项目结构在开始写仿真程序之前先把环境理清楚。HDevelop的安装没什么特别的一路下一步就行。但项目结构值得花点心思。我习惯把项目分成三个文件夹images放测试图像scripts放HDevelop脚本文件results放运行结果和日志。这样结构清晰后面找东西方便。HDevelop的脚本文件后缀是.hdev双击就能打开。如果你要跟其他人协作建议在脚本开头加一段注释说明这个脚本的用途、输入图像的要求、输出结果的格式。我见过太多项目脚本写了一堆过两个月自己都忘了当初为什么这么写。* 项目名称金属零件表面缺陷检测 * 创建日期2024-01-15 * 输入图像灰度图分辨率640x4808位 * 输出结果缺陷区域坐标和面积 * 备注阈值参数针对当前光照条件调优换批次需重新验证这段注释看起来简单但能省很多沟通成本。4.2 图像读取与预处理流程假设我们要做一个金属零件表面缺陷检测的仿真程序。第一步是读取图像并做预处理。* 读取图像 read_image (Image, metal_part_001.png) * 转为灰度图如果原图是彩色 rgb1_to_gray (Image, GrayImage) * 高斯滤波去噪 gaussian_filter (GrayImage, SmoothedImage, 1.5) * 局部阈值分割 mean_image (SmoothedImage, MeanImage, 15, 15) dyn_threshold (SmoothedImage, MeanImage, RegionDyn, 8, light)这几行代码的逻辑是先读图转灰度然后用高斯滤波平滑掉高频噪声再用动态阈值分割提取比周围亮的区域。dyn_threshold的参数8是灰度差阈值light表示提取比背景亮的区域。如果缺陷是暗的改成dark。这里的关键参数是高斯滤波的核大小和动态阈值的灰度差。核大小决定了平滑程度太大缺陷会被模糊掉太小噪声去不干净。我一般从1.5开始试根据图像噪声水平上下调整。灰度差阈值决定了分割的灵敏度太小会引入噪声太大会漏掉弱缺陷。这个参数需要结合具体图像反复调试。4.3 特征提取与缺陷判定分割出候选区域后下一步是提取特征并判定哪些是真正的缺陷。* 连通域分析 connection (RegionDyn, ConnectedRegions) * 筛选面积在合理范围内的区域 select_shape (ConnectedRegions, SelectedRegions, area, and, 50, 5000) * 计算区域特征 area_center (SelectedRegions, Area, Row, Column) * 计算圆度 circularity (SelectedRegions, Circularity) * 综合判定 DefectCount : |SelectedRegions| if (DefectCount 0) * 有缺陷输出缺陷信息 for Index : 0 to DefectCount - 1 by 1 * 记录每个缺陷的面积和位置 endfor else * 无缺陷 endif这段代码先用connection把分割结果拆成独立的连通域然后用select_shape按面积筛选去掉太小可能是噪声和太大可能是误分割的区域。接着计算面积、中心坐标、圆度等特征最后根据缺陷数量做判定。select_shape的参数and表示多个筛选条件之间是“与”关系。如果你有多个条件比如面积在50到5000之间且圆度大于0.8可以写成select_shape (ConnectedRegions, SelectedRegions, [area,circularity], and, [50,0.8], [5000,1.0])注意这里用了两个元组分别指定特征名、下限和上限顺序要对应。4.4 批量验证与性能统计单张图像调好后需要用一批图像验证算法的稳定性。这时候前面提到的元组批量处理就派上用场了。* 获取所有测试图像 list_files (./images, [files], ImageFiles) tuple_regexp_select (ImageFiles, [\\.png$,ignore_case], ImageFiles) * 统计变量初始化 TotalTime : 0 DefectTotal : 0 ImageCount : |ImageFiles| * 循环处理 for Index : 0 to ImageCount - 1 by 1 * 计时开始 count_seconds (StartTime) * 读取并处理 read_image (Image, ImageFiles[Index]) rgb1_to_gray (Image, GrayImage) gaussian_filter (GrayImage, SmoothedImage, 1.5) mean_image (SmoothedImage, MeanImage, 15, 15) dyn_threshold (SmoothedImage, MeanImage, RegionDyn, 8, light) connection (RegionDyn, ConnectedRegions) select_shape (ConnectedRegions, SelectedRegions, area, and, 50, 5000) * 计时结束 count_seconds (EndTime) SingleTime : (EndTime - StartTime) * 1000 * 累计 TotalTime : TotalTime SingleTime DefectTotal : DefectTotal |SelectedRegions| * 输出单张结果 dev_display (Image) dev_display (SelectedRegions) endfor * 输出统计结果 AverageTime : TotalTime / ImageCount这段代码的关键是count_seconds算子它返回当前时间戳单位是秒。两次调用相减再乘以1000就是毫秒数。我一般会跑三遍取平均值因为第一遍可能受磁盘缓存影响偏慢。统计结果出来后重点看两个指标平均处理时间和缺陷检出数量。平均处理时间要跟产线节拍对比留出至少百分之三十的余量。缺陷检出数量要跟人工标注的结果对比计算误判率和漏判率。如果误判率高说明筛选条件太松如果漏判率高说明分割阈值太严。4.5 参数调优的实操技巧参数调优是仿真程序里最耗时的环节但也有一些技巧能提高效率。第一个技巧是用dev_set_check控制可视化更新频率。默认情况下每次dev_display都会刷新窗口图像多的时候会拖慢速度。可以在循环开始前关掉自动更新dev_set_check (~give_error) dev_update_off ()dev_update_off会关闭变量窗口和图像窗口的自动更新循环结束后再dev_update_on打开。这样批量处理时速度能快不少。第二个技巧是用dev_set_paint和dev_set_color突出显示关键区域。比如你想看分割结果在原图上的位置可以设置绘制模式为边缘dev_set_paint (margin) dev_set_color (red) dev_display (SelectedRegions)这样区域会以红色边缘的形式叠加在原图上比填充模式更容易看清细节。第三个技巧是用dev_inspect_ctrl检查中间变量。如果你不确定某个中间结果对不对可以在代码里插入dev_inspect_ctrl (Variable)运行到这里会弹出一个窗口显示变量的值。这个功能在调试复杂流程时特别有用比在变量窗口里翻找快得多。注意dev_inspect_ctrl会暂停程序执行调试完记得删掉不然批量处理时会卡住。5. 常见问题与排查技巧实录5.1 语法报错速查表HDevelop的报错信息有时候比较隐晦我整理了一张常见报错和解决方法的对照表。报错信息可能原因解决方法Wrong number of values算子参数个数不对按F1查文档核对参数列表Unknown identifier变量名拼写错误或未定义检查变量名确保在使用前赋值Type mismatch参数类型不匹配检查传入的是图像还是元组必要时用tuple_系列算子转换Index out of range元组索引越界用File not found图像路径错误用绝对路径或确认相对路径的基准目录Timeout算子执行超时检查图像尺寸是否过大或算子参数是否合理这张表覆盖了我遇到过的八成以上报错。剩下两成通常是逻辑错误不是语法错误需要靠调试和日志来定位。5.2 图像读取失败的排查思路图像读取失败是最常见的问题之一。排查思路可以按这个顺序来。先确认路径对不对。HDevelop的相对路径是相对于当前脚本文件所在的目录不是相对于HDevelop的安装目录。如果你不确定可以在代码里加一行get_current_dir (CurrentDir)把当前目录打印出来看看。再确认文件格式支不支持。HDevelop支持常见的图像格式但有些特殊格式比如某些相机的原始数据格式需要额外的接口。如果你读的是这类文件得先确认有没有对应的算子。最后确认文件权限。如果图像文件被其他程序占用或者当前用户没有读取权限也会报错。这种情况在Windows上比较少见但在Linux环境下需要注意。5.3 处理速度慢的优化方向处理速度慢是另一个高频问题。优化方向可以按这个优先级来。第一优先级是减少图像尺寸。如果原图是两千万像素但缺陷特征在缩小四倍后依然清晰那就先缩小再处理。zoom_image_factor算子可以按比例缩放图像速度提升是平方级的。第二优先级是缩小处理区域。如果缺陷只可能出现在图像的某个区域用reduce_domain把处理范围限制在那个区域能省大量计算。第三优先级是换用更高效的算子。比如阈值分割threshold比dyn_threshold快如果光照均匀就用前者。形态学操作opening_circle比opening_rectangle1慢能用矩形就别用圆形。第四优先级是开启并行计算。前面提到过HDevelop的循环可以并行执行。在循环前加一行set_system (parallelize_operators, true)能自动利用多核。但要注意不是所有算子都支持并行具体得实测。5.4 调试过程中的独家避坑经验说几个文档里不会写、但实际工作中经常遇到的坑。第一个坑是变量名冲突。HDevelop的变量作用域是全局的你在函数里定义的变量名如果跟主程序里的重名会互相覆盖。我建议在函数内的变量名前加前缀比如fn_这样一眼就能看出是函数内的变量。第二个坑是图像句柄泄漏。HDevelop的图像变量在赋值新图像时旧图像的内存不会立即释放而是等垃圾回收。如果你在循环里反复读取大图像内存会持续增长最终导致程序变慢甚至崩溃。解决办法是在循环末尾手动释放不再使用的图像* 处理完后释放 clear_obj (Image) clear_obj (GrayImage)第三个坑是浮点数比较。HDevelop的浮点数运算有精度问题直接用比较两个浮点数可能得到错误结果。正确的做法是判断差值是否小于一个极小值if (abs(Value1 - Value2) 0.0001) * 认为相等 endif第四个坑是元组索引的边界情况。当元组为空时|Tuple|返回0但如果你写Tuple[0]会报错。所以在取元素之前一定要先判断长度if (|Tuple| 0) FirstElement : Tuple[0] endif这些坑我几乎每个都踩过有些还踩了好几次。写出来希望能帮你省点时间。6. 从仿真到部署的衔接要点仿真程序调好后最终要导出成可部署的形式。HDevelop提供了几种导出方式各有适用场景。第一种是导出为通用语言代码。在文件菜单里选“导出”可以生成对应语言的代码。这种方式适合算法逻辑已经稳定、不需要频繁改动的场景。导出的代码可以直接嵌入到上位机软件里。第二种是导出为动态链接库。这种方式适合需要跟其他系统集成的场景比如产线的PLC程序调用视觉算法。导出时需要指定接口函数的输入输出格式HDevelop会自动生成封装代码。第三种是保留HDevelop脚本通过接口调用。这种方式适合算法还在迭代、需要频繁调整的场景。上位机通过接口把图像传给HDevelopHDevelop处理完把结果返回。缺点是性能比前两种差一些因为多了一层通信开销。我个人的选择标准是如果算法已经稳定导出成通用语言代码如果还在迭代保留脚本通过接口调用如果需要跟硬件紧密耦合导出成动态链接库。三种方式没有绝对的好坏看具体需求。导出过程中有一个细节要注意仿真环境里用的相对路径导出后可能失效。因为导出后的程序运行目录变了相对路径的基准也跟着变。解决办法是在导出前把路径改成绝对路径或者在导出后的代码里加一段路径配置逻辑。还有一个细节是图像变量的生命周期管理。仿真环境里HDevelop会自动管理内存但导出后的代码需要手动管理。如果导出的是通用语言代码记得在适当的位置释放图像内存不然长时间运行会内存泄漏。7. 我个人的一些实操体会写了这么多最后分享几点个人体会不算总结就是一些零散的经验。HDevelop的仿真程序语法入门门槛其实不高但精通需要时间。我建议新手先从模仿开始找几个官方示例脚本一行一行读搞懂每行在干什么然后自己改参数看结果变化。这个过程比看文档快得多。调试的时候不要怕试错。HDevelop的即时反馈特性就是让你大胆试的改一个参数按一下运行结果立刻出来。我调一个复杂流程的参数经常要试几十次甚至上百次但每次只花几秒钟总体时间并不长。还有一点仿真程序里的参数不是一成不变的。今天调好的参数换一批工件、换一个光照条件可能就不适用了。所以仿真程序里最好把关键参数提取成变量放在脚本开头集中管理后面换批次时只改这几个变量就行不用满脚本找。最后说一个我最近才发现的技巧HDevelop支持在脚本里嵌入其他语言的代码片段。比如你可以在HDevelop脚本里写一段通用语言的代码通过接口调用。这个特性在需要做一些HDevelop不擅长的操作时特别有用比如复杂的字符串处理或者文件操作。具体用法是在脚本里用*注释掉一段代码然后用特定的标记包起来导出时会自动识别。这个技巧我也是偶然在文档角落里翻到的用好了能省不少事。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →