跳转至

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。 正确写法是给容差 + 比值双保险:

Diff := abs (Area - Area1)
Ratio := Diff / Area
if (Diff < 500 and Ratio < 0.01)
    dev_disp_text ('OK', 'window', 'top', 'center', 'green', [], [])
else
    dev_disp_text ('NG', 'window', 'top', 'center', 'red', [], [])
endif
容差的量级要按缺陷大小定:想知道"缺了一块 5×5 的料",容差就得小于 25 像素,否则缺陷被容差吃掉。

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

ResultValueget_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_shapeOperation 只在传多个特征时才有区别('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 万像素;缺料/多料会让面积变化。

两处需要注意:

  1. select_shape上限不一致:基准图用 102973,循环里用 104000。放宽上限能让"稍微大一点"的齿轮也进入候选,但这会让两张图的筛选条件不同AreaArea1 就不再可比 —— 要么统一,要么干脆用 'min' 开口。
  2. 见下一节的 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_modelGreediness 也不同(基准 0.3、循环 0.9)—— 循环里贪心到 0.9 会提高漏检风险

六、任务 4:一维码 + OCR 交叉验证#

检测要求.txt(GBK 编码)写的是:

1.能识别出条码的信息内容
2.读取字符
3.判断是否相等
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 两图的筛选口径不同,AreaArea1 不可比 统一成同一个值
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

  1. 骰子点数里 threshold (GrayImage, Region, 0, 128) 选中的到底是什么?为什么最后只剩"点"?
  2. select_shape 只有 'area' 一个特征时,写 'or''and' 有区别吗?
  3. 齿轮缺陷里 if (Area - Area1 == 0) 为什么不可靠?正确的写法是什么?容差该怎么定?
  4. "图在变、结果不变"这种现象,最可能的原因是什么?本轮哪三份源码踩了?
  5. 传感器长宽里 4 条线的 Parameter 下标怎么分布?distance_pl (Parameter[2], Parameter[3], Parameter[4..7]) 算的是哪两个对象的距离?
  6. find_shape_model (ImageReduced, ModelID, …) 为什么要传 ImageReduced 而不是整图?
  7. 一维码ocr 里 ROI 是怎么"跟着工件转"的?用到哪几个算子?
  8. 循环里反复 create_data_code_2d_model 却从不 clear,会有什么后果?

上一步:读码与 OCR 一维码 二维码 字符识别 · 相关:二值化与连通域分割模板匹配 形状匹配 Shape-Based Matching测量模型 2D Metrology · 回到:Halcon 学习地图