news 2026/10/1 19:36:53

HALCON涂写:paint_region与overpaint_region详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HALCON涂写:paint_region与overpaint_region详解

1. 先把 HALCON 涂写的底层逻辑捋清楚

1.1 图像涂写和区域涂写,差的不只是一个动词

搞机器视觉的朋友,尤其是从 OpenCV 那套转到 HALCON 的,最开始都会被一个概念绊一下:HALCON 图像、区域涂写这件事,被拆成了两套东西——一套往图像上刷,一套往区域上刷,而这两套东西背后又各有一套paint和overpaint的变体,再加上draw_*系列、set_draw、set_color这些窗口侧的显示控制,新手很容易一锅粥,写出来的代码不是报错就是"涂了半天看不见"。

我这次就是想把这一堆东西一次性捋清楚。所谓涂写,说白了就是在既有图像的基础上,用一块区域去刷颜色、刷灰度值,或者反过来,用一整张灰度图去覆盖另一张图的某个域,最终得到一张修改过的图。它能干的事其实很实在:缺陷标注、数据集合成、检测结果叠加、掩膜生成、文字水印、标签图输出。不管你是在 HDevelop 里调算法原型,还是在 C#、C++、Qt 里把这套东西嵌到产线软件里,这部分内容都绕不开。

想讲明白涂写,得先讲清楚 HALCON 图像对象长什么样。HALCON 的图像不是一块裸内存,它带着三样东西:像素数据本体、像素类型(byte、int1、uint2、int4、real、direction、complex 这些),以及最重要的 domain。domain 你可以理解成"这张图哪块地方算数"。一张 640×480 的图,像素缓冲区永远是 640×480,但 domain 可能只有其中一块矩形,也可能是任意形状的连通区域。

理解了 domain,就能理解为什么涂写会分成两类。一类是改变像素值的图像涂写,比如paint_gray、overpaint_gray,它操作的对象是像素;另一类是往 domain 对应的位置填值的区域涂写,比如paint_region、overpaint_region,输入是一块 Region,输出图像里 Region 覆盖的地方被刷成指定灰度,其他位置像素原封不动,但 domain 会变成"原 domain 并上这块 Region"。

这个区别在实际写代码时非常关键。我见过太多人拿paint_region去做抠图,结果发现输出图像里区域外那一圈虽然视觉上看不见了,其实像素值还在,只是不在 domain 里,一写盘或者一转换就原形毕露。write_image落盘的时候是不保留 domain 形状的,存的是整个像素矩阵,所以涂写时"被 domain 藏起来"的内容会全部回来。做标注图输出的时候,这一点必须心里有数。

1.2 HALCON 的句柄共享:为什么 overpaint 是个"危险"算子

HALCON 的变量在底层是带引用计数的句柄。你写Image2 := Image1,两个变量指向同一块内存,并没有真的复制。这时候如果你对Image2用overpaint_region,HALCON 会检测到这块内存被两个变量共享,于是内部做一次写时复制,把Image2指向新内存,Image1保持原样。

听起来很安全对吧?但麻烦在于这个复制是隐式的、不可见的,图像大的时候会带来一次意料之外的内存拷贝和时间开销。更麻烦的是在 C# 或者 C++ 里用HObject的时候,它本身就是值语义加引用计数的混合体,如果你把一个HObject存进列表、传给别的函数,再拿回来 overpaint,脑子不清楚的时候很容易出现"我以为改写的是 A,结果 A 没变 B 变了"的情况。这种 bug 特别难查,因为单步调试的时候一切正常,只有在多线程或者对象被缓存之后才暴露。

我自己的习惯是:只做展示或者中间结果叠加时,一律用paint_*系列带独立输出的版本;只有确定这张图用完就扔、且没有别的引用时,才考虑overpaint_*。省下的那点内存,抵不上一次熬夜 debug。

1.3 三类算子怎么选:先看一张对照表

把这些算子按行为摆在一起,选型就清楚多了。

算子输入输出是否改原图典型用途
paint_regionRegion, Image新 Image否区域染色、掩膜生成
overpaint_regionImage(可写), Region无是循环内叠加、省内存
paint_grayImageSource, GrayImage新 Image否用灰度图当颜料刷
overpaint_grayImage(可写), GrayImage无是图像拼接、ROI 替换
paint_xldXLD, Image新 Image否轮廓叠加到图上
gen_image_const—新 Image—造空画布
gen_image_protoImage新 Image—按原图尺寸类型造底板

提示:overpaint_*系列在 HALCON 文档里被标注为"会修改输入对象",在多线程环境里如果同一个 HObject 被多个线程共享,务必先 clone 再操作。

2. paint_region 与 overpaint_region 的参数全拆解

2.1 Grayval 能给什么值,以及一个被低估的特性

Grayval可以是一个数,也可以是一个数组。给单个数,Region 就被刷成同一个值;给数组,情况就有意思了——如果传入的是多个 region,那么每个 region 按顺序取一个值。这个特性很多人不知道,非常好用。比如你threshold出来一堆区域,想给每个缺陷一个不同灰度方便肉眼区分,直接传一个和区域数等长的数组就行,不需要写 for 循环一个个paint。

这里有个细节要留意:如果一个 region 对象内部其实包含多个连通域,建议先用connection拆成多个 region 再传进去,语义更明确,也避免长度对不上。我踩过一次坑,threshold出来的 region 里藏着三个小子块,我以为是一个,结果传了两元素数组上去直接报参数错误,排查了十几分钟才反应过来是连通域数量的问题。

灰度值的类型匹配也要留心。byte 图像里给 300,HALCON 会做饱和处理变成 255,不报错;real 图像里给 0.5 完全正常;但如果你把 128.7 丢给 byte 图,它会四舍五入成 129。做亚像素灰度插值时别指望 byte 图保留小数,该转 real 就转 real,convert_image_type之后涂完再转回来。这也是"halcon 转整型实数"这类问题的高频来源,很多人涂写结果差一个灰度级,最后查出来就是类型转换的四舍五入。

2.2 Type 参数:fill 和 margin 的边界行为

Type只有两个取值。'fill'是把区域内部全部填上;'margin'只画边界。实际跑下来,'margin'输出的通常是一条单像素宽的轮廓线,想加粗就得先对 Region 做dilation_rectangle1或者dilation_circle把区域撑开,再传'fill',效果反而更可控。

性能上'fill'一般比'margin'快,因为填充可以直接按行程编码批量写,而边界需要先算轮廓。如果你只是想在图上高亮显示轮廓,与其paint_region(..., 'margin'),不如直接用disp_obj显示 region 配合set_line_width,显示阶段加粗比像素阶段加粗便宜得多。

还有一个容易踩的点:'margin'画出来的边界可能不闭合。当区域在图像边缘被裁掉时,边界会沿着图像边沿走,看起来像是缺了一段。这不是 bug,是区域被 clip 的结果,做视觉检查时别误判。

2.3 和 domain 的联动:涂完图为什么"变小"了

paint_region的输出图像,其 domain 是输入图像 domain 与 Region 的并集,并且会被裁剪到图像的像素矩阵范围内。HALCON 的 Region 是允许坐标超出图像边界的,paint_region会把超出部分自动裁掉,不报错。这一点在做大范围掩膜的时候很省事,不用自己先 clip 一遍。

但并集这个语义会带来一个副作用:如果你输入的图像本身 domain 很窄(比如上游刚做过reduce_domain),paint_region的输出 domain 会变成窄 domain 加 Region,区域外那块依然不算数。后面接write_image的时候,那些不在 domain 里的像素虽然存在,但你会发现写出来的图跟你想的不一样。

实操建议是:涂写之前先想清楚这张图后面是要"继续参与计算"还是"只用来展示"。要参与计算,保留 domain 是有意义的,后续算子会自动忽略无关区域,速度更快;只用来展示或者落盘,就在涂写之前先full_domain把 domain 拉满,避免意外。

2.4 一个高频误用:拿 paint_region 当 reduce_domain

这两个算子名字看起来都跟"区域"有关,实际用途完全不同。reduce_domain是把图像的 domain 缩小到指定区域,像素值一个不动;paint_region是往区域上刷灰度值,同时 domain 变成并集。

我见过有人想"只看板子上的缺陷区域",写了paint_region,结果发现后面接的threshold结果全变了——因为那些被刷成 255 的区域直接进了阈值。要限制处理范围就用reduce_domain,要染色才用paint_region,别混。

3. 图像涂写:paint_gray、overpaint_gray 与写文字

3.1 paint_gray 的"颜料图"机制

paint_gray(ImageSource, GrayImage, ImageDestination)的作用和paint_region相似,但颜料换成了GrayImage的像素值。它的涂写范围不是由 region 决定的,而是由GrayImage的 domain 决定。输出图像的 domain 是ImageSource的 domain 和GrayImage的 domain 的并集。

这个机制其实把"区域涂写"和"灰度涂写"统一起来了。你完全可以先用reduce_domain把GrayImage限制在某块不规则区域上,再paint_gray过去,效果等同于"用一张形状任意的贴纸去盖"。做多相机拼接、零件模板替换的时候非常顺手。

要注意两者像素矩阵尺寸必须一致,尺寸不同会直接报错。另外别指望它做缩放对齐,HALCON 的图像算子基本都不做隐式几何变换,要缩放得自己先zoom_image_factor或者affine_trans_image。

3.2 overpaint_gray:图像拼接最省事的办法

overpaint_gray(ImageDestination, ImageSource)是把ImageSource的 domain 内的像素覆盖到目标图上。做图像拼接的时候,最典型的用法是准备一张大底板,然后把各个小块按位置盖上去。

这里有个实操技巧:光盖上去边缘会有一条明显的接缝,因为两次成像的亮度往往不一致。我一般的做法是先对小块做一次scale_image或者在重叠区做加权融合,再overpaint_gray,缝就淡很多。同理,如果你只是想做"背景替换",用paint_gray加一张纯色常量图反而更干脆。

3.3 在图像上写文字:dump_window 是最稳的路

"halcon 在图片写入文字"是个被问了很多年的问题。真相有点扫兴:HALCON 本身没有往图像内存里直接写字的算子,所有write_string、disp_text都是往窗口输出的。所以标准流程是三步:先在窗口上把图像显示出来,再用set_tposition加write_string写字,最后把窗口内容抓成图像。

* 1. 读图并取尺寸 read_image (Image, 'board.jpg') get_image_size (Image, Width, Height) * 2. 开一个和图像等尺寸的窗口,避免缩放 dev_close_window () dev_open_window (0, 0, Width, Height, 'black', WindowHandle) dev_display (Image) * 3. 设置字体、颜色、位置,写字 set_font (WindowHandle, 'Consolas-Bold-32') set_color (WindowHandle, 'red') set_tposition (WindowHandle, 40, 60) write_string (WindowHandle, 'OK 0.93') new_line (WindowHandle) write_string (WindowHandle, 'Lot A230915') * 4. 把窗口内容抓成图像 dump_window_image (ImageResult, WindowHandle)

这段流程我用了很多年,坑主要集中在四处。

  • 窗口尺寸必须和图像像素尺寸完全一致,差一点dump出来就被缩放,文字位置全乱。
  • 之前调用过dev_set_part会改变图像到窗口的映射,dump 出来的坐标全部偏移。
  • 窗口被遮挡或者最小化时,有些系统上 dump 出来是黑的,产线软件跑后台服务时尤其明显。
  • 新版本 HALCON 里dump_window_image已被dump_window取代,老项目升级会提示算子未定义,改个名字就行,但要注意新算子多了一个输出格式参数。

如果真的要在无界面环境里写文字,我的建议是别在 HALCON 里折腾,直接在 C# 侧用System.Drawing.Graphics.DrawString往Bitmap上画,画完再转成HObject喂给 HALCON。这条路更可控,不用真的开窗口,字体渲染也更自由。

3.4 多通道涂写:想刷彩色就得拆通道

byte 图像有单通道和三通道两种。三通道图像像素排列是交错的 RGB,而paint_region只能刷单值——往彩色图上直接刷,出来的就是灰色。想刷彩色,必须拆通道分别刷,再合回去。

decompose3 (ImageRGB, R, G, B) paint_region (Region, R, R2, 255, 'fill') paint_region (Region, G, G2, 0, 'fill') paint_region (Region, B, B2, 0, 'fill') compose3 (R2, G2, B2, ImagePainted)

compose3要求三张图尺寸一致,domain 不一致时会取交集,所以三张通道图的 domain 最好统一。刷什么颜色,可以直接给 RGB 三元组,也可以用 HSV 的思路来选。HALCON 里trans_from_rgb是 RGB 转 HSV,反过来用trans_to_rgb。实际操作中我更喜欢先在心里定 HSV,比如"亮橙色"就是 H=30、S=1.0、V=1.0,转成 RGB 大约是 (255, 128, 0),比硬猜三元组靠谱得多。

3.5 造画布:gen_image_const 和 gen_image_proto 的区别

gen_image_const(Image, Type, Width, Height)造一张全零的常量图,domain 是整幅;gen_image_proto(Image, ImageCleared, Grayval)按已有图像的尺寸和类型造底板,所有像素设成指定值。

做标注底板时gen_image_proto更省事,因为它自动继承类型和尺寸,不用手写参数,也不怕类型写错。但有个内部机制要知道:gen_image_const生成的图在 HALCON 内部是用常量表示的,并不占真实内存,你拿它当背景直接用是零成本的;一旦对它做overpaint,才会触发真正的内存分配。所以在循环里反复gen_image_const再overpaint,内存开销比想象中大,能复用就复用。

4. 交互涂写:draw 系列与显示控制

4.1 draw_region / draw_rectangle1 / draw_circle 到底怎么用

draw_*系列是一组交互式算子,运行时会阻塞,等你在窗口上用鼠标画完、右键确认才返回。

dev_open_window (0, 0, 512, 512, 'black', WindowHandle) dev_display (Image) draw_region (Region, WindowHandle) area_center (Region, Area, Row, Column)

常用的一共有这么几个:draw_region自由画任意形状,draw_rectangle1画正矩形,draw_rectangle2画带角度矩形,draw_circle画圆,draw_ellipse画椭圆,draw_polygon画多边形,draw_xld画轮廓。返回值上,draw_rectangle1给对角两点Row1, Column1, Row2, Column2;draw_circle给Row, Column, Radius;draw_ellipse给Row, Column, Phi, Radius1, Radius2。

这些算子最大的限制是必须有真实可见的窗口。产线软件后台跑服务的时候,这些算子会直接卡死在那里等人操作。所以交互涂写只能出现在调试工具、标定工具、标注工具里,正式算法流程里一律用程序生成的区域替代。

4.2 set_draw / set_color / set_line_width 控制的是显示,不是图像

这一节必须单独拎出来讲,因为混淆这里的人太多了。set_color、set_draw、set_line_width、set_font都是控制窗口显示属性的全局状态,影响的是dev_display的输出和 dump 出来的东西,不会改变图像本身的像素。

换句话说,你disp_obj(Region, WindowHandle)之后用set_color把区域显示成红色,图像对象的像素一点没变。如果你只是想看一眼效果,用set_color就够了,没必要真的去paint_region。反过来,如果你要输出一张带颜色的图给别人看,那就必须走paint_region加compose3的路,光靠set_color落盘是没用的。

set_draw(WindowHandle, 'margin')或者'fill'控制的是之后所有disp_obj区域的填充方式,这个设置是粘性的,设一次一直有效,忘了改回来会导致后面所有显示都是填充的。set_color支持颜色名、#RRGGBB十六进制字符串,也支持调色板索引。set_line_width影响的是显示的线宽,也影响paint_region里'margin'的效果吗?不影响,paint_region的边界宽度是内部固定的,想加粗还是得dilation。

5. 实战:一套完整的缺陷标注涂写流程

5.1 场景与整体设计

假设我们有一个钢板表面缺陷检测的项目,算法已经能给出缺陷区域和分类结果,现在需要输出一张标注图给客户看,同时输出一张标签图喂给下游的分割训练。需求拆成三块:原图上用彩色框和文字标出缺陷位置和类别;生成一张单通道标签图,每个缺陷一个不同灰度值;两者都保留原始分辨率。

流程设计上我一般这么排:先读原图并full_domain,然后用threshold加形态学得到缺陷候选区域,再用connection拆连通域、select_shape过滤噪点,接着把缺陷区和分类结果存起来,最后分两路走——一路往彩色图上涂框涂字,一路往单通道图上涂灰度标签。分两路的目的是避免把展示逻辑和训练数据生成逻辑搅在一起,后面改需求的时候不用推倒重来。

5.2 核心代码实现

先做缺陷提取和分类这一步,为了说明问题,这里用阈值加面积过滤模拟。

read_image (Image, 'steel_surface.png') get_image_size (Image, Width, Height) full_domain (Image, ImageFull) * 缺陷提取 rgb1_to_gray (ImageFull, GrayImage) threshold (GrayImage, DarkRegion, 0, 90) connection (DarkRegion, DefectCandidates) select_shape (DefectCandidates, Defects, 'area', 'and', 200, 99999) * 给每个缺陷编个序号,方便后面涂不同灰度 count_obj (Defects, NumDefects)

接下来生成标签图。这里有个提速的关键点:不要写 for 循环挨个paint_region,直接用region_to_label一次生成。

* 生成标签图,每个连通域一个 tag 值,类型用 int4 保留足够动态范围 region_to_label (Defects, LabelImage, 'int4', Width, Height) * 想要可视化的灰度标签图,缩放到 byte min_max_gray (LabelImage, LabelImage, 0, MinV, MaxV, RangeV) scale_image (LabelImage, LabelImageByte, 255.0 / MaxV, 0) convert_image_type (LabelImageByte, LabelImageByte, 'byte')

region_to_label这个算子在实例分割标注的预处理里非常好用,一次调用就把所有连通域编好号,比循环涂写快一个数量级。几十个缺陷的时候差别不大,上千个的时候就是几秒和几十毫秒的差距。

然后是彩色标注图那一路。要涂彩色必须走三通道,先把原图拆开。

decompose3 (ImageFull, R, G, B) * 用蓝色标注缺陷区域 paint_region (Defects, R, R2, 0, 'fill') paint_region (Defects, G, G2, 80, 'fill') paint_region (Defects, B, B2, 255, 'fill') compose3 (R2, G2, B2, ImageMarked) * 在图上写类别文字:借助窗口 dump 的方式 dev_open_window (0, 0, Width, Height, 'black', WindowHandle) dev_display (ImageMarked) set_font (WindowHandle, 'Consolas-Bold-28') set_color (WindowHandle, 'white') area_center (Defects, Areas, Rows, Cols) count_obj (Defects, N) for i := 1 to N by 1 set_tposition (WindowHandle, Rows[i - 1], Cols[i - 1]) write_string (WindowHandle, 'NG-' + i) endfor dump_window_image (ImageAnnotated, WindowHandle)

这段代码里有两个我想特别提的点。一个是area_center返回的是数组,索引从 0 开始,但 HDevelop 的循环变量从 1 开始,所以写Rows[i-1],这个细节新人特别容易搞错,直接写Rows[i]在只有一个缺陷的时候可能不报错,缺陷一多就越界。另一个是窗口尺寸必须严格等于Width, Height,只要不是一比一,文字位置就会整体偏移。

5.3 结果校验与参数计算

校验环节我一般会做三件事:用min_max_gray确认标签图的动态范围合理,用count_obj确认缺陷数量和预期一致,用write_image落盘之后重新read_image回来看一眼标注图有没有被意外缩放。

参数计算上,scale_image的那个系数255.0/MaxV是这么来的:标签图的最大值等于连通域数量,要映射到 byte 的 0 到 255 区间,缩放因子就是255/NumDefects。如果缺陷数超过 255,byte 就会溢出回绕,这时候必须用uint2或者int4保存,中途不能转 byte。这一点在缺陷密集的钢板上真的会碰到,我第一次遇到的时候标签图出现了诡异的灰度跳变,查了半天才发现是溢出。

另外region_to_label的输出类型选int4还是uint2也有讲究。uint2上限 65535,一般够用而且省一半内存;int4上限二十多亿,基本不可能碰到,但每像素占四个字节,大图上是实打实的内存压力。我现在的默认选择是uint2,只有在超大批量标注才切int4。

6. 跨语言调用与性能对策

6.1 C# 里调用涂写算子的几个注意点

.NET 侧调用 HALCON 走的是HOperatorSet静态类,方法名和 HDevelop 一一对应。paint_region对应HOperatorSet.PaintRegion(region, image, out imageResult, grayval, "fill")。

有几个和 HDevelop 不一样的地方。第一,HTuple传数组的时候要注意类型,new HTuple(new int[]{0, 80, 255})和new HTuple(0, 80, 255)在某些版本上行为有差异,我一般统一用new HTuple(0, 80, 255),参数个数不定的时候用HTuple数组构造再传。第二,HObject传出参数必须用out,而且用完要Dispose,不释放的话在长时间运行的产线软件里会稳定地涨内存,跑一整夜就爆掉。第三,多线程调用同一个HObject之前先CopyObj一份,别赌 HALCON 的写时复制在所有场景下都救得了你。

如果你是用 Qt 做界面再嵌 HALCON,还有个额外的坑:HALCON 的窗口句柄是基于原生窗口的,在 Qt 的QWidget上开 HALCON 窗口要用set_window_attr指定'border_width'之类,还得处理 resize 事件,不然窗口跟着界面缩放之后坐标全乱。我一般的做法是干脆不用 HALCON 的窗口,所有显示交给 Qt 的QImage,HALCON 只负责算,算完把HImage的指针给 Qt 显示。这样显示和算法彻底解耦,代价是每次显示都要拷一遍内存,但换来的是可控性。

6.2 大数据量下的性能对策

涂写本身的性能其实很少成为瓶颈,但它引发的问题经常被误判成涂写慢。常见的三种情况我列一下。

第一种是隐式拷贝。前面说过,overpaint_*遇到共享句柄会触发复制,一张 4K 的 byte 图是 800 万像素,三通道是 2400 万字节,一次复制就是二十多兆的内存操作。如果你在循环里反复触发,时间全耗在拷贝上。对策是提前把引用理清楚,或者干脆改用带输出的paint_*。

第二种是循环里反复gen_image_const。前面提过,常量图本身不占内存,但overpaint之后就会实体化,循环一千次就是一千次实体化。对策是造一次底板,循环里用paint_region输出到新的变量,最后统一合成。

第三种是用paint_region批量涂灰度,而不是region_to_label。前面算过,上千个连通域的时候差距是几十倍。判断标准很简单:只是要区分不同连通域,用region_to_label;要涂固定颜色,用paint_region传单值;要按业务规则涂一组特定值,才需要传数组或循环。

6.3 灰度值处理的小细节

涂写之后经常要接灰度拉伸,因为刷进去的值和原图的值域可能不匹配。HALCON 里的选择有几个:scale_image按线性公式g' = g * Mult + Add变换,参数自己算;scale_image_max自动拉伸到 0 到 255,最省事;stretch_image按百分位裁剪后再拉伸,抗噪点更好。

要注意scale_image_max在小图上作用在 domain 内,如果 domain 很小、里面又恰好只有一个灰度值,拉伸会除零,HALCON 会报警告。我一般会在前面加一句min_max_gray自己判断范围,范围小于阈值就跳过拉伸。

7. 常见问题排查速查

7.1 涂不上、看不见的三类原因

问得最多的就是"我涂了,但是看不见"。按我的排查顺序捋一遍,九成能命中。

第一类,涂到 domain 外面了。paint_region的输出 domain 是并集,看起来应该是扩大了才对,但如果你涂之前做过reduce_domain,或者涂的区域和 domain 完全不重叠,输出图的 domain 就是你涂的区域,这时候如果直接dev_display却盯着原来那块看,自然什么都看不到。对策是先full_domain再说。

第二类,显示属性和像素搞混了。你想看红色,用set_color设了,那是显示层面的,图像本身没变;你要落盘的图当然还是原色。这个前面专门讲过,属于最经典的混淆。

第三类,Grayval给错了或者类型不匹配。往 byte 图上给 0.5,直接舍成 0,如果原图那块本来也是黑色,你就以为没涂上。往 real 图上给 1 和给 1.0 是两回事,前者是整数一,后者是一点零,在某些插值场景下结果不一样。

7.2 报错速查表

报错信息关键词大概率原因处理方式
Wrong number of valuesGrayval数组长度和 region 数对不上用count_obj数一下,或先用connection拆开
Wrong image type灰度值和图像类型不兼容convert_image_type先统一类型
Image size mismatchpaint_gray两张图尺寸不同检查是否做过缩放或裁剪
Object is read-only对共享句柄做overpaint先copy_obj再操作
Operator not defined新版本算子改名,如dump_window_image换成dump_window
Window handle invalid窗口已关闭仍在使用检查窗口生命周期,别在窗口关闭后 dump

7.3 几条我自己总结的避坑经验

写文字到图像的时候,字体大小和图像分辨率一定要匹配。我见过把 512×512 图上用的 12 号字体搬到 4000×3000 的图上,dump 出来字小得跟芝麻一样,其实是像素密度差了八倍。经验公式是字号大致取图像短边的百分之一到百分之二,4000 宽的图取 40 到 80 号,肉眼看着舒服。

用region_to_label生成的标签图,别直接丢给分割网络当 label,还得确认一下 tag 值是不是从 1 开始连续。有些版本上它会跳过一些值,中间断号会让 one-hot 编码出现空类,训练的时候 loss 会莫名其妙地抖。用min_max_gray配合tuple_sort查一遍比较稳。

批量处理的时候,paint_region的输出图像记得在循环结束后统一释放。HALCON 的自动垃圾回收在 .NET 环境里不是实时的,循环里HObject越攒越多,几百张图之后就明显卡顿。手动Dispose虽然啰嗦,但能把内存曲线压平。

8. 一些实战心得

我把这套东西用在标注工具上的时候,最大的体会是:涂写看着简单,真正的复杂度全在"什么时候改像素、什么时候只改显示、什么时候改 domain"这三个判断上。我的判断标准是——要给机器看的,改像素或者改 domain;要给人看的,优先改显示属性。这条线划清了,代码会干净很多,也不会出现"渲染一版、落盘一版、结果对不上"的尴尬。

还有一个关于调试的小技巧分享给你:涂写出了问题,别急着看输出图,先把输入图像的 domain 用get_domain取出来disp_obj一下,再把涂写区域的area_center打出来,两者一对比,是坐标问题还是 domain 问题立刻就清楚了。这个习惯帮我省掉的排查时间,比任何调试工具都多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 19:36:05

深入理解Linux EOF:从文件结束符到heredoc分隔符的完整指南

作为一个常年跟Linux命令行打交道的人&#xff0c;我几乎每天都在和EOF打交道&#xff0c;也几乎每年都要在社区里回答几回和cat << EOF、unexpected EOF相关的问题。很多人对EOF的理解停留在“文件结束符”五个字上&#xff0c;但真正到了写Shell脚本、拼接多行输入、排…

作者头像 李华
网站建设 2026/10/1 19:35:22

Jev 模型与 TypeSafe SDK:AI 编程的工程化接入指南

1. Jev 到底是什么&#xff1a;从热搜词里还原它的真实面目 最近一段时间&#xff0c;技术圈里“Jev”这个词的出现频率突然高了起来&#xff0c;很多人第一次看到它是在各种 AI 编程工具的讨论里&#xff0c;比如有人问“Jev 在 Codex 里怎么用”“Jev 模型官网地址是什么”“…

作者头像 李华
网站建设 2026/10/1 19:35:02

Vue + WebApi 前后端分离开发完整实战:从接口联调到部署

前后端分离开发这几年已经是绝对的主流了&#xff0c;Vue 配合后端 WebApi 接口做数据交互&#xff0c;更是每个前端开发者绕不开的基本功。这个例子虽然看着简单——“写个页面调个接口把数据渲染出来”——但真正从零走一遍&#xff0c;你会发现里面有环境配置、接口约定、跨…

作者头像 李华
网站建设 2026/10/1 19:34:53

杂草识别数据集与YOLO训练全流程指南

简介&#xff1a;面向计算机视觉与智慧农业研究者的专业农业杂草识别数据集&#xff0c;可支撑稗草、马唐等多类常见恶性杂草的分类模型训练。数据包含近2000张真实农田与作物环境下的杂草高清图像&#xff0c;覆盖不同生长期、多种伴生场景与复杂田间背景&#xff0c;并经植保…

作者头像 李华
网站建设 2026/10/1 19:34:39

线性回归从原理到代码:机器学习入门必须吃透的基础模型

从入门机器学习的第一天起&#xff0c;线性回归大概率就是你面前的那道开胃菜。它既朴素又可靠&#xff1a;给一组特征&#xff08;比如房屋面积、楼层数、地段评分&#xff09;&#xff0c;去预测一个连续的输出变量&#xff08;比如房价&#xff09;&#xff0c;本质上就是找…

作者头像 李华
网站建设 2026/10/1 19:33:44

达梦DM8上实现LBS附近门店查询:GeoHash分桶与Haversine精算实战

先说个真实经历。前两年接了一个偏政企方向的项目&#xff0c;数据库必须换到达梦DM8&#xff0c;业务里却有一个躲不掉的位置服务模块——“附近门店”查询。当时第一反应是上网找资料&#xff0c;结果铺天盖地都是MySQL、PostgreSQL做LBS的教程&#xff0c;达梦相关的要么语焉…

作者头像 李华