☰
0.2mm 玻璃暗码做到 99.7% 解码率,这套 Halcon 参数调了很久
2026/10/16 9:23:53 网站建设 项目流程

0.2mm 玻璃暗码做到 99.7% 解码率,这套 Halcon 参数调了很久

六月份接了个活儿。玻璃盖板上的激光暗码,油墨层反光,码只有 0.2mm × 0.2mm,视野却要cover 20mm × 20mm。

算下来,码落到图像上也就 50 个像素出头。

什么概念?10 × 10 的 Data Matrix,一个模块大概 5 像素;12 × 12 的,模块只剩 4 像素刚出头。官方推荐至少 3~4 像素,表面再蹭点粉尘,这点余量根本不够看。

客户要求解码率 99.7% 以上。PM 当时还补了一句:这个指标是要写进验收单的。

我当时到现场第一件事不是开 Halcon,是拿尺子量工作距离,算镜头放大率。算完心里基本有数:解码器能拉的提升有限,真正的胜负在光学和打标工艺。但光学的账要慢慢算,客户产线等不起,先调参数,能拉一点是一点。

这篇文章就是那段调试的复盘。不讲光学,只讲 Halcon 2D 码解码参数怎么配,以及两个让我少走很多弯路的速度技巧。


算子外面那 5 个参数

我们项目里把读码封成一个算子,外面只暴露 5 个东西:

参数常用值 / 默认说明
_hvSymbolTypeData Matrix ECC 200、QR、PDF417、Aztec、DotCode 等 10 种码制
_hvModelParameterstandard_recognition/enhanced_recognition/maximum_recognition预设档位
_hvPolaritydark_on_light/light_on_dark极性
_hvTimeout200 ms单次超时
_hvTrainfalse/true训练模式开关

就 5 个。但 HALCON 背后那套 2D 码体系,参数多到能翻十几页文档。我们挑跟现场相关的说。


解码其实就三步

代码骨架大概这样:

// 1. 建模型CreateDataCode2dModel(_hvSymbolType,"default_parameters",_hvModelParameter,&_hvDataCodeHandle);// 2. 设参数SetDataCode2dParam(_hvDataCodeHandle,"polarity",_hvPolarity);SetDataCode2dParam(_hvDataCodeHandle,"timeout",_hvTimeout);// 3. 查找FindDataCode2d(image,&symbolXLD,_hvDataCodeHandle,HTuple(),HTuple(),&resultHandles,&decodedStrings);// 4. 用完清掉ClearDataCode2dModel(_hvDataCodeHandle);

先记住几件事。

模型句柄是有状态的。SetDataCode2dParam每调一次,句柄内部就变一次。多线程共用一个句柄,同步自己负责。

default_parameters是总开关。把它设回standard/enhanced/maximum,所有参数连训练状态一起清零。参数调乱了就靠它回滚。

参数不用一次设死。后面随时能改,当前值用GetDataCode2dParam查,支持哪些参数用QueryDataCode2dParams列。


参数怎么分

官方文档把参数分四类。我按自己的经验重新归了下。

1. 三档预设,其实就是三组默认值

参数standardenhancedmaximum
polaritydark_on_lightanyany
contrast_min301010
module_size_min621
module_size_max20100100
finder_pattern_tolerance(DM)lowlowany
module_gridfixedanyany
small_modules_robustnesslowlowhigh
candidate_selection(DM)defaultextensiveextensive

档位越高,搜索窗口越宽,越烂的码越想啃下来。代价就是慢。

我那项目里一开始同事图省事直接上maximum_recognition,结果解码是能解,但一拍 400ms 多,产线根本跑不动。后来改成enhanced,配合 ROI 和训练,硬是压到 100ms 以内。

图像质量稳定、打标和光源不飘的,standard完全够。波动大再逐级往上。别一上来就maximum,然后回头抱怨节拍不够。

2. 符号尺寸:让它知道你找的东西长什么样

码制关键参数说明
Data Matrix ECC 200symbol_rows_min/max、symbol_cols_min/max、symbol_shape行 [8…144]、列 [10…144],形状rectangle/square/any
QR Codeversion_min/max或symbol_size_min/max版本 1~40 对应 21×21 ~ 177×177 模块
PDF417symbol_cols_min/max、symbol_rows_min/max列 1~30,行 3~90
Aztec Codeformat、symbol_size_min/maxcompact/full_range/rune,11~151 模块
DotCode行偶数、列奇数列 [5…999],行 [4…998]

strict_model默认yes,尺寸对不上的符号直接不读。听着狠,其实是好事。码尺寸固定的话,把symbol_size卡死成一个值,这是最便宜的提速手段,没有之一。

文档里有几个坑容易踩:

  • 改symbol_cols/symbol_rows,symbol_shape会被重置成any;
  • symbol_cols_min比symbol_rows_max还大时,symbol_shape会被强制成rectangle;
  • 参数按传入顺序逐个生效,每步都做一致性检查。顺序不同结果可能不一样。

3. 模块外观:决定读不读得出来

参数含义standard 默认
polarity深底浅码 / 浅底深码 / 任意dark_on_light
mirrored是否镜像any
contrast_min前景背景最小灰度差30
module_size_min/max模块像素尺寸6 / 20
module_gap_min/max模块间隙no/small/bigno
small_modules_robustness极小模块鲁棒性low
slant_max(DM)L 形寻边最大倾角10°(enhanced 30°)
finder_pattern_tolerance(DM)寻边图案缺损容忍low
contrast_tolerance局部对比度波动容忍low
module_grid模块网格固定/可变fixed

module_size_min是耗时第一大户。从 6 降到 1,候选尺度成倍涨。不是真在做微小码,别动它。

模块至少 3~4 像素是官方推荐值。但这话潜台词是:这根本不是参数问题,是选型问题。镜头放大率和相机分辨率一确定,模块像素数就确定了。视野、工作距离、码尺寸,这笔账在选型阶段就得算清楚。算不清楚,后面调参数全是白费劲。

module_gap跟打标工艺绑定。激光打金属容易晕开,模块比理论瘦,module_gap得放开到small甚至big。如果模块退化成孤立小点,先上GrayErosionShape/GrayDilationShape做形态学,别硬刚解码参数。

4. 行为参数:管节拍和内存

参数含义默认
timeout超时 ms,false或 ≤0 关闭false
abort从其他线程中止当前解码true
strict_model尺寸不符是否拒读yes
strict_quiet_zone静区不足是否拒读no
persistence中间结果持久级别0
discard_undecoded_candidates丢弃未解码候选省内存no
string_encoding解码字符串编码latin1
candidate_selection(DM/DotCode)候选区域生成策略default
decoding_scheme(DM)default/raw原始码字default

中文是经典坑。默认latin1,码里编了 UTF-8 中文,解出来一定乱码。string_encoding显式设utf8。

timeout 精度大概 10ms,跟机器性能、图像尺寸、timer_mode都有关。设 200ms 不代表真给 200ms。算节拍时留点余量。


训练模式:训一次,后面省很多事

我那项目里,标准模式死活解不出来,开训练才过。

代码里我们用的是两段式 fallback:

// 第一段:正常模式,受 timeout 保护HalconCpp::FindDataCode2d(h0ImageReduce,&hSymbolXLD,_hvDataCodeHandle,HTuple(),HTuple(),&hResultHandles,&hDecodedStrings);// 第二段:失败且开了训练才走if(hDecodedStrings.Length()==0&&_hvTrain=="true"){HalconCpp::FindDataCode2d(h0ImageReduce,&hSymbolXLD,_hvDataCodeHandle,"train","all",&hResultHandles,&hDecodedStrings);}

train设all时,Halcon 不按预设窗口硬搜。它放宽搜索,找到符号后反推参数,把模块尺寸、对比度、极性、间隙这些外观特征学进模型。

预设参数其实就是在猜。standard赌你模块不小于 6 像素、对比度大于 30、深码浅底。错一条就读不出。现场图像什么样,只有解出来才知道。

训练模式就是拿一张真实样本,让 Halcon 自己把参数调对。打标深度漂了、光源老化了,训好的模型跟得上。用GetDataCode2dParam查"trained",能看到哪些参数进了训练态。

坑没少踩,说几个印象深的。

训练模式下 timeout 被忽略。文档原话:“the timeout is ignored if FindDataCode2d runs in training mode.” 训练等于一次全量放宽搜索,慢起来没谱。所以第一段正常模式必须保留,训练只当兜底。

训练结果只活在句柄里。进程一退全没。必须落盘:

// 序列化HalconCpp::SerializeDataCode2dModel(_hvDataCodeHandle,&hvSerializeItem);HalconCpp::FwriteSerializedItem(hvFileHandle,hvSerializeItem);// 反序列化HalconCpp::FreadSerializedItem(hvFileHandle,&hvSerializeItem);HalconCpp::DeserializeDataCode2dModel(hvSerializeItem,&_hvDataCodeHandle);

我习惯这样:文件存在就反序列化加载;不存在才新建。训练一次,后面直接加载。

别每拍都训练。调试阶段或者离线工具训好、存盘,产线只跑正常解码。每拍都训等于每拍都埋一颗不受超时保护的慢扫描炸弹。产线配置里_hvTrain就该是false。

训练流程我一般这么走:先抓一张有代表性的现场图,不是实验室里那种干干净净的标准图;正常模式先跑,跑不通再开train;训成功之后立刻SerializeDataCode2dModel落盘;再用GetDataCode2dParam复核一遍参数,别把废图特征学进去;最后上线把train关掉,靠落盘模型和timeout保节拍。


缩小解码区域:最划算的一招

这是速度提升里最划算的一招。

// 取第 i 个 ROIHalconCpp::SelectObj(_hoRoi,&hTempROi,i+1);// ROI 仿射跟随if(_bNeedTransRoi){HObject transObj;AffineTransRegion(hTempROi,&transObj,_hHomMatRoi,"nearest_neighbor");HalconCpp::ReduceDomain(_hoImage,transObj,&h0ImageReduce);}else{HalconCpp::ReduceDomain(_hoImage,hTempROi,&h0ImageReduce);}// 只在 domain 内解码FindDataCode2d(h0ImageReduce,...);

ReduceDomain不搬像素,只改定义域。算法只在 domain 里跑。500 万像素整图,码只占 300×300 一角,整图搜和 ROI 搜,耗时差着数量级。

官方文档末尾也认了,FindDataCode2d的运行时间主要看三样:搜索区域大小、module_size_min、符号尺寸限制宽窄。这三样里,缩区域是唯一不碰鲁棒性、纯赚速度的操作。收窄module_size和symbol_size也快,但那是拿泛化换的,来个尺寸不一样的码就抓瞎。

还有个隐藏好处。区域小,误读也少。产线图像里经常有看着像码的东西,铭牌、螺钉阵列、散热孔。整图搜索时参数一松,指不定就把哪个给"解"出来了。锁死在码该在的位置,等于用空间信息做硬过滤。

码可能在多个位置,怎么办

多工位、来料方向不定的场景,一个 ROI 罩不住。并行解:

std::vector<std::future<bool>>futureList;std::atomic<bool>g_stop(false);for(inti=0;i<nnn;i++){futureList.emplace_back(std::async(std::launch::async,[this,i,&g_stop]()->bool{if(g_stop)returnfalse;// SelectObj → ReduceDomain → FindDataCode2dif(hDecodedStrings.Length()>0){_hvDecodedDataStrings=hDecodedStrings;_hoSymbolXLDs=hSymbolXLD;g_stop.store(true);returntrue;}returnfalse;}));}

std::atomic<bool>做先到先得。哪个 ROI 先解出来,别的线程看到标志就撤,不占 CPU。

模型创建和参数设置留在主线程,只做一次。这不光是快慢问题,是线程安全要求:SetDataCode2dParam会改句柄状态,文档写明跨线程必须同步。工作线程只做ReduceDomain + FindDataCode2d,这两个是 reentrant 的。

要从外面强行中止解码,比如上位机急停,用SetDataCode2dParam(handle, "abort", ...)。文档里说这是唯一一个不用额外同步就能从其他线程发起的操作。

位置会漂的,ROI 得跟着走。_bNeedTransRoi + AffineTransRegion就是干这个:先用定位拿到工件位姿矩阵_hHomMatRoi,示教时画的 ROI 跟着变换到当前位置。画一次,后面自动跟。


上线前我习惯过一遍的清单

最后把散在代码和文档里的东西汇总一下,上线前逐条过:

#检查项原因
1模型创建 1 次,参数设置 1 次,都在主线程句柄有状态,跨线程修改需同步
2在线timeout必设保节拍;训练模式下失效
3训练成功立即SerializeDataCode2dModel落盘训练结果随进程消失
4产线配置train = false训练慢且不受 timeout 保护
5解码前ReduceDomain到最小必要区域运行时间三大因素之首
6中文内容显式设string_encoding = utf8默认latin1会乱码
7码尺寸固定时卡窄symbol_size/version无损提速 + 防误读
8长跑任务关注persistence/discard_undecoded_candidates控制模型内存增长
9急停用abort参数跨线程中止唯一线程安全的中止通道
10析构时ClearDataCode2dModel释放句柄防 Halcon 句柄泄漏

流程图我画了一张,放下面:

两个技巧是互补的。缩区域把每拍成本压到底,训练把"到底读不读得出来"的适配成本一次付清、离线付清。一个负责快,一个负责稳,合一起才是能上产线的方案。


写在最后

参数看着多,理清楚就四条线:档位预设管全局,尺寸约束管搜索,外观参数管匹配,行为参数管节拍。

但老实说,解码失败的第一现场,十有八九不在参数表里。

开头那个玻璃暗码,50 像素的码,模块就 4 像素出头,正好卡在生死线上。粉尘再吃掉一点,参数调出花来也救不回来。像素和毫米之间这笔账,才是读码稳定的地基。

我后面专门写了一篇光学选型的笔记:0.2mm 的码怎么在 20mm 视野里活下来,镜头放大率、相机分辨率、工作距离到底怎么配。准备也发出来,感兴趣的可以留意一下。

「像素与毫米」,机器视觉 × 精密加工的工程实录。产线上踩出来的坑,一期填一个。

你那边遇到过什么读码玄学?评论区聊聊,下期选题就从里面挑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询