17 综合实战 五个作业题与判定逻辑#
一句话总结
这五份练习把前面所有主线串起来了:Blob(骰子点数、齿轮缺陷)、模板匹配定位 + 2D 计量(传感器长宽)、条码 + OCR 比对(一维码ocr)、二维码(QR码识别)。 算子本身在 [[03 二值化与连通域分割]]、[[14 模板匹配 形状匹配]]、[[15 测量模型 2D Metrology]]、[[16 读码与 OCR]] 都讲过了,所以本篇的重点换成两件事: ① 检测结果怎么落成 OK/NG 判定;② 逐份挑出源码里真实存在的 bug(五份里四份有错,而且错法高度雷同)。
源文件(本轮新增): -
note/综合练习/骰子点数.hdev-note/综合练习/齿轮缺陷.hdev-note/综合练习/传感器长宽.hdev-note/综合练习/一维码ocr.hdev(同目录有检测要求.txt) -note/综合练习/QR码识别.hdev素材:
note/综合练习/img/1~7.jpg(骰子)、img1/1~3.bmp(齿轮)、QR码/1~6.bmp、一维码+ocr/1~3.bmp、传感器/1.bmp(单张 5.7 MB,11 张共 63 MB,库里只留了 1 张样本,其余仍在源目录)
该图为
骰子_1.png(934×698):暗背景 + 亮骰子 + 骰子上的暗点。threshold (GrayImage, Region, 0, 128)选中的其实是"所有暗区"(背景 和 点都在里面),全靠select_shape的面积上限把大块背景剔掉。
一、五个任务总览#
| 任务 | 素材 | 链路 | 判定依据 |
|---|---|---|---|
| 骰子点数 | img/1~7.jpg |
rgb1_to_gray → threshold(0,128) → connection → select_shape(area) → count_obj |
数出来的个数就是点数 |
| 齿轮缺陷 | img1/1~3.bmp |
threshold(185,255) → connection → select_shape(area) → area_center |
与首图的面积差是否为 0 |
| 传感器长宽 | 传感器/1~11.bmp |
gauss_filter → emphasize → 模板匹配定位 → 4 条线 2D 计量 → distance_pl |
长 / 宽直接当结果输出 |
| 一维码 + OCR | 一维码+ocr/1~3.bmp |
find_bar_code + 模板匹配定位 + find_text |
条码串 == OCR 串? |
| QR 码识别 | QR码/1~6.bmp |
create_data_code_2d_model → find_data_code_2d |
解出内容即完成 |
二、判定逻辑:OK/NG 怎么写才稳#
这五份里真正"做判定"的有两份,写法都值得说道:
2.1 齿轮:if (Area - Area1 == 0) —— 浮点相等是雷#
area_center (SelectedRegions, Area, Row, Column) * 首图(基准)
...
area_center (SelectedRegions1, Area1, Row1, Column1) * 当前图
if (Area - Area1 == 0)
dev_disp_text ('OK', 'window', 'top', 'center', 'green', [], [])
else
dev_disp_text ('NG', 'window', 'top', 'center', 'red', [], [])
endif
不要用 == 0 比浮点
area_center 返回的是浮点面积。光照抖一个灰度,阈值边缘就会多进/少进几个像素,面积差几乎不可能恰好为 0。
这么写在"像素完全一样的图"上能过,一上真机就是全 NG。
正确写法是给容差 + 比值双保险:
2.2 一维码 + OCR:if (num == DecodedDataStrings) —— 字符串比对#
num := ''
for Index1 := 0 to |ResultValue|-1 by 1
num := num + ResultValue[Index1]
endfor
if (num == DecodedDataStrings)
dev_disp_text ('OK', ...)
else
dev_disp_text ('NG', ...)
endif
ResultValue 是 get_text_result (…, 'class', …) 返回的逐字符元组,所以要先拼成一整串再跟条码串比。
这个思路是对的(同一信息用两条独立链路读出来,互相印证),但要注意 读码与 OCR 一维码 二维码 字符识别 里那条坑:条码正反都能读,DecodedDataStrings 可能是"两个串用逗号分隔",比对前最好先 tuple_regexp_select 或按逗号切一下。
2.3 通用判定模板#
| 判据类型 | 怎么写 | 适合 |
|---|---|---|
| 计数 | count_obj 后比数值 |
骰子点数、缺件检测 |
| 尺寸 | abs(实测 − 标称) < 容差 |
长宽高、直径 |
| 面积 | abs(Area − AreaRef) < 容差 或 Ratio < 百分比 |
缺料 / 毛刺 |
| 交叉验证 | 两条独立链路结果是否一致 | 条码 vs OCR(本轮一维码ocr) |
| 存在性 | |结果元组| > 0 |
读没读到码、找没找到目标 |
判定前先确认\"结果非空\"
find_bar_code / find_data_code_2d / find_shape_model 读不到时返回空元组,
此时 Parameter[0]、DecodedDataStrings 之类的下标访问会直接报错。
工程写法是先判断 if (|DecodedDataStrings| > 0),再取内容。
三、任务 1:骰子点数#
list_files ('./img', 'files', Files)
dev_close_window ()
dev_open_window (300, 400, 934, 698, 'black', WindowHandle)
for Index := 0 to |Files|-1 by 1
read_image (Image, Files[Index])
rgb1_to_gray (Image, GrayImage)
threshold (GrayImage, Region, 0, 128)
connection (Region, ConnectedRegions)
select_shape (ConnectedRegions, SelectedRegions, 'area', 'or', 100, 3424.66)
dev_set_color ('red')
dev_clear_window ()
dev_display (GrayImage)
dev_display (SelectedRegions)
count_obj (SelectedRegions, Number)
set_display_font (WindowHandle, 25, 'mono', 'true', 'false')
dev_disp_text ('骰子点数' + Number, 'window', 'top', 'center', 'black', [], [])
endfor
| 行 | 说明 |
|---|---|
rgb1_to_gray |
素材是彩色 jpg,先转灰度(也可 rgb3_to_gray,或挑一个对比度最好的通道) |
threshold (GrayImage, Region, 0, 128) |
取暗区。暗的既有背景、也有骰子上的点 |
select_shape (…, 'area', 'or', 100, 3424.66) |
靠面积上下限把大块背景(远大于 3424)和芝麻噪点(小于 100)两头切掉,剩下的就是点 |
count_obj (SelectedRegions, Number) |
剩下几个区域 = 几个点 |
'or' 只有一个特征时等价于 'and'
select_shape 的 Operation 只在传多个特征时才有区别('and' = 全部满足,'or' = 满足任一)。这里只有一个 'area',写 'or' 和 'and' 效果一样。
那个 3424.66 是怎么来的
这类"看着很怪的小数"必然是用 HALCON 的特征检测(特征直方图)在样图上量出来的,不是拍脑袋写的。
复现方法:在 HDevelop 里打开 Features > Feature Histogram,选 area,拖出目标点所在的那一段区间,把端点值抄下来。
换一批图(分辨率/拍摄距离变了)必须重新标定,因为 area 是像素面积、跟分辨率平方成正比。
四、任务 2:齿轮缺陷#
list_files ('./img1', 'files', Files)
read_image (Image, Files[0])
get_image_size (Image, Width, Height)
dev_open_window (200, 300, Width*0.8, Height*0.8, 'black', WindowHandle)
dev_display (Image)
threshold (Image, Regions, 185, 255)
connection (Regions, ConnectedRegions)
select_shape (ConnectedRegions, SelectedRegions, 'area', 'and', 89296.3, 102973)
area_center (SelectedRegions, Area, Row, Column) * 基准面积
...
for Index := 0 to |Files|-1 by 1
read_image (Image1, Files[Index])
threshold (Image1, Regions1, 185, 255)
connection (Regions1, ConnectedRegions1)
select_shape (ConnectedRegions1, SelectedRegions1, 'area', 'and', 89296.3, 104000)
area_center (SelectedRegions1, Area1, Row1, Column1)
dev_clear_window ()
dev_display (Image) * ← 这里应该显示 Image1
dev_display (SelectedRegions1)
if (Area - Area1 == 0) ... endif
endfor
思路是以首图为基准、比面积:齿轮是亮目标(threshold 185~255),面积约 9 万像素;缺料/多料会让面积变化。
两处需要注意:
select_shape的上限不一致:基准图用102973,循环里用104000。放宽上限能让"稍微大一点"的齿轮也进入候选,但这会让两张图的筛选条件不同,Area与Area1就不再可比 —— 要么统一,要么干脆用'min'开口。- 见下一节的 bug 汇总:
dev_display (Image)应为dev_display (Image1);== 0应改容差。
五、任务 3:传感器长宽(定位 + 计量,最完整的一份)#
list_files ('./传感器', 'files', Files)
read_image (Image, Files[0])
gauss_filter (Image, ImageGauss, 5)
emphasize (ImageGauss, ImageEmphasize, 14, 14, 3)
get_image_size (Image, Width, Height)
dev_display (ImageEmphasize)
* —— 定位:以首图 ROI 训练形状模型 ——
dev_set_draw ('margin')
draw_rectangle2 (WindowHandle, Row, Column, Phi, Length1, Length2)
gen_rectangle2 (Rectangle, Row, Column, Phi, Length1, Length2)
reduce_domain (Image, Rectangle, ImageReduced)
create_shape_model (ImageReduced, 'auto', -rad(360), rad(360), 'auto', 'auto', \
'use_polarity', 'auto', 'auto', ModelID)
find_shape_model (ImageReduced, ModelID, -rad(360), rad(360), 0.5, 1, 0.5, \
'least_squares', 0, 0.3, Row1, Column1, Angle, Score)
get_shape_model_contours (ModelContours, ModelID, 1)
hom_mat2d_identity (HomMat2DIdentity)
hom_mat2d_rotate (HomMat2DIdentity, Angle, 0, 0, HomMat2DRotate)
hom_mat2d_translate (HomMat2DRotate, Row1, Column1, HomMat2DTranslate)
affine_trans_contour_xld (ModelContours, ContoursAffineTrans, HomMat2DTranslate)
* —— 测量:画 4 条线 ——
draw_line (WindowHandle, Row11, Column11, Row2, Column2)
... (共 4 条)
create_metrology_model (MetrologyHandle)
add_metrology_object_line_measure (MetrologyHandle, Row11, Column11, Row2, Column2, 26, 5, 1, 30, [], [], Index)
add_metrology_object_line_measure (MetrologyHandle, Row12, Column12, Row21, Column21, 26, 5, 1, 30, [], [], Index1)
add_metrology_object_line_measure (MetrologyHandle, Row13, Column13, Row22, Column22, 20, 5, 1, 30, [], [], Index2)
add_metrology_object_line_measure (MetrologyHandle, Row14, Column14, Row23, Column23, 20, 5, 1, 30, [], [], Index3)
set_metrology_model_param (MetrologyHandle, 'reference_system', [Row1, Column1, Angle])
* —— 遍历 ——
for i := 0 to |Files|-1 by 1
read_image (Image1, Files[i])
gauss_filter (Image1, ImageGauss1, 5)
emphasize (ImageGauss1, ImageEmphasize1, 16, 16, 5)
find_shape_model (ImageEmphasize1, ModelID, -rad(360), rad(360), 0.5, 1, 0.5, \
'least_squares', 0, 0.9, Row4, Column4, Angle1, Score1)
hom_mat2d_rotate (HomMat2DIdentity1, Angle1, 0, 0, HomMat2DRotate1)
hom_mat2d_translate (HomMat2DRotate1, Row4, Column4, HomMat2DTranslate1)
affine_trans_contour_xld (ModelContours1, ContoursAffineTrans1, HomMat2DTranslate1)
align_metrology_model (MetrologyHandle, Row4, Column4, Angle1)
apply_metrology_model (ImageEmphasize1, MetrologyHandle)
get_metrology_object_result (MetrologyHandle, 'all', 'all', 'result_type', 'all_param', Parameter)
* 长 = 第 1 条线的终点 到 第 2 条线;宽 = 第 3 条线的终点 到 第 4 条线
distance_pl (Parameter[2], Parameter[3], Parameter[4], Parameter[5], Parameter[6], Parameter[7], Distance)
distance_pl (Parameter[10], Parameter[11], Parameter[12], Parameter[13], Parameter[14], Parameter[15], Distance1)
dev_disp_text ('长:' + Distance + '宽:' + Distance1, 'window', 'top', 'center', 'black', [], [])
endfor
Parameter 下标映射(4 条线,每条 4 个值,按 add_* 顺序拼接):
| 对象 | Parameter 下标 |
含义 |
|---|---|---|
| 第 1 条线 | 0, 1, 2, 3 |
[row_begin, column_begin, row_end, column_end] |
| 第 2 条线 | 4, 5, 6, 7 |
同上 |
| 第 3 条线 | 8, 9, 10, 11 |
同上 |
| 第 4 条线 | 12, 13, 14, 15 |
同上 |
所以 distance_pl (Parameter[2], Parameter[3], Parameter[4..7], Distance) = 第 1 条线的终点 到 第 2 条线的距离 = 长;
distance_pl (Parameter[10], Parameter[11], Parameter[12..15], Distance1) = 第 3 条线的终点 到 第 4 条线的距离 = 宽。
值得学的一招:拿 ImageReduced 当搜索图去\"自检\"模板
第 23 行 find_shape_model (ImageReduced, ModelID, …) 传的不是整图,而是抠好的 ROI。
这样搜索范围被限制在 ROI 内,必然能找到刚刚训练的那个模型 —— 本质上是拿模板自己验证自己,顺便把基准姿态 \((Row1, Column1, Angle)\) 取出来喂给 reference_system。是很实用的小技巧。
基准图与待测图的预处理参数不一致
基准图:emphasize (…, 14, 14, 3);循环里:emphasize (…, 16, 16, 5)。
同一个模型要匹配两幅图,预处理必须一致,否则纹理/对比度不一样,Score 会明显下降甚至漏检。
另外 find_shape_model 的 Greediness 也不同(基准 0.3、循环 0.9)—— 循环里贪心到 0.9 会提高漏检风险。
六、任务 4:一维码 + OCR 交叉验证#
检测要求.txt(GBK 编码)写的是:
list_files ('./一维码+ocr', 'files', Files)
read_image (Image, Files[0])
draw_rectangle2 (WindowHandle, Row, Column, Phi, Length1, Length2)
gen_rectangle2 (Rectangle, Row, Column, Phi, Length1, Length2)
reduce_domain (Image, Rectangle, ImageReduced)
create_shape_model (ImageReduced, 'auto', -rad(360), rad(360), 'auto', 'auto', \
'use_polarity', 'auto', 'auto', ModelID)
create_text_model_reader ('auto', 'Universal_0-9_Rej.occ', TextModel)
for Index := 0 to |Files|-2 by 1 * 目录里还有一个 检测要求.txt,所以要 -2
read_image (Image1, Files[Index])
create_bar_code_model ([], [], BarCodeHandle)
find_bar_code (Image, SymbolRegions, BarCodeHandle, 'Code 39', DecodedDataStrings) * ← 应为 Image1
find_shape_model (Image1, ModelID, -rad(360), rad(360), 0.5, 1, 0.5, \
'least_squares', 0, 0.3, Row1, Column1, Angle, Score)
get_shape_model_contours (ModelContours, ModelID, 1)
hom_mat2d_rotate (HomMat2DIdentity, Angle, 0, 0, HomMat2DRotate)
hom_mat2d_translate (HomMat2DRotate, Row1, Column1, HomMat2DTranslate)
affine_trans_contour_xld (ModelContours, ContoursAffineTrans, HomMat2DTranslate)
* 用匹配结果生成一个跟着转的 ROI,再 OCR
gen_rectangle2 (Rectangle1, Row1, Column1, Angle, Length2, Length1)
reduce_domain (Image1, Rectangle1, ImageReduced1)
find_text (ImageReduced1, TextModel, TextResultID)
get_text_result (TextResultID, 'class', ResultValue)
num := ''
for Index1 := 0 to |ResultValue|-1 by 1
num := num + ResultValue[Index1]
endfor
dev_disp_text (num, 'window', Row1+50, Column1, 'black', [], [])
if (num == DecodedDataStrings) ... endif
endfor
亮点:ROI 跟着匹配结果走 —— gen_rectangle2 (Rectangle1, Row1, Column1, Angle, Length2, Length1) 用匹配到的 \((Row1, Column1, Angle)\) 现场造一个 ROI,再 reduce_domain 后 OCR。
这样即使工件转了,OCR 区域也跟着转。这正是 模板匹配 形状匹配 Shape-Based Matching + 读码与 OCR 一维码 二维码 字符识别 的合体。
七、任务 5:QR 码识别#
list_files ('./QR码', 'files', Files)
read_image (Image, Files[0])
for Index := 0 to |Files|-1 by 1
read_image (Image1, Files[Index])
dev_display (Image1)
create_data_code_2d_model ('QR Code', [], [], DataCodeHandle)
find_data_code_2d (Image, SymbolXLDs, DataCodeHandle, [], [], ResultHandles, DecodedDataStrings)
dev_disp_text ('解码结果' + DecodedDataStrings, 'window', 'top', 'left', 'black', [], [])
endfor
八、本轮源码 bug 汇总(最有价值的一节)#
五份练习里的真实 bug
| # | 文件 | 问题 | 后果 | 修法 |
|---|---|---|---|---|
| 1 | 一维码ocr.hdev |
find_bar_code (Image, …) 传的是首图 Image,不是循环变量 Image1 |
循环 6 张图,每张显示的都是第 1 张的条码结果 | 改成 find_bar_code (Image1, …) |
| 2 | QR码识别.hdev |
同上 —— find_data_code_2d (Image, …) 应为 Image1 |
同上,解码结果恒定不变 | 改成 find_data_code_2d (Image1, …) |
| 3 | 齿轮缺陷.hdev |
循环里 dev_display (Image) 显示的是首图 |
显示的是基准图 + 当前图的区域,叠错了 | 改成 dev_display (Image1) |
| 4 | 齿轮缺陷.hdev |
if (Area - Area1 == 0) 用浮点判等 |
真机上几乎必然全 NG | 改容差(见 2.1) |
| 5 | 齿轮缺陷.hdev |
基准/循环 select_shape 面积上限不同(102973 vs 104000) |
两图的筛选口径不同,Area 与 Area1 不可比 |
统一成同一个值 |
| 6 | 传感器长宽.hdev |
基准 emphasize(14,14,3)、循环 emphasize(16,16,5) |
预处理不一致 → 匹配得分下降 / 漏检 | 统一参数 |
| 7 | 传感器长宽.hdev |
gen_region_line (RegionLines2, …) 连写两次赋给同一个变量 |
第一次画的长线被第二次覆盖,只剩宽线 | 用两个变量 |
| 8 | 传感器长宽.hdev |
缺 set_metrology_model_image_size |
与官方示例顺序不符,卡尺可能按默认尺寸生成 | 在 add_* 之前补上 |
| 9 | QR码识别.hdev / 一维码ocr.hdev |
create_*_model 写在循环里且从不 clear |
每轮泄漏一个句柄 | 建模型提到循环外,或在循环末尾 clear_*_model |
1、2、3 是同一个错误:循环里忘了把 Image 换成 Image1
这三份代码都是"先在循环外 read_image (Image, Files[0]) 拿首图,再进循环 read_image (Image1, Files[Index])"的写法。
一旦循环体内某个算子忘了改变量名,就会永远在处理首图 —— 而画面上还在换图,所以现象极具迷惑性:图在变、结果不变。
排查口诀:"结果恒定不变" = 先怀疑循环里用了错变量。
九、工程化 checklist#
写完一份检测程序,按这个清单过一遍:
- 循环里每一个算子用的都是当前图变量(不是首图)
- 判定用容差,不用
==;容差量级小于"最小要检出的缺陷" - 取结果前先判 元组非空(
|结果| > 0) -
create_*_model放在循环外,或在循环末尾clear_*_model - 基准图与待测图的预处理参数完全一致
-
select_shape的上下限在两张图上一致,否则结果不可比 -
draw_*交互算子已换成写死的坐标常量 -
set_metrology_model_image_size写在add_metrology_object_*之前 - 路径改成相对库目录的路径,不用绝对路径
- 面积类阈值随分辨率重新标定(像素面积 ∝ 分辨率²)
十、自检#
Question
- 骰子点数里
threshold (GrayImage, Region, 0, 128)选中的到底是什么?为什么最后只剩"点"? select_shape只有'area'一个特征时,写'or'和'and'有区别吗?- 齿轮缺陷里
if (Area - Area1 == 0)为什么不可靠?正确的写法是什么?容差该怎么定? - "图在变、结果不变"这种现象,最可能的原因是什么?本轮哪三份源码踩了?
- 传感器长宽里 4 条线的
Parameter下标怎么分布?distance_pl (Parameter[2], Parameter[3], Parameter[4..7])算的是哪两个对象的距离? find_shape_model (ImageReduced, ModelID, …)为什么要传ImageReduced而不是整图?- 一维码ocr 里 ROI 是怎么"跟着工件转"的?用到哪几个算子?
- 循环里反复
create_data_code_2d_model却从不clear,会有什么后果?
上一步:读码与 OCR 一维码 二维码 字符识别 · 相关:二值化与连通域分割、模板匹配 形状匹配 Shape-Based Matching、测量模型 2D Metrology · 回到:Halcon 学习地图
该图为