首页 / 资讯中心 / 文章详情

HALCON图像涂写避坑:窗口叠加层与像素矩阵区分及实战

HALCON图像涂写避坑:窗口叠加层与像素矩阵区分及实战 ★ FEATURED ARTICLE
上周又被问了那个老问题在 HDevelop 里明明用鼠标在图像上圈了个区域、旁边还写了缺陷两个字write_image存出来一看干干净净框没了字也没了。这不是算子写错了而是把窗口叠加层和图像像素矩阵当成了一回事。HALCON 的图像涂写paint之所以容易踩坑根子就在这条分界线上dev_display、dev_set_color、disp_text玩的是窗口那层贴纸write_image只搬运底下那张照片。这篇东西我就把 HALCON 里跟图像、区域涂写相关的算子摊开讲一遍哪些算子是真改像素、paint_region和overpaint_region该怎么选、Grayval里藏着什么类型陷阱、鼠标交互圈的 ROI 怎么回填、以及往图上写文字这件 HALCON 一直没给直通车道的事到底怎么绕。内容偏实战代码以 HDevelop 语法为主C#/C 调 HALCON 的写法逻辑一样只是对象句柄的传递方式不同。适合已经能跑通thresholdconnection但一到标个红框输出给客户看就卡住的同学。1. 图像涂写的三条技术路线选错一条就白干1.1 窗口图层与像素数据是两套独立系统先把这张表记住后面所有坑几乎都是它派生出来的。你做的动作典型算子作用对象能否被write_image保存改显示颜色dev_set_color/set_color窗口叠加层否显示文字disp_text/write_string窗口叠加层否显示区域轮廓dev_display/set_draw窗口叠加层否把区域填成灰度paint_region图像像素矩阵是原地改写像素overpaint_region图像像素矩阵是生成二值掩膜图region_to_bin新图像是缩小定义域reduce_domain图像的 domain 标记部分格式会存成裁剪图这张表的关键信息是第三列。HDevelop 的图形窗口本质上是一块画布你dev_display(Image)是把像素画上去然后dev_set_color(red)dev_display(Region)是在像素上面又刷了一层半透明的红。dev_clear_window一按这层红就没了底下的像素一个都没变。很多新手做完演示按了dump_window_image才发现能存下来是因为那个算子截的恰恰是画布也就是叠加层和底图合成后的结果——这一点在第 4 节会细讲。所以在动手涂写之前先问自己一个问题我要的是看得见还是存得下如果只是调试时看得见dev_set_color一行就够不要动像素动了反而污染后续的min_max_gray、intensity这类统计算子。如果这张图要落地成文件、要传给下游、要参与后续算法计算那必须走paint_region这条真改像素的路。1.2 reduce_domain 不是涂写它只是贴了一张允许通行的标签reduce_domain(Image, Region, ImageReduced)这个名字太有迷惑性很多人以为它把区域外的部分裁掉了。它没有。它只是在图像对象上挂了一个 domain告诉后续算子只处理这块范围内的像素。区别在哪reduce_domain之后区域外的像素值原封不动躺在内存里get_image_size返回的宽高也不变。而paint_region是把区域内的像素值真的改成你指定的灰度。一个改变的是计算范围一个改变的是数据本身。我见过一个很典型的事故有人想给模板匹配做带遮盖的模板用reduce_domain把干扰区域排除了然后create_shape_model拿ImageReduced去训练。结果模板里干扰区域还是被算了进去——因为create_shape_model内部对 domain 的处理方式和你想象的不一样它不是简单地忽略 domain 外像素。这种场景正确的做法是先用paint_region把那块干扰涂成一个和背景接近的灰度抹掉它的梯度再建模板。1.3 真正需要动像素的四个高频场景我梳理了一下自己项目里用到涂写的场合基本跑不出这四类第一类是打靶标记。检测出缺陷后要把缺陷区域标成红色导出给产线人员或者客户看这是最刚需的场景也是paint_region的主场。第二类是生成掩膜。把 ROI 以外的区域涂成 0这样后面做min_max_gray、自适应阈值、均值滤波时边缘不会因为 ROI 外的亮斑产生拖尾。这一招在检测圆形工件边缘时特别管用。第三类是给算法喂干净的输入。比如一张图上有个固定的反光点每次都误报与其在算法里加一堆排除逻辑不如在预处理阶段用paint_region把它涂成周围背景的中值灰度后面整条链路的复杂度都能降一档。第四类是可视化叠加。把深度图或者热力图伪彩之后再把缺陷轮廓叠上去生成一张能直接进报告的综合图。2. paint_region 与 overpaint_region参数里藏着的三个决定2.1 Grayval 是元组长度必须等于通道数这是最容易被忽略的一条。paint_region(Region, Image, ImageResult, Grayval, Type)里的Grayval不是单值而是一个元组长度必须等于Image的通道数。单通道灰度图写一个数paint_region(Defect, Image, ImageMarked, 255, fill)。三通道彩色图写三个数paint_region(Defect, Image, ImageMarked, [255, 0, 0], fill)得到的是纯红。这里顺序是 R、G、B不是 B、G、R写反了会得到蓝色别问我怎么知道的。如果你只给了[255]却喂了一张三通道图HALCON 不会自动帮你补齐行为是不可预期的。稳妥的做法是先count_channels(Image, Channels)判断一下再决定 Grayval 的写法。还有一个隐藏维度Grayval的长度是通道数不是你想要的颜色数。有些朋友想涂一个渐变那得自己算好每个像素值再用gen_image_proto之类的构造paint_region只支持常量填充。2.2 Type 的 fill 与 margin 决定是涂面还是描边Type只有两个合法取值fill把区域内部所有像素全部改成Grayval就是实心涂。margin只把区域的边界像素改成Grayval内部不动。想给缺陷画个红框最直觉的做法是gen_rectangle1生成矩形区域然后paint_region(..., margin)。但实测下来margin的线条只有 1 像素宽在 500 万像素的图上导出成 PNG 再缩放到 PPT 里基本看不见。我自己的惯用做法是用两个矩形做差得到指定粗细的边框环gen_rectangle1 (BoxOuter, Row1, Column1, Row2, Column2) gen_rectangle1 (BoxInner, Row1 3, Column1 3, Row2 - 3, Column2 - 3) difference (BoxOuter, BoxInner, BoxRing) union2 (BoxRing, DefectRegion, ToPaint) paint_region (ToPaint, ImageCopy, ImageMarked, [255, 0, 0], fill)这样边框宽度由3这个偏移量精确控制想要多粗就调多大比跟set_line_width较劲可靠得多。而且因为最后统一用fill涂一次调用搞定比先涂面再描边省一半时间。2.3 overpaint_region 原地改写带来的连锁反应overpaint_region(Image, Region, Grayval, Type)和paint_region的关系类似就地修改和返回新对象。前者直接把传进来的Image改掉后者生成一张ImageResult。那要不要为了省内存一律用overpaint_region我的经验是分情况如果这张图后面还要跟原图比对比如算差异图必须用paint_region先把原图copy_image一份留着。如果从头到尾就是预处理一下然后扔掉overpaint_region更省事也少一次大图的内存分配。但overpaint_region有个非常隐蔽的副作用HALCON 的图像对象是写时复制copy-on-write的如果你有另一个变量还指向同一张底层数据改写之后那个变量看到的内容也会变。曾经有个项目里我把图存进一个数组做批处理中间用overpaint_region做了预处理结果最后汇总输出时发现前面几帧的原始数据全被改花了。排查了两个小时才定位到这一行。所以但凡图像对象存在多个引用老老实实用paint_region生成新对象。还有一个坑是overpaint_region对 domain 的处理。它只改 domain 内的像素region 里超出 domain 的部分会被静默忽略不报错、不警告。等你发现涂了半天有一块没涂上回头查才发现是前面某一步reduce_domain把 domain 缩了。2.4 灰度值必须匹配图像类型否则涂了等于没涂这张表建议贴在显示器边上。图像类型合法灰度范围涂 255 会怎样byte0 ~ 255正常最亮白uint20 ~ 65535只是偏暗的灰远不到白int2-32768 ~ 32767正常但容易溢出误解real一般 0.0 ~ 1.0严重超范围显示被截断成纯白direction角度值语义被破坏后续方向类算子全乱最常见的翻车是real图像。你从某个流程拿到一张real类型的深度图或归一化图顺手paint_region涂了个255结果那一块全白和周围完全脱节而且这个 255 传下去做scale_image时会把整张图的动态范围拉坏。正确姿势是先确认类型get_image_type (Image, Type) if (Type ! byte) convert_image_type (Image, ImageByte, byte) endif或者按类型给对应的值real图涂1.0uint2图涂65535。我一般会在工程里写一个小工具函数输入图像和区间自动算出一个看起来是红色的灰度值避免每个项目重新试。3. 区域侧的准备从鼠标圈选到批量合并3.1 交互绘制set_draw 是窗口级的别和 dev_set_draw 搞混做半自动标注工具时draw_region系列是主力。但很多人第一次用会发现鼠标画出来的是一片黑我想要的是轮廓。原因在set_draw。set_draw(WindowHandle, Mode)的Mode取fill还是margin决定的是鼠标拖动过程中视觉反馈的形状以及draw_region返回的区域是填充的还是描边的。注意这个状态是挂在WindowHandle上的每个窗口各管各的。而 HDevelop 里还有一个全局的dev_set_draw它作用于当前活动窗口是给dev_display用的。两套东西名字像作用域不同混用会出现我明明设成 margin 了画出来还是实心的情况。常用的一族交互算子我列个清单方便对照算子返回值适合场景draw_region任意多边形区域不规则缺陷标注draw_rectangle1两个角点行列坐标正矩形 ROI最常用draw_rectangle2中心、角度、半长半宽有旋转的矩形draw_circle圆心 半径圆形工件draw_ellipse椭圆参数变形目标draw_polygon多边形区域复杂边界其中draw_rectangle1我最推荐因为它返回的是坐标而不是区域你可以在拿到坐标后做加工——比如向外扩 5 像素留边距或者坐标对齐到偶数方便后续处理。而draw_region返回的是已经光栅化的区域想再调整就得走shape_trans或者形态学灵活性差一些。3.2 行列顺序Row 在前Column 在后这个坑简单到不好意思写但每个月都能见到有人在群里问为什么我的框画在了左上角。HALCON 所有跟坐标有关的算子参数顺序都是Row, Column也就是先行后列。gen_rectangle1(Region, Row1, Column1, Row2, Column2)其中 Row 是纵坐标从上往下数Column 是横坐标从左往右数。而get_image_size(Image, Width, Height)返回的却是Width, Height也就是先列后行。于是就有了这个经典组合错误get_image_size (Image, Width, Height) gen_rectangle1 (BigBox, 0, 0, Width, Height) // 错了把宽当成了行如果图是 1920×1080这一行生成的是一个从 (0,0) 到 (1920,1080) 的矩形纵向超出了图像 840 像素。在没有 domain 限制的情况下它不报错只是超出部分被忽略看起来框的下半部分没了。正确写法是gen_rectangle1(BigBox, 0, 0, Height - 1, Width - 1)注意还要减 1因为坐标是 0 基的。这个减 1 也是高频疏漏边界那一行/列经常就这么丢了。3.3 多个区域先 union 再涂省时又省心假设一张图里有 40 个缺陷逐个paint_region就是 40 次调用每次都要分配一张新图内存和耗时都是线性叠加。实测在一张 4096×3000 的图上40 次单区域paint_region比一次合并区域后涂写慢 6 到 8 倍。优化思路很朴素connection (DefectAll, DefectList) select_shape (DefectList, Selected, area, and, 50, 99999) union1 (Selected, DefectUnion) copy_image (Image, ImageCopy) paint_region (DefectUnion, ImageCopy, ImageMarked, [255, 0, 0], fill)union1把区域数组压成一个区域paint_region一次搞定。唯一需要注意的是如果这些区域颜色各不相同比如按缺陷等级用红黄绿三色标注那必须分组涂按等级select_shape分成三组每组union1后各涂一次总共三次调用依然比 40 次快得多。这里顺带说一句paint_region内部对区域的扫描方式。它对每个区域做边界框定位然后逐行扫描填充。所以如果你的区域是那种又大又分散的比如图像左右两端各一个大块合并后边界框覆盖了整幅图扫描效率反而不如分开涂。这种情况建议按位置分簇别一股脑union1。4. 往图像里烧文字没有直通车道但有两条绕行路4.1 dump_window_image把窗口当画布截下来HALCON 能在原图上写文字吗这个问题热词里也出现了。直接回答截至目前的算子集里没有一个write_string_to_image式的直接接口。write_string(WindowHandle, String)写的是窗口不是图像。但这不代表做不到。业界最通用的路子是先在窗口里画好再把窗口整个截成图像* 1. 开一个和图像等大的窗口保证 1:1 映射 get_image_size (ImageMarked, Width, Height) dev_open_window (0, 0, Width, Height, black, WindowHandle) dev_set_part (WindowHandle, 0, 0, Height - 1, Width - 1) * 2. 显示底图 dev_display (ImageMarked) * 3. 设置文字属性 set_font (WindowHandle, mono) set_color (WindowHandle, white) set_tposition (WindowHandle, 20, 20) write_string (WindowHandle, DEFECT 3 PCS) * 4. 截图 dump_window_image (ImageWithText, WindowHandle)几个关键点必须说清楚窗口尺寸必须等于图像尺寸。dev_open_window的宽高直接影响dump_window_image的输出分辨率。窗口开小了截出来的图就被压缩了再叠回原图会糊。窗口开大了输出比原图还大坐标全对不上。dev_set_part保证 1:1。如果窗口大小和图像大小一致但没用dev_set_part把显示范围设成完整图像HALCON 默认会自适应缩放。一旦缩放你在窗口里的 (20,20) 映射到图像里就不是 (20,20) 了位置会飘。set_tposition的坐标是窗口坐标不是图像坐标。在 1:1 且无缩放的前提下这两者才等价。这个前提一旦被破坏文字位置就不可控。换行用\n。这对应热词里halcon 换行符号怎么设置。HDevelop 的字符串字面量支持转义序列write_string(WindowHandle, LINE 1\nLINE 2)就能出两行。要注意的是字号和行距是绑定的想让行距大一点得手动分两次set_tpositionwrite_stringHALCON 没有独立行距参数。字体。set_font可以传mono、sans、serif这类通用名也可以传字体文件的完整路径。项目部署时我强烈建议用文件路径固定字体因为不同机器上字体渲染结果不一样同一份代码在两台机器上跑的标注图字号会差一截对不齐会很难看。生产环境我用.ttf路径随工程一起打包。如果你只是想量一下文字占多大地方好排版可以用get_string_extents(WindowHandle, String, Ascent, Descent, TextWidth, TextHeight)拿到宽高之后再决定set_tposition往哪儿放。4.2 通道对齐彩色文字到底往哪几个通道写dump_window_image出来的是三通道 byte 图像也就是窗口渲染的 RGB 结果。你要把它叠回原来的彩色图上就得做通道级的合并。假设底图是ImageMarked三通道文字图是ImageWithText三通道希望文字盖在底图上* 用文字图做前景按灰度做掩膜 rgb1_to_gray (ImageWithText, TextGray) threshold (TextGray, TextMask, 1, 255) * 逐通道合并 access_channel (ImageMarked, ChR, 1) access_channel (ImageMarked, ChG, 2) access_channel (ImageMarked, ChB, 3) access_channel (ImageWithText, TR, 1) access_channel (ImageWithText, TG, 2) access_channel (ImageWithText, TB, 3) paint_region (TextMask, ChR, ChR2, 255, fill) paint_region (TextMask, ChG, ChG2, 255, fill) paint_region (TextMask, ChB, ChB2, 255, fill) compose3 (ChR2, ChG2, ChB2, ImageFinal)上面这段为了演示逻辑写得很啰嗦实际项目里更简洁的方式是直接用dump_window_image的结果作为整图的一部分——如果文字所在区域不重叠重要内容直接compose3之后按掩膜替换就行。这里有个细节要留神dump_window_image的背景色和底图的差异。如果窗口背景是黑的而底图是白底那你要么把窗口背景设成和底图一致要么严格用掩膜只取文字笔画别把整块背景一起盖上去。我吃过这个亏图上多了一个大黑方块排查半天才发现是背景色。4.3 另一条路把字形拆成区域可控但费工如果项目要求文字必须是纯二值、无抗锯齿、可精确定位到像素比如做样本生成、做二值化对比测试上面那条窗口截图的路子就不合适了因为抗锯齿会引入中间灰度。这时候只能自己造字库区域。思路是把每个字符预先生成一张小图threshold出区域后存成region文件用的时候按字符查表gen_region出来平移到目标位置最后union1paint_region。这条路我做过一次只覆盖了 0-9 和 A-Z 加几个符号工作量不小。它的优势是完全确定性的给定输入输出像素完全一致跨机器、跨版本都稳。如果你做的是自动化测试或者需要严格复现的样本生成这条路的稳定性值得那点工作量。5. 几个真实踩过的坑与排查链路5.1 涂了却看不出来从类型查到 domain这个现象我归纳成一套固定排查顺序基本三步能定位第一步查类型。get_image_type(Image, Type)看是不是real。是real而你又涂了255那就是范围问题改用1.0或者先转byte。第二步查 domain。get_domain(Image, Domain)然后area_center(Domain, DomainArea, _, _)和get_image_size算出来的总像素数比一下。如果 domain 面积远小于总像素说明前面有reduce_domain缩过范围你涂的区域如果落在 domain 外面就是白涂。第三步查通道。count_channels(Image, Channels)确认Grayval的长度对得上。三通道图给单值有时候不报错但效果诡异。还有个小概率情况涂的颜色和背景太接近。用min_max_gray量一下涂写前后的局部灰度确认差值够大。做可视化输出时涂写颜色的选择和底图的对比度要专门调一次别用默认的红——如果底图本身就是暖色调红框根本跳不出来。5.2 循环里涂写导致耗时暴涨的定位过程遇到过一个批处理任务单张图处理 200ms批量跑 500 张用了 8 分钟算下来单张接近 1 秒。用count_seconds在关键节点打点发现时间全耗在一个循环里的paint_region上。那段代码大概是这样for Index : 0 to |Defects| - 1 by 1 select_obj (Defects, OneDefect, Index 1) paint_region (OneDefect, ImageWork, ImageWork, [255, 0, 0], fill) endfor问题有两点。一是每次paint_region都生成一张新的三通道大图500 个缺陷就是 500 次全图内存分配和拷贝二是输入输出用了同一个变量名HALCON 内部要做引用检查额外开销。改成先合并再涂之后union1 (Defects, AllDefects) paint_region (AllDefects, ImageCopy, ImageMarked, [255, 0, 0], fill)单张图的这部分耗时从 700ms 降到 90ms 左右。这个案例我后来写进了组内的规范任何在循环体内对大图做paint_region的写法先想想能不能搬到循环外。还有一种情况不能合并需要按缺陷等级涂不同颜色。那就按等级分组组数远小于缺陷数收益依然很大。5.3 涂完再 reduce_domain输出图被裁了这个坑发生在导出环节。流程是reduce_domain限定检测区域 → 检测 →paint_region标注 →write_image导出。导出的 PNG 只有检测区域那一块周围全没了。原因是write_image会尊重图像的 domain。你的图像经过reduce_domain之后 domain 已经不是全图了write_image就按 domain 的边界框输出。解决办法是导出前调full_domainfull_domain (ImageMarked, ImageFull) write_image (ImageFull, png, 0, result.png)full_domain把 domain 恢复成整幅图范围。注意它不改像素值只是把 domain 的标记恢复所以原来 domain 外那些从未被处理过的像素会原样出现在输出里——如果那些区域的像素值本来就是原图数据那没问题如果图像是gen_image_const造出来的空图那块就是黑的。如果不想让 domain 外的内容露出来可以先paint_region把背景统一涂成白或黑再full_domain这样导出的是干净的白底标注图。另外提一个容易混的算子crop_domain(Image, ImageCropped)它是真的把图像裁成 domain 的边界框宽高都变了。什么时候用crop_domain当你的下一步只需要那块 ROI 的像素、不需要保留原图尺寸时用它能省内存也能提速。什么时候用full_domain要保留原图坐标系时用它因为crop_domain之后所有坐标都变了你之前记下的缺陷坐标全部失效。6. 一个完整小案例缺陷区域标红并输出可交付图片6.1 流程骨架与逐段代码把前面所有内容串起来做一个检测 标注 导出的最小可用流程。假设输入是一张有反光干扰的工件图。* ---------- 1. 读图与信息采集 ---------- read_image (Image, workpiece_01.png) get_image_size (Image, Width, Height) count_channels (Image, Channels) get_image_type (Image, ImageType) * ---------- 2. 预处理抹掉固定反光区 ---------- * 反光区坐标是工艺固定的直接涂成背景灰度 gen_rectangle1 (GlareArea, 120, 1680, 260, 1880) if (ImageType byte) BackGray : 128 else BackGray : 0.5 endif copy_image (Image, ImageClean) paint_region (GlareArea, ImageClean, ImageClean, BackGray, fill) * ---------- 3. 缺陷检测 ---------- rgb1_to_gray (ImageClean, GrayImage) dyn_threshold (GrayImage, mean_image(GrayImage, GrayMean, 21, 21), \ DefectRaw, 15, dark) connection (DefectRaw, DefectList) select_shape (DefectList, DefectSelected, area, and, 80, 999999) * ---------- 4. 生成标注区域缺陷面 外扩边框 ---------- union1 (DefectSelected, DefectUnion) smallest_rectangle1 (DefectSelected, Row1, Col1, Row2, Col2) gen_rectangle1 (BoxOuter, Row1, Col1, Row2, Col2) gen_rectangle1 (BoxInner, Row1 3, Col1 3, Row2 - 3, Col2 - 3) difference (BoxOuter, BoxInner, BoxRing) union2 (BoxRing, DefectUnion, ToMark) * ---------- 5. 涂写到彩色副本上 ---------- copy_image (Image, ImageMarked) paint_region (ToMark, ImageMarked, ImageMarked, [255, 0, 0], fill) * ---------- 6. 恢复全图 domain 并导出 ---------- full_domain (ImageMarked, ImageOut) write_image (ImageOut, png, 0, result_marked.png)这段代码有几个地方值得展开说。第 2 步的固定反光区处理坐标是硬编码的。这在单工位设备上完全可接受因为相机和工件的相对位置是锁死的。判断图像类型来选灰度值这一步不能省real图涂 128 会直接废掉。第 4 步的smallest_rectangle1我特意用了它而不是smallest_rectangle2。区别在于smallest_rectangle1返回的是正矩形的四个极值坐标不管你前面有多少个缺陷它只输出一组坐标——也就是把所有缺陷的并集框住。如果你要每个缺陷单独一个框得先select_obj逐个算再union1合并所有框。这里为了代码简洁用了整体框实际项目里按需选。第 5 步的copy_image是关键。因为前面第 2 步已经用paint_region改过ImageClean了如果标注也涂在ImageClean上导出的图里反光区会带着灰块客户看了会问这块是什么。保留原图Image作为标注底图视觉上更干净。6.2 输出前的三点自检导出之前我固定会做三件事做完基本不会返工。自检一确认涂写区域面积占比合理。area_center(ToMark, MarkArea, _, _)拿到面积除以Width * Height。如果占比超过 30%要么是检测参数太松把整张图都框进去了要么是背景被误检先回头查检测别急着出图。自检二确认导出图的宽高和原图一致。get_image_size(ImageOut, W2, H2)然后和Width, Height比。不一致基本就是 domain 的问题回第 5.3 节看。自检三确认颜色对比。如果底图是暖色调纯红框会糊在一起改成纯青或者加白边。这个没有通用公式我就是导出后打开看一眼觉得不跳就换色。做可视化这件事肉眼确认比任何参数都可靠。顺带说一个我常用的小技巧如果一张图上要标多个缺陷并且要编号编号文字用第 4 节的dump_window_image路子加上去最后整体导出。整个链路的顺序是——先涂写所有区域再在窗口里显示涂好的图再叠文字再dump_window_image再一次write_image。不要在没涂完的时候就截图那样文字会被后面的涂写覆盖掉。实际操作下来这套先像素后窗口最后截图的顺序能省掉大量来回调试的时间。我早年习惯先显示后涂写结果经常出现文字被红块盖住、编号位置对不上涂写结果的情况改顺序之后这类问题基本绝迹了。
阅读完成 · 觉得有帮助?
咨询建站